{"record":{"id":"c62e7ce8f3743ae2","repo":"Hmbown/CodeWhale","slug":"mcp-consent-history-must-be-an-object","errorCode":null,"errorMessage":"MCP consent history must be an object","messagePattern":"MCP consent history must be an object","errorType":"validation","errorClass":"anyhow","httpStatus":null,"severity":"error","filePath":"crates/tui/src/mcp/external_import.rs","lineNumber":300,"sourceCode":"}\n\n/// Record decisions against the latest consent document under the shared\n/// process lock. Malformed history is never silently replaced with empty state.\npub fn persist_decisions(\n    path: &Path,\n    candidates: &[ImportCandidate],\n    decisions: &HashMap<String, ImportDecision>,\n    now_unix: u64,\n) -> anyhow::Result<()> {\n    super::validate_mcp_config_path(path)?;\n    codewhale_config::with_config_write_lock(path, |path| {\n        let original = super::read_mcp_config_file(path)?;\n        let mut raw: Value = match original.as_deref() {\n            Some(raw) => serde_json::from_str(raw)\n                .map_err(|_| anyhow::anyhow!(\"Invalid MCP consent history; contents omitted\"))?,\n            None => serde_json::json!({}),\n        };\n        anyhow::ensure!(raw.is_object(), \"MCP consent history must be an object\");\n        let mut store: ImportConsentStore = if original.is_none() {\n            ImportConsentStore::default()\n        } else {\n            serde_json::from_value(raw.clone())\n                .map_err(|_| anyhow::anyhow!(\"Invalid MCP consent history; contents omitted\"))?\n        };\n        let before = serde_json::to_value(&store)?;\n        record_decisions(&mut store, candidates, decisions, now_unix);\n        let after = serde_json::to_value(&store)?;\n        super::apply_json_delta(&mut raw, &before, &after);\n        let rendered = serde_json::to_vec_pretty(&raw)?;\n        if rendered.len() as u64 > super::MAX_MCP_CONFIG_BYTES {\n            anyhow::bail!(\"MCP consent history exceeds size limit\");\n        }\n        crate::utils::write_atomic(path, &rendered)?;\n        Ok(())\n    })\n}","sourceCodeStart":282,"sourceCodeEnd":318,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/mcp/external_import.rs#L282-L318","documentation":"After JSON parsing succeeds, persist_decisions requires the top-level document to be a JSON object (the consent store lives as an object). A top-level array, string, number, or boolean is rejected with this error so malformed history is never overwritten.","triggerScenarios":"`persist_decisions` is called when the config file parses as JSON but its root is not an object — e.g. the file contains an array or a bare string instead of an object.","commonSituations":"A tool or script rewrote the config as a JSON array; someone pasted a JSON array of servers into the consent/config file; an earlier export wrote the wrong root shape.","solutions":["Rewrite the file so its root is a JSON object (at minimum {}), preserving any object contents","If the content is an array of servers, move it into the source-file shape (e.g. {\"mcpServers\":{...}}), not the consent/config path","Validate the root type before writing: the parsed JSON must be a dict/object"],"exampleFix":"// before\n[{\"name\":\"fs\",\"command\":\"npx\"}]\n// after\n{\"mcpServers\":{\"fs\":{\"command\":\"npx\"}}}","handlingStrategy":"validation","validationCode":"const doc = JSON.parse(fs.readFileSync(path, \"utf8\"));\nif (doc === null || typeof doc !== \"object\" || Array.isArray(doc)) { /* rewrite root as object */ }","typeGuard":"function isJsonObject(v) { return typeof v === \"object\" && v !== null && !Array.isArray(v); }","tryCatchPattern":"catch, then parse the file and confirm the root is an object; rewrite it as an object preserving valid keys before retrying","preventionTips":["Never paste arrays of servers into the consent/config path; use the source-file format instead","Check root type (object, not array) in any script that regenerates the config","Run a JSON shape check after external tooling touches the file"],"tags":["mcp","json","config-validation","consent"],"backgroundTag":"invalid-json-response","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"}