MCP Tasks in 2026: build long-running, resumable agent tools

Build long-running, resumable MCP tools with the Tasks extension in the 2026-07-28 spec: the lifecycle, server and client steps, and code.

Read time
16 min
Word count
2.7K
Sections
13
FAQs
8
Share
Concentric light rings around a glowing node in a dark data-center scene
Long-running MCP tools return a durable task handle instead of blocking the connection.
On this page · 13 sections
  1. Why blocking tool calls break at 30 seconds
  2. What the Tasks extension is
  3. The task lifecycle, status by status
  4. Server side: return a task from tools/call
  5. Client side: poll, resume, and persist the task ID
  6. Mid-flight input and the input_required state
  7. Tasks, streaming, or the explicit handle pattern
  8. Migrating off the 2025-11-25 experimental Tasks API
  9. What to build today while client support catches up
  10. India-specific considerations
  11. FAQ
  12. How eCorpIT can help
  13. References

Summary. On 28 July 2026 the Model Context Protocol (MCP) publishes the 2026-07-28 specification, and one of its two official extensions, Tasks, changes how you build tools that run longer than a few seconds. MCP clients cap a single tool call at roughly 30 to 60 seconds, so a CI run, a bulk import, or a model-training pass that rents a cloud NVIDIA H100 at $1.49 to $6.98 an hour across more than 15 providers will hit that ceiling, the agent will record a failed call, and the work may keep running with no way to collect the result. Tasks removes that trap. Instead of blocking, a server answers tools/call with a durable task handle, and the client drives the rest with tasks/get, tasks/update, and tasks/cancel. The release candidate was locked on 21 May 2026, giving SDK maintainers a ten-week window before the final specification. Tasks first shipped as an experimental core feature in the 2025-11-25 release; production use forced a redesign, so it now lives in its own extension repository and versions on its own timeline. This guide walks the five lifecycle states, the exact server and client steps, mid-flight input, and the migration off the old experimental API, with code for both sides.

The change matters because the agent ecosystem has standardised on MCP fast. The protocol now runs as a Linux Foundation project, and vendors including Microsoft ship MCP servers and clients, so when the transport underneath those integrations goes stateless and gains a real async primitive on the same day, the tools you already run behind it inherit new failure modes and new options at once. Tasks is the part of the 2026-07-28 release that most directly touches anyone whose server does work measured in minutes rather than milliseconds.

Why blocking tool calls break at 30 seconds

A standard MCP tool call is request and response: the client issues tools/call, the server returns a result, and the connection carries the wait. That model holds for fast lookups and fails for anything slower, because clients enforce their own timeouts. As Lily Ma, a Feature Product Manager on Microsoft's Azure Functions team, describes it, those limits "aren't standardized by the MCP spec and vary per client, but they're often in the ~30–60 second range." Cross that line and three things happen: the client times out, the agent observes a failed call, and the underlying work may still be running with nobody waiting for its output.

You could try to hold the connection open until the work finishes. The MCP maintainers reject that path for concrete reasons. Blocking ties up a connection for the whole operation, and many transport intermediaries impose their own timeouts that make long holds impractical beyond a few seconds. A held connection has no crash resilience: if the client restarts, the in-flight work is lost. And a blocked call gives the agent no progress signal, so it cannot tell a slow success from a silent hang.

The stateless rework in the same release makes the problem sharper. Once the protocol drops sessions, as the stateless protocol core does, there is no session store to lean on for tracking half-finished work. A durable, explicitly modelled task is the replacement, and it is the piece that lets enterprise AI agents in production call slow tools without gambling on a connection staying up.

What the Tasks extension is

Tasks is an official MCP extension, identified by the reverse-DNS string io.modelcontextprotocol/tasks. Under the new extensions framework, it negotiates through an extensions map on client and server capabilities, lives in its own experimental-ext-tasks repository, and versions independently of the base specification. That independence is the point of moving it out of the core: the primitive can evolve on its own cadence without waiting for a full spec revision, the same structure that carries the sibling MCP Apps extension for server-rendered interfaces.

At runtime the shape is simple. When a server decides a request will run long, it returns a CreateTaskResult, marked by resultType: "task", instead of the normal result. That object carries a unique taskId, an initial status, a ttlMs time-to-live, and a suggested pollIntervalMs. The task is created durably before the response is sent, so the handle is valid even if the client immediately disconnects. From there the client polls, supplies input if asked, and collects the final result when the task reaches a terminal state.

Two properties are worth fixing in your head before you write code. Task creation is server-directed: the client advertises support once through its capabilities, and the server alone decides per request whether a given call becomes a task. And a server must never return a task to a client that did not advertise the extension, so capability negotiation is not optional bookkeeping, it is the gate that keeps a task-unaware client from receiving a response shape it cannot parse.

The task lifecycle, status by status

