Chapter 261
Async Tasks (SEP-1686) — Call-Now, Fetch-Later for Long-Running Work
Async Tasks (SEP-1686) — Call-Now, Fetch-Later for Long-Running Work
Real agent work takes minutes to hours: CI runs, deep-research synthesis, batch exports. Synchronous tool calls drop connections, time out, or block the UI. SEP-1686, merged in 2025-11-25, adds a Tasks primitive: any request can be augmented to become a task, and the result can be fetched later or streamed via state notifications. Drift-risk note: Tasks are experimental through H1 2026; SDK surface is still being designed around the spec.
Type: Build Languages: Python (stdlib, async task state machine) Prerequisites: Phase 13 · 07 (MCP server), Phase 13 · 09 (transports) Time: ~75 minutes
Learning Objectives
- Identify when to promote a tool from synchronous to task-augmented (>30 seconds of server-side work).
- Walk the task lifecycle:
working→input_required→completed/failed/cancelled. - Persist task state so crashes do not lose in-flight work.
- Poll
tasks/statusand fetchtasks/resultcorrectly.
The Problem
A generate_report tool runs a multi-minute extraction pipeline. Options under the synchronous model:
- Hold the connection open for three minutes. Remote transports drop it; clients time out; UIs freeze.
- Return immediately with a placeholder; require the client to poll a custom endpoint. Breaks the MCP uniformity.
- Fire-and-forget; no result.
None are good. SEP-1686 adds a fourth: task augmentation. Any request (typically tools/call) can be tagged as a task. The server returns a task id immediately. The client polls tasks/status and fetches tasks/result when done. Server-side state survives restarts.
The Concept
Task augmentation
A request becomes a task by setting params._meta.task.required: true (or optional: true, server decides). The server responds immediately with:
{
"jsonrpc": "2.0", "id": 1,
"result": {
"_meta": {
"task": {
"id": "tsk_9f7b...",
"state": "working",
"ttl": 900000
}
}
}
}ttl is the server's promise to retain state; after ttl the task result is discarded.
Per-tool opt-in
Tool annotations can declare task support:
taskSupport: "forbidden"— this tool always runs synchronously. Safe for fast tools.taskSupport: "optional"— client may request task-augmentation.taskSupport: "required"— client MUST use task augmentation.
A generate_report tool would be required. A notes_search tool would be forbidden.
States
working -> input_required -> working (loop via elicitation)
working -> completed
working -> failed
working -> cancelledState machine is append-only: once completed, failed, or cancelled, the task is terminal.
Methods
tasks/status {taskId}— returns current state and a progress hint.tasks/result {taskId}— blocks or returns 404 if not yet done.tasks/cancel {taskId}— idempotent; terminal states ignore.tasks/list— optional; enumerates active and recently-completed tasks.
Streaming state changes
When the server supports it, the client can subscribe to state notifications:
server -> notifications/tasks/updated {taskId, state, progress?}Clients that stream rather than poll get better UX. Polling is always supported as the minimal surface.
Durable state
The spec requires servers that declare task support to persist state. A crash should not lose completed results within ttl. Stores range from SQLite to Redis to the filesystem. The Lesson 13 harness uses the filesystem.
Cancellation semantics
tasks/cancel is idempotent. If the task is mid-execution, the server attempts to stop (check executor-cooperative cancellation). If already terminal, the request is a no-op.
Crash recovery
When the server process restarts:
- Load all persisted task states.
- Mark any
workingtasks whose process died asfailedwith errorCRASH_RECOVERY. - Preserve
completed/failed/cancelledfor their ttl.
Async tasks plus sampling
A task can itself call sampling/createMessage. This is how long-running research tasks work: the server's task thread samples the client's model as needed, while the client's UI shows the task as working with periodic progress updates.
Why this is experimental
SEP-1686 shipped in 2025-11-25 but the broader roadmap calls out three open issues: durable subscription primitives, subtasks (parent-child task relationships), and result-TTL standardization. Expect the spec to evolve through 2026. Production code should treat Tasks as stable only for the common case and guard against future SDK changes for subtasks.
tp-task-lifecycleUse It
code/main.py implements a durable task store (filesystem-backed) and a generate_report tool that runs in a background thread. Clients call the tool, get a task id immediately, poll tasks/status while the worker updates progress, and fetch tasks/result when done. Cancellation works; crash recovery is simulated by killing the worker thread and reloading state.
What to look at:
- Task state JSON persisted to
/tmp/lesson-13-tasks/<id>.json. - Worker thread updates
progressfield; poll shows it advancing. - Cancellation from client side sets an event; worker checks and exits early.
- State reload on "crash" marks the in-flight task as
failedwithCRASH_RECOVERY.
Ship It
This lesson produces outputs/skill-task-store-designer.md. Given a long-running tool (research, build, export), the skill designs the task store (state shape, ttl, durability), picks the right taskSupport flag, and sketches progress notifications.
Exercises
-
Run
code/main.py. Kick off agenerate_reporttask, poll status, then fetch the result. -
Add a
tasks/cancelcall mid-run. Verify the worker honors it and the state becomescancelled. -
Simulate crash recovery: kill the worker thread, restart the loader, and observe the
CRASH_RECOVERYfailure mode. -
Extend the store to SQLite. Durability wins are the same; query options open up (list all tasks from session X).
-
Read the MCP roadmap post for 2026. Identify the one Tasks-related open issue most likely to affect SDK API design in the next year.
Key Terms
| Term | What people say | What it actually means |
|---|---|---|
| Task | "Long-running tool call" | Request augmented with _meta.task for async execution |
| SEP-1686 | "Tasks spec" | Spec Evolution Proposal that added Tasks in 2025-11-25 |
_meta.task | "Task envelope" | Per-request metadata containing id, state, ttl |
| taskSupport | "Tool flag" | forbidden / optional / required per tool |
tasks/status | "Poll method" | Fetch current state and optional progress hint |
tasks/result | "Fetch result" | Returns the completed payload or 404 if not yet done |
tasks/cancel | "Stop it" | Idempotent cancellation request |
| ttl | "Retention budget" | Milliseconds the server promises to keep the task state |
notifications/tasks/updated | "State push" | Server-initiated state-change event |
| Durable store | "Crash-safe state" | Filesystem / SQLite / Redis persistence layer |
Further Reading
- MCP — GitHub SEP-1686 issue — the originating proposal and full discussion
- WorkOS — MCP async tasks for AI agent workflows — design walkthrough with rationale
- DeepWiki — MCP task system and async operations — mechanics and state machine
- FastMCP — Tasks — SDK-level task implementation patterns
- MCP blog — 2026 roadmap — open issues and 2026 priorities including subtasks
