Ignore pre-2026 protocol_version pins at the StreamableHTTP transport (#2923)
This commit is contained in:
parent
fda4c54362
commit
5a3412ddc1
3 changed files with 37 additions and 9 deletions
|
|
@ -97,7 +97,9 @@ class Client:
|
||||||
|
|
||||||
Pinning to ``2026-07-28`` or later selects the stateless transport era: no initialize
|
Pinning to ``2026-07-28`` or later selects the stateless transport era: no initialize
|
||||||
handshake is sent on the wire (the session synthesizes its `InitializeResult` locally),
|
handshake is sent on the wire (the session synthesizes its `InitializeResult` locally),
|
||||||
and for HTTP the ``MCP-Protocol-Version`` header is set from the first request.
|
and for HTTP the ``MCP-Protocol-Version`` header is set from the first request. A modern
|
||||||
|
pin currently requires a URL or `Transport`; the in-memory `Server`/`MCPServer` path
|
||||||
|
does not yet have a modern entry point.
|
||||||
Leave as ``None`` to negotiate the version via the initialize handshake.
|
Leave as ``None`` to negotiate the version via the initialize handshake.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -93,13 +93,14 @@ class StreamableHTTPTransport:
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
url: The endpoint URL.
|
url: The endpoint URL.
|
||||||
protocol_version: Pin the MCP-Protocol-Version header from the first request
|
protocol_version: Pin the MCP-Protocol-Version header from the first request.
|
||||||
instead of waiting to snoop it from an InitializeResult. Required for
|
Only honoured for stateless 2026-07-28+ sessions that never send
|
||||||
stateless 2026-07-28 sessions that never send initialize.
|
initialize; for earlier (stateful) versions the header is populated
|
||||||
|
from the negotiated InitializeResult, so a pre-2026 value is ignored.
|
||||||
"""
|
"""
|
||||||
self.url = url
|
self.url = url
|
||||||
self.session_id: str | None = None
|
self.session_id: str | None = None
|
||||||
self.protocol_version: str | None = protocol_version
|
self.protocol_version: str | None = protocol_version if protocol_version in MODERN_PROTOCOL_VERSIONS else None
|
||||||
|
|
||||||
def _per_message_headers(self, message: JSONRPCMessage) -> dict[str, str]:
|
def _per_message_headers(self, message: JSONRPCMessage) -> dict[str, str]:
|
||||||
"""Per-POST routing headers (Mcp-Method, Mcp-Name) for 2026-07-28+ pinned transports.
|
"""Per-POST routing headers (Mcp-Method, Mcp-Name) for 2026-07-28+ pinned transports.
|
||||||
|
|
@ -158,7 +159,8 @@ class StreamableHTTPTransport:
|
||||||
def _maybe_extract_protocol_version_from_message(self, message: JSONRPCMessage) -> None:
|
def _maybe_extract_protocol_version_from_message(self, message: JSONRPCMessage) -> None:
|
||||||
"""Extract protocol version from initialization response message."""
|
"""Extract protocol version from initialization response message."""
|
||||||
if self.protocol_version is not None:
|
if self.protocol_version is not None:
|
||||||
# Constructor pin wins over snooping the InitializeResult.
|
# Only a modern constructor pin reaches here (pre-2026 values are dropped
|
||||||
|
# in __init__), and a modern pin never sends initialize.
|
||||||
return
|
return
|
||||||
if isinstance(message, JSONRPCResponse) and message.result: # pragma: no branch
|
if isinstance(message, JSONRPCResponse) and message.result: # pragma: no branch
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,12 @@ import pytest
|
||||||
from inline_snapshot import snapshot
|
from inline_snapshot import snapshot
|
||||||
|
|
||||||
from mcp.client import ClientSession
|
from mcp.client import ClientSession
|
||||||
from mcp.client.streamable_http import StreamableHTTPTransport, _encode_header_value, streamable_http_client
|
from mcp.client.streamable_http import (
|
||||||
|
MCP_PROTOCOL_VERSION,
|
||||||
|
StreamableHTTPTransport,
|
||||||
|
_encode_header_value,
|
||||||
|
streamable_http_client,
|
||||||
|
)
|
||||||
from mcp.types import JSONRPCMessage, JSONRPCNotification, JSONRPCRequest, JSONRPCResponse
|
from mcp.types import JSONRPCMessage, JSONRPCNotification, JSONRPCRequest, JSONRPCResponse
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -135,8 +140,8 @@ async def test_pinned_transport_ignores_returned_session_id_and_never_opens_get_
|
||||||
assert all("mcp-session-id" not in r.headers for r in recorded)
|
assert all("mcp-session-id" not in r.headers for r in recorded)
|
||||||
|
|
||||||
|
|
||||||
def test_constructor_pin_is_not_overwritten_by_an_initialize_result() -> None:
|
def test_modern_constructor_pin_is_not_overwritten_by_an_initialize_result() -> None:
|
||||||
"""A protocol_version passed at construction wins over the InitializeResult snoop."""
|
"""A 2026-07-28+ pin wins over the InitializeResult snoop (no initialize is ever sent)."""
|
||||||
transport = StreamableHTTPTransport("http://test/mcp", protocol_version="2026-07-28")
|
transport = StreamableHTTPTransport("http://test/mcp", protocol_version="2026-07-28")
|
||||||
init = JSONRPCResponse(
|
init = JSONRPCResponse(
|
||||||
jsonrpc="2.0",
|
jsonrpc="2.0",
|
||||||
|
|
@ -149,3 +154,22 @@ def test_constructor_pin_is_not_overwritten_by_an_initialize_result() -> None:
|
||||||
)
|
)
|
||||||
transport._maybe_extract_protocol_version_from_message(init) # pyright: ignore[reportPrivateUsage]
|
transport._maybe_extract_protocol_version_from_message(init) # pyright: ignore[reportPrivateUsage]
|
||||||
assert transport.protocol_version == "2026-07-28"
|
assert transport.protocol_version == "2026-07-28"
|
||||||
|
|
||||||
|
|
||||||
|
def test_stateful_constructor_pin_is_ignored_and_the_negotiated_version_wins() -> None:
|
||||||
|
"""A pre-2026 pin is a session-layer concern; the transport must not stamp it on the
|
||||||
|
initialize request and must adopt the server's negotiated version for later headers."""
|
||||||
|
transport = StreamableHTTPTransport("http://test/mcp", protocol_version="2025-06-18")
|
||||||
|
assert MCP_PROTOCOL_VERSION not in transport._prepare_headers() # pyright: ignore[reportPrivateUsage]
|
||||||
|
init = JSONRPCResponse(
|
||||||
|
jsonrpc="2.0",
|
||||||
|
id=1,
|
||||||
|
result={
|
||||||
|
"protocolVersion": "2025-03-26",
|
||||||
|
"capabilities": {},
|
||||||
|
"serverInfo": {"name": "s", "version": "0"},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
transport._maybe_extract_protocol_version_from_message(init) # pyright: ignore[reportPrivateUsage]
|
||||||
|
assert transport.protocol_version == "2025-03-26"
|
||||||
|
assert transport._prepare_headers()[MCP_PROTOCOL_VERSION] == "2025-03-26" # pyright: ignore[reportPrivateUsage]
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue