# 本地 MCP 配置特性实现方案 ## 架构分析 ### 当前架构流程 ``` ┌─────────────────────────────────────────────────────────────┐ │ ACP 协议请求 │ │ (来自 GUI Agent Server) │ ├─────────────────────────────────────────────────────────────┤ │ request.agent_config.context_servers │ │ ↓ │ │ unifiedAgent.ensureEngineForRequest() │ │ ↓ │ │ mcp.syncMcpConfigToProxyAndReload() │ │ ↓ │ │ mcpProxyManager.getAgentMcpConfig() │ │ ↓ │ │ 通过 engine.init() 传递给 ACP agent │ └─────────────────────────────────────────────────────────────┘ ``` ### 目标架构 ``` ┌─────────────┐ │ 用户界面 │ │ 配置 MCP 服务器 │ └──────┬──────┘ ↓ ┌─────────────┐ │ SQLite │ │ 持久化存储 │ └──────┬──────┘ ↓ ┌─────────────┐ │ 本地 MCP 配置 │ ← 用户启用的服务器 └──────┬──────┘ ↓ ┌─────────────┐ │ 合并配置 │ ← 合并到 ACP 请求 └──────┬──────┘ ↓ ┌─────────────┐ │ ACP agent │ ← 最终传递给 agent └─────────────┘ ``` --- ## 实现方案 ### 一、数据模型设计 ```typescript // 本地 MCP 服务器配置类型 interface LocalMcpServer { id: string; // 唯一标识符 name: string; // 显示名称 command?: string; // 对于 stdio 服务器 args?: string[]; env?: Record; url?: string; // 对于远程服务器 transport?: "sse" | "streamable-http"; headers?: Record; authToken?: string; enabled: boolean; // 用户可启用/禁用 allowTools?: string[]; // 工具白名单 denyTools?: string[]; // 工具黑名单 toolCount?: number; // 缓存的工具数量 tools?: McpToolInfo[]; // 发现的工具列表 lastUpdated?: number; // 时间戳 source?: "user" | "builtin"; // 分类用 } interface McpToolInfo { name: string; description?: string; inputSchema?: object; annotations?: object; } ``` --- ### 二、服务层实现 创建 `src/main/services/packages/localMcpService.ts`: ```typescript import { getDb } from "../../db"; import type { McpServerEntry } from "./mcp"; export interface LocalMcpServer { id: string; name: string; command?: string; args?: string[]; env?: Record; url?: string; transport?: "sse" | "streamable-http"; headers?: Record; authToken?: string; enabled: boolean; allowTools?: string[]; denyTools?: string[]; toolCount?: number; tools?: McpToolInfo[]; lastUpdated?: number; source?: "user" | "builtin"; } export interface McpToolInfo { name: string; description?: string; inputSchema?: object; annotations?: object; } class LocalMcpService { /** * 获取所有本地 MCP 服务器 */ getLocalMcpServers(): LocalMcpServer[] { const db = getDb(); if (!db) return []; const result = db .prepare("SELECT * FROM local_mcp_servers ORDER BY name") .all() as any[]; return result.map((row) => this.parseRow(row)); } /** * 根据 ID 获取单个服务器 */ getLocalMcpServer(id: string): LocalMcpServer | undefined { const db = getDb(); if (!db) return undefined; const row = db .prepare("SELECT * FROM local_mcp_servers WHERE id = ?") .get() as any; return row ? this.parseRow(row) : undefined; } /** * 添加或更新本地 MCP 服务器 */ upsertLocalMcpServer(server: LocalMcpServer): void { const db = getDb(); if (!db) return; db.prepare(` INSERT OR REPLACE INTO local_mcp_servers (id, name, command, args, env, url, transport, headers, auth_token, enabled, allow_tools, deny_tools, tool_count, tools, last_updated, source) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `).run( server.id, server.name, server.command || null, JSON.stringify(server.args), JSON.stringify(server.env), server.url || null, server.transport || null, JSON.stringify(server.headers), server.authToken || null, server.enabled ? 1 : 0, JSON.stringify(server.allowTools), JSON.stringify(server.denyTools), server.toolCount || 0, JSON.stringify(server.tools), Date.now(), server.source || "user" ); } /** * 删除本地 MCP 服务器 */ removeLocalMcpServer(id: string): void { const db = getDb(); if (!db) return; db.prepare("DELETE FROM local_mcp_servers WHERE id = ?").run(id); } /** * 启用/禁用本地 MCP 服务器 */ toggleLocalMcpServer(id: string, enabled: boolean): void { const db = getDb(); if (!db) return; db.prepare("UPDATE local_mcp_servers SET enabled = ? WHERE id = ?") .run(enabled ? 1 : 0, id); } /** * 更新已发现的工具 */ updateServerTools( id: string, tools: McpToolInfo[], toolCount: number, ): void { const db = getDb(); if (!db) return; db.prepare(` UPDATE local_mcp_servers SET tools = ?, tool_count = ? WHERE id = ? `).run(JSON.stringify(tools), toolCount, id); } /** * 获取启用的服务器,转换为 McpServerEntry 格式 */ getEnabledServersAsEntries(): Record { const servers = this.getLocalMcpServers(); const result: Record = {}; for (const server of servers) { if (!server.enabled) continue; if (server.url) { // 远程服务器 result[server.name] = { url: server.url, transport: server.transport, headers: server.headers, authToken: server.authToken, allowTools: server.allowTools, denyTools: server.denyTools, } as McpServerEntry; } else if (server.command) { // Stdio 服务器 result[server.name] = { command: server.command, args: server.args, env: server.env, allowTools: server.allowTools, denyTools: server.denyTools, } as McpServerEntry; } } return result; } /** * 解析数据库行 */ private parseRow(row: any): LocalMcpServer { return { id: row.id, name: row.name, command: row.command, args: row.args ? JSON.parse(row.args) : [], env: row.env ? JSON.parse(row.env) : {}, url: row.url, transport: row.transport, headers: row.headers ? JSON.parse(row.headers) : {}, authToken: row.authToken, enabled: row.enabled === 1, allowTools: row.allow_tools ? JSON.parse(row.allow_tools) : [], denyTools: row.deny_tools ? JSON.parse(row.deny_tools) : [], toolCount: row.tool_count, tools: row.tools ? JSON.parse(row.tools) : [], lastUpdated: row.last_updated, source: row.source, }; } } export const localMcpService = new LocalMcpService(); ``` --- ### 三、数据库迁移 创建 `src/main/db/migrations/003-add-local-mcp-servers.ts`: ```typescript export const migration = (db: any) => { db.exec(` CREATE TABLE IF NOT EXISTS local_mcp_servers ( id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, command TEXT, args TEXT, env TEXT, url TEXT, transport TEXT, headers TEXT, auth_token TEXT, enabled INTEGER DEFAULT 1, allow_tools TEXT, deny_tools TEXT, tool_count INTEGER DEFAULT 0, tools TEXT, last_updated INTEGER, source TEXT DEFAULT 'user' ) `); // 创建索引以加速查询 db.exec(` CREATE INDEX IF NOT EXISTS idx_local_mcp_enabled ON local_mcp_servers(enabled) `); db.exec(` CREATE INDEX IF NOT EXISTS idx_local_mcp_source ON local_mcp_servers(source) `); }; ``` 在 `src/main/db/index.ts` 中注册迁移: ```typescript import { migration as m003 } from "./migrations/003-add-local-mcp-servers"; // 在迁移列表中添加 const migrations = [ // ... 现有迁移 { id: "003-add-local-mcp-servers", fn: m003 }, ]; ``` --- ### 四、IPC 处理器 创建 `src/main/ipc/handlers/mcp-local.ts`: ```typescript import { ipcMain } from "electron"; import { localMcpService, type LocalMcpServer } from "../../services/packages/localMcpService"; export function registerMcpLocalHandlers(): void { // 获取所有服务器列表 ipcMain.handle("mcp-local:list", () => { return localMcpService.getLocalMcpServers(); }); // 获取单个服务器 ipcMain.handle("mcp-local:get", (_, id: string) => { return localMcpService.getLocalMcpServer(id); }); // 添加新服务器 ipcMain.handle("mcp-local:add", (_, server: LocalMcpServer) => { // 生成 UUID 如果没有提供 if (!server.id) { server.id = `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`; } localMcpService.upsertLocalMcpServer(server); return server; }); // 更新服务器 ipcMain.handle("mcp-local:update", (_, server: LocalMcpServer) => { localMcpService.upsertLocalMcpServer(server); return server; }); // 删除服务器 ipcMain.handle("mcp-local:remove", (_, id: string) => { localMcpService.removeLocalMcpServer(id); }); // 切换启用/禁用状态 ipcMain.handle("mcp-local:toggle", (_, id: string, enabled: boolean) => { localMcpService.toggleLocalMcpServer(id, enabled); }); // 获取已启用的服务器 ipcMain.handle("mcp-local:get-enabled", () => { const servers = localMcpService.getLocalMcpServers(); return servers.filter((s) => s.enabled); }); } ``` 在主进程入口注册: ```typescript // src/main/main.ts 或类似文件 import { registerMcpLocalHandlers } from "./ipc/handlers/mcp-local"; registerMcpLocalHandlers(); ``` --- ### 五、Preload 脚本扩展 添加 TypeScript 类型声明到 preload: ```typescript // src/main/preload.ts contextBridge.exposeInMainWorld("electron", { // ... 现有 API mcpLocal: { list: () => ipcRenderer.invoke("mcp-local:list"), get: (id: string) => ipcRenderer.invoke("mcp-local:get", id), add: (server: LocalMcpServer) => ipcRenderer.invoke("mcp-local:add", server), update: (server: LocalMcpServer) => ipcRenderer.invoke("mcp-local:update", server), remove: (id: string) => ipcRenderer.invoke("mcp-local:remove", id), toggle: ( id: string, enabled: boolean, ) => ipcRenderer.invoke("mcp-local:toggle", id, enabled), getEnabled: () => ipcRenderer.invoke("mcp-local:get-enabled"), }, }); ``` 添加全局类型声明: ```typescript // src/types/global.d.ts interface ElectronAPI { // ... 现有接口 mcpLocal: { list: () => Promise; get: (id: string) => Promise; add: (server: LocalMcpServer) => Promise; update: (server: LocalMcpServer) => Promise; remove: (id: string) => Promise; toggle: (id: string, enabled: boolean) => Promise; getEnabled: () => Promise; }; } interface LocalMcpServer { id: string; name: string; command?: string; args?: string[]; env?: Record; url?: string; transport?: "sse" | "streamable-http"; headers?: Record; authToken?: string; enabled: boolean; allowTools?: string[]; denyTools?: string[]; toolCount?: number; tools?: McpToolInfo[]; lastUpdated?: number; source?: "user" | "builtin"; } interface McpToolInfo { name: string; description?: string; inputSchema?: object; annotations?: object; } declare global { interface Window { electron: ElectronAPI; } } export {}; ``` --- ### 六、UI 组件 创建 `src/components/LocalMcpSettings.tsx`: ```typescript import React, { useState, useEffect } from "react"; import { Column, Row, Button, Card, SearchInput, Switch, Icon } from "@components/ui"; import type { LocalMcpServer } from "@shared/types"; export const LocalMcpSettings: React.FC = () => { const [servers, setServers] = useState([]); const [searchTerm, setSearchTerm] = useState(""); const [isAdding, setIsAdding] = useState(false); // 加载服务器列表 useEffect(() => { loadServers(); }, []); const loadServers = async () => { const list = await window.electron.mcpLocal.list(); setServers(list); }; // 处理启用/禁用切换 const handleToggle = async (id: string, enabled: boolean) => { await window.electron.mcpLocal.toggle(id, enabled); setServers((prev) => prev.map((s) => (s.id === id ? { ...s, enabled } : s)), ); }; // 处理删除 const handleDelete = async (id: string) => { if (window.confirm("确定要删除这个 MCP 服务器吗?")) { await window.electron.mcpLocal.remove(id); setServers((prev) => prev.filter((s) => s.id !== id)); } }; // 过滤服务器 const filteredServers = servers.filter((server) => { const query = searchTerm.toLowerCase(); return ( server.name.toLowerCase().includes(query) || server.command?.toLowerCase().includes(query) || server.url?.toLowerCase().includes(query) ); }); return ( {/* 标题栏 */}

