MCP 2026-07-28 one lesson per page
23 lessons
RedesignedUse Cases & Ecosystem · 20%Track 2 · 11 / 14

Long-running work: Tasks left the core and got simpler

Experimental tasks moved out of the core protocol into an official extension, io.modelcontextprotocol/tasks, with a different set of methods.

Listen to this lessonAudio overview in Gemini Notebook · about 15–25 min · opens in a new tab

The test suite on the train

On the train, an engineer asks their agent to "run the full integration suite and deploy to staging if it's green". It takes 25 minutes.

The server answers with resultType: "task", so the phone just stores taskId. The train enters a tunnel and the connection dies. Nothing is lost: the task lives on the server.

At minute 18, the task moves to input_required: "3 flaky tests failed. Deploy anyway?" The engineer, now at their desk with a laptop, sees the question on the next tasks/get and answers "no" with tasks/update. Same task ID, different device, no session in sight.

  • Opt in: the client lists the extension in each request's clientCapabilities.extensions, and the server advertises it in server/discover. A server must never return a task to a client that didn't declare it.
  • Server decides: for a supported request, the server may answer with resultType: "task" and a task handle (taskId, status, ttlMs, pollIntervalMs), with no per-request flag needed.
  • Poll with tasks/get. Blocking tasks/result is removed, and so is tasks/list.
  • Mid-flight input: the status changes to input_required with inputRequests, and the client answers with the new tasks/update.
  • Cancel with tasks/cancel. It's cooperative, so the task may still finish.
  • The statuses are working, input_required, completed, failed and cancelled; the last three are terminal. Status pushes are available via subscriptions/listen as notifications/tasks, each carrying the full task state.

Holding a connection open while a job runs breaks on timeouts, crashes and flaky networks. A durable task ID survives all three. Moving Tasks to an extension keeps the core small, and lets the feature change on its own schedule.

Message flow in 2026-07-28
ClientServertools/call (+ tasks extension)resultType: "task" {taskId, working}tasks/getinput_required + inputRequeststasks/update {inputResponses}acktasks/getcompleted + result
Solid arrows are requests, dashed are responses or notifications. Red ✕ is gone; green is new.

The method set

2026-07-28
// ① opt in on the request"_meta": { "io.modelcontextprotocol/clientCapabilities": {  "extensions": { "io.modelcontextprotocol/tasks": {} } } }// ② the server chooses to answer with a handle{"jsonrpc": "2.0", "id": 11, "result": {  "resultType": "task",  "taskId": "t-123",  "status": "working",  "createdAt": "2026-09-01T10:30:00Z",  "lastUpdatedAt": "2026-09-01T10:30:00Z",  "ttlMs": 3600000,  "pollIntervalMs": 5000}}// ③ poll, ④ answer a mid-flight question, ⑤ optionally cancel{"jsonrpc": "2.0", "id": 12, "method": "tasks/get", "params": { "taskId": "t-123", "_meta": { … per-request fields … } }}{"jsonrpc": "2.0", "id": 13, "method": "tasks/update", "params": { "taskId": "t-123", "inputResponses": { … }, "_meta": { … } }}{"jsonrpc": "2.0", "id": 14, "method": "tasks/cancel", "params": { "taskId": "t-123", "_meta": { … } }}// update and cancel are acknowledged with an empty result: { "resultType": "complete" }
What it used to look like (legacy, for comparison only)
Legacybefore
// 2025-11-25: experimental, in the core protocol{"jsonrpc": "2.0", "id": 11, "method": "tasks/result", "params": { "taskId": "t-123" }}// ↑ blocked until the task finished{"jsonrpc": "2.0", "id": 12, "method": "tasks/list"}

Shapes from the ext-tasks schema (schema/2026-07-28/schema.ts). The task fields sit directly on the result, not under a "task" key (that nested key was the 2025-11-25 shape), and createdAt and lastUpdatedAt are required. ttlMs may be null, and pollIntervalMs is optional.

  • Code written against core experimental tasks no longer matches what clients send.
  • You must create tasks durably before responding, persist task IDs, and keep honouring ttlMs.
  • If you return tasks to clients that didn't opt in, those clients can't handle the result.
First time here? Set up the test helper (once per terminal)
ShellSetup
# 1. In a SECOND terminal, start the reference server (Node 18+, no dependencies)curl -sO https://www.diegozuluaga.dev/mcpa/reference-server.mjsnode reference-server.mjs                 # http://localhost:3000/mcp, logs appear here # 2. In THIS terminal, define the helper every test uses (bash or zsh)export MCP=http://localhost:3000/mcpMETA='"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}'mcp() {  # usage: mcp <method> '<json body>' [extra curl args...]  curl -sS -N "$MCP" \    -H 'Content-Type: application/json' \    -H 'Accept: application/json, text/event-stream' \    -H 'MCP-Protocol-Version: 2026-07-28' \    -H "Authorization: Bearer ${MCP_USER:-alice}" \    -H "Mcp-Method: $1" "${@:3}" -d "$2" \    -w '\nHTTP %{http_code}\n'}# Demo auth: the reference server treats the bearer token as the user's name.# Prefix a command with MCP_USER=bob to act as someone else.
ShellTests
META_T='"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{"elicitation":{},"extensions":{"io.modelcontextprotocol/tasks":{}}}}' # 1. The server advertises the extensionmcp server/discover '{"jsonrpc":"2.0","id":"d","method":"server/discover","params":{'"$META"'}}' | grep -o 'io.modelcontextprotocol/tasks' # 2. With opt-in, the slow tool answers at once with resultType "task"TASK=$(mcp tools/call '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"build_report","arguments":{"month":"2026-09"},'"$META_T"'}}' -H 'Mcp-Name: build_report' | tee /dev/stderr | sed -n 's/.*"taskId":"\([^"]*\)".*/\1/p') # 3. Poll (repeat every few seconds): working, then input_required after about 8 smcp tasks/get '{"jsonrpc":"2.0","id":2,"method":"tasks/get","params":{"taskId":"'"$TASK"'",'"$META_T"'}}' # 4. Answer the mid-flight question, then keep polling until "completed"mcp tasks/update '{"jsonrpc":"2.0","id":3,"method":"tasks/update","params":{"taskId":"'"$TASK"'","inputResponses":{"charts":{"action":"accept","content":{"charts":true}}},'"$META_T"'}}' # 5. Without opt-in ($META) the same call does NOT become a task: it streams for 10 s instead# 6. Someone else's task: MCP_USER=bob mcp tasks/get ... must be rejected

Answer all 3 correctly and this lesson is marked as learned.

Q1Which statement about Tasks in 2026-07-28 is true?

Q2Spot the bug: a client that did NOT list io.modelcontextprotocol/tasks in its capabilities calls build_report, and the server answers with resultType "task".

Q3Your phone loses its connection while task t-123 is working. After reconnecting, what should the client do?

Feedback or a correction? Email diego [at] diegozuluaga [dot] dev or open an issue on GitHub.

Content CC BY 4.0 · Code MIT

Tip: ← and → move between lessons. Hover any heading and press # to copy a link to it.