You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Client accepts JSON-RPC response containing both result and error #1283
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.
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.
The response is indistinguishable from a clean ping. Root cause: JsonRpcMessage is a #[serde(untagged)] enum with variant order Request → Response → Notification → Error. Untagged deserialization takes the first matching variant, so a response carrying both result and error matches Response and the stray error is silently dropped as an unknown field (this issue); likewise a request or notification carrying a stray result/error matches Request/Notification and the field is dropped the same way.
Fix: a manual Deserialize impl that parses to serde_json::Value, dispatches on field presence first, and rejects the mutually exclusive combinations — result + error together, and method + result/error — before routing clean shapes to the existing per-variant deserialization (variant semantics otherwise unchanged byte for byte). deny_unknown_fields is 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_* need uv + the Python MCP SDK). Wanted to open this as a PR, but GitHub is currently refusing createPullRequest against this repo for my account (FORBIDDEN) — feel free to pick it up from the branch, or I can retry the PR later.
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