cd /news/agent-protocols/mcp-tasks-long-running-work-without-… · home › topics › agent-protocols › article
[ARTICLE · art-156773] src=pournasserian.com ↗ pub= topic=agent-protocols verified=true sentiment=· neutral

MCP Tasks: long-running work without holding a connection

The Model Context Protocol's Tasks extension, identified as io.modelcontextprotocol/tasks and defined by Specification Enhancement Proposal (SEP) 2663, lets an MCP server answer a tools/call with a task handle that the client polls via tasks/get instead of holding an HTTP stream open, according to a part-26 write-up of the 2026-07-28 MCP specification. The server alone decides per request whether to return a CreateTaskResult marked resultType "task", must not return a task to a client that did not declare the extension (returning error -32021, Missing Required Client Capability), and only tools/call supports tasks today. The official Python SDK, mcp 2.3.0, does not yet implement the extension; pull request #3005 adding it was still open on 9 October 2026, so the article defines the Task type in plain Pydantic.

by read8 min views2 publishedOct 24, 2026
MCP Tasks: long-running work without holding a connection
Image: Pournasserian (auto-discovered)

In today’s Model Context Protocol (MCP), a dropped HTTP stream cancels the request, and nothing can resume it. That’s fine for a three-second call and fatal for a 20-minute report. What I like about the Tasks extension is how little it asks of the connection: the server hands back a handle, the client polls, and nothing stays connected while the work runs.

This is part 26 of my series on what an MCP server does under the 2026-07-28 specification, after building an MCP App in part 25. The facts are as I read them in October 2026.

In brief #

  1. A task is a handle in place of a result. The server answerstools/call with a task, and the client pollstasks/get until it ends.
  2. The server decides, one request at a time, whether to return a task, and it may return one only to atools/call that declared the extension.
  3. A tool result with isError: true makes a completed task, not a failed one.failed is reserved for JSON-RPC 2.0 errors.
  4. Store the task durably before you return it, give it an unguessable ID, and check authorization on every task call.
  5. The official Python software development kit (SDK) doesn’t implement Tasks yet, so the Task type below is plain Pydantic.

Why Tasks exist, and who decides #

The Tasks extension’s identifier is io.modelcontextprotocol/tasks. It’s defined by Specification Enhancement Proposal (SEP) 2663 and negotiated like the other extensions in part 23. It fits expensive computations, batch processing and work that wraps an external job application programming interface (API). Blocking isn’t a real alternative (part 16): the Tasks overview warns that many clients and transport intermediaries impose timeouts that make it impractical beyond a few seconds.

The extension also fixes the experimental tasks of the 2025-11-25 specification: a fragile opt-in at both the method and the tool level, a blocking tasks/result that forced long-lived connections, and a tasks/list that couldn’t be scoped to one caller once sessions were gone.

Negotiation leaves the choice with the server:

  • The client declares the extension in its per-request capabilities, and the server advertises it in server/discover .
  • The server alone decides, request by request, whether to return a task (a CreateTaskResult , markedresultType: "task" ) or a normal result. Declaring support doesn’t mean the client requires a task.
  • A server MUST NOT return a task to a request that didn’t declare the extension, and a declaring client MUST handle either shape. If the work can’t be done without a task and the client didn’t opt in, the server returns -32021 (Missing Required Client Capability).
  • Only tools/call supports tasks today, and implementations SHOULD expect more methods later.

The task, its states and its methods #

The task object in Python

The official Python SDK, mcp 2.3.0, doesn’t implement the extension yet: pull request #3005, which adds it, was still open on 9 October 2026. The SDK’s own mcp.types.Task is documented as 2025-11-25 only, with ttl and poll_interval, so it isn’t SEP-2663’s task. Here is that task in plain Pydantic, with the wire’s camelCase names as aliases:

from datetime import datetime
from typing import Any, Literal

from pydantic import BaseModel, ConfigDict
from pydantic.alias_generators import to_camel

TaskStatus = Literal["working", "input_required", "completed", "cancelled", "failed"]

class Task(BaseModel):
    """The Task object of the Tasks extension (SEP-2663)."""

    model_config = ConfigDict(
        alias_generator=to_camel, validate_by_name=True, serialize_by_alias=True
    )

    task_id: str                         # stable, server-generated, unguessable
    status: TaskStatus
    status_message: str | None = None    # human-readable progress or reason
    created_at: datetime                 # ISO 8601
    last_updated_at: datetime            # ISO 8601
    ttl_ms: int | None                   # required; None means unlimited
    poll_interval_ms: int | None = None  # suggested polling cadence

class TaskState(Task):
    """A tasks/get result: the task plus the field its status adds."""

    input_requests: dict[str, Any] | None = None  # input_required
    result: dict[str, Any] | None = None          # completed
    error: dict[str, Any] | None = None           # failed

