Skip to content

Client accepts JSON-RPC response containing both result and error #1283

Closed

Description

@anilloutombam

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, commit fd7811f)
  • rustc: 1.98.1
  • macOS arm64
  • Protocol: MCP 2026-07-28
  • Transports: stdio and Streamable HTTP

To reproduce

  1. Connect an rmcp client to an MCP server.
  2. Send a tools/call request.
  3. 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

  1. yinnho commented on Sep 18, 2026

    @yinnho

    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/error next to method:

    → {"jsonrpc":"2.0","id":2,"method":"ping","result":{"injected":true},"error":{"code":-32603,"message":"boom"}}
    ← {"jsonrpc":"2.0","id":2,"result":{}}
    

    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.

  2. added
    P1High: significant functionality gap or spec violation
    T-modelModel/data structure changes
    T-serviceService layer changes
    ready for workIssue is well-defined and ready to be picked up
    on Sep 18, 2026
  3. DaleSeo commented on Oct 14, 2026

    @DaleSeo
    Member

    Fixed by #1287

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1High: significant functionality gap or spec violationT-modelModel/data structure changesT-serviceService layer changesbugSomething is not workingready for workIssue is well-defined and ready to be picked up

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions