The Day MCP Stopped Watching the Clock

Every developer who has integrated an AI with a real backend knows this pain. The user says, "Run the data refresh." The MCP tool kicks off a job. The job takes four minutes. The MCP request times out at thirty seconds. The model receives an error and confidently tells the user, "I was unable to run the data refresh," even though the refresh is, at this very moment, happily running.
For about a year, MCP did not have a good answer for this. You either polled, you faked it, or you accepted that long-running operations weren't really part of the protocol's worldview. The 2025-11-25 spec changed that by introducing Tasks: a first-class way to say "this isn't going to finish in the next thirty seconds, and that's fine."
Three revisions matter to this story:
| Revision | What it did for long-running work |
|---|---|
2025-06-18 | elicitation arrives: a server can ask the user a question while it is handling a request |
2025-11-25 | Tasks arrive as experimental, and the client decides which requests become tasks (SEP-1686) |
2026-07-28 | Tasks move into the io.modelcontextprotocol/tasks extension, and the server decides (SEP-2663) |
This post is about what Tasks are, what they replace, and why the addition is more interesting than the headline makes it sound.
This post describes Tasks as they shipped experimentally in 2025-11-25. They have since moved out of the core protocol into an official extension, io.modelcontextprotocol/tasks, under SEP-2663. A SEP is a Specification Enhancement Proposal, the way a change to MCP is proposed and accepted. 2026-07-28 is now the current specification.
One change matters more than the rest, because this post argues the opposite below: task creation is now server-directed. The client no longer opts in per request. It declares the extension once, inside _meta, the metadata field carried on every MCP request:
{
"method": "tools/call",
"params": {
"_meta": {
"io.modelcontextprotocol/clientCapabilities": {
"extensions": {
"io.modelcontextprotocol/tasks": {}
}
}
}
}
}
From there the server decides call by call whether to answer with an ordinary result or a task handle. The extension spec is blunt about it: "The server is the sole decider; clients do not signal task preference on the request itself." The per-tool execution.taskSupport declaration went with it.
The lifecycle below survives intact. The negotiation described under "Why This Is Not Just 'Async'" does not. Read the mechanics as 2025-11-25 history unless flagged otherwise.
The Synchronous Trap
MCP, like JSON-RPC underneath it, started life as a request/response protocol. You send tools/call. You get back CallToolResult. The transport assumes that pair of messages will fit comfortably inside one HTTP round-trip, or one back-and-forth on a stdio pipe, or one quick exchange over SSE, the server-sent events stream.
For most tools, that assumption is fine. search_customers runs a database query and returns in 200ms. read_file reads a file and returns immediately. calculate is, well, calculation.
But real systems have operations that don't fit this shape:
- Deployments that take minutes to roll out.
- Data refreshes that scan terabytes.
- CI runs that compile, test, and lint at their own pace.
- External integrations that legitimately take a while because they're sending email, generating PDFs, or waiting on a human.
- Anything involving "approval" where a human has to actually look at something.
The original MCP answer was: don't do that. Make every tool fast. If you have something slow, kick off a background job from the tool and return a job ID, then have the model poll a separate get_job_status tool until done. From the model's side, the loop looks like this:
This worked, in the same way that walking up the stairs with a sofa works. You can do it. You won't enjoy it. And every team reinvented it slightly differently.
Enter Tasks
The 2025-11-25 spec added experimental support for Tasks (SEP-1686). The core idea is small and elegant: certain MCP requests (in that revision, tools/call for clients, and sampling/createMessage/elicitation/create for servers) can be augmented with a task. The receiver returns a task handle instead of holding the original request open, and the requestor polls for status and eventually fetches the result.
A task has a lifecycle. The spec defines explicit states:
- working: the task is running
- input_required: the task is paused, waiting for more input from the user
- completed: the task is done; the result is ready to fetch
- failed: the task ran but produced an error
- cancelled: the task was cancelled (by the client or the server)
Those five states connect in a fixed way:
completed, failed and cancelled are terminal: a task that reaches one stays there.
That state machine alone solves a lot of problems. The "input_required" state is particularly clever: it gives a clean answer to operations that need to ask the user a question mid-flight, which previously required hacking around the request/response model. (Elicitation, which landed in 2025-06-18, becomes much more powerful when it can be invoked from inside a long-running task instead of having to fit inside a single tool call.)
Why This Is Not Just "Async"
The first time I read about Tasks, my reaction was: "Okay, MCP got promises. Took them long enough."
I was wrong. Tasks aren't just async/await for the protocol.
The interesting design choice, in 2025-11-25, was that the client decided whether to wait. In a normal async API, the server decides whether something runs synchronously or asynchronously, and the client lives with it. In that first version of Tasks, the client (the requestor) chose whether to augment a request with a task. The server controlled only which tools were eligible, by declaring each tool's execution.taskSupport as forbidden, optional, or required. The opt-in itself was one field in the request params:
{
"method": "tools/call",
"params": {
"name": "run_data_refresh",
"task": { "ttl": 60000 }
}
}
That's not promises. That's a negotiation about how long the client is willing to wait.
And then the spec changed its mind. In the extension that replaced it, the decision moved to the other end of the wire. The client declares the tasks capability once, and the server decides per call whether to hand back a result or a task handle. execution.taskSupport is gone, and so is the per-request opt-in. Side by side, that is most of what moved:
2025-11-25 (experimental, SEP-1686) | io.modelcontextprotocol/tasks (SEP-2663) | |
|---|---|---|
| Who decides a request becomes a task | the client, per request, with a task field in the params | the server, per call, at its own discretion |
| How support is declared | tasks capabilities at initialization, plus execution.taskSupport per tool in tools/list | the extension identifier in io.modelcontextprotocol/clientCapabilities, in every request's _meta |
| Fetching the result | tasks/result, which blocks until the task is terminal | tasks/get, whose terminal response carries the result or the error |
| Mid-flight input | elicitation side-channelled over an SSE stream held open by tasks/result | inputRequests in the tasks/get response, answered with tasks/update |
| Listing tasks | tasks/list, where the receiver declared it | removed |
| Requests that can become tasks | tools/call, plus client-hosted sampling/createMessage and elicitation/create | tool calls only |
| Suggested poll pacing | pollInterval | pollIntervalMs |
I've left the original argument standing rather than quietly deleting it, because the question it raises, who gets to decide how long you wait, is still the right one to ask of any protocol. The answer just went the other way. And the reasoning is not hard to reconstruct: per-request negotiation turned out to be more machinery than the problem needed, since the side that actually knows whether the work will take four minutes is the side doing the work.
It also opens up patterns that weren't really possible before:
- Background work that survives client disconnects. Start a task. Close your laptop. Open it tomorrow. The task is still there. Fetch the result. The bound is the server's
ttlMs: once it elapses the server may mark the taskfailedand delete it. - Multi-step approvals. The task pauses at
input_required, the user gets a notification in their host UI, they answer, the task continues. - Cancellation that actually works. Previously, cancelling a tool call meant ignoring its result. Now there's an explicit
cancelledstate and a way for the server to clean up. Cancellation is cooperative:tasks/cancelsignals intent, and the server decides whether to honour it. A cancelled task may still run to completion. - Resumable interactions across sessions, devices, or even different clients of the same MCP server.
These aren't new ideas in distributed systems. They're new in protocols designed for AI agents, and that's where the leverage is.
What This Means for Tool Design
The introduction of Tasks doesn't mean every tool should be a task. The default is still synchronous, because most tools really should be fast.
But for the operations where Tasks fit, they change how you should think about tool design:
1. Stop returning fake "job started" messages. If your tool kicks off background work, return a task handle. The client knows what to do with one. It doesn't know what to do with a string that says "Started job 47, check back later." Return one only to a client that declared the extension. A handle looks like this:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"resultType": "task",
"taskId": "786512e2-9e0d-44bd-8f29-789f320fe840",
"status": "working",
"ttlMs": 60000,
"pollIntervalMs": 5000
}
}
The server must create the task durably before sending that response, so the taskId is safe to store.
2. Stop polling from the model side. A model that calls get_status in a loop is burning your token budget and not actually waiting on anything useful. With Tasks, polling drops to a layer the model doesn't need to think about, and the pacing isn't guesswork. Each response can carry a suggested pollIntervalMs (pollInterval in 2025-11-25), which clients should respect and which the server can change over the life of the task. The official extension support matrix does not list Tasks yet, so check your host before you design around it.
The exchange a server designs for:
Every arrow starts at the client, so a dropped connection costs one poll.
3. Use input_required instead of inventing your own approval dance. I have seen MCP servers implement approval flows by returning a tool result that says "Please call approve_deployment(approval_token=...) next." This is, charitably, a workaround. A task that transitions to input_required and collects the answer through the protocol (elicitation in 2025-11-25, tasks/update in the extension) is the protocol-native version.
4. Design your tool's granularity around the task boundary. "Run database migration" is one task with several phases. "Run database migration phase 1, then phase 2, then phase 3" is three separate tools that the model has to coordinate, badly. Tasks let you fold internal complexity inside a single logical operation.
Where It's Going
Tasks were marked experimental in 2025-11-25, and that word earned its keep: the design was replaced inside a year. SEP-2663 redesigned the API in the extension: polling via tasks/get, client input via tasks/update, cancellation via tasks/cancel, and the blocking tasks/result method dropped. tasks/list went too, because safe listing needs to know which caller owns which task, and SEP-2567 had just removed sessions, the only scope a server could bind them to on its own. If you wrote against the experimental version, that is your migration.
But the direction is clear. Statelessness has already arrived: 2026-07-28 removed sessions (SEP-2567) and the initialize handshake (SEP-2575), so every request now carries its protocol version and client capabilities in _meta. The roadmap points next at agent coordination and longer-lived interactions, and Tasks sit in the first of its five priority areas.
| 2026 roadmap priority area | What it covers |
|---|---|
| 1. Agentic Messaging Primitives | Tasks, subscriptions/listen and progress notifications, made to compose. Continued work on Tasks aims at folding the extension into the core protocol. |
| 2. HTTP-Native Transport Unification and Hardening | One transport model: Streamable HTTP spoken over stdio, and caching extended from ttlMs and cacheScope towards ETags. |
| 3. Agent Identity and Enterprise-Ready Security | DPoP, workload identity federation, and delegation for agents acting for a user who is not at the keyboard. |
| 4. Improved Primitives | The shape of tools/call results, progressive discovery of tools, and annotations on results and resources. |
| 5. Improved SDK Developer Experience | The extension contract, and SDKs generated from the specification and checked against the conformance suite. |
Only the first area names Tasks, and it treats them as unfinished business. All of that needs a vocabulary for "this is going to take a while." Tasks give the protocol that vocabulary for the first time.
There's a deeper architectural point hiding in here. Up until Tasks, MCP encoded one specific kind of interaction: a human (or a model) initiates a step, something happens immediately, a result comes back. That's the shape of a conversation. With Tasks, the protocol can also encode the shape of an operation, something that has its own lifecycle, independent of any particular conversation turn.
Agents, especially multi-agent systems, fundamentally need both. A coordinator agent should be able to dispatch a long-running task to a worker agent, get a handle, do other things, and check back later. That's how real systems are built. Tasks are MCP catching up to that reality.
The Counterintuitive Bit
Here's the thing about Tasks that surprised me when I sat with it.
The most useful Task is not the one that runs for ten minutes. The most useful Task is the one that runs for fifteen seconds.
Long operations (minutes, hours, days) are obvious candidates. Everyone agrees those should be tasks.
But there's a vast middle ground of "long enough to be awkward, short enough that you didn't bother engineering for it." A tool that takes 8-12 seconds. A tool that takes 25 seconds on a bad day. A tool that has to call an external API whose latency is 95th-percentile-bimodal. These are the ones that quietly poison user experience. They're too short to engineer like a job. They're too long to ignore.
The three bands, and how each was handled:
| How long the tool takes | Before Tasks | With Tasks |
|---|---|---|
Under a second: search_customers, read_file | return synchronously | return synchronously, unchanged |
| 8 to 25 seconds: an external API with bimodal latency | hope it lands inside the timeout | the server returns a task handle when it decides the call will be slow |
| Minutes to hours: deployments, CI runs, terabyte scans | a hand-rolled job ID and a get_job_status tool for the model to poll | the same task handle, on the same client code path |
Tasks let you handle the "awkwardly long" case the same way you handle the "obviously long" case. That uniformity is the actual win. You stop deciding, per tool, which side of the synchronous/asynchronous fence to land on. You just return a task handle when it makes sense, and the client figures out the rest.
Reading the Tea Leaves
If you want to know which direction MCP is moving, the addition of Tasks is one of the strongest signals you'll find. The protocol started as a way to expose tool calls to AI models. It's becoming a way to coordinate work with AI models. Those are different products. The transition is mostly silent, but Tasks are one of the loud moments.
Two more developments to watch in the same direction:
- The Agents Working Group. It owns Tasks, and its charter says what it is there for: stabilising the
io.modelcontextprotocol/tasksextension and promoting it into the core MCP protocol. - The transport work. HTTP-Native Transport Unification and Hardening is the second roadmap area, and it already touches Tasks. Sending
tasks/get,tasks/updateortasks/cancelover Streamable HTTP means setting theMcp-Nameheader to the task ID, so load balancers can route the call to the instance holding that task's state.
The 2025-11-25 spec is the first time MCP felt less like an RPC and more like an orchestration substrate. Tasks are the centerpiece of that shift.
If you're building Spring AI + MCP servers and want a deeper look at long-running operations, asynchronous tool composition, and the patterns that surround Tasks, the MCP Architecture Patterns course has a thread of these patterns running through it. It also covers resilience, retries and idempotency, which is making a repeated call safe so that a retry cannot deploy twice. That course is coming soon.
If you want to understand the full evolution of the spec from the first 2024 release to today, the MCP Ecosystem module walks through every revision and what it added.
Either way: MCP stopped watching the clock. Your tools can finally take as long as they actually take.
Further Reading
- MCP Tasks extension: overview: the current design in one page, with the six-step flow, the five statuses, and separate checklists for client and server authors.
- SEP-2663: Tasks Extension: the proposal that flipped the design, including the three implementation problems it found in the 2025-11-25 version and the full normative text.
- MCP specification 2025-11-25: Tasks: the experimental version this post describes, still online, with the capability tables,
execution.taskSupportandtasks/result. - SEP-1686: Tasks: the original proposal, for the reasoning behind the requestor-driven design before you compare it with SEP-2663.
- modelcontextprotocol/ext-tasks: the extension's own repository, holding the versioned specification and the TypeScript and JSON schema an implementer builds against.
- MCP roadmap: the five current priority areas, the Core Maintainers named for each, and where Tasks sit among them.
- Agents Working Group charter: the group that owns Tasks, its leads, and its stated goal of promoting the extension into the core protocol.
- Extension support matrix: which clients implement which official extensions, so you can check whether Tasks has landed anywhere you ship to.
- Elicitation (2025-06-18): the mechanism
input_requiredleans on, if you have not met elicitation before.
Sources
- MCP specification 2026-07-28: changelog: that SEP-2663 moved tasks into the
io.modelcontextprotocol/tasksextension, replacedtasks/resultwithtasks/getplustasks/update, removedtasks/list, and that SEP-2567 and SEP-2575 removed sessions and the handshake in this revision. - SEP-2663: Tasks Extension: the quoted sentence about the server being the sole decider, the removal of client-hosted sampling and elicitation tasks, cooperative cancellation, the
ttlMsandpollIntervalMsrules, theMcp-Namerouting header, and the reasontasks/listhad to go. - MCP specification 2025-11-25: Tasks: the five lifecycle states and their legal transitions, the
taskscapabilities in each direction, theexecution.taskSupportvalues, and thepollIntervalfield name. - MCP specification 2025-11-25: changelog: that Tasks arrived in 2025-11-25 as experimental support under SEP-1686.
- MCP Tasks extension: overview: that a client declares the extension inside every request's
_meta, that the server advertises it throughserver/discover, and that a server returns a task only to a client that declared support. - Versioning: that 2026-07-28 is the current protocol version.
- MCP roadmap: the names of the five priority areas, and the statement that continued work on Tasks aims at eventual inclusion of the extension in the core protocol.
- Agents Working Group charter: the group's Tasks scope, stabilising the extension and promoting it into the core protocol.
- MCP specification 2025-06-18: changelog: that elicitation landed in the 2025-06-18 revision.
- Extension support matrix: that the official client support matrix still omits Tasks, read on 2026-09-06.