实现 MCP client 的 patch
This commit is contained in:
parent
2e1454281d
commit
bac3e5c253
@ -15,16 +15,16 @@ import { setDefaultCss } from './hook/css';
|
|||||||
import { pinkLog } from './views/setting/util';
|
import { pinkLog } from './views/setting/util';
|
||||||
import { useMessageBridge } from './api/message-bridge';
|
import { useMessageBridge } from './api/message-bridge';
|
||||||
|
|
||||||
const { postMessage, onMessage, isConnected } = useMessageBridge();
|
const bridge = useMessageBridge();
|
||||||
|
|
||||||
// 监听所有消息
|
// 监听所有消息
|
||||||
onMessage((message) => {
|
bridge.onMessage((message) => {
|
||||||
console.log('Received:', message.command, message.data);
|
console.log('Received:', message.command, message.data);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 发送消息
|
// 发送消息
|
||||||
const sendPing = () => {
|
const sendPing = () => {
|
||||||
postMessage({
|
bridge.postMessage({
|
||||||
command: 'ping',
|
command: 'ping',
|
||||||
data: { timestamp: Date.now() }
|
data: { timestamp: Date.now() }
|
||||||
});
|
});
|
||||||
|
@ -60,8 +60,9 @@ function gotoOption(ident: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-option-item .iconfont {
|
.sidebar-option-item .iconfont {
|
||||||
margin-right: 5px;
|
margin-top: 2px;
|
||||||
font-size: 20px;
|
margin-right: 7px;
|
||||||
|
font-size: 17px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-option-item.active {
|
.sidebar-option-item.active {
|
||||||
|
68
app/src/views/connect/connection-args.vue
Normal file
68
app/src/views/connect/connection-args.vue
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
<template>
|
||||||
|
<!-- STDIO 模式下的命令输入 -->
|
||||||
|
<div class="connection-option" v-if="connectionMethods.current === 'STDIO'">
|
||||||
|
<span>{{ t('command') }}</span>
|
||||||
|
<span style="width: 310px;">
|
||||||
|
<el-form :model="connectionArgs" :rules="rules" ref="stdioForm">
|
||||||
|
<el-form-item prop="commandString">
|
||||||
|
<el-input v-model="connectionArgs.commandString"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 其他模式下的URL输入 -->
|
||||||
|
<div class="connection-option" v-else>
|
||||||
|
<span>{{ "URL" }}</span>
|
||||||
|
<span style="width: 310px;">
|
||||||
|
<el-form :model="connectionArgs" :rules="rules" ref="urlForm">
|
||||||
|
<el-form-item prop="urlString">
|
||||||
|
<el-input v-model="connectionArgs.urlString" placeholder="http://"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineComponent, reactive, ref } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
import type { FormInstance, FormRules } from 'element-plus';
|
||||||
|
import { connectionArgs, connectionMethods } from './connection';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
interface ConnectionMethods {
|
||||||
|
current: string
|
||||||
|
}
|
||||||
|
const stdioForm = ref<FormInstance>()
|
||||||
|
const urlForm = ref<FormInstance>()
|
||||||
|
|
||||||
|
|
||||||
|
// 验证规则
|
||||||
|
const rules = reactive<FormRules>({
|
||||||
|
commandString: [
|
||||||
|
{ required: true, message: '命令不能为空', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
urlString: [
|
||||||
|
{ required: true, message: 'URL不能为空', trigger: 'blur' }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
// 验证当前活动表单
|
||||||
|
const validateForm = async () => {
|
||||||
|
try {
|
||||||
|
if (connectionMethods.current === 'STDIO') {
|
||||||
|
await stdioForm.value?.validate()
|
||||||
|
} else {
|
||||||
|
await urlForm.value?.validate()
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('请填写必填字段')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
@ -1,21 +1,23 @@
|
|||||||
|
import { useMessageBridge } from '@/api/message-bridge';
|
||||||
import { reactive } from 'vue';
|
import { reactive } from 'vue';
|
||||||
|
|
||||||
export const connectionMethods = reactive({
|
export const connectionMethods = reactive({
|
||||||
current: 'stdio',
|
current: 'STDIO',
|
||||||
data: [
|
data: [
|
||||||
{
|
{
|
||||||
value: 'stdio',
|
value: 'STDIO',
|
||||||
label: 'stdio'
|
label: 'STDIO'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'sse',
|
value: 'SSE',
|
||||||
label: 'sse'
|
label: 'SSE'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
export const connectionCommand = reactive({
|
export const connectionArgs = reactive({
|
||||||
commandString: ''
|
commandString: '',
|
||||||
|
urlString: ''
|
||||||
});
|
});
|
||||||
|
|
||||||
export interface EnvItem {
|
export interface EnvItem {
|
||||||
@ -39,3 +41,55 @@ export function onconnectionmethodchange() {
|
|||||||
console.log();
|
console.log();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 定义连接类型
|
||||||
|
type ConnectionType = 'STDIO' | 'SSE';
|
||||||
|
|
||||||
|
// 定义命令行参数接口
|
||||||
|
export interface MCPOptions {
|
||||||
|
connectionType: ConnectionType;
|
||||||
|
// STDIO 特定选项
|
||||||
|
command?: string;
|
||||||
|
args?: string[];
|
||||||
|
// SSE 特定选项
|
||||||
|
url?: string;
|
||||||
|
// 通用客户端选项
|
||||||
|
clientName?: string;
|
||||||
|
clientVersion?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export function doConnect() {
|
||||||
|
let connectOption: MCPOptions;
|
||||||
|
|
||||||
|
if (connectionMethods.current === 'STDIO') {
|
||||||
|
const commandComponents = connectionArgs.commandString.split(/\s+/g);
|
||||||
|
const command = commandComponents[0];
|
||||||
|
commandComponents.shift();
|
||||||
|
|
||||||
|
connectOption = {
|
||||||
|
connectionType: 'STDIO',
|
||||||
|
command: command,
|
||||||
|
args: commandComponents,
|
||||||
|
clientName: 'openmcp.connect.stdio.' + command,
|
||||||
|
clientVersion: '0.0.1'
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
const url = connectionArgs.urlString;
|
||||||
|
|
||||||
|
connectOption = {
|
||||||
|
connectionType: 'SSE',
|
||||||
|
url: url,
|
||||||
|
clientName: 'openmcp.connect.sse',
|
||||||
|
clientVersion: '0.0.1'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bridge = useMessageBridge();
|
||||||
|
bridge.postMessage({
|
||||||
|
command: 'connect',
|
||||||
|
data: connectOption
|
||||||
|
});
|
||||||
|
}
|
@ -11,12 +11,9 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="connection-option">
|
|
||||||
<span>{{ t('command') }}</span>
|
<ConnectionArgs></ConnectionArgs>
|
||||||
<span style="width: 310px;">
|
|
||||||
<el-input v-model="connectionCommand.commandString"></el-input>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="connection-option">
|
<div class="connection-option">
|
||||||
<span>{{ t('env-var') }}</span>
|
<span>{{ t('env-var') }}</span>
|
||||||
@ -50,7 +47,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="connect-action">
|
<div class="connect-action">
|
||||||
<el-button type="primary" size="large">
|
<el-button type="primary" size="large"
|
||||||
|
@click="doConnect()"
|
||||||
|
>
|
||||||
Connect
|
Connect
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
@ -60,12 +59,23 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineComponent } from 'vue';
|
import { defineComponent } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { connectionCommand, connectionEnv, connectionMethods, EnvItem, onconnectionmethodchange } from './connection';
|
import { connectionArgs, connectionEnv, connectionMethods, doConnect, EnvItem, onconnectionmethodchange } from './connection';
|
||||||
|
|
||||||
|
import ConnectionArgs from './connection-args.vue';
|
||||||
|
import { useMessageBridge } from '@/api/message-bridge';
|
||||||
|
|
||||||
defineComponent({ name: 'connect' });
|
defineComponent({ name: 'connect' });
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const bridge = useMessageBridge();
|
||||||
|
|
||||||
|
bridge.onMessage(message => {
|
||||||
|
if (message.command === 'connect') {
|
||||||
|
console.log('connect result');
|
||||||
|
console.log(message.data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description 添加环境变量
|
* @description 添加环境变量
|
||||||
|
77
test/patch-mcp-sdk.js
Normal file
77
test/patch-mcp-sdk.js
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
|
||||||
|
/**
|
||||||
|
* source: https://gist.github.com/Laci21/9dd074f3a5a461ab04adb7db678534d3
|
||||||
|
* issue: https://github.com/modelcontextprotocol/typescript-sdk/issues/217
|
||||||
|
*
|
||||||
|
* This script fixes the MCP SDK issue with pkce-challenge ES Module
|
||||||
|
* It replaces the static require with a dynamic import in the auth.js file
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// Path to the file that needs patching
|
||||||
|
const authFilePath = path.resolve(
|
||||||
|
__dirname,
|
||||||
|
'node_modules',
|
||||||
|
'@modelcontextprotocol',
|
||||||
|
'sdk',
|
||||||
|
'dist',
|
||||||
|
'cjs',
|
||||||
|
'client',
|
||||||
|
'auth.js'
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('Checking if MCP SDK patch is needed...');
|
||||||
|
|
||||||
|
// Check if the file exists
|
||||||
|
if (!fs.existsSync(authFilePath)) {
|
||||||
|
console.error(`Error: File not found at ${authFilePath}`);
|
||||||
|
console.log('Make sure you have installed @modelcontextprotocol/sdk package');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the file content
|
||||||
|
const fileContent = fs.readFileSync(authFilePath, 'utf8');
|
||||||
|
|
||||||
|
// Check if the file already contains our patch
|
||||||
|
if (fileContent.includes('loadPkceChallenge')) {
|
||||||
|
console.log('MCP SDK is already patched!');
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the file contains the problematic require
|
||||||
|
if (!fileContent.includes("require(\"pkce-challenge\")")) {
|
||||||
|
console.log('The MCP SDK file does not contain the expected require statement.');
|
||||||
|
console.log('This patch may not be needed or the SDK has been updated.');
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Applying patch to MCP SDK...');
|
||||||
|
|
||||||
|
// The code to replace the problematic require
|
||||||
|
const requireLine = "const pkce_challenge_1 = __importDefault(require(\"pkce-challenge\"));";
|
||||||
|
const replacementCode = `let pkce_challenge_1 = { default: null };
|
||||||
|
async function loadPkceChallenge() {
|
||||||
|
if (!pkce_challenge_1.default) {
|
||||||
|
const mod = await import("pkce-challenge");
|
||||||
|
pkce_challenge_1.default = mod.default;
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
// Replace the require line
|
||||||
|
let patchedContent = fileContent.replace(requireLine, replacementCode);
|
||||||
|
|
||||||
|
// Replace the function call to add the loading step
|
||||||
|
const challengeCall = "const challenge = await (0, pkce_challenge_1.default)();";
|
||||||
|
const replacementCall = "await loadPkceChallenge();\n const challenge = await pkce_challenge_1.default();";
|
||||||
|
patchedContent = patchedContent.replace(challengeCall, replacementCall);
|
||||||
|
|
||||||
|
// Write the patched content back to the file
|
||||||
|
fs.writeFileSync(authFilePath, patchedContent, 'utf8');
|
||||||
|
|
||||||
|
console.log('✅ MCP SDK patched successfully!');
|
||||||
|
console.log('The patch changes:');
|
||||||
|
console.log('1. Replaced static require with dynamic import for pkce-challenge');
|
||||||
|
console.log('2. Added async loading function to handle the import');
|
||||||
|
console.log('\nYou should now be able to run the application without ESM errors.');
|
@ -1,4 +1,5 @@
|
|||||||
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
||||||
|
|
||||||
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
||||||
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
||||||
|
|
||||||
@ -109,109 +110,3 @@ export async function connect(options: MCPOptions): Promise<MCPClient> {
|
|||||||
await client.connect();
|
await client.connect();
|
||||||
return client;
|
return client;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 命令行参数解析
|
|
||||||
function parseCommandLineArgs(): MCPOptions {
|
|
||||||
const args = process.argv.slice(2);
|
|
||||||
const options: MCPOptions = {
|
|
||||||
connectionType: 'STDIO' // 默认值
|
|
||||||
};
|
|
||||||
|
|
||||||
for (let i = 0; i < args.length; i++) {
|
|
||||||
const arg = args[i];
|
|
||||||
switch (arg) {
|
|
||||||
case '--type':
|
|
||||||
case '-t':
|
|
||||||
const type = args[++i];
|
|
||||||
if (type === 'STDIO' || type === 'SSE') {
|
|
||||||
options.connectionType = type;
|
|
||||||
} else {
|
|
||||||
console.warn(`Invalid connection type: ${type}. Using default (STDIO).`);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case '--command':
|
|
||||||
case '-c':
|
|
||||||
options.command = args[++i];
|
|
||||||
break;
|
|
||||||
case '--args':
|
|
||||||
case '-a':
|
|
||||||
options.args = args[++i].split(',');
|
|
||||||
break;
|
|
||||||
case '--url':
|
|
||||||
case '-u':
|
|
||||||
options.url = args[++i];
|
|
||||||
break;
|
|
||||||
case '--name':
|
|
||||||
case '-n':
|
|
||||||
options.clientName = args[++i];
|
|
||||||
break;
|
|
||||||
case '--version':
|
|
||||||
case '-v':
|
|
||||||
options.clientVersion = args[++i];
|
|
||||||
break;
|
|
||||||
case '--help':
|
|
||||||
printHelp();
|
|
||||||
process.exit(0);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
console.warn(`Unknown option: ${arg}`);
|
|
||||||
printHelp();
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return options;
|
|
||||||
}
|
|
||||||
|
|
||||||
function printHelp(): void {
|
|
||||||
console.log(`
|
|
||||||
Usage: node mcpserver.js [options]
|
|
||||||
|
|
||||||
Options:
|
|
||||||
-t, --type <STDIO|SSE> Connection type (default: STDIO)
|
|
||||||
|
|
||||||
STDIO specific options:
|
|
||||||
-c, --command <string> Command to execute (default: node)
|
|
||||||
-a, --args <string> Comma-separated arguments (default: server.js)
|
|
||||||
|
|
||||||
SSE specific options:
|
|
||||||
-u, --url <string> Server URL (required for SSE)
|
|
||||||
|
|
||||||
Client options:
|
|
||||||
-n, --name <string> Client name (default: mcp-client)
|
|
||||||
-v, --version <string> Client version (default: 1.0.0)
|
|
||||||
|
|
||||||
--help Show this help message
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 主入口
|
|
||||||
if (require.main === module) {
|
|
||||||
(async () => {
|
|
||||||
try {
|
|
||||||
const options = parseCommandLineArgs();
|
|
||||||
const client = await connect(options);
|
|
||||||
|
|
||||||
// 示例操作
|
|
||||||
console.log('Listing prompts...');
|
|
||||||
const prompts = await client.listPrompts();
|
|
||||||
console.log('Prompts:', prompts);
|
|
||||||
|
|
||||||
// 处理进程终止信号
|
|
||||||
process.on('SIGINT', async () => {
|
|
||||||
console.log('\nReceived SIGINT. Disconnecting...');
|
|
||||||
await client.disconnect();
|
|
||||||
process.exit(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
process.on('SIGTERM', async () => {
|
|
||||||
console.log('\nReceived SIGTERM. Disconnecting...');
|
|
||||||
await client.disconnect();
|
|
||||||
process.exit(0);
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error:', error);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user