Skip to content

Commit 29095ed

Browse files
committed
Update the tasks story and docs for the reworked extension
The stdio leg now asserts graceful degradation (a legacy connection gets a plain CallToolResult, never a task) instead of returning silently; the stories index lists tasks as a current feature story; the migration note reflects the -32021 capability error, the tasks/update acknowledgement, and the pluggable store, and drops the taskSupport item (SEP-2663 defines no per-tool execution flag).
1 parent d01918a commit 29095ed

5 files changed

Lines changed: 41 additions & 24 deletions

File tree

docs/migration.md

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -433,12 +433,17 @@ Two reference extensions ship in their own modules:
433433
- `mcp.server.tasks.Tasks` (`io.modelcontextprotocol/tasks`, SEP-2663) defers a
434434
`tools/call` as a task: for a client that declared the extension on a modern
435435
connection, the server may return a `CreateTaskResult` (`resultType: "task"`)
436-
instead of the `CallToolResult`, and the client polls `tasks/get` /
437-
`tasks/cancel`. The server decides augmentation (the legacy `params.task` field
438-
is ignored); a `tasks/*` call from a non-declaring client is rejected with
439-
`-32003`. This is the conformant core; `tasks/update` + the MRTR input loop,
440-
`ToolExecution.taskSupport` gating, `notifications/tasks`, and task routing
441-
headers are deferred.
436+
instead of the `CallToolResult`, and the client fetches the result via
437+
`tasks/get` (`tasks/update` and `tasks/cancel` are empty acknowledgements).
438+
The server decides augmentation (the legacy `params.task` field is ignored),
439+
passes multi round-trip `input_required` interims through un-augmented, and
440+
keeps completed tasks in a pluggable `TaskStore` (`Tasks(store=...)`,
441+
in-memory default) that enforces `default_ttl_ms`. A `tasks/*` call from a
442+
non-declaring modern client is rejected with `-32021` (missing required
443+
client capability); legacy calls get `METHOD_NOT_FOUND`. This is the core
444+
SEP-2663 surface; background execution (`working` tasks), the in-task
445+
`input_required` loop over `tasks/update`, `notifications/tasks`, and task
446+
routing headers are deferred.
442447

443448
A `MethodBinding` may set `protocol_versions` to scope an extension method to
444449
specific wire versions; a request at any other version is `METHOD_NOT_FOUND`. An

