Skip to content

MCP Tasks Extension (io.modelcontextprotocol/tasks)

The Model Context Protocol Tasks extension (io.modelcontextprotocol/tasks) allows MCP clients and servers to promote long-running or interactive multi-round-trip (MRTR) tools/call invocations into durable background tasks that clients can poll, supply interactive input to, or cancel.


Enabling MCP Tasks (MCPTaskConfig)

Enable the extension by setting MCPConfig(tasks=True) or passing an explicit MCPTaskConfig instance:

docs/examples/snippets/configuration_tasks.py
"""Snippet: enabling the MCP Tasks extension."""

from litestar import Litestar

from litestar_mcp import LitestarMCP, MCPConfig


def build() -> "Litestar":
    # start-example
    app = Litestar(plugins=[LitestarMCP(MCPConfig(tasks=True))])
    # end-example
    return app

When tasks is enabled (or auto-enabled because litestar-queues QueuePlugin is registered on the application), LitestarMCP advertises "extensions": {"io.modelcontextprotocol/tasks": {}} inside server/discover and initialize capabilities.

MCPTaskConfig Reference

Field Type Default Description
store Store or None None Litestar Store persisting TaskRecord documents. When None, LitestarMCP resolves "litestar_mcp_tasks" from the application's StoreRegistry (Litestar(stores=...)), creating a MemoryStore if not pre-registered.
default_ttl_ms int 300_000 Default time-to-live in milliseconds (5 minutes) applied to newly created task records when the client does not request a custom ttlMs. Must be non-negative.
max_ttl_ms int 3_600_000 Maximum time-to-live in milliseconds (1 hour) used to clamp client-requested ttlMs values. Must be >= default_ttl_ms.
poll_interval_ms int 1_000 Suggested polling cadence in milliseconds advertised as pollIntervalMs on serialized TaskRecord payloads. Must be positive.
status_callback Callable[[TaskRecord], Awaitable[None]] or None None Optional async callback invoked on every task state transition (working, input_required, completed, failed, cancelled) before LitestarMCP publishes a notifications/tasks event.
executor MCPTaskExecutor or Literal["in_process"] or bool or None None Pluggable task runner executing task-augmented tool calls outside the creating HTTP request. None auto-wires QueueTaskExecutor when QueuePlugin is present, or falls back to InProcessTaskExecutor. Pass "in_process" or False to force in-process execution even when QueuePlugin is installed.

Declaring Task-Capable Tools

Individual tools declare how they participate in the Tasks extension via @mcp_tool(task=...) / @mcp_tool(task_support=...) or route opt keys (mcp_task / mcp_task_support):

Declaration Normalized Mode Runtime Behavior on tools/call
task=True or task_support="optional" "optional" Promotes the tool call to a background TaskRecord (resultType: "task") when MCPConfig.tasks is enabled and the client advertises extensions["io.modelcontextprotocol/tasks"]. Executes synchronously for clients that do not advertise task support.
task_support="required" "required" Requires both server-side MCPConfig.tasks and client-side extensions["io.modelcontextprotocol/tasks"]. Rejects non-task-capable clients with JSON-RPC error -32021 (MISSING_REQUIRED_CLIENT_CAPABILITY).
task=False or task_support="forbidden" (or omitted) "forbidden" Always executes synchronously inside the tools/call request and returns a standard tool result.

Task State Machine

Every TaskRecord in MCPTaskStore progresses through a deterministic lifecycle:

stateDiagram-v2
    [*] --> working: tools/call (resultType = task)
    working --> input_required: MCPInputRequiredResult
    input_required --> working: tasks/update (inputResponses)
    working --> completed: Tool returns result (or isError = true)
    working --> failed: JSONRPCErrorException / unhandled executor crash
    working --> cancelled: tasks/cancel
    input_required --> cancelled: tasks/cancel
  • working: Initial status when MCPTaskStore.create() persists the task under mcp-task:{task_id} (or resumed status after all pending inputRequests are answered).
  • input_required: Paused state entered when the tool handler returns MCPInputRequiredResult. The serialized task record includes inputRequests describing the required client actions (for example, elicitation/create or sampling/createMessage).
  • completed: Terminal state when the tool handler returns a value or a tool-level error (isError: true). The final tool output is embedded under record.result with resultType: "complete".
  • failed: Terminal state recorded when a protocol-level JSONRPCErrorException or unrecoverable worker failure occurs. Carries a structured error: {"code": ..., "message": ..., "data": ...} payload.
  • cancelled: Terminal state recorded after cooperative cancellation completes via tasks/cancel.

Note: Terminal statuses (completed, failed, and cancelled) are monotonic. Once record.is_terminal() is True, subsequent transition calls are no-ops.


Multi-Round Input Requests (MCPInputRequiredResult)

Tools that require human confirmation, form elicitation, or client-side sampling mid-execution return an MCPInputRequiredResult instance and read client responses from get_mcp_request_context():

1. Mid-Task Pause and tasks/update Resumption

When a running background task returns MCPInputRequiredResult(input_requests={...}, request_state="..."):

  1. MCPHandlerService transitions the TaskRecord in MCPTaskStore to status="input_required", saves input_requests and request_state, and emits a notifications/tasks update.
  2. The client inspects inputRequests via tasks/get (or the notification) and submits responses via tasks/update:
    {
      "jsonrpc": "2.0",
      "id": 12,
      "method": "tasks/update",
      "params": {
        "taskId": "8f1d4e9a2c3b4a5d9e8f7a6b5c4d3e2f",
        "inputResponses": {
          "approval": {"action": "accept"}
        }
      }
    }
    
  3. MCPTaskStore.update() merges each accepted response key against the outstanding input_requests. If additional keys remain unanswered, the task stays in input_required with the remaining inputRequests.
  4. Once all outstanding keys are satisfied, MCPTaskStore.update() transitions the task back to status="working" and resumes the tool handler with context.input_responses and context.request_state populated on get_mcp_request_context().

