{"record":{"id":"c8e4f4fdd6d85fc9","repo":"Hmbown/CodeWhale","slug":"mcp-config-must-be-an-object","errorCode":null,"errorMessage":"MCP config must be an object","messagePattern":"MCP config must be an object","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/tui/src/mcp.rs","lineNumber":5838,"sourceCode":"pub fn mutate_config<T>(\n    path: &Path,\n    expected_revision: Option<&str>,\n    mutate: impl FnOnce(&mut McpConfig) -> Result<T>,\n) -> Result<(T, String)> {\n    validate_mcp_config_path(path)?;\n    codewhale_config::with_config_write_lock(path, |path| {\n        let original = read_mcp_config_file(path)?;\n        let revision = config_revision(original.as_deref());\n        if expected_revision.is_some_and(|expected| expected != revision) {\n            return Err(McpRevisionConflict.into());\n        }\n        let mut raw: serde_json::Value = match original.as_deref() {\n            Some(raw) => serde_json::from_str(raw).map_err(|_| {\n                anyhow::anyhow!(\"Failed to parse MCP config; file contents were omitted\")\n            })?,\n            None => serde_json::json!({}),\n        };\n        anyhow::ensure!(raw.is_object(), \"MCP config must be an object\");\n        let mut config: McpConfig = serde_json::from_value(raw.clone())\n            .map_err(|_| anyhow::anyhow!(\"Invalid MCP config; file contents were omitted\"))?;\n        let before = serde_json::to_value(&config)?;\n        let result = mutate(&mut config)?;\n        let after = serde_json::to_value(&config)?;\n        if before == after {\n            return Ok((result, revision));\n        }\n        // Preserve legacy spelling while applying the canonical typed delta.\n        let legacy = raw.get(\"mcpServers\").is_some();\n        if legacy {\n            let object = raw\n                .as_object_mut()\n                .context(\"MCP config must be an object\")?;\n            let servers = object.remove(\"mcpServers\").expect(\"checked above\");\n            object.insert(\"servers\".into(), servers);\n        }\n        apply_json_delta(&mut raw, &before, &after);","sourceCodeStart":5820,"sourceCodeEnd":5856,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/mcp.rs#L5820-L5856","documentation":"mutate_config in crates/tui/src/mcp.rs parses the config file into a serde_json::Value and requires the top level to be a JSON object. If the file parses as valid JSON but is an array, string, number, or null, this ensure! fails. The schema expects an object (with a servers/mcpServers map).","triggerScenarios":"Calling any mutate_config-backed operation (add/remove/update server, set_server_enabled) while the config file contains valid JSON that is not an object, e.g. a bare array or a quoted string.","commonSituations":"A file holding `[ ... ]` (the array shape accepted by external import sources, not the app's own config); an accidentally over-written file containing just \"null\" or a number; confusing the external-import source format with the app config format.","solutions":["Wrap the config top level in an object: {\"mcpServers\": { ... }} (or the servers map shape the app expects) and retry","If the file is an export/list meant for import, use the external import path instead of pointing the app config at it","Reset the file with init_config --force to regenerate a valid template"],"exampleFix":"// before (valid JSON, wrong shape)\n[{ \"name\": \"fs\", \"command\": \"npx\" }]\n// after\n{ \"mcpServers\": { \"fs\": { \"command\": \"npx\" } } }","handlingStrategy":"validation","validationCode":"fn top_level_is_object(path: &std::path::Path) -> bool {\n    std::fs::read_to_string(path).ok()\n        .and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok())\n        .map(|v| v.is_object())\n        .unwrap_or(false)\n}","typeGuard":"fn as_config_object(v: &serde_json::Value) -> Option<&serde_json::Map<String, serde_json::Value>> {\n    v.as_object()\n}","tryCatchPattern":"match result {\n    Err(e) if e.to_string() == \"MCP config must be an object\" => {\n        eprintln!(\"top level of mcp config must be {{...}}, got another JSON kind\");\n    }\n    _ => {}\n}","preventionTips":["Always keep a top-level object with mcpServers/servers as the root of the config","Do not confuse external-import source shapes (arrays allowed) with the app config shape (object required)","Run init_config --force to regenerate a valid template when in doubt","Type-check the file: jq 'type' mcp.json should print \"object\""],"tags":["mcp","config","schema"],"backgroundTag":"schema-validation-failed","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}