{"record":{"id":"7918b148140c5745","repo":"Hmbown/CodeWhale","slug":"failed-to-parse-mcp-config-file-contents-were-omitted","errorCode":null,"errorMessage":"Failed to parse MCP config; file contents were omitted","messagePattern":"Failed to parse MCP config; file contents were omitted","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/tui/src/mcp.rs","lineNumber":5834,"sourceCode":"}\n\n/// Every managed MCP writer rereads under the same OS-process lock. This is\n/// a delta operation, not a save of a previously loaded typed snapshot.\npub 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\")?;","sourceCodeStart":5816,"sourceCodeEnd":5852,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/mcp.rs#L5816-L5852","documentation":"This error comes from mutate_config in crates/tui/src/mcp.rs when it reads the existing MCP config file to apply a mutation. The raw file text failed serde_json::from_str, meaning the config file on disk is not valid JSON. The message intentionally omits the file contents (they may contain secrets like bearer tokens), so it never tells you what the bad text was.","triggerScenarios":"Any mutation entry point that routes through mutate_config (add/update/remove server, set_server_enabled, etc.) when the MCP config file exists and its text cannot be parsed as JSON — e.g. trailing commas, comments, BOM, truncated write, or a hand-edited file.","commonSituations":"Hand-editing mcp config and leaving a trailing comma or comment; a concurrent write or crash left a truncated file; copying a config5-style TOML content into the JSON file; editor saved with a BOM.","solutions":["Open the MCP config file and validate it with a JSON parser (jq . mcp.json or python -m json.tool) to find the syntax error and fix it","Delete or rename the broken file and let init_config (or the app) regenerate a fresh template, then re-add servers via supported commands","Restore the file from backup/version control if the edit was accidental"],"exampleFix":"// before (mcp.json, invalid: trailing comma)\n{ \"mcpServers\": { \"fs\": { \"command\": \"npx\", } } }\n// after\n{ \"mcpServers\": { \"fs\": { \"command\": \"npx\" } } }","handlingStrategy":"validation","validationCode":"use std::fs;\nfn mcp_config_is_valid_json(path: &std::path::Path) -> bool {\n    fs::read_to_string(path)\n        .ok()\n        .map(|s| serde_json::from_str::<serde_json::Value>(&s).is_ok())\n        .unwrap_or(false)\n}","typeGuard":"fn is_json(text: &str) -> bool {\n    serde_json::from_str::<serde_json::Value>(text).is_ok()\n}","tryCatchPattern":"match set_server_enabled(&path, name, true) {\n    Err(e) if e.to_string().contains(\"Failed to parse MCP config\") => {\n        eprintln!(\"config file is not valid JSON; validate with jq and fix it\");\n    }\n    Err(e) => return Err(e),\n    Ok(()) => {}\n}","preventionTips":["Validate the config file with jq/python -m json.tool after every hand edit","Never put comments or trailing commas in the MCP config — it is strict JSON","Let the app's add/update commands edit the file instead of hand-editing","Keep the config in version control so a corrupt edit is easy to revert"],"tags":["mcp","config","json"],"backgroundTag":"json-parse-error","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-22T06:17:15.046Z"}