ConduitMcp.Tasks (ConduitMCP v0.10.1)

Copy Markdown View Source

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.MyTasksStore

The 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).

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

status()

@type status() :: :working | :input_required | :completed | :failed | :cancelled

task_id()

@type task_id() :: String.t()

Functions

cancel(task_id)

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.

cancel(task_id, owner)

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.

cleanup(ttl_ms)

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).

create(task_id, metadata \\ %{}, owner \\ nil)

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.

delete(task_id)

Deletes a task by ID. See ConduitMcp.Tasks.Store.

generate_id()

Generates a unique task ID.

get(task_id)

Gets a task by ID. See ConduitMcp.Tasks.Store.

get(task_id, owner)

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.

list(opts \\ [])

Lists tasks, optionally filtered by :status. See ConduitMcp.Tasks.Store.

list(opts, owner)

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.

owner(conn)

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).

store()

Returns the configured task store module. Reads from Application.get_env(:conduit_mcp, :tasks_store), defaulting to ConduitMcp.Tasks.EtsStore.

update(task_id, updates)

Updates a task's status and/or metadata. See ConduitMcp.Tasks.Store.

valid_statuses()

Returns the list of valid task statuses.

valid_transition?(from, to)

Validates that a status transition is allowed.