Hmbown/CodeWhale · error · anyhow::Error

MCP config must be an object

Error message

MCP config must be an object

What it means

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).

Solutions

  1. Wrap the config top level in an object: {"mcpServers": { ... }} (or the servers map shape the app expects) and retry
  2. If the file is an export/list meant for import, use the external import path instead of pointing the app config at it
  3. Reset the file with init_config --force to regenerate a valid template

Example fix

// before (valid JSON, wrong shape)
[{ "name": "fs", "command": "npx" }]
// after
{ "mcpServers": { "fs": { "command": "npx" } } }
Defensive patterns

Strategy: validation

Validate before calling

fn top_level_is_object(path: &std::path::Path) -> bool {
    std::fs::read_to_string(path).ok()
        .and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok())
        .map(|v| v.is_object())
        .unwrap_or(false)
}

Type guard

fn as_config_object(v: &serde_json::Value) -> Option<&serde_json::Map<String, serde_json::Value>> {
    v.as_object()
}

Try / catch

match result {
    Err(e) if e.to_string() == "MCP config must be an object" => {
        eprintln!("top level of mcp config must be {{...}}, got another JSON kind");
    }
    _ => {}
}

Prevention

When it happens

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

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

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


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

Appendix: source

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

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")?;
            let servers = object.remove("mcpServers").expect("checked above");
            object.insert("servers".into(), servers);
        }
        apply_json_delta(&mut raw, &before, &after);

View on GitHub (pinned to 73e0f67d83)