Hmbown/CodeWhale · error
child returned a malformed MCP CallToolResult
Error message
child returned a malformed MCP CallToolResult
What it means
stdio_tool_call_result normalizes the child's response into a standard MCP CallToolResult. If the child claims a CallToolResult shape (looks_like_call_tool_result) but its contents fail valid_call_tool_result validation, the proxy refuses to pass it through. This keeps malformed results from propagating to callers that expect the MCP envelope contract.
Solutions
- Fix or update the MCP server so its CallToolResult conforms to the MCP spec (valid content array and required fields).
- Pin/downgrade or upgrade the server version to one matching the proxy's expected envelope shape.
- Capture the raw child response (the legacy nested 'result' path) and validate it manually to find the offending field before filing/fixing the server bug.
Defensive patterns
Strategy: type-guard
Validate before calling
fn result_is_conformant(v: &serde_json::Value) -> bool {
v.get("content").and_then(|c| c.as_array()).map(|arr|
arr.iter().all(|item| item.get("type").and_then(|t| t.as_str()).is_some()))
.unwrap_or(false)
} Type guard
fn is_valid_call_tool_result(fields: &serde_json::Map<String, serde_json::Value>) -> bool {
valid_call_tool_result(fields)
}
// guard the child response before trusting it Try / catch
match manager.call_tool(server, tool, args) {
Err(e) if e.to_string().contains("malformed MCP CallToolResult") => {
tracing::error!("server {server} returned non-conformant result: {e}");
// capture raw response for a bug report; do not retry blindly
}
other => other,
} Prevention
- Pin the MCP server to a spec-conformant version.
- Add an integration check that calls each tool once and validates the envelope.
- Never hand-roll CallToolResult serialization server-side; use the official SDK.
When it happens
Trigger: Calling a tool over the stdio proxy where the child's JSON response object resembles a CallToolResult (has the recognizable fields) but contains invalid values per valid_call_tool_result (e.g., wrong-typed content entries or missing required fields).
Common situations: MCP server implementation bug emitting non-conformant content arrays; version drift where the child uses an older/newer CallToolResult shape; hand-rolled servers that emit the envelope keys with wrong types.
Understand the failure class
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- child stdout was not valid UTF-8
- connection stdin poisoned by an earlier panic
- failed to read bounded stdio JSON-RPC frame
- failed to read bounded stdio JSON-RPC frame
- JSON-RPC line exceeded the
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/f7661e9938f6bea8.
Report an issue: GitHub.
Appendix: source
Thrown at crates/mcp/src/lib.rs:1175
"name": qualified_name.clone(),
"inputSchema": input_schema,
// Retain the pre-0.9.11 management fields for compatibility.
"server_name": server_name,
"tool_name": tool_name,
"qualified_name": qualified_name,
});
if let Some(description) = description {
value["description"] = Value::String(description);
}
value
}
fn stdio_tool_call_result(result: Value) -> Result<Value> {
let legacy_result = result.clone();
match result {
Value::Object(mut fields) if looks_like_call_tool_result(&fields) => {
if !valid_call_tool_result(&fields) {
bail!("child returned a malformed MCP CallToolResult");
}
// The child already returned a standard MCP CallToolResult. Expose
// it directly, while retaining the old nested result for clients
// that used the proxy before its MCP envelope was corrected.
fields.insert("result".to_string(), legacy_result);
Ok(Value::Object(fields))
}
value => Ok(json!({
"content": [{"type": "text", "text": legacy_value_text(&value)}],
"result": legacy_result
})),
}
}
fn stdio_resource_descriptor((resource, metadata): (McpResourceDescriptor, Value)) -> Value {
let McpResourceDescriptor {
server_name,
uri,View on GitHub (pinned to 73e0f67d83)