Skip to content

Ignore pre-2026 protocol_version pins at the StreamableHTTP transport#2923

Merged
maxisbey merged 1 commit into
mainfrom
followup-2910-stateful-pin-header
Jun 20, 2026
Merged

Ignore pre-2026 protocol_version pins at the StreamableHTTP transport#2923
maxisbey merged 1 commit into
mainfrom
followup-2910-stateful-pin-header

Conversation

@maxisbey

Copy link
Copy Markdown
Contributor

Follow-up to #2910 / #2917 addressing post-merge review feedback.

Motivation and Context

#2910 threaded the new protocol_version pin into StreamableHTTPTransport unconditionally and made the constructor pin win over the InitializeResult snoop. That's correct for a 2026-07-28 pin (no initialize is ever sent), but for a pre-2026 stateful pin it meant:

  • the MCP-Protocol-Version header was stamped on the initialize POST itself (the spec only defines it for subsequent requests; a strict server that doesn't support the pinned version may 400 the handshake), and
  • after the server negotiated a different version, the transport kept stamping the original pin on every later request — session.protocol_version and the wire header disagreed.

A stateful pin is a session-layer concern (it picks the protocolVersion field in the InitializeRequest body); the transport doesn't need it. This change drops pre-2026 pins at the transport constructor so the header is populated from the negotiated InitializeResult exactly as before #2910, while modern pins continue to be honoured from the first request.

Also adds a one-line caveat to the Client.protocol_version docstring noting that a 2026-07-28 pin currently requires a URL/HTTP transport (the in-memory Server/MCPServer path doesn't have a modern entry yet).

Review threads not addressed here

  • _streamable_http_modern.py returning -32700 for a valid-JSON non-request body — the body-parse block is replaced by the inbound classifier in the upcoming server-stateless work, which returns INVALID_REQUEST for that case. (At 2026-07-28 there are no client→server notifications over Streamable HTTP, so 202 is not the target shape — a deliberate rejection is.)
  • _related_request_id # type: ignore[call-arg] in server/session.py — already tracked; falls out of the ServerRunner driver split.

How Has This Been Tested?

  • New unit test test_stateful_constructor_pin_is_ignored_and_the_negotiated_version_wins covers both the header-omitted-on-initialize and negotiated-version-wins behaviours.
  • Existing test_modern_constructor_pin_is_not_overwritten_by_an_initialize_result (renamed for clarity) still pins the 2026 case.
  • tests/interaction -k 'streamable-http and 2025' (230 tests, which pass a stateful 2025-11-25 pin on every connection) passes unchanged.

Breaking Changes

None.

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

Additional context

The early-return guard in _maybe_extract_protocol_version_from_message is now reachable only via direct invocation in the unit test (a modern-pinned transport never sends initialize in production). Left as-is to keep this PR scoped; the single-owner refactor for the client pin will sweep it.

AI Disclaimer

A stateful (pre-2026) protocol_version pin is a session-layer concern: it
controls the protocolVersion field in the InitializeRequest body. #2910
threaded it into the transport unconditionally, which (a) stamped
MCP-Protocol-Version on the initialize POST itself and (b) kept stamping
the pinned value after the server negotiated a different version.

The transport now only stores 2026-07-28+ pins (which never send
initialize). For earlier versions it falls back to learning the
negotiated version from the InitializeResult, restoring pre-#2910 wire
behaviour for stateful sessions.

Also adds a docstring caveat to Client.protocol_version noting the
in-memory transport does not yet have a modern entry point.
@maxisbey maxisbey marked this pull request as ready for review June 20, 2026 15:28
@maxisbey maxisbey enabled auto-merge (squash) June 20, 2026 15:28
@maxisbey maxisbey merged commit 5a3412d into main Jun 20, 2026
31 checks passed
@maxisbey maxisbey deleted the followup-2910-stateful-pin-header branch June 20, 2026 15:29

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — small, well-scoped follow-up that restores the pre-#2910 header behaviour for stateful pins, with direct test coverage of both the omitted-on-initialize and negotiated-version cases.

Extended reasoning...

Overview

The PR touches three files: a one-conditional change in StreamableHTTPTransport.__init__ (only retain a constructor protocol_version pin when it is in MODERN_PROTOCOL_VERSIONS), accompanying docstring/comment updates in streamable_http.py and client.py, and unit tests. No control flow elsewhere changes; the existing _maybe_extract_protocol_version_from_message snoop simply becomes the source of the header for pre-2026 sessions again, which matches the behaviour before #2910.

Security risks

None identified. The change only affects which value populates the outgoing MCP-Protocol-Version header on the client side; it does not touch auth, parsing of untrusted input, or server-side validation. If anything it aligns the wire header with the spec (header reflects the negotiated version, and is not sent on the initialize request itself).

Level of scrutiny

This is client transport logic, not security-sensitive code, and the effective diff is a single guarded assignment plus documentation. The semantics (a stateful pin is a session-layer concern carried in the InitializeRequest body, while the transport header follows negotiation) are clearly argued in the PR description and are consistent with how Client threads protocol_version into ClientSession separately. The new test exercises both behaviours directly (MCP-ProtOCOL-Version absent before negotiation, negotiated value stamped after), and the renamed modern-pin test still covers the 2026 path.

Other factors

The bug-hunting pass found no issues, the PR is a follow-up addressing post-merge review feedback from #2910/#2917, and the author reports the 2025-pinned interaction suite passes unchanged. The deliberately-unaddressed review threads are explicitly called out as out of scope. Overall risk is low and intent is clear.

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.

2 participants