Files
qiming/qimingclaw/docs/persistent-mcp-bridge-architecture.md

328 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 持久化 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-mcpstdio主进程 spawn
│ │ └── MCP Client ← StdioClientTransport → 子进程 stdin/stdout
│ │
│ ├── HTTP Server127.0.0.1,自动分配端口)
│ │ └── 路由: POST/GET/DELETE /mcp/<serverId>
│ │ └── 每个 HTTP client session → Server + StreamableHTTPServerTransport
│ │ └── 代理 tools/list, tools/call → 共享的 MCP Client
│ │
│ └── 自动重启 + 健康检查 + session 清理
└── McpProxyManager.getAgentMcpConfig()
└── 单一 mcp-proxyqiming-mcp-stdio-proxy --config '{ mcpServers: { stdio... | url... } }'
├── 临时 server → { command, args, env }stdio 子进程)
└── 持久化 server → { url }bridgeStreamableHTTP → PersistentMcpBridge
Per ACP Session短生命周期
└── qiming-mcp-stdio-proxy单一进程混合 stdio + bridge
├── 临时 serverstdio 子进程
└── 持久化 serverStreamableHTTPClientTransport → 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 — 配置类型
```typescript
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`(非 SSEStreamableHTTP 是最新标准) |
| 端口分配 | `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**
```typescript
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`
```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
通过 UIMCPSettings 组件)添加的 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*