实现 MCP client 的 patch

This commit is contained in:
锦恢 2025-03-29 13:19:47 +08:00
parent 2e1454281d
commit bac3e5c253
7 changed files with 231 additions and 126 deletions

View File

@ -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() }
}); });

View File

@ -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 {

View 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>

View File

@ -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 {
@ -38,4 +40,56 @@ export const connectionEnv = reactive<IConnectionEnv>({
export function onconnectionmethodchange() { 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
});
} }

View File

@ -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
View 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.');

View File

@ -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);
}
})();
}