Skip to content

Lowercase response header names in StreamableHTTPTransport#460

Open
koic wants to merge 1 commit into
modelcontextprotocol:mainfrom
koic:lowercase_response_header_names
Open

Lowercase response header names in StreamableHTTPTransport#460
koic wants to merge 1 commit into
modelcontextprotocol:mainfrom
koic:lowercase_response_header_names

Conversation

@koic

@koic koic commented Jul 16, 2026

Copy link
Copy Markdown
Member

Motivation and Context

The Rack 3 SPEC requires response header names to be lowercase. https://github.com/rack/rack/blob/main/SPEC.rdoc says in its "The Headers" section:

Header keys must not contain uppercase ASCII characters (A-Z).

https://github.com/rack/rack/blob/main/SPEC.rdoc#the-headers

However, StreamableHTTPTransport emitted capitalized names such as Content-Type and Mcp-Session-Id. Any Rack::Lint-wrapped stack, including the default development mode of rackup, rejects every response from the transport with a LintError 500. Lowercasing the names restores Rack 3 compliance. HTTP header names are case-insensitive on the wire, so HTTP clients are unaffected; only in-process consumers that read the raw Rack headers hash see the new keys.

Lowercase is also the only representation valid across HTTP versions: RFC 9113 makes uppercase field names a protocol error on HTTP/2, so proxies and h2-capable servers already require it. The TypeScript SDK sets headers['mcp-session-id'] and the Python SDK defines MCP_SESSION_ID_HEADER = "mcp-session-id", so lowercase also matches what the reference SDKs emit; the MCP spec's Mcp-Session-Id casing is prose convention, not a wire requirement.

Examples and documentation deliberately keep the spec's Mcp-Session-Id casing where it does not name a Rack response hash key: curl invocations and examples/streamable_http_client.rb set request headers, which the Rack response rule does not govern; the rack-cors expose: entry carries a header name as an Access-Control-Expose-Headers value, which browsers match case-insensitively; and README/docs prose follows the MCP specification's documentation convention. Only code reading the raw Rack response headers hash, a case-sensitive Hash lookup, had to switch to lowercase keys.

AGENTS.md now records the lowercase rule under the code style guidelines, since the MCP spec's Mcp-Session-Id prose casing invites reintroducing capitalized names in future transport work.

How Has This Been Tested?

Added a regression test asserting that header names are lowercase in initialize, GET SSE, DELETE, and parse error responses. Existing tests that read the raw headers hash were updated to the new keys. Verified with rake test, rake rubocop, and rake conformance.

Breaking Changes

Code that reads the raw Rack response headers hash returned by handle_request, or by middleware around the mounted transport, with exact-case keys such as "Mcp-Session-Id" must switch to the lowercase keys. Clients speaking HTTP are unaffected because header names are case-insensitive on the wire.

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

The Rack 3 SPEC requires response header names to be lowercase.
https://github.com/rack/rack/blob/main/SPEC.rdoc says in its "The Headers" section:

> Header keys must not contain uppercase ASCII characters (A-Z).

https://github.com/rack/rack/blob/main/SPEC.rdoc#the-headers

However, `StreamableHTTPTransport` emitted capitalized names such as `Content-Type` and `Mcp-Session-Id`.
Any `Rack::Lint`-wrapped stack, including the default development mode of rackup, rejects every response
from the transport with a `LintError` 500. Lowercasing the names restores Rack 3 compliance.
HTTP header names are case-insensitive on the wire, so HTTP clients are unaffected;
only in-process consumers that read the raw Rack headers hash see the new keys.

Lowercase is also the only representation valid across HTTP versions: RFC 9113 makes uppercase field names
a protocol error on HTTP/2, so proxies and h2-capable servers already require it.
The TypeScript SDK sets `headers['mcp-session-id']` and the Python SDK defines `MCP_SESSION_ID_HEADER = "mcp-session-id"`,
so lowercase also matches what the reference SDKs emit; the MCP spec's `Mcp-Session-Id` casing is prose convention,
not a wire requirement.

Examples and documentation deliberately keep the spec's `Mcp-Session-Id` casing where it does not name
a Rack response hash key: curl invocations and `examples/streamable_http_client.rb` set request headers,
which the Rack response rule does not govern; the rack-cors `expose:` entry carries a header name as
an `Access-Control-Expose-Headers` value, which browsers match case-insensitively;
and README/docs prose follows the MCP specification's documentation convention.
Only code reading the raw Rack response headers hash, a case-sensitive Hash lookup, had to switch to lowercase keys.

AGENTS.md now records the lowercase rule under the code style guidelines, since the MCP spec's `Mcp-Session-Id` prose casing
invites reintroducing capitalized names in future transport work.

## How Has This Been Tested?

Added a regression test asserting that header names are lowercase in initialize, GET SSE, DELETE, and parse error responses.
Existing tests that read the raw headers hash were updated to the new keys. Verified with rake test, rake rubocop,
and rake conformance.

## Breaking Changes

Code that reads the raw Rack response headers hash returned by `handle_request`, or by middleware around the mounted transport,
with exact-case keys such as `"Mcp-Session-Id"` must switch to the lowercase keys. Clients speaking HTTP are
unaffected because header names are case-insensitive on the wire.
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.

1 participant