Skip to main content

Module async_task

Module async_task 

Source
Expand description

Async task management for long-running MCP operations

This module provides task lifecycle management for operations that may take longer than a typical request/response cycle. Legacy clients request task augmentation explicitly; final-protocol servers elect tasks after extension negotiation. Tasks can be tracked, polled, updated with input, and cancelled.

Task state lives behind the pluggable TaskStore trait, mirroring the shape of crate::session_store and crate::event_store: a trait, an error enum, and an in-memory default. By default routers use MemoryTaskStore, which keeps tasks in an in-process map (behavior identical to earlier versions). External stores (Redis, Postgres, etc.) can be plugged in so tasks/get works on any instance behind a load balancer in the sessionless 2026-07-28 flows (SEP-2663).

§Example

use std::sync::Arc;
use tower_mcp::async_task::{MemoryTaskStore, TaskStore};
use tower_mcp::McpRouter;

let store: Arc<dyn TaskStore> = Arc::new(MemoryTaskStore::new());
let router = McpRouter::new().task_store(store);

See examples/tasks.rs for a runnable server.

§Authorization

SEP-2663 requires servers to authorize every task request, and warns that a task ID can act as a bearer token: whoever holds it can poll, update, or cancel the task. This module answers that in two layers.

generate_task_id draws 128 bits from the system CSPRNG, so IDs cannot be enumerated or guessed. That only protects IDs nobody has seen, so each task also records the principal that created it (see TaskOwner), and every later operation must match under owner_matches.

Matching is equality, not “protect owned tasks and leave unowned ones open”:

Task ownerCallerResult
nonenoneallowed, no authentication configured
alicealiceallowed
alicebobdenied
alicenonedenied
nonealicedenied

The last row is deliberate. An unowned task can only exist if it was created with no authenticated context, so a request that now carries a principal is a different security context rather than an upgrade of the same one. Servers mixing public and authenticated paths (see AuthConfig::public_path) should expect a task created anonymously to be unreachable once a token is presented.

The principal comes from the OAuth sub claim that the HTTP and WebSocket transports bridge into request extensions. Without the oauth feature there is no principal, so every task is unowned and servers with no authentication behave as they did before ownership existed.

§Why a denial looks like a missing task

A refused operation returns exactly what an unknown task returns: -32602 with “Task not found”.

SEP-2663 mandates -32602 for an invalid or nonexistent task ID, but leaves the authorization failure to the server: tasks should be bound to “some sort of authorization context, the implementation of which is left to individual servers according to their existing bespoke permission models”. Reusing -32602 is therefore tower-mcp policy, not a spec requirement.

The reasoning is that answering “forbidden” would confirm the ID is real, which is what unguessable IDs exist to prevent. The same SEP notes that where binding is impossible “the task ID becomes the only line of defense against contamination”. A server that prefers a distinguishable error can wrap the router and translate.

Expiry follows the same rule: Task::is_expired runs from creation, and an expired task reads as absent rather than as expired, so a retention window cannot be probed either.

§Status notifications

A client may watch a task instead of polling it, by naming its ID in the taskIds filter of a subscriptions/listen stream. Each notifications/tasks carries the complete task, identical to the tasks/get response at that moment, so a client that hears about a completion already holds the result.

The router announces the transitions it drives. A server that drives one itself, most commonly TaskStore::require_input, announces it with McpRouter::notify_task_status_changed.

Notifications are best effort and tasks/get stays authoritative: a task outlives the request that created it, so there may be no subscriber at the moment a transition happens, and a client that missed one loses nothing but time.

Structs§

AppliedInputResponses
Outcome of applying tasks/update.inputResponses to a task.
CancellationToken
A shareable cancellation token for task management
MemoryTaskStore
In-memory TaskStore backed by a HashMap.
Task
Internal task representation with full state

Enums§

TaskStoreError
Errors returned by TaskStore implementations.

Traits§

TaskStore
Storage backend for async task state.

Functions§

generate_task_id
Generate an unguessable task identifier.
owner_matches
Whether principal may act on a task owned by owner.
tasks_extension
Build the validated extension declaration for the final Tasks extension.

Type Aliases§

Result
Result alias for task store operations.
TaskOwner
The principal a task belongs to.
TaskSnapshot
A task’s current snapshot: the task object plus any result or error captured so far.