New to Claude Skills? Learn how to install them →

How MCP's Tasks Extension Works

MCP's Tasks extension lets a server hand back a task ID instead of blocking on a slow tool call, so a client can poll for the result later. Here is the actual mechanics: states, methods and the polling flow.

August 21, 2026
Get Claude Skills
9 min read

A blocking protocol meets a job that takes minutes

MCP's core request and response model assumes a tool call finishes in the time a client is willing to hold a connection open for. That assumption breaks for anything genuinely slow: a batch data job, a large document conversion, a computation that legitimately takes several minutes. Before the Tasks extension, an MCP server facing that mismatch had few good options, none of them clean.

The 2026-07-28 MCP specification moved Tasks out of the protocol's experimental core and into a formal, versioned extension: io.modelcontextprotocol/tasks. That's more than a status change. It's the point at which Tasks became something a client can rely on being specified, rather than an experimental feature that might change shape under it. This piece is about what the extension actually does, not the broader 2026-07-28 revision it shipped inside; see that article for the statelessness change and the rest of what moved in the same spec release.

The core idea: call now, fetch later

Instead of a tool call blocking until it finishes, a server facing a slow operation can hand back a task instead of a final result. The client gets a task ID immediately, the server keeps working in the background, and the client polls for the outcome whenever it's convenient. The extension's own description of the pattern is "call-now, fetch-later execution," aimed specifically at "representing expensive computations and batch processing requests."

A task, per the specification, is "a durable state machine that carries information about the underlying execution state of a request." Durable is the operative word: the task persists on the server side independent of whether the client is actively connected, which fits naturally with the stateless core the 2026-07-28 spec built around it. Nothing about polling a task later depends on the same connection, or even the same client process, that kicked it off.

The five task states

A task moves through a fixed set of states over its lifetime:

StateMeaning
workingThe request is currently being processed
input_requiredThe server needs input from the client before the task can proceed
completedThe request completed successfully; results are in the result field
failedThe request failed due to a JSON-RPC error during execution
cancelledThe request was cancelled before completion

input_required is worth calling out specifically, because it's the async equivalent of the multi-round request pattern the same 2026-07-28 spec introduced for synchronous calls. A task that needs to ask the client something mid-execution, rather than failing outright, surfaces that need as a state the client can see when it polls, rather than a message pushed down a connection the server has to keep open.

The three methods

Tasks adds three JSON-RPC methods, all operating on a taskId:

tasks/get polls for the current state of a task:

method: "tasks/get"
params: { taskId: string }

The response is a task object matching whichever state the task is currently in, with resultType: "complete" marking it as a normal, immediate response rather than another task.

tasks/update sends input back to a task sitting in input_required:

method: "tasks/update"
params: {
  taskId: string,
  inputResponses: InputResponses
}

tasks/cancel cancels a task before it finishes:

method: "tasks/cancel"
params: { taskId: string }

Both tasks/update and tasks/cancel return an empty acknowledgement, again with resultType: "complete".

How a tool call becomes a task

The trigger is a tools/call response that comes back shaped as a CreateTaskResult instead of a normal result. The specification requires servers to mark this explicitly: resultType MUST be set to "task" on that response, which is how a client tells the difference between a tool call that finished immediately and one that just started a background task. A task creation response looks like this:

{
  "resultType": "task",
  "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840",
  "status": "working",
  "createdAt": "2026-07-28T10:30:00Z",
  "lastUpdatedAt": "2026-07-28T10:50:00Z",
  "ttlMs": 3600000,
  "pollIntervalMs": 5000
}

Two fields in that response do practical work for a client implementer. pollIntervalMs is the server's suggested polling frequency, so a client doesn't have to guess how aggressively to check back. ttlMs sets a lifetime for the task from createdAt, and it's the backstop for the case where a client stops polling and comes back much later wondering whether a task is still alive.

A worked example: an export that takes four minutes

Concretely, here's the shape of a full exchange for a tool that generates a large CSV export, an operation too slow to hold a connection open for.

The client sends a normal tools/call, declaring Tasks support in its capabilities as shown above. The server, recognising the job will take a while, immediately returns a task instead of a result:

{
  "resultType": "task",
  "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840",
  "status": "working",
  "createdAt": "2026-07-28T10:30:00Z",
  "lastUpdatedAt": "2026-07-28T10:30:00Z",
  "ttlMs": 3600000,
  "pollIntervalMs": 5000
}

The client waits roughly pollIntervalMs, then sends tasks/get with that taskId. If the export is still running, it gets back the same shape with status: "working" and an updated lastUpdatedAt. It keeps polling on that interval. Once the export finishes, tasks/get returns:

{
  "resultType": "complete",
  "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840",
  "status": "completed",
  "createdAt": "2026-07-28T10:30:00Z",
  "lastUpdatedAt": "2026-07-28T10:34:12Z",
  "result": { "downloadUrl": "https://example.com/exports/786512e2.csv" }
}

Nothing here required the client to hold a connection open for the four minutes the export actually took, and nothing required the server to push anything to the client unprompted. That's the entire value of the extension in miniature: a slow operation becomes a sequence of small, ordinary, statelessly-resumable requests instead of either a long block or a bespoke side-channel invented per server.

