A few weeks ago, I gave a talk on something that sounds simple but is actually fairly complex: how to handle long-running MCP tool calls.
By default, MCP tool calls are synchronous - client requests, server accepts, client blocks until server responds. If you’re querying a database or making an API request, the client wait time isn’t very noticeable, because the tool call typically completes fast.
However, there are some cases where this approach doesn’t work so well. Imagine a client calling a tool to prepare a report. The server needs to search for the data, extract and transform it, analyze it, summarize information and prepare conclusions, generate the final report in a standardized format, and return it to the client. This can take a while. Think about video conversion, audio transcription, browser automation, database migration, multi-step agent workflows - they all have the same problem.
This is where the new Tasks extension in the MCP 2026-07-28 specification comes in. It provides a clean, standard way to handle long-running tasks at the protocol level.
Before
Tasks were first introduced in the MCP 2025-11-25 specification as an experimental feature. This version defined two entities: a requestor (the task creator) and a receiver (the task processor). Interestingly, it was directionally agnostic - either the client or the server could initiate the request. This was intentional, to cover situations where the server needed to request input from the client while handling a task.
Here’s what the 2025-11-25 task lifecycle looked like:

Here’s a step-by-step walkthrough of how it worked:
- Client and server initialize a connection and discover task support at different levels:
- The server declares if it supports task-augmented tool calls globally (
tasks.requests.tools.call) - Individual tools declare their own support (
execution.taskSupport) - The client and server separately declare if they each support listing and canceling tasks (
tasks.listandtasks.cancel) - The client declares if it supports task-augmented sampling messages and elicitation requests globally (
tasks.requests.sampling.createMessageandtasks.requests.elicitation.create)
-
Assuming server and tool support exists, the client calls a tool and includes a
taskfield with a TTL. The server returns a task handle immediately. -
The client continuously polls the server for status (
working,input_required,completed). -
If the server requires input from the client to complete the task, it changes the task status to
input_required. When this happens, the client has to stop polling and instead open a blocking connection to receive, and respond to, the server’s question. Once the connection closes, the client goes back to continuously polling for status. -
When the task status changes to
completed, the client requests the task result from the server.
Issues
There were three issues with this design:
-
The initialization and discovery process involved checking capabilities on both sides of the connection, at different levels. This was confusing and easy to get wrong.
-
Elicitation requests were still blocking. This defeated the purpose of using tasks in the first place.
-
Listing tasks required proper authorization contexts to avoid leaking task information to unauthorized clients. Once the protocol itself became stateless, there was no longer a natural session scope for that binding.
After
In the MCP 2026-07-28 revision, tasks moved into a separate extension under the io.modelcontextprotocol/tasks namespace. This version brings a few important changes:
-
Instead of the previous multi-level capability check (global, tool-level, operation-level), there’s now only a single capability flag. If the flag is present, it signals that the entity (client or server) can support asynchronous tasks.
-
Some methods were dropped and others were modified. The
tasks/listandtasks/resultmethods no longer exist. Instead, there are now three primary methods for task management:tasks/getto check status and retrieve the final result,tasks/updateto respond to elicitation requests, andtasks/cancelto cancel tasks. -
Elicitation requests are no longer blocking. If the server requires additional input, it includes this request inline, in a
tasks/getresponse. The client can then respond with the required information asynchronously viatasks/update. -
Task cancellation is non-blocking and non-guaranteed. The server acknowledges a cancellation request, but handles it asynchronously, so the task can still complete.
-
There is a clearer separation between protocol errors and task errors. If a JSON-RPC error occurs during execution, the task moves to a
failedstatus. However, if a tool call returnsisError: true, it is still consideredcompleted, with that result included.
Here’s what the 2026-07-28 task lifecycle looks like:

