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:
"""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 whenMCPTaskStore.create()persists the task undermcp-task:{task_id}(or resumed status after all pendinginputRequestsare answered).input_required: Paused state entered when the tool handler returnsMCPInputRequiredResult. The serialized task record includesinputRequestsdescribing the required client actions (for example,elicitation/createorsampling/createMessage).completed: Terminal state when the tool handler returns a value or a tool-level error (isError: true). The final tool output is embedded underrecord.resultwithresultType: "complete".failed: Terminal state recorded when a protocol-levelJSONRPCErrorExceptionor unrecoverable worker failure occurs. Carries a structurederror: {"code": ..., "message": ..., "data": ...}payload.cancelled: Terminal state recorded after cooperative cancellation completes viatasks/cancel.
Note: Terminal statuses (
completed,failed, andcancelled) are monotonic. Oncerecord.is_terminal()isTrue, 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="..."):
MCPHandlerServicetransitions theTaskRecordinMCPTaskStoretostatus="input_required", savesinput_requestsandrequest_state, and emits anotifications/tasksupdate.- The client inspects
inputRequestsviatasks/get(or the notification) and submits responses viatasks/update: MCPTaskStore.update()merges each accepted response key against the outstandinginput_requests. If additional keys remain unanswered, the task stays ininput_requiredwith the remaininginputRequests.- Once all outstanding keys are satisfied,
MCPTaskStore.update()transitions the task back tostatus="working"and resumes the tool handler withcontext.input_responsesandcontext.request_statepopulated onget_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):
"""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
- Initial
tools/call: Becausecontext.input_responsesisNone,MCPHandlerServiceruns the handler synchronously inside the initial HTTP request. ReturningMCPInputRequiredResultsends{"resultType": "input_required", "inputRequests": {...}, "requestState": "..."}directly to the caller without creating a task record. - Follow-up
tools/call: When the client re-invokestools/callwith_meta/inputResponsesandrequestStatepopulated,context.input_responsesis non-empty.MCPHandlerServicecreates the durableTaskRecord, forwardsinput_responsesandrequest_stateinto 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") |