diff --git a/src/components/mcp/McpFormModal.tsx b/src/components/mcp/McpFormModal.tsx index ba46279..731f222 100644 --- a/src/components/mcp/McpFormModal.tsx +++ b/src/components/mcp/McpFormModal.tsx @@ -27,7 +27,7 @@ import { mcpServerToToml, } from "@/utils/tomlUtils"; import { normalizeTomlText } from "@/utils/textNormalization"; -import { formatJSON } from "@/utils/formatters"; +import { formatJSON, parseSmartMcpJson } from "@/utils/formatters"; import { useMcpValidation } from "./useMcpValidation"; import { useUpsertMcpServer } from "@/hooks/useMcp"; @@ -241,15 +241,41 @@ const McpFormModal: React.FC = ({ } } } else { - // JSON validation (use hook's complete validation) - const err = validateJsonConfig(value); - if (err) { - setConfigError(err); - return; + // JSON validation with smart parsing + try { + const result = parseSmartMcpJson(value); + + // 验证解析后的配置对象 + const configJson = JSON.stringify(result.config); + const validationErr = validateJsonConfig(configJson); + + if (validationErr) { + setConfigError(validationErr); + return; + } + + // 自动填充提取的 id(仅当表单 id 为空且不在编辑模式时) + if (result.id && !formId.trim() && !isEditing) { + const uniqueId = ensureUniqueId(result.id); + setFormId(uniqueId); + + // 如果 name 也为空,同时填充 name + if (!formName.trim()) { + setFormName(result.id); + } + } + + // 如果智能解析提取了配置(格式转换),自动格式化输入框内容 + if (result.id && result.formattedConfig !== value.trim()) { + setFormConfig(result.formattedConfig); + } + + setConfigError(""); + } catch (err: any) { + const errorMessage = err?.message || String(err); + setConfigError(t("mcp.error.jsonInvalid") + ": " + errorMessage); } } - - setConfigError(""); }; const handleFormatJson = () => { diff --git a/src/utils/formatters.ts b/src/utils/formatters.ts index 09a647f..b0cf396 100644 --- a/src/utils/formatters.ts +++ b/src/utils/formatters.ts @@ -13,6 +13,57 @@ export function formatJSON(value: string): string { return JSON.stringify(parsed, null, 2); } +/** + * 智能解析 MCP JSON 配置 + * 支持两种格式: + * 1. 纯配置对象:{ "command": "npx", "args": [...], ... } + * 2. 带键名包装: "server-name": { "command": "npx", ... } 或 { "server-name": {...} } + * + * @param jsonText - JSON 字符串 + * @returns { id?: string, config: object, formattedConfig: string } + * @throws 如果 JSON 格式无效 + */ +export function parseSmartMcpJson(jsonText: string): { + id?: string; + config: any; + formattedConfig: string; +} { + let trimmed = jsonText.trim(); + if (!trimmed) { + return { config: {}, formattedConfig: "" }; + } + + // 如果是键值对片段("key": {...}),包装成完整对象 + if (trimmed.startsWith('"') && !trimmed.startsWith('{')) { + trimmed = `{${trimmed}}`; + } + + const parsed = JSON.parse(trimmed); + + // 如果是单键对象且值是对象,提取键名和配置 + const keys = Object.keys(parsed); + if ( + keys.length === 1 && + parsed[keys[0]] && + typeof parsed[keys[0]] === "object" && + !Array.isArray(parsed[keys[0]]) + ) { + const id = keys[0]; + const config = parsed[id]; + return { + id, + config, + formattedConfig: JSON.stringify(config, null, 2), + }; + } + + // 否则直接使用 + return { + config: parsed, + formattedConfig: JSON.stringify(parsed, null, 2), + }; +} + /** * TOML 格式化功能已禁用 *