Here it parses the final tasks/get reply of the flow drawn further down:

reply = """{
  "resultType": "complete", "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840",
  "status": "completed", "createdAt": "2026-10-09T10:30:00Z",
  "lastUpdatedAt": "2026-10-09T10:50:00Z", "ttlMs": 3600000, "pollIntervalMs": 5000,
  "result": {"content": [{"type": "text", "text": "Hello, Luca!"}], "isError": false}
}"""

state = TaskState.model_validate_json(reply)
print(state.status, state.ttl_ms, state.result["content"][0]["text"])

It printed:

completed 3600000 Hello, Luca!

Both ttlMs and pollIntervalMs may change during a task’s life.

Five statuses, three of them final

A task starts working, may wait at input_required for an answer, and ends completed, failed or cancelled:

Status tasks/get also returns
working Just the task
input_required inputRequests : all outstanding server-to-client requests
completed result : exactly what the original request would have returned
failed error : the JSON-RPC error, only for protocol-level failures
cancelled Just the task

Three methods and a notification

Method Purpose Response
tasks/get Poll the current state The full task with its status-specific field; a pure, idempotent read
tasks/update Supply inputResponses for outstandinginputRequests An empty acknowledgement, eventually consistent
tasks/cancel Signal the intent to cancel An empty acknowledgement, cooperative and eventually consistent
notifications/tasks Optional server push of the full task state Opt in through subscriptions/listen with taskIds

Reads and writes are separate on purpose: tasks/get stays cacheable and safe to retry, and all write semantics live in tasks/update. Here is a complete flow, with a question for the user midway:

Running tasks on the server #

What the extension asks of the server

  • Durable before acknowledging. Don’t return the task until atasks/get for it would succeed; in an eventually consistent store, wait for consistency.
  • Unique keys. AninputRequests key is unique for the task’s whole life and never reused. Outstanding requests reappear on every poll, so clients SHOULD deduplicate.
  • Eventual consistency. An update or a cancellation may be acknowledged beforetasks/get shows it. Cancellation is cooperative: the server needn’t stop, and the task may still endcompleted .
  • Expiry. OncettlMs has elapsed, a server may mark the taskfailed and delete it; “task not found” (-32602 ) for a purged task is compliant.
  • Routing. Over Streamable HTTP, clients MUST set theMcp-Name header to thetaskId on everytasks/* call, beyond the three core methods part 7 lists, so load balancers can route to the instance or partition holding the task’s state.
  • No progress or log notifications. Tasks support neithernotifications/progress nornotifications/message ; usestatusMessage .
  • No old methods.tasks/result returns-32601 , and there’s notasks/list .

If you need input before deciding to start, resolve those rounds of Multi Round-Trip Requests (MRTR, part 13) synchronously, then return the task; the two phases keep separate inputRequests key spaces. I recommend this for work that is both long and needs a confirmation, with the rule from part 14: no side effects before the final input round.

Security

A task ID may work as a bearer token, so give it enough entropy that it can’t be enumerated, and check authentication and authorization on every task-related request. Without tasks/list, no caller can learn that another’s tasks exist. And inputRequests arriving through a task get the same trust and user-experience treatment as direct elicitation.

A durable implementation

The rule above asks that tasks/get can find a task before you return it. My advice goes further: back the task store with something durable, not memory, or a restart loses every task. Separate request handling from execution with a queue and workers, and make the workers idempotent, since queues redeliver. Have them write statusMessage as they go, because it’s your only progress channel. Put together:

Finally, ask clients to persist task IDs, as the SEP says they SHOULD, so polling can resume after a crash.

When to use a task #

Situation Use
Finishes in seconds Plain tools/call
Quick, but needs a confirmation or a missing value first tools/call with MRTR
Minutes or hours, batch jobs, wraps an external job API Tasks
Long and needs input before starting MRTR rounds, then return a task
Long and needs input midway Tasks with input_required , answered bytasks/update

Because the server decides, you can return a direct result when the work is fast or cached and a task when it isn’t, without changing the tool’s contract. Client SDKs can hide the polling and expose only the final result. In part 12 I drew the line at about a minute, as a rule of thumb; the overview’s timeout warning suggests a lower bar.

Method and caveats #

  • Built from my guide’s chapter on Tasks and one pattern from its design-patterns chapter, written against the 2026-07-28 specification and SEP-2663.
  • The Python sample is plain Pydantic 2.14, installed with mcp 2.3.0, not SDK code. It printed the output shown on 9 October 2026, the day I also checked pull request #3005.
── more in #agent-protocols 4 stories · sorted by recency
── more on @model context protocol 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/mcp-tasks-long-runni…] indexed:0 read:8min 2026-10-24 · —