Hmbown/CodeWhale · error · anyhow::Error

Failed to parse MCP config; file contents were omitted

Error message

Failed to parse MCP config; file contents were omitted

What it means

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.

Solutions

  1. 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
  2. Delete or rename the broken file and let init_config (or the app) regenerate a fresh template, then re-add servers via supported commands
  3. Restore the file from backup/version control if the edit was accidental

Example fix

// before (mcp.json, invalid: trailing comma)
{ "mcpServers": { "fs": { "command": "npx", } } }
// after
{ "mcpServers": { "fs": { "command": "npx" } } }
Defensive patterns

Strategy: validation

Validate before calling

use std::fs;
fn mcp_config_is_valid_json(path: &std::path::Path) -> bool {
    fs::read_to_string(path)
        .ok()
        .map(|s| serde_json::from_str::<serde_json::Value>(&s).is_ok())
        .unwrap_or(false)
}

Type guard

fn is_json(text: &str) -> bool {
    serde_json::from_str::<serde_json::Value>(text).is_ok()
}

Try / catch

match set_server_enabled(&path, name, true) {
    Err(e) if e.to_string().contains("Failed to parse MCP config") => {
        eprintln!("config file is not valid JSON; validate with jq and fix it");
    }
    Err(e) => return Err(e),
    Ok(()) => {}
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Background: JSON parse error: "Unexpected token" / "not valid JSON" / "failed to parse" — what JSON parsers are really complaining about — this error's family across 45 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/7918b148140c5745. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/src/mcp.rs:5834

}

/// Every managed MCP writer rereads under the same OS-process lock. This is
/// a delta operation, not a save of a previously loaded typed snapshot.
pub fn mutate_config<T>(
    path: &Path,
    expected_revision: Option<&str>,
    mutate: impl FnOnce(&mut McpConfig) -> Result<T>,
) -> Result<(T, String)> {
    validate_mcp_config_path(path)?;
    codewhale_config::with_config_write_lock(path, |path| {
        let original = read_mcp_config_file(path)?;
        let revision = config_revision(original.as_deref());
        if expected_revision.is_some_and(|expected| expected != revision) {
            return Err(McpRevisionConflict.into());
        }
        let mut raw: serde_json::Value = match original.as_deref() {
            Some(raw) => serde_json::from_str(raw).map_err(|_| {
                anyhow::anyhow!("Failed to parse MCP config; file contents were omitted")
            })?,
            None => serde_json::json!({}),
        };
        anyhow::ensure!(raw.is_object(), "MCP config must be an object");
        let mut config: McpConfig = serde_json::from_value(raw.clone())
            .map_err(|_| anyhow::anyhow!("Invalid MCP config; file contents were omitted"))?;
        let before = serde_json::to_value(&config)?;
        let result = mutate(&mut config)?;
        let after = serde_json::to_value(&config)?;
        if before == after {
            return Ok((result, revision));
        }
        // Preserve legacy spelling while applying the canonical typed delta.
        let legacy = raw.get("mcpServers").is_some();
        if legacy {
            let object = raw
                .as_object_mut()
                .context("MCP config must be an object")?;

View on GitHub (pinned to 73e0f67d83)