本地 MCP 服务器

{/* 搜索框 */} setSearchTerm(e.target.value)} /> {/* 服务器列表 */} {filteredServers.length === 0 ? (

{searchTerm ? "未找到匹配的服务器" : "暂无服务器,点击上方按钮添加"}

) : ( filteredServers.map((server) => (

{server.name}

{server.source === "builtin" && ( 内置 )}
{/* 服务器信息 */}
{server.command ? ( {server.command} ) : ( {server.url} )}
{/* 工具统计 */} {server.toolCount ? (
{server.toolCount} 个工具
) : null} {/* 工具列表预览 */} {server.tools && server.tools.length > 0 ? (
工具:
{server.tools.slice(0, 5).map((tool) => ( {tool.name} ))} {server.tools.length > 5 && ( +{server.tools.length - 5} )}
) : null}
{/* 操作区 */}
{server.enabled ? ( ) : ( )}
handleToggle(server.id, checked) } />
)) )}
{/* 添加服务器模态框 */} {isAdding && setIsAdding(false)} />}
); }; // 添加服务器模态框 const AddServerModal: React.FC<{ onClose: () => void }> = ({ onClose }) => { const [name, setName] = useState(""); const [command, setCommand] = useState(""); const [args, setArgs] = useState(""); const [url, setUrl] = useState(""); const handleSubmit = async () => { if (!name) return alert("请输入服务器名称"); await window.electron.mcpLocal.add({ name, command: command || undefined, args: args ? args .split(" ") .filter((a) => a.trim()) .map((a) => a.trim()) : undefined, url: url || undefined, enabled: true, }); onClose(); }; return (

