864 lines
24 KiB
Markdown
864 lines
24 KiB
Markdown
# 本地 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<string, string>;
|
||
url?: string; // 对于远程服务器
|
||
transport?: "sse" | "streamable-http";
|
||
headers?: Record<string, string>;
|
||
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<string, string>;
|
||
url?: string;
|
||
transport?: "sse" | "streamable-http";
|
||
headers?: Record<string, string>;
|
||
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<string, McpServerEntry> {
|
||
const servers = this.getLocalMcpServers();
|
||
const result: Record<string, McpServerEntry> = {};
|
||
|
||
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<LocalMcpServer[]>;
|
||
get: (id: string) => Promise<LocalMcpServer | undefined>;
|
||
add: (server: LocalMcpServer) => Promise<LocalMcpServer>;
|
||
update: (server: LocalMcpServer) => Promise<LocalMcpServer>;
|
||
remove: (id: string) => Promise<void>;
|
||
toggle: (id: string, enabled: boolean) => Promise<void>;
|
||
getEnabled: () => Promise<LocalMcpServer[]>;
|
||
};
|
||
}
|
||
|
||
interface LocalMcpServer {
|
||
id: string;
|
||
name: string;
|
||
command?: string;
|
||
args?: string[];
|
||
env?: Record<string, string>;
|
||
url?: string;
|
||
transport?: "sse" | "streamable-http";
|
||
headers?: Record<string, string>;
|
||
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<LocalMcpServer[]>([]);
|
||
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 (
|
||
<Column gap={16} className="mcp-settings">
|
||
{/* 标题栏 */}
|
||
<Row justify="space-between" align="center">
|
||
<h2>本地 MCP 服务器</h2>
|
||
<Button icon="plus" onClick={() => setIsAdding(true)}>
|
||
添加服务器
|
||
</Button>
|
||
</Row>
|
||
|
||
{/* 搜索框 */}
|
||
<SearchInput
|
||
placeholder="搜索服务器..."
|
||
value={searchTerm}
|
||
onChange={(e) => setSearchTerm(e.target.value)}
|
||
/>
|
||
|
||
{/* 服务器列表 */}
|
||
<Column gap={8}>
|
||
{filteredServers.length === 0 ? (
|
||
<Card padding={24}>
|
||
<p style={{ color: "#888", textAlign: "center" }}>
|
||
{searchTerm
|
||
? "未找到匹配的服务器"
|
||
: "暂无服务器,点击上方按钮添加"}
|
||
</p>
|
||
</Card>
|
||
) : (
|
||
filteredServers.map((server) => (
|
||
<Card key={server.id} padding={12} className="mcp-server-card">
|
||
<Row justify="space-between" align="flex-start">
|
||
<div style={{ flex: 1 }}>
|
||
<Row align="center" gap={8} mb={4}>
|
||
<h3 style={{ margin: 0 }}>{server.name}</h3>
|
||
{server.source === "builtin" && (
|
||
<span className="badge badge-built-in">内置</span>
|
||
)}
|
||
</Row>
|
||
|
||
{/* 服务器信息 */}
|
||
<div style={{ fontSize: "12px", color: "#666" }}>
|
||
{server.command ? (
|
||
<code>{server.command}</code>
|
||
) : (
|
||
<span>{server.url}</span>
|
||
)}
|
||
</div>
|
||
|
||
{/* 工具统计 */}
|
||
{server.toolCount ? (
|
||
<div style={{ marginTop: 4 }}>
|
||
<span className="tool-count">{server.toolCount} 个工具</span>
|
||
</div>
|
||
) : null}
|
||
|
||
{/* 工具列表预览 */}
|
||
{server.tools && server.tools.length > 0 ? (
|
||
<div className="tools-preview" style={{ marginTop: 8 }}>
|
||
<small>工具:</small>
|
||
<div style={{ display: "flex", flexWrap: "wrap", gap: 4 }}>
|
||
{server.tools.slice(0, 5).map((tool) => (
|
||
<span key={tool.name} className="tool-tag">
|
||
{tool.name}
|
||
</span>
|
||
))}
|
||
{server.tools.length > 5 && (
|
||
<span className="tool-tag">
|
||
+{server.tools.length - 5}
|
||
</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
) : null}
|
||
</div>
|
||
|
||
{/* 操作区 */}
|
||
<Row gap={8} align="center">
|
||
<div className="server-status">
|
||
{server.enabled ? (
|
||
<Icon name="check-circle" color="green" />
|
||
) : (
|
||
<Icon name="circle" color="gray" />
|
||
)}
|
||
</div>
|
||
<Switch
|
||
checked={server.enabled}
|
||
onChange={(checked) =>
|
||
handleToggle(server.id, checked)
|
||
}
|
||
/>
|
||
<Button
|
||
size="small"
|
||
variant="danger"
|
||
onClick={() => handleDelete(server.id)}
|
||
>
|
||
删除
|
||
</Button>
|
||
</Row>
|
||
</Row>
|
||
</Card>
|
||
))
|
||
)}
|
||
</Column>
|
||
|
||
{/* 添加服务器模态框 */}
|
||
{isAdding && <AddServerModal onClose={() => setIsAdding(false)} />}
|
||
</Column>
|
||
);
|
||
};
|
||
|
||
// 添加服务器模态框
|
||
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 (
|
||
<div className="modal-overlay">
|
||
<div className="modal">
|
||
<h3>添加 MCP 服务器</h3>
|
||
|
||
<div className="form-group">
|
||
<label>服务器名称 *</label>
|
||
<input
|
||
type="text"
|
||
value={name}
|
||
onChange={(e) => setName(e.target.value)}
|
||
placeholder="my-mcp-server"
|
||
/>
|
||
</div>
|
||
|
||
<div className="form-group">
|
||
<label>类型</label>
|
||
<div className="radio-group">
|
||
<label>
|
||
<input
|
||
type="radio"
|
||
name="type"
|
||
value="stdio"
|
||
checked={!url}
|
||
onChange={() => setUrl("")}
|
||
/>
|
||
Stdio (本地命令)
|
||
</label>
|
||
<label>
|
||
<input
|
||
type="radio"
|
||
name="type"
|
||
value="remote"
|
||
checked={!!url}
|
||
onChange={() => setCommand("")}
|
||
/>
|
||
Remote (HTTP URL)
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
{!url ? (
|
||
<div className="form-group">
|
||
<label>命令 *</label>
|
||
<input
|
||
type="text"
|
||
value={command}
|
||
onChange={(e) => setCommand(e.target.value)}
|
||
placeholder="npx -y mcp-server-github"
|
||
/>
|
||
<label style={{ marginTop: 8, display: "block" }}>
|
||
参数 (可选)
|
||
<input
|
||
type="text"
|
||
value={args}
|
||
onChange={(e) => setArgs(e.target.value)}
|
||
placeholder="--token xxx"
|
||
style={{ width: "100%", marginTop: 4 }}
|
||
/>
|
||
</label>
|
||
</div>
|
||
) : (
|
||
<div className="form-group">
|
||
<label>URL *</label>
|
||
<input
|
||
type="url"
|
||
value={url}
|
||
onChange={(e) => setUrl(e.target.value)}
|
||
placeholder="http://localhost:3001/mcp"
|
||
/>
|
||
</div>
|
||
)}
|
||
|
||
<Row gap={8} justify="flex-end" mt={16}>
|
||
<Button variant="secondary" onClick={onClose}>
|
||
取消
|
||
</Button>
|
||
<Button primary onClick={handleSubmit}>
|
||
添加
|
||
</Button>
|
||
</Row>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
```
|
||
|
||
---
|
||
|
||
### 七、集成到 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*
|