A task is a durable state machine. It carries exactly one status at a time, and three of the five states are terminal, meaning the task stops changing once it lands there.

Status Meaning Terminal
working The operation is in progress. No
input_required The server needs client input before it can continue. See inputRequests. No
completed The operation finished; the result field holds the final output. Yes
failed A JSON-RPC error occurred; the error field holds the details. Yes
cancelled The operation was cancelled, which is not always honoured. Yes

The client learns the status by calling tasks/get with the taskId. While the state is working, the client waits for the interval the server suggested and polls again. On completed, the result field contains exactly what the original request would have returned synchronously, so the calling code can treat it as if the tool had answered immediately. On failed, the error field carries a standard JSON-RPC error the agent can reason about. A cancelled task confirms the client's own cancel request was accepted, though the server is not obligated to have actually stopped the work.

Server side: return a task from tools/call

Six responsibilities fall on the server. First, advertise the extension in your server/discover capabilities so clients know Tasks is available:


            {
  "capabilities": {
    "extensions": {
      "io.modelcontextprotocol/tasks": {}
    }
  }
}
          

Second, before returning a task, verify the client actually included the extension in its per-request capabilities; if it did not, fall back to a synchronous result or an error rather than handing back a task handle it cannot drive. Third, when a request will run long, respond with a CreateTaskResult and persist the task before you reply:


            {
  "resultType": "task",
  "task": {
    "taskId": "5b1e9c7a-0d3f-4a2b-9c11-8f2e6d4a7c90",
    "status": "working",
    "ttlMs": 3600000,
    "pollIntervalMs": 2000
  }
}
          

Fourth, serve tasks/get: return the current state on each poll, and include the result field on completed or the error field on failed. Fifth, handle tasks/update by accepting inputResponses keyed to any outstanding inputRequests, acknowledging with an empty result, and ignoring responses for keys you do not recognise or have already satisfied. Sixth, handle tasks/cancel: acknowledge the request with an empty result and stop the work when you can, remembering that cancellation is cooperative and a task may still reach a non-cancelled terminal state.

The durability requirement in step three is the one teams underestimate. The specification says the task must be durably created before the response is sent, which means a task record in a store that survives a process restart, not an in-memory map. If your server crashes after replying with a taskId but before persisting it, the client holds a handle to nothing.

Client side: poll, resume, and persist the task ID

The client contract has five parts. Declare support by putting the extension in your per-request capabilities under _meta:


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

Then handle a polymorphic result: when you issue a supported request such as tools/call, be ready to receive either the ordinary result or a CreateTaskResult with resultType: "task". Poll for completion by calling tasks/get with the returned taskId, respecting the pollIntervalMs the server sent, and continue until the status is completed, failed, or cancelled. Handle input requests when the status is input_required by reading the inputRequests map and answering through tasks/update. Finally, persist task IDs durably so polling can resume after a client crash or restart.

A minimal poll loop in Python-flavoured pseudocode makes the flow concrete:


            result = call_tool("train_model", args, capabilities=TASKS_CAP)

if result.get("resultType") == "task":
    task = result["task"]
    save_task_id(task["taskId"])          # durable, survives a restart
    interval = task.get("pollIntervalMs", 2000) / 1000

    while True:
        state = tasks_get(task["taskId"])
        status = state["status"]
        if status == "completed":
            return state["result"]
        if status == "failed":
            raise ToolError(state["error"])
        if status == "cancelled":
            return None
        if status == "input_required":
            answers = fulfil(state["inputRequests"])
            tasks_update(task["taskId"], answers)
        time.sleep(interval)
          

The single most valuable line is save_task_id. Because the handle is durable on the server, a client that stored the ID can restart, reconnect to any server instance under the new stateless model, and resume polling the same task. That is what turns a fragile long call into work that survives a dropped mobile connection or a redeployed client.

Mid-flight input and the input_required state

Long jobs often need a human decision partway through: approve a destructive step, pick between options, confirm a spend. Tasks models this with the input_required status. When a task needs an answer, tasks/get returns an inputRequests map that can carry an elicitation, and the client fulfils it with tasks/update.

The design ties into how the stateless core restructured server-to-client requests. Rather than holding a Server-Sent Events stream open, a server returns an InputRequiredResult that names what it needs and echoes an opaque requestState:


            {
  "resultType": "inputRequired",
  "inputRequests": {
    "confirm": {
      "type": "elicitation",
      "message": "Delete 3 files?",
      "schema": { "type": "boolean" }
    }
  },
  "requestState": "eyJzdGVwIjoxLCJmaWxlcyI6WyJhIiwiYiIsImMiXX0="
}
          