Why polling instead of a push, streaming or webhook

It's a fair question, given that webhooks and Server-Sent Events already solve "notify me when a slow thing finishes" elsewhere. The answer traces back to the same architectural decision the rest of the 2026-07-28 spec makes: the protocol moved away from held-open connections entirely, deprecating the old HTTP+SSE transport in the same revision that introduced Tasks. A push-based notification for task completion would need exactly the kind of persistent, server-to-client channel the stateless core is designed to avoid, since it reintroduces the sticky-session and shared-state problems statelessness was meant to solve in the first place.

Polling isn't the theoretically fastest option, there's inherent latency between a task finishing and the client's next poll landing, bounded by whatever pollIntervalMs the server suggests. In exchange, it fits cleanly into infrastructure that scales the way the rest of 2026-07-28 assumes: any server instance behind a plain load balancer can answer a tasks/get for any task ID, with no requirement that the same instance that started the task is the one still connected to the client when it finishes.

TTLs: a backstop, not a guarantee

The specification is deliberately soft-worded here, and the distinction matters if you're building against this extension. Servers MAY discard a task once its TTL elapses, they aren't required to keep every task around indefinitely. Correspondingly, a client MAY treat the TTL as a backstop: if a task's observable status hasn't reflected an update by createdAt plus ttlMs, the client MAY consider it no longer usable.

Practically, that means don't write a client that assumes a task ID is durable forever just because you haven't polled it yet. Poll within roughly the window the server suggests via pollIntervalMs, and treat a task you've left untouched past its TTL as one you should re-issue rather than trust.

Task IDs are bearer tokens, not just identifiers

One specification detail with real security implications: task IDs double as the authorization mechanism for retrieving a task's result. The spec's guidance to server implementers is that IDs should be generated so "a third party cannot enumerate or guess them," explicitly framing them as bearer tokens. Anyone holding a valid task ID can poll tasks/get for its result, so a server that generates predictable, sequential task IDs is effectively leaking other clients' results to anyone who guesses nearby values. If you're implementing the server side of this extension, generate task IDs with the same care you'd give an API key or a session token, not a simple auto-incrementing counter.

Opting in as a client

Support for Tasks isn't assumed. A server MUST NOT return a CreateTaskResult to a client that didn't declare support for the extension on its request. A client signals support through the per-request capability metadata the 2026-07-28 spec's stateless core already carries on every call:

"_meta": {
  "io.modelcontextprotocol/clientCapabilities": {
    "extensions": {
      "io.modelcontextprotocol/tasks": {}
    }
  }
}

Without that declaration, a server that would otherwise hand back a task has to either complete the call synchronously or fail it outright, since it has no way to hand a client a task-shaped response it never said it could handle.

What this actually enables

The practical unlock is for MCP servers wrapping operations that were previously a poor fit for the protocol: a server that kicks off a long data export, a document processing pipeline, a batch enrichment job against an external API with its own rate limits. Before Tasks, a server author facing one of these had to either make the client wait uncomfortably long on a blocking call, build a bespoke out-of-band polling mechanism outside MCP's own schema, or artificially chunk the work into smaller synchronous calls that didn't map naturally to the underlying job. Tasks gives that pattern a standard shape inside the protocol itself, with states, TTLs and polling all specified rather than invented per server.

AWS is credited in the specification's own announcement as a contributor to the extension, and it's described there as "one of the first official MCP extensions," a framing worth noting given how new the extension mechanism itself is: 2026-07-28 is also the revision that formalised extensions as versioned, namespaced additions to the core protocol rather than ad hoc protocol changes.

What this means if you maintain an MCP server or client

If you maintain a server with any operation that can genuinely take longer than a client should reasonably block on, Tasks is the specified way to handle it rather than a workaround you'd otherwise have to invent. Implementing it means: returning CreateTaskResult with resultType: "task" for calls you're deferring, generating unguessable task IDs, honouring tasks/get, tasks/update and tasks/cancel, and picking sensible ttlMs and pollIntervalMs defaults for the kind of work your server does.

If you maintain a client, supporting Tasks means declaring the capability in _meta.clientCapabilities, handling a resultType: "task" response by switching to polling instead of expecting an immediate result, and respecting the TTL rather than polling a task ID indefinitely after the server may have already discarded it.

Either way, this is opt-in on both sides. A server that never returns task results and a client that never declares the capability can both keep working exactly as they did before 2026-07-28, since nothing about the extension changes behaviour for a call that completes synchronously. It only matters once you have an operation slow enough that blocking on it stops making sense.

Where to go next

For the rest of what changed in the same spec revision, statelessness, multi-round requests, the deprecation clock on Roots, Sampling, Logging and HTTP+SSE, see What changed in the MCP 2026-07-28 spec. For how MCP and Agent Skills divide responsibility more generally, see Agent Skills vs MCP. Browse MCP-capable platforms at getclaudeskills.com/platforms.

Verified directly against the Model Context Protocol's own 2026-07-28 announcement at blog.modelcontextprotocol.io and the Tasks extension specification (SEP-2663) in the modelcontextprotocol/modelcontextprotocol repository on GitHub. Adoption by specific MCP clients or servers beyond the specification itself could not be independently confirmed at time of writing.

Frequently asked questions