添加 MCP 服务器

setName(e.target.value)} placeholder="my-mcp-server" />
{!url ? (
setCommand(e.target.value)} placeholder="npx -y mcp-server-github" />
) : (
setUrl(e.target.value)} placeholder="http://localhost:3001/mcp" />
)}
); }; ``` --- ### 七、集成到 ACP 协议流程 修改 `crates/agent-electron-client/src/main/services/engines/unifiedAgent.ts`: 在 `ensureEngineForRequest` 方法中,在获取 freshMcpServers 之后,添加本地 MCP 配置的合并: ```typescript // ... 在 existing code 中,获取 freshMcpServers 之后 // ==================== 新增代码开始 ==================== // 合并本地 MCP 配置 (启用的服务器) const localMcpEntries = localMcpService.getEnabledServersAsEntries(); if (Object.keys(localMcpEntries).length > 0) { log.info( `[UnifiedAgent] 📚 Merging ${Object.keys(localMcpEntries).length} local MCP servers`, ); // 将本地 MCP 同步到 proxy await syncMcpConfigToProxyAndReload(localMcpEntries); // 合并到 freshMcpServers freshMcpServers = { ...(freshMcpServers || {}), ...localMcpEntries, }; } // ==================== 新增代码结束 ==================== // ... existing code continues ``` --- ## 总结 ### 需要创建/修改的文件 | 文件 | 操作 | 说明 | |------|------|------| | `src/main/services/packages/localMcpService.ts` | 新建 | 本地 MCP 服务层 | | `src/main/db/migrations/003-add-local-mcp-servers.ts` | 新建 | 数据库迁移 | | `src/main/ipc/handlers/mcp-local.ts` | 新建 | IPC 处理器 | | `src/components/LocalMcpSettings.tsx` | 新建 | UI 组件 | | `src/main/preload.ts` | 修改 | 添加 mcpLocal API 暴露 | | `src/types/global.d.ts` | 修改 | 添加 TypeScript 类型 | | `crates/agent-electron-client/src/main/services/engines/unifiedAgent.ts` | 修改 | 合并本地 MCP 到 ACP 配置 | | `src/main/db/index.ts` | 修改 | 注册迁移 | ### 核心流程 ``` 用户配置 MCP 服务器 ↓ 存储到 SQLite ↓ 用户启用/禁用 ↓ 获取启用的服务器 ↓ 合并到 ACP 请求配置 ↓ 通过 proxy 传递给 agent ``` ### 特性清单 - ✅ 本地 MCP 服务器配置 - ✅ SQLite 持久化 - ✅ 启用/禁用控制 - ✅ 支持 stdio 和 remote 两种类型 - ✅ 自动合并到 ACP 协议配置 - ✅ 与现有 MCP proxy 架构兼容 --- ## 后续优化方向 1. **工具发现功能** - 实现 `mcp-local:fetch-tools` IPC 处理器 - 支持从配置的 MCP 服务器获取工具列表 - 缓存工具信息到数据库 2. **工具白名单/黑名单** - 实现 `allowTools` 和 `denyTools` 配置 - 在 UI 中提供工具选择界面 3. **预设服务器模板** - 添加常用 MCP 服务器预设 - 一键导入配置 4. **服务器健康检查** - 定期检查 MCP 服务器是否可用 - 显示连接状态 5. **配置导入/导出** - 支持将 MCP 配置导出为 JSON - 支持从 JSON 导入配置 --- *文档创建时间:2026-04-22* *最后更新:2026-04-22*