Skip to content

feat(transports): forward sessionless MCP 2026-07-28 traffic alongside handshake eras - #13

Open
jonpspri wants to merge 1 commit into
mainfrom
mcp-2026-07-28
Open

jonpspri wants to merge 1 commit into
mainfrom
mcp-2026-07-28

Conversation

@jonpspri

@jonpspri jonpspri commented Oct 12, 2026 •

Copy link
Copy Markdown
Collaborator

Related Issue

Closes #12


Summary

Makes the reverse proxy carry both MCP protocol eras over the server-side Streamable HTTP leg. The adapter previously refused any request that arrived before an initialize handshake produced a session, which made the sessionless 2026-07-28 era impossible to forward.

  • Era auto-detection (per connection, fixed for adapter lifetime): a server/discover request or a params._meta["io.modelcontextprotocol/protocolVersion"] field switches the adapter to sessionless mode; initialize keeps the handshake-era path. An established handshake session can never be flipped, and a mcp-session-id minted on a modern-era response is ignored (spec: modern servers must not mint sessions).
  • Sessionless forwarding: no initialize gate, no session headers. The adapter mints the spec-required HTTP headers per request: MCP-Protocol-Version (matching the body _meta), Mcp-Method, and Mcp-Name for tools/call / resources/read / prompts/get, using the =?base64?...?= sentinel for non-header-safe values.
  • Era-aware errors: on modern connections, error statuses with JSON-RPC bodies (404 -32601 method not found, 400 unsupported protocol version) are relayed to the gateway as ordinary responses. SessionExpiredError and the gateway re-registration flow remain handshake-era only. Bodyless error statuses still raise.
  • Unchanged: SSE adapter (legacy handshake-era transport by spec), stdio adapter (era-agnostic passthrough), and the entire handshake-era HTTP path (session capture, headers, 404 retry/re-registration).
  • Test harness: integration extra moves to fastmcp>=4,<5 (FastMCP 4 / MCP SDK v2 serve both eras per connection). Companion server code needed no changes - every API it uses survived v4.
  • Docs: new "MCP Protocol Eras" section in USER_README.md.

Reviewability

  • This PR has one clear purpose
  • The linked issue is not labeled triage
  • Unrelated bugs or improvements are tracked in separate issues/PRs
  • Tests are included with the code they validate

Type of Change

  • Feature / Enhancement

Verification

Check Command Status
Tests uv run pytest tests/ -q 161 passed (89% total cov)
Integration (live) uv run pytest tests/integration -v 9 passed: handshake 2025-06-18 + 2025-11-25 over stdio/SSE/streamable-http; sessionless 2026-07-28 discover/tools/resources/prompts; modern-era 404 relay; WS end-to-end
Lint uv run ruff check src tests All checks passed
Type check uv run mypy src/mcp_reverse_proxy No issues (13 files)
Packaging uv build sdist + wheel built

Manual validation: a throwaway smoke script drove server/discover + tools/call + tools/list through the adapter against a live FastMCP 4.1.0 server with no handshake; server returned supportedVersions: ["2026-07-28"] and correct call results. (Script removed after use; the flow is pinned by tests/integration/test_modern_era.py.)

Note: black --check reports 18 files reformat-needed identically at HEAD (pre-existing drift); this change introduces no new violations, so unrelated reformatting was left out of scope.


Checklist

  • Tests added/updated for changes and pytest passes
  • ruff check . and mypy src/ are green
  • Documentation updated (USER_README.md)
  • Commits signed off (git commit -s, DCO)
  • No secrets, credentials, or private keys committed

Notes

Design constraint: the era is detected from the first messages on a connection and fixed until stop()/reconnect - a gateway switching eras mid-connection is not a real topology. Era support by transport: stdio and Streamable HTTP carry both eras; SSE is handshake-era only by specification.

…e handshake eras

The Streamable HTTP adapter required an initialize handshake and a
server-minted session before forwarding any request, which made
sessionless 2026-07-28 traffic impossible. The adapter now auto-detects
the protocol era per connection:

- Modern era (2026-07-28): a server/discover request or a
  params._meta io.modelcontextprotocol/protocolVersion field switches
  the adapter to sessionless forwarding - no initialize, no session
  headers. Per request it mints the spec-required MCP-Protocol-Version,
  Mcp-Method, and Mcp-Name headers (base64 sentinel encoding for
  non-header-safe values). Error statuses carrying JSON-RPC bodies
  (404 -32601, 400 unsupported version) are relayed to the gateway as
  ordinary responses, so handshake-era session-expiry recovery
  (SessionExpiredError, re-registration) can never fire on modern
  connections. A mcp-session-id on a modern response is ignored per spec.
- Handshake era (<= 2025-11-25): unchanged - initialize capture,
  session headers, 404 retry/re-registration.

SSE stays handshake-era only (legacy transport by spec); stdio is
era-agnostic passthrough.

Test harness: bump the integration extra to fastmcp>=4,<5 (FastMCP 4 /
MCP SDK v2 serve both eras per connection, auto-detected). Integration
suite now covers handshake 2025-06-18 and 2025-11-25 over stdio/SSE/
streamable-http, plus live sessionless 2026-07-28 round-trips
(server/discover, tools/call, tools/list, resources/read, prompts/get)
and modern-era 404 error relay. Unit tests cover era detection, header
minting/encoding, and era-aware error handling.

Signed-off-by: Jonathan Springer <jps@s390x.com>
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.

[FEATURE]: Forward sessionless MCP 2026-07-28 traffic alongside handshake eras

1 participant