# 接入替换文档:qiming-mcp-stdio-proxy → agent-electron-client 将 `crates/agent-electron-client` 中旧的 `mcp-stdio-proxy`(bin: `mcp-proxy`,Rust 二进制)替换为新的 `qiming-mcp-stdio-proxy`(纯 TypeScript/Node.js)。 --- ## 一、旧 mcp-proxy 完整能力梳理 通过 `mcp-proxy --help` 确认,旧 Rust 二进制(v0.1.54)支持以下子命令: | 子命令 | 方向 | 用途 | |--------|------|------| | `proxy` | **stdio → Streamable HTTP/SSE** | 将本地 stdio MCP server 暴露为 HTTP 服务 | | `convert` | **Streamable HTTP/SSE → stdio** | 将远程 HTTP MCP 服务转换为本地 stdio | | `detect` | — | 自动检测远程服务协议类型 | | `check` | — | 检查远程服务可用性 | | `health` | — | 健康检查(MCP 握手级别) | ### `proxy` 子命令详情 ```bash mcp-proxy proxy --port --host --config '' [--protocol sse|stream] # 默认 stream (Streamable HTTP) [--log-dir ] [--diagnostic] ``` - 启动 HTTP 服务器,聚合 `--config` 中定义的 stdio MCP servers - 对外暴露 Streamable HTTP(默认)或 SSE 端点 - 支持 `--allow-tools` / `--deny-tools` 工具过滤 ### `convert` 子命令详情 ```bash mcp-proxy convert [--config ''] # 也支持 config 模式 [--auth "Bearer token"] [--protocol sse|stream] [--ping-interval ] ``` - 将远程 HTTP/SSE MCP 服务转为本地 stdio 接口 - Agent 引擎(claude-code / qimingcode)通过 stdio 消费 --- ## 二、Tauri 客户端实际架构(参考基准) Tauri 客户端使用的是 **stdio → Streamable HTTP → stdio** 双跳架构: ``` ┌─ Tauri Main Process ─────────────────────────────────────────────┐ │ │ │ 1. 启动 proxy 进程: │ │ mcp-proxy proxy --port 18099 --host 127.0.0.1 │ │ --config '{"mcpServers":{...}}' │ │ --protocol stream │ │ │ │ 2. TCP 健康检查: │ │ wait_for_mcp_proxy_ready(18099, "127.0.0.1", timeout=15s) │ │ │ │ 3. 注入 Agent MCP 配置: │ │ { "mcp-proxy": { │ │ "command": "mcp-proxy", │ │ "args": ["convert", "http://127.0.0.1:18099"] │ │ }} │ │ │ └───────────────────────────────────────────────────────────────────┘ 数据流: Child MCP Servers (stdio × N) ↑ StdioClientTransport │ mcp-proxy proxy (Rust, 聚合 + Streamable HTTP 服务器) │ http://127.0.0.1:18099 ↓ mcp-proxy convert http://127.0.0.1:18099 (Rust, HTTP → stdio) │ stdio ↓ Agent Engine (claude-code / qimingcode) ``` **Tauri 这样做的原因**: - Rust 原生 `Command::new()` + `CREATE_NO_WINDOW` 避免 Windows 弹窗 - 进程组管理(Unix: setsid, Windows: JobObject) - TCP 健康检查确认服务就绪 **存在的问题**: - 需要端口管理(默认 18099,冲突时需要 kill 旧进程) - 两层 mcp-proxy 进程(proxy + convert) - Electron 客户端复用此架构时,Rust 二进制 + `.cmd` wrapper 导致 Windows 弹窗 --- ## 三、Electron 客户端当前架构(有问题) Electron 当前**试图复制** Tauri 架构,但有几个问题: ``` Electron Main Process │ spawnJsFile() → run-mcp-proxy.js → Rust binary │ mcp-proxy proxy --port 8080 --host 127.0.0.1 --config '...' ▼ mcp-proxy proxy (Rust binary, HTTP server) │ http://127.0.0.1:8080 ▼ Agent Engine │ mcpServers: { "mcp-proxy": { command: "mcp-proxy", args: ["convert", "http://..."] } } ▼ mcp-proxy convert (Rust binary, HTTP → stdio) ``` **问题清单**: 1. Windows 上 Rust 二进制 `.cmd` wrapper 弹出控制台窗口 2. `spawnJsFile()` spawn 的是 `run-mcp-proxy.js`(Node.js wrapper),它下载并执行 Rust binary 3. 端口管理复杂(分配、占用检测、kill 占用进程) 4. 单进程限制(Electron 代码中强制过滤为单服务) 5. 两层进程开销 --- ## 四、新架构设计 ### 方案:纯 stdio 直通(消除 HTTP 中间层) ``` Agent Engine (claude-code / qimingcode) │ spawns as MCP server (stdio) ▼ qiming-mcp-stdio-proxy --config '{"mcpServers":{...}}' │ StdioServerTransport (上游) + StdioClientTransport × N (下游) ▼ Child MCP Servers (stdio × N) ``` **核心变化**: - **消除 HTTP 中间层**:不再需要 `proxy` → HTTP → `convert` 的双跳 - **Agent 直接 spawn proxy**:proxy 是 Agent 引擎的一个 stdio MCP server - **Electron 不管理 proxy 进程**:生命周期随 Agent 引擎 - **纯 Node.js**:无 Rust 二进制,无 `.cmd` wrapper,Windows 无弹窗 ### 架构对比 | | Tauri(旧) | Electron 旧 | Electron 新 | |---|---|---|---| | **聚合层** | `mcp-proxy proxy` (Rust, HTTP) | 同左 | `qiming-mcp-stdio-proxy` (Node.js, stdio) | | **桥接层** | `mcp-proxy convert` (Rust, HTTP→stdio) | 同左 | **无**(直接 stdio) | | **进程数** | 2 (proxy + convert) | 2 | 1 | | **通信协议** | stdio → HTTP → stdio | 同左 | stdio → stdio | | **端口** | 需要 (18099) | 需要 (8080) | **无** | | **Windows 弹窗** | 无(Rust CREATE_NO_WINDOW) | **有**(.cmd wrapper) | **无**(Node.js) | | **多服务** | 支持 | 强制单服务 | 支持 N 个 | | **生命周期** | Tauri 管理 | Electron 管理 | Agent 引擎管理 | ### 为什么可以省掉 HTTP 层 旧架构需要 HTTP 中间层是因为: - Tauri 的 Rust 进程管理需要一个长驻 HTTP 服务作为控制面 - Agent 引擎通过 `mcp-proxy convert ` 以 stdio 方式接入 新架构中: - `qiming-mcp-stdio-proxy` 本身就是一个 stdio MCP server - Agent 引擎直接 spawn 它,无需中转 - 配置通过 `--config` 参数传入,无需运行时控制面 --- ## 五、配置格式兼容性 新旧 proxy 使用**完全相同**的 `mcpServers` JSON 配置格式: ```typescript interface McpServersConfig { mcpServers: Record; }>; } ``` - SQLite 中持久化的 `mcp_proxy_config` 键值可直接复用 - `resolveServersConfig()` 的 uvx 解析和 env 注入逻辑不变 --- ## 六、需要修改的文件清单 | # | 文件 | 改动量 | 说明 | |---|------|--------|------| | 1 | `src/main/services/packages/mcp.ts` | **大改** | 核心:移除 HTTP 进程管理,改为配置提供者 | | 2 | `src/main/services/system/dependencies.ts` | 小改 | 更新包名、bin 名、最低版本 | | 3 | `src/main/ipc/mcpHandlers.ts` | 中改 | 简化 start/stop/restart 为 no-op | | 4 | `src/main/ipc/processHandlers.ts` | 中改 | restartAll/stopAll 移除 MCP 进程管理 | | 5 | `src/main/main.ts` | 小改 | cleanup 中移除 mcpProxyManager.cleanup() | | 6 | `src/renderer/App.tsx` | 中改 | 移除 mcpProxy 进程启动/轮询 | | 7 | `src/renderer/components/ClientPage.tsx` | 中改 | 移除 mcpProxy 启停逻辑 | | 8 | `src/renderer/components/MCPSettings.tsx` | 小改 | 移除端口配置 UI | | 9 | `src/main/services/engines/unifiedAgent.ts` | 无改 | syncMcpConfigToProxyAndReload 已简化 | | 10 | `src/main/ipc/agentHandlers.ts` | 无改 | getAgentMcpConfig() 返回值已变 | --- ## 七、逐文件改动详解 ### 7.1 `src/main/services/packages/mcp.ts` — 核心重构 旧文件约 692 行,重构后预计约 300 行。 #### 7.1.1 删除的代码(约 400 行) | 删除项 | 原因 | |--------|------| | `import { spawn, ChildProcess, exec }` | 不再 spawn 进程 | | `import * as net` | 不再管理端口 | | `import { DEFAULT_MCP_PROXY_PORT, DEFAULT_MCP_PROXY_HOST }` | 不再使用端口/主机 | | `isPortInUse()` 函数 | 不再管理端口 | | `killProcessOnPort()` 函数 | 不再管理端口 | | `McpProxyManager.process` / `.port` / `.host` / `.startPromise` 字段 | 无进程实例 | | `McpProxyManager.start()` 方法体(进程 spawn 逻辑) | 整体重写为验证 | | `McpProxyManager.stop()` / `.restart()` 方法体 | 简化为 no-op | | `McpProxyManager.isProcessRunning()` | 无进程 | | `McpProxyManager.getPort()` / `.setPort()` | 不再使用端口 | | `McpProxyManager.cleanup()` 方法体 | 简化为 no-op | | `McpProxyStatus.pid` / `.port` / `.host` 字段 | 不再使用 | | `McpProxyStartConfig` 接口 | 不再使用 | | `extractRealServersFromMcpServers()` 中解析 `mcp-proxy convert` 桥接项 | 无桥接模式 | #### 7.1.2 保留的代码(约 200 行) | 保留项 | 说明 | |--------|------| | `getUvBinDir()` / `resolveUvCommand()` | 仍需为 child server 解析 uvx | | `resolveServersConfig()` | 仍需注入 env + 解析 uvx | | `DEFAULT_MCP_PROXY_CONFIG` | 默认配置不变 | | `McpServerEntry` / `McpServersConfig` 接口 | 配置格式不变 | | `McpProxyManager.config` + get/set/add/remove | 配置管理不变 | | `syncMcpConfigToProxyAndReload()` | 保留但简化(不 restart) | #### 7.1.3 `McpProxyManager` 类重写 ```typescript class McpProxyManager { private config: McpServersConfig = JSON.parse(JSON.stringify(DEFAULT_MCP_PROXY_CONFIG)); /** * 获取 qiming-mcp-stdio-proxy 脚本路径 */ private getProxyScriptPath(): string | null { const dirs = getAppPaths(); const packageDir = path.join(dirs.nodeModules, 'qiming-mcp-stdio-proxy'); if (!fs.existsSync(packageDir)) return null; return resolveNpmPackageEntry(packageDir, 'qiming-mcp-stdio-proxy'); } /** * start() → 仅验证 binary 可用性(不再启动进程) */ async start(): Promise<{ success: boolean; error?: string }> { if (!isInstalledLocally('qiming-mcp-stdio-proxy')) { return { success: false, error: 'qiming-mcp-stdio-proxy 未安装' }; } const scriptPath = this.getProxyScriptPath(); if (!scriptPath) { return { success: false, error: 'qiming-mcp-stdio-proxy 入口文件未找到' }; } log.info('[McpProxy] qiming-mcp-stdio-proxy 就绪:', scriptPath); return { success: true }; } async stop(): Promise<{ success: boolean }> { return { success: true }; } async restart(): Promise<{ success: boolean; error?: string }> { return this.start(); } getStatus(): McpProxyStatus { const serverNames = Object.keys(this.config.mcpServers || {}); return { running: !!this.getProxyScriptPath(), serverCount: serverNames.length, serverNames, }; } /** * getAgentMcpConfig() — 核心变更 * * 旧: 返回 { "mcp-proxy": { command: "mcp-proxy", args: ["convert", "http://127.0.0.1:8080"] } } * 新: 返回 { "mcp-proxy": { command: "node", args: ["proxy.js", "--config", "..."] } } * * Agent 引擎直接 spawn qiming-mcp-stdio-proxy 作为 stdio MCP server, * 不再经过 HTTP 中间层。 */ getAgentMcpConfig(): Record; }> | null { const servers = this.config.mcpServers; if (!servers || Object.keys(servers).length === 0) return null; const scriptPath = this.getProxyScriptPath(); if (!scriptPath) { // fallback: 直接返回解析后的各 server stdio 配置(无聚合) return resolveServersConfig(servers); } // 解析配置:uvx → 应用内路径,注入 env const resolvedConfig: McpServersConfig = { mcpServers: resolveServersConfig(servers), }; const configJson = JSON.stringify(resolvedConfig); // Windows: 用 Electron 的 Node.js 执行(避免 .cmd 弹窗) // macOS/Linux: 用系统 node if (isWindows()) { return { 'mcp-proxy': { command: process.execPath, args: [scriptPath, '--config', configJson], env: { ELECTRON_RUN_AS_NODE: '1' }, }, }; } return { 'mcp-proxy': { command: 'node', args: [scriptPath, '--config', configJson], }, }; } cleanup(): void { /* no-op */ } // getConfig / setConfig / addServer / removeServer — 保持不变 } ``` #### 7.1.4 `McpProxyStatus` 接口简化 ```typescript // 旧 export interface McpProxyStatus { running: boolean; pid?: number; // ← 删除 port?: number; // ← 删除 host?: string; // ← 删除 serverCount?: number; serverNames?: string[]; } // 新 export interface McpProxyStatus { running: boolean; // 语义变化:"binary 可用" 而非 "进程在运行" serverCount?: number; serverNames?: string[]; } ``` #### 7.1.5 `syncMcpConfigToProxyAndReload()` 简化 ```typescript export async function syncMcpConfigToProxyAndReload( mcpServers: Record }>, ): Promise { if (!mcpServers || Object.keys(mcpServers).length === 0) return; // 提取真实服务(过滤旧桥接项 command==='mcp-proxy') const realOnly: Record }> = {}; for (const [name, entry] of Object.entries(mcpServers)) { if (!entry || entry.command === 'mcp-proxy') continue; realOnly[name] = { command: entry.command, args: Array.isArray(entry.args) ? entry.args : [], env: entry.env }; } if (Object.keys(realOnly).length === 0) return; log.info('[McpProxy] 同步 MCP 配置:', Object.keys(realOnly).join(', ')); mcpProxyManager.setConfig({ mcpServers: realOnly }); // 持久化到 SQLite try { const { getDb } = await import('../../db'); const db = getDb(); db?.prepare('INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)') .run('mcp_proxy_config', JSON.stringify({ mcpServers: realOnly })); } catch (e) { log.warn('[McpProxy] 持久化 MCP 配置失败:', e); } // 不再需要 restart — 无后台进程 // Agent 下次 init 时 getAgentMcpConfig() 会使用新配置 } ``` --- ### 7.2 `src/main/services/system/dependencies.ts` **改动 1**:`SETUP_REQUIRED_DEPENDENCIES` 中更新 MCP 条目 ```typescript // 旧 { name: "mcp-stdio-proxy", displayName: "MCP 服务", type: "npm-local", description: "MCP 协议转换工具(应用内安装)", required: true, minVersion: "0.1.48", binName: "mcp-proxy", }, // 新 { name: "qiming-mcp-stdio-proxy", displayName: "MCP 服务", type: "npm-local", description: "MCP 协议聚合代理(应用内安装)", required: true, minVersion: "1.0.0", binName: "qiming-mcp-stdio-proxy", }, ``` **改动 2**:`checkAllDependencies()` 中更新 case ```diff - case 'mcp-stdio-proxy': + case 'qiming-mcp-stdio-proxy': ``` --- ### 7.3 `src/main/ipc/mcpHandlers.ts` - `mcp:start` → 移除 options 参数(port/host/configJson),调用 `mcpProxyManager.start()`(仅验证) - `mcp:stop` → 调用 `mcpProxyManager.stop()`(no-op) - `mcp:restart` → 调用 `mcpProxyManager.restart()`(仅验证) - `mcp:getPort` / `mcp:setPort` → 保留为 no-op 向后兼容 - `mcp:getConfig` / `mcp:setConfig` → 不变 --- ### 7.4 `src/main/ipc/processHandlers.ts` `services:restartAll`: - 移除 `mcpProxyManager.stop()` 调用 - `mcpProxyManager.start()` 保留但语义变为"验证可用性" `services:stopAll`: - MCP Proxy 部分改为 `results.mcpProxy = { success: true }` (no-op) --- ### 7.5 `src/main/main.ts` `cleanupAllProcesses()` 中移除或注释 `mcpProxyManager.cleanup()`(已 no-op)。 --- ### 7.6 `src/renderer/App.tsx` + `ClientPage.tsx` `mcpProxy` 在 serviceKeys/startOrder 中**保留**,因为 `mcp.start()` 已变为轻量验证。UI 展示的 `running` 状态语义从"进程运行中"变为"binary 可用"。 可选移除 `pid` 字段的展示。 --- ### 7.7 `src/renderer/components/MCPSettings.tsx` - 移除端口配置 UI(不再使用端口) - 启动/停止按钮可保留(已为 no-op)或改为"检测可用性" - MCP server 列表编辑功能保留不变 --- ## 八、关键行为变更对照表 | 行为 | 旧(mcp-proxy Rust) | 新(qiming-mcp-stdio-proxy TS) | |------|----------------------|-------------------------------| | **binary 类型** | Rust 编译二进制 | Node.js 脚本 | | **通信链路** | stdio → HTTP (Streamable) → stdio | stdio → stdio | | **进程数** | 2 (proxy + convert) | 1 | | **端口** | 需要(默认 8080/18099) | **无** | | **进程管理** | Electron spawn + 监控 + cleanup | Agent 引擎自动管理 | | **多服务聚合** | 支持(但 Electron 强制单服务) | 支持 N 个 | | **Agent 注入** | `mcp-proxy convert http://...` | `node proxy.js --config '...'` | | **Windows 弹窗** | 有(.cmd wrapper) | **无**(Node.js + ELECTRON_RUN_AS_NODE) | | **配置热更新** | restart proxy HTTP 进程 | 更新配置,Agent 下次 init 使用 | | **健康检查** | TCP 连接测试 | 不需要(无 HTTP 服务) | | **远程 MCP 服务** | 支持(convert URL → stdio) | 不支持(仅聚合本地 stdio servers) | ### 注意:远程 MCP 服务能力 旧 `mcp-proxy` 的 `convert` 子命令支持将远程 SSE/Streamable HTTP MCP 服务转为 stdio。新的 `qiming-mcp-stdio-proxy` 仅支持聚合本地 stdio servers。 如果需要访问远程 MCP 服务,有两个选项: 1. **保留旧 `mcp-proxy` 作为 convert 工具**:远程服务配置为 `{ command: "mcp-proxy", args: ["convert", "https://..."] }`,作为 `qiming-mcp-stdio-proxy` 的一个 child server 2. **后续为 qiming-mcp-stdio-proxy 添加 HTTP client transport** --- ## 九、实施步骤 ### Phase 1: 发布 npm 包 ```bash cd crates/qiming-mcp-stdio-proxy npm run build npm publish ``` ### Phase 2: 修改 Electron 客户端 按以下顺序修改(每步可独立提交): 1. `dependencies.ts` — 更新包名/bin 名/版本 2. `mcp.ts` — 核心重构 3. `mcpHandlers.ts` — 简化 IPC 4. `processHandlers.ts` — 移除 MCP 进程管理 5. `main.ts` — 移除 cleanup 6. `App.tsx` + `ClientPage.tsx` — UI 适配 7. `MCPSettings.tsx` — 移除端口配置 ### Phase 3: 测试验证 - [ ] 初始化向导自动安装 `qiming-mcp-stdio-proxy` - [ ] `getAgentMcpConfig()` 返回 `{ command: "node", args: ["proxy.js", "--config", "..."] }` - [ ] Agent 通过 proxy 聚合多个 MCP server 的 tools - [ ] Windows 上无控制台弹窗 - [ ] macOS/Linux 上功能正常 - [ ] MCP 设置页可编辑 server 列表 - [ ] 配置修改后 Agent 重新 init 生效 - [ ] 远程 MCP 服务可通过 `mcp-proxy convert` 作为 child server 接入 --- ## 十、回滚方案 若需回滚到旧 `mcp-proxy`: 1. `dependencies.ts` 改回 `name: "mcp-stdio-proxy"`, `binName: "mcp-proxy"` 2. `mcp.ts` 恢复进程管理逻辑 3. 重新安装 `npm install mcp-stdio-proxy` 配置格式完全兼容,SQLite 数据无需迁移。 --- ## 十一、共享类型更新 如果 `@shared/types/electron.ts` 导出了 `McpProxyStatus`,需同步移除 `pid` / `port` / `host` 字段: ```typescript export interface McpProxyStatus { running: boolean; serverCount?: number; serverNames?: string[]; } ``` 同时删除 `McpProxyStartConfig` 接口。