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:

alt

Here’s a step-by-step walkthrough of how it worked:

  1. 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.list and tasks.cancel)
  • The client declares if it supports task-augmented sampling messages and elicitation requests globally (tasks.requests.sampling.createMessage and tasks.requests.elicitation.create)
  1. Assuming server and tool support exists, the client calls a tool and includes a task field with a TTL. The server returns a task handle immediately.

  2. The client continuously polls the server for status (working, input_required, completed).

  3. 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.

  4. When the task status changes to completed, the client requests the task result from the server.

Issues

There were three issues with this design:

  1. 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.

  2. Elicitation requests were still blocking. This defeated the purpose of using tasks in the first place.

  3. 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/list and tasks/result methods no longer exist. Instead, there are now three primary methods for task management: tasks/get to check status and retrieve the final result, tasks/update to respond to elicitation requests, and tasks/cancel to cancel tasks.

  • Elicitation requests are no longer blocking. If the server requires additional input, it includes this request inline, in a tasks/get response. The client can then respond with the required information asynchronously via tasks/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 failed status. However, if a tool call returns isError: true, it is still considered completed, with that result included.

Here’s what the 2026-07-28 task lifecycle looks like:

alt

You can immediately see that it’s a lot simpler. Here’s a step-by-step walkthrough of how it works:

  1. The client can call server/discover to discover task support.

  2. Assuming server support exists, the client calls a tool and includes io.modelcontextprotocol/tasks in its request to advertise its own support. The server can return a task handle and TTL immediately.

  3. The client continuously polls the server for status (working, input_required, completed, failed, or cancelled ) using tasks/get.

  4. If the server requires input from the client to complete the task, it changes the task status to input_required and includes its request in the next tasks/get response. The client responds to the server’s question in a separate tasks/update request and goes back to continuously polling for status.

  5. When the task status changes to completed, the server includes the task result in the final tasks/get response.

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:

bash
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:

bash
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:

bash
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:

bash
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:

bash
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:

bash
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):

bash
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):

bash
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):

bash
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:

bash
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:

bash
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):

bash
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:

bash
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 inputRequeststasks/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.