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 owner | Caller | Result |
|---|---|---|
| none | none | allowed, no authentication configured |
alice | alice | allowed |
alice | bob | denied |
alice | none | denied |
| none | alice | denied |
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§
- Applied
Input Responses - Outcome of applying
tasks/update.inputResponsesto a task. - Cancellation
Token - A shareable cancellation token for task management
- Memory
Task Store - In-memory
TaskStorebacked by aHashMap. - Task
- Internal task representation with full state
Enums§
- Task
Store Error - Errors returned by
TaskStoreimplementations.
Traits§
- Task
Store - Storage backend for async task state.
Functions§
- generate_
task_ id - Generate an unguessable task identifier.
- owner_
matches - Whether
principalmay act on a task owned byowner. - tasks_
extension - Build the validated extension declaration for the final Tasks extension.
Type Aliases§
- Result
- Result alias for task store operations.
- Task
Owner - The principal a task belongs to.
- Task
Snapshot - A task’s current snapshot: the task object plus any result or error captured so far.