# 持久化 MCP Server Bridge 架构设计 ## 背景与问题 ### 原有方案 `qiming-mcp-stdio-proxy` 作为 MCP 聚合代理,由 ACP 引擎在每个 session 启动时 spawn,聚合所有用户配置的 MCP server 到一个 stdio endpoint。 ``` ACP Session └── qiming-mcp-stdio-proxy (stdio) ├── chrome-devtools-mcp (子进程) ├── filesystem-mcp (子进程) └── ... ``` **核心问题**:proxy 进程的生命周期与 ACP session 绑定 — session 结束 → proxy 进程退出 → 所有 MCP server 子进程被 kill。 对于 chrome-devtools-mcp 这类有状态(stateful)的 MCP server: | 问题 | 说明 | |------|------| | 状态丢失 | 每次 session 启动都 spawn 新 chrome-devtools-mcp → 启动新 Chrome → 之前的浏览器状态(页面、登录态、DOM 修改)全部丢失 | | `--isolated` 模式 | 避免 Chrome profile 冲突,但每次都是全新浏览器,无法跨 session 连续操作 | | 非 `--isolated` 模式 | Chrome profile 锁冲突:"browser already running",因为上一个 Chrome 可能还没完全退出 | ### 设计目标 将需要持久化状态的 MCP server 的生命周期从 ACP session 中解耦,由 Electron 主进程管理。 --- ## 新方案:双通道架构 ### 概览 引入 `persistent` 标志位,将 MCP server 分为两类,走不同的通道: | 类型 | 标志 | 生命周期 | 通道 | |------|------|---------|------| | **临时 server**(ephemeral) | `persistent: false`(默认) | 跟随 ACP session | qiming-mcp-stdio-proxy 聚合(不变) | | **持久化 server**(persistent) | `persistent: true` | 跟随 Electron 主进程 | PersistentMcpBridge HTTP ← qiming-mcp-stdio-proxy bridge 入口({ url }) | ### 架构图 ``` Electron Main Process(长生命周期) │ ├── PersistentMcpBridge(新组件,单例) │ │ │ ├── 持久化子进程: chrome-devtools-mcp(stdio,主进程 spawn) │ │ └── MCP Client ← StdioClientTransport → 子进程 stdin/stdout │ │ │ ├── HTTP Server(127.0.0.1,自动分配端口) │ │ └── 路由: POST/GET/DELETE /mcp/ │ │ └── 每个 HTTP client session → Server + StreamableHTTPServerTransport │ │ └── 代理 tools/list, tools/call → 共享的 MCP Client │ │ │ └── 自动重启 + 健康检查 + session 清理 │ └── McpProxyManager.getAgentMcpConfig() └── 单一 mcp-proxy:qiming-mcp-stdio-proxy --config '{ mcpServers: { stdio... | url... } }' ├── 临时 server → { command, args, env }(stdio 子进程) └── 持久化 server → { url }(bridge,StreamableHTTP → PersistentMcpBridge) Per ACP Session(短生命周期) │ └── qiming-mcp-stdio-proxy(单一进程,混合 stdio + bridge) ├── 临时 server:stdio 子进程 └── 持久化 server:StreamableHTTPClientTransport → PersistentMcpBridge HTTP ``` ### 数据流 ``` Agent 引擎(claude-code / qimingcode) │ └── mcp_servers.mcp-proxy (stdio) ← 单一入口 └── qiming-mcp-stdio-proxy --config '{ mcpServers: {...} }' ├── stdio 条目 → 子进程(临时 server) └── bridge 条目 { url } → StreamableHTTPClientTransport └── HTTP POST /mcp/ → Electron PersistentMcpBridge └── MCP Client (StdioClientTransport) → chrome-devtools-mcp(持久化子进程) ``` --- ## 核心组件 ### 1. McpServerEntry — 配置类型 ```typescript interface McpServerEntry { command: string; args: string[]; env?: Record; persistent?: boolean; // 标记为持久化 server } ``` `persistent: true` 的 server 由 PersistentMcpBridge 管理,不再传给 qiming-mcp-stdio-proxy。 ### 2. PersistentMcpBridge — 主进程 HTTP Bridge **文件**: `src/main/services/packages/persistentMcpBridge.ts` **职责**: - 管理持久化 MCP server 子进程的完整生命周期 - 提供 HTTP bridge 供 ACP session 的 bridge client 连接 - 工具列表缓存、子进程自动重启、session 清理 **关键设计**: | 设计点 | 方案 | |--------|------| | 子进程管理 | MCP SDK `StdioClientTransport` 内部 spawn,通过 `Client.connect()` 启动 | | HTTP 协议 | MCP SDK `StreamableHTTPServerTransport`(非 SSE,StreamableHTTP 是最新标准) | | 端口分配 | `server.listen(0, '127.0.0.1')` 系统自动分配,仅监听 loopback | | 路径路由 | 单端口,`/mcp/` 路由到对应 server | | Session 模型 | 每个 HTTP client session 独立的 `Server` + `StreamableHTTPServerTransport` 对,共享底层 `Client` | | 并发安全 | MCP SDK `Client.callTool()` 内部使用 JSON-RPC 请求 ID 做响应关联,天然支持并发 | | 自动重启 | 子进程退出 → 5 秒冷却 → 自动重新 spawn + connect | | 安全防护 | HTTP body 10MB 限制、stale session 60 秒定期清理、进程 force-kill 兜底 | **API**: ```typescript class PersistentMcpBridge { start(servers: Record): Promise; // 启动 bridge stop(): Promise; // 停止 bridge getBridgeUrl(serverId: string): string | null; // 获取 HTTP URL isRunning(): boolean; // 是否在运行 isServerHealthy(serverId: string): boolean; // server 健康检查 } export const persistentMcpBridge: PersistentMcpBridge; // 单例 ``` ### 3. Bridge 入口(已并入 qiming-mcp-stdio-proxy) 持久化 server 的桥接不再使用独立脚本,由 **qiming-mcp-stdio-proxy** 的 `--config` 中 `{ url }` 条目完成: - **stdio 条目**:`{ command, args, env }` → proxy 内部 spawn 子进程(临时 server) - **bridge 条目**:`{ url: "http://127.0.0.1:PORT/mcp/" }` → proxy 内部用 `StreamableHTTPClientTransport` 连接 PersistentMcpBridge 协议桥接: ``` Agent 引擎 (stdio) ↕ qiming-mcp-stdio-proxy(单一进程) ├── stdio 上游 → 子进程 └── bridge 上游 → StreamableHTTPClientTransport → PersistentMcpBridge HTTP ``` ### 4. McpProxyManager — 配置分发 **文件**: `src/main/services/packages/mcp.ts` `getAgentMcpConfig()` 构建**单一** mcp-proxy 配置,临时与持久化 server 均写入同一份 `mcpServers`: ```typescript getAgentMcpConfig() { const proxyServers = {}; // 临时 server → { command, args, env } for (const [name, entry] of ephemeral) { proxyServers[name] = resolveServersConfig({ [name]: entry })[name]; } // 持久化 server → { url: persistentMcpBridge.getBridgeUrl(name) } if (persistentMcpBridge.isRunning()) { for (const [name, entry] of persistent) { const url = persistentMcpBridge.getBridgeUrl(name); if (url) proxyServers[name] = { url }; } } return { 'mcp-proxy': { command: process.execPath, args: [proxyScript, '--config', JSON.stringify({ mcpServers: proxyServers })], env: { ELECTRON_RUN_AS_NODE: '1' }, }, }; } ``` Agent 引擎收到的 MCP 配置(仅一个 key): ```json { "mcp-proxy": { "command": "/path/to/electron", "args": ["/path/to/qiming-mcp-stdio-proxy", "--config", "{\"mcpServers\":{\"filesystem\":{\"command\":\"npx\",\"args\":[\"...\"]},\"chrome-devtools\":{\"url\":\"http://127.0.0.1:PORT/mcp/chrome-devtools\"}}}"], "env": { "ELECTRON_RUN_AS_NODE": "1" } } } ``` --- ## 生命周期管理 ### 启动流程 ``` app.whenReady() → runStartupTasks() └── mcpProxyManager.start() ├── 验证 qiming-mcp-stdio-proxy 已安装(缓存脚本路径) └── persistentMcpBridge.start(persistentServers) ├── 为每个 persistent server 创建 StdioClientTransport + Client ├── Client.connect() → spawn 子进程 → MCP initialize ├── Client.listTools() → 缓存 tool 列表 └── 启动 HTTP server(自动端口) + session 清理定时器 ``` ### ACP Session 流程 ``` agentService.init() → getAgentMcpConfig() └── mcp-proxy: ACP 引擎 spawn 单一 qiming-mcp-stdio-proxy ├── stdio 上游 → 临时 server 子进程 └── bridge 上游 → HTTP 连接 PersistentMcpBridge(持久化 server) Session 结束: └── qiming-mcp-stdio-proxy 进程退出 ├── 临时 server 子进程全部退出 ✓ └── bridge HTTP 连接关闭,持久化 MCP server 子进程继续运行 ✓ (关键!) ``` ### 停止/退出流程 ``` app 退出 / services:stopAll └── mcpProxyManager.cleanup() / mcpProxyManager.stop() └── persistentMcpBridge.stop() ├── 清理 session cleanup 定时器 ├── 关闭所有 HTTP session transport ├── 关闭 HTTP server └── 逐个停止持久化 server: ├── Client.close() ├── transport.close() → 子进程终止 └── 兜底: process.kill(pid, SIGTERM/SIGKILL) ``` ### 子进程自动重启 ``` 子进程异常退出 → transport.onclose 触发 └── scheduleRestart(5秒冷却) └── 清理旧 client/transport → spawnAndConnect() └── 新 StdioClientTransport + Client → listTools() ``` --- ## 配置 ### 默认配置 ```typescript const DEFAULT_MCP_PROXY_CONFIG = { mcpServers: { 'chrome-devtools': { command: 'chrome-devtools-mcp', args: [], persistent: true, // 持久化: 由 PersistentMcpBridge 管理 }, }, }; ``` ### 用户自定义 Server 通过 UI(MCPSettings 组件)添加的 server 默认为临时(ephemeral),走 qiming-mcp-stdio-proxy 聚合。 需要持久化的 server 需在配置中设置 `persistent: true`。 --- ## 依赖 | 依赖 | 用途 | 位置 | |------|------|------| | `@modelcontextprotocol/sdk` | MCP Client/Server/Transport 实现 | `package.json` devDependencies | | `qiming-mcp-stdio-proxy` | 临时 + 持久化 聚合(stdio + bridge 入口) | 应用依赖 + `~/.qiming-agent/node_modules/` | ### TypeScript 兼容性 MCP SDK 使用 package.json `exports` 字段的子路径导出,但项目 `tsconfig.main.json` 的 `moduleResolution: "node"` 不支持。 解决方案:`src/shared/types/mcp-sdk.d.ts` 提供 ambient module 声明,为以下子路径提供类型: - `@modelcontextprotocol/sdk/client` - `@modelcontextprotocol/sdk/server` - `@modelcontextprotocol/sdk/client/stdio.js` - `@modelcontextprotocol/sdk/server/streamableHttp.js` - `@modelcontextprotocol/sdk/server/stdio.js` - `@modelcontextprotocol/sdk/client/streamableHttp.js` - `@modelcontextprotocol/sdk/types.js` --- ## 文件清单 | 文件 | 状态 | 说明 | |------|------|------| | `src/main/services/packages/persistentMcpBridge.ts` | 新建 | 核心 bridge 服务 | | `src/main/services/packages/mcp.ts` | 修改 | 统一 mcp-proxy 配置(stdio + bridge 合并进一份 config) | | `src/shared/types/mcp-sdk.d.ts` | 新建 | MCP SDK 子路径类型声明 | | `src/main/services/packages/mcp.test.ts` | 修改 | 新增持久化相关测试 | | `src/main/main.ts` | 小改 | cleanup 流程说明 | | `src/main/ipc/processHandlers.ts` | 小改 | stopAll/restartAll 说明 | | `package.json` | 修改 | SDK 依赖 + extraResources | --- ## 验证 1. `npm run build` → TypeScript 编译通过 2. `npm run test` → 所有测试通过(含新增的 persistent bridge 测试用例) 3. `npm run electron:dev` → 开发模式验证: - PersistentMcpBridge 启动 → chrome-devtools-mcp 成功 spawn → 28 tools 就绪 - HTTP bridge 监听自动分配端口(如 57278) - ACP session 收到单一 `mcp-proxy` 配置(config 内含 stdio + bridge 条目) - qiming-mcp-stdio-proxy 内 bridge 连接 PersistentMcpBridge HTTP → tools 正常列出和调用 4. 多 session 测试 → 同一 Chrome 实例跨 session 持续可用 5. 应用退出 → 无残留进程 --- *Last updated: 2026-02-27*