Task management for long-running MCP operations (experimental).
Frozen, legacy era only
This module serves the 2025-11-25 tasks methods (tasks/get,
tasks/cancel, tasks/result, tasks/list) to legacy-era clients
only. It is frozen: no new features, and it goes away with the legacy
era. For the 2026-07-28 tasks extension
(io.modelcontextprotocol/tasks) see ConduitMcp.Ext.Tasks.
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 and :limit. See ConduitMcp.Tasks.Store.
Lists tasks scoped to owner, optionally filtered by :status and
:limit.
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: every caller may see it, unless
:tasks_require_owner is set, in which case no caller may (see get/2).
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 caller may not see the task, so a
task's existence is never leaked to a non-owner.
task "owner" | caller owner | default | :tasks_require_owner |
|---|---|---|---|
| unowned | nil | allow | deny |
| unowned | X | allow | deny |
Y | nil | deny | deny |
Y | Y | allow | allow |
Y | X | deny | deny |
The Y/nil row keeps authorization default-closed: an unauthenticated
request never reads a principal's task. Set
config :conduit_mcp, :tasks_require_owner, true to also refuse unowned
tasks, which is the right posture once every creation site stamps an owner.
See the "Owner scoping" section of ConduitMcp.Tasks.Store.
Lists tasks, optionally filtered by :status and :limit. See ConduitMcp.Tasks.Store.
Lists tasks scoped to owner, optionally filtered by :status and
:limit.
The owner is pushed into the store query rather than applied afterwards, so
a store can express it as a predicate (the default EtsStore compiles all
three options into one :ets.select/3 match spec). The facade re-checks the
returned rows and then applies :limit itself: a custom store that ignores
:owner must not turn into a silent authorization bypass, nor return more
than :limit rows. Such a store must ignore :limit too, or it truncates the
caller's own rows before the re-check (see ConduitMcp.Tasks.Store.list/1).
Returns the caller's own tasks plus unowned ones. nil = no principal: sees
only unowned tasks; nothing under :tasks_require_owner (see get/2).
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
ConduitMcp.Principal.id/1 — the stable scalar identity assigned by both
auth plugs. Returns nil for nil/non-conn input or when no principal is
present. nil = no principal: sees only unowned tasks; nothing under
:tasks_require_owner (see get/2).
Return a stable scalar
Ownership is checked by exact match (===), so a custom extractor must
return a stable, comparable identity — the user's sub/id scalar, never
a struct or claims map. A term carrying any per-request volatile field
(exp, iat, jti) fails to match its own tasks on the next request, so
the owner's own task 404s; an extractor mapping distinct users to equal
terms leaks tasks between them. ConduitMcp.Principal.id/1 is already
such a scalar — prefer it.
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.