Skip to content

mcp: header-only 2026 request against unsupported server returns -32602 instead of -32022 #1346

Closed

Description

@Enigmage

What I did

Configured a server with ServerOptions.SupportedProtocolVersions = []string{"2025-11-25"}.
Sent a tools/list request with only the Mcp-Protocol-Version header (no _meta in body):

POST /mcp
Content-Type: application/json
Accept: application/json, text/event-stream
Mcp-Protocol-Version: 2026-07-28

{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}

Reproduced with tools/list, tools/call, and server/discover.

What I saw

HTTP 400, error code -32602 (InvalidParams):

{"jsonrpc":"2.0","id":1,"error":{"code":-32602,"message":"missing or invalid _meta field \"io.modelcontextprotocol/protocolVersion\""}}

What I expected

HTTP 400, error code -32022 (UnsupportedProtocolVersion):

{"jsonrpc":"2.0","id":1,"error":{"code":-32022,"message":"...","data":{"supported":["2025-11-25"],"requested":"2026-07-28"}}}

SDK version

v1.8.0 and current main.

Go version

go1.26.2

Why this is a spec violation

The MCP 2026-07-28 streamable HTTP spec states:

"If the server does not implement the requested protocol version (whether the version is unknown to the server, or is a known version the server has chosen not to support), it MUST respond with 400 Bad Request and an UnsupportedProtocolVersionError listing its supported versions."

No exception is made for cases where _meta is also absent. The version check is independent of the _meta check. Returning -32602 instead of -32022 violates this MUST.

The backward-compatibility section of the spec describes clients inspecting the error body to distinguish "unsupported version" (retry with a different version) from "bad request" (fix the request). Returning -32602 breaks this client fallback logic — the client cannot tell if its version is rejected or its request is malformed.

Root cause

In streamable.go, the per-message dispatch loop (around line 1704) enters the SEP-2575 validation branch whenever protocolVersion >= "2026-07-28" (from the Mcp-Protocol-Version header). Inside that branch:

  1. headerVersion == "" → false (header is present, so this passes)
  2. metaVersion == "" → true → returns -32602 immediately

The version-supported check (-32022) is never reached because metaVersion is empty — the request has no _meta. The supported-list check sits downstream in server.go's handle(), past the _meta validation gate.

Note on #1173

Issue #1173 (now closed) covered a different path: the Mcp-Protocol-Version header with a version sorting below 2026-07-28 returns a plain-text 400 instead of -32022. That path is streamable.go:412. This issue is the complementary case: a header version sorting at or above 2026-07-28, where the version is unsupported, returns -32602 instead of -32022. Different branch, same class of problem.

Proposed fix

In the per-message dispatch block in streamable.go, before checking metaVersion, check whether headerVersion is in the server's supported set. If not, return -32022:

if (jreq.IsCall() && protocolVersion >= protocolVersion20260728) || metaVersion != "" {
    headerVersion := protocolVersionFromContext(req.Context())
    // Check version support before validating _meta presence.
    if headerVersion != "" && !slices.Contains(supportedVersions, headerVersion) {
        data, _ := json.Marshal(UnsupportedProtocolVersionData{
            Supported: supportedVersions,
            Requested: headerVersion,
        })
        writeJSONRPCError(w, http.StatusBadRequest, jreq.ID, &jsonrpc.Error{
            Code:    CodeUnsupportedProtocolVersion,
            Message: fmt.Sprintf("protocol version %q is not supported", headerVersion),
            Data:    data,
        })
        return
    }
    // ... existing metaVersion == "" check follows
}

Happy to open a PR if the approach is confirmed.

Activity

  1. 0d00ciallo0721 commented on Oct 6, 2026

    @0d00ciallo0721

    Before attempting a patch, could a maintainer clarify the intended precedence here? The reproduction has Mcp-Protocol-Version: 2026-07-28 but omits _meta.io.modelcontextprotocol/protocolVersion from the request body. The 2026-07-28 spec says every request declares its version in _meta, the HTTP header must match that body value, and a missing/mismatched required value is a HeaderMismatch (-32020); UnsupportedProtocolVersion (-32022) is specified for a declared version the server does not support. Should this issue instead cover a well-formed request whose header and _meta both declare 2026-07-28 against a server that only supports 2025-11-25? Also, open PR #1345 (same reporter) touches mcp/streamable.go for adjacent version-validation behavior; please confirm the intended scope is independent of that work. I have not started a patch pending clarification.

  2. Enigmage commented on Oct 6, 2026

    @Enigmage
    Author

    Hi @0d00ciallo0721, you are right, traced through the code more carefully after your comment.

    For a well-formed request (Mcp-Protocol-Version: 2026-07-28 header and _meta.io.modelcontextprotocol/protocolVersion: "2026-07-28" in the body) against a SupportedProtocolVersions: ["2025-11-25"] server, the SDK already returns -32022 correctly, the check at server.go:2018 handles this path. We also verified this live on a cluster with a 2025-11-25-only server, which returned:

    {
      "jsonrpc": "2.0",
      "id": 6,
      "error": {
        "code": -32022,
        "message": "unsupported protocol version",
        "data": { "supported": ["2025-11-25"], "requested": "2026-07-28" }
      }
    }

    The reproduction I filed uses only the header without _meta, which is a malformed request by the spec's own rules. Your reading is correct: a missing _meta version is a HeaderMismatch situation, not an UnsupportedProtocolVersion situation. The well-formed case is already handled correctly so there is nothing to cover here either. Closing as not a bug. Apologies for the noise.

    Regarding scope: #1345 and this issue operate on mutually exclusive paths, #1345 fires on requests with no header and no _meta (legacy format, usesNewProtocol == false); this issue was about a request with a header but no _meta (malformed modern format, usesNewProtocol == true). No overlap.

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions