Skip to content

Tasks Extension

Christian Presley edited this page Oct 1, 2026 · 6 revisions

Tasks Extension

The Tasks extension lets a server run a long tool call in the background1. The server answers the tools/call with a task record instead of the tool result. The client polls the task, answers its input requests and reads the result when the task is complete. The extension is optional. The server enables it with the tasks option.

Applies to: MCP 2026-07-28, extension io.modelcontextprotocol/tasks (SEP-2663), zig-sdk 0.1.0.

Enable the extension

var server = try mcp.Server.init(gpa, io, .{
    .info = .{ .name = "jobs", .version = "0.1.0" },
    .tasks = .{ .ttl_ms = 600_000, .poll_interval_ms = 1000, .max_tasks = 1024 },
});
try server.addTool(.{ .name = "slow_compute", .task_support = .optional }, slowCompute);
try server.addTool(.{ .name = "failing_job", .task_support = .required }, failingJob);

The server then advertises the extension under capabilities.extensions in the server/discover result2. Each tool declares its relation to tasks with task_support:

Value Meaning
none The tool never becomes a task. This is the default.
optional The tool can become a task when the client declared the extension. Without the extension, the tool runs at once.
required The tool needs the extension. A client without the extension gets error -32021.

Write a task tool

A tool handler decides itself when a call becomes a task. It returns .start_task. The server then creates the task, answers the request with a CreateTaskResult and runs the handler again inside the task. The second run sees ctx.inTask() as true.

fn slowCompute(ctx: *mcp.RequestContext, args: SlowArgs) anyerror!mcp.Outcome(mcp.CallToolResult) {
    if (!ctx.inTask()) return .start_task;
    var left = args.seconds * 10;
    while (left > 0) : (left -= 1) {
        try ctx.checkCancel();
        try ctx.io.sleep(.fromMilliseconds(100), .awake);
    }
    return .{ .complete = try mcp.CallToolResult.text(ctx.arena, "done", .{}) };
}

A handler must call ctx.checkCancel() in its loops. A tasks/cancel request sets the cancel token, and the check returns error.Canceled. The task then ends with the status cancelled.

A task can ask the client for input. The handler returns .input_required inside the task, as it does in a multi round-trip request3. The task then waits with the status input_required. The client answers with tasks/update, and the server runs the handler again with the answers. The handler reads them with ctx.elicitResponse, ctx.sampleResponse and ctx.rootsResponse.

A tool can also collect input first and start the task afterwards. Round one returns .input_required outside a task. Round two, with the answers, returns .start_task. The task run sees the same answers.

Task states

Status Meaning
working The handler runs.
input_required The handler waits for answers to its input requests.
completed The handler returned a tool result. A tool error (isError) is also completed.
failed The handler returned a protocol error with ctx.setError. The error object is in the task.
cancelled The client canceled the task.

The task stays in the store for ttl_ms milliseconds after its creation. The server never removes a task before that time.

Methods

Method Result
tools/call on a task tool CreateTaskResult: resultType is task, plus taskId, status, createdAt, lastUpdatedAt, ttlMs and pollIntervalMs.
tasks/get The task with its status. A completed task has the tool result under result. A failed task has the error under error. A task that waits for input has its inputRequests.
tasks/update An empty result. The server merges the answers in inputResponses into the task. The task continues when no request is open.
tasks/cancel An empty result. The server also accepts the call on a finished task.

All three task methods need the extension in _meta.clientCapabilities.extensions. Without it the server answers with error -32021. An unknown taskId gives error -32602. The methods tasks/list and tasks/result do not exist in this revision of the extension and give error -32601.

On Streamable HTTP, a task method must carry the Mcp-Name header with the taskId4. The client transport of the SDK sets it.

The client

The client declares the extension in capabilities.extensions. Then callTool waits for a task and returns the tool result, and callToolOrTask, getTask, updateTask, cancelTask and awaitTask give full control. See Client Guide.

Limits

Option Default Effect
ttl_ms 600000 The lifetime of a task record after its creation.
poll_interval_ms 1000 The poll interval the server advises.
max_tasks 1024 The number of tasks kept at the same time. A new task above the limit gives error -32603.

Each task has its own arena. The server copies the request parameters into it. The task store is in memory. A server restart removes all tasks.

Conformance

The official conformance suite has ten scenarios for the extension. The suite does not score them. The fixture server passes all functional checks of the scenarios.

One check, wire-schema-valid, fails in the 8 scenarios that create a task. The suite validates the CreateTaskResult against the CallToolResult schema of the core specification, which requires content. The extension result has no content. The CI job runs each scenario alone with a baseline that permits only this check to fail. See Conformance.

References

  1. MCP-EXT-TASKS: Tasks extension specification.
  2. MCP-SEP-2663: SEP-2663, the Tasks extension.
  3. MCP-SEP-2322: SEP-2322, multi round-trip requests.
  4. MCP-SEP-2575: SEP-2575, the stateless protocol and the -32021 error.

Clone this wiki locally