ultraworkers/claw-code · error · std::io::Error
MCP response for {method} used mismatched id: expected {id:?
Error message
MCP response for {method} used mismatched id: expected {id:?}, got {:?} What it means
`McpStdioProcess::request` (runtime/src/mcp_stdio.rs:1314) requires the response id to equal the request id. `JsonRpcId` (mcp_stdio.rs:33) is an untagged enum {Number(u64), String, Null} with type-sensitive equality: a server echoing numeric `1` for a request sent as string `"1"` mismatches, as does any drifted value. Because request() awaits exactly one response, out-of-order delivery is not the usual cause — wrong echo is.
Source
Thrown at rust/crates/runtime/src/mcp_stdio.rs:1314
params: Option<TParams>,
) -> io::Result<JsonRpcResponse<TResult>> {
let method = method.into();
let request = JsonRpcRequest::new(id.clone(), method.clone(), params);
self.send_request(&request).await?;
let response = self.read_response().await?;
if response.jsonrpc != "2.0" {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"MCP response for {method} used unsupported jsonrpc version `{}`",
response.jsonrpc
),
));
}
if response.id != id {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"MCP response for {method} used mismatched id: expected {id:?}, got {:?}",
response.id
),
));
}
Ok(response)
}
pub async fn initialize(
&mut self,
id: JsonRpcId,
params: McpInitializeParams,
) -> io::Result<JsonRpcResponse<McpInitializeResult>> {
self.request(id, "initialize", Some(params)).await
}View on GitHub (pinned to 08106b0c37)
Solutions
- In the server, copy the request id verbatim — same JSON type and value — into the response.
- Prefer numeric u64 ids on the client side if the server stack loses string typing.
- If you control both ends, assert id equality in tests for every method.
Example fix
# before (server: hardcoded / coerced id)
{"jsonrpc":"2.0","id":1,"result":{}} # request id was "1" (string)
# after (server: echo the request id exactly)
{"jsonrpc":"2.0","id":"1","result":{}} Defensive patterns
Strategy: try-catch
Type guard
fn ids_compatible(sent: &JsonRpcId, got: &JsonRpcId) -> bool {
matches!((sent, got),
(JsonRpcId::Number(a), JsonRpcId::Number(b)) if a == b)
|| matches!((sent, got), (JsonRpcId::String(a), JsonRpcId::String(b)) if a == b)
|| matches!((sent, got), (JsonRpcId::Null, JsonRpcId::Null))
} Try / catch
match process.request(id.clone(), method, params).await {
Err(e) if e.kind() == io::ErrorKind::InvalidData
&& e.to_string().contains("mismatched id") => {
// server coerces/rewrites ids: switch client to numeric u64 ids or fix server echo
}
other => other,
} Prevention
- Echo the request id verbatim — same JSON type and value (string vs number is a mismatch here)
- Prefer numeric u64 ids when the server stack is loose about string typing
- Never let a framework auto-assign response ids
When it happens
Trigger: Server coercing ids to numbers (e.g. JSON.parse then re-emit, or Python int('1')); server echoing a hardcoded id like 0 or 1 for every response; server sending a notification-shaped response with a fresh id; responding to a server-initiated request with the client's id.
Common situations: Hand-rolled MCP servers that don't propagate the request id; frameworks that auto-assign sequential ids; string/number confusion across language boundaries (JS BigInt handling, Python str vs int).
Related errors
- MCP stdio stream closed while reading headers
- MCP stdio stream closed while reading line
- MCP response for {method} used unsupported jsonrpc version `
- missing Content-Length header
- MCP stdio stream closed while reading headers
AI-assisted analysis of ultraworkers/claw-code@08106b0c37 (2026-08-18).
Data as JSON: /api/errors/d43bb4e09aea4831.
Report an issue: GitHub.