Experimental MCP Tasks for Deferred Results

MCP tasks, introduced in the 2025-11-25 specification and still experimental, wrap a request in a durable, receiver-managed state machine. The requestor gets a task identifier and state metadata, polls with `tasks/get`, and retrieves the original deferred result with `tasks/result`, without holding the initial transport request open.
What an MCP task is
An MCP task wraps a request in a durable state machine so its execution can continue beyond the initial request-response exchange. The requestor receives a task record, checks that record later, and retrieves the underlying result after execution reaches a terminal state.
The MCP tasks specification, checked August 26, 2026, defines tasks for work such as expensive computations, batch processing, and operations backed by external job APIs. A task augments an existing MCP request; it does not introduce a separate kind of application result.
The specification uses two role names because tasks can operate in either protocol direction:
- The requestor sends the task-augmented request. Depending on the request, this can be a client or a server.
- The receiver accepts and executes that request, generates the task identifier, and controls the task lifecycle. This can also be a client or a server.
The requestor decides whether to ask for task execution and is responsible for polling. The receiver decides which request types support tasks, how long task records remain available, and whether optional operations such as listing and cancellation are exposed.
Introduced in 2025-11-25 and still experimental
Tasks first appeared in the MCP specification dated November 25, 2025. The release’s record of key changes, checked August 26, 2026, describes them as experimental support for tracking durable requests through polling and deferred result retrieval.
The experimental label matters at an implementation boundary. The task design and behavior may change in later protocol versions, so support for MCP generally does not prove support for this utility. Clients and servers need to negotiate the task capabilities defined by the same specification version rather than assume that a peer implements them.
An implementation also cannot infer task support from the existence of tasks/get or another method alone. The initialization capability states which request categories accept task augmentation and, separately, whether task listing or cancellation is available.
Capability negotiation controls where tasks apply
Both parties declare task support during initialization. The tasks.requests capability is exhaustive: if a request type is absent, the requestor should not augment that request with a task. If the entire capabilities.tasks object is absent, the peer should not try to create tasks.
In the 2025-11-25 specification, a server can declare task-augmented tools/call support through tasks.requests.tools.call. A client can declare support for task-augmented sampling/createMessage and elicitation/create requests. This makes tasks bidirectional: a client can request deferred execution from a server, while a server can do the same for a client-side sampling or elicitation operation. The interaction with client-side model requests is covered further in using tools inside MCP sampling requests.
Tool calls add another negotiation layer. A listed tool can set execution.taskSupport to required, optional, or forbidden:
requiredmeans the client must invoke that tool as a task.optionalallows either task-augmented or ordinary execution.forbidden, or an omitted value, means the client must not invoke the tool as a task.
The server-level tasks.requests.tools.call capability still comes first. A tool-level value cannot enable task execution when the server did not declare task-augmented tool calls. Conversely, when the server capability exists, each tool’s setting determines whether task augmentation is permitted for that tool.
Receivers that do not declare task support for a request type process that request normally and ignore task-augmentation metadata. A receiver that does declare support may require augmentation and reject an ordinary request for that request type.
The durable state carried by a task
The versioned MCP schema reference, checked August 26, 2026, defines a Task as a record containing an identifier, status, timestamps, retention information, and an optional polling hint.
| Field | Meaning |
|---|---|
taskId |
Receiver-generated string that uniquely identifies the task among tasks controlled by that receiver. |
status |
Current state: working, input_required, completed, failed, or cancelled. |
statusMessage |
Optional human-readable context, such as a completion summary, cancellation reason, or failure detail. |
createdAt |
ISO 8601 timestamp recording when the task was created. |
lastUpdatedAt |
ISO 8601 timestamp recording when the task was last updated. |
ttl |
Actual retention duration from creation, in milliseconds; null means unlimited retention. |
pollInterval |
Optional suggested delay between status checks, in milliseconds. |
The requestor does not choose the identifier. The receiver must generate it, and it must be unique within the receiver’s task set. That identifier is the handle used by later tasks/get, tasks/result, and tasks/cancel requests.
The request-side task metadata is smaller than the resulting task record. It can contain an optional ttl, expressed in milliseconds, requesting how long the receiver should retain the task from creation. The receiver can override that request. The authoritative lifetime is the ttl returned by the receiver, not the value originally requested.
Durability is therefore bounded by the receiver’s retention decision. It means the task state and deferred result can outlive the initial protocol request; it does not mean the record must be retained permanently.
Creation uses a two-phase response
A normal MCP request returns the operation result directly. A task-augmented request instead returns a CreateTaskResult as soon as the receiver accepts the task. That response contains the Task object but not the underlying operation result.
The requestor asks for this behavior by including task in the original request parameters. For example, the relevant part of a tool call can take this shape:
{
"name": "run_batch",
"arguments": {},
"task": {
"ttl": 60000
}
}
If accepted, the immediate result has this shape:
{
"task": {
"taskId": "receiver-generated-id",
"status": "working",
"createdAt": "2025-11-25T10:30:00Z",
"lastUpdatedAt": "2025-11-25T10:30:00Z",
"ttl": 60000,
"pollInterval": 5000
}
}
These values illustrate the schema; they are not timing recommendations. The requested and actual TTL can differ, and pollInterval is optional.
The two responses serve different purposes. CreateTaskResult confirms acceptance and provides the durable handle. The later tasks/result response carries exactly the result shape belonging to the original request type. A task created for tools/call, for example, ultimately yields the corresponding tool-call result structure.
Polling and deferred result retrieval
Requestors poll a task’s state with tasks/get, passing its taskId. The receiver returns the complete current task record, including its status, timestamps, actual TTL, and any polling interval it chooses to suggest.
A requestor should respect pollInterval when present. It should continue polling until the task reaches completed, failed, or cancelled, or until it reaches input_required. Because the polling interval is a receiver-provided hint rather than a required field, a requestor also needs behavior for a response that omits it; the specification does not prescribe a default interval.
The deferred operation result is retrieved separately through tasks/result. Once the task is terminal, the receiver returns what the original request would have returned, whether that is a successful result or a JSON-RPC error.
This separation lets the requestor release the initial transport request instead of holding it open for the full execution time. It can store the task identifier, issue short tasks/get requests, and call tasks/result after observing a terminal state.
There is an important blocking rule: if tasks/result arrives while the task is still working or input_required, the receiver must hold that particular response until the task becomes terminal. A requestor specifically trying to avoid an open transport request should therefore poll with tasks/get and retrieve the result after the reported state becomes terminal.
Calling tasks/result does not itself end status polling. If a result request fails, is cancelled, or is not being awaited actively, the requestor should continue using tasks/get to follow the task.
The status state machine
Every task begins in working. From there, the receiver can move it to input_required, completed, failed, or cancelled.
input_required means the receiver needs a message from the requestor before it can finish. After receiving that input, the task normally returns to working, although the specification also permits a move directly to a terminal state.
completed, failed, and cancelled are terminal. Once a task enters one of those states, it must not transition again. A receiver cannot change a cancelled task to completed even if the underlying execution continues and eventually produces work.
For input_required, the requestor should call tasks/result pre-emptively so it can receive the request for input. Messages needed to continue the task carry related-task metadata, allowing the requestor to associate them with the right execution. Once the required input arrives, the receiver should move the task out of input_required.
A failed task represents failure of the wrapped execution. For a task wrapping tools/call, a tool result with isError: true also places the task in failed. The optional statusMessage should provide diagnostic information, but it is not a substitute for retrieving the underlying result.
TTL defines the retention boundary
The TTL runs from task creation, not from completion or the most recent poll. The receiver must report createdAt and lastUpdatedAt in every task response and must return the actual ttl, including null for unlimited retention, from tasks/get.
After the TTL elapses, the receiver may delete the task and its result regardless of the current status. A task can therefore expire before a slow operation finishes if the retained lifetime is too short. A requestor that needs the result must retrieve it while the receiver still retains the task.
The receiver can impose its own maximum TTL and override the requested value. The specification recommends that receivers document their maximum supported TTL, cap concurrent tasks per requestor, remove expired records promptly, and monitor resource use. Persisting state and results consumes receiver storage; polling consumes request traffic; and longer retention extends the period during which task data needs access control.
Status notifications do not replace polling
A receiver may send notifications/tasks/status when a task changes state. The notification includes the full task object, so a requestor can update its view without an immediate tasks/get call.
Notifications are optional, however. A requestor must not depend on receiving them and should continue polling. The receiver may send notifications for only some transitions or none at all. This is why pollInterval and tasks/get remain part of the core mechanism even when a transport can deliver server-initiated messages.
The progress token from the original task-augmented request remains valid for the task’s lifetime, so ordinary MCP progress notifications can accompany task execution. Progress reporting and task status are separate: progress can describe ongoing work, while task status controls when the deferred result is available.
Listing and cancellation are separate capabilities
tasks/list returns task records and supports cursor-based pagination. A receiver should limit each response with opaque cursors and must include nextCursor when more tasks are available. If a task is retrievable through tasks/get for a requestor, it must also appear through tasks/list when that operation is supported.
Listing is optional and has its own tasks.list capability. A receiver can support task creation and lookup without offering a general inventory.
tasks/cancel is also optional. For a valid non-terminal task, the receiver should attempt to stop execution and must set the state to cancelled before responding. Cancellation fixes the protocol state; it does not guarantee that the underlying work stopped immediately.
Cancellation does not define deletion. A receiver may remove a cancelled task immediately or retain it until later, including until its TTL expires. Requestors should not rely on cancelled-task retention and should collect needed information before cancelling.
A cancellation request for an already terminal task must fail with JSON-RPC code -32602 for invalid parameters. The terminal state remains unchanged.
Correlating messages with the task
Task-related requests, notifications, and responses use the _meta key io.modelcontextprotocol/related-task with an object containing the matching taskId. This connects messages such as elicitation or sampling exchanges to the execution that caused them.
The dedicated task methods already carry the identifier as a parameter. For tasks/get, tasks/result, and tasks/cancel, that parameter is the source of truth, and any conflicting related-task metadata must be ignored. A tasks/result response does require related-task metadata because the underlying result structure does not otherwise contain the task identifier.
Protocol errors and execution failures stay distinct
Task operations preserve MCP’s distinction between protocol-level failure and failure of the wrapped work. The practical boundary is the same one described in distinguishing JSON-RPC protocol errors from MCP tool failures.
An invalid or unknown taskId in tasks/get, tasks/result, or tasks/cancel produces JSON-RPC error -32602. The same code applies to an invalid list cursor and an attempt to cancel a terminal task. Internal protocol failures use -32603. A receiver that requires task augmentation for a supported request type may reject an ordinary request with -32600.
Failure during the underlying execution instead moves the task to failed. tasks/get should then include useful diagnostics in statusMessage. Retrieving the result still preserves the original operation’s result boundary: if the wrapped request produced a JSON-RPC error, tasks/result returns that error; if it produced a normal JSON-RPC response, tasks/result returns that response, including a tool result whose isError value reports tool execution failure.
An expired task may be indistinguishable from an unknown task at the protocol-code level: after removing an expired record, the receiver may respond with -32602 because the task can no longer be found. Requestors should use the returned TTL and timestamps rather than expect a separate permanent tombstone.
Access control and operational records
A task identifier grants access to state-changing and result-retrieval methods, so it cannot be treated as harmless display metadata. When an authorization context exists, the receiver must bind the task to that context and reject tasks/get, tasks/result, and tasks/cancel calls made under a different context. The same boundary should follow the environment separation used for MCP credentials across development and production.
Where the receiver cannot bind tasks to an authorization context, it must generate cryptographically secure, unguessable task identifiers. It should document that limitation and consider shorter TTLs. A receiver that cannot identify requestors should not expose tasks.list, because listing would reveal task metadata without requiring an identifier to be guessed.
When listing is available with authorization, the receiver must return only tasks associated with the requestor’s authorization context. The specification also calls for rate limits on task operations to reduce denial-of-service and enumeration risks.
Receivers should limit concurrent tasks and maximum TTLs, clean up expired records, and monitor resource consumption. They should log creation, completion, and retrieval events, including authorization context when available, and watch for excessive polling or repeated failed lookups. Requestors should log lifecycle events and retain the association between task identifiers and their original operations. Those records belong in the broader design for MCP server observability, particularly because execution and result retrieval occur in separate protocol exchanges.
What to check next
Before depending on tasks, check the peer’s negotiated capability for the exact request type, the tool-level execution.taskSupport value where applicable, the receiver’s maximum TTL and concurrency policy, and whether list or cancellation support was declared.
Then check the operational boundary: how task identifiers are stored, which authorization context owns them, what polling interval is used when none is suggested, how expired records appear, and whether the requestor retrieves results before retention ends. Finally, pin the MCP specification version in compatibility tests while tasks remain experimental.
Sources
- MCP tasks specificationmodelcontextprotocol.io
- record of key changesmodelcontextprotocol.io
- MCP schema referencemodelcontextprotocol.io
See also
Configure OAuth resource indicators, audience validation, and separate upstream credentials so an MCP server rejects misbound tokens and avoids passthrough.
How MCP behavior hints describe tool risk, and why approval policy must rely on trusted server identity and configuration.
Add outputSchema and structuredContent to an MCP tool, validate both sides, and evolve the contract without silently breaking clients.
Why a listing call over a large Cloudinary library can silently truncate inside an MCP client, how to confirm it, and how to ask for less instead.