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
- Fix the server to emit `capabilities` as a JSON object, e.g. {"tools":{},"resources":{}}.
- 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)).
- Validate the server's initialize response against the MCP JSON schema.
- 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
- Emit capabilities as a JSON object (or omit the field entirely if none).
- Never serialize capabilities as an array or string, even informally.
- Cover the initialize response shape in the server's integration tests.
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
- MCP server ' ': initialize serverInfo. must be a string
- MCP server ' ' initialize result omitted protocolVersion
- MCP server ' ' negotiated unsupported protocol version ' '…
- MCP server ' ': initialize capabilities must be an object
- MCP server ' ': initialize serverInfo. must be a string
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)