Skip to content

Validate MCP-Protocol-Version header in StreamableHTTPTransport#347

Merged
koic merged 1 commit into
modelcontextprotocol:mainfrom
koic:validate_mcp-protocol-version_header_in_streamable_http_transport
May 14, 2026
Merged

Validate MCP-Protocol-Version header in StreamableHTTPTransport#347
koic merged 1 commit into
modelcontextprotocol:mainfrom
koic:validate_mcp-protocol-version_header_in_streamable_http_transport

Conversation

@koic
Copy link
Copy Markdown
Member

@koic koic commented May 13, 2026

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:

{
  "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 #346.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

## 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.
@koic koic merged commit d6b5392 into modelcontextprotocol:main May 14, 2026
11 checks passed
@koic koic deleted the validate_mcp-protocol-version_header_in_streamable_http_transport branch May 14, 2026 15:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Streamable HTTP accepts unsupported or malformed MCP-Protocol-Version headers instead of returning 400

2 participants