Skip to main content

The Day MCP Stopped Watching the Clock

· 19 min read
TheMCPGuy
MCP Developer & Educator

Series cover. A stopwatch reads thirty seconds. A red row shows a tool call timing out at that mark, and a green row shows the same call returning a task handle that is polled until it completes. The strapline reads: return a handle, let the work finish.

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:

RevisionWhat it did for long-running work
2025-06-18elicitation arrives: a server can ask the user a question while it is handling a request
2025-11-25Tasks arrive as experimental, and the client decides which requests become tasks (SEP-1686)
2026-07-28Tasks 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.

Update (2026-08-02): Tasks moved, and the design flipped

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 taskthe client, per request, with a task field in the paramsthe server, per call, at its own discretion
How support is declaredtasks capabilities at initialization, plus execution.taskSupport per tool in tools/listthe extension identifier in io.modelcontextprotocol/clientCapabilities, in every request's _meta
Fetching the resulttasks/result, which blocks until the task is terminaltasks/get, whose terminal response carries the result or the error
Mid-flight inputelicitation side-channelled over an SSE stream held open by tasks/resultinputRequests in the tasks/get response, answered with tasks/update
Listing taskstasks/list, where the receiver declared itremoved
Requests that can become taskstools/call, plus client-hosted sampling/createMessage and elicitation/createtool calls only
Suggested poll pacingpollIntervalpollIntervalMs

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 task failed and 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 cancelled state and a way for the server to clean up. Cancellation is cooperative: tasks/cancel signals 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 areaWhat it covers
1. Agentic Messaging PrimitivesTasks, 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 HardeningOne transport model: Streamable HTTP spoken over stdio, and caching extended from ttlMs and cacheScope towards ETags.
3. Agent Identity and Enterprise-Ready SecurityDPoP, workload identity federation, and delegation for agents acting for a user who is not at the keyboard.
4. Improved PrimitivesThe shape of tools/call results, progressive discovery of tools, and annotations on results and resources.
5. Improved SDK Developer ExperienceThe 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 takesBefore TasksWith Tasks
Under a second: search_customers, read_filereturn synchronouslyreturn synchronously, unchanged
8 to 25 seconds: an external API with bimodal latencyhope it lands inside the timeoutthe server returns a task handle when it decides the call will be slow
Minutes to hours: deployments, CI runs, terabyte scansa hand-rolled job ID and a get_job_status tool for the model to pollthe 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/tasks extension 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/update or tasks/cancel over Streamable HTTP means setting the Mcp-Name header 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.taskSupport and tasks/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_required leans on, if you have not met elicitation before.

Sources

  • MCP specification 2026-07-28: changelog: that SEP-2663 moved tasks into the io.modelcontextprotocol/tasks extension, replaced tasks/result with tasks/get plus tasks/update, removed tasks/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 ttlMs and pollIntervalMs rules, the Mcp-Name routing header, and the reason tasks/list had to go.
  • MCP specification 2025-11-25: Tasks: the five lifecycle states and their legal transitions, the tasks capabilities in each direction, the execution.taskSupport values, and the pollInterval field 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 through server/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.