Repository navigation
Client accepts JSON-RPC response containing both result and error #1283
Description
Describe the bug
The rmcp 3.4.0 client accepts a malformed JSON-RPC response containing both result and error. It resolves the pending tools/call as Ok(CallToolResult), using the result and silently ignoring the error.
JSON-RPC responses must contain either result or error, not both.
Environment
- rmcp: 3.4.0 (
rmcp-v3.4.0, commitfd7811f) - rustc: 1.98.1
- macOS arm64
- Protocol: MCP
2026-07-28 - Transports: stdio and Streamable HTTP
To reproduce
- Connect an rmcp client to an MCP server.
- Send a
tools/callrequest. - Have the server return a response shaped like:
{
"jsonrpc": "2.0",
"id": 1,
"result": { "content": [], "resultType": "complete" },
"error": { "code": -32603, "message": "injected error" }
}A minimal client call is:
let result = client
.call_tool(
CallToolRequestParams::new("malformed_message")
.with_arguments(
serde_json::json!({"variant": "result-with-error"})
.as_object()
.unwrap()
.clone(),
),
)
.await;
assert!(result.is_err());This was found while testing MCP Failure Lab 0.9.0. The malformed_message tool intentionally returned a response containing both result and error.
I reproduced it with the mcp-failure-lab@0.9.0 package and the v0.9.0 Git tag over stdio and Streamable HTTP.
Expected behavior
The client rejects the response as an invalid JSON-RPC message and returns an error for the pending request. The connection may remain usable afterward.
Actual behavior
call_tool returns Ok(CallToolResult). A subsequent normal request succeeds.
Additional context
The likely parser path is the untagged JsonRpcMessage enum: JsonRpcResponse accepts the result shape while Serde ignores the unexpected error field.
Other tested official clients, including C# ModelContextProtocol 2.2.0, reject the same wire response.
Activity
yinnho commentedon Sep 18, 2026
Reproduced on v3.4.0 — both the client face from this issue and the symmetric server face.
On the server side, a stock rmcp 3.4.0 server happily accepts a request that carries a stray
result/errornext tomethod:The response is indistinguishable from a clean ping. Root cause:
JsonRpcMessageis a#[serde(untagged)]enum with variant orderRequest → Response → Notification → Error. Untagged deserialization takes the first matching variant, so a response carrying bothresultanderrormatchesResponseand the strayerroris silently dropped as an unknown field (this issue); likewise a request or notification carrying a strayresult/errormatchesRequest/Notificationand the field is dropped the same way.Fix: a manual
Deserializeimpl that parses toserde_json::Value, dispatches on field presence first, and rejects the mutually exclusive combinations —result+errortogether, andmethod+result/error— before routing clean shapes to the existing per-variant deserialization (variant semantics otherwise unchanged byte for byte).deny_unknown_fieldsis not an option here since JSON-RPC 2.0 explicitly permits extension fields; the manual dispatch only rejects the conflicting combinations.Branch with the fix plus three new tests (response with both fields rejected; ping+result, ping+error, notification+result rejected; all four clean shapes + extension fields still deserialize): https://github.com/yinnho/rust-sdk/tree/fix/jsonrpc-result-error-mutual-exclusion (ed530fc, on top of fd7811f/v3.4.0).
cargo test -p rmcp --lib: 205 passed including the new tests. Full workspace: 1165 passed, 2 skipped (test_with_python_*needuv+ the Python MCP SDK). Wanted to open this as a PR, but GitHub is currently refusingcreatePullRequestagainst this repo for my account (FORBIDDEN) — feel free to pick it up from the branch, or I can retry the PR later.fix(model): reject malformed JSON-RPC messages with conflicting resul…
fix(model): reject malformed JSON-RPC messages with conflicting resul…
fix(model): reject malformed JSON-RPC messages with conflicting resul…
fix(model): reject malformed JSON-RPC messages with conflicting resul…
DaleSeo commentedon Oct 14, 2026
Fixed by #1287