2. Pre-Task Synchronous Input (task_input_before_start=True)

Some tools need to complete an initial elicitation or approval round before allocating a background TaskRecord or enqueuing a job. Set task_input_before_start=True on @mcp_tool (or mcp_task_input_before_start=True in route opt):

docs/examples/snippets/tool_task_input_before_start.py
"""Task-capable tool that completes an MRTR input round before task creation."""

from litestar import post

from litestar_mcp import MCPInputRequiredResult, get_mcp_request_context, mcp_tool


# start-example
@mcp_tool(
    task_support="required",
    task_input_before_start=True,
)
@post("/reports")
async def publish_report() -> MCPInputRequiredResult | dict[str, str]:
    context = get_mcp_request_context()
    if not context.input_responses:
        return MCPInputRequiredResult(
            input_requests={
                "approval": {
                    "method": "elicitation/create",
                    "params": {"message": "Publish this report?"},
                }
            },
            request_state="integrity-protected-state",
        )
    return {"status": "published"}


# end-example
  1. Initial tools/call: Because context.input_responses is None, MCPHandlerService runs the handler synchronously inside the initial HTTP request. Returning MCPInputRequiredResult sends {"resultType": "input_required", "inputRequests": {...}, "requestState": "..."} directly to the caller without creating a task record.
  2. Follow-up tools/call: When the client re-invokes tools/call with _meta / inputResponses and requestState populated, context.input_responses is non-empty. MCPHandlerService creates the durable TaskRecord, forwards input_responses and request_state into the task execution context, and returns {"resultType": "task", "taskId": "...", "status": "working", ...}.

Task JSON-RPC Methods & Ownership Isolation

All task methods require the caller to advertise extensions["io.modelcontextprotocol/tasks"] (otherwise the server returns JSON-RPC error -32021). Every TaskRecord is bound to context.owner_id (derived from the authenticated principal when authentication middleware is active). A caller with a different owner_id cannot inspect, update, or cancel another principal's task.

Method Parameters Response & Behavior
tools/call (task-promoted) {"name": "<tool>", "arguments": {...}} Creates a TaskRecord in MCPTaskStore, dispatches execution to the active MCPTaskExecutor, and returns {"resultType": "task", "taskId": "...", "status": "working", "createdAt": "...", "lastUpdatedAt": "...", "ttlMs": 300000, "pollIntervalMs": 1000}.
tasks/get {"taskId": "<id>"} Returns the serialized TaskRecord (taskId, status, createdAt, lastUpdatedAt, ttlMs, pollIntervalMs, and optional statusMessage, inputRequests, result, or error). Raises INVALID_PARAMS (-32602) if the task does not exist or belongs to a different owner_id.
tasks/result (Accessed via tasks/get) When status == "completed", the tasks/get payload includes "result": {"resultType": "complete", "content": [...], "isError": false | true}. When status == "failed", tasks/get includes "error": {"code": ..., "message": ...}.
tasks/list / notifications/tasks Subscription stream / status_callback Task state changes invoke MCPTaskConfig.status_callback(record) and publish "notifications/tasks" with record.to_dict() to active subscription streams.
tasks/update {"taskId": "<id>", "inputResponses": {...}} Supplies answers to pending inputRequests while status == "input_required". When all outstanding keys are answered, transitions the task to working, wakes the local runner or calls executor.resume(call), and returns {}.
tasks/cancel {"taskId": "<id>"} Requests cooperative cancellation of a non-terminal task (working or input_required) and returns {}. The task transitions to status="cancelled" once the in-process runner or distributed queue worker acknowledges cancellation.

SEP-2663 isError: true vs Protocol Error Handling

In accordance with the MCP tool error contract (SEP-2663), litestar-mcp distinguishes between application/tool errors (which an LLM can read and recover from) and protocol errors:

Failure Scenario Task Lifecycle Outcome Wire Representation
Missing or invalid tool argument (_validate_tool_arguments) Task transitions to status: "completed" record.result contains {"resultType": "complete", "isError": true, "content": [{"type": "text", "text": "{\"error\":\"Invalid tool arguments\",\"errors\":[...]}"}]}
Route guard rejection or HTTPException inside tool handler Task transitions to status: "completed" record.result contains {"resultType": "complete", "isError": true, "content": [{"type": "text", "text": "..."}]}
Standard Python exception raised by tool handler Task transitions to status: "completed" record.result contains {"resultType": "complete", "isError": true, "content": [{"type": "text", "text": "{\"error\":\"...\"}"}]}
Explicit JSONRPCErrorException raised during task execution Task transitions to status: "failed" record.error contains {"code": <int>, "message": "<str>", "data": ...}
Client lacks io.modelcontextprotocol/tasks capability Rejected before task creation Immediate JSON-RPC error -32021 (MISSING_REQUIRED_CLIENT_CAPABILITY)
Unknown taskId or mismatched owner_id on tasks/get / update / cancel Request rejected Immediate JSON-RPC error -32602 (INVALID_PARAMS, "Failed to retrieve task: Task not found")