Task management for long-running MCP operations (experimental).
Tasks provide a durable state machine for operations that may take time to complete. Clients can poll for status, cancel in-flight operations, and receive results asynchronously.
Task Lifecycle
working → completed
working → failed
working → cancelled
working → input_required → working (after elicitation)Storage
Storage is delegated to a pluggable store module implementing
ConduitMcp.Tasks.Store. The default store is
ConduitMcp.Tasks.EtsStore (in-memory, single-node). For durable,
multi-node deployments — or to back tasks with a job queue like Oban —
implement the behaviour and configure it as the application's task store:
config :conduit_mcp, :tasks_store, MyApp.MyTasksStoreThe standard tasks/get, tasks/cancel, tasks/result, and
tasks/list JSON-RPC routes dispatch through this module, so swapping
the store requires no handler changes. See
examples/oban_tasks_server/ for an Oban + SQLite implementation and
examples/oban_task_store.ex for a Postgres-flavored reference.
Configuration
Enable tasks in your transport config:
{ConduitMcp.Transport.StreamableHTTP,
server_module: MyServer,
tasks: [enabled: true]}
Summary
Functions
Cancels a task. Dispatches to the configured store's cancel/1 callback;
falls back to update(task_id, %{"status" => "cancelled"}) for stores
that don't implement it.
Cancels a task, scoped to owner.
Prunes terminal-state tasks older than ttl_ms. Dispatches to the
configured store's cleanup/1 callback; returns 0 for stores that
don't implement it (e.g., stores backed by native TTL like Redis).
Creates a new task. See ConduitMcp.Tasks.Store.
Deletes a task by ID. See ConduitMcp.Tasks.Store.
Generates a unique task ID.
Gets a task by ID. See ConduitMcp.Tasks.Store.
Gets a task by ID, scoped to owner.
Lists tasks, optionally filtered by :status. See ConduitMcp.Tasks.Store.
Lists tasks scoped to owner, optionally filtered by :status.
Extracts the owner principal from a Plug.Conn (or conn-like map).
Returns the configured task store module. Reads from
Application.get_env(:conduit_mcp, :tasks_store), defaulting to
ConduitMcp.Tasks.EtsStore.
Updates a task's status and/or metadata. See ConduitMcp.Tasks.Store.
Returns the list of valid task statuses.
Validates that a status transition is allowed.
Types
@type status() :: :working | :input_required | :completed | :failed | :cancelled
@type task_id() :: String.t()
Functions
Cancels a task. Dispatches to the configured store's cancel/1 callback;
falls back to update(task_id, %{"status" => "cancelled"}) for stores
that don't implement it.
Cancels a task, scoped to owner.
Returns {:error, :not_found} (without cancelling) when the task is owned by
a different principal. See get/2 for the scoping rules.
Prunes terminal-state tasks older than ttl_ms. Dispatches to the
configured store's cleanup/1 callback; returns 0 for stores that
don't implement it (e.g., stores backed by native TTL like Redis).
Creates a new task. See ConduitMcp.Tasks.Store.
Pass owner (typically owner(conn)) to stamp the task with a principal so
it can be owner-scoped via get/2, cancel/2, and list/2. When owner is
nil (the default) the task is left unowned and readable by anyone, which
preserves back-compatibility for apps that don't use scoping.
The owner is stored under the top-level "owner" key of the task metadata —
see the "Owner scoping" section of ConduitMcp.Tasks.Store.
Deletes a task by ID. See ConduitMcp.Tasks.Store.
Generates a unique task ID.
Gets a task by ID. See ConduitMcp.Tasks.Store.
Gets a task by ID, scoped to owner.
Returns {:error, :not_found} when the task is owned by a different
principal, so a task's existence is never leaked to a non-owner. A nil
owner (no principal) or an unowned task is always accessible — see the
"Owner scoping" section of ConduitMcp.Tasks.Store.
Lists tasks, optionally filtered by :status. See ConduitMcp.Tasks.Store.
Lists tasks scoped to owner, optionally filtered by :status.
Returns only tasks the caller may see: their own (matching "owner") plus
any unowned tasks. A nil owner (no principal) returns everything, matching
list/1. See get/2 for the scoping rules.
Extracts the owner principal from a Plug.Conn (or conn-like map).
Applies the configured :task_owner_fun
(config :conduit_mcp, :task_owner_fun), which defaults to
conn.assigns[:current_user]. Returns nil for nil/non-conn input or when
no principal is present — nil means "no scoping" throughout this module.
Return a stable scalar
Ownership is checked by exact match (==), so the extractor should
return a stable, comparable identity — typically the user's sub/id
scalar, not the whole current_user struct. A struct carrying any
per-request volatile field would fail to match its own tasks on a later
request, and an extractor that maps distinct users to equal terms would
leak tasks between them. The default works when current_user is itself a
stable id; map it to one otherwise, e.g.
task_owner_fun: &(&1.assigns[:current_user] && &1.assigns.current_user.id).
Returns the configured task store module. Reads from
Application.get_env(:conduit_mcp, :tasks_store), defaulting to
ConduitMcp.Tasks.EtsStore.
Updates a task's status and/or metadata. See ConduitMcp.Tasks.Store.
Returns the list of valid task statuses.
Validates that a status transition is allowed.