12 KiB
持久化 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/<serverId>
│ │ └── 每个 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/<serverId> → Electron PersistentMcpBridge
└── MCP Client (StdioClientTransport) → chrome-devtools-mcp(持久化子进程)
核心组件
1. McpServerEntry — 配置类型
interface McpServerEntry {
command: string;
args: string[];
env?: Record<string, string>;
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/<serverId> 路由到对应 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:
class PersistentMcpBridge {
start(servers: Record<string, McpServerEntry>): Promise<void>; // 启动 bridge
stop(): Promise<void>; // 停止 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/<serverId>" }→ 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:
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):
{
"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()
配置
默认配置
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 |
验证
npm run build→ TypeScript 编译通过npm run test→ 所有测试通过(含新增的 persistent bridge 测试用例)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 正常列出和调用
- 多 session 测试 → 同一 Chrome 实例跨 session 持续可用
- 应用退出 → 无残留进程
Last updated: 2026-02-27