You can immediately see that it’s a lot simpler. Here’s a step-by-step walkthrough of how it works:
-
The client can call
server/discoverto discover task support. -
Assuming server support exists, the client calls a tool and includes
io.modelcontextprotocol/tasksin its request to advertise its own support. The server can return a task handle and TTL immediately. -
The client continuously polls the server for status (
working,input_required,completed,failed, orcancelled) usingtasks/get. -
If the server requires input from the client to complete the task, it changes the task status to
input_requiredand includes its request in the nexttasks/getresponse. The client responds to the server’s question in a separatetasks/updaterequest and goes back to continuously polling for status. -
When the task status changes to
completed, the server includes the task result in the finaltasks/getresponse.
Examples
Here are a few request-response examples that show this in action. All examples assume a server at http://localhost:5000/mcp.
You can see a live demo at https://www.youtube.com/watch?v=dSoXxTqHJv0
Server discovery
Request:
curl -X POST http://localhost:5000/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "MCP-Protocol-Version: 2026-07-28" \
-H "Mcp-Method: server/discover" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "server/discover",
"params": {
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": { "extensions": { "io.modelcontextprotocol/tasks": {} } }
}
}
}'Response:
event: message
data: {"result":{"supportedVersions":["2026-07-28"],"capabilities":{"logging":{},"tools":{},"extensions":{"io.modelcontextprotocol/tasks":{}}},"ttlMs":0,"cacheScope":"private","resultType":"complete","_meta":{"io.modelcontextprotocol/serverInfo":{"name":"TasksDemo","version":"1.0.0.0"}}},"id":1,"jsonrpc":"2.0"}List tools
Request:
curl -X POST http://localhost:5000/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "MCP-Protocol-Version: 2026-07-28" \
-H "Mcp-Method: tools/list" \
-d '{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/list",
"params": {
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": { "extensions": { "io.modelcontextprotocol/tasks": {} } }
}
}
}'Response:
event: message
data: {"result":{"tools":[{"name":"generate_report","description":"Generates a simulated report on a topic with a specified tone.","inputSchema":{"type":"object","properties":{"topic":{"description":"The topic for the report","type":"string"},"tone":{"description":"Tone for the report (formal or casual)","type":"string"}},"required":["topic","tone"]}}],"ttlMs":0,"cacheScope":"private","resultType":"complete","_meta":{"io.modelcontextprotocol/serverInfo":{"name":"TasksDemo","version":"1.0.0.0"}}},"id":2,"jsonrpc":"2.0"}Create a task
Request:
curl -X POST http://localhost:5000/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "MCP-Protocol-Version: 2026-07-28" \
-H "Mcp-Method: tools/call" \
-H "Mcp-Name: generate_report" \
-d '{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "generate_report",
"arguments": { "topic": "The Future of AI", "tone": "formal" },
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": { "extensions": { "io.modelcontextprotocol/tasks": {} } }
}
}
}'Response:
event: message
data:
data: {"result":{"taskId":"2d57ea7c18c64009a4756b42c231c3cd","status":"working","createdAt":"2026-08-27T20:09:04.9538938\u002B00:00","lastUpdatedAt":"2026-08-27T20:09:04.9538938\u002B00:00","ttlMs":300000,"pollIntervalMs":1000,"resultType":"task","_meta":{"io.modelcontextprotocol/serverInfo":{"name":"TasksDemo","version":"1.0.0.0"}}},"id":3,"jsonrpc":"2.0"}Check task status
Request (polling loop):
curl -X POST http://localhost:5000/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "MCP-Protocol-Version: 2026-07-28" \
-H "Mcp-Method: tasks/get" \
-H "Mcp-Name: 2d57ea7c18c64009a4756b42c231c3cd" \
-d '{
"jsonrpc": "2.0",
"id": 4,
"method": "tasks/get",
"params": {
"taskId": "2d57ea7c18c64009a4756b42c231c3cd",
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": { "extensions": { "io.modelcontextprotocol/tasks": {} } }
}
}
}'Response (while task is in progress):
event: message
data: {"result":{"resultType":"complete","taskId":"2d57ea7c18c64009a4756b42c231c3cd","status":"working","createdAt":"2026-08-26T15:11:40.3069356\u002B00:00","lastUpdatedAt":"2026-08-26T15:11:40.3069356\u002B00:00","ttlMs":300000,"pollIntervalMs":1000,"_meta":{"io.modelcontextprotocol/serverInfo":{"name":"TasksDemo","version":"1.0.0.0"}}},"id":4,"jsonrpc":"2.0"}Response (after task is complete):
event: message
data: {"result":{"resultType":"complete","taskId":"2d57ea7c18c64009a4756b42c231c3cd","status":"completed","createdAt":"2026-08-26T15:11:40.3069356\u002B00:00","lastUpdatedAt":"2026-08-26T15:12:10.3532622\u002B00:00","ttlMs":300000,"pollIntervalMs":1000,"result":{"content":[{"type":"text","text":"# Report: The Future of AI\n\n## Executive Summary\nThis is a simulated report on the topic \u0027The Future of AI\u0027, generated with a formal tone.\n\n## Key Findings\n- The formal approach provides clear communication.\n- All stages of the pipeline completed successfully.\n\n## Conclusion\nReport generation for \u0027The Future of AI\u0027 completed with formal tone preference."}]},"_meta":{"io.modelcontextprotocol/serverInfo":{"name":"TasksDemo","version":"1.0.0.0"}}},"id":3,"jsonrpc":"2.0"}Cancel a task
Request:
curl -X POST http://localhost:5000/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "MCP-Protocol-Version: 2026-07-28" \
-H "Mcp-Method: tasks/cancel" \
-H "Mcp-Name: 48ddc33ef1274b4eab470f705cf6010d" \
-d '{
"jsonrpc": "2.0",
"id": 5,
"method": "tasks/cancel",
"params": {
"taskId": "48ddc33ef1274b4eab470f705cf6010d",
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": { "extensions": { "io.modelcontextprotocol/tasks": {} } }
}
}
}'Response:
event: message
data: {"result":{"resultType":"complete","_meta":{"io.modelcontextprotocol/serverInfo":{"name":"TasksDemo","version":"1.0.0.0"}}},"id":5,"jsonrpc":"2.0"}Response (check status):
curl -X POST http://localhost:5000/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "MCP-Protocol-Version: 2026-07-28" \
-H "Mcp-Method: tasks/get" \
-H "Mcp-Name: 48ddc33ef1274b4eab470f705cf6010d" \
-d '{
"jsonrpc": "2.0",
"id": 6,
"method": "tasks/get",
"params": {
"taskId": "48ddc33ef1274b4eab470f705cf6010d",
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": { "extensions": { "io.modelcontextprotocol/tasks": {} } }
}
}
}'Response:
event: message
data: {"result":{"resultType":"complete","taskId":"48ddc33ef1274b4eab470f705cf6010d","status":"cancelled","createdAt":"2026-08-26T15:35:44.2281432\u002B00:00","lastUpdatedAt":"2026-08-26T15:35:54.7579924\u002B00:00","ttlMs":300000,"pollIntervalMs":1000,"_meta":{"io.modelcontextprotocol/serverInfo":{"name":"TasksDemo","version":"1.0.0.0"}}},"id":6,"jsonrpc":"2.0"}Summary
The new Tasks extension consists of numerous changes - fewer methods and a design aligned with MCP’s stateless core - but they’re all in service of making asynchronous task management over MCP simpler.
Here’s a quick summary of the changes:
| Dimension | 2025-11-25 | 2026-07-28 |
|---|---|---|
| Location in spec | Core (experimental) | Official extension |
| Task creation | Client requests task execution per request | Client advertises support; server decides per request |
| Capability negotiation | Multi-layer check | Single extension capability |
| Result retrieval | Separate, blocking tasks/result |
Inlined directly in tasks/get |
| Mid-task input | Blocking tasks/result side-channel |
inputRequests → tasks/update |
| Task listing | tasks/list (paginated) |
Removed entirely |
| Cancellation | Synchronous, returns task state | Ack-only, cooperative |
To learn more, start with the MCP 2025-11-25 version, then read SEP-2663 for the proposed changes and, finally, see the current MCP 2026-07-28 version.