Skip to content

Commit 5b126bf

Browse files
committed
docs: migration.md entries for the dispatcher swap
ServerSession proxy shape, lowlevel _handle_* removal, add_request_handler going public with params_type, raise_exceptions semantics narrowing, BaseSession/RequestResponder server-side cancellation tracking removal.
1 parent 513ccc3 commit 5b126bf

1 file changed

Lines changed: 88 additions & 4 deletions

File tree

docs/migration.md

Lines changed: 88 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -442,7 +442,7 @@ async def handle_set_logging_level(level: str) -> None:
442442
mcp._mcp_server.subscribe_resource()(handle_subscribe) # pyright: ignore[reportPrivateUsage]
443443
```
444444

445-
In v2, the lowlevel `Server` no longer has decorator methods (handlers are constructor-only), so the equivalent workaround is `_add_request_handler`:
445+
In v2, the lowlevel `Server` no longer has decorator methods (handlers are constructor-only), so the equivalent workaround is `add_request_handler`:
446446

447447
**After (v2):**
448448

@@ -461,11 +461,11 @@ async def handle_subscribe(ctx: ServerRequestContext, params: SubscribeRequestPa
461461
return EmptyResult()
462462

463463

464-
mcp._lowlevel_server._add_request_handler("logging/setLevel", handle_set_logging_level) # pyright: ignore[reportPrivateUsage]
465-
mcp._lowlevel_server._add_request_handler("resources/subscribe", handle_subscribe) # pyright: ignore[reportPrivateUsage]
464+
mcp._lowlevel_server.add_request_handler("logging/setLevel", SetLevelRequestParams, handle_set_logging_level) # pyright: ignore[reportPrivateUsage]
465+
mcp._lowlevel_server.add_request_handler("resources/subscribe", SubscribeRequestParams, handle_subscribe) # pyright: ignore[reportPrivateUsage]
466466
```
467467

468-
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.
468+
`_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.
469469

470470
### `MCPServer`'s `Context` logging: `message` renamed to `data`, `extra` removed
471471

@@ -620,6 +620,8 @@ ctx: ClientRequestContext
620620
server_ctx: ServerRequestContext[LifespanContextT, RequestT]
621621
```
622622

623+
`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`.
624+
623625
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:
624626

625627
**Before (v1):**
@@ -813,6 +815,55 @@ server = Server("my-server", on_list_tools=handle_list_tools)
813815

814816
If you need to check whether a handler is registered, track this yourself — there is currently no public introspection API.
815817

818+
### Lowlevel `Server`: `add_request_handler` is now public and takes `params_type`
819+
820+
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.
821+
822+
```python
823+
# Before (v1 / earlier v2 prereleases)
824+
server._add_request_handler("custom/method", my_handler)
825+
826+
# After (v2)
827+
server.add_request_handler("custom/method", MyParams, my_handler)
828+
server.add_notification_handler("notifications/custom", MyNotifyParams, my_notify_handler)
829+
```
830+
831+
### Lowlevel `Server`: private `_handle_*` dispatch methods removed
832+
833+
`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.
834+
835+
These were private, but some users subclassed `Server` and overrode them to intercept requests. Use middleware instead:
836+
837+
```python
838+
from typing import Any
839+
840+
from pydantic import BaseModel
841+
842+
from mcp.server import Server, ServerRequestContext
843+
from mcp.server.context import CallNext, HandlerResult
844+
845+
846+
async def logging_middleware(
847+
ctx: ServerRequestContext[Any, Any], method: str, params: BaseModel, call_next: CallNext
848+
) -> HandlerResult:
849+
print(f"handling {method}")
850+
result = await call_next()
851+
print(f"done {method}")
852+
return result
853+
854+
855+
server = Server("my-server", on_call_tool=...)
856+
server.middleware.append(logging_middleware)
857+
```
858+
859+
For lower-level interception (raw method/params before validation, including unknown methods), use `DispatchMiddleware` from `mcp.shared.dispatcher`.
860+
861+
### Lowlevel `Server.run(raise_exceptions=True)`: transport errors no longer re-raised
862+
863+
`raise_exceptions=True` now only governs handler exceptions: an exception raised by an `on_*` handler propagates out of `run()` instead of being converted to a JSON-RPC error response.
864+
865+
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.
866+
816867
### Lowlevel `Server`: decorator-based handlers replaced with constructor `on_*` params
817868

818869
The lowlevel `Server` class no longer uses decorator methods for handler registration. Instead, handlers are passed as `on_*` keyword arguments to the constructor.
@@ -1039,6 +1090,39 @@ from mcp.server import ServerRequestContext
10391090
# but None in notification handlers
10401091
```
10411092

1093+
### `ServerSession` is now a thin proxy (no longer a `BaseSession`)
1094+
1095+
`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`.
1096+
1097+
`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:
1098+
1099+
**Constructor change:**
1100+
1101+
```python
1102+
# Before (v1)
1103+
session = ServerSession(read_stream, write_stream, init_options, stateless=False)
1104+
1105+
# After (v2)
1106+
session = ServerSession(dispatcher, connection, stateless=False)
1107+
# where `dispatcher` is a JSONRPCDispatcher and `connection` is a Connection
1108+
```
1109+
1110+
In practice, replace direct `ServerSession` use with `Server.run(read_stream, write_stream, init_options)` and let the framework wire it up.
1111+
1112+
**Removed from `mcp.server.session`:**
1113+
1114+
- `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).
1115+
- `ServerRequestResponder` type alias.
1116+
- `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 request.
1117+
- `ServerSession.__aenter__` / `__aexit__``ServerSession` is no longer an async context manager.
1118+
- 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` or `DispatchMiddleware` (see the `_handle_*` removal section above).
1119+
1120+
### `BaseSession` / `RequestResponder`: server-side cancellation tracking removed
1121+
1122+
`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`.
1123+
1124+
`BaseSession` is still used by `ClientSession`, which never relied on these members. `RequestResponder.respond()` is unchanged.
1125+
10421126
### Experimental Tasks support removed
10431127

10441128
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`.

0 commit comments

Comments
 (0)