Hmbown/CodeWhale · error

MCP server ' ': initialize capabilities must be an object

Error message

MCP server '{server_name}': initialize capabilities must be an object

What it means

After validating serverInfo, validate_initialize_result checks the `capabilities` field of the MCP initialize response. If capabilities is present but is not a JSON object (e.g. a string, array, or null-shaped value), this bail fires. Capability presence (tools/resources keys) is how the client decides what the server supports, so it must be an object.

Solutions

  1. Fix the server to emit `capabilities` as a JSON object (possibly empty `{}`) in the initialize result.
  2. If the server genuinely has no capabilities, have it omit the field entirely (the client treats None as no capabilities).
  3. Inspect the child's raw initialize response to confirm the wire shape.
  4. Update the server to a spec-conformant version.

Example fix

// before
{"result":{"serverInfo":{...},"capabilities":"tools"}}
// after
{"result":{"serverInfo":{...},"capabilities":{"tools":{}}}}
Defensive patterns

Strategy: validation

Validate before calling

fn capabilities_ok(init: &serde_json::Value) -> bool {
    init.get("result").and_then(|r| r.get("capabilities"))
        .map_or(true, |c| c.is_null() || c.is_object())
}

Type guard

fn as_capabilities(v: &Value) -> Option<&serde_json::Map<String, Value>> {
    v.get("result")?.get("capabilities")?.as_object()
}

Try / catch

match spawn_with_timeouts(&config, handshake, request_timeout) {
    Ok(c) => c,
    Err(e) if e.to_string().contains("capabilities must be an object") => {
        eprintln!("server returned non-object capabilities; fix server output");
        return Err(e);
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: spawn_with_timeouts completes a handshake where the initialize result contains `capabilities` with a non-object JSON type (string, number, array, boolean).

Common situations: Servers that serialize capabilities incorrectly; hand-rolled servers returning `"capabilities": "none"`; buggy middleware mangling the response.

Related errors


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

Appendix: source

Thrown at crates/mcp/src/stdio_client.rs:464

    let server_info = response
        .get("serverInfo")
        .and_then(Value::as_object)
        .with_context(|| {
            format!("MCP server '{server_name}': initialize result omitted serverInfo")
        })?;
    for field in ["name", "version"] {
        if !server_info.get(field).is_some_and(Value::is_string) {
            bail!("MCP server '{server_name}': initialize serverInfo.{field} must be a string");
        }
    }

    match response.get("capabilities") {
        None => Ok(None),
        Some(Value::Object(capabilities)) => Ok(Some(ServerCapabilities {
            tools: capabilities.contains_key("tools"),
            resources: capabilities.contains_key("resources"),
        })),
        Some(_) => bail!("MCP server '{server_name}': initialize capabilities must be an object"),
    }
}

/// A live connection to one MCP server subprocess.
pub struct ChildProcessMcpClient {
    server_name: String,
    capabilities: Option<ServerCapabilities>,
    connection: Mutex<Connection>,
    request_timeout: Duration,
}

impl std::fmt::Debug for ChildProcessMcpClient {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("ChildProcessMcpClient")
            .field("server_name", &self.server_name)
            .finish_non_exhaustive()
    }
}

View on GitHub (pinned to 73e0f67d83)