P0 安全/稳定性修复 (6项):
- sqlite persist 改用临时文件+rename,I/O错误日志
- IPC fs 处理器加入 checkPathAllowed 路径验证
- handleCompress 命令注入修复 + 跨平台 (powershell/tar/zip/unzip)
- browser 全页截图简化,使用 capturePage full rect
- workspace 竞态条件修复
- editFile 5MB 大小限制
P1 安全/稳定性修复 (7项):
- 大小写不敏感搜索: query 同步转换小写
- web_fetch 无 content-length 时流式读取+10MB限制防OOM
- Windows SIGTERM 改用 taskkill /PID /T /F 强制终止
- startsWith 路径检查 Windows 大小写不敏感
- 安全黑名单扩展: 33个目录路径 + 30条危险命令 (Linux+Windows)
- plan_track stepIndex 改用 typeof === 'number' 精确检查
- MCP 前缀改用双下划线分隔 mcp_{server}__{tool} 防歧义
P2 代码质量修复 (8项):
- 修复 EXECUTING→THINKING 死循环Bug (转换表缺失)
- COMPRESSING 硬上限绕过时同步 _loopState 全局状态
- Abort 路径补齐 onDone 回调, 防止UI状态残留
- 幻觉检测从4条扩展到16条规则, 覆盖全部工具类别
- 移除 getTokenEfficiency 死代码
- 跨平台路径清理 (replace(/[\/]+$/, ''))
- 动态 import 加 try/catch, 失败自动批准不阻断
- #modelSelect 加 null 守卫, 防止运行时崩溃
文档 & UI 更新:
- README.md: 版本号/特性表/架构图/安全机制/工具清单全面刷新
- 帮助面板: 新增 Plan Mode、抗幻觉&稳定性章节, Agent Loop补8状态机
- 工具面板: plan_track卡片 + plan-mode徽章样式, 标题更新为42+1
- 工作空间面板宽度: 480px → 400px
- 源码注释移除所有版本标签, 仅保留4处必要位置
- 版本号 0.12.2 → 0.12.3
170 lines
5.2 KiB
TypeScript
170 lines
5.2 KiB
TypeScript
/**
|
|
* MCP Client - Model Context Protocol 渲染端客户端
|
|
* 管理 MCP Server 配置,通过 IPC 调用主进程进行协议通信
|
|
*/
|
|
|
|
import { state, KEYS } from '../state/state.js';
|
|
import { logInfo, logWarn, logSuccess, logError } from './log-service.js';
|
|
import type { ToolDefinition, ToolResult } from '../types.js';
|
|
|
|
export interface MCPServerConfig {
|
|
name: string;
|
|
command: string;
|
|
args: string[];
|
|
enabled: boolean;
|
|
description?: string;
|
|
}
|
|
|
|
// ─── 配置管理 ───
|
|
|
|
export async function getMCPServers(): Promise<MCPServerConfig[]> {
|
|
const db = state.get<any>(KEYS.DB);
|
|
if (!db) return [];
|
|
try {
|
|
return await db.getSetting('mcp_servers', []);
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
export async function saveMCPServers(servers: MCPServerConfig[]): Promise<void> {
|
|
const db = state.get<any>(KEYS.DB);
|
|
if (db) await db.saveSetting('mcp_servers', servers);
|
|
}
|
|
|
|
export async function addMCPServer(config: MCPServerConfig): Promise<void> {
|
|
const servers = await getMCPServers();
|
|
const existing = servers.findIndex(s => s.name === config.name);
|
|
if (existing >= 0) {
|
|
servers[existing] = config;
|
|
} else {
|
|
servers.push(config);
|
|
}
|
|
await saveMCPServers(servers);
|
|
logInfo(`MCP Server 已${existing >= 0 ? '更新' : '添加'}: ${config.name}`);
|
|
}
|
|
|
|
export async function removeMCPServer(name: string): Promise<void> {
|
|
const bridge = window.metonaDesktop;
|
|
if (bridge?.mcp) await bridge.mcp.stopServer(name);
|
|
const servers = await getMCPServers();
|
|
await saveMCPServers(servers.filter(s => s.name !== name));
|
|
logInfo(`MCP Server 已删除: ${name}`);
|
|
}
|
|
|
|
export async function toggleMCPServer(name: string): Promise<boolean> {
|
|
const servers = await getMCPServers();
|
|
const server = servers.find(s => s.name === name);
|
|
if (!server) return false;
|
|
server.enabled = !server.enabled;
|
|
await saveMCPServers(servers);
|
|
logInfo(`MCP Server ${server.enabled ? '已启用' : '已禁用'}: ${name}`);
|
|
return server.enabled;
|
|
}
|
|
|
|
export async function getEnabledMCPServers(): Promise<MCPServerConfig[]> {
|
|
const servers = await getMCPServers();
|
|
return servers.filter(s => s.enabled);
|
|
}
|
|
|
|
// ─── MCP 服务器生命周期 ───
|
|
|
|
/** 启动所有已启用的 MCP 服务器 */
|
|
export async function startAllMCPServers(): Promise<void> {
|
|
const bridge = window.metonaDesktop;
|
|
if (!bridge?.mcp) return;
|
|
|
|
const configs = await getEnabledMCPServers();
|
|
for (const config of configs) {
|
|
try {
|
|
const result = await bridge.mcp.startServer(config);
|
|
if (result.success) {
|
|
logSuccess(`MCP 服务器已启动: ${config.name}`);
|
|
} else {
|
|
logWarn(`MCP 服务器启动失败: ${config.name}`, result.error);
|
|
}
|
|
} catch (err) {
|
|
logError(`MCP 服务器启动异常: ${config.name}`, (err as Error).message);
|
|
}
|
|
}
|
|
}
|
|
|
|
/** 停止所有 MCP 服务器 */
|
|
export async function stopAllMCPServers(): Promise<void> {
|
|
const bridge = window.metonaDesktop;
|
|
if (bridge?.mcp) await bridge.mcp.stopAll();
|
|
}
|
|
|
|
/** 获取 MCP 服务器状态 */
|
|
export async function getMCPServerStatuses(): Promise<Array<{ name: string; running: boolean; toolCount: number }>> {
|
|
const bridge = window.metonaDesktop;
|
|
if (!bridge?.mcp) return [];
|
|
try {
|
|
return await bridge.mcp.getStatuses();
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
// ─── 工具定义(供 Agent 使用)───
|
|
|
|
/** 从主进程获取所有 MCP 工具并转换为 ToolDefinition 格式 */
|
|
export async function getMCPToolDefinitions(): Promise<ToolDefinition[]> {
|
|
const bridge = window.metonaDesktop;
|
|
if (!bridge?.mcp) return [];
|
|
|
|
try {
|
|
const mcpTools = await bridge.mcp.getTools() as Array<{
|
|
serverName: string;
|
|
name: string;
|
|
fullName: string;
|
|
description: string;
|
|
inputSchema: { type: string; properties?: Record<string, unknown>; required?: string[] };
|
|
}>;
|
|
|
|
if (mcpTools.length === 0) return [];
|
|
|
|
logInfo(`MCP 工具已注册: ${mcpTools.length} 个`, mcpTools.map(t => t.fullName).join(', '));
|
|
|
|
return mcpTools.map(t => ({
|
|
type: 'function' as const,
|
|
function: {
|
|
name: t.fullName,
|
|
description: t.description,
|
|
parameters: {
|
|
type: 'object' as const,
|
|
required: t.inputSchema.required || [],
|
|
properties: (t.inputSchema.properties || {}) as Record<string, { type: string; description?: string }>
|
|
}
|
|
}
|
|
}));
|
|
} catch (err) {
|
|
logError('MCP 工具获取失败', (err as Error).message);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/** 执行 MCP 工具调用 */
|
|
export async function executeMCPTool(toolName: string, args: Record<string, unknown>): Promise<ToolResult> {
|
|
const bridge = window.metonaDesktop;
|
|
if (!bridge?.mcp) {
|
|
return { success: false, error: 'MCP API 不可用' };
|
|
}
|
|
|
|
// 解析工具全名: mcp_{serverName}__{toolName}(双下划线分隔,避免 serverName/toolName 含下划线时歧义)
|
|
const match = toolName.match(/^mcp_(.+?)__(.+)$/);
|
|
if (!match) {
|
|
return { success: false, error: `无效的 MCP 工具名: ${toolName}` };
|
|
}
|
|
|
|
const serverName = match[1];
|
|
const actualToolName = match[2];
|
|
|
|
try {
|
|
const result = await bridge.mcp.callTool(serverName, actualToolName, args);
|
|
return result;
|
|
} catch (err) {
|
|
return { success: false, error: (err as Error).message };
|
|
}
|
|
}
|