Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions docs/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -424,10 +424,21 @@ from mcp.server.apps import Apps
mcp = MCPServer("demo", extensions=[Apps()])
```

The reference extension is `mcp.server.apps.Apps` (`io.modelcontextprotocol/ui`):
it binds a tool to a `ui://` UI resource via `_meta.ui.resourceUri`, and
`client_supports_apps(ctx)` gates the SEP-2133 text-only fallback (checking the
client advertised the `text/html;profile=mcp-app` MIME type).
Two reference extensions ship in their own modules:

- `mcp.server.apps.Apps` (`io.modelcontextprotocol/ui`) binds a tool to a `ui://`
UI resource via `_meta.ui.resourceUri`, and `client_supports_apps(ctx)` gates the
SEP-2133 text-only fallback (checking the client advertised the
`text/html;profile=mcp-app` MIME type).
- `mcp.server.tasks.Tasks` (`io.modelcontextprotocol/tasks`, SEP-2663) defers a
`tools/call` as a task: for a client that declared the extension on a modern
connection, the server may return a `CreateTaskResult` (`resultType: "task"`)
instead of the `CallToolResult`, and the client polls `tasks/get` /
`tasks/cancel`. The server decides augmentation (the legacy `params.task` field
is ignored); a `tasks/*` call from a non-declaring client is rejected with
`-32003`. This is the conformant core; `tasks/update` + the MRTR input loop,
`ToolExecution.taskSupport` gating, `notifications/tasks`, and task routing
headers are deferred.

A `MethodBinding` may set `protocol_versions` to scope an extension method to
specific wire versions; a request at any other version is `METHOD_NOT_FOUND`. An
Expand Down
1 change: 1 addition & 0 deletions examples/stories/apps/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,5 @@ uv run python -m stories.apps.client --http

## See also

`tasks/` (the interceptive half of the extension API),
`custom_methods/` (registering a non-spec method without an extension).
8 changes: 7 additions & 1 deletion examples/stories/manifest.toml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,13 @@ lowlevel = false
transports = ["in-memory", "http-asgi"]
era = "dual-in-body"

[story.tasks]
# SEP-2663 tasks extension; server-decided augmentation + tasks/get drop to client.session.
# extensions ride server/discover (modern-only), so the connection is pinned to "auto".
lowlevel = false
transports = ["in-memory", "http-asgi"]
era = "dual-in-body"

[story.schema_validators]

[story.middleware]
Expand Down Expand Up @@ -150,6 +157,5 @@ fixed_port = 8000 # issuer/PRM metadata bake in :8
[deferred]
caching = "client honouring + per-result override unlanded"
subscriptions = "#2901 — Client.listen / ServerEventBus"
tasks = "SEP-2663 — tasks extension runtime (server-decided augmentation, CreateTaskResult)"
skills = "#2896 — SEP-2640"
events = "#2901 + #2896"
54 changes: 39 additions & 15 deletions examples/stories/tasks/README.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,48 @@
# tasks

Task-augmented execution: a requestor augments a `tools/call` with a `task`, the
receiver returns a `CreateTaskResult` immediately, and the requestor polls
`tasks/get` and retrieves the deferred result.

**Status: deferred.** Tasks ship in 2026-07-28 as
[SEP-2663](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/seps/2663-tasks-extension.md),
an `io.modelcontextprotocol/tasks` extension that is wire-incompatible with the
2025-11-25 in-core design still carried (types-only) in `mcp_types`. The runtime
needs to be built to the SEP — server-decided augmentation (ignoring the legacy
`params.task`), the `{tasks/get, tasks/update, tasks/cancel}` method set, the
`resultType: "task"` envelope, `execution.taskSupport` gating, and `ttlMs`
fields — so it lands in a separate PR with the conformance `tasks-*` scenarios
wired in.
Task-augmented execution (SEP-2663). A client declares the

@cubic-dev-ai cubic-dev-ai Bot Jun 26, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Docs are inconsistent: the Tasks story is documented as implemented/runnable here, but the stories index still labels tasks/ as "not yet implemented".

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At examples/stories/tasks/README.md, line 3:

<comment>Docs are inconsistent: the Tasks story is documented as implemented/runnable here, but the stories index still labels `tasks/` as "not yet implemented".</comment>

<file context>
@@ -1,24 +1,48 @@
-`resultType: "task"` envelope, `execution.taskSupport` gating, and `ttlMs`
-fields — so it lands in a separate PR with the conformance `tasks-*` scenarios
-wired in.
+Task-augmented execution (SEP-2663). A client declares the
+`io.modelcontextprotocol/tasks` extension; the server may then answer a
+`tools/call` with a `CreateTaskResult` (carrying a task id) instead of blocking,
</file context>
Fix with cubic

`io.modelcontextprotocol/tasks` extension; the server may then answer a
`tools/call` with a `CreateTaskResult` (carrying a task id) instead of blocking,
and the client polls `tasks/get` for status and the eventual result.

## Run it

```bash
# stdio (default — the client spawns the server as a subprocess)

@cubic-dev-ai cubic-dev-ai Bot Jun 26, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: README run instructions imply stdio demonstrates tasks flow, but stdio cannot negotiate the tasks extension. Users running the default command will not see the documented task behavior.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At examples/stories/tasks/README.md, line 11:

<comment>README run instructions imply stdio demonstrates tasks flow, but stdio cannot negotiate the tasks extension. Users running the default command will not see the documented task behavior.</comment>

<file context>
@@ -1,24 +1,48 @@
+## Run it
+
+```bash
+# stdio (default — the client spawns the server as a subprocess)
+uv run python -m stories.tasks.client
+
</file context>
Suggested change
# stdio (default — the client spawns the server as a subprocess)
# stdio (legacy handshake only; cannot negotiate `io.modelcontextprotocol/tasks` yet)
Fix with cubic

