Long-Running Work: the Tasks Extension
Why not just block?
Section titled “Why not just block?”Progress notifications keep a request honest, but they still require the request to stay open. That fails for real work:
- Intermediaries time out. Load balancers, gateways, and mobile networks cut idle or long connections after seconds or a few minutes.
- Clients crash or move. A laptop lid closes; the phone switches from Wi-Fi to cellular. An in-flight request is simply lost.
- Humans are slow. An approval gate may sit for hours.
The Tasks extension (io.modelcontextprotocol/tasks) lets a server answer immediately with a durable task handle instead of the result. The client polls that handle, supplies input if asked, and collects the result whenever it is ready — even from a fresh connection or a new process.
Tasks moved out of the core protocol and into an official extension in 2026-07-28 (SEP-2663). The full specification lives in the modelcontextprotocol/ext-tasks repository; the shapes below are what you will see on the wire.
Negotiating the extension
Section titled “Negotiating the extension”Extensions are advertised in the extensions field of capabilities — on every client request, and in the server’s server/discover response:
{ "_meta": { "io.modelcontextprotocol/protocolVersion": "2026-07-28", "io.modelcontextprotocol/clientCapabilities": { "extensions": { "io.modelcontextprotocol/tasks": {} } } }}{ "result": { "resultType": "complete", "supportedVersions": ["2026-07-28"], "capabilities": { "tools": {}, "extensions": { "io.modelcontextprotocol/tasks": {} } } }}The rule is strict: a server must never return a task to a client that did not declare the extension on that request. Because the protocol is stateless, check the capabilities each time — there is nothing to remember from an earlier call.
The flow
Section titled “The flow”sequenceDiagram
participant C as Client
participant S as Server
C->>S: tools/call (with tasks capability)
S-->>C: resultType task (taskId, status working)
loop until terminal
C->>S: tasks/get (taskId)
S-->>C: status working
end
C->>S: tasks/get (taskId)
S-->>C: status input_required, inputRequests
C->>S: tasks/update (taskId, inputResponses)
S-->>C: ack
C->>S: tasks/get (taskId)
S-->>C: status completed, result 1. The server decides to return a task
Section titled “1. The server decides to return a task”The server chooses per request whether the work is long enough to deserve a task. The client does not ask for one; it just has to be ready for either shape.
{ "jsonrpc": "2.0", "id": 1, "result": { "resultType": "task", "task": { "taskId": "tsk_9f2c1a", "status": "working", "statusMessage": "Queued behind 2 other builds", "ttlMs": 3600000, "pollIntervalMs": 5000 } }}The task must be durably created before this response is sent — a taskId the server has already forgotten is worse than no task at all.
2. The client polls
Section titled “2. The client polls”{ "jsonrpc": "2.0", "id": 2, "method": "tasks/get", "params": { "taskId": "tsk_9f2c1a" }}Respect pollIntervalMs. Store the taskId somewhere durable so polling can resume after a crash — that persistence is the whole point.
3. Statuses
Section titled “3. Statuses”| Status | Meaning | Terminal |
|---|---|---|
working | in progress | no |
input_required | the server needs client input; see inputRequests | no |
completed | done; result holds what the original request would have returned | yes |
failed | a JSON-RPC error occurred; error holds it | yes |
cancelled | cancellation was honored | yes |
Once a task reaches a terminal state it never changes again.
4. Mid-flight input
Section titled “4. Mid-flight input”When a task needs the user — an approval, a choice — it moves to input_required and the tasks/get response carries an inputRequests map with the same shapes MRTR uses (elicitation, and so on). The client answers with tasks/update:
{ "jsonrpc": "2.0", "id": 3, "method": "tasks/update", "params": { "taskId": "tsk_9f2c1a", "inputResponses": { "approve_prod": { "action": "accept", "content": { "confirm": true } } } }}The server acknowledges with an empty result and ignores responses for keys it did not ask for or has already satisfied. No second connection, no server-initiated message — the same stateless discipline as MRTR.
5. Completion and retrieval
Section titled “5. Completion and retrieval”{ "jsonrpc": "2.0", "id": 4, "result": { "resultType": "complete", "task": { "taskId": "tsk_9f2c1a", "status": "completed", "ttlMs": 3600000 }, "result": { "content": [{ "type": "text", "text": "Build 4812 passed in 6m 12s" }], "structuredContent": { "buildId": 4812, "ok": true } } }}The result is exactly what a synchronous tools/call would have returned. ttlMs tells the client how long the server promises to keep the task retrievable.
6. Cancellation
Section titled “6. Cancellation”tasks/cancel is acknowledged with an empty result but is cooperative: the server may or may not be able to stop the underlying job, and the task may still end completed or failed. Treat cancelled as one possible outcome, not a guarantee.
Notifications instead of polling
Section titled “Notifications instead of polling”A server may push full task snapshots as notifications/tasks. Clients opt in through the normal subscriptions/listen mechanism (see change notifications). Each notification carries the whole task state, so no follow-up tasks/get is needed. Polling remains the default and the fallback.
When to use tasks
Section titled “When to use tasks”| Situation | Why tasks fit |
|---|---|
| CI pipelines, batch jobs, model training | minutes to hours; blocking is impossible |
| approval gates, human review | input_required surfaces the request; nobody holds a connection open |
| wrapping an API that already has job ids | return a task when you create the job, resolve it when the job does |
| mobile / flaky networks | the taskId survives disconnects |
| bulk operations with meaningful partial progress | statusMessage reports progress without a live stream |
And when not to: anything that finishes in a few seconds. A task adds two round trips minimum and a polling loop; plain tools/call with progress is cheaper and simpler.
Server checklist
Section titled “Server checklist”- Advertise
io.modelcontextprotocol/tasksinserver/discovercapabilities. - On each request, check the client’s
extensionsbefore returning a task. - Persist the task before responding with
resultType: "task". - Serve
tasks/get, returningresultoncompletedanderroronfailed. - Accept
tasks/updateandtasks/cancel; acknowledge both with an empty result. - Bind tasks to the authenticated user. A
taskIdis a handle, not a credential — validate the caller on every poll (see state handle hijacking).
SDK support for the extension is evolving; where your SDK exposes a task helper, use it, otherwise the wire shapes above are small enough to implement by hand on top of an ordinary request handler.