Validate MCP-Protocol-Version header in StreamableHTTPTransport#347
Merged
Conversation
## Motivation and Context
MCP 2025-11-25 (basic/transports#protocol-version-header) requires servers
to respond with `400 Bad Request` when receiving a request with an invalid
or unsupported `MCP-Protocol-Version` header:
> If the server receives a request with an invalid or unsupported
> `MCP-Protocol-Version`, it MUST respond with `400 Bad Request`.
The Ruby SDK was not reading the header at all, accepting any value
(including malformed strings like `not-a-version` and unsupported versions
like `1900-01-01`) and dispatching requests normally with HTTP 200.
## Behavior
`StreamableHTTPTransport` now validates the `MCP-Protocol-Version` header
on POST (non-initialize), GET, and DELETE requests. Missing headers are
accepted as before; the spec SHOULD requirement to default to `2025-03-26`
is intentionally left for a follow-up. The `initialize` POST is exempt
because the client does not know the negotiated version until the response
arrives, matching the Python (`src/mcp/server/streamable_http.py`) and
TypeScript (`packages/server/src/server/streamableHttp.ts`) SDKs.
The error response uses a JSON-RPC envelope with `code: -32600`
(`INVALID_REQUEST`) to match the Python SDK shape:
```json
{
"jsonrpc": "2.0",
"id": null,
"error": {
"code": -32600,
"message": "Bad Request: Unsupported protocol version: <value>. Supported versions: ..."
}
}
```
Validation order is session then protocol version, matching the Python and
TypeScript SDKs.
`handle_post` was reorganized so that non-Hash request bodies (e.g., JSON
arrays, which MCP 2025-11-25 no longer supports as batches) also pass
through the header check. The pre-existing broken response for Array
bodies sent with a valid header is unchanged and is tracked as a separate
follow-up.
## How Has This Been Tested?
Added regression tests covering:
- POST `initialize` ignores the header (bypass)
- POST non-initialize with unsupported / malformed / valid / missing values
- POST array body with unsupported value returns 400
- GET with unsupported / missing values
- DELETE with unsupported value in both stateful and stateless modes
- DELETE validates session before protocol version
`bundle exec rake test` and `bundle exec rake rubocop` both pass.
## Breaking Changes
Strictly additive for compliant clients. Third-party clients sending stale
or malformed `MCP-Protocol-Version` values will now receive `400` instead
of `200`, which is the intended spec behavior.
Fixes modelcontextprotocol#346.
atesgoral
approved these changes
May 14, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation and Context
MCP 2025-11-25 (basic/transports#protocol-version-header) requires servers to respond with
400 Bad Requestwhen receiving a request with an invalid or unsupportedMCP-Protocol-Versionheader:The Ruby SDK was not reading the header at all, accepting any value (including malformed strings like
not-a-versionand unsupported versions like1900-01-01) and dispatching requests normally with HTTP 200.Behavior
StreamableHTTPTransportnow validates theMCP-Protocol-Versionheader on POST (non-initialize), GET, and DELETE requests. Missing headers are accepted as before; the spec SHOULD requirement to default to2025-03-26is intentionally left for a follow-up. TheinitializePOST is exempt because the client does not know the negotiated version until the response arrives, matching the Python (src/mcp/server/streamable_http.py) and TypeScript (packages/server/src/server/streamableHttp.ts) SDKs.The error response uses a JSON-RPC envelope with
code: -32600(INVALID_REQUEST) to match the Python SDK shape:{ "jsonrpc": "2.0", "id": null, "error": { "code": -32600, "message": "Bad Request: Unsupported protocol version: <value>. Supported versions: ..." } }Validation order is session then protocol version, matching the Python and TypeScript SDKs.
handle_postwas reorganized so that non-Hash request bodies (e.g., JSON arrays, which MCP 2025-11-25 no longer supports as batches) also pass through the header check. The pre-existing broken response for Array bodies sent with a valid header is unchanged and is tracked as a separate follow-up.How Has This Been Tested?
Added regression tests covering:
initializeignores the header (bypass)bundle exec rake testandbundle exec rake rubocopboth pass.Breaking Changes
Strictly additive for compliant clients. Third-party clients sending stale or malformed
MCP-Protocol-Versionvalues will now receive400instead of200, which is the intended spec behavior.Fixes #346.
Types of changes
Checklist