Fix unknown-method error code and add a protocol version registry (#2836)

This commit is contained in:
Max 2026-06-11 16:47:22 +01:00 • committed by GitHub
commit 7267818e44
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 430 additions and 23 deletions

View file

@ -83,6 +83,10 @@ class Connection:
"""The full `initialize` request params; `None` before initialization."""
protocol_version: str | None
"""The protocol version negotiated during `initialize`; `None` before
initialization. Stateless connections don't require the handshake, so this
normally stays `None` there (a client that sends `initialize` anyway still
commits it). Handlers read this as `ServerSession.protocol_version`."""
initialized: anyio.Event
"""Set when `notifications/initialized` arrives (matches TS `oninitialized`);

View file

@ -243,12 +243,17 @@ class ServerRunner(Generic[LifespanT]):
# (read loop parked), so awaiting the peer anywhere on this path deadlocks.
if method == "initialize":
return self._handle_initialize(params)
# Methods without a handler are METHOD_NOT_FOUND regardless of
# initialization state: JSON-RPC 2.0 reserves -32601 for "not
# available on this server", and clients probing a server before
# the handshake key off that code. The init gate below therefore
# only ever applies to methods the server actually serves.
entry = self.server.get_request_handler(method)
if entry is None:
raise MCPError(code=METHOD_NOT_FOUND, message="Method not found", data=method)
if not self.connection.initialize_accepted and method not in _INIT_EXEMPT:
# Pinned compat: the same error shape the union validation produced.
raise MCPError(code=INVALID_PARAMS, message="Invalid request parameters", data="")
entry = self.server.get_request_handler(method)
if entry is None:
raise MCPError(code=METHOD_NOT_FOUND, message="Method not found")
# Absent params validate as {} (required fields still reject), so
# the handler receives the model with its defaults, never None.
typed_params = entry.params_type.model_validate({} if params is None else params, by_name=False)

View file

@ -50,6 +50,17 @@ class ServerSession:
"""The client's `initialize` request params; `None` before initialization."""
return self._connection.client_params
@property
def protocol_version(self) -> str | None:
"""The protocol version negotiated during `initialize`.
`None` before initialization completes. Stateless connections don't
require the handshake, so this is normally `None` there (on streamable
HTTP the per-request version is the `MCP-Protocol-Version` header,
available via `ctx.request.headers`).
"""
return self._connection.protocol_version
async def send_request(
self,
request: types.ServerRequest,

View file

@ -28,7 +28,7 @@ from mcp.server.transport_security import TransportSecurityMiddleware, Transport
from mcp.shared._context_streams import ContextReceiveStream, ContextSendStream, create_context_streams
from mcp.shared._stream_protocols import ReadStream, WriteStream
from mcp.shared.message import ServerMessageMetadata, SessionMessage
from mcp.shared.version import SUPPORTED_PROTOCOL_VERSIONS
from mcp.shared.version import SUPPORTED_PROTOCOL_VERSIONS, is_version_at_least
from mcp.types import (
DEFAULT_NEGOTIATED_VERSION,
INTERNAL_ERROR,
@ -238,7 +238,7 @@ class StreamableHTTPServerTransport:
the stream is closed early because they didn't receive a priming event.
"""
# Only provide close callbacks when client supports resumability
if self._event_store and protocol_version >= "2025-11-25":
if self._event_store and is_version_at_least(protocol_version, "2025-11-25"):
async def close_stream_callback() -> None:
self.close_sse_stream(request_id)
@ -271,7 +271,7 @@ class StreamableHTTPServerTransport:
if not self._event_store:
return
# Priming events have empty data which older clients cannot handle.
if protocol_version < "2025-11-25":
if not is_version_at_least(protocol_version, "2025-11-25"):
return
priming_event_id = await self._event_store.store_event(
str(request_id), # Convert RequestId to StreamId (str)