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
- Fix the server to emit `capabilities` as a JSON object (possibly empty `{}`) in the initialize result.
- If the server genuinely has no capabilities, have it omit the field entirely (the client treats None as no capabilities).
- Inspect the child's raw initialize response to confirm the wire shape.
- 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
- Assert the initialize response shape in the server's own tests.
- Use a JSON-RPC/MCP schema validator on server output during development.
- Never emit capabilities as a string or array; use an object or omit it.
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
- MCP server ' ': initialize serverInfo. must be a string
- .inputSchema is required for an advertised MCP tool
- .inputSchema must be a valid object-shaped MCP input schema
- MCP server ' ': initialize capabilities must be an object
- MCP server ' ' initialize result omitted protocolVersion
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)