Hmbown/CodeWhale · error · anyhow
MCP consent history must be an object
Error message
MCP consent history must be an object
What it means
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.
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
Example fix
// before
[{"name":"fs","command":"npx"}]
// after
{"mcpServers":{"fs":{"command":"npx"}}} Defensive patterns
Strategy: validation
Validate before calling
const doc = JSON.parse(fs.readFileSync(path, "utf8"));
if (doc === null || typeof doc !== "object" || Array.isArray(doc)) { /* rewrite root as object */ } Type guard
function isJsonObject(v) { return typeof v === "object" && v !== null && !Array.isArray(v); } Try / catch
catch, then parse the file and confirm the root is an object; rewrite it as an object preserving valid keys before retrying
Prevention
- 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
When it happens
Trigger: `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.
Common situations: 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.
Understand the failure class
Background: "Invalid JSON response" and "Failed to parse response" errors: when an API answers 200 but the body isn't the JSON your library expected — this error's family across 28 libraries.
Related errors
- Invalid MCP consent history; contents omitted
- app_denied
- consent_required
- .annotations is not a valid MCP annotations object
- .size must be an integer
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/c62e7ce8f3743ae2.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/src/mcp/external_import.rs:300
}
/// Record decisions against the latest consent document under the shared
/// process lock. Malformed history is never silently replaced with empty state.
pub fn persist_decisions(
path: &Path,
candidates: &[ImportCandidate],
decisions: &HashMap<String, ImportDecision>,
now_unix: u64,
) -> anyhow::Result<()> {
super::validate_mcp_config_path(path)?;
codewhale_config::with_config_write_lock(path, |path| {
let original = super::read_mcp_config_file(path)?;
let mut raw: Value = match original.as_deref() {
Some(raw) => serde_json::from_str(raw)
.map_err(|_| anyhow::anyhow!("Invalid MCP consent history; contents omitted"))?,
None => serde_json::json!({}),
};
anyhow::ensure!(raw.is_object(), "MCP consent history must be an object");
let mut store: ImportConsentStore = if original.is_none() {
ImportConsentStore::default()
} else {
serde_json::from_value(raw.clone())
.map_err(|_| anyhow::anyhow!("Invalid MCP consent history; contents omitted"))?
};
let before = serde_json::to_value(&store)?;
record_decisions(&mut store, candidates, decisions, now_unix);
let after = serde_json::to_value(&store)?;
super::apply_json_delta(&mut raw, &before, &after);
let rendered = serde_json::to_vec_pretty(&raw)?;
if rendered.len() as u64 > super::MAX_MCP_CONFIG_BYTES {
anyhow::bail!("MCP consent history exceeds size limit");
}
crate::utils::write_atomic(path, &rendered)?;
Ok(())
})
}View on GitHub (pinned to 73e0f67d83)