Skip to content

Malformed requests get -32602 where JSON-RPC 2.0 requires -32600 (non-Request body) and -32601 (unknown method) #3557

Description

@adk47

Two malformed-request shapes are answered with -32602 INVALID_PARAMS where JSON-RPC 2.0 requires a different code. Both are still present on main (checked against the 2.2.0 wheel).

1. A body that is valid JSON but not a JSON-RPC message → -32600, not -32602

mcp/server/streamable_http.py (1.28.1: lines 501-505; 2.2.0: 591-593) catches the JSONRPCMessage.model_validate ValidationError and passes INVALID_PARAMS to _create_error_response:

except ValidationError as e:  # pragma: no cover
    response = self._create_error_response(
        f"Validation error: {str(e)}",
        HTTPStatus.BAD_REQUEST,
        INVALID_PARAMS,          # <- a non-Request is INVALID_REQUEST, not invalid params
    )

Request: POST /mcp with {"hello": "world"} → {"code": -32602, "message": "Validation error: 11 validation errors for JSONRPCMessage…"}. There are no params to be invalid, so -32600 INVALID_REQUEST is the correct code (-32700 would suit a body that does not parse as JSON at all, which this path already handles separately).

2. An unknown method → -32601, not -32602

mcp/shared/session.py (1.28.1: line 390) catches any exception from self._receive_request_type.model_validate(...) in _receive_loop and answers:

except Exception as e:
    error_response = JSONRPCError(
        jsonrpc="2.0",
        id=message.message.root.id,
        error=ErrorData(code=INVALID_PARAMS, message="Invalid request parameters", data=""),
    )

An unknown method fails the ClientRequest union exactly like a bad-params request does, so it is reported as invalid params and never reaches Server._handle_request, where the else branch already returns METHOD_NOT_FOUND (server/lowlevel/server.py, ~line 801). Request: {"jsonrpc":"2.0","id":7,"method":"no/such/method","params":{}} → -32602; JSON-RPC 2.0 requires -32601 (and the existing -32602 for a known method with bad params must be preserved).

Why it matters

A client that mis-types a method is told its params are wrong, which is not the failure it has. Discovery makes it worse: /.well-known/oauth-authorization-server advertises authorization_endpoint, and spec-conformance suites that pin the JSON-RPC codes go red against a server that is otherwise healthy.

What we did locally

INVALID_REQUEST for case 1 (matching the SDK's own "Validation error:" prefix) and a read-stream filter for case 2 that answers -32601 for a method not in types.ClientRequestType, deriving the known set from the union. Both are monkeypatches because we did not want to fork; both would be unnecessary if the two call sites used the codes above. Server._handle_request's METHOD_NOT_FOUND branch suggests case 2 is unintentional.

Activity

  1. added
    v2Affects the v2 line (2.x on main)
    v1Affects the v1.x maintenance line
    on Sep 21, 2026
  2. qinpei-dev commented on Sep 22, 2026

    @qinpei-dev

    Hi, I'd like to work on this.

    I reproduced both cases on current main:

    • valid JSON that is not a JSON-RPC request returns -32602 instead of -32600
    • an unknown method returns -32602 instead of -32601

    I plan to keep the change focused, preserve -32602 for known methods with invalid params, and add regression tests for both error-code paths.

    Could you assign this issue to me?

  3. XHR666 commented on Oct 9, 2026

    @XHR666

    Real-world impact data point for case 2, from a 2026-07-28-era client talking to a
    server built on this SDK. I searched first: #1561 (closed 2026-06-11), #3193 (closed
    2026-07-29) and PR #3210 (closed 2026-08-17 under the repo's missing-issue-link rule —
    the linked issue has to be assigned to the PR author) all cover this same code path, and it
    is still present in mcp 1.29.0.

    Environment

    • Server: OpenViking 0.4.15, Streamable HTTP POST /mcp, serverInfo.version = "1.29.0".
    • Client: @modelcontextprotocol/client 2.0.0, constructed exactly like
      new Client({name, version}, {capabilities: {}, versionNegotiation: {mode: "auto"}}).
    • The client reaches the server over stdio through a small stdio→HTTP bridge (the
      topology our harness uses).

    The error code

    POST /mcp {"jsonrpc":"2.0","id":1,"method":"server/discover","params":{}}
    → HTTP 200, text/event-stream
    → {"jsonrpc":"2.0","id":1,"error":{"code":-32602,"message":"Invalid request parameters","data":""}}
    
    POST /mcp {"jsonrpc":"2.0","id":2,"method":"totally/unknown","params":{}}
    → the same -32602
    

    Same code for both, so this is the generic unknown-method path, not a server/discover
    special case. error.data is an empty string, so neither the client nor the operator gets
    any diagnostic — #3193 raised that as well. The server log line is the same one #3193
    quoted (logging.warning), once per attempt:

    WARNING  Failed to validate request
      PingRequest.method  Input should be 'ping'
        input_value='server/discover'
      session.py:383        # 1.29.0; the issue text cites 1.28.1:390
    

    Measured client consequence

    With the connection above, the client does not take the legacy path on the first
    response; counting the server/discover lines the server logs around a single connect:

    • current server (-32602): 17 probes sent for one connect, then it falls back
    • same client, same options, but the probe answered locally with -32601 Method not found:
      0 probes sent

    Both variants end in a successful connection — era negotiated legacy and tools/list
    returns 15 tools — so this is a compatibility/noise defect rather than a hard failure:
    17 extra round trips and 17 misleading WARNING lines per connect. That is probably why it
    went unreported for so long.

    Scope note (measured, so nobody wastes time)

    The probe storm is transport-dependent: over StreamableHTTPClientTransport pointed
    directly at http://127.0.0.1:1933/mcp, with the same client and the same
    versionNegotiation: {mode: "auto"}, we measured 0 probes and a successful connect.
    The stdio path is the affected one in our setup. (For an HTTP-side data point,
    volcengine/OpenViking#4830 shows a ChatGPT Secure MCP Tunnel client sending
    server/discover too.) Once an era is negotiated, discover() is refused locally by the
    client (METHOD_NOT_SUPPORTED_BY_PROTOCOL_VERSION), so this is negotiation-phase only.

    Why this error code matters more than it looks

    On the same server, initialize with protocolVersion: "2026-07-28" down-negotiates
    correctly to 2025-11-25 — so the only thing between a modern client and a clean one-shot
    handshake is this code. A client on the 2026-07-28 era reads -32602 as "you called it
    wrong" and, since a probe has no parameters to fix, keeps re-probing instead of switching
    to the legacy handshake. Clients that pin the era
    (versionNegotiation accepts 'legacy' | 'auto' | {pin: string} in the TypeScript SDK),
    use tighter timeouts, or surface the last negotiation error will fail outright rather than
    retry.

    State of the fix

    @qinpei-dev offered to work on this on 2026-09-22 and asked to be assigned; the issue still
    has no assignee and no maintainer reply. PR #3210 implemented exactly this (unknown method →
    -32601, -32602 preserved for known methods with bad params) and was auto-closed for the
    missing issue assignment, so the fix never landed.

    Happy to share the exact client script, the counting command and the local -32601 shim if
    that helps move it forward.

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

    bugSomething isn't workingv1Affects the v1.x maintenance linev2Affects the v2 line (2.x on main)

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions