Skip to content

Commit 2a8a295

Browse files
committed
fix: allow ping handler override, add task overloads, update stale docstrings
- Server.__init__: register default ping handler after user handlers so it can be overridden via constructor - handler.py: add typed overloads for experimental task request methods (tasks/get, tasks/result, tasks/list, tasks/cancel) and task notification (notifications/tasks/status) - Update stale docstrings in task_result_handler.py, request_context.py, and helpers.py to reflect new handler pattern - Update docs/experimental/index.md example - Add task handler migration section to docs/migration.md
1 parent e72832e commit 2a8a295

File tree

7 files changed

+105
-22
lines changed

7 files changed

+105
-22
lines changed

docs/experimental/index.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,13 @@ Tasks are useful for:
2727
Experimental features are accessed via the `.experimental` property:
2828

2929
```python
30-
# Server-side
31-
@server.experimental.get_task()
32-
async def handle_get_task(request: GetTaskRequest) -> GetTaskResult:
33-
...
30+
# Server-side: register a custom task handler
31+
server = Server(
32+
name="my-server",
33+
handlers=[
34+
RequestHandler("tasks/get", handler=handle_get_task),
35+
],
36+
)
3437

3538
# Client-side
3639
result = await session.experimental.call_tool_as_task("tool_name", {"arg": "value"})

docs/migration.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,41 @@ from mcp.shared.context import RequestContext
533533
# but None in notification handlers
534534
```
535535

536+
### Experimental: task handler decorators removed
537+
538+
The experimental decorator methods on `ExperimentalHandlers` (`@server.experimental.list_tasks()`, `@server.experimental.get_task()`, etc.) have been removed. Custom task handlers are now registered as `RequestHandler` objects passed to the `Server` constructor, consistent with the new handler pattern.
539+
540+
Default task handlers are still registered automatically via `server.experimental.enable_tasks()`.
541+
542+
**Before (v1):**
543+
544+
```python
545+
server = Server("my-server")
546+
server.experimental.enable_tasks(task_store)
547+
548+
@server.experimental.get_task()
549+
async def custom_get_task(request: GetTaskRequest) -> GetTaskResult:
550+
...
551+
```
552+
553+
**After (v2):**
554+
555+
```python
556+
from mcp.server.lowlevel import Server, RequestHandler
557+
from mcp.types import GetTaskRequestParams, GetTaskResult
558+
559+
async def custom_get_task(ctx, params: GetTaskRequestParams) -> GetTaskResult:
560+
...
561+
562+
server = Server(
563+
"my-server",
564+
handlers=[
565+
RequestHandler("tasks/get", handler=custom_get_task),
566+
],
567+
)
568+
server.experimental.enable_tasks(task_store)
569+
```
570+
536571
## New Features
537572

538573
### `streamable_http_app()` available on lowlevel Server

src/mcp/server/experimental/request_context.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -168,10 +168,7 @@ async def run_task(
168168
RuntimeError: If task support is not enabled or task_metadata is missing
169169
170170
Example:
171-
@server.call_tool()
172-
async def handle_tool(name: str, args: dict):
173-
ctx = server.request_context
174-
171+
async def handle_tool(ctx: RequestContext, params: CallToolRequestParams) -> CallToolResult:
175172
async def work(task: ServerTaskContext) -> CallToolResult:
176173
result = await task.elicit(
177174
message="Are you sure?",

src/mcp/server/experimental/task_result_handler.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -47,14 +47,14 @@ class TaskResultHandler:
4747
# Create handler with store and queue
4848
handler = TaskResultHandler(task_store, message_queue)
4949
50-
# Register it with the server
51-
@server.experimental.get_task_result()
52-
async def handle_task_result(req: GetTaskPayloadRequest) -> GetTaskPayloadResult:
53-
ctx = server.request_context
54-
return await handler.handle(req, ctx.session, ctx.request_id)
55-
56-
# Or use the convenience method
57-
handler.register(server)
50+
# Register as a handler with the lowlevel server
51+
async def handle_task_result(ctx, params):
52+
return await handler.handle(
53+
GetTaskPayloadRequest(params=params), ctx.session, ctx.request_id
54+
)
55+
server = Server(handlers=[
56+
RequestHandler("tasks/result", handler=handle_task_result),
57+
])
5858
"""
5959

6060
def __init__(

src/mcp/server/lowlevel/handler.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,21 @@
1111
CallToolRequestParams,
1212
CallToolResult,
1313
CancelledNotificationParams,
14+
CancelTaskRequestParams,
15+
CancelTaskResult,
1416
CompleteRequestParams,
1517
CompleteResult,
1618
EmptyResult,
1719
GetPromptRequestParams,
1820
GetPromptResult,
21+
GetTaskPayloadRequestParams,
22+
GetTaskPayloadResult,
23+
GetTaskRequestParams,
24+
GetTaskResult,
1925
ListPromptsResult,
2026
ListResourcesResult,
2127
ListResourceTemplatesResult,
28+
ListTasksResult,
2229
ListToolsResult,
2330
NotificationParams,
2431
PaginatedRequestParams,
@@ -28,6 +35,7 @@
2835
RequestParams,
2936
SetLevelRequestParams,
3037
SubscribeRequestParams,
38+
TaskStatusNotificationParams,
3139
UnsubscribeRequestParams,
3240
)
3341

@@ -137,6 +145,38 @@ def __init__(
137145
handler: Callable[[Ctx, CompleteRequestParams], Awaitable[CompleteResult]],
138146
) -> None: ...
139147

148+
@overload
149+
def __init__(
150+
self,
151+
method: Literal["tasks/get"],
152+
handler: Callable[[Ctx, GetTaskRequestParams], Awaitable[GetTaskResult]],
153+
) -> None:
154+
"""Experimental: Tasks may evolve in future protocol versions."""
155+
156+
@overload
157+
def __init__(
158+
self,
159+
method: Literal["tasks/result"],
160+
handler: Callable[[Ctx, GetTaskPayloadRequestParams], Awaitable[GetTaskPayloadResult]],
161+
) -> None:
162+
"""Experimental: Tasks may evolve in future protocol versions."""
163+
164+
@overload
165+
def __init__(
166+
self,
167+
method: Literal["tasks/list"],
168+
handler: Callable[[Ctx, PaginatedRequestParams | None], Awaitable[ListTasksResult]],
169+
) -> None:
170+
"""Experimental: Tasks may evolve in future protocol versions."""
171+
172+
@overload
173+
def __init__(
174+
self,
175+
method: Literal["tasks/cancel"],
176+
handler: Callable[[Ctx, CancelTaskRequestParams], Awaitable[CancelTaskResult]],
177+
) -> None:
178+
"""Experimental: Tasks may evolve in future protocol versions."""
179+
140180
@overload
141181
def __init__(
142182
self,
@@ -187,6 +227,14 @@ def __init__(
187227
handler: Callable[[Ctx, NotificationParams | None], Awaitable[None]],
188228
) -> None: ...
189229

230+
@overload
231+
def __init__(
232+
self,
233+
method: Literal["notifications/tasks/status"],
234+
handler: Callable[[Ctx, TaskStatusNotificationParams], Awaitable[None]],
235+
) -> None:
236+
"""Experimental: Tasks may evolve in future protocol versions."""
237+
190238
@overload
191239
def __init__(
192240
self,

src/mcp/server/lowlevel/server.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -136,9 +136,6 @@ def __init__(
136136
self._session_manager: StreamableHTTPSessionManager | None = None
137137
logger.debug("Initializing server %r", name)
138138

139-
# Register default ping handler
140-
self._request_handlers["ping"] = RequestHandler("ping", handler=_ping_handler)
141-
142139
# Process user-provided handlers with duplicate detection
143140
for handler in handlers:
144141
if isinstance(handler, RequestHandler):
@@ -152,6 +149,10 @@ def __init__(
152149
else:
153150
raise TypeError(f"Unknown handler type: {type(handler)}")
154151

152+
# Register default ping handler (after user handlers, so users can override)
153+
if "ping" not in self._request_handlers:
154+
self._request_handlers["ping"] = RequestHandler("ping", handler=_ping_handler)
155+
155156
def _add_handler(self, handler: Handler) -> None:
156157
"""Add a handler, silently replacing any existing handler for the same method."""
157158
if isinstance(handler, RequestHandler):

src/mcp/shared/experimental/tasks/helpers.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,8 @@ async def cancel_task(
7272
- Task is already in a terminal state (completed, failed, cancelled)
7373
7474
Example:
75-
@server.experimental.cancel_task()
76-
async def handle_cancel(request: CancelTaskRequest) -> CancelTaskResult:
77-
return await cancel_task(store, request.params.taskId)
75+
async def handle_cancel(ctx, params: CancelTaskRequestParams) -> CancelTaskResult:
76+
return await cancel_task(store, params.task_id)
7877
"""
7978
task = await store.get_task(task_id)
8079
if task is None:

0 commit comments

Comments
 (0)