# Tasks (extension)

Canonical URL: https://vercel-mcp-reference.vercel.app/internals/tasks/
Markdown: https://vercel-mcp-reference.vercel.app/internals/tasks.md
Audience: engineer, architect, security. MCP spec version: 2026-07-28. Last reviewed: 2026-08-26. Status: stable.

> **Now an official extension, redesigned on the way out of core.** Tasks were introduced in MCP 2025-11-25 as an experimental core utility. The 2026-07-28 revision moves them out of the core protocol into the official **`io.modelcontextprotocol/tasks`** extension and redesigns the model: polling via `tasks/get` replaces the blocking `tasks/result`, a new `tasks/update` carries client-to-server input, `tasks/list` is removed, and servers may return task handles unsolicited (SEP-2663). This page describes the extension as published; the [changes from the 2025-11-25 core design](#what-changed-from-the-2025-11-25-core-design) are listed below because SDKs and clients that still speak the old shape are in the wild.

## Plain-language explanation

**TL;DR:** A **task** turns a slow MCP request into a **durable, pollable** one. Instead of holding the connection open until the work finishes, the [server](https://vercel-mcp-reference.vercel.app/glossary/#server) immediately returns a **`CreateTaskResult`**: a `taskId` plus status, marked `resultType: "task"`. The [client](https://vercel-mcp-reference.vercel.app/glossary/#client) then **polls** with `tasks/get` until the task reaches a terminal status, at which point the `tasks/get` response itself carries the final result (or error). If the task needs something from the user mid-flight, it parks in `input_required` and the client answers via `tasks/update`. It is the protocol-level mechanism for exactly the shape the [async-jobs pattern](https://vercel-mcp-reference.vercel.app/patterns/async-jobs/) hand-rolls.

Tasks exist because some operations are too slow to block on: an expensive computation, a batch job, a call to an external job API. On Vercel the pressure is sharper still, because a [Vercel Function](https://vercel-mcp-reference.vercel.app/glossary/#vercel-function) invocation has a hard `maxDuration` budget; "hold the connection open and hope" is not merely fragile there, it has a deadline. Without tasks you invent your own handle-and-poll convention per server. The extension standardizes the handle, the polling, the input round-trip, and the lifecycle.

Two design decisions changed with the move to an extension, and both fit the stateless 2026-07-28 revision. First, tasks are **server-directed**: the client opts in once via the extension capability, and the *server* decides per request whether to return a task instead of a direct result; there is no per-request `task` flag and no per-tool warmup. Second, tasks are now **client-polls-server only**: the [stateless revision](https://vercel-mcp-reference.vercel.app/internals/spec-2026-07-28/) removed server-initiated requests entirely, so the 2025-11-25 notion of a server task-augmenting a `sampling/createMessage` it sends to the client no longer has anything to attach to.

## Formal protocol perspective

**Negotiation.** The tasks extension uses the standard 2026-07-28 extension mechanism: the client includes `io.modelcontextprotocol/tasks` in the `extensions` field of the `io.modelcontextprotocol/clientCapabilities` it sends in each request's `_meta`; the server advertises the same key in the `extensions` of the capabilities it returns from `server/discover`. A server **must not** return a task to a client that did not declare support.

**Task creation.** In response to a supported request (for example `tools/call`), the server may return a `CreateTaskResult`, identified by `resultType: "task"`, containing a `Task` object: a unique `taskId`, the initial status, `ttlMs` (retention), and `pollIntervalMs` (suggested polling cadence). The task is durably created *before* the response is sent, so the handle the client receives is always redeemable.

**The three operations:**

- `tasks/get`: poll a task's status (clients respect the returned `pollIntervalMs`). The response carries the current `Task`; for terminal states it also carries the outcome inline: a `result` field (what the original request would have returned synchronously) on `completed`, or an `error` field (the JSON-RPC error) on `failed`.
- `tasks/update`: supply `inputResponses` keyed to the outstanding `inputRequests` of a task in `input_required`. The server acknowledges with an empty result and ignores responses for unknown or already-satisfied keys.
- `tasks/cancel`: request cancellation at any time. Cancellation is **cooperative**: the server acknowledges the intent but is not obligated to stop the work, and the task may still reach a terminal status other than `cancelled`.

**Mid-flight input.** When the task needs something (typically the moral equivalent of an [elicitation](https://vercel-mcp-reference.vercel.app/client-side/elicitation/)), it moves to `input_required` and the `tasks/get` response includes an `inputRequests` map. The client presents those to the user or model and answers via `tasks/update`. This is the task-shaped sibling of the [MRTR](https://vercel-mcp-reference.vercel.app/internals/spec-2026-07-28/) retry loop: in both, the server *returns* its questions instead of initiating a request.

**Notifications.** Servers can push status updates via `notifications/tasks`, delivered through the `subscriptions/listen` mechanism, each carrying the full task state. Polling remains the default; notifications are an optimization, not a requirement.

## Lifecycle

```mermaid
stateDiagram-v2
    [*] --> working: CreateTaskResult returned
    working --> input_required: server needs input
    input_required --> working: tasks/update with inputResponses
    working --> completed
    working --> failed
    working --> cancelled: tasks/cancel honored
    input_required --> completed
    input_required --> failed
    input_required --> cancelled: tasks/cancel honored
    completed --> [*]
    failed --> [*]
    cancelled --> [*]
```

The statuses are `working`, `input_required`, `completed`, `failed`, and `cancelled`. `completed`, `failed`, and `cancelled` are **terminal**: once reached, the task's state does not change. `failed` means a JSON-RPC error occurred during execution and the `error` field has the details; `completed` means the `result` field contains the final output.

## Request / result flow

```mermaid
sequenceDiagram
    participant C as Client
    participant S as Server
    C->>S: tools/call with tasks extension capability in _meta
    S-->>C: CreateTaskResult with resultType task, taskId, working
    loop poll until terminal, respect pollIntervalMs
        C->>S: tasks/get with taskId
        S-->>C: Task status
    end
    Note over C,S: server needs user input
    S-->>C: Task status input_required with inputRequests
    C->>S: tasks/update with inputResponses
    S-->>C: acknowledged
    C->>S: tasks/get with taskId
    S-->>C: Task completed with result inline
    Note over S: after ttlMs the task may be purged
```

## What changed from the 2025-11-25 core design

If you last read this page (or an SDK) against 2025-11-25, recalibrate:

- **Opt-in moved from the request to the capability.** The per-request `task` augmentation field and the per-tool `execution.taskSupport` declaration are gone; the client declares the extension once per request in `_meta` capabilities, and the server decides when to return a task, including **unsolicited**.
- **`tasks/result` is gone.** There is no blocking retrieval call; the terminal `tasks/get` response carries the `result` or `error` inline.
- **`tasks/update` is new.** Under the core design, `input_required` surfaced a nested server-initiated elicitation or sampling request; those no longer exist, so input flows client-to-server through `tasks/update`.
- **`tasks/list` is removed.**
- **Field renames**: `ttl` is now `ttlMs`, `pollInterval` is now `pollIntervalMs`, and `CreateTaskResult` is identified by `resultType: "task"`.
- **Directionality collapsed.** The requestor/receiver framing (either side could create tasks on the other) is gone with server-initiated requests; the client polls the server, full stop.

## Common misconceptions

- **`CreateTaskResult` contains the answer.** No: it contains the `taskId`, status, `ttlMs`, and `pollIntervalMs`. The answer arrives in a later `tasks/get` response, once the status is terminal.
- **There is a `tasks/result` call to fetch the outcome.** Not anymore: that was the 2025-11-25 core design. Under the extension, the terminal `tasks/get` response carries the outcome inline.
- **The status notification can be relied on.** No: `notifications/tasks` rides the opt-in `subscriptions/listen` stream and support varies; polling `tasks/get` is the default and always works.
- **`tasks/cancel` stops the work.** Not necessarily: cancellation is cooperative. The server acknowledges the intent; the task may still land on `completed` or `failed`.
- **A failed tool call is a protocol error.** No: a task whose underlying work produced a JSON-RPC error lands in `failed` with the `error` field populated; a tool that ran and returned `isError: true` is a `completed` task whose `result` carries that tool-level error. The two layers stay distinct, exactly as in a direct call.
- **Tasks make a serverless server durable by themselves.** No: tasks standardize the *conversation about* background work. The work, and the task state, still need somewhere to live that outlives a function invocation. See the Vercel reality section below.

## Debugging notes

- **Task not found on `tasks/get`** - the server may have purged it after `ttlMs`, or the `taskId` is from another principal's context. Treat "not found" as possibly-expired first, bug second.
- **Stuck in `input_required`** - the server is waiting on `tasks/update`. Check that you read the `inputRequests` map from the `tasks/get` response and that your `inputResponses` keys match; the server silently ignores unknown keys.
- **You returned a task and the client hung** - the client never declared `io.modelcontextprotocol/tasks` in its per-request capabilities and does not understand `resultType: "task"`. Never return a task to a client that did not opt in; fall back to a synchronous result or the [async-jobs pattern](https://vercel-mcp-reference.vercel.app/patterns/async-jobs/).
- **`cancelled` never arrives after `tasks/cancel`** - legal: cancellation is cooperative and the work may finish anyway. Poll to a terminal state rather than assuming.
- **Task vanishes between polls on Vercel** - you stored task state in process memory. A new invocation is a new (or best-effort reused) instance; [Fluid compute](https://vercel-mcp-reference.vercel.app/glossary/#fluid-compute) reuse is never a correctness guarantee. See below.

## Security implications

The `taskId` is the capability that grants access to a task's status, its result, and its input channel, so it is the security pivot:

- **Bind tasks to the authorization context.** Scope each task to the verified principal that created it, and refuse `tasks/get`, `tasks/update`, and `tasks/cancel` for tasks outside that context. On Vercel that principal comes from `withMcpAuth`, never from a tool argument; see [Where the principal comes from](https://vercel-mcp-reference.vercel.app/security/identity-and-principals/) and the [authorization checklist](https://vercel-mcp-reference.vercel.app/security/checklist/#authorization--scoping).
- **No auth context?** Then `taskId`s must be cryptographically random with enough entropy to resist guessing, and `ttlMs` should be short: an unauthenticated task id is a bearer token for the result.
- **`tasks/update` is an input surface.** It injects data into a running job; validate `inputResponses` exactly as you would tool arguments, against the schema of what was asked for.
- **Rate-limit and cap.** Limit concurrent tasks per principal and enforce a maximum `ttlMs` to prevent enumeration and resource-exhaustion attacks; clean up expired tasks; log task lifecycle events for audit. See [Monitoring and audit](https://vercel-mcp-reference.vercel.app/security/checklist/#monitoring--audit).

## What the TypeScript SDK actually ships

Verified against the installed v2 packages (`@modelcontextprotocol/server` **2.0.0** and `@modelcontextprotocol/client` **2.0.0**, the line `mcp-handler` 2.1.1 peers on) in this repo's `examples/minimal-server` (in the repository):

- **Schema types only, and of the old shape.** The packages export the *2025-11-25 core* task types (`CreateTaskResult`, `GetTaskRequest`, `GetTaskPayloadRequest`, `ListTasksRequest`, `CancelTaskRequest`, `TaskStatusNotification`, `TaskAugmentedRequestParams`, the per-tool `taskSupport` field, `RELATED_TASK_META_KEY`). Nothing of the redesigned extension exists: no `tasks/update` anywhere in the typings.
- **No runtime task API at all.** SDK v1 (1.26.0) shipped a working two-sided surface under its `experimental/tasks` export (`registerToolTask`, pluggable `TaskStore`, `callToolStream`); v2.0.0 exposes **none** of it. The server package's exports are `.`, `./stdio`, `./validators/ajv`, `./validators/cf-worker`, and `./_shims`; none is a task runtime, and there is no task method on `McpServer` or `Client`.

Wire status: the pinned stack (`mcp-handler` 2.1.1 on `@modelcontextprotocol/server` 2.0.0) serves the 2026-07-28 contract natively over Streamable HTTP and falls back to stateless 2025-11-25 Streamable HTTP for legacy clients; the SDK `Client` defaults to that legacy handshake unless you opt in to modern version negotiation, so the examples' in-memory test suites exercise only the legacy path.

## Why this repo has no runnable tasks example

Two reasons, one harder than before:

1. **The released stack has no task runtime.** As verified above, SDK v2 has no server-side or client-side task runtime API, old shape or new: no `tasks/update`, no pluggable `TaskStore`, no `registerToolTask`. The wire itself is not the obstacle: the pinned handler serves 2026-07-28 natively, including `server/discover`, so a server could advertise the `io.modelcontextprotocol/tasks` extension in its capabilities and read the client's per-request `_meta` opt-in. What it could not do is run a task, so an example would have to hand-roll the extension's state machine and wire messages against typings that still describe the superseded core design, teaching idioms that match neither the SDK nor the spec.
2. **Task state needs a durable home, and that is on you.** Task state must survive across invocations: the `tasks/get` poll may land on a different invocation than the `tools/call` that created the task, and a stateless [Vercel Function](https://vercel-mcp-reference.vercel.app/glossary/#vercel-function) gives you no durable process (see [Serverless sessions](https://vercel-mcp-reference.vercel.app/internals/serverless-sessions/)). A production deployment needs a task store backed by Redis or Postgres, which would drag deployed infrastructure into a test suite this repo requires to run offline. The extension's own model agrees: the task must be *durably created* before the `CreateTaskResult` is sent.

The durable, portable shape for background work on Vercel today is the hand-rolled one: the [async-jobs pattern](https://vercel-mcp-reference.vercel.app/patterns/async-jobs/) with its runnable `examples/async-jobs-server` (in the repository), backed by Queues or Workflows and an external job store.

## Relationship to the async-jobs pattern

[async-jobs](https://vercel-mcp-reference.vercel.app/patterns/async-jobs/) is the application-level *pattern* (an opaque handle, progress, cancellation, idempotent retrieval) that servers implemented by hand before there was protocol support. **The tasks extension is the protocol formalizing that pattern**, and the 2026-07-28 redesign moved it *closer* to the hand-rolled shape: `CreateTaskResult` is the handle, `tasks/get` is the poll that also returns the outcome, `tasks/cancel` is the cancel, and there is no blocking retrieval call left to hold a connection open. Because the extension is opt-in on both sides and the released SDK does not implement it, the hand-rolled pattern remains the portable approach today; the extension is where it is heading, and it now needs negotiation (`server/discover` capabilities) rather than guesswork to adopt.

## Related

- [async-jobs pattern](https://vercel-mcp-reference.vercel.app/patterns/async-jobs/) - the hand-rolled equivalent you should ship today, with its runnable example.
- [The 2026-07-28 stateless revision](https://vercel-mcp-reference.vercel.app/internals/spec-2026-07-28/) - the revision that moved tasks out of core, and the MRTR pattern tasks now rhyme with.
- [Serverless sessions](https://vercel-mcp-reference.vercel.app/internals/serverless-sessions/) - why in-process task state does not survive on Vercel.
- [Where the principal comes from](https://vercel-mcp-reference.vercel.app/security/identity-and-principals/) - the authorization context tasks must be bound to.
- [Capability primitives](https://vercel-mcp-reference.vercel.app/internals/primitives/) - the request types a task can stand in for.

## Bibliography

- Model Context Protocol, *Tasks extension overview* - <https://modelcontextprotocol.io/extensions/tasks/overview>
- Model Context Protocol, *Extensions overview* - <https://modelcontextprotocol.io/extensions/overview>
- Model Context Protocol, *ext-tasks specification repository* - <https://github.com/modelcontextprotocol/ext-tasks>
- Model Context Protocol Specification, *Key Changes*, version 2026-07-28 (SEP-2663, tasks as an extension) - <https://modelcontextprotocol.io/specification/2026-07-28/changelog>
- Model Context Protocol Specification, *Cancellation*, version 2026-07-28 - <https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/cancellation>
- Model Context Protocol Specification, *Progress*, version 2026-07-28 - <https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/progress>
- Model Context Protocol TypeScript SDK, source repository - <https://github.com/modelcontextprotocol/typescript-sdk>
- mcp-handler, source repository - <https://github.com/vercel/mcp-handler>
