import React, { useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { X, Plus, Save, Trash2, Wrench } from "lucide-react"; import { McpServer, McpStatus } from "../../types"; interface McpPanelProps { onClose: () => void; onNotify?: (message: string, type: "success" | "error", duration?: number) => void; } const emptyServer: McpServer & { id?: string } = { type: "stdio", command: "", args: [], env: {}, }; const parseEnvText = (text: string): Record => { const lines = text .split("\n") .map((l) => l.trim()) .filter((l) => l.length > 0); const env: Record = {}; for (const l of lines) { const idx = l.indexOf("="); if (idx > 0) { const k = l.slice(0, idx).trim(); const v = l.slice(idx + 1).trim(); if (k) env[k] = v; } } return env; }; const formatEnvText = (env?: Record): string => { if (!env) return ""; return Object.entries(env) .map(([k, v]) => `${k}=${v}`) .join("\n"); }; const McpPanel: React.FC = ({ onClose, onNotify }) => { const { t } = useTranslation(); const [status, setStatus] = useState(null); const [servers, setServers] = useState>({}); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [editingId, setEditingId] = useState(null); const [form, setForm] = useState(emptyServer); const [formEnvText, setFormEnvText] = useState(""); const [formArgsText, setFormArgsText] = useState(""); const reload = async () => { setLoading(true); try { const s = await window.api.getClaudeMcpStatus(); setStatus(s); const text = await window.api.readClaudeMcpConfig(); if (text) { try { const obj = JSON.parse(text); const list = (obj?.mcpServers || {}) as Record; setServers(list); } catch (e) { console.error("Failed to parse mcp.json", e); setServers({}); } } else { setServers({}); } } finally { setLoading(false); } }; useEffect(() => { reload(); }, []); const handleToggleEnable = async (enable: boolean) => { try { const changed = await window.api.setClaudeMcpEnableAllProjects(enable); if (changed) { await reload(); onNotify?.(t("mcp.notice.restartClaude"), "success", 2000); } } catch (e: any) { onNotify?.(e?.message || t("mcp.error.toggleFailed"), "error", 5000); } }; const resetForm = () => { setEditingId(null); setForm(emptyServer); setFormArgsText(""); setFormEnvText(""); }; const beginEdit = (id?: string) => { if (!id) { resetForm(); return; } const spec = servers[id]; setEditingId(id); setForm({ id, ...spec }); setFormArgsText((spec.args || []).join(" ")); setFormEnvText(formatEnvText(spec.env)); }; const submitForm = async () => { if (!form.id || !form.id.trim()) { onNotify?.(t("mcp.error.idRequired"), "error", 3000); return; } if (!form.command || !form.command.trim()) { onNotify?.(t("mcp.error.commandRequired"), "error", 3000); return; } setSaving(true); try { const spec: McpServer = { type: form.type, command: form.command.trim(), args: formArgsText .split(/\s+/) .map((s) => s.trim()) .filter((s) => s.length > 0), env: parseEnvText(formEnvText), ...(form.cwd ? { cwd: form.cwd } : {}), }; await window.api.upsertClaudeMcpServer(form.id.trim(), spec); await reload(); resetForm(); onNotify?.(t("mcp.msg.saved"), "success", 1500); } catch (e: any) { onNotify?.(e?.message || t("mcp.error.saveFailed"), "error", 6000); } finally { setSaving(false); } }; const removeServer = async (id: string) => { try { await window.api.deleteClaudeMcpServer(id); await reload(); if (editingId === id) resetForm(); onNotify?.(t("mcp.msg.deleted"), "success", 1500); } catch (e: any) { onNotify?.(e?.message || t("mcp.error.deleteFailed"), "error", 5000); } }; const addTemplateFetch = async () => { try { await window.api.upsertClaudeMcpServer("mcp-fetch", { type: "stdio", command: "uvx", args: ["mcp-server-fetch"], }); await reload(); onNotify?.(t("mcp.msg.templateAdded"), "success", 1500); } catch (e: any) { onNotify?.(e?.message || t("mcp.error.saveFailed"), "error", 5000); } }; const validateCommand = async () => { if (!form.command) return; const ok = await window.api.validateMcpCommand(form.command.trim()); onNotify?.( ok ? t("mcp.validation.ok") : t("mcp.validation.fail"), ok ? "success" : "error", 1500, ); }; const serverEntries = useMemo(() => Object.entries(servers), [servers]); return (
{/* Backdrop */}
{/* Header */}

{t("mcp.title")}

{/* Content */}
{/* Left: status & list */}
{t("mcp.enableProject")}
{status?.settingsLocalPath}
{/* Right: form */}
{editingId ? t("mcp.editServer") : t("mcp.addServer")}
setForm((s) => ({ ...s, id: e.target.value }))} />
setForm((s) => ({ ...s, cwd: e.target.value }))} />
setForm((s) => ({ ...s, command: e.target.value }))} />
setFormArgsText(e.target.value)} />