Community-maintained FDE reference. Not an official Vercel or Anthropic project. About this project
Tasks (extension)
Now an official extension, redesigned on the way out of core. Tasks were introduced in MCP 2025-11-25 as an experimental core utility. The 2026-07-28 revision moves them out of the core protocol into the official
io.modelcontextprotocol/tasksextension and redesigns the model: polling viatasks/getreplaces the blockingtasks/result, a newtasks/updatecarries client-to-server input,tasks/listis removed, and servers may return task handles unsolicited (SEP-2663). This page describes the extension as published; the changes from the 2025-11-25 core design are listed below because SDKs and clients that still speak the old shape are in the wild.
Plain-language explanation
TL;DR: A task turns a slow MCP request into a durable, pollable one. Instead of holding the connection open until the work finishes, the server immediately returns a CreateTaskResult: a taskId plus status, marked resultType: "task". The client then polls with tasks/get until the task reaches a terminal status, at which point the tasks/get response itself carries the final result (or error). If the task needs something from the user mid-flight, it parks in input_required and the client answers via tasks/update. It is the protocol-level mechanism for exactly the shape the async-jobs pattern hand-rolls.
Tasks exist because some operations are too slow to block on: an expensive computation, a batch job, a call to an external job API. On Vercel the pressure is sharper still, because a Vercel Function invocation has a hard maxDuration budget; “hold the connection open and hope” is not merely fragile there, it has a deadline. Without tasks you invent your own handle-and-poll convention per server. The extension standardizes the handle, the polling, the input round-trip, and the lifecycle.
Two design decisions changed with the move to an extension, and both fit the stateless 2026-07-28 revision. First, tasks are server-directed: the client opts in once via the extension capability, and the server decides per request whether to return a task instead of a direct result; there is no per-request task flag and no per-tool warmup. Second, tasks are now client-polls-server only: the stateless revision removed server-initiated requests entirely, so the 2025-11-25 notion of a server task-augmenting a sampling/createMessage it sends to the client no longer has anything to attach to.
Formal protocol perspective
Negotiation. The tasks extension uses the standard 2026-07-28 extension mechanism: the client includes io.modelcontextprotocol/tasks in the extensions field of the io.modelcontextprotocol/clientCapabilities it sends in each request’s _meta; the server advertises the same key in the extensions of the capabilities it returns from server/discover. A server must not return a task to a client that did not declare support.
Task creation. In response to a supported request (for example tools/call), the server may return a CreateTaskResult, identified by resultType: "task", containing a Task object: a unique taskId, the initial status, ttlMs (retention), and pollIntervalMs (suggested polling cadence). The task is durably created before the response is sent, so the handle the client receives is always redeemable.
The three operations:
tasks/get: poll a task’s status (clients respect the returnedpollIntervalMs). The response carries the currentTask; for terminal states it also carries the outcome inline: aresultfield (what the original request would have returned synchronously) oncompleted, or anerrorfield (the JSON-RPC error) onfailed.tasks/update: supplyinputResponseskeyed to the outstandinginputRequestsof a task ininput_required. The server acknowledges with an empty result and ignores responses for unknown or already-satisfied keys.tasks/cancel: request cancellation at any time. Cancellation is cooperative: the server acknowledges the intent but is not obligated to stop the work, and the task may still reach a terminal status other thancancelled.
Mid-flight input. When the task needs something (typically the moral equivalent of an elicitation), it moves to input_required and the tasks/get response includes an inputRequests map. The client presents those to the user or model and answers via tasks/update. This is the task-shaped sibling of the MRTR retry loop: in both, the server returns its questions instead of initiating a request.
Notifications. Servers can push status updates via notifications/tasks, delivered through the subscriptions/listen mechanism, each carrying the full task state. Polling remains the default; notifications are an optimization, not a requirement.
Lifecycle
Diagram source (Mermaid)
stateDiagram-v2
[*] --> working: CreateTaskResult returned
working --> input_required: server needs input
input_required --> working: tasks/update with inputResponses
working --> completed
working --> failed
working --> cancelled: tasks/cancel honored
input_required --> completed
input_required --> failed
input_required --> cancelled: tasks/cancel honored
completed --> [*]
failed --> [*]
cancelled --> [*]The statuses are working, input_required, completed, failed, and cancelled. completed, failed, and cancelled are terminal: once reached, the task’s state does not change. failed means a JSON-RPC error occurred during execution and the error field has the details; completed means the result field contains the final output.
Request / result flow
Diagram source (Mermaid)
sequenceDiagram
participant C as Client
participant S as Server
C->>S: tools/call with tasks extension capability in _meta
S-->>C: CreateTaskResult with resultType task, taskId, working
loop poll until terminal, respect pollIntervalMs
C->>S: tasks/get with taskId
S-->>C: Task status
end
Note over C,S: server needs user input
S-->>C: Task status input_required with inputRequests
C->>S: tasks/update with inputResponses
S-->>C: acknowledged
C->>S: tasks/get with taskId
S-->>C: Task completed with result inline
Note over S: after ttlMs the task may be purgedWhat changed from the 2025-11-25 core design
If you last read this page (or an SDK) against 2025-11-25, recalibrate:
- Opt-in moved from the request to the capability. The per-request
taskaugmentation field and the per-toolexecution.taskSupportdeclaration are gone; the client declares the extension once per request in_metacapabilities, and the server decides when to return a task, including unsolicited. tasks/resultis gone. There is no blocking retrieval call; the terminaltasks/getresponse carries theresultorerrorinline.tasks/updateis new. Under the core design,input_requiredsurfaced a nested server-initiated elicitation or sampling request; those no longer exist, so input flows client-to-server throughtasks/update.tasks/listis removed.- Field renames:
ttlis nowttlMs,pollIntervalis nowpollIntervalMs, andCreateTaskResultis identified byresultType: "task". - Directionality collapsed. The requestor/receiver framing (either side could create tasks on the other) is gone with server-initiated requests; the client polls the server, full stop.
Common misconceptions
CreateTaskResultcontains the answer. No: it contains thetaskId, status,ttlMs, andpollIntervalMs. The answer arrives in a latertasks/getresponse, once the status is terminal.- There is a
tasks/resultcall to fetch the outcome. Not anymore: that was the 2025-11-25 core design. Under the extension, the terminaltasks/getresponse carries the outcome inline. - The status notification can be relied on. No:
notifications/tasksrides the opt-insubscriptions/listenstream and support varies; pollingtasks/getis the default and always works. tasks/cancelstops the work. Not necessarily: cancellation is cooperative. The server acknowledges the intent; the task may still land oncompletedorfailed.- A failed tool call is a protocol error. No: a task whose underlying work produced a JSON-RPC error lands in
failedwith theerrorfield populated; a tool that ran and returnedisError: trueis acompletedtask whoseresultcarries that tool-level error. The two layers stay distinct, exactly as in a direct call. - Tasks make a serverless server durable by themselves. No: tasks standardize the conversation about background work. The work, and the task state, still need somewhere to live that outlives a function invocation. See the Vercel reality section below.
Debugging notes
- Task not found on
tasks/get- the server may have purged it afterttlMs, or thetaskIdis from another principal’s context. Treat “not found” as possibly-expired first, bug second. - Stuck in
input_required- the server is waiting ontasks/update. Check that you read theinputRequestsmap from thetasks/getresponse and that yourinputResponseskeys match; the server silently ignores unknown keys. - You returned a task and the client hung - the client never declared
io.modelcontextprotocol/tasksin its per-request capabilities and does not understandresultType: "task". Never return a task to a client that did not opt in; fall back to a synchronous result or the async-jobs pattern. cancellednever arrives aftertasks/cancel- legal: cancellation is cooperative and the work may finish anyway. Poll to a terminal state rather than assuming.- Task vanishes between polls on Vercel - you stored task state in process memory. A new invocation is a new (or best-effort reused) instance; Fluid compute reuse is never a correctness guarantee. See below.
Security implications
The taskId is the capability that grants access to a task’s status, its result, and its input channel, so it is the security pivot:
- Bind tasks to the authorization context. Scope each task to the verified principal that created it, and refuse
tasks/get,tasks/update, andtasks/cancelfor tasks outside that context. On Vercel that principal comes fromwithMcpAuth, never from a tool argument; see Where the principal comes from and the authorization checklist. - No auth context? Then
taskIds must be cryptographically random with enough entropy to resist guessing, andttlMsshould be short: an unauthenticated task id is a bearer token for the result. tasks/updateis an input surface. It injects data into a running job; validateinputResponsesexactly as you would tool arguments, against the schema of what was asked for.- Rate-limit and cap. Limit concurrent tasks per principal and enforce a maximum
ttlMsto prevent enumeration and resource-exhaustion attacks; clean up expired tasks; log task lifecycle events for audit. See Monitoring and audit.
What the TypeScript SDK actually ships
Verified against the installed v2 packages (@modelcontextprotocol/server 2.0.0 and @modelcontextprotocol/client 2.0.0, the line mcp-handler 2.1.1 peers on) in this repo’s examples/minimal-server (in the repository):
- Schema types only, and of the old shape. The packages export the 2025-11-25 core task types (
CreateTaskResult,GetTaskRequest,GetTaskPayloadRequest,ListTasksRequest,CancelTaskRequest,TaskStatusNotification,TaskAugmentedRequestParams, the per-tooltaskSupportfield,RELATED_TASK_META_KEY). Nothing of the redesigned extension exists: notasks/updateanywhere in the typings. - No runtime task API at all. SDK v1 (1.26.0) shipped a working two-sided surface under its
experimental/tasksexport (registerToolTask, pluggableTaskStore,callToolStream); v2.0.0 exposes none of it. The server package’s exports are.,./stdio,./validators/ajv,./validators/cf-worker, and./_shims; none is a task runtime, and there is no task method onMcpServerorClient.
Wire status: the pinned stack (mcp-handler 2.1.1 on @modelcontextprotocol/server 2.0.0) serves the 2026-07-28 contract natively over Streamable HTTP and falls back to stateless 2025-11-25 Streamable HTTP for legacy clients; the SDK Client defaults to that legacy handshake unless you opt in to modern version negotiation, so the examples’ in-memory test suites exercise only the legacy path.
Why this repo has no runnable tasks example
Two reasons, one harder than before:
- The released stack has no task runtime. As verified above, SDK v2 has no server-side or client-side task runtime API, old shape or new: no
tasks/update, no pluggableTaskStore, noregisterToolTask. The wire itself is not the obstacle: the pinned handler serves 2026-07-28 natively, includingserver/discover, so a server could advertise theio.modelcontextprotocol/tasksextension in its capabilities and read the client’s per-request_metaopt-in. What it could not do is run a task, so an example would have to hand-roll the extension’s state machine and wire messages against typings that still describe the superseded core design, teaching idioms that match neither the SDK nor the spec. - Task state needs a durable home, and that is on you. Task state must survive across invocations: the
tasks/getpoll may land on a different invocation than thetools/callthat created the task, and a stateless Vercel Function gives you no durable process (see Serverless sessions). A production deployment needs a task store backed by Redis or Postgres, which would drag deployed infrastructure into a test suite this repo requires to run offline. The extension’s own model agrees: the task must be durably created before theCreateTaskResultis sent.
The durable, portable shape for background work on Vercel today is the hand-rolled one: the async-jobs pattern with its runnable examples/async-jobs-server (in the repository), backed by Queues or Workflows and an external job store.
Relationship to the async-jobs pattern
async-jobs is the application-level pattern (an opaque handle, progress, cancellation, idempotent retrieval) that servers implemented by hand before there was protocol support. The tasks extension is the protocol formalizing that pattern, and the 2026-07-28 redesign moved it closer to the hand-rolled shape: CreateTaskResult is the handle, tasks/get is the poll that also returns the outcome, tasks/cancel is the cancel, and there is no blocking retrieval call left to hold a connection open. Because the extension is opt-in on both sides and the released SDK does not implement it, the hand-rolled pattern remains the portable approach today; the extension is where it is heading, and it now needs negotiation (server/discover capabilities) rather than guesswork to adopt.
Related
- async-jobs pattern - the hand-rolled equivalent you should ship today, with its runnable example.
- The 2026-07-28 stateless revision - the revision that moved tasks out of core, and the MRTR pattern tasks now rhyme with.
- Serverless sessions - why in-process task state does not survive on Vercel.
- Where the principal comes from - the authorization context tasks must be bound to.
- Capability primitives - the request types a task can stand in for.
Bibliography
- Model Context Protocol, Tasks extension overview - https://modelcontextprotocol.io/extensions/tasks/overview
- Model Context Protocol, Extensions overview - https://modelcontextprotocol.io/extensions/overview
- Model Context Protocol, ext-tasks specification repository - https://github.com/modelcontextprotocol/ext-tasks
- Model Context Protocol Specification, Key Changes, version 2026-07-28 (SEP-2663, tasks as an extension) - https://modelcontextprotocol.io/specification/2026-07-28/changelog
- Model Context Protocol Specification, Cancellation, version 2026-07-28 - https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/cancellation
- Model Context Protocol Specification, Progress, version 2026-07-28 - https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/progress
- Model Context Protocol TypeScript SDK, source repository - https://github.com/modelcontextprotocol/typescript-sdk
- mcp-handler, source repository - https://github.com/vercel/mcp-handler