The client gathers the answers and re-issues the call with inputResponses plus the echoed requestState, and because everything the server needs travels in that payload, any server instance can pick the retry up. Server-initiated prompts may now only be issued while the server is actively processing a client request, so a user is never prompted out of nowhere, and every elicitation traces back to something they or their agent started. For teams building managed background agents that run detached from a live UI, an auditable, replayable input step is more useful than a stream that dies with the connection.

Servers that would rather push than be polled can emit notifications/tasks/status; clients opt into those through the subscriptions/listen mechanism, and each notification carries the full task state so there is no extra tasks/get round-trip. Polling stays the default and the safe baseline, and notifications are the optimisation for clients that support them.

Tasks, streaming, or the explicit handle pattern

Tasks is not the only way to model slow work, and the right choice depends on what your clients support today. The stateless core also blesses an explicit-handle pattern, where a tool mints an ordinary identifier such as a job_id and the model passes it back as a normal argument on a later call. The table below compares the options across the vectors that decide production readiness.

Decision vector Blocking call Explicit handle pattern Tasks extension
Holds a connection for the duration Yes No No
Survives a client restart or dropped network No Yes, if the agent keeps the ID Yes, the ID is a durable handle
Standard status model (working, completed, failed) No No, you hand-roll it Yes, defined by the spec
Standard mid-flight input No No Yes, via input_required
Works with today's request/response clients Yes Yes Only where the client and SDK support the extension
Client and SDK support in mid-2026 Universal Universal Still rolling out

The honest reading in July 2026 is that the explicit handle pattern is what ships now and Tasks is what you converge on as clients catch up. Lily Ma is blunt about the gap: "So while Tasks is now a defined extension, broad client and SDK support is still in progress." Clients must advertise the extension and SDKs must implement the lifecycle before a server can rely on it, which is why the extension support matrix, not your own server code, decides whether Tasks is available end to end.

Migrating off the 2025-11-25 experimental Tasks API

Tasks is not new in name. It shipped as an experimental core feature in the 2025-11-25 release, and anyone who built against that API has migration work. The redesign reshapes the lifecycle around the stateless model: a server answers tools/call with a task handle, and the client drives it with tasks/get, tasks/update, and tasks/cancel. The most visible removal is tasks/list, which is gone because it cannot be scoped safely once protocol-level sessions no longer exist, there is no session to say which tasks are "yours" to enumerate. If your client relied on listing tasks, replace it with your own durable store of the IDs you were handed.

Two other 2026-07-28 changes touch task-heavy servers. Tool schemas move to full JSON Schema 2020-12, so inputSchema and outputSchema may now use composition and conditionals, and structuredContent can be any JSON value rather than only an object. And the error code for a missing resource changes from the MCP-custom -32002 to the JSON-RPC standard -32602 Invalid Params, so any client that matched on the literal -32002 needs updating. Treat the migration as part of a wider move to the stateless transport rather than an isolated task change; the two are designed together, and the same discipline that keeps production agent frameworks stable applies here.

What to build today while client support catches up

Because Tasks depends on ecosystem support that is still landing, the pragmatic pattern for mid-2026 is a request/response design that mimics the task lifecycle and swaps in the real extension once clients are ready. Microsoft's Azure Functions team published a working version of exactly this, built on Durable Functions, a framework for authoring stateful, long-running workflows with checkpointing and recovery handled for you.

Their sample exposes two tools. A start_mining tool kicks off a Durable Functions orchestration, waits inside a configurable budget, and either returns the result inline if the work finished in time or returns a workflow_id if it is still running. A get_mining_result tool takes that workflow_id and returns the current state, such as completed, running, failed, or not_found. To keep the agent honest, workflow_id is a required parameter of get_mining_result, so the model cannot poll without having started a run, and the running response carries a poll_after_seconds value and a next instruction that tells the agent to poll again rather than assume the job is done.

The sample also exposes the weakness Tasks is built to remove. The poll path relies on the agent correctly remembering, and not hallucinating, the workflow_id it was handed; if it garbles or invents an ID, the poll lands on the wrong instance or none at all, which is why the tool returns not_found rather than guessing. Once the Tasks extension is implemented across clients and SDKs, the server returns a task handle, the client and SDK track the lifecycle, and the agent no longer has to shepherd an identifier through the conversation. Build the handle version now, keep the tool contract close to the task lifecycle, and the later swap is mechanical.

India-specific considerations

For the many product and platform teams in India shipping MCP servers to global buyers, two points carry weight. First, the durable-store requirement is an architecture decision, not a detail: a task record that survives a restart usually means a managed database or a durable workflow runtime, and that choice affects data residency under the Digital Personal Data Protection Act 2023 (DPDP) when a long-running task holds user data between the first call and the final poll. Design the store with the same residency and retention rules you would apply to any personal data, and describe that handling to buyers who ask.