uv run python -m stories.tasks.client

# HTTP — the client self-hosts the server on a free port, runs, then tears it down
uv run python -m stories.tasks.client --http
```

## What to look at

- `server.py` `MCPServer("tasks-example", extensions=[Tasks(default_ttl_ms=...)])` —
opt in at construction. The extension advertises `io.modelcontextprotocol/tasks`
and serves `tasks/get` and `tasks/cancel`.
- `mcp.server.tasks.Tasks.intercept_tool_call` — the server DECIDES augmentation;
the legacy `params.task` field is ignored. It augments only for a client that
declared the extension on the request, returning a flat `CreateTaskResult`
(`resultType: "task"`).
- `client.py` `Client(target, extensions={EXTENSION_ID: {}})` — declaring the
extension is what lets the server defer; `main` then reads the `CreateTaskResult`
and polls `tasks/get`, whose completed `DetailedTask` inlines the original
`CallToolResult`.

## Scope

This is the SEP-2663 conformant *core*. The tool runs to completion inline (so a
task is observed as `completed` immediately), and the store is in-memory. Deferred
to follow-ups, each needing deeper SDK plumbing: `tasks/update` + the MRTR
`input_required` loop, `ToolExecution.taskSupport` gating with the `-32021`
required-task error, `notifications/tasks`, and SEP-2243 task routing headers.

## Spec

[SEP-2663 — Tasks extension](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/seps/2663-tasks-extension.md)
[SEP-2663 — Tasks extension](https://modelcontextprotocol.io/seps/2663-tasks-extension.md)
· [SEP-2133 — extensions capability](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/2133)

## See also

`apps/` (the additive half of the extension API).
`apps/` (the additive half of the extension API),
`custom_methods/` (a non-spec method without an extension).
Empty file.
54 changes: 54 additions & 0 deletions examples/stories/tasks/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"""Declare the tasks extension, let the server defer a tool call, then poll tasks/get.

The client declares `io.modelcontextprotocol/tasks` (via `Client(extensions=...)`),
so the server is free to answer `tools/call` with a `CreateTaskResult`. `Client`
exposes only spec verbs, so the augmented call and `tasks/get` drop to
`client.session`; the thin `_send` helper keeps that out of the story below.
"""

from typing import Any, Literal, cast

import mcp_types as types
from pydantic import TypeAdapter

from mcp.client import Client, ClientSession
from mcp.server.tasks import EXTENSION_ID, GetTaskRequestParams
from stories._harness import Target, run_client

_RAW: TypeAdapter[dict[str, Any]] = TypeAdapter(dict)


class _GetTaskRequest(types.Request[GetTaskRequestParams, Literal["tasks/get"]]):
method: Literal["tasks/get"] = "tasks/get"
params: GetTaskRequestParams


async def _send(session: ClientSession, request: types.Request[Any, Any]) -> dict[str, Any]:
"""Send a request whose result has a non-spec (extension) shape; return the raw dict."""
return await session.send_request(cast("types.ClientRequest", request), cast("Any", _RAW))


async def main(target: Target, *, mode: str = "auto") -> None:
async with Client(target, mode=mode, extensions={EXTENSION_ID: {}}) as client:
# The extension is a modern-only capability negotiated over server/discover.
# A legacy connection (today's stdio) cannot carry it, and the server then
# must not augment, so the task flow only runs once it is negotiated.
if client.server_capabilities.extensions is None:
return
assert client.server_capabilities.extensions == {EXTENSION_ID: {}}

# The server augments this tools/call into a task because we declared the extension.
call = types.CallToolRequest(
params=types.CallToolRequestParams(name="render_report", arguments={"title": "Q3", "sections": 2})
)
created = await _send(client.session, call)
assert created["resultType"] == "task", created
task_id = created["taskId"]

task = await _send(client.session, _GetTaskRequest(params=GetTaskRequestParams(task_id=task_id)))
assert task["status"] == "completed", task
assert task["result"]["content"][0]["text"].startswith("# Q3"), task


if __name__ == "__main__":
run_client(main)
27 changes: 27 additions & 0 deletions examples/stories/tasks/server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"""Tasks (SEP-2663): the server defers a tool call as a task the client polls.

`Tasks` is an opt-in `Extension`. The server decides, per request, to return a
`CreateTaskResult` instead of a `CallToolResult` for a client that declared the
`io.modelcontextprotocol/tasks` extension; the client then polls `tasks/get` for
status and the eventual result. `render_report` is the kind of slower, multi-step
tool a caller would rather run as a task than block on.
"""

from mcp.server.mcpserver import MCPServer
from mcp.server.tasks import Tasks
from stories._hosting import run_server_from_args


def build_server() -> MCPServer:
mcp = MCPServer("tasks-example", extensions=[Tasks(default_ttl_ms=60_000)])

@mcp.tool(description="Render a multi-section report for the given title.", structured_output=False)
def render_report(title: str, sections: int) -> str:
body = "\n".join(f"## Section {n}\n(generated)" for n in range(1, sections + 1))
return f"# {title}\n\n{body}"

return mcp


if __name__ == "__main__":
run_server_from_args(build_server)
Loading
Loading