From 2314b5accab4b3ef17dfeaba38ead078bb361298 Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Fri, 6 Jun 2025 15:36:58 -0700 Subject: [PATCH 01/10] test: add failing tests for MCP-Protocol-Version header requirement Add integration tests to verify the new spec requirement that HTTP clients must include the negotiated MCP-Protocol-Version header in all requests after initialization. Tests verify: 1. Client includes MCP-Protocol-Version header after initialization 2. Server validates header presence and returns 400 for missing/invalid 3. Server accepts requests with valid negotiated version These tests currently fail as the feature is not yet implemented. Related to spec change: https://github.com/modelcontextprotocol/modelcontextprotocol/pull/548 --- tests/shared/test_streamable_http.py | 115 +++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) diff --git a/tests/shared/test_streamable_http.py b/tests/shared/test_streamable_http.py index 615e68efca..bc20453ee2 100644 --- a/tests/shared/test_streamable_http.py +++ b/tests/shared/test_streamable_http.py @@ -1358,3 +1358,118 @@ async def test_streamablehttp_request_context_isolation(context_aware_server: No assert ctx["headers"].get("x-request-id") == f"request-{i}" assert ctx["headers"].get("x-custom-value") == f"value-{i}" assert ctx["headers"].get("authorization") == f"Bearer token-{i}" + + +@pytest.mark.anyio +async def test_client_includes_protocol_version_header_after_init( + context_aware_server, basic_server_url +): + """Test that client includes MCP-Protocol-Version header after initialization.""" + async with streamablehttp_client(f"{basic_server_url}/mcp") as ( + read_stream, + write_stream, + _, + ): + async with ClientSession(read_stream, write_stream) as session: + # Initialize and get the negotiated version + init_result = await session.initialize() + negotiated_version = init_result.protocolVersion + + # Call a tool that echoes headers to verify the header is present + tool_result = await session.call_tool("echo_headers", {}) + + assert len(tool_result.content) == 1 + assert isinstance(tool_result.content[0], TextContent) + headers_data = json.loads(tool_result.content[0].text) + + # Verify protocol version header is present + assert "mcp-protocol-version" in headers_data + assert headers_data["mcp-protocol-version"] == negotiated_version + + +def test_server_validates_protocol_version_header(basic_server, basic_server_url): + """Test that server returns 400 Bad Request version header is missing or invalid.""" + # First initialize a session to get a valid session ID + init_response = requests.post( + f"{basic_server_url}/mcp", + headers={ + "Accept": "application/json, text/event-stream", + "Content-Type": "application/json", + }, + json=INIT_REQUEST, + ) + assert init_response.status_code == 200 + session_id = init_response.headers.get(MCP_SESSION_ID_HEADER) + + # Test request without MCP-Protocol-Version header (should fail) + response = requests.post( + f"{basic_server_url}/mcp", + headers={ + "Accept": "application/json, text/event-stream", + "Content-Type": "application/json", + MCP_SESSION_ID_HEADER: session_id, + }, + json={"jsonrpc": "2.0", "method": "tools/list", "id": "test-1"}, + ) + assert response.status_code == 400 + assert ( + "MCP-Protocol-Version" in response.text + or "protocol version" in response.text.lower() + ) + + # Test request with invalid protocol version (should fail) + response = requests.post( + f"{basic_server_url}/mcp", + headers={ + "Accept": "application/json, text/event-stream", + "Content-Type": "application/json", + MCP_SESSION_ID_HEADER: session_id, + "MCP-Protocol-Version": "invalid-version", + }, + json={"jsonrpc": "2.0", "method": "tools/list", "id": "test-2"}, + ) + assert response.status_code == 400 + assert ( + "MCP-Protocol-Version" in response.text + or "protocol version" in response.text.lower() + ) + + # Test request with unsupported protocol version (should fail) + response = requests.post( + f"{basic_server_url}/mcp", + headers={ + "Accept": "application/json, text/event-stream", + "Content-Type": "application/json", + MCP_SESSION_ID_HEADER: session_id, + "MCP-Protocol-Version": "1999-01-01", # Very old unsupported version + }, + json={"jsonrpc": "2.0", "method": "tools/list", "id": "test-3"}, + ) + assert response.status_code == 400 + assert ( + "MCP-Protocol-Version" in response.text + or "protocol version" in response.text.lower() + ) + + # Test request with valid protocol version (should succeed) + init_data = None + assert init_response.headers.get("Content-Type") == "text/event-stream" + for line in init_response.text.splitlines(): + if line.startswith("data: "): + init_data = json.loads(line[6:]) + break + + assert init_data is not None + negotiated_version = init_data["result"]["protocolVersion"] + + response = requests.post( + f"{basic_server_url}/mcp", + headers={ + "Accept": "application/json, text/event-stream", + "Content-Type": "application/json", + MCP_SESSION_ID_HEADER: session_id, + "MCP-Protocol-Version": negotiated_version, + }, + json={"jsonrpc": "2.0", "method": "tools/list", "id": "test-4"}, + ) + assert response.status_code == 200 From c6cca130a3e8a0a4bcd2604c6e9f16c2559f7937 Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Fri, 6 Jun 2025 19:12:38 -0700 Subject: [PATCH 02/10] feat: track protocol version in StreamableHttpTransport The client now tracks the negotiated protocol version from the server's response headers, enabling version-aware communication between client and server. --- src/mcp/client/streamable_http.py | 53 +++++++++++++++++++++++++++---- 1 file changed, 47 insertions(+), 6 deletions(-) diff --git a/src/mcp/client/streamable_http.py b/src/mcp/client/streamable_http.py index 4718705331..f35b512a27 100644 --- a/src/mcp/client/streamable_http.py +++ b/src/mcp/client/streamable_http.py @@ -39,6 +39,7 @@ GetSessionIdCallback = Callable[[], str | None] MCP_SESSION_ID = "mcp-session-id" +MCP_PROTOCOL_VERSION = "MCP-Protocol-Version" LAST_EVENT_ID = "last-event-id" CONTENT_TYPE = "content-type" ACCEPT = "Accept" @@ -97,17 +98,22 @@ def __init__( ) self.auth = auth self.session_id = None + self.protocol_version = None self.request_headers = { ACCEPT: f"{JSON}, {SSE}", CONTENT_TYPE: JSON, **self.headers, } - def _update_headers_with_session(self, base_headers: dict[str, str]) -> dict[str, str]: - """Update headers with session ID if available.""" + def _update_headers_with_session( + self, base_headers: dict[str, str] + ) -> dict[str, str]: + """Update headers with session ID and protocol version if available.""" headers = base_headers.copy() if self.session_id: headers[MCP_SESSION_ID] = self.session_id + if self.protocol_version: + headers[MCP_PROTOCOL_VERSION] = self.protocol_version return headers def _is_initialization_request(self, message: JSONRPCMessage) -> bool: @@ -128,12 +134,25 @@ def _maybe_extract_session_id_from_response( self.session_id = new_session_id logger.info(f"Received session ID: {self.session_id}") + def _maybe_extract_protocol_version_from_message( + self, + message: JSONRPCMessage, + ) -> None: + """Extract protocol version from initialization response message.""" + if isinstance(message.root, JSONRPCResponse) and message.root.result: + # Check if result has protocolVersion field + result = message.root.result + if "protocolVersion" in result: + self.protocol_version = result["protocolVersion"] + logger.info(f"Negotiated protocol version: {self.protocol_version}") + async def _handle_sse_event( self, sse: ServerSentEvent, read_stream_writer: StreamWriter, original_request_id: RequestId | None = None, resumption_callback: Callable[[str], Awaitable[None]] | None = None, + is_initialization: bool = False, ) -> bool: """Handle an SSE event, returning True if the response is complete.""" if sse.event == "message": @@ -141,6 +160,10 @@ async def _handle_sse_event( message = JSONRPCMessage.model_validate_json(sse.data) logger.debug(f"SSE message: {message}") + # Extract protocol version from initialization response + if is_initialization: + self._maybe_extract_protocol_version_from_message(message) + # If this is a response and we have original_request_id, replace it if original_request_id is not None and isinstance(message.root, JSONRPCResponse | JSONRPCError): message.root.id = original_request_id @@ -256,9 +279,11 @@ async def _handle_post_request(self, ctx: RequestContext) -> None: content_type = response.headers.get(CONTENT_TYPE, "").lower() if content_type.startswith(JSON): - await self._handle_json_response(response, ctx.read_stream_writer) + await self._handle_json_response( + response, ctx.read_stream_writer, is_initialization + ) elif content_type.startswith(SSE): - await self._handle_sse_response(response, ctx) + await self._handle_sse_response(response, ctx, is_initialization) else: await self._handle_unexpected_content_type( content_type, @@ -269,18 +294,29 @@ async def _handle_json_response( self, response: httpx.Response, read_stream_writer: StreamWriter, + is_initialization: bool = False, ) -> None: """Handle JSON response from the server.""" try: content = await response.aread() message = JSONRPCMessage.model_validate_json(content) + + # Extract protocol version from initialization response + if is_initialization: + self._maybe_extract_protocol_version_from_message(message) + session_message = SessionMessage(message) await read_stream_writer.send(session_message) except Exception as exc: logger.error(f"Error parsing JSON response: {exc}") await read_stream_writer.send(exc) - async def _handle_sse_response(self, response: httpx.Response, ctx: RequestContext) -> None: + async def _handle_sse_response( + self, + response: httpx.Response, + ctx: RequestContext, + is_initialization: bool = False, + ) -> None: """Handle SSE response from the server.""" try: event_source = EventSource(response) @@ -288,7 +324,12 @@ async def _handle_sse_response(self, response: httpx.Response, ctx: RequestConte is_complete = await self._handle_sse_event( sse, ctx.read_stream_writer, - resumption_callback=(ctx.metadata.on_resumption_token_update if ctx.metadata else None), + resumption_callback=( + ctx.metadata.on_resumption_token_update + if ctx.metadata + else None + ), + is_initialization=is_initialization, ) # If the SSE event indicates completion, like returning respose/error # break the loop From 4ce7e4c3ce2fa8bce388c0dcec1e7e828de6a792 Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Fri, 6 Jun 2025 19:51:25 -0700 Subject: [PATCH 03/10] feat: implement MCP-Protocol-Version header validation in server - Add MCP_PROTOCOL_VERSION_HEADER constant - Add _validate_protocol_version method to check header presence and validity - Validate protocol version for all non-initialization requests (POST, GET, DELETE) - Return 400 Bad Request for missing or invalid protocol versions - Update tests to include MCP-Protocol-Version header in requests - Fix test_streamablehttp_client_resumption to pass protocol version when resuming This implements the server-side validation required by the spec change that mandates clients include the negotiated protocol version in all subsequent HTTP requests after initialization. Github-Issue: #548 --- src/mcp/server/streamable_http.py | 39 ++++++++++++++- tests/shared/test_streamable_http.py | 74 ++++++++++++++++++++++++---- 2 files changed, 101 insertions(+), 12 deletions(-) diff --git a/src/mcp/server/streamable_http.py b/src/mcp/server/streamable_http.py index 9356a99487..82b9de1b9c 100644 --- a/src/mcp/server/streamable_http.py +++ b/src/mcp/server/streamable_http.py @@ -25,6 +25,7 @@ from starlette.types import Receive, Scope, Send from mcp.shared.message import ServerMessageMetadata, SessionMessage +from mcp.shared.version import SUPPORTED_PROTOCOL_VERSIONS from mcp.types import ( INTERNAL_ERROR, INVALID_PARAMS, @@ -45,6 +46,7 @@ # Header names MCP_SESSION_ID_HEADER = "mcp-session-id" +MCP_PROTOCOL_VERSION_HEADER = "MCP-Protocol-Version" LAST_EVENT_ID_HEADER = "last-event-id" # Content types @@ -353,9 +355,10 @@ async def _handle_post_request(self, scope: Scope, request: Request, receive: Re ) await response(scope, receive, send) return - # For non-initialization requests, validate the session elif not await self._validate_session(request, send): return + elif not await self._validate_protocol_version(request, send): + return # For notifications and responses only, return 202 Accepted if not isinstance(message.root, JSONRPCRequest): @@ -515,6 +518,9 @@ async def _handle_get_request(self, request: Request, send: Send) -> None: if not await self._validate_session(request, send): return + if not await self._validate_protocol_version(request, send): + return + # Handle resumability: check for Last-Event-ID header if last_event_id := request.headers.get(LAST_EVENT_ID_HEADER): await self._replay_events(last_event_id, request, send) @@ -595,6 +601,8 @@ async def _handle_delete_request(self, request: Request, send: Send) -> None: if not await self._validate_session(request, send): return + if not await self._validate_protocol_version(request, send): + return await self._terminate_session() @@ -682,7 +690,34 @@ async def _validate_session(self, request: Request, send: Send) -> bool: return True - async def _replay_events(self, last_event_id: str, request: Request, send: Send) -> None: + async def _validate_protocol_version(self, request: Request, send: Send) -> bool: + """Validate the protocol version header in the request.""" + # Get the protocol version from the request headers + protocol_version = request.headers.get(MCP_PROTOCOL_VERSION_HEADER) + + # If no protocol version provided, return error + if not protocol_version: + response = self._create_error_response( + "Bad Request: Missing MCP-Protocol-Version header", + HTTPStatus.BAD_REQUEST, + ) + await response(request.scope, request.receive, send) + return False + + # Check if the protocol version is supported + if protocol_version not in SUPPORTED_PROTOCOL_VERSIONS: + response = self._create_error_response( + f"Bad Request: Unsupported protocol version: {protocol_version}", + HTTPStatus.BAD_REQUEST, + ) + await response(request.scope, request.receive, send) + return False + + return True + + async def _replay_events( + self, last_event_id: str, request: Request, send: Send + ) -> None: """ Replays events that would have been sent after the specified event ID. Only used when resumability is enabled. diff --git a/tests/shared/test_streamable_http.py b/tests/shared/test_streamable_http.py index bc20453ee2..60b8a4ccf9 100644 --- a/tests/shared/test_streamable_http.py +++ b/tests/shared/test_streamable_http.py @@ -26,6 +26,7 @@ from mcp.client.streamable_http import streamablehttp_client from mcp.server import Server from mcp.server.streamable_http import ( + MCP_PROTOCOL_VERSION_HEADER, MCP_SESSION_ID_HEADER, SESSION_ID_PATTERN, EventCallback, @@ -560,11 +561,24 @@ def test_session_termination(basic_server, basic_server_url): ) assert response.status_code == 200 + # Extract negotiated protocol version from SSE response + init_data = None + assert response.headers.get("Content-Type") == "text/event-stream" + for line in response.text.splitlines(): + if line.startswith("data: "): + init_data = json.loads(line[6:]) + break + assert init_data is not None + negotiated_version = init_data["result"]["protocolVersion"] + # Now terminate the session session_id = response.headers.get(MCP_SESSION_ID_HEADER) response = requests.delete( f"{basic_server_url}/mcp", - headers={MCP_SESSION_ID_HEADER: session_id}, + headers={ + MCP_SESSION_ID_HEADER: session_id, + MCP_PROTOCOL_VERSION_HEADER: negotiated_version, + }, ) assert response.status_code == 200 @@ -595,16 +609,27 @@ def test_response(basic_server, basic_server_url): ) assert response.status_code == 200 - # Now terminate the session + # Extract negotiated protocol version from SSE response + init_data = None + assert response.headers.get("Content-Type") == "text/event-stream" + for line in response.text.splitlines(): + if line.startswith("data: "): + init_data = json.loads(line[6:]) + break + assert init_data is not None + negotiated_version = init_data["result"]["protocolVersion"] + + # Now get the session ID session_id = response.headers.get(MCP_SESSION_ID_HEADER) - # Try to use the terminated session + # Try to use the session with proper headers tools_response = requests.post( mcp_url, headers={ "Accept": "application/json, text/event-stream", "Content-Type": "application/json", MCP_SESSION_ID_HEADER: session_id, # Use the session ID we got earlier + MCP_PROTOCOL_VERSION_HEADER: negotiated_version, }, json={"jsonrpc": "2.0", "method": "tools/list", "id": "tools-1"}, stream=True, @@ -646,12 +671,23 @@ def test_get_sse_stream(basic_server, basic_server_url): session_id = init_response.headers.get(MCP_SESSION_ID_HEADER) assert session_id is not None + # Extract negotiated protocol version from SSE response + init_data = None + assert init_response.headers.get("Content-Type") == "text/event-stream" + for line in init_response.text.splitlines(): + if line.startswith("data: "): + init_data = json.loads(line[6:]) + break + assert init_data is not None + negotiated_version = init_data["result"]["protocolVersion"] + # Now attempt to establish an SSE stream via GET get_response = requests.get( mcp_url, headers={ "Accept": "text/event-stream", MCP_SESSION_ID_HEADER: session_id, + MCP_PROTOCOL_VERSION_HEADER: negotiated_version, }, stream=True, ) @@ -666,6 +702,7 @@ def test_get_sse_stream(basic_server, basic_server_url): headers={ "Accept": "text/event-stream", MCP_SESSION_ID_HEADER: session_id, + MCP_PROTOCOL_VERSION_HEADER: negotiated_version, }, stream=True, ) @@ -694,11 +731,22 @@ def test_get_validation(basic_server, basic_server_url): session_id = init_response.headers.get(MCP_SESSION_ID_HEADER) assert session_id is not None + # Extract negotiated protocol version from SSE response + init_data = None + assert init_response.headers.get("Content-Type") == "text/event-stream" + for line in init_response.text.splitlines(): + if line.startswith("data: "): + init_data = json.loads(line[6:]) + break + assert init_data is not None + negotiated_version = init_data["result"]["protocolVersion"] + # Test without Accept header response = requests.get( mcp_url, headers={ MCP_SESSION_ID_HEADER: session_id, + MCP_PROTOCOL_VERSION_HEADER: negotiated_version, }, stream=True, ) @@ -711,6 +759,7 @@ def test_get_validation(basic_server, basic_server_url): headers={ "Accept": "application/json", MCP_SESSION_ID_HEADER: session_id, + MCP_PROTOCOL_VERSION_HEADER: negotiated_version, }, ) assert response.status_code == 406 @@ -1004,6 +1053,7 @@ async def test_streamablehttp_client_resumption(event_server): captured_resumption_token = None captured_notifications = [] tool_started = False + captured_protocol_version = None async def message_handler( message: RequestResponder[types.ServerRequest, types.ClientResult] | types.ServerNotification | Exception, @@ -1032,6 +1082,8 @@ async def on_resumption_token_update(token: str) -> None: assert isinstance(result, InitializeResult) captured_session_id = get_session_id() assert captured_session_id is not None + # Capture the negotiated protocol version + captured_protocol_version = result.protocolVersion # Start a long-running tool in a task async with anyio.create_task_group() as tg: @@ -1064,10 +1116,12 @@ async def run_tool(): captured_notifications_pre = captured_notifications.copy() captured_notifications = [] - # Now resume the session with the same mcp-session-id + # Now resume the session with the same mcp-session-id and protocol version headers = {} if captured_session_id: headers[MCP_SESSION_ID_HEADER] = captured_session_id + if captured_protocol_version: + headers[MCP_PROTOCOL_VERSION_HEADER] = captured_protocol_version async with streamablehttp_client(f"{server_url}/mcp", headers=headers) as ( read_stream, @@ -1413,7 +1467,7 @@ def test_server_validates_protocol_version_header(basic_server, basic_server_url ) assert response.status_code == 400 assert ( - "MCP-Protocol-Version" in response.text + MCP_PROTOCOL_VERSION_HEADER in response.text or "protocol version" in response.text.lower() ) @@ -1424,13 +1478,13 @@ def test_server_validates_protocol_version_header(basic_server, basic_server_url "Accept": "application/json, text/event-stream", "Content-Type": "application/json", MCP_SESSION_ID_HEADER: session_id, - "MCP-Protocol-Version": "invalid-version", + MCP_PROTOCOL_VERSION_HEADER: "invalid-version", }, json={"jsonrpc": "2.0", "method": "tools/list", "id": "test-2"}, ) assert response.status_code == 400 assert ( - "MCP-Protocol-Version" in response.text + MCP_PROTOCOL_VERSION_HEADER in response.text or "protocol version" in response.text.lower() ) @@ -1441,13 +1495,13 @@ def test_server_validates_protocol_version_header(basic_server, basic_server_url "Accept": "application/json, text/event-stream", "Content-Type": "application/json", MCP_SESSION_ID_HEADER: session_id, - "MCP-Protocol-Version": "1999-01-01", # Very old unsupported version + MCP_PROTOCOL_VERSION_HEADER: "1999-01-01", # Very old unsupported version }, json={"jsonrpc": "2.0", "method": "tools/list", "id": "test-3"}, ) assert response.status_code == 400 assert ( - "MCP-Protocol-Version" in response.text + MCP_PROTOCOL_VERSION_HEADER in response.text or "protocol version" in response.text.lower() ) @@ -1468,7 +1522,7 @@ def test_server_validates_protocol_version_header(basic_server, basic_server_url "Accept": "application/json, text/event-stream", "Content-Type": "application/json", MCP_SESSION_ID_HEADER: session_id, - "MCP-Protocol-Version": negotiated_version, + MCP_PROTOCOL_VERSION_HEADER: negotiated_version, }, json={"jsonrpc": "2.0", "method": "tools/list", "id": "test-4"}, ) From 370b993534678d1c3766861e87d38603ab1053bf Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Fri, 6 Jun 2025 20:01:04 -0700 Subject: [PATCH 04/10] refactor: extract protocol version parsing to helper function - Add extract_protocol_version_from_sse helper function to reduce code duplication - Replace repeated protocol version extraction logic in 5 test functions - Fix line length issues in docstrings to comply with 88 char limit This improves test maintainability by centralizing the SSE response parsing logic. --- tests/shared/test_streamable_http.py | 39 ++++++++++------------------ 1 file changed, 14 insertions(+), 25 deletions(-) diff --git a/tests/shared/test_streamable_http.py b/tests/shared/test_streamable_http.py index 60b8a4ccf9..95cdd2ec60 100644 --- a/tests/shared/test_streamable_http.py +++ b/tests/shared/test_streamable_http.py @@ -65,6 +65,17 @@ } +# Helper functions +def extract_protocol_version_from_sse(response: requests.Response) -> str: + """Extract the negotiated protocol version from an SSE initialization response.""" + assert response.headers.get("Content-Type") == "text/event-stream" + for line in response.text.splitlines(): + if line.startswith("data: "): + init_data = json.loads(line[6:]) + return init_data["result"]["protocolVersion"] + raise ValueError("Could not extract protocol version from SSE response") + + # Simple in-memory event store for testing class SimpleEventStore(EventStore): """Simple in-memory event store for testing.""" @@ -562,14 +573,7 @@ def test_session_termination(basic_server, basic_server_url): assert response.status_code == 200 # Extract negotiated protocol version from SSE response - init_data = None - assert response.headers.get("Content-Type") == "text/event-stream" - for line in response.text.splitlines(): - if line.startswith("data: "): - init_data = json.loads(line[6:]) - break - assert init_data is not None - negotiated_version = init_data["result"]["protocolVersion"] + negotiated_version = extract_protocol_version_from_sse(response) # Now terminate the session session_id = response.headers.get(MCP_SESSION_ID_HEADER) @@ -610,14 +614,7 @@ def test_response(basic_server, basic_server_url): assert response.status_code == 200 # Extract negotiated protocol version from SSE response - init_data = None - assert response.headers.get("Content-Type") == "text/event-stream" - for line in response.text.splitlines(): - if line.startswith("data: "): - init_data = json.loads(line[6:]) - break - assert init_data is not None - negotiated_version = init_data["result"]["protocolVersion"] + negotiated_version = extract_protocol_version_from_sse(response) # Now get the session ID session_id = response.headers.get(MCP_SESSION_ID_HEADER) @@ -1506,15 +1503,7 @@ def test_server_validates_protocol_version_header(basic_server, basic_server_url ) # Test request with valid protocol version (should succeed) - init_data = None - assert init_response.headers.get("Content-Type") == "text/event-stream" - for line in init_response.text.splitlines(): - if line.startswith("data: "): - init_data = json.loads(line[6:]) - break - - assert init_data is not None - negotiated_version = init_data["result"]["protocolVersion"] + negotiated_version = extract_protocol_version_from_sse(init_response) response = requests.post( f"{basic_server_url}/mcp", From 68323943a5b29d110bc022c2b24717ea0d546e65 Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Fri, 6 Jun 2025 20:06:48 -0700 Subject: [PATCH 05/10] refactor: consolidate header validation into single method - Add _validate_request_headers method that combines session and protocol validation - Replace repeated calls to _validate_session and _validate_protocol_version - Improves code maintainability and extensibility for future header validations - No functional changes, all tests passing This refactoring makes it easier to add new header validations in the future by having a single entry point for all non-initialization request validations. --- src/mcp/server/streamable_http.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/mcp/server/streamable_http.py b/src/mcp/server/streamable_http.py index 82b9de1b9c..29d4b92528 100644 --- a/src/mcp/server/streamable_http.py +++ b/src/mcp/server/streamable_http.py @@ -355,9 +355,7 @@ async def _handle_post_request(self, scope: Scope, request: Request, receive: Re ) await response(scope, receive, send) return - elif not await self._validate_session(request, send): - return - elif not await self._validate_protocol_version(request, send): + elif not await self._validate_request_headers(request, send): return # For notifications and responses only, return 202 Accepted @@ -516,9 +514,7 @@ async def _handle_get_request(self, request: Request, send: Send) -> None: await response(request.scope, request.receive, send) return - if not await self._validate_session(request, send): - return - if not await self._validate_protocol_version(request, send): + if not await self._validate_request_headers(request, send): return # Handle resumability: check for Last-Event-ID header @@ -599,9 +595,7 @@ async def _handle_delete_request(self, request: Request, send: Send) -> None: await response(request.scope, request.receive, send) return - if not await self._validate_session(request, send): - return - if not await self._validate_protocol_version(request, send): + if not await self._validate_request_headers(request, send): return await self._terminate_session() @@ -661,6 +655,13 @@ async def _handle_unsupported_request(self, request: Request, send: Send) -> Non ) await response(request.scope, request.receive, send) + async def _validate_request_headers(self, request: Request, send: Send) -> bool: + if not await self._validate_session(request, send): + return False + if not await self._validate_protocol_version(request, send): + return False + return True + async def _validate_session(self, request: Request, send: Send) -> bool: """Validate the session ID in the request.""" if not self.mcp_session_id: From b9a0c96a9ca74edf658acf6f41f68dd7d0ba013e Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Fri, 6 Jun 2025 20:20:08 -0700 Subject: [PATCH 06/10] refactor: rename _update_headers_for_request to _prepare_request_headers This better reflects that the method prepares headers for outgoing HTTP requests, not just updating them with context. The method adds both session ID and protocol version headers as needed. --- src/mcp/client/streamable_http.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/mcp/client/streamable_http.py b/src/mcp/client/streamable_http.py index f35b512a27..8d639a3136 100644 --- a/src/mcp/client/streamable_http.py +++ b/src/mcp/client/streamable_http.py @@ -105,9 +105,7 @@ def __init__( **self.headers, } - def _update_headers_with_session( - self, base_headers: dict[str, str] - ) -> dict[str, str]: + def _prepare_request_headers(self, base_headers: dict[str, str]) -> dict[str, str]: """Update headers with session ID and protocol version if available.""" headers = base_headers.copy() if self.session_id: @@ -197,7 +195,7 @@ async def handle_get_stream( if not self.session_id: return - headers = self._update_headers_with_session(self.request_headers) + headers = self._prepare_request_headers(self.request_headers) async with aconnect_sse( client, @@ -217,7 +215,7 @@ async def handle_get_stream( async def _handle_resumption_request(self, ctx: RequestContext) -> None: """Handle a resumption request using GET with SSE.""" - headers = self._update_headers_with_session(ctx.headers) + headers = self._prepare_request_headers(ctx.headers) if ctx.metadata and ctx.metadata.resumption_token: headers[LAST_EVENT_ID] = ctx.metadata.resumption_token else: @@ -250,7 +248,7 @@ async def _handle_resumption_request(self, ctx: RequestContext) -> None: async def _handle_post_request(self, ctx: RequestContext) -> None: """Handle a POST request with response processing.""" - headers = self._update_headers_with_session(ctx.headers) + headers = self._prepare_request_headers(ctx.headers) message = ctx.session_message.message is_initialization = self._is_initialization_request(message) @@ -426,7 +424,7 @@ async def terminate_session(self, client: httpx.AsyncClient) -> None: return try: - headers = self._update_headers_with_session(self.request_headers) + headers = self._prepare_request_headers(self.request_headers) response = await client.delete(self.url, headers=headers) if response.status_code == 405: From d14666bb7897f6b76cb442cbdf621908bff61531 Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Mon, 9 Jun 2025 18:39:34 +0100 Subject: [PATCH 07/10] feat: add backwards compatibility for missing MCP-Protocol-Version header When the MCP-Protocol-Version header is not present in requests, the server now assumes protocol version "2025-03-26" instead of returning an error. This maintains backwards compatibility with older clients that don't send the version header. The server still validates and returns 400 Bad Request for invalid or unsupported protocol versions when the header is explicitly provided. This change addresses the backwards compatibility requirement from https://github.com/modelcontextprotocol/modelcontextprotocol/pull/668 - Modified _validate_protocol_version to assume "2025-03-26" when header is missing - Updated tests to verify backwards compatibility behavior - Added new test specifically for backwards compatibility scenario --- src/mcp/server/streamable_http.py | 9 ++--- tests/shared/test_streamable_http.py | 49 ++++++++++++++++++---------- 2 files changed, 34 insertions(+), 24 deletions(-) diff --git a/src/mcp/server/streamable_http.py b/src/mcp/server/streamable_http.py index 29d4b92528..7a460e76c1 100644 --- a/src/mcp/server/streamable_http.py +++ b/src/mcp/server/streamable_http.py @@ -696,14 +696,9 @@ async def _validate_protocol_version(self, request: Request, send: Send) -> bool # Get the protocol version from the request headers protocol_version = request.headers.get(MCP_PROTOCOL_VERSION_HEADER) - # If no protocol version provided, return error + # If no protocol version provided, assume version 2025-03-26 if not protocol_version: - response = self._create_error_response( - "Bad Request: Missing MCP-Protocol-Version header", - HTTPStatus.BAD_REQUEST, - ) - await response(request.scope, request.receive, send) - return False + protocol_version = "2025-03-26" # Check if the protocol version is supported if protocol_version not in SUPPORTED_PROTOCOL_VERSIONS: diff --git a/tests/shared/test_streamable_http.py b/tests/shared/test_streamable_http.py index 95cdd2ec60..f44416796e 100644 --- a/tests/shared/test_streamable_http.py +++ b/tests/shared/test_streamable_http.py @@ -1439,7 +1439,7 @@ async def test_client_includes_protocol_version_header_after_init( def test_server_validates_protocol_version_header(basic_server, basic_server_url): - """Test that server returns 400 Bad Request version header is missing or invalid.""" + """Test that server returns 400 Bad Request version if header missing or invalid.""" # First initialize a session to get a valid session ID init_response = requests.post( f"{basic_server_url}/mcp", @@ -1452,22 +1452,6 @@ def test_server_validates_protocol_version_header(basic_server, basic_server_url assert init_response.status_code == 200 session_id = init_response.headers.get(MCP_SESSION_ID_HEADER) - # Test request without MCP-Protocol-Version header (should fail) - response = requests.post( - f"{basic_server_url}/mcp", - headers={ - "Accept": "application/json, text/event-stream", - "Content-Type": "application/json", - MCP_SESSION_ID_HEADER: session_id, - }, - json={"jsonrpc": "2.0", "method": "tools/list", "id": "test-1"}, - ) - assert response.status_code == 400 - assert ( - MCP_PROTOCOL_VERSION_HEADER in response.text - or "protocol version" in response.text.lower() - ) - # Test request with invalid protocol version (should fail) response = requests.post( f"{basic_server_url}/mcp", @@ -1516,3 +1500,34 @@ def test_server_validates_protocol_version_header(basic_server, basic_server_url json={"jsonrpc": "2.0", "method": "tools/list", "id": "test-4"}, ) assert response.status_code == 200 + + +def test_server_backwards_compatibility_no_protocol_version( + basic_server, basic_server_url +): + """Test server accepts requests without protocol version header.""" + # First initialize a session to get a valid session ID + init_response = requests.post( + f"{basic_server_url}/mcp", + headers={ + "Accept": "application/json, text/event-stream", + "Content-Type": "application/json", + }, + json=INIT_REQUEST, + ) + assert init_response.status_code == 200 + session_id = init_response.headers.get(MCP_SESSION_ID_HEADER) + + # Test request without MCP-Protocol-Version header (backwards compatibility) + response = requests.post( + f"{basic_server_url}/mcp", + headers={ + "Accept": "application/json, text/event-stream", + "Content-Type": "application/json", + MCP_SESSION_ID_HEADER: session_id, + }, + json={"jsonrpc": "2.0", "method": "tools/list", "id": "test-backwards-compat"}, + stream=True, + ) + assert response.status_code == 200 # Should succeed for backwards compatibility + assert response.headers.get("Content-Type") == "text/event-stream" From bb4eaabda074ecd09b9fa2a44b1cdc390fce3314 Mon Sep 17 00:00:00 2001 From: Felix Weinberger <3823880+felixweinberger@users.noreply.github.com> Date: Tue, 10 Jun 2025 00:32:22 -0700 Subject: [PATCH 08/10] refactor: align protocol version header capitalization to lowercase (#911) --- src/mcp/client/auth.py | 10 ++++++++-- src/mcp/client/streamable_http.py | 2 +- src/mcp/server/auth/routes.py | 3 ++- src/mcp/server/streamable_http.py | 2 +- tests/shared/test_streamable_http.py | 6 +++--- 5 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/mcp/client/auth.py b/src/mcp/client/auth.py index 7782022ce6..4e777d6007 100644 --- a/src/mcp/client/auth.py +++ b/src/mcp/client/auth.py @@ -17,7 +17,13 @@ import anyio import httpx -from mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata, OAuthMetadata, OAuthToken +from mcp.client.streamable_http import MCP_PROTOCOL_VERSION +from mcp.shared.auth import ( + OAuthClientInformationFull, + OAuthClientMetadata, + OAuthMetadata, + OAuthToken, +) from mcp.types import LATEST_PROTOCOL_VERSION logger = logging.getLogger(__name__) @@ -121,7 +127,7 @@ async def _discover_oauth_metadata(self, server_url: str) -> OAuthMetadata | Non # Extract base URL per MCP spec auth_base_url = self._get_authorization_base_url(server_url) url = urljoin(auth_base_url, "/.well-known/oauth-authorization-server") - headers = {"MCP-Protocol-Version": LATEST_PROTOCOL_VERSION} + headers = {MCP_PROTOCOL_VERSION: LATEST_PROTOCOL_VERSION} async with httpx.AsyncClient() as client: try: diff --git a/src/mcp/client/streamable_http.py b/src/mcp/client/streamable_http.py index 8d639a3136..44e0c61a7b 100644 --- a/src/mcp/client/streamable_http.py +++ b/src/mcp/client/streamable_http.py @@ -39,7 +39,7 @@ GetSessionIdCallback = Callable[[], str | None] MCP_SESSION_ID = "mcp-session-id" -MCP_PROTOCOL_VERSION = "MCP-Protocol-Version" +MCP_PROTOCOL_VERSION = "mcp-protocol-version" LAST_EVENT_ID = "last-event-id" CONTENT_TYPE = "content-type" ACCEPT = "Accept" diff --git a/src/mcp/server/auth/routes.py b/src/mcp/server/auth/routes.py index dff468ebdf..8647334e00 100644 --- a/src/mcp/server/auth/routes.py +++ b/src/mcp/server/auth/routes.py @@ -16,6 +16,7 @@ from mcp.server.auth.middleware.client_auth import ClientAuthenticator from mcp.server.auth.provider import OAuthAuthorizationServerProvider from mcp.server.auth.settings import ClientRegistrationOptions, RevocationOptions +from mcp.server.streamable_http import MCP_PROTOCOL_VERSION_HEADER from mcp.shared.auth import OAuthMetadata @@ -55,7 +56,7 @@ def cors_middleware( app=request_response(handler), allow_origins="*", allow_methods=allow_methods, - allow_headers=["mcp-protocol-version"], + allow_headers=[MCP_PROTOCOL_VERSION_HEADER], ) return cors_app diff --git a/src/mcp/server/streamable_http.py b/src/mcp/server/streamable_http.py index 7a460e76c1..dd3c8840bc 100644 --- a/src/mcp/server/streamable_http.py +++ b/src/mcp/server/streamable_http.py @@ -46,7 +46,7 @@ # Header names MCP_SESSION_ID_HEADER = "mcp-session-id" -MCP_PROTOCOL_VERSION_HEADER = "MCP-Protocol-Version" +MCP_PROTOCOL_VERSION_HEADER = "mcp-protocol-version" LAST_EVENT_ID_HEADER = "last-event-id" # Content types diff --git a/tests/shared/test_streamable_http.py b/tests/shared/test_streamable_http.py index f44416796e..b94f03bedb 100644 --- a/tests/shared/test_streamable_http.py +++ b/tests/shared/test_streamable_http.py @@ -1415,7 +1415,7 @@ async def test_streamablehttp_request_context_isolation(context_aware_server: No async def test_client_includes_protocol_version_header_after_init( context_aware_server, basic_server_url ): - """Test that client includes MCP-Protocol-Version header after initialization.""" + """Test that client includes mcp-protocol-version header after initialization.""" async with streamablehttp_client(f"{basic_server_url}/mcp") as ( read_stream, write_stream, @@ -1435,7 +1435,7 @@ async def test_client_includes_protocol_version_header_after_init( # Verify protocol version header is present assert "mcp-protocol-version" in headers_data - assert headers_data["mcp-protocol-version"] == negotiated_version + assert headers_data[MCP_PROTOCOL_VERSION_HEADER] == negotiated_version def test_server_validates_protocol_version_header(basic_server, basic_server_url): @@ -1518,7 +1518,7 @@ def test_server_backwards_compatibility_no_protocol_version( assert init_response.status_code == 200 session_id = init_response.headers.get(MCP_SESSION_ID_HEADER) - # Test request without MCP-Protocol-Version header (backwards compatibility) + # Test request without mcp-protocol-version header (backwards compatibility) response = requests.post( f"{basic_server_url}/mcp", headers={ From 56c1b0af6fc044282ba36cdf0c0ec7060de1bebe Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Wed, 11 Jun 2025 17:43:48 +0100 Subject: [PATCH 09/10] fix: address @ochafik comments - update test comment - log warning if no protocol version is set - nits --- src/mcp/client/streamable_http.py | 12 ++++-------- src/mcp/server/streamable_http.py | 13 ++++++------- src/mcp/types.py | 1 + tests/shared/test_streamable_http.py | 20 +++++--------------- 4 files changed, 16 insertions(+), 30 deletions(-) diff --git a/src/mcp/client/streamable_http.py b/src/mcp/client/streamable_http.py index 44e0c61a7b..8c93c7ada4 100644 --- a/src/mcp/client/streamable_http.py +++ b/src/mcp/client/streamable_http.py @@ -143,6 +143,8 @@ def _maybe_extract_protocol_version_from_message( if "protocolVersion" in result: self.protocol_version = result["protocolVersion"] logger.info(f"Negotiated protocol version: {self.protocol_version}") + else: + logger.warning(f"Initialization response does not contain protocolVersion: {result}") async def _handle_sse_event( self, @@ -277,9 +279,7 @@ async def _handle_post_request(self, ctx: RequestContext) -> None: content_type = response.headers.get(CONTENT_TYPE, "").lower() if content_type.startswith(JSON): - await self._handle_json_response( - response, ctx.read_stream_writer, is_initialization - ) + await self._handle_json_response(response, ctx.read_stream_writer, is_initialization) elif content_type.startswith(SSE): await self._handle_sse_response(response, ctx, is_initialization) else: @@ -322,11 +322,7 @@ async def _handle_sse_response( is_complete = await self._handle_sse_event( sse, ctx.read_stream_writer, - resumption_callback=( - ctx.metadata.on_resumption_token_update - if ctx.metadata - else None - ), + resumption_callback=(ctx.metadata.on_resumption_token_update if ctx.metadata else None), is_initialization=is_initialization, ) # If the SSE event indicates completion, like returning respose/error diff --git a/src/mcp/server/streamable_http.py b/src/mcp/server/streamable_http.py index dd3c8840bc..45ce112cf7 100644 --- a/src/mcp/server/streamable_http.py +++ b/src/mcp/server/streamable_http.py @@ -27,6 +27,7 @@ from mcp.shared.message import ServerMessageMetadata, SessionMessage from mcp.shared.version import SUPPORTED_PROTOCOL_VERSIONS from mcp.types import ( + DEFAULT_PROTOCOL_VERSION, INTERNAL_ERROR, INVALID_PARAMS, INVALID_REQUEST, @@ -295,7 +296,7 @@ async def _handle_post_request(self, scope: Scope, request: Request, receive: Re has_json, has_sse = self._check_accept_headers(request) if not (has_json and has_sse): response = self._create_error_response( - ("Not Acceptable: Client must accept both application/json and " "text/event-stream"), + ("Not Acceptable: Client must accept both application/json and text/event-stream"), HTTPStatus.NOT_ACCEPTABLE, ) await response(scope, receive, send) @@ -696,9 +697,9 @@ async def _validate_protocol_version(self, request: Request, send: Send) -> bool # Get the protocol version from the request headers protocol_version = request.headers.get(MCP_PROTOCOL_VERSION_HEADER) - # If no protocol version provided, assume version 2025-03-26 - if not protocol_version: - protocol_version = "2025-03-26" + # If no protocol version provided, assume default version + if protocol_version is None: + protocol_version = DEFAULT_PROTOCOL_VERSION # Check if the protocol version is supported if protocol_version not in SUPPORTED_PROTOCOL_VERSIONS: @@ -711,9 +712,7 @@ async def _validate_protocol_version(self, request: Request, send: Send) -> bool return True - async def _replay_events( - self, last_event_id: str, request: Request, send: Send - ) -> None: + async def _replay_events(self, last_event_id: str, request: Request, send: Send) -> None: """ Replays events that would have been sent after the specified event ID. Only used when resumability is enabled. diff --git a/src/mcp/types.py b/src/mcp/types.py index 2949ed8e7e..d55b8eec3b 100644 --- a/src/mcp/types.py +++ b/src/mcp/types.py @@ -23,6 +23,7 @@ """ LATEST_PROTOCOL_VERSION = "2025-03-26" +DEFAULT_PROTOCOL_VERSION = "2025-03-26" ProgressToken = str | int Cursor = str diff --git a/tests/shared/test_streamable_http.py b/tests/shared/test_streamable_http.py index b94f03bedb..c4604e3fa3 100644 --- a/tests/shared/test_streamable_http.py +++ b/tests/shared/test_streamable_http.py @@ -1412,9 +1412,7 @@ async def test_streamablehttp_request_context_isolation(context_aware_server: No @pytest.mark.anyio -async def test_client_includes_protocol_version_header_after_init( - context_aware_server, basic_server_url -): +async def test_client_includes_protocol_version_header_after_init(context_aware_server, basic_server_url): """Test that client includes mcp-protocol-version header after initialization.""" async with streamablehttp_client(f"{basic_server_url}/mcp") as ( read_stream, @@ -1439,7 +1437,7 @@ async def test_client_includes_protocol_version_header_after_init( def test_server_validates_protocol_version_header(basic_server, basic_server_url): - """Test that server returns 400 Bad Request version if header missing or invalid.""" + """Test that server returns 400 Bad Request version if header unsupported or invalid.""" # First initialize a session to get a valid session ID init_response = requests.post( f"{basic_server_url}/mcp", @@ -1464,10 +1462,7 @@ def test_server_validates_protocol_version_header(basic_server, basic_server_url json={"jsonrpc": "2.0", "method": "tools/list", "id": "test-2"}, ) assert response.status_code == 400 - assert ( - MCP_PROTOCOL_VERSION_HEADER in response.text - or "protocol version" in response.text.lower() - ) + assert MCP_PROTOCOL_VERSION_HEADER in response.text or "protocol version" in response.text.lower() # Test request with unsupported protocol version (should fail) response = requests.post( @@ -1481,10 +1476,7 @@ def test_server_validates_protocol_version_header(basic_server, basic_server_url json={"jsonrpc": "2.0", "method": "tools/list", "id": "test-3"}, ) assert response.status_code == 400 - assert ( - MCP_PROTOCOL_VERSION_HEADER in response.text - or "protocol version" in response.text.lower() - ) + assert MCP_PROTOCOL_VERSION_HEADER in response.text or "protocol version" in response.text.lower() # Test request with valid protocol version (should succeed) negotiated_version = extract_protocol_version_from_sse(init_response) @@ -1502,9 +1494,7 @@ def test_server_validates_protocol_version_header(basic_server, basic_server_url assert response.status_code == 200 -def test_server_backwards_compatibility_no_protocol_version( - basic_server, basic_server_url -): +def test_server_backwards_compatibility_no_protocol_version(basic_server, basic_server_url): """Test server accepts requests without protocol version header.""" # First initialize a session to get a valid session ID init_response = requests.post( From 5a5e7a97aa1dca80487cec13442d18bab66f311a Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Thu, 12 Jun 2025 15:31:23 +0100 Subject: [PATCH 10/10] fix: address @ihrpr comments - Use InitializeResult type for parsing server init response - Add explanatory comment for DEFAULT_NEGOTIATED_VERSION constant - Include supported protocol versions in error response when version mismatch occurs --- src/mcp/client/streamable_http.py | 14 ++++++++------ src/mcp/server/streamable_http.py | 8 +++++--- src/mcp/types.py | 9 ++++++++- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/src/mcp/client/streamable_http.py b/src/mcp/client/streamable_http.py index 8c93c7ada4..39ac34d8ad 100644 --- a/src/mcp/client/streamable_http.py +++ b/src/mcp/client/streamable_http.py @@ -22,6 +22,7 @@ from mcp.shared.message import ClientMessageMetadata, SessionMessage from mcp.types import ( ErrorData, + InitializeResult, JSONRPCError, JSONRPCMessage, JSONRPCNotification, @@ -138,13 +139,14 @@ def _maybe_extract_protocol_version_from_message( ) -> None: """Extract protocol version from initialization response message.""" if isinstance(message.root, JSONRPCResponse) and message.root.result: - # Check if result has protocolVersion field - result = message.root.result - if "protocolVersion" in result: - self.protocol_version = result["protocolVersion"] + try: + # Parse the result as InitializeResult for type safety + init_result = InitializeResult.model_validate(message.root.result) + self.protocol_version = str(init_result.protocolVersion) logger.info(f"Negotiated protocol version: {self.protocol_version}") - else: - logger.warning(f"Initialization response does not contain protocolVersion: {result}") + except Exception as exc: + logger.warning(f"Failed to parse initialization response as InitializeResult: {exc}") + logger.warning(f"Raw result: {message.root.result}") async def _handle_sse_event( self, diff --git a/src/mcp/server/streamable_http.py b/src/mcp/server/streamable_http.py index 45ce112cf7..13ee27b323 100644 --- a/src/mcp/server/streamable_http.py +++ b/src/mcp/server/streamable_http.py @@ -27,7 +27,7 @@ from mcp.shared.message import ServerMessageMetadata, SessionMessage from mcp.shared.version import SUPPORTED_PROTOCOL_VERSIONS from mcp.types import ( - DEFAULT_PROTOCOL_VERSION, + DEFAULT_NEGOTIATED_VERSION, INTERNAL_ERROR, INVALID_PARAMS, INVALID_REQUEST, @@ -699,12 +699,14 @@ async def _validate_protocol_version(self, request: Request, send: Send) -> bool # If no protocol version provided, assume default version if protocol_version is None: - protocol_version = DEFAULT_PROTOCOL_VERSION + protocol_version = DEFAULT_NEGOTIATED_VERSION # Check if the protocol version is supported if protocol_version not in SUPPORTED_PROTOCOL_VERSIONS: + supported_versions = ", ".join(SUPPORTED_PROTOCOL_VERSIONS) response = self._create_error_response( - f"Bad Request: Unsupported protocol version: {protocol_version}", + f"Bad Request: Unsupported protocol version: {protocol_version}. " + + f"Supported versions: {supported_versions}", HTTPStatus.BAD_REQUEST, ) await response(request.scope, request.receive, send) diff --git a/src/mcp/types.py b/src/mcp/types.py index d55b8eec3b..824cee73c8 100644 --- a/src/mcp/types.py +++ b/src/mcp/types.py @@ -23,7 +23,14 @@ """ LATEST_PROTOCOL_VERSION = "2025-03-26" -DEFAULT_PROTOCOL_VERSION = "2025-03-26" + +""" +The default negotiated version of the Model Context Protocol when no version is specified. +We need this to satisfy the MCP specification, which requires the server to assume a +specific version if none is provided by the client. See section "Protocol Version Header" at +https://modelcontextprotocol.io/specification +""" +DEFAULT_NEGOTIATED_VERSION = "2025-03-26" ProgressToken = str | int Cursor = str