SEP-2663 Tasks extension for MCP 2026-07-28.
Tasks remains outside the core protocol catalog. The extension owns its three
top-level methods and uses the generic extension middleware seam to augment
selected tools/call operations. Store and runner processes are supplied by
the application through extension options and are never started implicitly.
A runtime installs the extension with application policy, for example:
extensions: [
{Snodo.Extensions.Tasks,
store: {Snodo.Extensions.Tasks.Store.Memory, MyApp.TaskStore},
runner: MyApp.TaskRunner,
task_support: %{"slow_job" => :optional, "durable_job" => :required}}
]:optional tools execute as tasks when the client declares the extension and
fall back to ordinary synchronous execution otherwise. :required tools
return -32021 when the per-request capability is absent. A policy value may
also be an arity-2 function receiving (params, context), allowing a server
to decide per invocation.
Finish ordinary MRTR exchanges before selecting asynchronous execution, or
use await_input/3 while a task is running. A worker cannot complete with an
ordinary input-required result or another task handle; these are failed as
protocol errors instead of storing a continuation as a terminal result.
Limits
A task's work input is at most :max_work_input_bytes of JSON, default
262,144 (256 KiB), or :infinity; a larger one is refused with -32602 before
anything is stored. A taskId is at most 256 bytes and subscriptions/listen
takes at most 100 taskIds; beyond that the request is refused with -32602.
When the runner is at its :max_jobs, or the store at its :max_tasks or
:max_active_tasks_per_scope, task creation is refused with a -32603 error
whose data is %{"retryable" => true, "limit" => name}. Nothing is stored,
and the same call can succeed once running tasks finish or expired tasks are
reaped.
Summary
Functions
Suspends the current Tasks worker until a matching tasks/update arrives.
Returns the stable idempotency key for the current task execution.
Generates an unguessable, URL-safe task identifier.
Returns input responses supplied on a pre-task MRTR retry.
Returns true when this request negotiated the Tasks extension.
Returns the full request params visible to a task-aware tool.
Builds a protocol-neutral notifications/tasks status event for an application source.
Functions
@spec await_input(Snodo.Context.t(), String.t(), map()) :: {:ok, map()} | {:error, term()}
Suspends the current Tasks worker until a matching tasks/update arrives.
The key must be unique for the lifetime of the task. The request is exposed
verbatim in tasks/get.inputRequests; callers receive the matching response.
@spec execution_id(Snodo.Context.t()) :: String.t() | nil
Returns the stable idempotency key for the current task execution.
@spec generate_id() :: String.t()
Generates an unguessable, URL-safe task identifier.
@spec input_responses(Snodo.Context.t()) :: map()
Returns input responses supplied on a pre-task MRTR retry.
@spec negotiated?(Snodo.Context.t()) :: boolean()
Returns true when this request negotiated the Tasks extension.
@spec request_params(Snodo.Context.t()) :: map()
Returns the full request params visible to a task-aware tool.
@spec status_event(Snodo.Extensions.Tasks.Task.t(), keyword()) :: Snodo.Subscription.Event.t()
Builds a protocol-neutral notifications/tasks status event for an application source.