Second, the ecosystem-support gap is a delivery risk to price in. A team that promises Tasks end to end in mid-2026 is betting on client and SDK support that the maintainers themselves call a work in progress, so the safe commercial commitment is the handle pattern today with a clean path to Tasks, not a hard dependency on an extension the buyer's chosen client may not yet speak. eCorpIT builds and reviews MCP server integrations on that basis, and treats the durable store and the client matrix as first-class parts of the estimate.

FAQ

How eCorpIT can help

eCorpIT designs and reviews MCP server integrations for teams adopting agentic tooling, including the durable task stores, capability negotiation, and stateless transport changes the 2026-07-28 release introduces. We build the explicit handle pattern where client support is not yet there and keep the tool contract shaped for a clean move to Tasks. To scope an MCP server or migrate an existing one, see our MCP server development and integration work or talk to our senior engineering team at /contact-us/.

References

  1. MCP Tasks, extension overview — Model Context Protocol documentation.
  1. The 2026-07-28 MCP Specification Release Candidate — David Soria Parra and Den Delimarsky, MCP Blog.
  1. experimental-ext-tasks specification repository — Model Context Protocol on GitHub.
  1. Tasks extension pull request — modelcontextprotocol repository.
  1. MCP 2025-11-25 specification — the release where Tasks shipped as an experimental core feature.
  1. Extensions overview and negotiation — how MCP extensions declare and negotiate support.
  1. Extension support matrix — client and host support for MCP extensions.
  1. How to build long-running MCP tools on Azure Functions — Lily Ma, Microsoft Azure SDK Blog, 16 July 2026.
  1. Session removal, SEP-2567 — the stateless-transport change behind the Tasks redesign.
  1. MCP Apps extension overview — the sibling official extension in the same release.
  1. Draft specification changelog — every change against 2025-11-25.
  1. H100 rental prices compared across 15+ cloud providers, 2026 — per-hour on-demand GPU rates.

_Last updated: 28 July 2026._

Frequently asked

Quick answers.

01 What is the MCP Tasks extension?
Tasks is an official Model Context Protocol extension, identified by io.modelcontextprotocol/tasks, for long-running operations. Instead of blocking a tool call, a server returns a durable task handle, and the client polls for progress with tasks/get, supplies input with tasks/update, and can stop it with tasks/cancel until the task reaches a terminal state.
02 Why did MCP move Tasks out of the core specification?
Tasks first shipped as an experimental core feature in the 2025-11-25 release. Production use surfaced enough redesign that the maintainers judged an extension the better home, so it now lives in its own repository and versions independently of the base spec. The 2026-07-28 release candidate reintroduces it as one of two official extensions.
03 What are the five task lifecycle states?
A task holds one status at a time: working, input_required, completed, failed, or cancelled. The last three are terminal, so the task stops changing once it reaches them. On completed the result field holds the output; on failed the error field holds a JSON-RPC error; a cancelled state confirms the cancel was accepted.
04 How does a client resume a task after a crash?
The task handle is durable on the server, created before the response is sent. A client that persists the taskId can restart, reconnect to any server instance under the stateless model, and resume calling tasks/get with the same ID. Storing the task ID durably is the step that makes long work survive dropped connections.
05 Can I use Tasks in production right now?
Only where both ends support it. Clients must advertise the extension and SDKs must implement the lifecycle before a server can rely on it, and in mid-2026 that support is still rolling out. The extension support matrix decides availability end to end, so many teams ship an explicit handle pattern today and move to Tasks as clients catch up.
06 What happened to tasks/list?
The 2026-07-28 redesign removes tasks/list because it cannot be scoped safely once protocol-level sessions no longer exist; there is no session to define which tasks belong to a caller. Clients that enumerated tasks should keep their own durable store of the task IDs they were handed and track state from there.
07 How is Tasks different from the explicit handle pattern?
The handle pattern has a tool mint an ordinary job_id that the model passes back as an argument, and you hand-roll the status model yourself. Tasks provides a standard lifecycle, a defined status set, and structured mid-flight input, but depends on client and SDK support. The handle pattern works with any request/response client today.
08 Does Tasks help agents on unreliable mobile connections?
Yes. Because a task ID is a durable handle rather than a live connection, a mobile client on an intermittent network can drop and reconnect without losing the work. The specification names unreliable connections as a primary use case, alongside CI pipelines, batch processing, human-in-the-loop approvals, and external job systems.

About the author

Manu Shukla

Founder & Director

Founder of eCorpIT. Hands-on engineer leading senior-only delivery for AI apps, custom software, and cloud systems for global clients.

Subscribe

One engineering note a week. No fluff, no spam.

Senior-architect playbooks on AI agents, mobile apps, cloud, security, data, and marketing — delivered every Wednesday.

Past the reading

Read enough. Let's build something.

A senior architect responds in 24 working hours with scope, indicative cost, and a timeline. NDA before any technical conversation.