实现 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 { 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);
|
||||
});
|
||||
|
||||
// 发送消息
|
||||
const sendPing = () => {
|
||||
postMessage({
|
||||
bridge.postMessage({
|
||||
command: 'ping',
|
||||
data: { timestamp: Date.now() }
|
||||
});
|
||||
|
@ -60,8 +60,9 @@ function gotoOption(ident: string) {
|
||||
}
|
||||
|
||||
.sidebar-option-item .iconfont {
|
||||
margin-right: 5px;
|
||||
font-size: 20px;
|
||||
margin-top: 2px;
|
||||
margin-right: 7px;
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.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';
|
||||
|
||||
export const connectionMethods = reactive({
|
||||
current: 'stdio',
|
||||
current: 'STDIO',
|
||||
data: [
|
||||
{
|
||||
value: 'stdio',
|
||||
label: 'stdio'
|
||||
value: 'STDIO',
|
||||
label: 'STDIO'
|
||||
},
|
||||
{
|
||||
value: 'sse',
|
||||
label: 'sse'
|
||||
value: 'SSE',
|
||||
label: 'SSE'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
export const connectionCommand = reactive({
|
||||
commandString: ''
|
||||
export const connectionArgs = reactive({
|
||||
commandString: '',
|
||||
urlString: ''
|
||||
});
|
||||
|
||||
export interface EnvItem {
|
||||
@ -39,3 +41,55 @@ export function onconnectionmethodchange() {
|
||||
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>
|
||||
</div>
|
||||
|
||||
<div class="connection-option">
|
||||
<span>{{ t('command') }}</span>
|
||||
<span style="width: 310px;">
|
||||
<el-input v-model="connectionCommand.commandString"></el-input>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<ConnectionArgs></ConnectionArgs>
|
||||
|
||||
|
||||
<div class="connection-option">
|
||||
<span>{{ t('env-var') }}</span>
|
||||
@ -50,7 +47,9 @@
|
||||
</div>
|
||||
|
||||
<div class="connect-action">
|
||||
<el-button type="primary" size="large">
|
||||
<el-button type="primary" size="large"
|
||||
@click="doConnect()"
|
||||
>
|
||||
Connect
|
||||
</el-button>
|
||||
</div>
|
||||
@ -60,12 +59,23 @@
|
||||
<script setup lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
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' });
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const bridge = useMessageBridge();
|
||||
|
||||
bridge.onMessage(message => {
|
||||
if (message.command === 'connect') {
|
||||
console.log('connect result');
|
||||
console.log(message.data);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @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 { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
||||
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
||||
|
||||
@ -109,109 +110,3 @@ export async function connect(options: MCPOptions): Promise<MCPClient> {
|
||||
await client.connect();
|
||||
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