From d0fe9d75336bd7231e43c0956a44a6ed4adf61c1 Mon Sep 17 00:00:00 2001 From: Jason Date: Thu, 9 Oct 2025 11:30:28 +0800 Subject: [PATCH] feat(mcp): add configuration wizard and simplify form modal - Simplify McpFormModal to 3 inputs: title (required), description (optional), and JSON config (optional) - Add JSON validation similar to ProviderForm (must be object, real-time error display) - Create McpWizardModal component for quick configuration: - 5 input fields: type (stdio/sse), command (required), args, cwd, env - Real-time JSON preview - Emerald theme color (consistent with MCP button) - Z-index 70 (above McpFormModal's 60) - Add "or use configuration wizard" link next to JSON config label - Update i18n translations (zh/en) for form and wizard - All changes pass TypeScript typecheck and Prettier formatting --- src/components/mcp/McpFormModal.tsx | 217 ++++++++----------- src/components/mcp/McpWizardModal.tsx | 289 ++++++++++++++++++++++++++ src/i18n/locales/en.json | 25 +++ src/i18n/locales/zh.json | 25 +++ 4 files changed, 431 insertions(+), 125 deletions(-) create mode 100644 src/components/mcp/McpWizardModal.tsx diff --git a/src/components/mcp/McpFormModal.tsx b/src/components/mcp/McpFormModal.tsx index c509e2d..26582f8 100644 --- a/src/components/mcp/McpFormModal.tsx +++ b/src/components/mcp/McpFormModal.tsx @@ -1,8 +1,9 @@ import React, { useState } from "react"; import { useTranslation } from "react-i18next"; -import { X, Save, Wrench } from "lucide-react"; +import { X, Save, AlertCircle } from "lucide-react"; import { McpServer } from "../../types"; import { buttonStyles, inputStyles } from "../../lib/styles"; +import McpWizardModal from "./McpWizardModal"; interface McpFormModalProps { editingId?: string; @@ -11,33 +12,25 @@ interface McpFormModalProps { onClose: () => void; } -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; +/** + * 验证 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 格式错误"; } - return env; -}; - -const formatEnvText = (env?: Record): string => { - if (!env) return ""; - return Object.entries(env) - .map(([k, v]) => `${k}=${v}`) - .join("\n"); }; /** - * MCP 表单模态框组件 - * 用于添加或编辑 MCP 服务器 + * MCP 表单模态框组件(简化版) + * 仅包含:标题(必填)、描述(可选)、JSON 配置(可选,带格式校验) */ const McpFormModal: React.FC = ({ editingId, @@ -47,32 +40,25 @@ const McpFormModal: React.FC = ({ }) => { const { t } = useTranslation(); const [formId, setFormId] = useState(editingId || ""); - const [formType, setFormType] = useState<"stdio" | "sse">( - initialData?.type || "stdio", + const [formDescription, setFormDescription] = useState(""); + const [formJson, setFormJson] = useState( + initialData ? JSON.stringify(initialData, null, 2) : "", ); - const [formCommand, setFormCommand] = useState(initialData?.command || ""); - const [formArgsText, setFormArgsText] = useState( - (initialData?.args || []).join(" "), - ); - const [formEnvText, setFormEnvText] = useState( - formatEnvText(initialData?.env), - ); - const [formCwd, setFormCwd] = useState(initialData?.cwd || ""); + const [jsonError, setJsonError] = useState(""); const [saving, setSaving] = useState(false); + const [isWizardOpen, setIsWizardOpen] = useState(false); // 编辑模式下禁止修改 ID const isEditing = !!editingId; - const handleValidateCommand = async () => { - if (!formCommand) return; - try { - const ok = await window.api.validateMcpCommand(formCommand.trim()); - const message = ok ? t("mcp.validation.ok") : t("mcp.validation.fail"); - // 这里简单使用 alert,实际项目中应该使用 notification 系统 - alert(message); - } catch (_error) { - alert(t("mcp.validation.fail")); - } + const handleJsonChange = (value: string) => { + setFormJson(value); + setJsonError(validateJson(value)); + }; + + const handleWizardApply = (json: string) => { + setFormJson(json); + setJsonError(validateJson(json)); }; const handleSubmit = async () => { @@ -80,29 +66,38 @@ const McpFormModal: React.FC = ({ alert(t("mcp.error.idRequired")); return; } - if (!formCommand.trim()) { - alert(t("mcp.error.commandRequired")); + + // 验证 JSON + const currentJsonError = validateJson(formJson); + setJsonError(currentJsonError); + if (currentJsonError) { + alert(t("mcp.error.jsonInvalid")); return; } setSaving(true); try { - const server: McpServer = { - type: formType, - command: formCommand.trim(), - args: formArgsText - .split(/\s+/) - .map((s) => s.trim()) - .filter((s) => s.length > 0), - env: parseEnvText(formEnvText), - ...(formCwd ? { cwd: formCwd } : {}), - // 保留原有的 enabled 状态 - ...(initialData?.enabled !== undefined - ? { enabled: initialData.enabled } - : {}), - }; + let server: McpServer; + if (formJson.trim()) { + // 解析 JSON 配置 + server = JSON.parse(formJson) as McpServer; + } else { + // 空 JSON 时提供默认值 + server = { + type: "stdio", + command: "", + args: [], + }; + } + + // 保留原有的 enabled 状态 + if (initialData?.enabled !== undefined) { + server.enabled = initialData.enabled; + } onSave(formId.trim(), server); + } catch (error) { + alert(t("mcp.error.saveFailed")); } finally { setSaving(false); } @@ -133,94 +128,59 @@ const McpFormModal: React.FC = ({ {/* Content */}
- {/* ID */} + {/* ID (标题) */}
setFormId(e.target.value)} disabled={isEditing} />
- {/* Type & CWD */} -
-
- - -
-
- - setFormCwd(e.target.value)} - /> -
-
- - {/* Command */} + {/* Description (描述) */}
-
- setFormCommand(e.target.value)} - /> - -
-
- - {/* Args */} -
- setFormArgsText(e.target.value)} + placeholder={t("mcp.form.descriptionPlaceholder")} + value={formDescription} + onChange={(e) => setFormDescription(e.target.value)} />
- {/* Env */} + {/* JSON 配置 */}
- +
+ + +