Skip to content

Commit 4c8fe51

Browse files
committed
refactor: decouple ExperimentalHandlers from Server internals, update migration docs
- ExperimentalHandlers takes add_handler/has_handler callbacks instead of dicts - Remove decorator methods and func_inspection dependency from ExperimentalHandlers - Default task handlers use (ctx, params) signature with proper types - Add has_handler() method to Server - Update migration docs with handler pattern, context changes, typed examples
1 parent 5fbe987 commit 4c8fe51

File tree

3 files changed

+199
-157
lines changed

3 files changed

+199
-157
lines changed

docs/migration.md

Lines changed: 151 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -406,23 +406,167 @@ params = CallToolRequestParams(
406406
)
407407
```
408408

409-
## New Features
409+
### Lowlevel `Server`: decorator-based handlers replaced with `RequestHandler`/`NotificationHandler`
410410

411-
### `streamable_http_app()` available on lowlevel Server
411+
The lowlevel `Server` class no longer uses decorator methods for handler registration. Instead, handlers are `RequestHandler` and `NotificationHandler` objects passed to the constructor or added via `add_handler()`.
412412

413-
The `streamable_http_app()` method is now available directly on the lowlevel `Server` class, not just `MCPServer`. This allows using the streamable HTTP transport without the MCPServer wrapper.
413+
**Before (v1):**
414414

415415
```python
416416
from mcp.server.lowlevel.server import Server
417417

418418
server = Server("my-server")
419419

420-
# Register handlers...
421420
@server.list_tools()
422-
async def list_tools():
423-
return [...]
421+
async def handle_list_tools():
422+
return [types.Tool(name="my_tool", description="A tool", inputSchema={})]
423+
424+
@server.call_tool()
425+
async def handle_call_tool(name: str, arguments: dict):
426+
return [types.TextContent(type="text", text=f"Called {name}")]
427+
```
428+
429+
**After (v2):**
430+
431+
```python
432+
from mcp.server.lowlevel import Server, RequestHandler
433+
from mcp.shared.context import RequestHandlerContext
434+
from mcp.types import (
435+
CallToolRequestParams,
436+
CallToolResult,
437+
ListToolsResult,
438+
PaginatedRequestParams,
439+
TextContent,
440+
Tool,
441+
)
442+
443+
async def handle_list_tools(
444+
ctx: RequestHandlerContext, params: PaginatedRequestParams | None
445+
) -> ListToolsResult:
446+
return ListToolsResult(tools=[
447+
Tool(name="my_tool", description="A tool", inputSchema={})
448+
])
449+
450+
async def handle_call_tool(
451+
ctx: RequestHandlerContext, params: CallToolRequestParams
452+
) -> CallToolResult:
453+
return CallToolResult(
454+
content=[TextContent(type="text", text=f"Called {params.name}")],
455+
is_error=False,
456+
)
457+
458+
server = Server(
459+
"my-server",
460+
handlers=[
461+
RequestHandler("tools/list", handler=handle_list_tools),
462+
RequestHandler("tools/call", handler=handle_call_tool),
463+
],
464+
)
465+
```
466+
467+
**Key differences:**
468+
469+
- Handlers receive `(ctx, params)` instead of the full request object or unpacked arguments. `ctx` is a `RequestHandlerContext` (for requests) or `NotificationHandlerContext` (for notifications) with `session`, `lifespan_context`, and `experimental` fields. `params` is the typed request params object.
470+
- Handlers return the full result type (e.g. `ListToolsResult`) rather than unwrapped values (e.g. `list[Tool]`).
471+
- Registration uses method strings (`"tools/call"`) instead of request types (`CallToolRequest`).
472+
- Handlers can be added after construction with `server.add_handler()` (silently replaces existing handlers for the same method).
473+
- `server.has_handler(method)` checks if a handler is registered for a given method string.
474+
475+
**Notification handlers:**
476+
477+
```python
478+
from mcp.server.lowlevel import NotificationHandler
479+
from mcp.shared.context import NotificationHandlerContext
480+
from mcp.types import ProgressNotificationParams
481+
482+
async def handle_progress(
483+
ctx: NotificationHandlerContext, params: ProgressNotificationParams
484+
) -> None:
485+
print(f"Progress: {params.progress}/{params.total}")
486+
487+
server = Server(
488+
"my-server",
489+
handlers=[
490+
NotificationHandler("notifications/progress", handler=handle_progress),
491+
],
492+
)
493+
```
494+
495+
### Lowlevel `Server`: `request_context` property and `request_ctx` contextvar removed
496+
497+
The `server.request_context` property and the `request_ctx` module-level contextvar have been removed. Request context is now passed directly to handlers as the first argument (`ctx`).
498+
499+
**Before (v1):**
500+
501+
```python
502+
from mcp.server.lowlevel.server import request_ctx
503+
504+
@server.call_tool()
505+
async def handle_call_tool(name: str, arguments: dict):
506+
ctx = server.request_context # or request_ctx.get()
507+
await ctx.session.send_log_message(level="info", data="Processing...")
508+
return [types.TextContent(type="text", text="Done")]
509+
```
510+
511+
**After (v2):**
512+
513+
```python
514+
from mcp.shared.context import RequestHandlerContext
515+
from mcp.types import CallToolRequestParams, CallToolResult, TextContent
516+
517+
async def handle_call_tool(
518+
ctx: RequestHandlerContext, params: CallToolRequestParams
519+
) -> CallToolResult:
520+
await ctx.session.send_log_message(level="info", data="Processing...")
521+
return CallToolResult(
522+
content=[TextContent(type="text", text="Done")],
523+
is_error=False,
524+
)
525+
```
526+
527+
### `RequestContext` split into `HandlerContext`, `RequestHandlerContext`, `NotificationHandlerContext`
528+
529+
The `RequestContext` class in `mcp.shared.context` has been replaced with a three-class hierarchy:
530+
531+
- `HandlerContext` — base class with `session`, `lifespan_context`, `experimental`
532+
- `RequestHandlerContext(HandlerContext)` — adds `request_id`, `meta`, `request`, `close_sse_stream`, `close_standalone_sse_stream`
533+
- `NotificationHandlerContext(HandlerContext)` — empty subclass for notifications
534+
535+
**Before (v1):**
536+
537+
```python
538+
from mcp.shared.context import RequestContext
539+
```
540+
541+
**After (v2):**
542+
543+
```python
544+
from mcp.shared.context import HandlerContext, RequestHandlerContext, NotificationHandlerContext
545+
```
546+
547+
## New Features
548+
549+
### `streamable_http_app()` available on lowlevel Server
550+
551+
The `streamable_http_app()` method is now available directly on the lowlevel `Server` class, not just `MCPServer`. This allows using the streamable HTTP transport without the MCPServer wrapper.
552+
553+
```python
554+
from mcp.server.lowlevel import Server, RequestHandler
555+
from mcp.shared.context import RequestHandlerContext
556+
from mcp.types import ListToolsResult, PaginatedRequestParams
557+
558+
async def handle_list_tools(
559+
ctx: RequestHandlerContext, params: PaginatedRequestParams | None
560+
) -> ListToolsResult:
561+
return ListToolsResult(tools=[...])
562+
563+
server = Server(
564+
"my-server",
565+
handlers=[
566+
RequestHandler("tools/list", handler=handle_list_tools),
567+
],
568+
)
424569

425-
# Create a Starlette app for streamable HTTP
426570
app = server.streamable_http_app(
427571
streamable_http_path="/mcp",
428572
json_response=False,

0 commit comments

Comments
 (0)