Files
qiming/qimingclaw/crates/qiming-mcp-stdio-proxy/MIGRATION.md

560 lines
20 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.
# 接入替换文档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 <PORT> --host <HOST> --config '<mcpServers JSON>'
[--protocol sse|stream] # 默认 stream (Streamable HTTP)
[--log-dir <DIR>]
[--diagnostic]
```
- 启动 HTTP 服务器,聚合 `--config` 中定义的 stdio MCP servers
- 对外暴露 Streamable HTTP默认或 SSE 端点
- 支持 `--allow-tools` / `--deny-tools` 工具过滤
### `convert` 子命令详情
```bash
mcp-proxy convert <URL>
[--config '<mcpServers JSON>'] # 也支持 config 模式
[--auth "Bearer token"]
[--protocol sse|stream]
[--ping-interval <SEC>]
```
- 将远程 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` wrapperWindows 无弹窗
### 架构对比
| | 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 <url>` 以 stdio 方式接入
新架构中:
- `qiming-mcp-stdio-proxy` 本身就是一个 stdio MCP server
- Agent 引擎直接 spawn 它,无需中转
- 配置通过 `--config` 参数传入,无需运行时控制面
---
## 五、配置格式兼容性
新旧 proxy 使用**完全相同**的 `mcpServers` JSON 配置格式:
```typescript
interface McpServersConfig {
mcpServers: Record<string, {
command: string;
args: string[];
env?: Record<string, string>;
}>;
}
```
- 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<string, {
command: string; args: string[]; env?: Record<string, string>;
}> | 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<string, { command: string; args?: string[]; env?: Record<string, string> }>,
): Promise<void> {
if (!mcpServers || Object.keys(mcpServers).length === 0) return;
// 提取真实服务(过滤旧桥接项 command==='mcp-proxy'
const realOnly: Record<string, { command: string; args: string[]; env?: Record<string, string> }> = {};
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` 接口。