Repository navigation
mcp: header-only 2026 request against unsupported server returns -32602 instead of -32022 #1346
Description
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 Requestand anUnsupportedProtocolVersionErrorlisting 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:
headerVersion == ""→ false (header is present, so this passes)metaVersion == ""→ true → returns-32602immediately
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
0d00ciallo0721 commentedon Oct 6, 2026
Before attempting a patch, could a maintainer clarify the intended precedence here? The reproduction has
Mcp-Protocol-Version: 2026-07-28but omits_meta.io.modelcontextprotocol/protocolVersionfrom 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 aHeaderMismatch(-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_metaboth declare2026-07-28against a server that only supports2025-11-25? Also, open PR #1345 (same reporter) touchesmcp/streamable.gofor adjacent version-validation behavior; please confirm the intended scope is independent of that work. I have not started a patch pending clarification.Enigmage commentedon Oct 6, 2026
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.