import React, { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; import { Save } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, } from "@/components/ui/dialog"; import { McpServerSpec } from "@/types"; interface McpWizardModalProps { isOpen: boolean; onClose: () => void; onApply: (title: string, json: string) => void; initialTitle?: string; initialServer?: McpServerSpec; } /** * 解析环境变量文本为对象 */ 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; }; /** * 解析headers文本为对象(支持 KEY: VALUE 或 KEY=VALUE) */ const parseHeadersText = (text: string): Record => { const lines = text .split("\n") .map((l) => l.trim()) .filter((l) => l.length > 0); const headers: Record = {}; for (const l of lines) { // 支持 KEY: VALUE 或 KEY=VALUE const colonIdx = l.indexOf(":"); const equalIdx = l.indexOf("="); let idx = -1; if (colonIdx > 0 && (equalIdx === -1 || colonIdx < equalIdx)) { idx = colonIdx; } else if (equalIdx > 0) { idx = equalIdx; } if (idx > 0) { const k = l.slice(0, idx).trim(); const v = l.slice(idx + 1).trim(); if (k) headers[k] = v; } } return headers; }; /** * MCP 配置向导模态框 * 帮助用户快速生成 MCP JSON 配置 */ const McpWizardModal: React.FC = ({ isOpen, onClose, onApply, initialTitle, initialServer, }) => { const { t } = useTranslation(); const [wizardType, setWizardType] = useState<"stdio" | "http" | "sse">( "stdio", ); const [wizardTitle, setWizardTitle] = useState(""); // stdio 字段 const [wizardCommand, setWizardCommand] = useState(""); const [wizardArgs, setWizardArgs] = useState(""); const [wizardEnv, setWizardEnv] = useState(""); // http 和 sse 字段 const [wizardUrl, setWizardUrl] = useState(""); const [wizardHeaders, setWizardHeaders] = useState(""); // 生成预览 JSON const generatePreview = (): string => { const config: McpServerSpec = { type: wizardType, }; if (wizardType === "stdio") { // stdio 类型必需字段 config.command = wizardCommand.trim(); // 可选字段 if (wizardArgs.trim()) { config.args = wizardArgs .split("\n") .map((s) => s.trim()) .filter((s) => s.length > 0); } if (wizardEnv.trim()) { const env = parseEnvText(wizardEnv); if (Object.keys(env).length > 0) { config.env = env; } } } else { // http 和 sse 类型必需字段 config.url = wizardUrl.trim(); // 可选字段 if (wizardHeaders.trim()) { const headers = parseHeadersText(wizardHeaders); if (Object.keys(headers).length > 0) { config.headers = headers; } } } return JSON.stringify(config, null, 2); }; const handleApply = () => { if (!wizardTitle.trim()) { toast.error(t("mcp.error.idRequired"), { duration: 3000 }); return; } if (wizardType === "stdio" && !wizardCommand.trim()) { toast.error(t("mcp.error.commandRequired"), { duration: 3000 }); return; } if ((wizardType === "http" || wizardType === "sse") && !wizardUrl.trim()) { toast.error(t("mcp.wizard.urlRequired"), { duration: 3000 }); return; } const json = generatePreview(); onApply(wizardTitle.trim(), json); handleClose(); }; const handleClose = () => { // 重置表单 setWizardType("stdio"); setWizardTitle(""); setWizardCommand(""); setWizardArgs(""); setWizardEnv(""); setWizardUrl(""); setWizardHeaders(""); onClose(); }; const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter" && e.metaKey) { e.preventDefault(); handleApply(); } }; useEffect(() => { if (!isOpen) return; const title = initialTitle ?? ""; setWizardTitle(title); const resolvedType = initialServer?.type ?? (initialServer?.url ? "http" : "stdio"); setWizardType(resolvedType); if (resolvedType === "http" || resolvedType === "sse") { setWizardUrl(initialServer?.url ?? ""); const headersCandidate = initialServer?.headers; const headers = headersCandidate && typeof headersCandidate === "object" ? headersCandidate : undefined; setWizardHeaders( headers ? Object.entries(headers) .map(([k, v]) => `${k}: ${v ?? ""}`) .join("\n") : "", ); setWizardCommand(""); setWizardArgs(""); setWizardEnv(""); return; } setWizardCommand(initialServer?.command ?? ""); const argsValue = initialServer?.args; setWizardArgs(Array.isArray(argsValue) ? argsValue.join("\n") : ""); const envCandidate = initialServer?.env; const env = envCandidate && typeof envCandidate === "object" ? envCandidate : undefined; setWizardEnv( env ? Object.entries(env) .map(([k, v]) => `${k}=${v ?? ""}`) .join("\n") : "", ); setWizardUrl(""); setWizardHeaders(""); }, [isOpen]); const preview = generatePreview(); return ( !open && handleClose()}> {t("mcp.wizard.title")} {/* Content */}
{/* Hint */}

{t("mcp.wizard.hint")}

{/* Form Fields */}
{/* Type */}
{/* Title */}
setWizardTitle(e.target.value)} onKeyDown={handleKeyDown} placeholder={t("mcp.form.titlePlaceholder")} className="w-full rounded-lg border border-border-default px-3 py-2 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-emerald-500/20 dark:bg-gray-800 dark:text-gray-100" />
{/* Stdio 类型字段 */} {wizardType === "stdio" && ( <> {/* Command */}
setWizardCommand(e.target.value)} onKeyDown={handleKeyDown} placeholder={t("mcp.wizard.commandPlaceholder")} className="w-full rounded-lg border border-border-default px-3 py-2 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-emerald-500/20 dark:bg-gray-800 dark:text-gray-100" />
{/* Args */}