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. MCP Tasks: long-running work without holding a connection 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 https://pournasserian.com/writing/mcp-2026-apps-building . 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 answers tools/call with a task, and the client polls tasks/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 a tools/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 https://modelcontextprotocol.io/seps/2663-tasks-extension and negotiated like the other extensions in part 23 https://pournasserian.com/writing/mcp-2026-extensions . 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 https://pournasserian.com/writing/mcp-2026-progress-and-cancellation : the Tasks overview https://modelcontextprotocol.io/extensions/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 https://pournasserian.com/writing/mcp-2026-server-discover . - The server alone decides, request by request, whether to return a task a CreateTaskResult , marked resultType: "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 https://github.com/modelcontextprotocol/python-sdk/pull/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: python 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 .""" JSON in and out uses the wire names: taskId, ttlMs 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 outstanding inputRequests | 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 https://pournasserian.com/writing/mcp-2026-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 a tasks/get for it would succeed; in an eventually consistent store, wait for consistency. - Unique keys. An inputRequests 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 before tasks/get shows it. Cancellation is cooperative: the server needn’t stop, and the task may still end completed . - Expiry. Once ttlMs has elapsed, a server may mark the task failed and delete it; “task not found” -32602 for a purged task is compliant. - Routing. Over Streamable HTTP, clients MUST set the Mcp-Name header https://pournasserian.com/writing/mcp-2026-transports to the taskId on every tasks/ 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 neither notifications/progress nor notifications/message ; use statusMessage . - No old methods. tasks/result returns -32601 , and there’s no tasks/list . If you need input before deciding to start, resolve those rounds of Multi Round-Trip Requests MRTR, part 13 https://pournasserian.com/writing/mcp-2026-multi-round-trip-requests 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 https://pournasserian.com/writing/mcp-2026-request-state : 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 https://pournasserian.com/writing/mcp-2026-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 by tasks/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 https://pournasserian.com/writing/mcp-2026-choosing-a-primitive 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.