From 9471cb0d198e5256ff480d621a90a79447529506 Mon Sep 17 00:00:00 2001 From: Jason Date: Thu, 9 Oct 2025 12:04:37 +0800 Subject: [PATCH] fix(mcp): update MCP wizard to support http type and improve args input - Replace deprecated 'sse' type with 'http' (as per Claude Code official docs) - Add HTTP-specific fields: url (required) and headers (optional) - Implement dynamic UI: show different fields based on selected type - Improve args input: support multi-line input (one argument per line) - Add headers parsing supporting both 'KEY: VALUE' and 'KEY=VALUE' formats - Update backend validation to enforce type-specific required fields - Update i18n translations (zh/en) with new HTTP-related labels --- src-tauri/src/claude_mcp.rs | 22 ++- src/components/mcp/McpWizardModal.tsx | 258 ++++++++++++++++++-------- src/i18n/locales/en.json | 9 +- src/i18n/locales/zh.json | 9 +- src/types.ts | 9 +- 5 files changed, 216 insertions(+), 91 deletions(-) diff --git a/src-tauri/src/claude_mcp.rs b/src-tauri/src/claude_mcp.rs index ba6f928..4998952 100644 --- a/src-tauri/src/claude_mcp.rs +++ b/src-tauri/src/claude_mcp.rs @@ -80,12 +80,24 @@ pub fn upsert_mcp_server(id: &str, spec: Value) -> Result { .get("type") .and_then(|x| x.as_str()) .unwrap_or(""); - if t != "stdio" && t != "sse" { - return Err("MCP 服务器 type 必须是 'stdio' 或 'sse'".into()); + if t != "stdio" && t != "http" { + return Err("MCP 服务器 type 必须是 'stdio' 或 'http'".into()); } - let cmd = spec.get("command").and_then(|x| x.as_str()).unwrap_or(""); - if cmd.is_empty() { - return Err("MCP 服务器缺少 command".into()); + + // stdio 类型必须有 command + if t == "stdio" { + let cmd = spec.get("command").and_then(|x| x.as_str()).unwrap_or(""); + if cmd.is_empty() { + return Err("stdio 类型的 MCP 服务器缺少 command 字段".into()); + } + } + + // http 类型必须有 url + if t == "http" { + let url = spec.get("url").and_then(|x| x.as_str()).unwrap_or(""); + if url.is_empty() { + return Err("http 类型的 MCP 服务器缺少 url 字段".into()); + } } let path = user_config_path(); diff --git a/src/components/mcp/McpWizardModal.tsx b/src/components/mcp/McpWizardModal.tsx index a1db723..2536f3e 100644 --- a/src/components/mcp/McpWizardModal.tsx +++ b/src/components/mcp/McpWizardModal.tsx @@ -30,6 +30,34 @@ const parseEnvText = (text: string): Record => { 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 配置 @@ -40,35 +68,54 @@ const McpWizardModal: React.FC = ({ onApply, }) => { const { t } = useTranslation(); - const [wizardType, setWizardType] = useState<"stdio" | "sse">("stdio"); + const [wizardType, setWizardType] = useState<"stdio" | "http">("stdio"); + // stdio 字段 const [wizardCommand, setWizardCommand] = useState(""); const [wizardArgs, setWizardArgs] = useState(""); const [wizardCwd, setWizardCwd] = useState(""); const [wizardEnv, setWizardEnv] = useState(""); + // http 字段 + const [wizardUrl, setWizardUrl] = useState(""); + const [wizardHeaders, setWizardHeaders] = useState(""); // 生成预览 JSON const generatePreview = (): string => { const config: McpServer = { type: wizardType, - command: wizardCommand.trim(), }; - // 添加可选字段 - if (wizardArgs.trim()) { - config.args = wizardArgs - .split(/\s+/) - .map((s) => s.trim()) - .filter((s) => s.length > 0); - } + if (wizardType === "stdio") { + // stdio 类型必需字段 + config.command = wizardCommand.trim(); - if (wizardCwd.trim()) { - config.cwd = wizardCwd.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; + if (wizardCwd.trim()) { + config.cwd = wizardCwd.trim(); + } + + if (wizardEnv.trim()) { + const env = parseEnvText(wizardEnv); + if (Object.keys(env).length > 0) { + config.env = env; + } + } + } else { + // http 类型必需字段 + config.url = wizardUrl.trim(); + + // 可选字段 + if (wizardHeaders.trim()) { + const headers = parseHeadersText(wizardHeaders); + if (Object.keys(headers).length > 0) { + config.headers = headers; + } } } @@ -76,10 +123,14 @@ const McpWizardModal: React.FC = ({ }; const handleApply = () => { - if (!wizardCommand.trim()) { + if (wizardType === "stdio" && !wizardCommand.trim()) { alert(t("mcp.error.commandRequired")); return; } + if (wizardType === "http" && !wizardUrl.trim()) { + alert(t("mcp.wizard.urlRequired")); + return; + } const json = generatePreview(); onApply(json); @@ -93,6 +144,8 @@ const McpWizardModal: React.FC = ({ setWizardArgs(""); setWizardCwd(""); setWizardEnv(""); + setWizardUrl(""); + setWizardHeaders(""); onClose(); }; @@ -163,7 +216,7 @@ const McpWizardModal: React.FC = ({ value="stdio" checked={wizardType === "stdio"} onChange={(e) => - setWizardType(e.target.value as "stdio" | "sse") + setWizardType(e.target.value as "stdio" | "http") } className="w-4 h-4 text-emerald-500 bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700 focus:ring-emerald-500 dark:focus:ring-emerald-400 focus:ring-2" /> @@ -174,84 +227,129 @@ const McpWizardModal: React.FC = ({ - {/* Command */} -
- - setWizardCommand(e.target.value)} - onKeyDown={handleKeyDown} - placeholder={t("mcp.wizard.commandPlaceholder")} - required - className="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-emerald-500/20 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100" - /> -
+ {/* Stdio 类型字段 */} + {wizardType === "stdio" && ( + <> + {/* Command */} +
+ + setWizardCommand(e.target.value)} + onKeyDown={handleKeyDown} + placeholder={t("mcp.wizard.commandPlaceholder")} + required + className="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-emerald-500/20 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100" + /> +
- {/* Args */} -
- - setWizardArgs(e.target.value)} - onKeyDown={handleKeyDown} - placeholder={t("mcp.wizard.argsPlaceholder")} - className="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-emerald-500/20 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100" - /> -
+ {/* Args */} +
+ +