io.modelcontextprotocol/tasks extension. It supersedes the earlier “delete the task machinery” direction recorded during the SDK v2 migration. The Feature Program carries the one-line status; user-facing usage is documented at Background Tasks and Background Tasks (client).
TL;DR
Background tasks live on. The MCP spec moved them out of core and into a Final, merged extension —io.modelcontextprotocol/tasks (SEP-2663) — that keeps the polling model FastMCP already implements. No SDK, in any language, ships a runtime for it yet. FastMCP owns the only production-shaped execution engine (Docket/Redis) built for a near-identical protocol.
The plan: rebuild task support on SEP-2663 as fastmcp-tasks, an in-repo optional package, gated by task=True exactly as MCP Apps is gated by app=True. Remove the SEP-1686 wire layer; keep and re-home the execution engine. Along the way, introduce a FastMCP-native server extension API so tasks (and later Apps) plug in through one documented mechanism instead of bespoke surgery on core.
Net effect: a server that already uses @mcp.tool(task=True) needs no code change, and FastMCP plausibly becomes the first runtime implementation of the tasks extension anywhere.
Background: where tasks stand today
FastMCP 3 shipped background tasks against SEP-1686, the task protocol that briefly lived in the core MCP spec. The implementation is ~4,000 lines across server, client, CLI, and an SDK shim, split into two very different halves:- A wire layer — capability advertisement, the
tasks/get|result|list|cancelhandlers, aCreateTaskResulton augmentedtools/call, and a Redis-backed push relay that lets a worker reach a client to deliver notifications and elicitation requests. - An execution engine — Docket (queue, worker, result store, TTL,
memory://orredis://backends) plus FastMCP-built durability: auth-scoped compound keys that isolate task access by caller, request-context snapshot/restore across worker processes, argument-coercion parity with the sync path, and thefastmcp tasks workerCLI.
What changed upstream: SEP-2663
Tasks were reworked, not removed. SEP-2663 (“Tasks Extension”) is Final and was merged upstream on 2026-05-15, superseding SEP-1686. It defines theio.modelcontextprotocol/tasks extension, a capability-negotiated feature layered on the SEP-2133 extensions mechanism. It keeps SEP-1686’s polling core and tightens it.
The wire shape:
- Client advertises the tasks capability (per-request, in
_meta). This is consent — “I can handle a task result” — not a request to run one. - Client issues a normal
tools/call. The server decides whether to run it as a task. - If tasked, the server returns a
CreateTaskResult(a claimed result shape carryingresultType: "task") with a server-generatedtaskId. - Client polls
tasks/getuntil the status is terminal; the result is inlined into that response. - In-task input (elicit/sample/roots requested during execution) is poll-based: status flips to
input_required, outstanding requests appear in aninputRequestsmap, and the client answers viatasks/update. tasks/cancelis cooperative. Optional push exists (notifications/tasksoversubscriptions/listen) but servers need not send it.
Critically: no runtime exists. The
ext-tasks repo is schema + prose only. The TypeScript and Python SDKs carry the wire types and conformance fixtures — no client/server implementation. The field is open.
The decision
Build it. Two facts flip the earlier “delete and wait” call:- The spec is what FastMCP already implements, minus a push relay it can now shed. The rebuild is dominated by deletion and a thin new wire adapter, not a from-scratch effort.
- FastMCP is uniquely positioned. SEP-2663 assumes a durable server-side store, server-minted high-entropy ids, eventual-consistency-aware creation, and multi-node routing — precisely what Docket/Redis provides. No other framework has this built.
_sdk_patches.py shim, the TaskNotificationHandler, and a cluster of protocol-era xfails). Rebuilding on SEP-2663 clears that debt and produces a flagship v4 capability with a zero-code-change migration story.
Architecture
Engine and wire split
The existing code already separates cleanly along this line; the rebuild makes the boundary a package boundary.- Removed: the SEP-1686 wire layer — capability advertisement, the four CRUD handlers, and (the big win) the entire Redis push relay (
server/tasks/elicitation.py,notifications.py), which existed only because SEP-1686 had no poll-based in-task input channel. SEP-2663’sinput_required/tasks/updatereplaces it; the request/response store survives, the push envelope does not. - Kept and re-homed: the Docket execution engine, the auth-scoped key encoding (this is our authorization layer for
tasks/get/update/cancel— stronger than the spec’s “taskIds may be bearer tokens”), context snapshot/restore, argument coercion, and the worker CLI. All of it is wire-agnostic. - New: a thin SEP-2663 wire adapter — capability, the
tasks/get/update/cancelmethods, and atools/callinterceptor that decides-and-tasks.
Packaging
fastmcp-tasks becomes an in-repo uv workspace member on the fastmcp_remote template (own pyproject.toml, lockstep-versioned, re-exported through the fastmcp metapackage). The DX parallel with MCP Apps is exact:
Core keeps only the declaration:
task=True / TaskConfig is metadata on a component, with no engine import. Everything else — engine and wire adapter — lives in the fastmcp-tasks package. The existing [tasks] extra re-points from the SEP-1686 machinery to fastmcp-tasks, so pip install fastmcp[tasks] and task=True keep working with modern wire underneath.
Activation stays implicit-but-loud (the existing require_docket() pattern, not silent degradation): task=True anywhere triggers a lazy import of fastmcp-tasks at build time; a missing install raises immediately. A tool the author marked as a task silently running inline would be a correctness bug, not a graceful fallback.
The extension API
MCP extensions (SEP-2133) are a genuinely new abstraction in SDK v2 — they did not exist in v1. So MCP Apps hand-rolling its integration wasn’t a wrong choice; it predates the tool. Today FastMCP’s server bypasses the SDK’sExtension class entirely (it hand-splices the ui capability onto the low-level server and walks tool metadata directly), while the client forwards ClientExtension natively. Every new protocol extension currently means bespoke core surgery.
Tasks is the forcing function to fix that. The design adds a single registration point:
add_extension is required for task=True to work — it is not autodetected from the presence of task=True flags. This is deliberate. The extension needs configuration that has to live somewhere (backend URL, worker concurrency, TTL defaults), and add_extension(TasksExtension(...)) is its natural home; autodetection would only scatter that config into settings/env and hide the moment of enablement. Requiring it also keeps capability advertisement honest — the server advertises the tasks capability iff the extension is registered — and removes the worst footgun, a tool silently running on an in-memory backend in production because nobody configured Redis. The two concerns stay cleanly separated: task=True is per-component intent (“this tool can be a task”); add_extension is server-wide enablement and config (“this server runs tasks, here’s how”). Using task=True with no extension registered is a loud build-time error.
The extension API contributes a negotiated capability, additive request methods, and a tools/call interceptor — with access to FastMCP-level constructs the SDK’s Extension withholds (the component registry, Context, auth scope). It is designed against tasks because tasks exercises the full surface (capability + methods + interception + client claims + notifications), where Apps exercises only a subset. Apps migrates onto the extension API as a fast-follow, deleting the hand-rolled splices and confirming the design generalizes.
Extension vs. middleware — the discriminator, so we do not over-apply this: an extension is a negotiated contract change the client must understand; middleware is unilateral server behavior the client never sees. PII detection, auth, rate limiting → middleware. Tasks, Apps → extensions. Litmus test: delete the capability advertisement — if nothing about the client’s behavior changes, it was middleware.
Client experience
SEP-2663 removed the client-side “make this a task” flag — the server decides. That maps onto FastMCP’s existing two-tier client surface, the friendlycall_tool vs the low-level call_tool_mcp, so there is almost no new API:
call_tool(name, args)(friendly) — advertises the capability and, if the server tasks the call, transparently drives the poll loop and returns the finished result. Whether the server tasked it is invisible. The machinery already exists: the migration wired claim-resolution throughcall_tool_mcp’sallow_claimedpath, so a returnedCreateTaskResultis finished into an ordinaryCallToolResult. In-taskinput_requiredroutes through the client’s existing elicitation handler, answered viatasks/update— so background elicitation looks identical to foreground elicitation, with zero new client API.call_tool_mcp(...)(low-level) — hands back the rawCreateTaskResultclaimed shape for callers managing the task themselves.- A “return quickly” flag on the friendly interface yields the
Taskhandle (.status(),.wait(),.cancel(), awaitable) without blocking — the escape hatch for progress and cancellation.
TaskConfig modes translate directly: required → always task (-32003 for non-declaring clients), optional → task iff the client declared, forbidden → never.
Sequencing
- Design + unit-test the extension API against tasks’ full surface (capability, methods, interception, client claims/notifications) — as its own testable layer, proven in isolation with a trivial in-test extension before any tasks logic lands on it.
- Build
fastmcp-tasks— extract the engine from the removed SEP-1686 layer, write the SEP-2663 adapter, port the client half. - Migrate MCP Apps onto the extension API — fast-follow, off the critical path, with Apps’ existing green tests as the regression net.
Scope for v1 (non-goals)
- Polling only. The optional
notifications/taskspush andsubscriptions/listenintegration are deferred to a laterfastmcp-tasksversion. This lets the second Redis notification queue die rather than be ported. tools/callonly — do not lead the spec. SEP-2663 augmentstools/callonly. FastMCP 3 offeredtask=Trueon prompts and resources ahead of the SDK under SEP-1686, and that was a mistake: it produced wire-inexpressible capability, a permanent xfail cluster, and the sdk-feedback #3 gap. The rebuild does not repeat it —task=is a tools-only surface, and the generic prompt/resource task spine is dropped rather than carried. If the spec extends augmentation later, the surface grows with it.- Ship experimental. The
ext-tasksschema is labeled experimental with no releases;fastmcp-tasksships labeled experimental initially and revs on its own cadence when the schema moves.
Risks
Design decisions (resolved)
These were the open forks; the maintainer has settled them. Recorded here so the direction is unambiguous going into implementation.- Wire adapter location — in the
fastmcp-taskspackage. The engine and the SEP-2663 wire adapter live in the package; core carries only thetask=Truedeclaration. This isolates the experimental schema’s churn from core, at the cost of diverging from the Apps precedent (where theuiwire glue lives in core today — Apps will converge onto this model when it migrates to the extension API). - Extension API shape — a FastMCP-native
mcp.add_extension(), required to enable tasks. Chosen over a thin pass-through to the SDK’sMCPServer(extensions=...)because the FastMCP-native API can hand extensions theContext, component registry, and auth scope the SDK’sExtensionwithholds.add_extensionis required fortask=True(not autodetected) — it is the single home for backend config and the honest source of capability advertisement. See The extension API. - Client default — transparent completion on the friendly interface.
call_tooldrives the poll loop and returns the finished result;call_tool_mcpexposes the rawCreateTaskResult; a “return quickly” flag yields theTaskhandle. See Client experience. - Experimental labeling — yes.
fastmcp-tasksships labeled experimental for at least one minor cycle, tracking the experimentalext-tasksschema. - Resource/prompt spine — dropped; tools-only. The rebuild does not lead the SDK on augmentable request types, correcting the SEP-1686-era mistake. See Scope for v1.

