[v2] Dispatcher/ServerRunner receive-path swap — replaces BaseSession (#2710)

This commit is contained in:
Max 2026-06-09 12:58:47 +01:00 • committed by GitHub
commit 5d826490b6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -472,7 +472,7 @@ async def my_tool(x: int, ctx: Context) -> str:
The internal layers (`ToolManager.call_tool`, `Tool.run`, `Prompt.render`, `ResourceTemplate.create_resource`, etc.) now require `context` as a positional argument.
### Registering lowlevel handlers on `MCPServer` (workaround)
### Registering lowlevel handlers from `MCPServer`
`MCPServer` does not expose public APIs for `subscribe_resource`, `unsubscribe_resource`, or `set_logging_level` handlers. In v1, the workaround was to reach into the private lowlevel server and use its decorator methods:
@ -486,7 +486,7 @@ async def handle_set_logging_level(level: str) -> None:
mcp._mcp_server.subscribe_resource()(handle_subscribe) # pyright: ignore[reportPrivateUsage]
```
In v2, the lowlevel `Server` no longer has decorator methods (handlers are constructor-only), so the equivalent workaround is `_add_request_handler`:
In v2, the lowlevel `Server` supports arbitrary request handlers directly via `add_request_handler` (the decorator methods are gone; handlers are otherwise constructor-only). From `MCPServer`, access it via `_lowlevel_server`:
**After (v2):**
@ -505,11 +505,11 @@ async def handle_subscribe(ctx: ServerRequestContext, params: SubscribeRequestPa
return EmptyResult()
mcp._lowlevel_server._add_request_handler("logging/setLevel", handle_set_logging_level) # pyright: ignore[reportPrivateUsage]
mcp._lowlevel_server._add_request_handler("resources/subscribe", handle_subscribe) # pyright: ignore[reportPrivateUsage]
mcp._lowlevel_server.add_request_handler("logging/setLevel", SetLevelRequestParams, handle_set_logging_level) # pyright: ignore[reportPrivateUsage]
mcp._lowlevel_server.add_request_handler("resources/subscribe", SubscribeRequestParams, handle_subscribe) # pyright: ignore[reportPrivateUsage]
```
This is a private API and may change. A public way to register these handlers on `MCPServer` is planned; until then, use this workaround or use the lowlevel `Server` directly.
`_lowlevel_server` is private and may change. A public way to register these handlers on `MCPServer` is planned; until then, use this workaround or use the lowlevel `Server` directly.
### `MCPServer`'s `Context` logging: `message` renamed to `data`, `extra` removed
@ -664,6 +664,8 @@ ctx: ClientRequestContext
server_ctx: ServerRequestContext[LifespanContextT, RequestT]
```
`ServerRequestContext` is now a standalone dataclass — it no longer subclasses `RequestContext[ServerSession]`. It carries the same fields (`session`, `request_id`, `meta`, `lifespan_context`, `request`, `close_sse_stream`, `close_standalone_sse_stream`), so handler code is unaffected, but `isinstance(ctx, RequestContext)` checks and `RequestContext[ServerSession]` annotations need updating to `ServerRequestContext`.
The high-level `Context` class (injected into `@mcp.tool()` etc.) similarly dropped its `ServerSessionT` parameter: `Context[ServerSessionT, LifespanContextT, RequestT]` → `Context[LifespanContextT, RequestT]`. Both remaining parameters have defaults, so bare `Context` is usually sufficient:
**Before (v1):**
@ -857,6 +859,54 @@ server = Server("my-server", on_list_tools=handle_list_tools)
If you need to check whether a handler is registered, track this yourself — there is currently no public introspection API.
### Lowlevel `Server`: `add_request_handler` is now public and takes `params_type`
The private `_add_request_handler(method, handler)` escape hatch is now the public `add_request_handler(method, params_type, handler)`, alongside a matching `add_notification_handler`. Each takes a `params_type` model that incoming params are validated against before the handler runs. A message with no `params` member validates `{}` against the model, so handlers never receive `None`: all-optional models arrive with their defaults, and models with required fields reject the message as `INVALID_PARAMS` before the handler runs (matching the Go SDK).
```python
# Before (v1 / earlier v2 prereleases)
server._add_request_handler("custom/method", my_handler)
# After (v2)
server.add_request_handler("custom/method", MyParams, my_handler)
server.add_notification_handler("notifications/custom", MyNotifyParams, my_notify_handler)
```
### Lowlevel `Server`: private `_handle_*` dispatch methods removed
`Server._handle_message`, `_handle_request`, and `_handle_notification` have been removed. The receive loop and per-message dispatch now live in `JSONRPCDispatcher` and `ServerRunner`, which `Server.run()` drives internally.
These were private, but some users subclassed `Server` and overrode them to intercept requests. Use middleware instead:
```python
from collections.abc import Mapping
from typing import Any
from mcp.server import Server, ServerRequestContext
from mcp.server.context import CallNext, HandlerResult
async def logging_middleware(
ctx: ServerRequestContext[Any, Any], method: str, params: Mapping[str, Any] | None, call_next: CallNext
) -> HandlerResult:
print(f"handling {method}")
result = await call_next()
print(f"done {method}")
return result
server = Server("my-server", on_call_tool=...)
server.middleware.append(logging_middleware)
```
Middleware runs before params validation, so `params` is the raw inbound mapping (or `None`), and it also wraps unknown methods.
### Lowlevel `Server.run(raise_exceptions=True)`: transport errors no longer re-raised
`raise_exceptions=True` now only governs handler exceptions: an exception raised by an `on_*` handler propagates out of `run()`. The JSON-RPC error response is still written to the client first, regardless of the flag.
Previously it also re-raised exceptions yielded by the transport onto the read stream (e.g. JSON parse errors). Those are now debug-logged and dropped regardless of `raise_exceptions`. If you relied on `run()` exiting on a transport-level parse error, that no longer happens.
### Lowlevel `Server`: decorator-based handlers replaced with constructor `on_*` params
The lowlevel `Server` class no longer uses decorator methods for handler registration. Instead, handlers are passed as `on_*` keyword arguments to the constructor.
@ -1083,6 +1133,39 @@ from mcp.server import ServerRequestContext
# but None in notification handlers
```
### `ServerSession` is now a thin proxy (no longer a `BaseSession`)
`ServerSession` no longer subclasses `BaseSession`. It is now a small connection-scoped proxy that exposes `send_request`, `send_notification`, the typed convenience helpers (`create_message`, `elicit_form`, `send_log_message`, `send_tool_list_changed`, ...), `client_params`, and `check_client_capability`. The receive loop, `initialize` handling, and per-request task isolation that previously lived in `ServerSession` have moved to `JSONRPCDispatcher` and `ServerRunner`.
`ServerSession` is normally constructed for you by `Server.run()` and reached via `ctx.session` in handlers, so most servers are unaffected. If you were constructing or subclassing it directly:
**Constructor change:**
```python
# Before (v1)
session = ServerSession(read_stream, write_stream, init_options, stateless=False)
# After (v2)
session = ServerSession(dispatcher, connection, stateless=False)
# where `dispatcher` is a JSONRPCDispatcher and `connection` is a Connection
```
In practice, replace direct `ServerSession` use with `Server.run(read_stream, write_stream, init_options)` and let the framework wire it up.
**Removed from `mcp.server.session`:**
- `InitializationState` enum and `ServerSession._initialization_state` — initialization tracking is now on `Connection` (`connection.initialized` is an `anyio.Event`, `connection.client_params` holds the init params).
- `ServerRequestResponder` type alias.
- `ServerSession.incoming_messages` stream — there is no longer a public stream of inbound messages to iterate. Register handlers via the `on_*` constructor params (or `add_request_handler`) and use `Server.middleware` to observe every inbound request and notification (`initialize`, unknown methods, validation failures, and `notifications/initialized` included).
- `ServerSession.__aenter__` / `__aexit__` — `ServerSession` is no longer an async context manager.
- The private `_receive_loop`, `_received_request`, `_received_notification`, and `_handle_incoming` overrides — there is nothing to override on `ServerSession` anymore. To intercept inbound messages, use `Server.middleware` (see the `_handle_*` removal section above).
### `BaseSession` / `RequestResponder`: server-side cancellation tracking removed
`BaseSession._in_flight` and the `RequestResponder` members that supported it (`cancel()`, the `cancelled` and `in_flight` properties, the `on_complete` constructor argument, and the internal `CancelScope`) have been removed. These existed to let `ServerSession` cancel a handler when a `CancelledNotification` arrived; `ServerSession` no longer drives a receive loop, so they were dead code. Inbound-cancellation handling for the server now lives in `JSONRPCDispatcher`.
`BaseSession` is still used by `ClientSession`, which never relied on these members. `RequestResponder.respond()` is unchanged.
### Experimental Tasks support removed
Tasks (SEP-1686) have been removed from the MCP specification and are no longer part of this SDK. The `mcp.client.experimental`, `mcp.server.experimental`, `mcp.shared.experimental`, and `mcp.server.lowlevel.experimental` modules have been removed, along with all `Task*` types, the `tasks` capability fields, `Tool.execution`, and the `experimental` properties on `ClientSession`, `ServerSession`, `Server`, and `ServerRequestContext`.