{"record":{"id":"73e9b3471c81b87e","repo":"BigPizzaV3/CodexPlusPlus","slug":"json","errorCode":null,"errorMessage":"JSON 顶层必须是对象","messagePattern":"JSON 顶层必须是对象","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/codex-plus-core/src/mcp_config.rs","lineNumber":265,"sourceCode":"    Ok(if text.trim().is_empty() {\n        String::new()\n    } else {\n        ensure_trailing_newline(text)\n    })\n}\n\n/// 解析社区文档里常见的 MCP JSON。\n///\n/// 接受四种外层形状：`{\"mcpServers\":{…}}`（Claude 标准）、`{\"servers\":{…}}`\n/// （VS Code）、裸的 `{id: cfg}`，以及单个 server 对象（此时 id 留空由调用方填）。\npub fn parse_mcp_servers_json(json: &str) -> anyhow::Result<McpJsonImport> {\n    let trimmed = json.trim();\n    if trimmed.is_empty() {\n        anyhow::bail!(\"请先粘贴 MCP 配置 JSON\");\n    }\n    let root: Value = serde_json::from_str(trimmed).context(\"不是合法的 JSON\")?;\n    let Value::Object(root) = root else {\n        anyhow::bail!(\"JSON 顶层必须是对象\");\n    };\n\n    let servers = if let Some(Value::Object(map)) = root.get(\"mcpServers\") {\n        map.clone()\n    } else if let Some(Value::Object(map)) = root.get(\"servers\") {\n        map.clone()\n    } else if looks_like_single_server(&root) {\n        // 单个 server 对象，没有 id，用占位 id 让用户在界面上改\n        let mut map = Map::new();\n        map.insert(\"mcp-server\".to_string(), Value::Object(root));\n        map\n    } else {\n        root\n    };\n\n    if servers.is_empty() {\n        anyhow::bail!(\"JSON 里没有找到任何 MCP 服务器\");\n    }","sourceCodeStart":247,"sourceCodeEnd":283,"githubUrl":"https://github.com/BigPizzaV3/CodexPlusPlus/blob/b1ed92e5e4a2d74095d4b8db5af43cef7acba9c6/crates/codex-plus-core/src/mcp_config.rs#L247-L283","documentation":"After JSON syntax validation, parse_mcp_servers_json requires the top-level JSON value to be an object, because all supported wrapper shapes ({mcpServers:...}, {servers:...}, bare {id:cfg}, single server object) are objects. Arrays, strings, numbers or booleans at top level are rejected with this message.","triggerScenarios":"Passing input like \"[{...}]\" (array of servers), \"\\\"text\\\"\", \"123\", or \"true\" to parse_mcp_servers_json.","commonSituations":"User pastes a JSON array exported by another tool instead of the object form; a script serializes only the servers list; accidental double-selection pastes a fragment starting with '['.","solutions":["Wrap the array in an object: {\"mcpServers\": {...entries...}}.","If you have a list of server configs, key each one by its id inside an object before importing.","Validate with JSON.parse and check typeof parsed === 'object' && !Array.isArray(parsed) before calling."],"exampleFix":"// before\nparse_mcp_servers_json(\"[{\\\"command\\\":\\\"npx\\\"}]\")\n// after\nparse_mcp_servers_json(\"{\\\"mcpServers\\\":{\\\"my-server\\\":{\\\"command\\\":\\\"npx\\\"}}}\")","handlingStrategy":"validation","validationCode":"function isJsonObject(text) {\n  try {\n    const v = JSON.parse(text);\n    return typeof v === 'object' && v !== null && !Array.isArray(v);\n  } catch { return false; }\n}","typeGuard":"function isRecord(v) { return typeof v === 'object' && v !== null && !Array.isArray(v); }","tryCatchPattern":"match parse_mcp_servers_json(input) {\n    Ok(import) => apply(import),\n    Err(e) if e.to_string().contains(\"顶层必须是对象\") => ui.hint(\"JSON 顶层需为对象，如 {\\\"mcpServers\\\":{...}}\"),\n    Err(e) => return Err(e),\n}","preventionTips":["Validate top-level shape with JSON.parse + Array.isArray check before import","Convert array exports to the {\"mcpServers\":{...}} object form first","Document the accepted wrapper shapes in the import UI","Reject fragments: ensure the pasted text parses as complete JSON"],"tags":["json","mcp","validation","shape"],"backgroundTag":"unexpected-response-shape","analyzedSha":"b1ed92e5e4a2d74095d4b8db5af43cef7acba9c6","analyzedAt":"2026-09-19T23:35:21.129Z","contentChangedAt":"2026-09-19T23:35:21.129Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}