import React, { useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { X, Plus, Server } from "lucide-react"; import { McpServer } from "../../types"; import McpListItem from "./McpListItem"; import McpFormModal from "./McpFormModal"; import { ConfirmDialog } from "../ConfirmDialog"; import { extractErrorMessage } from "../../utils/errorUtils"; import { mcpPresets } from "../../config/mcpPresets"; import McpToggle from "./McpToggle"; import { buttonStyles, cardStyles, cn } from "../../lib/styles"; interface McpPanelProps { onClose: () => void; onNotify?: ( message: string, type: "success" | "error", duration?: number, ) => void; } /** * MCP 管理面板 * 采用与主界面一致的设计风格,右上角添加按钮,每个 MCP 占一行 */ const McpPanel: React.FC = ({ onClose, onNotify }) => { const { t } = useTranslation(); const [servers, setServers] = useState>({}); const [loading, setLoading] = useState(true); const [isFormOpen, setIsFormOpen] = useState(false); const [editingId, setEditingId] = useState(null); const [confirmDialog, setConfirmDialog] = useState<{ isOpen: boolean; title: string; message: string; onConfirm: () => void; } | null>(null); const reload = async () => { setLoading(true); try { const cfg = await window.api.getMcpConfig("claude"); setServers(cfg.servers || {}); } finally { setLoading(false); } }; useEffect(() => { const setup = async () => { try { // 先从 ~/.claude.json 导入已存在的 MCP(设为 enabled=true) await window.api.importMcpFromClaude(); // 读取现有 config.json 内容 const cfg = await window.api.getMcpConfig("claude"); const existing = cfg.servers || {}; // 将预设落库为禁用(若缺失) const missing = mcpPresets.filter((p) => !existing[p.id]); for (const p of missing) { const seed: McpServer = { ...(p.server as McpServer), enabled: false, source: "preset", } as unknown as McpServer; await window.api.upsertMcpServerInConfig("claude", p.id, seed); } } catch (e) { console.warn("MCP 初始化导入/落库失败(忽略继续)", e); } finally { await reload(); } }; setup(); }, []); const handleToggle = async (id: string, enabled: boolean) => { try { const server = servers[id]; if (!server) { const preset = mcpPresets.find((p) => p.id === id); if (!preset) return; await window.api.upsertMcpServerInConfig("claude", id, preset.server as McpServer); } await window.api.setMcpEnabled("claude", id, enabled); await reload(); onNotify?.( enabled ? t("mcp.msg.enabled") : t("mcp.msg.disabled"), "success", 1500, ); } catch (e: any) { const detail = extractErrorMessage(e); onNotify?.( detail || t("mcp.error.saveFailed"), "error", detail ? 6000 : 5000, ); } }; const handleEdit = (id: string) => { setEditingId(id); setIsFormOpen(true); }; const handleAdd = () => { setEditingId(null); setIsFormOpen(true); }; const handleDelete = (id: string) => { setConfirmDialog({ isOpen: true, title: t("mcp.confirm.deleteTitle"), message: t("mcp.confirm.deleteMessage", { id }), onConfirm: async () => { try { await window.api.deleteMcpServerInConfig("claude", id); await reload(); setConfirmDialog(null); onNotify?.(t("mcp.msg.deleted"), "success", 1500); } catch (e: any) { const detail = extractErrorMessage(e); onNotify?.( detail || t("mcp.error.deleteFailed"), "error", detail ? 6000 : 5000, ); } }, }); }; const handleSave = async (id: string, server: McpServer) => { try { await window.api.upsertMcpServerInConfig("claude", id, server); await reload(); setIsFormOpen(false); setEditingId(null); onNotify?.(t("mcp.msg.saved"), "success", 1500); } catch (e: any) { const detail = extractErrorMessage(e); onNotify?.( detail || t("mcp.error.saveFailed"), "error", detail ? 6000 : 5000, ); // 继续抛出错误,让表单层可以给到直观反馈(避免被更高层遮挡) throw e; } }; const handleCloseForm = () => { setIsFormOpen(false); setEditingId(null); }; const serverEntries = useMemo(() => Object.entries(servers), [servers]); return (
{/* Backdrop */}
{/* Panel */}
{/* Header */}

{t("mcp.title")}

{/* Info Section */}
{t("mcp.serverCount", { count: Object.keys(servers).length })}
{/* Content - Scrollable */}
{loading ? (
{t("mcp.loading")}
) : ( (() => { const notInstalledPresets = mcpPresets.filter( (p) => !servers[p.id], ); const hasAny = serverEntries.length > 0 || notInstalledPresets.length > 0; if (!hasAny) { return (

{t("mcp.empty")}

{t("mcp.emptyDescription")}

); } return (
{/* 已安装 */} {serverEntries.map(([id, server]) => ( ))} {/* 预设(未安装) */} {notInstalledPresets.map((p) => { const s = { ...(p.server as McpServer), enabled: false, } as McpServer; return (
handleToggle(p.id, en)} />

{p.id}

{p.description && (

{p.description}

)}
); })}
); })() )}
{/* Form Modal */} {isFormOpen && ( )} {/* Confirm Dialog */} {confirmDialog && ( setConfirmDialog(null)} /> )}
); }; export default McpPanel;