examples/stories/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ opens with a banner saying what replaces it.
132132
| [`sampling`](sampling/) | server asks the client's LLM mid-tool (push request) | deprecated |
133133
| [`stickynotes`](stickynotes/) | capstone: tools mutate state → resources + `list_changed` + elicit guard | current |
134134
| [`custom_methods`](custom_methods/) | vendor-prefixed JSON-RPC via `add_request_handler` / `send_request` | current |
135+
| [`tasks`](tasks/) | `io.modelcontextprotocol/tasks` extension: server-decided `CreateTaskResult`, `tasks/get` | current |
135136
| [`schema_validators`](schema_validators/) | tool input schema from pydantic / TypedDict / dataclass / dict | current |
136137
| [`middleware`](middleware/) | server-side request/response middleware | current |
137138
| [`parallel_calls`](parallel_calls/) | two clients rendezvous in one tool; per-call progress attribution | current |
@@ -154,7 +155,6 @@ opens with a banner saying what replaces it.
154155
| [`caching`](caching/) | `CacheableResult` ttl/scope hints; client honouring | not yet implemented |
155156
| [`mrtr`](mrtr/) | `InputRequiredResult` round-trip with `requestState` HMAC | not yet implemented — [#2898](https://github.com/modelcontextprotocol/python-sdk/issues/2898) |
156157
| [`subscriptions`](subscriptions/) | `subscriptions/listen`, `ServerEventBus`, `Client.listen()` | not yet implemented — [#2901](https://github.com/modelcontextprotocol/python-sdk/issues/2901) |
157-
| [`tasks`](tasks/) | `io.modelcontextprotocol/tasks` extension | not yet implemented |
158158
| [`apps`](apps/) | MCP Apps: `ui://` resource + `_meta.ui` | not yet implemented — [#2896](https://github.com/modelcontextprotocol/python-sdk/issues/2896) |
159159
| [`skills`](skills/) | SEP-2640 skills extension | not yet implemented — [#2896](https://github.com/modelcontextprotocol/python-sdk/issues/2896) |
160160
| [`events`](events/) | `io.modelcontextprotocol/events` extension | not yet implemented |

examples/stories/tasks/README.md

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,40 +2,46 @@
22

33
Task-augmented execution (SEP-2663). A client declares the
44
`io.modelcontextprotocol/tasks` extension; the server may then answer a
5-
`tools/call` with a `CreateTaskResult` (carrying a task id) instead of blocking,
6-
and the client polls `tasks/get` for status and the eventual result.
5+
`tools/call` with a `CreateTaskResult` (carrying a task id) instead of the
6+
`CallToolResult`, and the client fetches the result via `tasks/get`.
77

88
## Run it
99

1010
```bash
11-
# stdio (default — the client spawns the server as a subprocess)
11+
# stdio (default) — today's stdio negotiates the legacy wire, which cannot carry
12+
# the extension capability, so this leg demonstrates graceful degradation: the
13+
# same tools/call returns a plain CallToolResult, never a task.
1214
uv run python -m stories.tasks.client
1315

14-
# HTTP — the client self-hosts the server on a free port, runs, then tears it down
16+
# HTTP — the modern wire negotiates the extension; the server defers the call as
17+
# a task and the client reads the result back via tasks/get
1518
uv run python -m stories.tasks.client --http
1619
```
1720

1821
## What to look at
1922

2023
- `server.py` `MCPServer("tasks-example", extensions=[Tasks(default_ttl_ms=...)])`
2124
opt in at construction. The extension advertises `io.modelcontextprotocol/tasks`
22-
and serves `tasks/get` and `tasks/cancel`.
25+
and serves `tasks/get`, `tasks/update`, and `tasks/cancel` on the modern wire
26+
(legacy calls are `METHOD_NOT_FOUND`; the extension is not defined there).
2327
- `mcp.server.tasks.Tasks.intercept_tool_call` — the server DECIDES augmentation;
2428
the legacy `params.task` field is ignored. It augments only for a client that
2529
declared the extension on the request, returning a flat `CreateTaskResult`
2630
(`resultType: "task"`).
2731
- `client.py` `Client(target, extensions={EXTENSION_ID: {}})` — declaring the
2832
extension is what lets the server defer; `main` then reads the `CreateTaskResult`
29-
and polls `tasks/get`, whose completed `DetailedTask` inlines the original
33+
and fetches `tasks/get`, whose completed envelope inlines the original
3034
`CallToolResult`.
3135

3236
## Scope
3337

34-
This is the SEP-2663 conformant *core*. The tool runs to completion inline (so a
35-
task is observed as `completed` immediately), and the store is in-memory. Deferred
36-
to follow-ups, each needing deeper SDK plumbing: `tasks/update` + the MRTR
37-
`input_required` loop, `ToolExecution.taskSupport` gating with the `-32021`
38-
required-task error, `notifications/tasks`, and SEP-2243 task routing headers.
38+
This is the core SEP-2663 surface. The tool runs to completion inline, so a task
39+
is recorded directly as `completed` (the SEP allows any initial status), and
40+
completed tasks live in a pluggable `TaskStore` (`Tasks(store=...)`, in-memory
41+
default) that enforces `default_ttl_ms`. Deferred to follow-ups, each needing
42+
deeper SDK plumbing: background execution (returning `working` tasks), the
43+
in-task `input_required`/`inputResponses` loop over `tasks/update`,
44+
`notifications/tasks`, and SEP-2243 task routing headers.
3945

4046
## Spec
4147

examples/stories/tasks/client.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""Declare the tasks extension, let the server defer a tool call, then poll tasks/get.
1+
"""Declare the tasks extension, let the server defer a tool call, then fetch the result via tasks/get.
22
33
The client declares `io.modelcontextprotocol/tasks` (via `Client(extensions=...)`),
44
so the server is free to answer `tools/call` with a `CreateTaskResult`. `Client`
@@ -32,8 +32,14 @@ async def main(target: Target, *, mode: str = "auto") -> None:
3232
async with Client(target, mode=mode, extensions={EXTENSION_ID: {}}) as client:
3333
# The extension is a modern-only capability negotiated over server/discover.
3434
# A legacy connection (today's stdio) cannot carry it, and the server then
35-
# must not augment, so the task flow only runs once it is negotiated.
35+
# must not augment: the same tools/call degrades to a plain CallToolResult.
3636
if client.server_capabilities.extensions is None:
37+
result = await client.call_tool("render_report", {"title": "Q3", "sections": 2})
38+
assert isinstance(result.content[0], types.TextContent), result
39+
assert result.content[0].text.startswith("# Q3"), result
40+
# No 2025-style related-task _meta either; SEP-2663 augmentation would
41+
# have replaced the whole result, failing CallToolResult parsing above.
42+
assert result.meta is None, result
3743
return
3844
assert client.server_capabilities.extensions == {EXTENSION_ID: {}}
3945

examples/stories/tasks/server.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
"""Tasks (SEP-2663): the server defers a tool call as a task the client polls.
1+
"""Tasks (SEP-2663): the server defers a tool call as a task the client fetches.
22
33
`Tasks` is an opt-in `Extension`. The server decides, per request, to return a
44
`CreateTaskResult` instead of a `CallToolResult` for a client that declared the
5-
`io.modelcontextprotocol/tasks` extension; the client then polls `tasks/get` for
6-
status and the eventual result. `render_report` is the kind of slower, multi-step
7-
tool a caller would rather run as a task than block on.
5+
`io.modelcontextprotocol/tasks` extension; the client then fetches the result via
6+
`tasks/get`. `render_report` is the kind of slower, multi-step tool a caller
7+
would rather run as a task than block on.
88
"""
99

1010
from mcp.server.mcpserver import MCPServer

0 commit comments

Comments
 (0)