From 4d459464d32c8d5821eb381b11cbc12accc85684 Mon Sep 17 00:00:00 2001 From: Kirigaya <1193466151@qq.com> Date: Tue, 20 May 2025 03:47:15 +0800 Subject: [PATCH] update --- package-lock.json | 4 +- renderer/src/App.vue | 7 +- renderer/src/api/message-bridge.ts | 3 + renderer/src/components/sidebar/connected.vue | 8 +- .../src/views/connect/connection-args.vue | 8 +- .../views/connect/connection-environment.vue | 20 ++-- renderer/src/views/connect/connection-log.vue | 6 +- .../src/views/connect/connection-method.vue | 6 +- .../src/views/connect/connection-panel.vue | 13 +-- renderer/src/views/connect/core.ts | 22 +++-- renderer/src/views/connect/index.vue | 98 +++++++++++++++---- renderer/src/views/connect/type.ts | 10 +- renderer/src/views/setting/index.vue | 18 ++-- service/src/main.ts | 27 +++-- service/src/mcp/connect.service.ts | 79 ++++++++++----- service/src/server.ts | 33 +++---- service/src/setting/setting.controller.ts | 2 - 17 files changed, 223 insertions(+), 141 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4d55f5f..d55a957 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "openmcp", - "version": "0.0.9", + "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "openmcp", - "version": "0.0.9", + "version": "0.1.0", "workspaces": [ "service", "renderer", diff --git a/renderer/src/App.vue b/renderer/src/App.vue index 2c37d3a..54dfa83 100644 --- a/renderer/src/App.vue +++ b/renderer/src/App.vue @@ -55,7 +55,12 @@ onMounted(async () => { // } // 进行桥接 - await bridge.awaitForWebsocket(); + console.log('enter'); + + await bridge.awaitForWebsocket(); + + console.log('enter2'); + // 根据是否需要密码进行后续的选择 if (!privilegeStatus.allow) { diff --git a/renderer/src/api/message-bridge.ts b/renderer/src/api/message-bridge.ts index f652321..739d542 100644 --- a/renderer/src/api/message-bridge.ts +++ b/renderer/src/api/message-bridge.ts @@ -77,6 +77,8 @@ export class MessageBridge { throw new Error('setupSignature must be a string'); } + console.log(wsUrl); + this.ws = new WebSocket(wsUrl); const ws = this.ws; @@ -117,6 +119,7 @@ export class MessageBridge { } public async awaitForWebsocket() { + if (this.isConnected) { return await this.isConnected; } diff --git a/renderer/src/components/sidebar/connected.vue b/renderer/src/components/sidebar/connected.vue index 0cdfea8..afebeef 100644 --- a/renderer/src/components/sidebar/connected.vue +++ b/renderer/src/components/sidebar/connected.vue @@ -37,14 +37,16 @@ import { mcpClientAdapter } from '@/views/connect/core'; defineComponent({ name: 'connected' }); const { t } = useI18n(); -const client = mcpClientAdapter.masterNode; +const client = computed(() => mcpClientAdapter.masterNode); + +console.log(client); const fullDisplayServerName = computed(() => { - return client.connectionResult.name + '/' + client.connectionResult.version; + return client.value.connectionResult.name + '/' + client.value.connectionResult.version; }); const displayServerName = computed(() => { - const name = client.connectionResult.name; + const name = client.value.connectionResult.name; if (name.length <= 3) return name; // 处理中文混合名称 diff --git a/renderer/src/views/connect/connection-args.vue b/renderer/src/views/connect/connection-args.vue index e264b18..dbb5b2d 100644 --- a/renderer/src/views/connect/connection-args.vue +++ b/renderer/src/views/connect/connection-args.vue @@ -1,6 +1,6 @@ \ No newline at end of file diff --git a/renderer/src/views/connect/type.ts b/renderer/src/views/connect/type.ts index eeb3245..c183b85 100644 --- a/renderer/src/views/connect/type.ts +++ b/renderer/src/views/connect/type.ts @@ -6,14 +6,6 @@ export interface ConnectionTypeOptionItem { label: string; } -export interface IConnectionArgs { - type: ConnectionType; - commandString?: string; - cwd?: string; - url?: string; - oauth?: string; -} - export interface IConnectionResult { info?: string; @@ -64,7 +56,7 @@ export interface IConnectionEnvironment { } export interface IConnectionArgs { - type: ConnectionType; + connectionType: ConnectionType; commandString?: string; cwd?: string; url?: string; diff --git a/renderer/src/views/setting/index.vue b/renderer/src/views/setting/index.vue index 9b83a6b..6aca28b 100644 --- a/renderer/src/views/setting/index.vue +++ b/renderer/src/views/setting/index.vue @@ -2,17 +2,13 @@
- - + +
diff --git a/service/src/main.ts b/service/src/main.ts index f5f67b8..d4e4de1 100644 --- a/service/src/main.ts +++ b/service/src/main.ts @@ -27,25 +27,11 @@ const logger = pino({ export type MessageHandler = (message: VSCodeMessage) => void; -interface IStdioLaunchSignature { - type: 'STDIO'; - commandString: string; - cwd: string; -} - -interface ISSELaunchSignature { - type:'SSE'; - url: string; - oauth: string; -} - -export type ILaunchSigature = IStdioLaunchSignature | ISSELaunchSignature; - function refreshConnectionOption(envPath: string) { const serverPath = path.join(__dirname, '..', '..', 'servers'); const defaultOption = { - type:'STDIO', + connectionType: 'STDIO', commandString: 'mcp run main.py', cwd: serverPath }; @@ -73,6 +59,17 @@ function acquireConnectionOption() { return refreshConnectionOption(envPath); } + // 按照前端的规范,整理成 commandString 样式 + option.data = option.data.map((item: any) => { + if (item.connectionType === 'STDIO') { + item.commandString = [item.command, ...item.args]?.join(' '); + } else { + item.url = item.url; + } + + return item; + }); + return option; } catch (error) { diff --git a/service/src/mcp/connect.service.ts b/service/src/mcp/connect.service.ts index fb22a3a..c560436 100644 --- a/service/src/mcp/connect.service.ts +++ b/service/src/mcp/connect.service.ts @@ -1,4 +1,4 @@ -import { execSync, spawnSync } from 'node:child_process'; +import { exec, execSync, spawnSync } from 'node:child_process'; import { RequestClientType } from '../common'; import { connect } from './client.service'; import { RestfulResponse } from '../common/index.dto'; @@ -6,6 +6,7 @@ import { McpOptions } from './client.dto'; import { randomUUID } from 'node:crypto'; import path from 'node:path'; import fs from 'node:fs'; +import * as os from 'os'; export const clientMap: Map = new Map(); export function getClient(clientId?: string): RequestClientType | undefined { @@ -17,13 +18,22 @@ export function tryGetRunCommandError(command: string, args: string[] = [], cwd? console.log('current command', command); console.log('current args', args); - const commandString = [command, ...args].join(' '); + const commandString = command + ' ' + args.join(' '); - const result = execSync(commandString, { - cwd: cwd || process.cwd() - }).toString('utf-8'); + const result = spawnSync(commandString, { + cwd: cwd || process.cwd(), + STDIO: 'pipe', + encoding: 'utf-8' + }); + + if (result.error) { + return result.error.message; + } + if (result.status !== 0) { + return result.stderr || `Command failed with code ${result.status}`; + } + return null; - return result; } catch (error) { return error instanceof Error ? error.message : String(error); } @@ -48,8 +58,15 @@ function getCommandFileExt(option: McpOptions) { return undefined; } +function collectAllOutputExec(command: string, cwd: string) { + return new Promise((resolve, reject) => { + exec(command, { cwd }, (error, stdout, stderr) => { + resolve(error + stdout + stderr); + }); + }); +} -function preprocessCommand(option: McpOptions): [McpOptions, string] { +async function preprocessCommand(option: McpOptions): Promise<[McpOptions, string]> { // 对于特殊表示的路径,进行特殊的支持 if (option.args) { option.args = option.args.map(arg => { @@ -83,49 +100,56 @@ function preprocessCommand(option: McpOptions): [McpOptions, string] { switch (ext) { case '.py': - info = initUv(cwd); + info = await initUv(option, cwd); break; case '.js': case '.ts': - info = initNpm(cwd); + info = await initNpm(option, cwd); break; default: break; - } + } - - return [option, info]; + return [option, '']; } -function initUv(cwd: string) { +async function initUv(option: McpOptions, cwd: string) { let projectDir = cwd; while (projectDir!== path.dirname(projectDir)) { - if (fs.readFileSync(projectDir).includes('pyproject.toml')) { + if (fs.readdirSync(projectDir).includes('pyproject.toml')) { break; } projectDir = path.dirname(projectDir); - } - - console.log(projectDir); - + } const venv = path.join(projectDir, '.venv'); - const mcpCli = path.join(venv, 'bin', 'mcp'); + + // judge by OS + const mcpCli = os.platform() === 'win32' ? + path.join(venv, 'Scripts','mcp.exe') : + path.join(venv, 'bin', 'mcp'); + + if (option.command === 'mcp') { + option.command = mcpCli; + option.cwd = projectDir; + } + if (fs.existsSync(mcpCli)) { return ''; } let info = ''; - info += execSync('uv sync', { cwd: projectDir }).toString('utf-8') + '\n'; - info += execSync('uv add mcp "mcp[cli]"', { cwd: projectDir }).toString('utf-8') + '\n'; - + + info += await collectAllOutputExec('uv sync', projectDir) + '\n'; + info += await collectAllOutputExec('uv add mcp "mcp[cli]"', projectDir) + '\n'; + return info; } -function initNpm(cwd: string) { +async function initNpm(option: McpOptions, cwd: string) { let projectDir = cwd; while (projectDir !== path.dirname(projectDir)) { @@ -148,9 +172,10 @@ export async function connectService( option: McpOptions ): Promise { try { - console.log('ready to connect', option); - - const info = preprocessCommand(option); + const { env, ...others } = option; + console.log('ready to connect', others); + + const [_, info] = await preprocessCommand(option); const client = await connect(option); const uuid = randomUUID(); @@ -172,6 +197,8 @@ export async function connectService( return connectResult; } catch (error) { + console.log(error); + // TODO: 这边获取到的 error 不够精致,如何才能获取到更加精准的错误 // 比如 error: Failed to spawn: `server.py` // Caused by: No such file or directory (os error 2) diff --git a/service/src/server.ts b/service/src/server.ts index a2ab3eb..cbada75 100644 --- a/service/src/server.ts +++ b/service/src/server.ts @@ -28,32 +28,18 @@ const logger = pino({ export type MessageHandler = (message: VSCodeMessage) => void; -interface IStdioLaunchSignature { - type: 'STDIO'; - commandString: string; - cwd: string; -} - -interface ISSELaunchSignature { - type: 'SSE'; - url: string; - oauth: string; -} - -export type ILaunchSigature = IStdioLaunchSignature | ISSELaunchSignature; - function refreshConnectionOption(envPath: string) { const serverPath = path.join(__dirname, '..', '..', 'servers'); const defaultOption = { - type:'STDIO', + connectionType: 'STDIO', commandString: 'mcp run main.py', cwd: serverPath }; - fs.writeFileSync(envPath, JSON.stringify(defaultOption, null, 4)); + fs.writeFileSync(envPath, JSON.stringify(defaultOption, null, 4)); - return { data: [ defaultOption ] }; + return { data: [defaultOption] }; } function acquireConnectionOption() { @@ -74,6 +60,17 @@ function acquireConnectionOption() { return refreshConnectionOption(envPath); } + // 按照前端的规范,整理成 commandString 样式 + option.data = option.data.map((item: any) => { + if (item.connectionType === 'STDIO') { + item.commandString = [item.command, ...item.args]?.join(' '); + } else { + item.url = item.url; + } + + return item; + }); + return option; } catch (error) { @@ -112,7 +109,7 @@ const wss = new WebSocketServer( port: 8282, verifyClient: (info, callback) => { console.log(info.req.url); - const ok = verifyToken(info.req.url || ''); + const ok = verifyToken(info.req.url || ''); if (!ok) { callback(false, 401, 'Unauthorized: Invalid token'); diff --git a/service/src/setting/setting.controller.ts b/service/src/setting/setting.controller.ts index 3a0a6b2..0b6ffa1 100644 --- a/service/src/setting/setting.controller.ts +++ b/service/src/setting/setting.controller.ts @@ -8,7 +8,6 @@ export class SettingController { @Controller('setting/save') async saveSetting(data: RequestData, webview: PostMessageble) { - const client = getClient(data.clientId); saveSetting(data); console.log('Settings saved successfully'); @@ -20,7 +19,6 @@ export class SettingController { @Controller('setting/load') async loadSetting(data: RequestData, webview: PostMessageble) { - const client = getClient(data.clientId); const config = loadSetting(); return { code: 200,