import React, { useState } from "react"; import { useTranslation } from "react-i18next"; import { X, Save, AlertCircle } from "lucide-react"; import { McpServer } from "../../types"; import { mcpPresets } from "../../config/mcpPresets"; import { buttonStyles, inputStyles } from "../../lib/styles"; import McpWizardModal from "./McpWizardModal"; import { extractErrorMessage } from "../../utils/errorUtils"; import { AppType } from "../../lib/tauri-api"; interface McpFormModalProps { appType: AppType; editingId?: string; initialData?: McpServer; onSave: (id: string, server: McpServer) => Promise; onClose: () => void; existingIds?: string[]; onNotify?: ( message: string, type: "success" | "error", duration?: number, ) => void; } /** * 验证 JSON 格式 */ const validateJson = (text: string): string => { if (!text.trim()) return ""; try { const parsed = JSON.parse(text); if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { return "JSON 必须是对象"; } return ""; } catch { return "JSON 格式错误"; } }; /** * MCP 表单模态框组件(简化版) * 仅包含:标题(必填)、描述(可选)、JSON 配置(可选,带格式校验) */ const McpFormModal: React.FC = ({ appType, editingId, initialData, onSave, onClose, existingIds = [], onNotify, }) => { const { t } = useTranslation(); const [formId, setFormId] = useState(editingId || ""); const [formDescription, setFormDescription] = useState( (initialData as any)?.description || "", ); const [formJson, setFormJson] = useState( initialData ? JSON.stringify(initialData, null, 2) : "", ); const [jsonError, setJsonError] = useState(""); const [saving, setSaving] = useState(false); const [isWizardOpen, setIsWizardOpen] = useState(false); const [idError, setIdError] = useState(""); // 编辑模式下禁止修改 ID const isEditing = !!editingId; // 预设选择状态(仅新增模式显示;-1 表示自定义) const [selectedPreset, setSelectedPreset] = useState( isEditing ? null : -1, ); const handleIdChange = (value: string) => { setFormId(value); if (!isEditing) { const exists = existingIds.includes(value.trim()); setIdError(exists ? t("mcp.error.idExists") : ""); } }; const ensureUniqueId = (base: string): string => { let candidate = base.trim(); if (!candidate) candidate = "mcp-server"; if (!existingIds.includes(candidate)) return candidate; let i = 1; while (existingIds.includes(`${candidate}-${i}`)) i++; return `${candidate}-${i}`; }; // 应用预设(写入表单但不落库) const applyPreset = (index: number) => { if (index < 0 || index >= mcpPresets.length) return; const p = mcpPresets[index]; const id = ensureUniqueId(p.id); setFormId(id); setFormDescription(p.description || ""); const json = JSON.stringify(p.server, null, 2); setFormJson(json); // 触发一次校验 setJsonError(validateJson(json)); setSelectedPreset(index); }; // 切回自定义 const applyCustom = () => { setSelectedPreset(-1); // 恢复到空白模板 setFormId(""); setFormDescription(""); setFormJson(""); setJsonError(""); }; const handleJsonChange = (value: string) => { setFormJson(value); // 基础 JSON 校验 const baseErr = validateJson(value); if (baseErr) { setJsonError(baseErr); return; } // 进一步结构校验:仅允许单个服务器对象,禁止整份配置 if (value.trim()) { try { const obj = JSON.parse(value); if (obj && typeof obj === "object") { if (Object.prototype.hasOwnProperty.call(obj, "mcpServers")) { setJsonError(t("mcp.error.singleServerObjectRequired")); return; } // 若带有类型,做必填字段提示(不阻止输入,仅给出即时反馈) const typ = (obj as any)?.type; if (typ === "stdio" && !(obj as any)?.command?.trim()) { setJsonError(t("mcp.error.commandRequired")); return; } if (typ === "http" && !(obj as any)?.url?.trim()) { setJsonError(t("mcp.wizard.urlRequired")); return; } } } catch { // 解析异常已在基础校验覆盖 } } setJsonError(""); }; const handleWizardApply = (title: string, json: string) => { setFormId(title); setFormJson(json); setJsonError(validateJson(json)); }; const handleSubmit = async () => { if (!formId.trim()) { onNotify?.(t("mcp.error.idRequired"), "error", 3000); return; } // 新增模式:阻止提交重名 ID if (!isEditing && existingIds.includes(formId.trim())) { setIdError(t("mcp.error.idExists")); return; } // 验证 JSON const currentJsonError = validateJson(formJson); setJsonError(currentJsonError); if (currentJsonError) { onNotify?.(t("mcp.error.jsonInvalid"), "error", 3000); return; } setSaving(true); try { let server: McpServer; if (formJson.trim()) { // 解析 JSON 配置 server = JSON.parse(formJson) as McpServer; // 前置必填校验,避免后端拒绝后才提示 if (server?.type === "stdio" && !server?.command?.trim()) { onNotify?.(t("mcp.error.commandRequired"), "error", 3000); return; } if (server?.type === "http" && !server?.url?.trim()) { onNotify?.(t("mcp.wizard.urlRequired"), "error", 3000); return; } } else { // 空 JSON 时提供默认值(注意:后端会校验 stdio 需要非空 command / http 需要 url) server = { type: "stdio", command: "", args: [], }; } // 保留原有的 enabled 状态 if (initialData?.enabled !== undefined) { server.enabled = initialData.enabled; } // 保存 description 到 server 对象 if (formDescription.trim()) { (server as any).description = formDescription.trim(); } // 显式等待父组件保存流程,以便正确处理成功/失败 await onSave(formId.trim(), server); } catch (error: any) { // 提取后端错误信息(支持 string / {message} / tauri payload) const detail = extractErrorMessage(error); const msg = detail || t("mcp.error.saveFailed"); onNotify?.(msg, "error", detail ? 6000 : 4000); } finally { setSaving(false); } }; const getFormTitle = () => { if (appType === "claude") { return isEditing ? t("mcp.editClaudeServer") : t("mcp.addClaudeServer"); } else { return isEditing ? t("mcp.editCodexServer") : t("mcp.addCodexServer"); } }; return (
{/* Backdrop */}
{/* Modal */}
{/* Header */}

{getFormTitle()}

{/* Content - Scrollable */}
{/* 预设选择(仅新增时展示) */} {!isEditing && (
{mcpPresets.map((p, idx) => ( ))}
)} {/* ID (标题) */}
{!isEditing && idError && ( {idError} )}
handleIdChange(e.target.value)} disabled={isEditing} />
{/* Description (描述) */}
setFormDescription(e.target.value)} />
{/* JSON 配置 */}
{(isEditing || selectedPreset === -1) && ( )}