skip to content

Asynchronous Tasks

Long-running work returns a durable task handle: the client polls tasks/get and sends mid-flight input with tasks/update. Interviewers ask why tasks moved out of the core protocol.

part ofAPI stylesoverview, primer and where to startread it →
on this pageshow

questions

6

In MCP, what does a server return when a tools/call runs as an asynchronous task?

level: middleimportance: must knowfreq 62%

answer

  1. a receipt, not an answer
  2. the reply is immediate
  3. resultType has a third value
  4. taskId is the durable handle
  5. polled later with tasks/get

basics

~20 s

The server answers immediately with a CreateTaskResult: resultType "task" plus a taskId. The work keeps running server-side, and the client polls tasks/get with that taskId until the task reaches a terminal status and yields its result.

solid answer

~40 s

In MCP revision 2026-07-28 long-running work is handled by the `io.modelcontextprotocol/tasks` extension. Instead of holding the request open until the work finishes, the server replies to `tools/call` right away with a `CreateTaskResult` whose `resultType` is `"task"` and which carries a `taskId`. That id is a durable handle: the client uses it with `tasks/get` to read the task's current status — `working`, `input_required`, `completed`, `failed` or `cancelled` — and to collect the underlying result once it completes. Because the handle is an explicit value rather than connection state, the follow-up calls can land on any server node and do not depend on the original connection surviving. The client must have opted into the extension first; a server must not spring a task result on a client that did not.

code

json · 8 lines
json
{
  "jsonrpc": "2.0",
  "id": 7,
  "result": {
    "resultType": "task",
    "taskId": "tsk_9f2a41"
  }
}

go deeper

for a junior

Know the shape: an async MCP tool call comes back immediately with a task handle rather than the answer, and the client fetches the answer later using that handle.

for a middle

Be ready to name the pieces exactly — CreateTaskResult, resultType "task", taskId, and tasks/get for polling — and to explain that the work continues server-side while the request returns.

for a senior

Show that you understand why the handle exists: 2026-07-28 removed sessions and stream resumability, so durable work must be reachable by an explicit id from any node, after any reconnect.

for a principal

Own the operational consequence — task state must live in shared storage rather than in a process, which turns "long-running tool" into a durability, retention and cleanup decision rather than a connection-handling one.

