Skip to content

Commit cf7fd46

Browse files
committed
Polish the tasks surface from final review
- The polling driver floors a server-supplied negative pollIntervalMs to zero instead of crashing (trio) or busy-looping (asyncio); tested. - Client.call_tool documents TaskInputRequiredError alongside the other task errors, and the docs and story prose catch up: routing headers are no longer listed as deferred, failed tasks are named alongside completed ones in the store wording, and the migration guide's old removal note now points at the landed extension. - The tasks story imports EXTENSION_ID from mcp.shared.tasks.
1 parent 2d95f23 commit cf7fd46

7 files changed

Lines changed: 39 additions & 10 deletions

File tree

docs/advanced/tasks.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ then come back.
2727
`pollIntervalMs` hint, one second between polls in its absence — and surfaces
2828
only the final `CallToolResult`. A `failed` task raises the typed
2929
`TaskFailedError` carrying the inlined JSON-RPC error; a `cancelled` one raises
30-
`TaskCancelledError`.
30+
`TaskCancelledError`; an `input_required` one raises `TaskInputRequiredError`
31+
the automatic in-task input loop is not implemented yet, so drive that task
32+
manually (below).
3133

3234
Degradation is built in. A modern client that does not declare the extension is
3335
never augmented: it keeps getting plain `CallToolResult`s. And a legacy

docs/migration.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1568,7 +1568,7 @@ Behavior changes:
15681568

15691569
Tasks ([SEP-1686](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/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 the `experimental` properties on `ClientSession`, `ServerSession`, `Server`, and `ServerRequestContext`. The corresponding `Task*` types remain in `mcp_types` as types-only definitions, except the `TaskExecutionMode` alias, whose literal is now inlined on `ToolExecution.task_support`.
15701570

1571-
The 2026-07-28 revision reintroduces Tasks as an official extension: [SEP-2663](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2663), `io.modelcontextprotocol/tasks`, redesigned around polling (`tasks/get`) instead of a blocking `tasks/result`. This SDK does not implement the extension yet.
1571+
Tasks have since returned as the built-in `Tasks` extension ([SEP-2663](https://modelcontextprotocol.io/seps/2663-tasks-extension.md)), with a different wire shape than the experimental SEP-1686 surface — see [Server extensions API](#server-extensions-api-sep-2133) above and [Tasks](advanced/tasks.md).
15721572

15731573
## Transports
15741574

examples/stories/tasks/README.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,12 @@ uv run python -m stories.tasks.client --http
4747

4848
This is the core SEP-2663 surface. The tool runs to completion inline, so a task
4949
is recorded directly as `completed` (the SEP allows any initial status), and
50-
completed tasks live in a pluggable `TaskStore` (`Tasks(store=...)`, in-memory
51-
default) that enforces `default_ttl_ms`. Deferred to follow-ups, each needing
52-
deeper SDK plumbing: background execution (returning `working` tasks), the
53-
in-task `input_required`/`inputResponses` loop over `tasks/update`,
54-
`notifications/tasks`, and SEP-2243 task routing headers.
50+
finished (completed or failed) tasks live in a pluggable `TaskStore`
51+
(`Tasks(store=...)`, in-memory default) that enforces `default_ttl_ms`. Deferred
52+
to follow-ups, each needing deeper SDK plumbing: background execution (returning
53+
`working` tasks), the in-task `input_required`/`inputResponses` loop over
54+
`tasks/update`, and `notifications/tasks` (the SEP-2243 `Mcp-Name` routing
55+
header is already handled by the shared header table).
5556

5657
## Spec
5758

examples/stories/tasks/client.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,7 @@
1515
import mcp_types as types
1616

1717
from mcp.client import Client, TasksExtension
18-
from mcp.server.tasks import EXTENSION_ID
19-
from mcp.shared.tasks import CreateTaskResult, GetTaskRequest, GetTaskRequestParams, GetTaskResult
18+
from mcp.shared.tasks import EXTENSION_ID, CreateTaskResult, GetTaskRequest, GetTaskRequestParams, GetTaskResult
2019
from stories._harness import Target, run_client
2120

2221

src/mcp/client/_tasks.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ async def run_task_driver(
127127
if snapshot.status == "input_required":
128128
raise TaskInputRequiredError(created.task_id)
129129
interval_ms = snapshot.poll_interval_ms if snapshot.poll_interval_ms is not None else created.poll_interval_ms
130-
await sleep(DEFAULT_POLL_INTERVAL_SECONDS if interval_ms is None else interval_ms / 1000)
130+
await sleep(DEFAULT_POLL_INTERVAL_SECONDS if interval_ms is None else max(0, interval_ms) / 1000)
131131

132132

133133
class TasksExtension(ClientExtension):

src/mcp/client/client.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -719,6 +719,15 @@ async def call_tool(
719719
MCPError: A callback returned `ErrorData` for an embedded input request.
720720
pydantic.ValidationError: The server returned a result that does not
721721
conform to the negotiated protocol version.
722+
TaskFailedError: The call was augmented into a task that `failed`
723+
(a JSON-RPC error during execution).
724+
TaskCancelledError: The call was augmented into a task that was
725+
cancelled before completing.
726+
TaskInputRequiredError: The call was augmented into a task that
727+
reached `input_required`; the SDK's automatic in-task input
728+
loop is not implemented yet — drive the task manually via
729+
`session.call_tool(..., allow_claimed=True)` and the
730+
`mcp.shared.tasks` wrappers.
722731
"""
723732

724733
async def retry(r: InputResponses | None, s: str | None) -> CallToolResult | InputRequiredResult | Result:

tests/client/test_tasks.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,24 @@ async def test_no_interval_anywhere_falls_back_to_one_second() -> None:
149149
assert slept == [DEFAULT_POLL_INTERVAL_SECONDS] == [1.0]
150150

151151

152+
async def test_negative_poll_interval_is_floored_to_zero() -> None:
153+
"""SDK-defined: a misbehaving server's negative interval must not crash or
154+
busy-loop the driver — it is floored to a zero-length sleep."""
155+
get_task, _ = _scripted_get_task(
156+
[
157+
_snapshot("working", poll_interval_ms=-5000),
158+
_snapshot("completed", result=dict(_COMPLETED_RESULT)),
159+
]
160+
)
161+
sleep, slept = _recording_sleep()
162+
163+
with anyio.fail_after(5):
164+
result = await run_task_driver(_created(), get_task=get_task, sleep=sleep)
165+
166+
assert result.content == [TextContent(text="done")]
167+
assert slept == [0.0]
168+
169+
152170
async def test_failed_snapshot_raises_task_failed_error_with_code_and_status_message() -> None:
153171
"""SEP-2663: a `failed` task inlines the JSON-RPC error and SHOULD carry a
154172
`statusMessage` diagnostic — both surface on the typed error."""

0 commit comments

Comments
 (0)