Hmbown/CodeWhale · error

MCP server ' ': initialize capabilities must be an object

Error message

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

What it means

After validating protocolVersion and serverInfo, `validate_initialize_result` reads the optional `capabilities` field of the `initialize` response. If it is present but not a JSON object, the client bails rather than guessing capability support. Capabilities are consulted later to decide whether tools/resources calls are legal, so a malformed value must fail the handshake.

Solutions

  1. Fix the server to emit `capabilities` as a JSON object, e.g. {"tools":{},"resources":{}}.
  2. If the server has no capabilities to advertise, omit the `capabilities` field entirely instead of sending a non-object value (the client treats absence as Ok(None)).
  3. Validate the server's initialize response against the MCP JSON schema.
  4. Check whether an intermediary (proxy, gateway) is transforming the response shape.

Example fix

// before: capabilities as an array
{"capabilities":["tools","resources"]}
// after: capabilities as an object
{"capabilities":{"tools":{},"resources":{}}}
Defensive patterns

Strategy: validation

Validate before calling

fn capabilities_ok(v: &serde_json::Value) -> bool {
    match v.get("capabilities") {
        None => true,
        Some(c) => c.is_object(),
    }
}

Type guard

fn is_capabilities_object(v: &serde_json::Value) -> bool {
    v.get("capabilities").map_or(true, serde_json::Value::is_object)
}

Try / catch

match spawn_with_timeouts(&config, hs, req).await {
    Err(e) if e.to_string().contains("capabilities must be an object") => {
        eprintln!("server encodes capabilities wrongly; omit or use an object");
    }
    other => other,
}

Prevention

When it happens

Trigger: `spawn_with_timeouts` receives an `initialize` JSON result where `capabilities` is present but is a string, number, array, bool, or null-typed JSON value instead of an object, e.g. `"capabilities": []`.

Common situations: A custom MCP server serializing capabilities as an array of names (e.g. `["tools","resources"]`) instead of an object; a server emitting `"capabilities": null` from a template; a proxy that flattens the object.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@433685b202 (2026-09-15). Data as JSON: /api/errors/bb90444dbe2e331f. Report an issue: GitHub.

Appendix: source

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

    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 433685b202)