MCP Tasks: How Agents Wait — the Async Extension, Dissected
A tool call that finishes in two seconds needs a request and a response. A tool call that renders a video, crawls ten thousand pages, or waits for a human approval needs something else: a handle, a status, and a way to come back later. The MCP spec that finalizes today ships that primitive — but not where it used to live.
Today, July 28, 2026, is the day the Model Context Protocol's next specification revision is scheduled to become final, closing the ten-week validation window that opened when the release candidate locked on May 21. We covered the headline change — the stateless core — when the RC dropped, and the enterprise authorization extension yesterday. This piece is about the third leg of the release, and the one most directly relevant to agents that do real work: Tasks, the protocol's answer to long-running operations.
Tasks has an unusual trajectory. It shipped as an experimental core feature in the 2025-11-25 revision. Production use surfaced enough redesign that the maintainers pulled it out of the specification entirely and rebuilt it as an official extension — SEP-2663, merged May 15, 2026, now living in its own ext-tasks repository under the identifier io.modelcontextprotocol/tasks. That demotion is not a downgrade. Under the extensions framework introduced in this release (SEP-2133), extensions have reverse-DNS identifiers, dedicated repositories, delegated maintainers, and independent versioning — Tasks can now evolve on implementation feedback without waiting for a spec release train. Anyone who built against the 2025-11-25 experimental API, however, has a migration ahead: the blocking tasks/result method is gone, tasks/list is gone, and the lifecycle is reshaped around the same principle that reshaped the core — no sessions.
Why async is the agent problem, not a nice-to-have
Synchronous request/response is a poor fit for agent workloads in a specific, structural way. An HTTP connection held open is a resource on both ends and on every proxy in between. Timeouts at the gateway, the load balancer, and the client each impose their own ceiling, and the effective limit is the minimum of all of them — usually well under two minutes. Real agent work routinely exceeds that: media generation, large crawls, batch inference, multi-step pipelines, and anything that pauses for a human decision.
The pre-Tasks workarounds are familiar and all bad in their own way. Servers stream progress notifications over a connection that still cannot outlive the infrastructure's patience. Tool authors invent ad-hoc job patterns — a start_job tool that returns a job ID and a check_job tool to poll it — which works but is invisible to the protocol: no standard status vocabulary, no standard cancellation, nothing a client or gateway can reason about generically. Tasks standardizes exactly that pattern, at the protocol layer, with defined semantics for every step.
Server-directed creation: the server decides what becomes a task
The first design decision worth noticing is who initiates. In the new extension, task creation is server-directed. The client does not ask for a task; it signals that it can handle one, by including io.modelcontextprotocol/tasks in the extensions it advertises in the per-request _meta capabilities — the same envelope that carries protocol version and client identity now that the initialize handshake is gone. The server then decides, per request and at its own discretion, whether to answer synchronously or to materialize a task. The spec is blunt about it: the server MAY return a CreateTaskResult in lieu of a standard result, and the server is the sole decider. If the server needs the capability and the client did not advertise it, the call fails with error -32003 and a requiredCapabilities field naming the extension.
This is the right allocation of knowledge. Only the server knows whether this particular tools/call will take 800 milliseconds or 40 minutes — and it may not know until it has started. Server-directed creation lets the same tool answer small inputs synchronously and large inputs asynchronously, without the client guessing in advance. Currently tools/call is the only method that supports task-augmented execution; the spec explicitly leaves the door open to extending it to other request types.
When the server does choose a task, the response is a CreateTaskResult discriminated by resultType: "task":
{
"resultType": "task",
"task": {
"taskId": "7f3d2a9c46e1b8...",
"status": "working",
"createdAt": "2026-07-28T09:00:00Z",
"ttlMs": 86400000,
"pollIntervalMs": 5000
}
}
One normative line here carries most of the reliability weight: a server MUST NOT return CreateTaskResult until the task is durably created — until a tasks/get for that taskId would already resolve. There is no window where the client holds a handle to nothing. If your task store is Postgres or Redis, the write commits before the response leaves. This is the same discipline x402 facilitators apply to settlement records, and it is what makes the handle safe to persist and resume against after a crash — which the spec tells clients they SHOULD do.
Five statuses, one state machine
The lifecycle is a five-state machine. Two states are non-terminal: working and input_required. Three are terminal and irreversible: completed, failed, and cancelled. Every task starts in working.
The compression in that first state is deliberate. Whether the job is actively executing or sitting in a queue behind a hundred others, the client sees working. Queue position, worker assignment, and retry internals are the server's business; the optional statusMessage field exists for human-readable color, but the state vocabulary stays minimal so every client and every gateway can implement the same loop. On completed, the task's result contains exactly what the original request would have returned synchronously — the caller's downstream code does not change. On failed, error carries the JSON-RPC error.
The client's side of the contract is polling. It calls tasks/get with the taskId, respecting the server's suggested pollIntervalMs, until a terminal status arrives. Reads and writes are deliberately split across two methods — tasks/get never mutates, which keeps it idempotent, safe to retry, and cacheable — while tasks/update carries anything that changes state. For deployments that want push instead of poll, servers can emit notifications/tasks to clients that opted in via subscriptions/listen, each notification carrying the same full task state a tasks/get would return. Notifications are an optimization, not a replacement: the polling loop is the baseline that works through any intermediary.
Retention is governed by ttlMs — time-to-live from creation, mutable over the task's lifetime, null meaning unlimited. The semantics are honest about what a TTL is: a backstop, not a promise. After the TTL elapses, the server MAY mark the task failed and later delete it entirely, at which point tasks/get returns -32602 as if the task never existed. A client that stops polling for a day should treat an expired TTL as the handle going stale.
input_required: elicitation without a live connection
The most interesting state is input_required, because it solves a problem the old model could not: how does a server ask the client something in the middle of a job when there is no session and possibly no open connection?
The answer is to make the questions part of the task state. When the server needs input, the task transitions to input_required and the tasks/get response grows an inputRequests map — keyed entries, each carrying the method and params of what would otherwise be a standalone server-to-client request, an elicitation being the canonical case. The client answers through tasks/update, supplying an inputResponses map keyed to match. The server MAY accept a partial set of responses; the task stays in input_required until everything outstanding has arrived, then typically returns to working.
The keys do real work. Each key MUST be unique over the lifetime of the task, a server MUST NOT reuse a key after its response has been delivered, and clients SHOULD track keys to avoid answering twice — which turns the keys into idempotency tokens. A retried tasks/update cannot double-submit an approval, for the same reason a reused EIP-3009 nonce cannot double-spend an authorization. Anyone who has built payment rails will recognize the shape.
The spec also closes a trust loophole explicitly: an elicitation surfaced through inputRequests MUST be treated by the host exactly like a direct elicitation/create — same trust model, same user-facing behavior. A server must not be able to smuggle a lower-scrutiny prompt to the user just because it arrived inside a task envelope rather than as a live request.
Cancellation is a request, not a command
Cancellation is cooperative by design. tasks/cancel signals intent; the server acknowledges with an empty result and decides whether and when to honor it. Eventual transition to cancelled is not guaranteed — the work may complete or fail first, and a task that already paid for its side effects may be past the point of stopping. Clients MAY discard their local state the moment they send the cancellation and move on.
That reads as weak until you consider what a hard-cancel guarantee would cost: every server would need preemptible workers and transactional rollback of arbitrary side effects. The protocol instead tells the truth about distributed work — you can stop paying attention, but you cannot always stop the world. Billing implications follow directly, and we return to them below.
What got deleted, and why the deletions are the design
Against the 2025-11-25 experimental API, three removals stand out. tasks/result — the blocking call that parked a connection until the task finished — is gone; it reintroduced through the back door exactly the long-lived connection Tasks exists to eliminate. The per-request opt-in parameter is gone in favor of capability advertisement plus server discretion. And tasks/list is gone for a reason worth quoting in full: without sessions, there is no safe way to scope "whose tasks?" A list endpoint needs an owner, the protocol no longer has a session to define one, and rather than ship an enumeration surface with fuzzy authorization semantics, the extension deleted it. Task discovery is now the client's job: persist your taskIds durably, because nobody will remind you of them.
That deletion points at the extension's one sharp edge. A taskId is, in the spec's own words, usable as a bearer token: servers MUST generate IDs with enough entropy that they cannot be enumerated or guessed. But beyond entropy, the specification does not bind a task to the identity that created it. Whether a token presented on tasks/get must match the token that created the task is left to the implementation. A serious deployment should bind tasks to the authenticated principal — the OAuth subject from the authorization layer, or the API key at the gateway — and treat entropy as defense in depth, not as the access-control model.
mcp v2.0.0b1, TypeScript v2 (split into @modelcontextprotocol/server and @modelcontextprotocol/client), Go v1.7.0-pre.1, and C# v2.0.0-preview.1, per the official SDK betas post. The betas cover the stateless core and multi round-trip requests; as an extension, Tasks versions independently — check each SDK's extension support before depending on it.
What it means for LLM4Agents
LLM4Agents operates an OpenAI-compatible gateway where agents pay per call in stablecoins, and exposes its own MCP server with tool families that are exactly the workloads Tasks was built for: video and media generation, workspace ingestion, batch inference. Today those long-running tools either stretch synchronous timeouts or use ad-hoc job tools. The Tasks extension gives that pattern a standard shape, and three consequences follow.
First, the billing question gets sharper and better at the same time. Our settlement model reserves funds at request time and settles for actual usage — the same shape as the x402 upto scheme, where verify authorizes a ceiling and settle charges the real amount. A task-shaped call maps onto that cleanly: reserve at CreateTaskResult, settle at terminal status. completed settles for usage; failed at the protocol layer releases the reserve; and cooperative cancellation is precisely why reserve-then-settle is the right primitive — a tasks/cancel after the GPU hours are spent still settles for the work done, and the protocol's honesty about that beats a billing model that pretends cancellation is free.
Second, the durability MUST is a gift to a billing gateway. Because a task must be durably created before the handle is returned, the task record and the payment reserve can commit in the same transaction. Every taskId we hand an agent is backed by both a job row and a hold on funds — no orphaned charges, no phantom jobs.
Third, the identity gap is ours to close. The spec stops at entropy; a gateway that authenticates every caller can do better by binding each task to the paying agent's identity and rejecting tasks/get from anyone else. Payment-anchored identity — the theme running from our ERC-8004 audit onward — applies here too: the entity that paid for the task is the natural owner of its handle.
Staying on the frontier
Concrete sequence, in order. One: adopt the extension server-side — implement tasks/get, tasks/update, and tasks/cancel on the LLM4Agents MCP server, and migrate the long-running tool families (video, batch, ingestion) to server-directed task creation, keeping synchronous answers for small inputs. Two: wire the lifecycle into billing — reserve on task creation in the same transaction as the durable task write, settle on terminal status, release on protocol-level failure, and document the cancellation-settles-for-work-done policy before the first dispute, not after. Three: bind tasks to caller identity at the gateway and scope every task method to the creating principal, treating ID entropy as secondary defense. Four: expose a scoped task index for our own callers — the spec deleted tasks/list because it has no session to scope by, but a gateway that authenticates agents can safely offer "my tasks" as a value-add the raw protocol cannot. Five: track the extension's independent release cadence and the SDKs' extension support, and pin versions — an extension that iterates faster than the spec is the point, and also the operational risk.
The through-line of the 2026-07-28 release is that MCP stopped assuming a conversation and started assuming infrastructure. The stateless core made requests routable; Tasks makes work durable. For agents that pay for what they consume, that second property is the one that matters: a job you can hold a handle to is a job you can meter, settle, and prove. The protocols are converging on what payment rails learned long ago — the receipt is the product.
Give your agents work that outlives the request
One gateway, 345+ models, stablecoin billing per call — built for the async agent stack.
Register your agent