| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
There was a problem hiding this comment.
4 issues found across 9 files
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
Sorry, something went wrong.
There was a problem hiding this comment.
Additional findings (outside current diff — PR may have been updated during review):
🟡 examples/stories/README.md:155-160 — The stories index still lists apps under "— deferred (README only) —" as "not yet implemented — #2896", but this PR adds a fully runnable apps story (client.py/server.py), adds [story.apps] to manifest.toml, and removes apps from the manifest's [deferred] table. The apps row should move into the feature-stories table, the same fix already applied for tasks in 29095ed.
Extended reasoning...What the inconsistency is
This PR ships a complete, runnable apps story: examples/stories/apps/client.py, server.py, and __init__.py are added, apps/README.md is rewritten with "Run it" instructions, [story.apps] is added to examples/stories/manifest.toml, and apps is removed from the manifest's [deferred] table. Yet the stories index (examples/stories/README.md) still lists apps in the "— deferred (README only) —" section as not yet implemented — #2896 (line 158).
Why it's squarely in this PR's touched code
The PR edits exactly this table: the tasks row was moved from the deferred section into the feature-stories table (in 29095ed, addressing the cubic bot's P3 finding about the same class of inconsistency for tasks). The apps row was simply missed in that fix, even though this PR's diff is what makes the apps story runnable.
Why nothing else catches it
The deferred section header explicitly describes its entries as "README-only placeholders; no client.py, not expanded into legs" — which is now false for apps, since apps/client.py exists and is run by CI. The manifest consistency test (test_manifest_matches_filesystem) checks the manifest's [deferred] keys against README-only directories — and the manifest itself was correctly updated — but no test enforces the markdown table in the index README, so the stale row survives silently.
Step-by-step proof
Impact and fix
Documentation-only: readers are pointed to a closed/obsolete deferral and may not discover the runnable example. The fix is a one-line move: relocate the apps row from the deferred section into the feature-stories table (next to tasks), updating its description to reflect the runnable story (e.g. io.modelcontextprotocol/ui extension: `ui://` resource + `_meta.ui.resourceUri`, graceful degradation) with status current.
🟡 src/mcp/server/apps.py:178-191 — The docstring says client_supports_apps returns True "only when the client advertised the extension AND listed the text/html;profile=mcp-app MIME type in its settings" (docs/migration.md repeats this), but the implementation also returns True when the declared extension settings carry no mimeTypes key at all (return mime_types is None or APP_MIME_TYPE in mime_types). Either document the lenient absent-mimeTypes behaviour (here and in migration.md) or require an explicit listing — and add a test pinning the absent-mimeTypes case either way.
Extended reasoning...client_supports_apps (src/mcp/server/apps.py, lines 178–191) documents a strict contract: it "Returns True only when the client advertised the extension AND listed the text/html;profile=mcp-app MIME type in its settings". docs/migration.md repeats the same wording for the Apps reference extension ("checking the client advertised the text/html;profile=mcp-app MIME type"). The implementation, however, is lenient about a missing mimeTypes key:
settings = extensions.get(EXTENSION_ID) if extensions else None
if settings is None:
return False
mime_types = settings.get("mimeTypes")
return mime_types is None or APP_MIME_TYPE in mime_typesA client that advertises {"io.modelcontextprotocol/ui": {}} — declaring the extension but listing no MIME types — is treated as Apps-capable, which contradicts the documented "AND listed the … MIME type" condition.
The tests only pin the two ends of the spectrum: test_apps_tool_returns_rich_output_when_client_negotiated_apps covers a client that explicitly lists APP_MIME_TYPE (returns True), and test_client_supports_apps_false_when_mime_type_not_offered covers a client that lists a different MIME type (returns False). The absent-mimeTypes branch (mime_types is None) is exercised by no test, so the lenient behaviour is unpinned and could drift either way without any test failing.
This is a contract/documentation inconsistency rather than a crash: clients that follow the documented pattern (always listing mimeTypes) behave as described. But client_supports_apps is a public helper that drives SEP-2133 graceful degradation, and the lenient reading means a UI-only result can be served to a host that never said it can render text/html;profile=mcp-app. Whether the leniency is intentional (treat an unenumerated declaration as supporting the default profile) or not, the docstring/migration text and the behaviour currently disagree.
Pick one and pin it:
Either way, add a test for the absent-mimeTypes case (a client advertising {EXTENSION_ID: {}}) so the chosen behaviour is locked in.
Sorry, something went wrong.
There was a problem hiding this comment.
3 issues found across 10 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="src/mcp/client/session.py">
<violation number="1" location="src/mcp/client/session.py:832">
P1: Task-augmented `tools/call` responses still fail pre-adapter validation. Skip or widen `validate_server_result` for declared tasks-extension `tools/call` responses, otherwise transparent task calls never reach `CreateTaskResult` parsing.</violation>
</file>
<file name="src/mcp/client/_tasks.py">
<violation number="1" location="src/mcp/client/_tasks.py:105">
P2: The driver ignores the `CreateTaskResult.pollIntervalMs` hint for the first `tasks/get`. Servers that set an initial cadence can see an immediate extra poll and may rate-limit or reject it.</violation>
</file>
<file name="src/mcp/client/client.py">
<violation number="1" location="src/mcp/client/client.py:755">
P2: Task polling ignores `Client.call_tool`'s per-call `read_timeout_seconds`, so augmented calls can block indefinitely on `tasks/get` even when the caller set a timeout.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Fix all with cubic | Re-trigger cubic
Sorry, something went wrong.
| ), | ||
| ), | ||
| _CallToolResultAdapter, | ||
| self._call_tool_adapter, |
There was a problem hiding this comment.
P1: Task-augmented tools/call responses still fail pre-adapter validation. Skip or widen validate_server_result for declared tasks-extension tools/call responses, otherwise transparent task calls never reach CreateTaskResult parsing.
Prompt for AI agentsCheck if this issue is valid — if so, understand the root cause and fix it. At src/mcp/client/session.py, line 832:
<comment>Task-augmented `tools/call` responses still fail pre-adapter validation. Skip or widen `validate_server_result` for declared tasks-extension `tools/call` responses, otherwise transparent task calls never reach `CreateTaskResult` parsing.</comment>
<file context>
@@ -760,7 +829,7 @@ async def call_tool(
),
),
- _CallToolResultAdapter,
+ self._call_tool_adapter,
request_read_timeout_seconds=read_timeout_seconds,
progress_callback=progress_callback,
</file context>
Sorry, something went wrong.
There was a problem hiding this comment.
1 issue found across 2 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="examples/servers/everything-server/mcp_everything_server/server.py">
<violation number="1" location="examples/servers/everything-server/mcp_everything_server/server.py:615">
P2: `confirm_delete` deletes on any accepted elicitation response, ignoring the submitted `confirm` boolean. Treat accepted-but-false confirmation as keeping the file.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Fix all with cubic | Re-trigger cubic
Sorry, something went wrong.
There was a problem hiding this comment.
3 issues found across 10 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="src/mcp/client/session.py">
<violation number="1" location="src/mcp/client/session.py:832">
P1: Task-augmented `tools/call` responses still fail pre-adapter validation. Skip or widen `validate_server_result` for declared tasks-extension `tools/call` responses, otherwise transparent task calls never reach `CreateTaskResult` parsing.</violation>
</file>
<file name="src/mcp/client/client.py">
<violation number="1" location="src/mcp/client/client.py:755">
P2: Task polling ignores `Client.call_tool`'s per-call `read_timeout_seconds`, so augmented calls can block indefinitely on `tasks/get` even when the caller set a timeout.</violation>
</file>
<file name="examples/servers/everything-server/mcp_everything_server/server.py">
<violation number="1" location="examples/servers/everything-server/mcp_everything_server/server.py:615">
P2: `confirm_delete` deletes on any accepted elicitation response, ignoring the submitted `confirm` boolean. Treat accepted-but-false confirmation as keeping the file.</violation>
</file>
<file name="src/mcp/server/tasks.py">
<violation number="1" location="src/mcp/server/tasks.py:51">
P3: This docstring cites the wrong SEP for task routing headers. Use SEP-2663 here so the server runtime notes match the shared header table and conformance context.</violation>
</file>
<file name="tests/docs_src/test_tasks.py">
<violation number="1" location="tests/docs_src/test_tasks.py:62">
P0: `test_a_legacy_connection_never_sees_tasks`: `server_capabilities.extensions is None` is wrong on legacy — the server returns extensions in its initialize result regardless of wire version.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Fix all with cubic | Re-trigger cubic
Sorry, something went wrong.
| async def test_a_legacy_connection_never_sees_tasks() -> None: | ||
| """tutorial001 + the degradation paragraph: a legacy handshake cannot carry the | ||
| capability, so the same declaring client gets plain results on that wire.""" | ||
| async with Client(tutorial001.mcp, mode="legacy", extensions={EXTENSION_ID: {}}) as client: |
There was a problem hiding this comment.
P0: test_a_legacy_connection_never_sees_tasks: server_capabilities.extensions is None is wrong on legacy — the server returns extensions in its initialize result regardless of wire version.
Prompt for AI agentsCheck if this issue is valid — if so, understand the root cause and fix it. At tests/docs_src/test_tasks.py, line 62:
<comment>`test_a_legacy_connection_never_sees_tasks`: `server_capabilities.extensions is None` is wrong on legacy — the server returns extensions in its initialize result regardless of wire version.</comment>
<file context>
@@ -0,0 +1,163 @@
+async def test_a_legacy_connection_never_sees_tasks() -> None:
+ """tutorial001 + the degradation paragraph: a legacy handshake cannot carry the
+ capability, so the same declaring client gets plain results on that wire."""
+ async with Client(tutorial001.mcp, mode="legacy", extensions={EXTENSION_ID: {}}) as client:
+ assert client.server_capabilities.extensions is None
+ result = await client.call_tool("bake", {"flavor": "rye"})
</file context>
Sorry, something went wrong.
There was a problem hiding this comment.
1 issue found across 7 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="src/mcp/client/session.py">
<violation number="1" location="src/mcp/client/session.py:832">
P1: Task-augmented `tools/call` responses still fail pre-adapter validation. Skip or widen `validate_server_result` for declared tasks-extension `tools/call` responses, otherwise transparent task calls never reach `CreateTaskResult` parsing.</violation>
</file>
<file name="tests/docs_src/test_tasks.py">
<violation number="1" location="tests/docs_src/test_tasks.py:62">
P0: `test_a_legacy_connection_never_sees_tasks`: `server_capabilities.extensions is None` is wrong on legacy — the server returns extensions in its initialize result regardless of wire version.</violation>
</file>
<file name="src/mcp/client/_tasks.py">
<violation number="1" location="src/mcp/client/_tasks.py:122">
P2: Negative `pollIntervalMs` is clamped to 0 seconds, which can create a tight repoll loop against `tasks/get` for misbehaving servers instead of backing off to a safe default.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Fix all with cubic | Re-trigger cubic
Sorry, something went wrong.
📚 Documentation preview
|
Sorry, something went wrong.
There was a problem hiding this comment.
2 issues found across 13 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="src/mcp/client/session.py">
<violation number="1" location="src/mcp/client/session.py:832">
P1: Task-augmented `tools/call` responses still fail pre-adapter validation. Skip or widen `validate_server_result` for declared tasks-extension `tools/call` responses, otherwise transparent task calls never reach `CreateTaskResult` parsing.</violation>
</file>
<file name="tests/docs_src/test_tasks.py">
<violation number="1" location="tests/docs_src/test_tasks.py:62">
P0: `test_a_legacy_connection_never_sees_tasks`: `server_capabilities.extensions is None` is wrong on legacy — the server returns extensions in its initialize result regardless of wire version.</violation>
</file>
<file name="docs/advanced/tasks.md">
<violation number="1" location="docs/advanced/tasks.md:128">
P2: This overstates `update_task`: the SDK currently treats `tasks/update` as a no-op and does not expose `inputRequests`, so users following this doc cannot actually answer in-task input through this surface.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Fix all with cubic | Re-trigger cubic
Sorry, something went wrong.
| * `update_task` answers a task's in-task `inputRequests`, and `cancel_task` asks | ||
| the server to stop one. Both hide the empty acknowledgement and return `None`. | ||
| Cancellation is cooperative in SEP-2663 — it may never take effect, and in this | ||
| SDK the work has always finished already — so follow with `get_task` for the | ||
| status that actually resulted. |
There was a problem hiding this comment.
P2: This overstates update_task: the SDK currently treats tasks/update as a no-op and does not expose inputRequests, so users following this doc cannot actually answer in-task input through this surface.
Prompt for AI agentsCheck if this issue is valid — if so, understand the root cause and fix it. At docs/advanced/tasks.md, line 128:
<comment>This overstates `update_task`: the SDK currently treats `tasks/update` as a no-op and does not expose `inputRequests`, so users following this doc cannot actually answer in-task input through this surface.</comment>
<file context>
@@ -105,20 +106,30 @@ Background execution is on the roadmap (below).
+ bare task id: task ids are bearer capabilities (above), so a client that
+ reconnected, or restarted with nothing but the persisted id, can resume a task
+ it no longer holds the `CreateTaskResult` for.
+* `update_task` answers a task's in-task `inputRequests`, and `cancel_task` asks
+ the server to stop one. Both hide the empty acknowledgement and return `None`.
+ Cancellation is cooperative in SEP-2663 — it may never take effect, and in this
</file context>
| * `update_task` answers a task's in-task `inputRequests`, and `cancel_task` asks | |
| the server to stop one. Both hide the empty acknowledgement and return `None`. | |
| Cancellation is cooperative in SEP-2663 — it may never take effect, and in this | |
| SDK the work has always finished already — so follow with `get_task` for the | |
| status that actually resulted. | |
| * `update_task` sends an `inputResponses` payload for servers that implement | |
| in-task input; this SDK's `Tasks` server currently has no outstanding | |
| `inputRequests`, so its handler acknowledges the update as a no-op. | |
| `cancel_task` asks the server to stop one. Both hide the empty acknowledgement | |
| and return `None`. Cancellation is cooperative in SEP-2663 — it may never take | |
| effect, and in this SDK the work has always finished already — so follow with | |
| `get_task` for the status that actually resulted. |
Sorry, something went wrong.
There was a problem hiding this comment.
The two doc spots I'd flagged as still stale (the tasks story README's deferred list and the migration guide's "expected to return" sentence) are now addressed, and this pass found no new issues — but this PR adds a substantial new public surface (the Tasks server/client extensions, mcp.shared.tasks wire models, new mcp exports, and transparent task polling in Client.call_tool), so it should get a human's final review rather than a shadow approval.
Extended reasoning...This PR ships the SEP-2663 Tasks extension core: a new server extension (mcp.server.tasks.Tasks with a pluggable TaskStore), the client half (TasksExtension, typed mcp.client.tasks functions, new Task*Error exceptions exported from mcp), SEP-2663 wire models in mcp.shared.tasks, Mcp-Name routing-header rows in mcp.shared.inbound, everything-server conformance fixtures, a runnable story, docs (docs/advanced/tasks.md, migration notes), and ~1,600 lines of new tests.
Task ids are entropy-bearing bearer capabilities (secrets.token_urlsafe(16)), which is deliberate per the SEP but means any holder of an id can poll another connection's task result — the docs call this out and offer a custom-store seam for stricter scoping. tasks/get responses are deep-copied to avoid aliasing the stored record, and capability/version gating (-32021 vs -32601) looks consistent with the rest of the SDK. No auth/crypto code is touched, but the retention-of-results-by-bearer-id model is a design point a maintainer should consciously accept.
High. This is new public API surface across mcp/__init__.py, the client, the server, and the shared wire layer, plus protocol-conformance behavior (conformance expected-failures baseline was rewritten). It is well outside the "simple, mechanical, obvious" bar for shadow approval regardless of test coverage.
The PR has already been through several substantive human/bot review rounds and every earlier finding — including my own doc-staleness comments — appears addressed in the current revision; the latest bug-hunting pass found nothing new. Coverage claims (100% on the new modules, conformance scenarios passing) are plausible from the test files but I did not re-run CI. Given the active human review already underway, this deferral is mainly a signal that the automated review is now clean.
Sorry, something went wrong.
| class GetTaskResult(Task): | ||
| """SEP-2663 `tasks/get` response: the task snapshot with its outcome inlined. | ||
|
|
||
| A lenient client-side parse model: `result` is set when the task `completed` | ||
| (even a tool result with `isError: true`), `error` (the JSON-RPC error dict) | ||
| when it `failed`, and both stay `None` for non-terminal statuses. | ||
| """ | ||
|
|
||
| result_type: str = "complete" | ||
| result: dict[str, Any] | None = None | ||
| error: dict[str, Any] | None = None |
There was a problem hiding this comment.
🟡 GetTaskResult drops the SEP-2663 inputRequests map (its config chain never sets extra='allow', so pydantic's default extra='ignore' silently discards it), yet TaskInputRequiredError's message and docs/advanced/tasks.md tell users to fetch the snapshot with get_task and answer its inputRequests via update_task — a caller following that guidance gets a snapshot with no way to see which input requests are pending. Add input_requests: dict[str, Any] | None = None (alias inputRequests) to GetTaskResult, or soften the guidance to not point at a field the model doesn't carry.
Extended reasoning...What the bug is. GetTaskResult (src/mcp/shared/tasks.py:135-145) defines only result_type/result/error on top of Task, and its config chain (Task → _CarriesTtlMs/_TasksModel → BaseModel, where _TasksModel sets only alias_generator=to_camel, populate_by_name=True) never sets extra='allow'. Pydantic's default is extra='ignore', so when a third-party SEP-2663 server includes an inputRequests map on a tasks/get response for a task in status input_required, the field is silently discarded — it is not even retained in model_extra.\n\nWhy this matters: the SDK's own recovery guidance depends on the field. TaskInputRequiredError (src/mcp/client/tasks.py:106-108) tells users to "fetch the snapshot with mcp.client.tasks.get_task and answer with mcp.client.tasks.update_task", update_task's docstring keys input_responses off "the snapshot's inputRequests keys" (src/mcp/client/tasks.py:266, 274), and docs/advanced/tasks.md:128 says "update_task answers a task's in-task inputRequests". A caller following that path receives a GetTaskResult with no way to see which input-request keys are pending or what each request asks, so the documented get_task → update_task manual loop is not actually drivable through the typed API — they must fall back to a hand-rolled send_request with a raw dict result model.\n\nThe code path that triggers it. The SDK's own Tasks server never emits input_required (tasks are born terminal), but Client.call_tool / wait_task transparently poll any SEP-2663 server, and input_required is a normal spec status there. When a poll hits it, run_task_driver raises TaskInputRequiredError — whose message is the exact instruction that then cannot be completed.\n\nStep-by-step proof. (1) A user connects Client(url, extensions=[TasksExtension()]) to a third-party SEP-2663 server and calls a slow tool; the server augments it into a task. (2) Mid-execution the server parks the task at status: "input_required" with, say, "inputRequests": {"confirm": {"method": "elicitation/create", "params": {...}}} in its tasks/get response. (3) The transparent driver raises TaskInputRequiredError('task_…'), telling the user to call get_task and answer its inputRequests with update_task. (4) The user calls snapshot = await get_task(session, task_id) — pydantic validates the response against GetTaskResult, and because the model has no input_requests field and extra='ignore' applies, the inputRequests key is dropped. (5) snapshot exposes only status/result/error/timestamps; there is no attribute (nor model_extra entry) revealing the pending keys or payloads, so the user cannot construct the input_responses dict update_task requires.\n\nWhy nothing else prevents it. The earlier cubic inline comment (fe1b2f2b) raised exactly this and was marked "Addressed in cade8f1", but cade8f1 only added the typed mcp.client.tasks functions — it did not touch src/mcp/shared/tasks.py or add an input_requests field, so the gap is still live at HEAD and the "addressed" marker is inaccurate. Nothing crashes and the SDK's own end-to-end flows are unaffected (its server never parks at input_required, and the automatic in-task input loop is an explicitly documented deferred follow-up), which is why this ships silently.\n\nHow to fix. Add input_requests: dict[str, Any] | None = None (camelCase alias inputRequests via the existing _TasksModel alias generator) to GetTaskResult, so the snapshot preserves the map and the documented manual loop becomes drivable. Alternatively, soften TaskInputRequiredError's message and the docs to stop pointing at get_task → update_task until the field (or the automatic loop) lands.
🔬 also observed by cubic-dev-ai
Sorry, something went wrong.
|
I built a Postgres-backed durable task store implementing the SEP-2663 spec — persistent backend, idempotency, exactly-once crash recovery, TTL cleanup. The repo is at mcp-durable-tasks Full implementation: 68 tests with real SIGKILL fault injection at commit boundaries, comparative correctness benchmarks (naive approach double-charges 100% of the time; ours 0 at every concurrency level), p50/p95/p99 latency measured, DESIGN.md decision log. Ready to use as a standalone library or contribute upstream once this PR lands. |
Sorry, something went wrong.
Sorry, something went wrong.
Implement io.modelcontextprotocol/tasks per SEP-2663 (Final), wire-incompatible with the 2025-11-25 in-core design still carried (types-only) in mcp_types, so the extension defines its own SEP-2663-shaped models: - The server decides task augmentation per request; the legacy params.task field is ignored. Only a client that declared the extension on a modern (2026-07-28) connection is augmented - a legacy handshake cannot carry the capability, so it is never augmented. - A task-augmented tools/call returns a flat CreateTaskResult (resultType: "task", taskId/status/createdAt/lastUpdatedAt/ttlMs). - tasks/get returns a DetailedTask (resultType: "complete"); a completed task inlines the original CallToolResult. isError: true is a completed task (failed is reserved for JSON-RPC errors). - tasks/cancel is an empty ack. tasks/result is not registered, so it returns -32601. A tasks/* call from a non-declaring client returns -32003 with a requiredCapabilities payload. Task ids are entropy-bearing. Ships a runnable tasks story (server-decided augmentation + tasks/get polling) and a migration note. Deferred to follow-ups (each needs deeper SDK plumbing): tasks/update + the MRTR input_required loop, ToolExecution.taskSupport gating with -32021, notifications/tasks, and SEP-2243 task routing headers.
A task record is now created only when the tool call has produced its outcome, and the store is a pluggable async protocol: - An input_required interim passes through un-augmented; only the leg that completes the call becomes a task, so a multi round-trip exchange yields exactly one task. An error (a raised MCPError, or ErrorData returned by a nested interceptor) propagates as the JSON-RPC error the client is waiting on instead of stranding a half-created task. - tasks/cancel acknowledges without rewriting terminal status: the tool has always finished by the time a tasks/* request can arrive, and SEP-2663's cooperative cancellation permits an ack without effect. - The missing-capability error uses mcp_types' -32021 constant via require_client_extension, replacing the stale -32003 numeral SEP-2663's prose still carries from before the error-code renumber. - tasks/update is served as the SEP-required acknowledgement; unknown inputResponses keys are ignored, absent inputResponses is rejected. - tasks/* bindings are version-scoped to the modern wire, so legacy clients get METHOD_NOT_FOUND instead of a capability error they could never satisfy on that wire. - The default clock is a real UTC wallclock; clocks return datetimes and wire formatting lives in one place. - TaskStore is an async protocol (Tasks(store=...)) with an in-memory default that enforces ttlMs (expired records drop on access and are swept on insert); stored results are copied at both boundaries; ttlMs stays on the wire when null (required-but-nullable in the extension schema); non-positive default_ttl_ms is rejected at construction.
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).
…sk lifecycle - The wire models (Task, CreateTaskResult, request params, typed request wrappers, and a lenient GetTaskResult parser) move to mcp.shared.tasks so client code can import them without reaching into the server tier; mcp.server.tasks re-exports the public names. - Tasks(augment=...) scopes augmentation per request (SEP-2663: the server decides at its own discretion); None keeps augment-everything. - A JSON-RPC error under augmentation now records a task born "failed" (error inlined on tasks/get with no result key, statusMessage carrying the diagnostic) and returns CreateTaskResult(status="failed"). Errors on every non-augmented path propagate unchanged. - require_client_extension moves to mcp.server.extension so the extension tier no longer reaches into the composition tier; mcp.server.mcpserver keeps re-exporting it.
…ders) SEP-2663 mandates Mcp-Name: <taskId> on tasks/get, tasks/update, and tasks/cancel Streamable HTTP POSTs. Adding the three methods to NAME_BEARING_METHODS covers both ends at once: the client's modern header stamp emits the header automatically and the server's validation ladder rejects a mismatched or absent value. On a server without the tasks extension a header-bearing tasks/* request still reaches -32601 method dispatch, while a headerless one fails the header rung first, consistent with the SEP-2243 header rules; a test pins the ordering.
TasksExtension contributes a ResultClaim for resultType 'task': a declaring Client admits CreateTaskResult on tools/call and resolves it by polling tasks/get (honoring pollIntervalMs with a 1s fallback and the caller's per-request read timeout) until terminal, returning the inlined CallToolResult; failed, cancelled, and input_required tasks surface as typed TaskFailedError, TaskCancelledError, and TaskInputRequiredError. Manual driving stays available via session.call_tool(..., allow_claimed=True) and the mcp.shared.tasks wrappers. The tasks story's modern path is the plain typed call_tool, with a compact manual leg over the shared wrappers.
The everything-server now opts into Tasks with an allowlist augment predicate, so every pre-existing tool stays synchronous while the new task fixture tools (slow_compute, failing_job, protocol_error_job, confirm_delete, multi_input, test_tool_with_task) exercise augmentation, failed-task recording, the required-capability error, and the MRTR-then-task composition. Eight of the nine baselined tasks-* expected failures now pass and are removed; tasks-mrtr-input stays baselined with an accurate rationale (tasks are born terminal -- the in-task input_required/tasks/update resume loop is a documented follow-up). Verified locally against the pinned harness: all four server legs green with zero unexpected failures and zero stale baselines.
Adds docs/advanced/tasks.md (sibling of the MCP Apps page) covering the server opt-in with the augment/default_ttl_ms/clock knobs, the pluggable TaskStore protocol and its per-process caveat, the inline execution model (tasks born terminal; acks without effect), transparent client polling with the typed task errors, manual driving over the mcp.shared.tasks wrappers, and the per-wire error-code table. Three compiled docs_src tutorials back the page, each behaviorally tested; the page is registered in the mkdocs nav (which also feeds llms.txt). The migration note and module docstring no longer list routing headers as deferred -- Mcp-Name stamping and validation landed with the shared header table.
Mcp-Name stamping and validation for tasks/* landed with the shared header table; the module docstring's deferred list now matches the migration note and docs page.
- 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.
- Client.call_tool's per-call read_timeout_seconds now bounds each tasks/get poll as well as the initial call (per-request bound, not a whole-loop deadline); pinned by a recording test. - The everything-server's confirm_delete keeps the file when an accepted elicitation answers confirm: false (conformance legs re-verified). - The routing-header parentheticals attribute the requirement to SEP-2663 and the header family to SEP-2243.
The private driver module becomes the public mcp.client.tasks: get_task, wait_task, update_task, and cancel_task are typed free functions over ClientSession, so manually driving a task no longer means hand-building wire requests. wait_task accepts a bare task id (the persist-and-resume shape -- task ids are bearer capabilities that survive reconnects) or the CreateTaskResult, which additionally seeds the poll-interval fallback; the polling loop now exists once, shared with the TasksExtension claim resolver. update_task and cancel_task hide the empty acknowledgement and return None. The task errors gain a common TaskError base (one except arm for any non-completion) and pickle support, since they are public API. The manual-driving docs, story leg, and tutorial are rewritten onto the functions.
…ucture The restructured migration guide dropped the server extensions section this branch used to edit, so the feature prose now lives in the extensions and tasks pages. The migration guide's experimental Tasks section and the whats-new removal list now say the extension ships as the built-in Tasks extension instead of claiming it is unimplemented. The extensions page also points at TasksExtension as the shipped behavioural example next to advertise().
When a client abandons an augmented tools/call while the tool body is still running, the cancellation propagates through the Tasks interceptor uncaught, so no task record is written to the store. Pin that, and that the same connection's later augmented calls still complete normally.
…berate Reuse the shared _tasks_server fixture instead of hand-building an identical server, and adopt the suite's task-group idiom for abandoning the in-flight call. Record in the interceptor comment that catching only MCPError is deliberate: cancellation must propagate so an abandoned call leaves no task record.
…26-07-28 line The 15-commit branch rebased onto latest main with four textual conflicts (extension.py + mcpserver/server.py, the everything-server, client/__init__.py, whats-new.md), all resolved to main's structure plus the branch's additions. This commit carries the semantic adaptations that the merge could not surface: - Gate task augmentation on the declared-capabilities fact, not clientInfo. Main made clientInfo optional on the 2026-07-28 wire (spec #3002) and split `session.client_capabilities` from `session.client_params`, so a conformant client can declare the extension while `client_params` is None. The interceptor's `_client_declared_tasks` now reads `client_capabilities`, matching main's rewritten `require_client_extension` (whose main body the relocated function carries). A pair-only declaring envelope is augmented. - Interceptors now run at the handler layer (`compose_tool_call_handler`), so `call_next` returns the wrapped handler's domain result rather than a serialized dict, and every modern-era result envelope carries the `serverInfo` `_meta` identity stamp. The stored/inlined tool result stays the un-enveloped payload. Reword `_wire_payload` and the error-fold comment accordingly, and have the tests assert-and-strip the envelope stamp through strict helpers mirroring tests/_stamp.py instead of hard-coding it into every snapshot. A short-circuited `input_required` interim is now sieved to the core shape like any handler result, so its fixture is core-shaped. - Declare `name_param = "taskId"` on GetTaskRequest/CancelTaskRequest/ UpdateTaskRequest: main added `Request.name_param` as the per-request key a client mirrors into the `Mcp-Name` header, built expressly for the tasks verbs (its placeholder test models `tasks/get`). The server-side NAME_BEARING_METHODS rows remain the source of the spec-mandated validation. - Follow main's spelling and toolchain moves: httpx -> httpx2 in the tasks server tests, `mcp.types` (not `mcp_types`) in docs/example imports with the matching `hl_lines` shift, and the GitHub form for SEP-2663 links. - Refresh prose that the merge left stale: the migration guide's "Detached work" note now points at the extension for the fetch-later half, and the tasks story's docstring states the fixed-contract invariant rather than claiming identity with a task-less server's stamped result.
|
@maxisbey any updates on this? i really need this for our app that is going live soon 🙏 thank you |
Sorry, something went wrong.
| Back | FazBrowse Home | New Git URL |
Summary
The SEP-2663 Tasks extension (io.modelcontextprotocol/tasks) — the conformant core. Built on the extension API from #3003 (this PR is based on that branch; merge after it).
SEP-2663 (Final) is wire-incompatible with the 2025-11-25 in-core Tasks design still carried (types-only) in mcp_types, so the extension defines its own SEP-2663-shaped models.
What's implemented (conformant core)
Ships a runnable tasks story and a migration note.
Deferred to follow-ups
Each needs deeper SDK plumbing and is called out in the module/README/migration:
These map to the remaining conformance tasks-* scenarios; the core targets tasks-dispatch-and-envelope, tasks-capability-negotiation, tasks-wire-fields, tasks-lifecycle (partial), tasks-request-state-removal.
Testing
14 spec-derived in-memory Client(server) tests, 100% coverage of tasks.py, strict-no-cover clean, pyright + ruff + markdownlint green, both story legs (in-memory + http-asgi) pass.
AI Disclaimer
This PR was developed with the assistance of either Claude or Codex. I've reviewed and verified the changes.