From c833b0d559c8e4771ba3f15105aeb268758e659c Mon Sep 17 00:00:00 2001 From: youyi <2572082773@qq.com> Date: Tue, 20 Jan 2026 16:50:10 +0800 Subject: [PATCH 1/8] fix: allow Tools(WebSearch and WebFetch) --- src/ui/components/EventCard.tsx | 1 + src/ui/components/PromptInput.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ui/components/EventCard.tsx b/src/ui/components/EventCard.tsx index 4f37c34..0744dc2 100644 --- a/src/ui/components/EventCard.tsx +++ b/src/ui/components/EventCard.tsx @@ -187,6 +187,7 @@ const ToolUseCard = ({ messageContent, showIndicator = false }: { messageContent case "Glob": case "Grep": return input?.pattern || null; case "Task": return input?.description || null; case "WebFetch": return input?.url || null; + case "WebSearch": return input?.query || null; default: return null; } }; diff --git a/src/ui/components/PromptInput.tsx b/src/ui/components/PromptInput.tsx index ddfd1f2..81a440c 100644 --- a/src/ui/components/PromptInput.tsx +++ b/src/ui/components/PromptInput.tsx @@ -2,7 +2,7 @@ import { useCallback, useEffect, useRef } from "react"; import type { ClientEvent } from "../types"; import { useAppStore } from "../store/useAppStore"; -const DEFAULT_ALLOWED_TOOLS = "Read,Edit,Bash"; +const DEFAULT_ALLOWED_TOOLS = "Read,Edit,Bash,WebSearch,WebFetch"; const MAX_ROWS = 12; const LINE_HEIGHT = 21; const MAX_HEIGHT = MAX_ROWS * LINE_HEIGHT; From d36eca783c90425420efc19c245ac546f36c5d1c Mon Sep 17 00:00:00 2001 From: yyforreal <2572082773@qq.com> Date: Sun, 25 Jan 2026 23:25:47 +0800 Subject: [PATCH 2/8] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9EMCP=E6=A8=A1?= =?UTF-8?q?=E5=9D=97=E5=8F=8A=E9=85=8D=E7=BD=AE=E7=AE=A1=E7=90=86=E4=B8=8E?= =?UTF-8?q?IPC=E9=80=9A=E4=BF=A1=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bun.lock | 1 + package.json | 5 +- src/electron/libs/mcp/builtin-servers.ts | 205 ++++++++++++ src/electron/libs/mcp/index.ts | 71 +++++ src/electron/libs/mcp/mcp-config.ts | 129 ++++++++ src/electron/libs/mcp/mcp-ipc-handlers.ts | 280 +++++++++++++++++ src/electron/libs/mcp/mcp-manager.ts | 118 +++++++ src/electron/libs/mcp/mcp-store.ts | 246 +++++++++++++++ src/electron/libs/runner.ts | 24 +- src/electron/main.ts | 11 +- src/electron/preload.cts | 37 ++- src/ui/components/APIConfigPanel.tsx | 171 ++++++++++ src/ui/components/MCPErrorGuide.tsx | 214 +++++++++++++ src/ui/components/MCPServerForm.tsx | 365 ++++++++++++++++++++++ src/ui/components/MCPToolsPanel.tsx | 361 +++++++++++++++++++++ src/ui/components/SettingsModal.tsx | 225 ++++--------- types.d.ts | 40 +++ 17 files changed, 2322 insertions(+), 181 deletions(-) create mode 100644 src/electron/libs/mcp/builtin-servers.ts create mode 100644 src/electron/libs/mcp/index.ts create mode 100644 src/electron/libs/mcp/mcp-config.ts create mode 100644 src/electron/libs/mcp/mcp-ipc-handlers.ts create mode 100644 src/electron/libs/mcp/mcp-manager.ts create mode 100644 src/electron/libs/mcp/mcp-store.ts create mode 100644 src/ui/components/APIConfigPanel.tsx create mode 100644 src/ui/components/MCPErrorGuide.tsx create mode 100644 src/ui/components/MCPServerForm.tsx create mode 100644 src/ui/components/MCPToolsPanel.tsx diff --git a/bun.lock b/bun.lock index b0739b6..ca4abdb 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "electron-vite-template", diff --git a/package.json b/package.json index 016466f..cfa18b5 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,8 @@ "dist:mac-arm64": "bun run transpile:electron && bun run build && electron-builder --mac --arm64", "dist:mac-x64": "bun run transpile:electron && bun run build && electron-builder --mac --x64", "dist:win": "bun run transpile:electron && bun run build && electron-builder --win --x64", - "dist:linux": "bun run transpile:electron && bun run build && electron-builder --linux --x64" + "dist:linux": "bun run transpile:electron && bun run build && electron-builder --linux --x64", + "postinstall": "electron-rebuild -f -w better-sqlite3" }, "dependencies": { "@anthropic-ai/claude-agent-sdk": "^0.2.6", @@ -58,4 +59,4 @@ "patchedDependencies": { "@anthropic-ai/claude-agent-sdk@0.2.6": "patches/@anthropic-ai%2Fclaude-agent-sdk@0.2.6.patch" } -} +} \ No newline at end of file diff --git a/src/electron/libs/mcp/builtin-servers.ts b/src/electron/libs/mcp/builtin-servers.ts new file mode 100644 index 0000000..c3844a8 --- /dev/null +++ b/src/electron/libs/mcp/builtin-servers.ts @@ -0,0 +1,205 @@ +/** + * 内置 MCP Server 配置模板 + * 提供预配置的 MCP Server,如 Playwright 浏览器工具 + */ + +import { app } from "electron"; +import * as path from "path"; +import { MCPServerConfig, MCPBrowserMode } from "./mcp-config.js"; + +/** 内置 Server 类型 */ +export type BuiltinServerType = "playwright"; + +/** Playwright MCP Server ID(固定) */ +export const PLAYWRIGHT_SERVER_ID = "builtin-playwright"; + +/** + * 获取默认的用户数据目录 + * 用于持久化浏览器会话(cookies、登录状态等) + */ +export function getDefaultUserDataDir(): string { + return path.join(app.getPath("userData"), "playwright-data"); +} + +/** + * 构建 Playwright MCP Server 的命令参数 + * @param browserMode 浏览器运行模式 + * @param userDataDir 用户数据目录(可选) + */ +export function buildPlaywrightArgs( + browserMode: MCPBrowserMode = "visible", + userDataDir?: string +): string[] { + // 基础参数:使用 -y 自动确认下载 + const args: string[] = ["-y", "@playwright/mcp@latest"]; + + // headless 模式 + if (browserMode === "headless") { + args.push("--headless"); + } + + // 用户数据目录(用于持久化会话) + if (userDataDir) { + args.push("--user-data-dir", userDataDir); + } + + return args; +} + +/** + * 创建 Playwright MCP Server 配置 + * @param browserMode 浏览器运行模式 + * @param userDataDir 用户数据目录(留空则不持久化) + */ +export function createPlaywrightServerConfig( + browserMode: MCPBrowserMode = "visible", + userDataDir?: string +): MCPServerConfig { + const now = new Date().toISOString(); + + return { + id: PLAYWRIGHT_SERVER_ID, + name: "浏览器自动化", + description: "通过 Playwright 控制浏览器,支持网页操作、信息采集、自动填表等任务", + command: "npx", + args: buildPlaywrightArgs(browserMode, userDataDir), + transportType: "stdio", + enabled: false, + isBuiltin: true, + builtinType: "playwright", + browserMode, + userDataDir, + createdAt: now, + updatedAt: now, + }; +} + +/** + * 获取所有内置 Server 配置模板 + */ +export function getBuiltinServerTemplates(): MCPServerConfig[] { + return [ + createPlaywrightServerConfig("visible"), + ]; +} + +/** + * 检查是否为内置 Server + */ +export function isBuiltinServer(serverId: string): boolean { + return serverId === PLAYWRIGHT_SERVER_ID; +} + +/** + * 更新 Playwright Server 的配置 + * @param config 现有配置 + * @param browserMode 新的浏览器模式 + * @param userDataDir 新的用户数据目录(undefined 表示不修改,null 表示清除) + */ +export function updatePlaywrightConfig( + config: MCPServerConfig, + browserMode: MCPBrowserMode, + userDataDir?: string | null +): MCPServerConfig { + // 如果 userDataDir 是 undefined,保持原值;如果是 null,则清除 + const newUserDataDir = userDataDir === undefined + ? config.userDataDir + : (userDataDir ?? undefined); + + return { + ...config, + args: buildPlaywrightArgs(browserMode, newUserDataDir), + browserMode, + userDataDir: newUserDataDir, + updatedAt: new Date().toISOString(), + }; +} + +/** + * 更新 Playwright Server 的浏览器模式(向后兼容) + * @deprecated 使用 updatePlaywrightConfig 代替 + */ +export function updatePlaywrightBrowserMode( + config: MCPServerConfig, + browserMode: MCPBrowserMode +): MCPServerConfig { + return updatePlaywrightConfig(config, browserMode); +} + +/** + * 检测 Node.js 环境是否可用 + */ +export async function checkNodeEnvironment(): Promise<{ + available: boolean; + version?: string; + error?: string; +}> { + const { exec } = await import("child_process"); + const { promisify } = await import("util"); + const execAsync = promisify(exec); + + try { + const { stdout } = await execAsync("node --version"); + const version = stdout.trim(); + return { available: true, version }; + } catch (error) { + return { + available: false, + error: "Node.js 环境未检测到。请确保已安装 Node.js 并添加到系统 PATH 中。", + }; + } +} + +/** + * 检测 npx 命令是否可用 + */ +export async function checkNpxAvailable(): Promise<{ + available: boolean; + error?: string; +}> { + const { exec } = await import("child_process"); + const { promisify } = await import("util"); + const execAsync = promisify(exec); + + try { + await execAsync("npx --version"); + return { available: true }; + } catch (error) { + return { + available: false, + error: "npx 命令不可用。请确保已安装 npm 并添加到系统 PATH 中。", + }; + } +} + +/** + * 预检查 Playwright MCP Server 的运行环境 + */ +export async function preflightPlaywrightCheck(): Promise<{ + ready: boolean; + issues: string[]; + suggestions: string[]; +}> { + const issues: string[] = []; + const suggestions: string[] = []; + + // 检查 Node.js + const nodeCheck = await checkNodeEnvironment(); + if (!nodeCheck.available) { + issues.push("Node.js 未安装"); + suggestions.push("请访问 https://nodejs.org 下载并安装 Node.js"); + } + + // 检查 npx + const npxCheck = await checkNpxAvailable(); + if (!npxCheck.available) { + issues.push("npx 命令不可用"); + suggestions.push("请确保 npm 已正确安装"); + } + + return { + ready: issues.length === 0, + issues, + suggestions, + }; +} diff --git a/src/electron/libs/mcp/index.ts b/src/electron/libs/mcp/index.ts new file mode 100644 index 0000000..28e1b98 --- /dev/null +++ b/src/electron/libs/mcp/index.ts @@ -0,0 +1,71 @@ +/** + * MCP (Model Context Protocol) 模块 + * + * 提供 MCP Server 配置管理功能 + * 注意:MCP Server 进程由 Claude SDK 自动管理 + * + * @module mcp + * + * 模块结构: + * - mcp-config: 类型定义和常量 + * - mcp-store: 配置持久化存储 + * - mcp-manager: 配置管理器 + * - mcp-ipc-handlers: IPC 处理器 + * - builtin-servers: 内置 Server 配置 + */ + +// ============ 类型定义 ============ +export type { + MCPServerStatus, + MCPBrowserMode, + MCPTransportType, + MCPServerConfig, + MCPServerRuntimeState, + MCPConfigState, + MCPGlobalSettings, + MCPServerInfo, + MCPConfigChangeEvent, + MCPToolInfo, + MCPServerTools, +} from "./mcp-config.js"; + +export { + DEFAULT_GLOBAL_SETTINGS, + DEFAULT_MCP_CONFIG_STATE, +} from "./mcp-config.js"; + +// ============ 配置存储 ============ +export { + loadMCPConfig, + saveMCPConfig, + getMCPServerById, + addMCPServer, + updateMCPServer, + removeMCPServer, + toggleMCPServer, + updateGlobalSettings, + getEnabledServers, + generateServerId, +} from "./mcp-store.js"; + +// ============ 配置管理 ============ +export { MCPManager, getMCPManager } from "./mcp-manager.js"; + +// ============ 内置 Server ============ +export type { BuiltinServerType } from "./builtin-servers.js"; +export { + PLAYWRIGHT_SERVER_ID, + createPlaywrightServerConfig, + updatePlaywrightConfig, + getDefaultUserDataDir, + buildPlaywrightArgs, + getBuiltinServerTemplates, + isBuiltinServer, + updatePlaywrightBrowserMode, + checkNodeEnvironment, + checkNpxAvailable, + preflightPlaywrightCheck, +} from "./builtin-servers.js"; + +// ============ IPC 处理器 ============ +export { setupMCPHandlers, cleanupMCP } from "./mcp-ipc-handlers.js"; diff --git a/src/electron/libs/mcp/mcp-config.ts b/src/electron/libs/mcp/mcp-config.ts new file mode 100644 index 0000000..5eafc82 --- /dev/null +++ b/src/electron/libs/mcp/mcp-config.ts @@ -0,0 +1,129 @@ +/** + * MCP 配置管理核心模块 - 类型定义 + * 用于定义 MCP Server 配置的 TypeScript 接口 + */ + +/** MCP Server 运行状态 */ +export type MCPServerStatus = "running" | "stopped" | "error" | "starting"; + +/** MCP Server 运行模式(针对浏览器工具) */ +export type MCPBrowserMode = "visible" | "headless"; + +/** MCP Server 传输类型 */ +export type MCPTransportType = "stdio" | "sse"; + +/** 单个 MCP Server 配置 */ +export interface MCPServerConfig { + /** 唯一标识符 */ + id: string; + /** 显示名称 */ + name: string; + /** 描述信息 */ + description?: string; + /** 启动命令 */ + command: string; + /** 命令参数 */ + args?: string[]; + /** 环境变量 */ + env?: Record; + /** 传输类型 */ + transportType: MCPTransportType; + /** 是否已启用 */ + enabled: boolean; + /** 是否为内置工具 */ + isBuiltin?: boolean; + /** 内置工具类型(如 playwright) */ + builtinType?: string; + /** 浏览器运行模式(仅对浏览器工具有效) */ + browserMode?: MCPBrowserMode; + /** 用户数据目录(用于持久化浏览器会话,仅对浏览器工具有效) */ + userDataDir?: string; + /** 创建时间 */ + createdAt: string; + /** 更新时间 */ + updatedAt: string; +} + +/** MCP Server 运行时状态 */ +export interface MCPServerRuntimeState { + /** Server ID */ + serverId: string; + /** 运行状态 */ + status: MCPServerStatus; + /** 错误信息(如果有) */ + errorMessage?: string; + /** 错误详情/日志 */ + errorDetails?: string; + /** 进程 PID(如果正在运行) */ + pid?: number; + /** 重启次数 */ + restartCount: number; + /** 最后一次启动时间 */ + lastStartTime?: string; + /** 最后一次停止时间 */ + lastStopTime?: string; +} + +/** MCP 配置整体状态 */ +export interface MCPConfigState { + /** 配置版本号(用于迁移) */ + version: number; + /** 所有 MCP Server 配置列表 */ + servers: MCPServerConfig[]; + /** 全局设置 */ + globalSettings: MCPGlobalSettings; +} + +/** MCP 全局设置 */ +export interface MCPGlobalSettings { + /** 是否在应用启动时自动启动已启用的 Server */ + autoStartOnLaunch: boolean; + /** Server 启动超时时间(毫秒) */ + startupTimeout: number; + /** 最大自动重启次数 */ + maxRestartAttempts: number; +} + +/** 默认全局设置 */ +export const DEFAULT_GLOBAL_SETTINGS: MCPGlobalSettings = { + autoStartOnLaunch: true, + startupTimeout: 30000, // 30秒 + maxRestartAttempts: 3, +}; + +/** 默认配置状态 */ +export const DEFAULT_MCP_CONFIG_STATE: MCPConfigState = { + version: 1, + servers: [], + globalSettings: DEFAULT_GLOBAL_SETTINGS, +}; + +/** 用于 IPC 通信的 MCP Server 状态信息 */ +export interface MCPServerInfo { + config: MCPServerConfig; + runtime: MCPServerRuntimeState; +} + +/** MCP 配置变更事件类型 */ +export type MCPConfigChangeEvent = + | { type: "server-added"; server: MCPServerConfig } + | { type: "server-updated"; server: MCPServerConfig } + | { type: "server-removed"; serverId: string } + | { type: "server-status-changed"; serverId: string; status: MCPServerStatus; error?: string } + | { type: "global-settings-updated"; settings: MCPGlobalSettings }; + +/** MCP 工具信息(从 MCP Server 获取) */ +export interface MCPToolInfo { + /** 工具名称 */ + name: string; + /** 工具描述 */ + description?: string; + /** 输入参数 Schema */ + inputSchema?: object; +} + +/** MCP Server 提供的工具列表 */ +export interface MCPServerTools { + serverId: string; + tools: MCPToolInfo[]; +} diff --git a/src/electron/libs/mcp/mcp-ipc-handlers.ts b/src/electron/libs/mcp/mcp-ipc-handlers.ts new file mode 100644 index 0000000..61b2f74 --- /dev/null +++ b/src/electron/libs/mcp/mcp-ipc-handlers.ts @@ -0,0 +1,280 @@ +/** + * MCP IPC 处理器 + * 处理渲染进程与主进程之间的 MCP 配置相关通信 + * 注意:MCP Server 进程由 Claude SDK 自动管理,这里只处理配置 + */ + +import { BrowserWindow, ipcMain } from "electron"; +import { getMCPManager, MCPManager } from "./mcp-manager.js"; +import { + addMCPServer, + updateMCPServer, + removeMCPServer, + toggleMCPServer, + getMCPServerById, + generateServerId, +} from "./mcp-store.js"; +import { + createPlaywrightServerConfig, + updatePlaywrightConfig, + getDefaultUserDataDir, + PLAYWRIGHT_SERVER_ID, + preflightPlaywrightCheck, +} from "./builtin-servers.js"; +import type { MCPBrowserMode } from "./mcp-config.js"; +import type { + MCPServerConfig, + MCPTransportType, +} from "./mcp-config.js"; + +let mainWindow: BrowserWindow | null = null; +let manager: MCPManager | null = null; + +/** IPC 响应用的 Server 信息(扁平化结构,便于前端使用) */ +interface IPCServerInfo { + id: string; + name: string; + description?: string; + command: string; + args?: string[]; + env?: Record; + transportType: MCPTransportType; + enabled: boolean; + isBuiltin?: boolean; + builtinType?: string; + browserMode?: "visible" | "headless"; + /** 用户数据目录(用于持久化浏览器会话) */ + userDataDir?: string; +} + +/** + * 将 Server 配置转换为 IPC 可传输的信息 + */ +function toServerInfo(config: MCPServerConfig): IPCServerInfo { + return { + id: config.id, + name: config.name, + description: config.description, + command: config.command, + args: config.args, + env: config.env, + transportType: config.transportType, + enabled: config.enabled, + isBuiltin: config.isBuiltin, + builtinType: config.builtinType, + browserMode: config.browserMode, + userDataDir: config.userDataDir, + }; +} + +/** + * 设置 MCP IPC 处理器 + */ +export function setupMCPHandlers(win: BrowserWindow): void { + mainWindow = win; + manager = getMCPManager(); + + // 监听配置变化并广播到渲染进程 + manager.on("config-changed", () => { + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.webContents.send("mcp-config-changed"); + } + }); + + // 获取所有 MCP Server + ipcMain.handle("mcp-get-servers", async () => { + if (!manager) return []; + + const config = manager.getConfig(); + return config.servers.map((s: MCPServerConfig) => toServerInfo(s)); + }); + + // 启用 MCP Server + ipcMain.handle("mcp-enable-server", async (_, serverId: string) => { + if (!manager) throw new Error("MCP Manager not initialized"); + + const config = manager.getConfig(); + const server = getMCPServerById(config, serverId); + + if (!server) { + throw new Error(`Server not found: ${serverId}`); + } + + // 更新配置(SDK 会在下次对话时自动启动) + const newConfig = toggleMCPServer(config, serverId, true); + manager.updateConfig(newConfig); + + return { success: true }; + }); + + // 禁用 MCP Server + ipcMain.handle("mcp-disable-server", async (_, serverId: string) => { + if (!manager) throw new Error("MCP Manager not initialized"); + + // 更新配置 + const config = manager.getConfig(); + const newConfig = toggleMCPServer(config, serverId, false); + manager.updateConfig(newConfig); + + return { success: true }; + }); + + // 一键启用浏览器自动化 + ipcMain.handle("mcp-enable-browser-automation", async () => { + if (!manager) throw new Error("MCP Manager not initialized"); + + // 预检查环境 + const preflight = await preflightPlaywrightCheck(); + if (!preflight.ready) { + throw new Error(`环境检查失败: ${preflight.issues.join(", ")}\n建议: ${preflight.suggestions.join(", ")}`); + } + + let config = manager.getConfig(); + + // 检查是否已存在 Playwright Server + let server = getMCPServerById(config, PLAYWRIGHT_SERVER_ID); + + if (!server) { + // 创建新的 Playwright Server 配置 + const playwrightConfig = createPlaywrightServerConfig("visible"); + config = addMCPServer(config, playwrightConfig); + server = playwrightConfig; + } + + // 启用并保存配置(SDK 会在下次对话时自动启动) + config = toggleMCPServer(config, PLAYWRIGHT_SERVER_ID, true); + manager.updateConfig(config); + + return { success: true }; + }); + + // 添加新的 MCP Server + ipcMain.handle("mcp-add-server", async (_, serverConfig: { + name: string; + description?: string; + command: string; + args?: string[]; + env?: Record; + transportType: "stdio" | "sse"; + }) => { + if (!manager) throw new Error("MCP Manager not initialized"); + + const serverId = generateServerId(); + + const newServer: Omit = { + id: serverId, + name: serverConfig.name, + description: serverConfig.description, + command: serverConfig.command, + args: serverConfig.args, + env: serverConfig.env, + transportType: serverConfig.transportType as MCPTransportType, + enabled: false, + isBuiltin: false, + }; + + let config = manager.getConfig(); + config = addMCPServer(config, newServer); + manager.updateConfig(config); + + return { success: true, serverId }; + }); + + // 更新 MCP Server 配置 + ipcMain.handle("mcp-update-server", async (_, serverId: string, updates: { + name?: string; + description?: string; + command?: string; + args?: string[]; + env?: Record; + transportType?: "stdio" | "sse"; + }) => { + if (!manager) throw new Error("MCP Manager not initialized"); + + let config = manager.getConfig(); + const server = getMCPServerById(config, serverId); + + if (!server) { + throw new Error(`Server not found: ${serverId}`); + } + + // 更新配置 + config = updateMCPServer(config, serverId, updates as Partial); + manager.updateConfig(config); + + return { success: true }; + }); + + // 删除 MCP Server + ipcMain.handle("mcp-delete-server", async (_, serverId: string) => { + if (!manager) throw new Error("MCP Manager not initialized"); + + let config = manager.getConfig(); + const server = getMCPServerById(config, serverId); + + if (!server) { + throw new Error(`Server not found: ${serverId}`); + } + + // 不允许删除内置 Server + if (server.isBuiltin) { + throw new Error("Cannot delete builtin server"); + } + + // 从配置中移除 + config = removeMCPServer(config, serverId); + manager.updateConfig(config); + + return { success: true }; + }); + + // 更新浏览器自动化配置(headless 模式和持久化会话) + ipcMain.handle("mcp-update-browser-config", async (_, options: { + browserMode?: MCPBrowserMode; + userDataDir?: string | null; // null 表示清除 + enablePersistence?: boolean; // 便捷选项:是否启用持久化 + }) => { + if (!manager) throw new Error("MCP Manager not initialized"); + + let config = manager.getConfig(); + const server = getMCPServerById(config, PLAYWRIGHT_SERVER_ID); + + if (!server) { + throw new Error("Playwright server not found. Please enable browser automation first."); + } + + // 处理 enablePersistence 便捷选项 + let userDataDir = options.userDataDir; + if (options.enablePersistence === true && !userDataDir) { + userDataDir = getDefaultUserDataDir(); + } else if (options.enablePersistence === false) { + userDataDir = null; // 清除持久化 + } + + // 更新 Playwright 配置 + const browserMode = options.browserMode ?? server.browserMode ?? "visible"; + const updatedServer = updatePlaywrightConfig(server, browserMode, userDataDir); + + // 更新配置 + config = updateMCPServer(config, PLAYWRIGHT_SERVER_ID, updatedServer); + manager.updateConfig(config); + + return { + success: true, + browserMode: updatedServer.browserMode, + userDataDir: updatedServer.userDataDir, + }; + }); + + // 获取默认的用户数据目录 + ipcMain.handle("mcp-get-default-user-data-dir", async () => { + return getDefaultUserDataDir(); + }); +} + +/** + * 清理 MCP 资源 + */ +export function cleanupMCP(): void { + mainWindow = null; +} diff --git a/src/electron/libs/mcp/mcp-manager.ts b/src/electron/libs/mcp/mcp-manager.ts new file mode 100644 index 0000000..7ddb60c --- /dev/null +++ b/src/electron/libs/mcp/mcp-manager.ts @@ -0,0 +1,118 @@ +/** + * MCP 配置管理器 + * 负责 MCP Server 配置的管理(启动由 Claude SDK 自动处理) + */ + +import { EventEmitter } from "events"; +import { + MCPServerConfig, + MCPConfigState, + MCPConfigChangeEvent, +} from "./mcp-config.js"; +import { loadMCPConfig, saveMCPConfig, getEnabledServers } from "./mcp-store.js"; + +/** MCP Manager 事件类型 */ +export interface MCPManagerEvents { + "config-changed": (event: MCPConfigChangeEvent) => void; +} + +/** + * MCP 配置管理器 + * 单例模式,管理 MCP Server 配置 + * 注意:Server 进程由 Claude SDK 自动启动和管理 + */ +export class MCPManager extends EventEmitter { + private static instance: MCPManager | null = null; + + /** 当前配置 */ + private config: MCPConfigState; + + private constructor() { + super(); + this.config = loadMCPConfig(); + } + + /** 获取单例实例 */ + public static getInstance(): MCPManager { + if (!MCPManager.instance) { + MCPManager.instance = new MCPManager(); + } + return MCPManager.instance; + } + + /** 重置实例(主要用于测试) */ + public static resetInstance(): void { + MCPManager.instance = null; + } + + /** 获取当前配置 */ + public getConfig(): MCPConfigState { + return this.config; + } + + /** 重新加载配置 */ + public reloadConfig(): void { + this.config = loadMCPConfig(); + } + + /** 保存配置 */ + public saveConfig(): void { + saveMCPConfig(this.config); + } + + /** 更新配置 */ + public updateConfig(newConfig: MCPConfigState): void { + this.config = newConfig; + this.saveConfig(); + this.emit("config-changed", { type: "config-updated" }); + } + + /** 获取已启用的 Servers */ + public getEnabledServers(): MCPServerConfig[] { + return getEnabledServers(this.config); + } + + /** + * 构建用于 Claude SDK 的 MCP Servers 配置 + * 返回格式符合 SDK 的 mcpServers 选项 + */ + public buildSDKConfig(): Record + }> { + const mcpServers: Record + }> = {}; + + for (const server of this.config.servers) { + if (!server.enabled) continue; + + // 只支持 stdio 类型 + if (server.transportType !== 'stdio') { + console.log(`[mcp-manager] Skipping server ${server.id}: unsupported transport type ${server.transportType}`); + continue; + } + + mcpServers[server.id] = { + type: 'stdio', + command: server.command, + args: server.args, + env: server.env, + }; + + console.log(`[mcp-manager] Configured server: ${server.id} (${server.name})`); + } + + return mcpServers; + } +} + +/** 导出单例获取函数 */ +export function getMCPManager(): MCPManager { + return MCPManager.getInstance(); +} diff --git a/src/electron/libs/mcp/mcp-store.ts b/src/electron/libs/mcp/mcp-store.ts new file mode 100644 index 0000000..8d3817c --- /dev/null +++ b/src/electron/libs/mcp/mcp-store.ts @@ -0,0 +1,246 @@ +/** + * MCP 配置存储模块 + * 负责 MCP 配置的读取、保存、校验逻辑 + */ + +import { app } from "electron"; +import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs"; +import { join } from "path"; +import { + MCPConfigState, + MCPServerConfig, + MCPGlobalSettings, + MCPBrowserMode, + DEFAULT_MCP_CONFIG_STATE, + DEFAULT_GLOBAL_SETTINGS, +} from "./mcp-config.js"; +import { + PLAYWRIGHT_SERVER_ID, + createPlaywrightServerConfig, +} from "./builtin-servers.js"; + +const CONFIG_FILE_NAME = "mcp-config.json"; + +/** 获取配置文件路径 */ +function getConfigPath(): string { + const userDataPath = app.getPath("userData"); + return join(userDataPath, CONFIG_FILE_NAME); +} + +/** 验证 MCP Server 配置格式 */ +function validateServerConfig(server: Partial): server is MCPServerConfig { + return !!( + server.id && + server.name && + server.command && + server.transportType && + typeof server.enabled === "boolean" + ); +} + +/** 验证整体配置格式 */ +function validateConfigState(config: unknown): config is MCPConfigState { + if (!config || typeof config !== "object") return false; + const c = config as MCPConfigState; + + if (typeof c.version !== "number") return false; + if (!Array.isArray(c.servers)) return false; + if (!c.globalSettings || typeof c.globalSettings !== "object") return false; + + // 验证每个 server 配置 + for (const server of c.servers) { + if (!validateServerConfig(server)) return false; + } + + return true; +} + +/** 迁移旧版本配置(未来扩展用) */ +function migrateConfig(config: MCPConfigState): MCPConfigState { + // 目前版本为 1,无需迁移 + // 未来如果配置格式变化,在这里处理迁移逻辑 + return config; +} + +/** + * 修复内置 Server 配置 + * 确保内置 Server 始终使用最新的命令和参数格式 + */ +function fixBuiltinServerConfigs(config: MCPConfigState): MCPConfigState { + const fixedServers = config.servers.map((server: MCPServerConfig) => { + // 修复 Playwright 内置 Server + if (server.id === PLAYWRIGHT_SERVER_ID || server.builtinType === "playwright") { + const browserMode: MCPBrowserMode = server.browserMode || "visible"; + const latestConfig = createPlaywrightServerConfig(browserMode); + + // 保留用户的启用状态和时间戳,但更新命令和参数 + return { + ...latestConfig, + enabled: server.enabled, + createdAt: server.createdAt || latestConfig.createdAt, + updatedAt: latestConfig.updatedAt, + }; + } + + return server; + }); + + return { + ...config, + servers: fixedServers, + }; +} + +/** + * 加载 MCP 配置 + * @returns 配置对象,如果配置不存在或损坏则返回默认配置 + */ +export function loadMCPConfig(): MCPConfigState { + try { + const configPath = getConfigPath(); + + if (!existsSync(configPath)) { + console.info("[mcp-store] Config file not found, using default config"); + return { ...DEFAULT_MCP_CONFIG_STATE }; + } + + const raw = readFileSync(configPath, "utf8"); + const config = JSON.parse(raw); + + if (!validateConfigState(config)) { + console.warn("[mcp-store] Invalid config format, using default config"); + return { ...DEFAULT_MCP_CONFIG_STATE }; + } + + // 迁移配置(如果需要) + const migratedConfig = migrateConfig(config); + + // 修复内置 Server 配置(确保使用最新的命令格式) + const fixedConfig = fixBuiltinServerConfigs(migratedConfig); + + // 确保全局设置包含所有字段(兼容旧配置) + fixedConfig.globalSettings = { + ...DEFAULT_GLOBAL_SETTINGS, + ...fixedConfig.globalSettings, + }; + + console.info(`[mcp-store] Loaded MCP config with ${fixedConfig.servers.length} servers`); + return fixedConfig; + } catch (error) { + console.error("[mcp-store] Failed to load MCP config:", error); + return { ...DEFAULT_MCP_CONFIG_STATE }; + } +} + +/** + * 保存 MCP 配置 + * @param config 配置对象 + */ +export function saveMCPConfig(config: MCPConfigState): void { + try { + const configPath = getConfigPath(); + const userDataPath = app.getPath("userData"); + + // 确保目录存在 + if (!existsSync(userDataPath)) { + mkdirSync(userDataPath, { recursive: true }); + } + + // 更新服务器配置的更新时间 + const updatedConfig = { + ...config, + servers: config.servers.map((server: MCPServerConfig) => ({ + ...server, + updatedAt: new Date().toISOString(), + })), + }; + + writeFileSync(configPath, JSON.stringify(updatedConfig, null, 2), "utf8"); + console.info("[mcp-store] MCP config saved successfully"); + } catch (error) { + console.error("[mcp-store] Failed to save MCP config:", error); + throw error; + } +} + +/** + * 获取指定 ID 的 MCP Server 配置 + */ +export function getMCPServerById(config: MCPConfigState, serverId: string): MCPServerConfig | undefined { + return config.servers.find((s: MCPServerConfig) => s.id === serverId); +} + +/** + * 添加新的 MCP Server 配置 + */ +export function addMCPServer(config: MCPConfigState, server: Omit): MCPConfigState { + const now = new Date().toISOString(); + const newServer: MCPServerConfig = { + ...server, + createdAt: now, + updatedAt: now, + }; + + return { + ...config, + servers: [...config.servers, newServer], + }; +} + +/** + * 更新 MCP Server 配置 + */ +export function updateMCPServer(config: MCPConfigState, serverId: string, updates: Partial): MCPConfigState { + return { + ...config, + servers: config.servers.map((s: MCPServerConfig) => + s.id === serverId + ? { ...s, ...updates, updatedAt: new Date().toISOString() } + : s + ), + }; +} + +/** + * 删除 MCP Server 配置 + */ +export function removeMCPServer(config: MCPConfigState, serverId: string): MCPConfigState { + return { + ...config, + servers: config.servers.filter((s: MCPServerConfig) => s.id !== serverId), + }; +} + +/** + * 启用/禁用 MCP Server + */ +export function toggleMCPServer(config: MCPConfigState, serverId: string, enabled: boolean): MCPConfigState { + return updateMCPServer(config, serverId, { enabled }); +} + +/** + * 更新全局设置 + */ +export function updateGlobalSettings(config: MCPConfigState, settings: Partial): MCPConfigState { + return { + ...config, + globalSettings: { + ...config.globalSettings, + ...settings, + }, + }; +} + +/** + * 获取所有已启用的 MCP Server + */ +export function getEnabledServers(config: MCPConfigState): MCPServerConfig[] { + return config.servers.filter((s: MCPServerConfig) => s.enabled); +} + +/** + * 生成唯一的 Server ID + */ +export function generateServerId(): string { + return `mcp-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`; +} diff --git a/src/electron/libs/runner.ts b/src/electron/libs/runner.ts index 63c5049..35e49f9 100644 --- a/src/electron/libs/runner.ts +++ b/src/electron/libs/runner.ts @@ -2,8 +2,9 @@ import { query, type SDKMessage, type PermissionResult } from "@anthropic-ai/cla import type { ServerEvent } from "../types.js"; import type { Session } from "./session-store.js"; -import { getCurrentApiConfig, buildEnvForConfig, getClaudeCodePath} from "./claude-settings.js"; +import { getCurrentApiConfig, buildEnvForConfig, getClaudeCodePath } from "./claude-settings.js"; import { getEnhancedEnv } from "./util.js"; +import { getMCPManager } from "./mcp/mcp-manager.js"; export type RunnerOptions = { @@ -44,7 +45,7 @@ export async function runClaude(options: RunnerOptions): Promise { try { // 获取当前配置 const config = getCurrentApiConfig(); - + if (!config) { onEvent({ type: "session.status", @@ -52,14 +53,25 @@ export async function runClaude(options: RunnerOptions): Promise { }); return; } - + // 使用 Anthropic SDK const env = buildEnvForConfig(config); const mergedEnv = { ...getEnhancedEnv(), ...env }; - + + // 构建 MCP Servers 配置(使用 MCP Manager) + const manager = getMCPManager(); + const mcpServers = manager.buildSDKConfig(); + const mcpServerCount = Object.keys(mcpServers).length; + + if (mcpServerCount > 0) { + console.log(`[MCP] Configured ${mcpServerCount} MCP server(s) for Claude SDK:`, Object.keys(mcpServers)); + } else { + console.log('[MCP] No MCP servers configured'); + } + const q = query({ prompt, options: { @@ -71,6 +83,8 @@ export async function runClaude(options: RunnerOptions): Promise { permissionMode: "bypassPermissions", includePartialMessages: true, allowDangerouslySkipPermissions: true, + // 注入 MCP Servers 配置 + mcpServers: mcpServerCount > 0 ? mcpServers : undefined, canUseTool: async (toolName, input, { signal }) => { // For AskUserQuestion, we need to wait for user response if (toolName === "AskUserQuestion") { @@ -99,7 +113,7 @@ export async function runClaude(options: RunnerOptions): Promise { }); } - // Auto-approve other tools + // Auto-approve all other tools (including MCP tools) return { behavior: "allow", updatedInput: input }; } } diff --git a/src/electron/main.ts b/src/electron/main.ts index a95a611..fc21960 100644 --- a/src/electron/main.ts +++ b/src/electron/main.ts @@ -9,6 +9,7 @@ import { saveApiConfig } from "./libs/config-store.js"; import { getCurrentApiConfig } from "./libs/claude-settings.js"; import type { ClientEvent } from "./types.js"; import "./libs/claude-settings.js"; +import { setupMCPHandlers, cleanupMCP } from "./libs/mcp/mcp-ipc-handlers.js"; let cleanupComplete = false; let mainWindow: BrowserWindow | null = null; @@ -33,6 +34,7 @@ function cleanup(): void { globalShortcut.unregisterAll(); stopPolling(); cleanupAllSessions(); + cleanupMCP(); killViteDevServer(); } @@ -129,10 +131,13 @@ app.on("ready", () => { saveApiConfig(config); return { success: true }; } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : String(error) + return { + success: false, + error: error instanceof Error ? error.message : String(error) }; } }); + + // Setup MCP handlers + setupMCPHandlers(mainWindow!); }) diff --git a/src/electron/preload.cts b/src/electron/preload.cts index af70454..7b96264 100644 --- a/src/electron/preload.cts +++ b/src/electron/preload.cts @@ -6,7 +6,7 @@ electron.contextBridge.exposeInMainWorld("electron", { callback(stats); }), getStaticData: () => ipcInvoke("getStaticData"), - + // Claude Agent IPC APIs sendClientEvent: (event: any) => { electron.ipcRenderer.send("client-event", event); @@ -23,18 +23,41 @@ electron.contextBridge.exposeInMainWorld("electron", { electron.ipcRenderer.on("server-event", cb); return () => electron.ipcRenderer.off("server-event", cb); }, - generateSessionTitle: (userInput: string | null) => + generateSessionTitle: (userInput: string | null) => ipcInvoke("generate-session-title", userInput), - getRecentCwds: (limit?: number) => + getRecentCwds: (limit?: number) => ipcInvoke("get-recent-cwds", limit), - selectDirectory: () => + selectDirectory: () => ipcInvoke("select-directory"), - getApiConfig: () => + getApiConfig: () => ipcInvoke("get-api-config"), - saveApiConfig: (config: any) => + saveApiConfig: (config: any) => ipcInvoke("save-api-config", config), checkApiConfig: () => - ipcInvoke("check-api-config") + ipcInvoke("check-api-config"), + + // MCP APIs + getMCPServers: () => + ipcInvoke("mcp-get-servers"), + enableMCPServer: (serverId: string) => + ipcInvoke("mcp-enable-server", serverId), + disableMCPServer: (serverId: string) => + ipcInvoke("mcp-disable-server", serverId), + enableBrowserAutomation: () => + ipcInvoke("mcp-enable-browser-automation"), + addMCPServer: (config: any) => + ipcInvoke("mcp-add-server", config), + updateMCPServer: (serverId: string, config: any) => + ipcInvoke("mcp-update-server", serverId, config), + deleteMCPServer: (serverId: string) => + ipcInvoke("mcp-delete-server", serverId), + onMCPStatusChange: (callback: (serverId: string, status: MCPServerStatus, error?: string) => void) => { + const cb = (_: Electron.IpcRendererEvent, serverId: string, status: MCPServerStatus, error?: string) => { + callback(serverId, status, error); + }; + electron.ipcRenderer.on("mcp-status-change", cb); + return () => electron.ipcRenderer.off("mcp-status-change", cb); + } } satisfies Window['electron']) function ipcInvoke(key: Key, ...args: any[]): Promise { diff --git a/src/ui/components/APIConfigPanel.tsx b/src/ui/components/APIConfigPanel.tsx new file mode 100644 index 0000000..5243da2 --- /dev/null +++ b/src/ui/components/APIConfigPanel.tsx @@ -0,0 +1,171 @@ +/** + * API 配置面板组件 + * 从 SettingsModal 中抽取的 API 配置功能 + */ + +import { useEffect, useState } from "react"; + +interface APIConfigPanelProps { + onSuccess?: () => void; +} + +export function APIConfigPanel({ onSuccess }: APIConfigPanelProps) { + const [apiKey, setApiKey] = useState(""); + const [baseURL, setBaseURL] = useState(""); + const [model, setModel] = useState(""); + const [loading, setLoading] = useState(false); + const [saving, setSaving] = useState(false); + const [error, setError] = useState(null); + const [success, setSuccess] = useState(false); + + useEffect(() => { + // 加载当前配置 + setLoading(true); + window.electron.getApiConfig() + .then((config) => { + if (config) { + setApiKey(config.apiKey); + setBaseURL(config.baseURL); + setModel(config.model); + } + }) + .catch((err) => { + console.error("Failed to load API config:", err); + setError("加载配置失败"); + }) + .finally(() => { + setLoading(false); + }); + }, []); + + const handleSave = async () => { + // 验证输入 + if (!apiKey.trim()) { + setError("请输入 API Key"); + return; + } + if (!baseURL.trim()) { + setError("请输入 Base URL"); + return; + } + if (!model.trim()) { + setError("请输入模型名称"); + return; + } + + // 验证 URL 格式 + try { + new URL(baseURL); + } catch { + setError("Base URL 格式不正确"); + return; + } + + setError(null); + setSaving(true); + + try { + const result = await window.electron.saveApiConfig({ + apiKey: apiKey.trim(), + baseURL: baseURL.trim(), + model: model.trim(), + apiType: "anthropic" + }); + + if (result.success) { + setSuccess(true); + setTimeout(() => { + setSuccess(false); + onSuccess?.(); + }, 1500); + } else { + setError(result.error || "保存配置失败"); + } + } catch (err) { + console.error("Failed to save API config:", err); + setError("保存配置失败"); + } finally { + setSaving(false); + } + }; + + if (loading) { + return ( +
+ +
+ ); + } + + return ( +
+

+ 支持 Anthropic 官方 API 以及兼容 Anthropic 格式的第三方 API。 +

+ + + + + + + + {error && ( +
+ {error} +
+ )} + + {success && ( +
+ 配置保存成功! +
+ )} + + +
+ ); +} diff --git a/src/ui/components/MCPErrorGuide.tsx b/src/ui/components/MCPErrorGuide.tsx new file mode 100644 index 0000000..221defc --- /dev/null +++ b/src/ui/components/MCPErrorGuide.tsx @@ -0,0 +1,214 @@ +/** + * MCP 错误引导组件 + * 展示错误详情和解决建议 + */ + +import { useState } from "react"; + +interface MCPErrorGuideProps { + /** 错误类型 */ + errorType: "node-not-found" | "network" | "timeout" | "server-crash" | "unknown"; + /** 错误消息 */ + errorMessage?: string; + /** 错误详情 */ + errorDetails?: string; + /** 重试回调 */ + onRetry?: () => void; + /** 关闭回调 */ + onClose?: () => void; +} + +/** 错误类型配置 */ +const ERROR_CONFIGS: Record = { + "node-not-found": { + title: "Node.js 环境未检测到", + description: "MCP 工具需要 Node.js 环境才能运行。", + suggestions: [ + "请访问 https://nodejs.org 下载并安装 Node.js", + "安装完成后,请重启应用", + "确保 Node.js 已添加到系统 PATH 环境变量", + ], + helpUrl: "https://nodejs.org/", + }, + "network": { + title: "网络连接问题", + description: "无法下载 MCP 工具所需的依赖包。", + suggestions: [ + "请检查您的网络连接是否正常", + "如果您使用代理,请确保代理设置正确", + "尝试稍后重试", + ], + }, + "timeout": { + title: "启动超时", + description: "MCP Server 启动时间过长,已自动终止。", + suggestions: [ + "这可能是由于网络缓慢或首次下载依赖导致", + "请检查网络连接后重试", + "如果问题持续,请查看错误详情获取更多信息", + ], + }, + "server-crash": { + title: "服务异常退出", + description: "MCP Server 意外崩溃,已尝试自动重启但失败。", + suggestions: [ + "请检查错误详情获取更多信息", + "尝试禁用后重新启用该工具", + "如果问题持续,请检查系统资源是否充足", + ], + }, + "unknown": { + title: "发生错误", + description: "MCP 工具遇到了一个问题。", + suggestions: [ + "请查看错误详情获取更多信息", + "尝试重新启用该工具", + "如果问题持续,请联系技术支持", + ], + }, +}; + +export function MCPErrorGuide({ + errorType, + errorMessage, + errorDetails, + onRetry, + onClose, +}: MCPErrorGuideProps) { + const [showDetails, setShowDetails] = useState(false); + + const config = ERROR_CONFIGS[errorType] || ERROR_CONFIGS["unknown"]; + + return ( +
+ {/* 头部 */} +
+
+ + + + + +
+
+

{config.title}

+

{config.description}

+ + {/* 错误消息 */} + {errorMessage && ( +

+ {errorMessage} +

+ )} +
+ + {/* 关闭按钮 */} + {onClose && ( + + )} +
+ + {/* 解决建议 */} +
+

解决建议:

+
    + {config.suggestions.map((suggestion, index) => ( +
  • + + {index + 1} + + {suggestion} +
  • + ))} +
+
+ + {/* 错误详情(可折叠) */} + {errorDetails && ( +
+ + + {showDetails && ( +
+                            {errorDetails}
+                        
+ )} +
+ )} + + {/* 操作按钮 */} +
+ {onRetry && ( + + )} + + {config.helpUrl && ( + + 获取帮助 → + + )} +
+
+ ); +} + +/** + * 根据错误消息推断错误类型 + */ +export function inferErrorType(errorMessage: string): MCPErrorGuideProps["errorType"] { + const msg = errorMessage.toLowerCase(); + + if (msg.includes("node") && (msg.includes("not found") || msg.includes("未检测"))) { + return "node-not-found"; + } + + if (msg.includes("network") || msg.includes("enotfound") || msg.includes("econnrefused")) { + return "network"; + } + + if (msg.includes("timeout") || msg.includes("超时")) { + return "timeout"; + } + + if (msg.includes("crash") || msg.includes("exit") || msg.includes("崩溃")) { + return "server-crash"; + } + + return "unknown"; +} diff --git a/src/ui/components/MCPServerForm.tsx b/src/ui/components/MCPServerForm.tsx new file mode 100644 index 0000000..e927709 --- /dev/null +++ b/src/ui/components/MCPServerForm.tsx @@ -0,0 +1,365 @@ +/** + * MCP Server 配置表单组件 + * 支持添加和编辑 MCP Server 配置 + */ + +import { useState, useEffect } from "react"; + +/** 表单数据类型 */ +interface MCPServerFormData { + name: string; + description: string; + command: string; + args: string; + envVars: string; + transportType: "stdio" | "sse"; +} + +/** 表单验证错误 */ +interface FormErrors { + name?: string; + command?: string; + args?: string; + envVars?: string; +} + +interface MCPServerFormProps { + /** 编辑模式时传入的初始数据 */ + initialData?: { + id: string; + name: string; + description?: string; + command: string; + args?: string[]; + env?: Record; + transportType: "stdio" | "sse"; + }; + /** 关闭表单 */ + onClose: () => void; + /** 保存成功回调 */ + onSave: (data: { + name: string; + description?: string; + command: string; + args?: string[]; + env?: Record; + transportType: "stdio" | "sse"; + }) => Promise; + /** 删除回调(编辑模式) */ + onDelete?: () => Promise; +} + +export function MCPServerForm({ initialData, onClose, onSave, onDelete }: MCPServerFormProps) { + const isEditMode = !!initialData; + + const [formData, setFormData] = useState({ + name: initialData?.name || "", + description: initialData?.description || "", + command: initialData?.command || "", + args: initialData?.args?.join(" ") || "", + envVars: initialData?.env + ? Object.entries(initialData.env).map(([k, v]) => `${k}=${v}`).join("\n") + : "", + transportType: initialData?.transportType || "stdio", + }); + + const [errors, setErrors] = useState({}); + const [saving, setSaving] = useState(false); + const [deleting, setDeleting] = useState(false); + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); + + /** 验证表单 */ + const validateForm = (): boolean => { + const newErrors: FormErrors = {}; + + if (!formData.name.trim()) { + newErrors.name = "请输入工具名称"; + } + + if (!formData.command.trim()) { + newErrors.command = "请输入启动命令"; + } + + // 验证环境变量格式 + if (formData.envVars.trim()) { + const lines = formData.envVars.trim().split("\n"); + for (const line of lines) { + if (line.trim() && !line.includes("=")) { + newErrors.envVars = "环境变量格式不正确,每行应为 KEY=VALUE 格式"; + break; + } + } + } + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + /** 解析环境变量字符串 */ + const parseEnvVars = (str: string): Record | undefined => { + if (!str.trim()) return undefined; + + const result: Record = {}; + const lines = str.trim().split("\n"); + + for (const line of lines) { + if (!line.trim()) continue; + const eqIndex = line.indexOf("="); + if (eqIndex > 0) { + const key = line.substring(0, eqIndex).trim(); + const value = line.substring(eqIndex + 1).trim(); + if (key) result[key] = value; + } + } + + return Object.keys(result).length > 0 ? result : undefined; + }; + + /** 解析参数字符串 */ + const parseArgs = (str: string): string[] | undefined => { + if (!str.trim()) return undefined; + // 简单按空格分割,支持引号包裹的参数 + const args: string[] = []; + let current = ""; + let inQuote = false; + let quoteChar = ""; + + for (const char of str) { + if ((char === '"' || char === "'") && !inQuote) { + inQuote = true; + quoteChar = char; + } else if (char === quoteChar && inQuote) { + inQuote = false; + quoteChar = ""; + } else if (char === " " && !inQuote) { + if (current.trim()) args.push(current.trim()); + current = ""; + } else { + current += char; + } + } + + if (current.trim()) args.push(current.trim()); + return args.length > 0 ? args : undefined; + }; + + /** 保存表单 */ + const handleSave = async () => { + if (!validateForm()) return; + + setSaving(true); + try { + await onSave({ + name: formData.name.trim(), + description: formData.description.trim() || undefined, + command: formData.command.trim(), + args: parseArgs(formData.args), + env: parseEnvVars(formData.envVars), + transportType: formData.transportType, + }); + onClose(); + } catch (err) { + console.error("Failed to save MCP server:", err); + setErrors({ ...errors, name: `保存失败: ${err}` }); + } finally { + setSaving(false); + } + }; + + /** 删除确认 */ + const handleDelete = async () => { + if (!onDelete) return; + + setDeleting(true); + try { + await onDelete(); + onClose(); + } catch (err) { + console.error("Failed to delete MCP server:", err); + setErrors({ ...errors, name: `删除失败: ${err}` }); + } finally { + setDeleting(false); + setShowDeleteConfirm(false); + } + }; + + return ( +
+
+ {/* 头部 */} +
+ + {isEditMode ? "编辑 MCP 工具" : "添加 MCP 工具"} + + +
+ + {/* 表单内容 */} +
+ {/* 名称 */} + + + {/* 描述 */} + + + {/* 启动命令 */} + + + {/* 命令参数 */} + + + {/* 环境变量 */} +