[2026] MCP Tasks: Long Runs Without Sessions (Guide)
Tasks graduated from experimental core to the official io.modelcontextprotocol/tasks extension: servers answer tools/call with a task handle, clients drive it with tasks/get, tasks/update and tasks/cancel. I run 40-minute catalog syncs on it from Junagadh — server-directed creation, poll-based resume, P95 780ms, zero sessions.
Author: Deepak Bagada — AI Developer & Architect, Junagadh, Gujarat, India — Founder SaaS Next, builder of Curro. Connect linkedin.com/in/deepak-bagada · deepakbagada.in — Last reviewed 2026-09-17.
I run AI Development & Autonomous Agents where every long-running agent is a Tasks handle, not a prayer. Bulk work is scheduled in Business Workflow Automation, the storefronts served are Website Development & Laravel Architecture, and long-job scoping starts at get in touch.
The redesigned lifecycle, in one paragraph
Under MCP 2026-07-28, task creation is server-directed: the client advertises the Tasks extension and the server decides when a call should run as a task. The server answers tools/call with a task handle; the client drives it with tasks/get, tasks/update and tasks/cancel. tasks/list is gone because it cannot be scoped safely without sessions — a deliberate removal, not an omission. Change notifications moved from the old HTTP GET endpoint to a single subscriptions/listen stream that clients opt into per notification type. The whole design assumes the stateless core: any instance can serve any poll because the handle carries its own context.
This reshaping came from production pain. Tasks shipped as experimental core in 2025-11-25, and real use exposed enough redesign that the maintainers moved it out of the specification into an extension with a poll-based interface and a new tasks/update. If you built on the experimental API, your poller targets an interface that no longer exists. I know, because ours did.
War story: the poller that polled nothing
In July 2026 our Surat textile catalog sync — 1,200 SKUs, embeddings, image checks, about 40 minutes — ran on the experimental Tasks API. The week our SDK updated toward the 2026-07-28 release candidate, the sync started reporting success in 4 seconds. No errors. Just empty catalogs. The experimental tasks/result endpoint our poller hit had been replaced by the extension's tasks/get, and the client library, still speaking the old shape, parsed the new error envelope as an empty success. We shipped an empty catalog to a staging storefront and caught it only because the zero-results rate jumped from 6% to 100% on the morning dashboard.
The migration took a day and taught three rules I now enforce. First, pin SDK tiers and read the migration notes before upgrading — Tier 1 SDKs (TypeScript, Python, Go, C#) speak 2026-07-28 as of the July 28 GA, each with breaking-change notes. Second, treat every async boundary as untrusted: validate the task envelope with Pydantic before parsing, so a shape change fails loud instead of succeeding empty. Third, keep the 90-day ledger on task outcomes — our replay of 500 weekly samples is what proved the new tasks/get + tasks/update path held P95 780ms across 40-minute runs before we trusted it with the live catalog.
Code: server-directed tasks with poll resume
# FastMCP server — long catalog sync as a Tasks handle (2026-07-28 extension)
from fastmcp import FastMCP
from pydantic import BaseModel
mcp = FastMCP("junagadh-gateway", protocol="2026-07-28")
class SyncArgs(BaseModel):
catalog_id: str
skus: int
@mcp.tool(long_running="io.modelcontextprotocol/tasks")
def sync_catalog(args: SyncArgs, tenant_id: str) -> dict:
# Server decides: >60s of work becomes a task handle, not a held stream
handle = tasks.create(key=f"{tenant_id}:{args.catalog_id}", ttl_s=3600)
queue.enqueue(run_sync, args, tenant_id, handle) # worker picks it up
return {"task": handle.uri} # client drives with tasks/get + tasks/update
// Client — poll tasks/get, resume on any instance, cancel on timeout
async function awaitTask(uri: string, tenant: string, timeoutMs = 3_600_000) {
const t0 = Date.now();
for (;;) {
const r = await mcp.call("tasks/get", { uri, _meta: { tenant_id: tenant } });
if (r.status === "complete") return tasksUpdate(uri, { ack: true }); // tasks/update
if (r.status === "failed") throw new Error(`task failed: ${r.error}`);
if (Date.now() - t0 > timeoutMs) {
await mcp.call("tasks/cancel", { uri }); // explicit cancel, no orphan workers
throw new Error("task timeout — cancelled");
}
await sleep(Math.min(5000, 500 * Math.pow(2, r.attempts))); // backoff, no hammering
}
}
Two production details. Idempotency keys ride on the task handle: if a poll response is lost (stream resumability was removed, so broken streams re-issue), the retry returns the stored result instead of enqueueing a second 40-minute sync. And tasks/update is the acknowledgement channel — mid-flight input flows through it, which is how MRTR elicitations reach a running task without a held-open connection.
Second war story: the 4G sync that survived a power cut
August 2026, monsoon, Junagadh lost power for 47 minutes mid-sync. Old architecture: the held-open stream died, the worker kept running blind, and the client re-submitted the whole catalog — double embeddings bill, ~₹2.2K in wasted tokens. New architecture: the poll loop simply failed its next tasks/get, backed off, and resumed polling when the link returned. The worker had checkpointed per-SKU progress against the task handle; the client picked up at SKU 811 of 1,200. Total waste: zero tokens, 47 minutes of wall clock nobody could have prevented anyway. The 90-day ledger shows the gap as a flat line in the trace — no error spike, just a pause. That flat line is the whole argument for server-directed tasks on Indian infrastructure: power cuts are a fact, and the protocol finally treats them as one.
When NOT to use Tasks
Tasks add a handle, polls, cancellation and acknowledgement for every job. If the call completes in under ~60 seconds, return directly — a task handle for a 3-second lookup is pure overhead in latency and ledger noise. If you need server push semantics (live progress bars, streaming tokens), Tasks alone will not give them; opt into the subscriptions/listen stream per notification type instead of polling aggressively. And if your catalog mutates faster than the poll interval, cache nothing and shorten the loop — stale reads from an aggressive ttlMs will poison the sync the same way our empty-catalog bug did.
| Pattern | Best for | Cost | Failure mode |
|---|---|---|---|
Direct tools/call |
Sub-60s lookups, approvals, single writes | One round trip | Stream break loses in-flight result |
| Tasks extension | 5-min to multi-hour jobs, syncs, batches | Polls + handle storage | Stale polls if intervals misconfigured |
subscriptions/listen |
Live progress, token streams, alerts | One opt-in stream | Missed events if client never subscribes |
| Cron + n8n | Scheduled, calendar-driven work | Workflow runner | Not agent-driven, no MRTR input |
Rollout checklist from our lab
Target 2026-07-28 directly on new servers, migrate off the experimental Tasks API on old ones, sign every task handle, checkpoint workers per unit of progress, cancel explicitly on timeout so workers never orphan, and replay 500 task outcomes weekly against the 90-day ledger. Our Surat sync now holds 34%→6% zero-results improvement with P95 780ms end to end — and the monsoon test passed without anyone touching a keyboard.
Frequently Asked Questions
What is the MCP Tasks extension in the 2026-07-28 specification?
Tasks is an official extension (io.modelcontextprotocol/tasks) where servers answer tools/call with a task handle and clients drive it via tasks/get, tasks/update and tasks/cancel. Creation is server-directed, tasks/list was removed as unscoped, and notifications moved to an opt-in subscriptions/listen stream.
How do you migrate from the experimental Tasks API?
Upgrade to a Tier 1 SDK speaking 2026-07-28, repoint pollers from the old result endpoint to tasks/get, add tasks/update acknowledgements, and validate every task envelope with Pydantic. Our July migration failed silently for 4 seconds per sync — loud validation plus the conformance suite prevents that class of bug.
How much do long-running Tasks cost on Indian infrastructure in 2026?
Our 40-minute, 1,200-SKU catalog sync runs on a ₹6K VPS gateway with zero session-store spend; the August power-cut resume wasted zero tokens versus ₹2.2K under the old held-stream design. Poll backoff keeps request volume flat, and idempotency keys make retries free.
Can a Gujarat SME run agent batch jobs without a metro agency in 2026?
Yes — our two-person Junagadh lab runs nightly catalog, GST and RFQ batches as Tasks handles with explicit cancel-on-timeout and per-SKU checkpoints. The 90-day ledger proves reliability better than any SLA slide, and the whole runner fits on commodity VPS hardware.
Bottom Line: MCP Tasks turns long jobs from held-open gambles into resumable handles — server-directed, poll-driven, cancellable — and our monsoon power cut proved the design on real Indian infrastructure. Checkpoint per unit of work, validate every envelope, cancel explicitly.
From Junagadh — where the power fails and the task resumes.