Hmbown/CodeWhale · error

MCP server ' ': received a child message without jsonrpc…

Error message

MCP server '{server}': {method} received a child message without jsonrpc "2.0"

What it means

After parsing a stdout line as JSON, request() requires every message to carry `"jsonrpc": "2.0"`. A message reaching the rendezvous without the correct jsonrpc version fails fast with this error, since it indicates a non-conformant peer rather than a matching response.

Solutions

  1. Fix the server to include `"jsonrpc":"2.0"` on every response message, per the MCP/JSON-RPC 2.0 spec.
  2. Inspect the child's stdout to see the offending message.
  3. Update or replace non-conformant server implementations.
  4. If a proxy rewrites messages, ensure it preserves the jsonrpc field.

Example fix

// before
{"id":1,"result":{...}}
// after
{"jsonrpc":"2.0","id":1,"result":{...}}
Defensive patterns

Strategy: validation

Validate before calling

fn is_jsonrpc_20(msg: &serde_json::Value) -> bool {
    msg.get("jsonrpc").and_then(serde_json::Value::as_str) == Some("2.0")
}
// validate a captured line before feeding it through the client in tests

Try / catch

match client.request(method, params, timeout) {
    Ok(v) => v,
    Err(e) if e.to_string().contains("without jsonrpc") => {
        eprintln!("server sent a non-2.0 JSON-RPC message; check server implementation");
        Err(e)
    }
    Err(e) => Err(e),
}

Prevention

When it happens

Trigger: The child sends a well-formed JSON line over stdout that lacks the jsonrpc field or has a value other than "2.0" (and is not filtered earlier by the reader as a notification/request).

Common situations: Servers implementing a different JSON-RPC dialect or version; hand-rolled servers omitting the jsonrpc field in responses; middleware rewriting messages.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/d5432189497953e4. Report an issue: GitHub.

Appendix: source

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

                    bail!("MCP server '{server}': {method} timed out after {timeout:?}");
                }
                Err(RecvTimeoutError::Disconnected) => {
                    bail!(
                        "MCP server '{server}': process closed stdout before answering {method}{}",
                        self.exit_note()
                    );
                }
            };

            // Servers occasionally emit banners or log lines on stdout. They
            // are skipped; only the matching response ends the wait. Valid
            // requests and notifications are handled by the reader before
            // they reach this rendezvous.
            let Ok(message) = serde_json::from_str::<Value>(&line) else {
                continue;
            };
            if message.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
                bail!(
                    "MCP server '{server}': {method} received a child message without jsonrpc \"2.0\""
                );
            }
            if message.get("id").and_then(Value::as_u64) != Some(id) {
                continue;
            }
            if let Some(error) = message.get("error") {
                bail!("MCP server '{server}': {method} failed: {error}");
            }
            return message.get("result").cloned().with_context(|| {
                format!(
                    "MCP server '{server}': {method} response contained neither result nor error"
                )
            });
        }
    }

    /// The child's exit status, when it has one, for appending to a failure

View on GitHub (pinned to 73e0f67d83)