## The problem tasks solve A `tools/call` in MCP is an ordinary JSON-RPC request. If the tool takes twenty minutes — a large build, a batch export, a deep crawl — a synchronous reply means holding an HTTP response stream or a stdio exchange open for twenty minutes. That is fragile: proxies time out, laptops sleep, load balancers drop idle connections. MCP revision 2026-07-28 is explicitly a stateless protocol in which a connection is not a conversation, so a design that depends on one connection living for the duration of the work is the wrong shape. The answer is the **Tasks extension**, identified by `io.modelcontextprotocol/tasks`. It is an optional extension, not core protocol: both sides opt in through the `extensions` capability map before a server may use it. ## The immediate reply: CreateTaskResult When the extension is in force and the server decides to run a call asynchronously, it does not block. It returns a `CreateTaskResult` straight away. Two things matter about that object: - `resultType` is `"task"`. Every result in 2026-07-28 carries a required `resultType`; `"complete"` means an ordinary finished result, `"input_required"` means the core multi-round-trip interim result, and `"task"` is the value the Tasks extension adds. A client that sees `"task"` knows the payload it wanted is not here yet. - `taskId` is the handle. It is a server-minted identifier that names this unit of work. The client therefore gets a fast, cheap response, and the connection that carried the `tools/call` can close without losing anything. ## Following the work with tasks/get The client polls `tasks/get`, naming the `taskId`. The reply reports the task's current lifecycle status. Five statuses exist: - `working` — still running; poll again later. - `input_required` — the task is blocked waiting for something from the client; the client supplies it with `tasks/update`. - `completed` — finished successfully; this is where the client collects the result the original `tools/call` was asking for. - `failed` — finished unsuccessfully. - `cancelled` — ended because cancellation was requested, cooperatively, via `tasks/cancel`. The last three are terminal. Polling is the baseline mechanism and it always works, on stdio and over Streamable HTTP alike. A server may additionally push `notifications/tasks` on a `subscriptions/listen` stream so a client does not have to poll tightly, but that is an optimisation layered on top, not a replacement for the handle. ## Why a handle rather than a session This design falls directly out of the statelessness rule introduced in 2026-07-28. Protocol-level sessions and the `Mcp-Session-Id` header were removed in that revision; cross-request state must be an explicit, server-minted identifier that the client passes back. The `taskId` is exactly that pattern applied to long-running work. Practically, it means a remote MCP server can sit behind a load balancer with no sticky routing: the `tools/call` that created the task, the `tasks/get` that polls it, and the `tasks/cancel` that stops it can each be served by a different node, as long as the task's own state lives somewhere shared. It also means a dropped connection is not a lost job. In 2026-07-28 there is no SSE resumability — no `Last-Event-ID`, no event ids — so a broken stream on an ordinary request loses the in-flight request and the client must re-issue it as a new request with a new JSON-RPC id. With a task, the work itself is not in flight on the wire; only the poll is. The client reconnects and polls again with the same `taskId`. ## What not to say An earlier task design in revision 2025-11-25 had a blocking `tasks/result` method, a `tasks/list` method, and a per-tool `execution.taskSupport` field that let a tool advertise whether it could run as a task. That design was superseded. In 2026-07-28 there is no per-tool flag — the client opts in once, at the extension level — and `tasks/result` and `tasks/list` are not part of the current extension. Describing them as current mechanics is the clearest sign a candidate learned MCP from pre-2026 material. ## Interview framing The crisp summary: an asynchronous MCP tool call returns a receipt, not an answer. The receipt is `CreateTaskResult` with `resultType: "task"` and a `taskId`; the answer is collected later through `tasks/get`. Everything else — the lifecycle statuses, `tasks/update`, `tasks/cancel`, `notifications/tasks` — hangs off that handle.

  • If the connection carrying the original tools/call drops, is the task lost?
    No. The work is not in flight on the wire — only the poll is. The client reconnects and calls `tasks/get` with the same `taskId`. That is the practical advantage over an ordinary long request, where a broken stream in 2026-07-28 has no resumability at all and the client must re-issue the whole request with a new JSON-RPC id.
  • Can a server return a CreateTaskResult to a client that never mentioned the tasks extension?
    No. Tasks are an opt-in extension in 2026-07-28, identified by `io.modelcontextprotocol/tasks`. If the client did not declare it, the server must fall back to core behaviour — answer synchronously — or reject the request. Springing an unrecognised `resultType: "task"` on such a client would leave it holding a handle it has no methods to follow.
  • How does a client tell a task result apart from a multi-round-trip interim result?
    By `resultType`. `"task"` means a `CreateTaskResult` carrying a `taskId` to poll; `"input_required"` is the core protocol's interim result, which the client resolves by retrying the original request. `"complete"` is an ordinary finished result, and a client must treat a missing `resultType` from an older server as `"complete"`.

saying these in an interview costs you the question

  • Says the client calls tasks/result to block until the task finishes
  • Claims each tool advertises task support with execution.taskSupport
  • Thinks the original connection must stay open for the task
  • Assumes tasks are core protocol and always available
  • Calls the taskId a session id or ties it to one connection

context

open as a page

In MCP's tasks extension, what task statuses exist and which of them are terminal?

level: middleimportance: should knowfreq 47%

basics

~20 s

A task is working, input_required, completed, failed or cancelled. The first two are live states the client keeps polling; completed, failed and cancelled are terminal. Cancellation is cooperative — tasks/cancel requests it, the server decides when the task actually stops.

open as a page

In MCP's tasks extension, how does a client feed input to a task already running?

level: middleimportance: should knowfreq 38%

basics

~10 s

When a task reports the input_required status, the client calls tasks/update with that taskId and supplies what the server asked for. The task then resumes. A running task is never re-issued — only fed.

open as a page

How can an MCP client learn a task advanced without polling tasks/get in a loop?

level: seniorimportance: should knowfreq 34%

basics

~20 s

A server may push notifications/tasks over a subscriptions/listen stream, so the client learns of transitions instead of polling tightly. It is optional and best-effort: a robust client still polls tasks/get after any reconnect, because a push delivered while the stream was down is simply lost.

open as a page

Why do MCP tasks live in the io.modelcontextprotocol/tasks extension rather than core?

level: seniorimportance: should knowfreq 44%

basics

~20 s

Revision 2026-07-28 kept the core protocol small and moved long-running work into the optional io.modelcontextprotocol/tasks extension. Both sides must opt in; the client does so once through its capabilities, and a server facing a client that did not must answer synchronously instead.

open as a page

When should an MCP server run a tool as a task instead of answering synchronously?

level: principalimportance: should knowfreq 30%

basics

~20 s

Use a task when the work outlives what a single request can safely hold open — minutes, human approvals, or anything that must survive a dropped connection. Keep short, cheap, idempotent calls synchronous: a task adds durable state, polling and a retention policy nobody needs for a two-second query.

open as a page