Thomas Piacentini
← Atelier

Fiche

MCP Tasks, Elicitation and Apps

Texte original · anglais

Explicit scope, MRTR elicitation, durable Tasks, Apps UI resources and safe cancellation on MCP 2026-07-28.

MRTR elicitation round trip

  1. Client sends tools/call id 1 with elicitation: {"form": {}} in _meta capabilities.
  2. Server returns resultType: "input_required" with inputRequests.delete_choice.
  3. Server signs requestState with principal, method, argumentsDigest, candidateIds, nonce, expiry.
  4. Host renders the form. The user picks accept, decline or cancel.
  5. Client retries tools/call with a fresh id 2, inputResponses and the same requestState.
  6. Server verifies the state, re-authorizes the workspace and re-checks containment.
  7. Server claims the nonce once, deletes, returns resultType: "complete".
Sources

Task lifecycle on a stateless core

_metataskIdleaseworkertasks/updateworkertasks/cancelClientdeclares tasks extensionclienttools/callresultType taskprocessDurable storepersist before returnstoreWorkeradvance_workerprocessworkingstatusMessageinput_requiredinputRequestscompletednested resultcancelled or failederror on failed
Nodes and connections
NodeRoleKind
Clientdeclares tasks extensionclient
tools/callresultType taskprocess
Durable storepersist before returnstore
Workeradvance_workerprocess
workingstatusMessage
input_requiredinputRequests
completednested result
cancelled or failederror on failed
FromToConnection
Clienttools/call_meta
tools/callDurable storetaskId
Durable storeWorkerlease
workinginput_requiredworker
input_requiredworkingtasks/update
workingcompletedworker
workingcancelled or failedtasks/cancel
Sources

Task methods and signals

CommandWhat it does
tasks/getFull snapshot. Outer resultType is always complete
tasks/updateSends inputResponses. Returns an empty ack
tasks/cancelRecords cancel intent. The ack is not a final status
subscriptions/listenPOST with a response SSE stream of task snapshots
notifications/tasksFull snapshot tagged with the listen request id
notifications/cancelledRequest cancel on stdio only. Never for a Task
Close the response streamRequest cancel on Streamable HTTP
tasks/status, tasks/result, tasks/listRemoved. Method not found
Sources

Drive a durable task

service = TaskService(directory)file-backed TaskStore
created = service.dispatch(make_request(1, "tools/call",
{"name": "generate_report", "arguments": {"size": "large"}}))
task_id = created["result"]["taskId"]resultType task, working
service = TaskService(directory)new instance reloads it
service.advance_worker(task_id)working to input_required
service.dispatch(make_request(3, "tasks/get", {"taskId": task_id}))
service.dispatch(make_request(4, "tasks/update", {"taskId": task_id,
"inputResponses": {"approve_outline": {"action": "accept",
"content": {"approved": True}}}}))
service.advance_worker(task_id)stage 2 to completed
body, headers = make_http_request(5, "tasks/get", {"taskId": task_id})
headers["Mcp-Name"]equals params.taskId
Sources

Idempotency, buffers and reconnect

ledger = MutationLedger(Path(directory) / "mutations.sqlite3")
ledger.charge("acct-7", 1200, rpc_id=41, idempotency_key="checkout-7")
ledger.charge("acct-7", 1200, rpc_id=42, idempotency_key="checkout-7")
ledger.executionsone execution, same receipt
classify_retry(side_effect=True, idempotency_key=None)unsafe
buffer = BoundedSseBuffer(3)capacity at least two
buffer.push_final(response)final response never dropped
reconnect_plan(previous_subscription_id, affected_resources,
unsafe_mutation_was_in_flight=True)sendLastEventId False
retry_delay_ms("<client>", attempt)capped at 8000 ms
Sources

Elicitation answers

Factoracceptdeclinecancel
MeaningUser submittedUser refusedUser dismissed
Server actionValidate content, continueComplete refusal resultStop safely
NonceConsumed with the deleteConsumed, no deleteNot consumed
Retry laterNoNoYes, until expiry
Resultdeleted: Truedeleted: Falsedeleted: False
Sources

Numbers and defaults

SettingDefaultNote
Protocol version2026-07-28In _meta on every request
Task ttl_ms900_000Measured from creation
Task poll_interval_ms1_000Minimum suggested poll delay
server/discover ttlMs300_000cacheScope public
tools/list ttlMs60_000Sorted by tool name
requestState expiry300Seconds after the form
ReplayStore max_entries1_000Full store gives -32023
Backoff cap8_000250 ms times 2 per attempt
Sources

Errors and failures

SymptomCauseFix
-32602Missing or non-string protocol versionSend the version in _meta
-32022Unsupported version stringRead supported and requested data
-32021Missing form or tasks or ui capabilityDeclare it in this request
-32020Header and body disagreeMatch Mcp-Method and Mcp-Name
Task id gives not foundHandle sent before the store could read itPersist before return
Mutation runs twiceNew JSON-RPC id used as dedupeAdd a durable idempotency key
Hung work never endsProgress resets every deadlineKeep an absolute maximum timeout
UI says stopped, worker runsCancel ack read as finalPoll the Task to a terminal status
Sources

Apps rules

Extension
io.modelcontextprotocol/ui, declared by both peers on each request.
Binding
_meta.ui.resourceUri on the tool in tools/list, before any call.
Resource
ui://notes/timeline.html with MIME text/html;profile=mcp-app.
CSP
empty connectDomains, resourceDomains, frameDomains, baseUriDomains by default.
Bridge
View sends ui/initialize, then ui/notifications/initialized after the host answers.
Fallback
no ui capability gives the tool without _meta.ui and a text result.
Cache key
ui:// URI, server identity and version, content digest, principal if private.
Sources

Scope rules

  • Roots are deprecated. Put scope in a workspaceUri argument or a resource URI.
  • Enforce three layers: authorization, containment and an OS sandbox.
  • Normalize the URI first. Then compare path components, not string prefixes.
  • Never use form mode for passwords, API keys, tokens or payment data.
  • URL mode accept means the user agreed to open the URL. It is not proof of completion.
  • An empty "elicitation": {} means form support. URL-only does not.
Sources

Questions

Who decides that a call becomes a Task?
The server. The client only declares the extension.
Where is the final task result?
Under result in the next tasks/get snapshot.
Input before or after task creation?
Before: MRTR retry. After: tasks/update.
Does ui/initialize start an MCP session?
No. It is the iframe bridge handshake only.
Is an isError: true tool result a failed Task?
No. The Task is completed.
Resume a dropped stream with Last-Event-ID?
No. Open a new listen request and refetch.
Sources
Original source
---
id: mcp-tasks-elicitation-apps
title: MCP Tasks, Elicitation and Apps
code: MT
subtitle: Explicit scope, MRTR elicitation, durable Tasks, Apps UI resources and safe cancellation on MCP 2026-07-28.
hue: 280
sources: phases/13-tools-and-protocols/12-mcp-roots-and-elicitation, phases/13-tools-and-protocols/13-mcp-async-tasks, phases/13-tools-and-protocols/14-mcp-apps, phases/13-tools-and-protocols/29-mcp-reliability-cancellation-and-flow-control
source_hash: 79e5ebd870162891
---

## MRTR elicitation round trip
type: flow
from: phases/13-tools-and-protocols/12-mcp-roots-and-elicitation/docs/en.md, phases/13-tools-and-protocols/12-mcp-roots-and-elicitation/code/main.py

1. **Client**: sends `tools/call` id 1 with `elicitation: {"form": {}}` in `_meta` capabilities.
2. **Server**: returns `resultType: "input_required"` with `inputRequests.delete_choice`.
3. **Server**: signs `requestState` with principal, method, `argumentsDigest`, `candidateIds`, nonce, expiry.
4. **Host**: renders the form. The user picks accept, decline or cancel.
5. **Client**: retries `tools/call` with a fresh id 2, `inputResponses` and the same `requestState`.
6. **Server**: verifies the state, re-authorizes the workspace and re-checks containment.
7. **Server**: claims the nonce once, deletes, returns `resultType: "complete"`.

## Task lifecycle on a stateless core
type: architecture
from: phases/13-tools-and-protocols/13-mcp-async-tasks/docs/en.md, phases/13-tools-and-protocols/13-mcp-async-tasks/code/main.py

```arch
node | client | 1,1 | Client | declares tasks extension | client
node | call | 2,1 | tools/call | resultType task | process
node | store | 3,1 | Durable store | persist before return | store
node | worker | 4,1 | Worker | advance_worker | process
node | working | 1,2 | working | statusMessage |
node | input | 2,2 | input_required | inputRequests |
node | done | 3,2 | completed | nested result |
node | stop | 4,2 | cancelled or failed | error on failed |
edge | client | call | _meta
edge | call | store | taskId
edge | store | worker | lease
edge | working | input | worker
edge | input | working | tasks/update
edge | working | done | worker
edge | working | stop | tasks/cancel
```

## Task methods and signals
type: command-table
from: phases/13-tools-and-protocols/13-mcp-async-tasks/docs/en.md, phases/13-tools-and-protocols/29-mcp-reliability-cancellation-and-flow-control/docs/en.md

| Command | What it does |
|---|---|
| `tasks/get` | Full snapshot. Outer `resultType` is always complete |
| `tasks/update` | Sends `inputResponses`. Returns an empty ack |
| `tasks/cancel` | Records cancel intent. The ack is not a final status |
| `subscriptions/listen` | POST with a response SSE stream of task snapshots |
| `notifications/tasks` | Full snapshot tagged with the listen request id |
| `notifications/cancelled` | Request cancel on stdio only. Never for a Task |
| Close the response stream | Request cancel on Streamable HTTP |
| `tasks/status`, `tasks/result`, `tasks/list` | Removed. Method not found |

## Drive a durable task
type: annotated-code
lang: python
from: phases/13-tools-and-protocols/13-mcp-async-tasks/code/main.py

```python
service = TaskService(directory)                # file-backed TaskStore
created = service.dispatch(make_request(1, "tools/call",
    {"name": "generate_report", "arguments": {"size": "large"}}))
task_id = created["result"]["taskId"]           # resultType task, working
service = TaskService(directory)                # new instance reloads it
service.advance_worker(task_id)                 # working to input_required
service.dispatch(make_request(3, "tasks/get", {"taskId": task_id}))
service.dispatch(make_request(4, "tasks/update", {"taskId": task_id,
    "inputResponses": {"approve_outline": {"action": "accept",
                                           "content": {"approved": True}}}}))
service.advance_worker(task_id)                 # stage 2 to completed
body, headers = make_http_request(5, "tasks/get", {"taskId": task_id})
headers["Mcp-Name"]                             # equals params.taskId
```

## Idempotency, buffers and reconnect
type: annotated-code
lang: python
from: phases/13-tools-and-protocols/29-mcp-reliability-cancellation-and-flow-control/code/main.py

```python
ledger = MutationLedger(Path(directory) / "mutations.sqlite3")
ledger.charge("acct-7", 1200, rpc_id=41, idempotency_key="checkout-7")
ledger.charge("acct-7", 1200, rpc_id=42, idempotency_key="checkout-7")
ledger.executions                               # one execution, same receipt
classify_retry(side_effect=True, idempotency_key=None)  # unsafe
buffer = BoundedSseBuffer(3)                    # capacity at least two
buffer.push_final(response)                     # final response never dropped
reconnect_plan(previous_subscription_id, affected_resources,
               unsafe_mutation_was_in_flight=True)  # sendLastEventId False
retry_delay_ms("<client>", attempt)             # capped at 8000 ms
```

## Elicitation answers
type: comparison
from: phases/13-tools-and-protocols/12-mcp-roots-and-elicitation/docs/en.md, phases/13-tools-and-protocols/12-mcp-roots-and-elicitation/code/main.py

| Factor | accept | decline | cancel |
|---|---|---|---|
| Meaning | User submitted | User refused | User dismissed |
| Server action | Validate content, continue | Complete refusal result | Stop safely |
| Nonce | Consumed with the delete | Consumed, no delete | Not consumed |
| Retry later | No | No | Yes, until expiry |
| Result | `deleted: True` | `deleted: False` | `deleted: False` |

## Numbers and defaults
type: defaults
from: phases/13-tools-and-protocols/13-mcp-async-tasks/code/main.py, phases/13-tools-and-protocols/12-mcp-roots-and-elicitation/code/main.py, phases/13-tools-and-protocols/14-mcp-apps/code/main.py, phases/13-tools-and-protocols/29-mcp-reliability-cancellation-and-flow-control/code/main.py

| Setting | Default | Note |
|---|---|---|
| Protocol version | 2026-07-28 | In `_meta` on every request |
| Task `ttl_ms` | 900_000 | Measured from creation |
| Task `poll_interval_ms` | 1_000 | Minimum suggested poll delay |
| `server/discover` `ttlMs` | 300_000 | `cacheScope` public |
| `tools/list` `ttlMs` | 60_000 | Sorted by tool name |
| `requestState` expiry | 300 | Seconds after the form |
| `ReplayStore` `max_entries` | 1_000 | Full store gives -32023 |
| Backoff cap | 8_000 | 250 ms times 2 per attempt |

## Errors and failures
type: troubleshooting
from: phases/13-tools-and-protocols/12-mcp-roots-and-elicitation/docs/en.md, phases/13-tools-and-protocols/13-mcp-async-tasks/docs/en.md, phases/13-tools-and-protocols/14-mcp-apps/docs/en.md, phases/13-tools-and-protocols/29-mcp-reliability-cancellation-and-flow-control/docs/en.md

| Symptom | Cause | Fix |
|---|---|---|
| `-32602` | Missing or non-string protocol version | Send the version in `_meta` |
| `-32022` | Unsupported version string | Read `supported` and `requested` data |
| `-32021` | Missing form or tasks or ui capability | Declare it in this request |
| `-32020` | Header and body disagree | Match `Mcp-Method` and `Mcp-Name` |
| Task id gives not found | Handle sent before the store could read it | Persist before return |
| Mutation runs twice | New JSON-RPC id used as dedupe | Add a durable idempotency key |
| Hung work never ends | Progress resets every deadline | Keep an absolute maximum timeout |
| UI says stopped, worker runs | Cancel ack read as final | Poll the Task to a terminal status |

## Apps rules
type: concepts
from: phases/13-tools-and-protocols/14-mcp-apps/docs/en.md, phases/13-tools-and-protocols/14-mcp-apps/code/main.py

- **Extension**: `io.modelcontextprotocol/ui`, declared by both peers on each request.
- **Binding**: `_meta.ui.resourceUri` on the tool in `tools/list`, before any call.
- **Resource**: `ui://notes/timeline.html` with MIME `text/html;profile=mcp-app`.
- **CSP**: empty `connectDomains`, `resourceDomains`, `frameDomains`, `baseUriDomains` by default.
- **Bridge**: View sends `ui/initialize`, then `ui/notifications/initialized` after the host answers.
- **Fallback**: no ui capability gives the tool without `_meta.ui` and a text result.
- **Cache key**: `ui://` URI, server identity and version, content digest, principal if private.

## Scope rules
type: tips
from: phases/13-tools-and-protocols/12-mcp-roots-and-elicitation/docs/en.md

- Roots are deprecated. Put scope in a `workspaceUri` argument or a resource URI.
- Enforce three layers: authorization, containment and an OS sandbox.
- Normalize the URI first. Then compare path components, not string prefixes.
- Never use form mode for passwords, API keys, tokens or payment data.
- URL mode `accept` means the user agreed to open the URL. It is not proof of completion.
- An empty `"elicitation": {}` means form support. URL-only does not.

## Questions
type: qa
from: phases/13-tools-and-protocols/13-mcp-async-tasks/docs/en.md, phases/13-tools-and-protocols/14-mcp-apps/docs/en.md, phases/13-tools-and-protocols/29-mcp-reliability-cancellation-and-flow-control/docs/en.md

- **Who decides that a call becomes a Task?** The server. The client only declares the extension.
- **Where is the final task result?** Under `result` in the next `tasks/get` snapshot.
- **Input before or after task creation?** Before: MRTR retry. After: `tasks/update`.
- **Does `ui/initialize` start an MCP session?** No. It is the iframe bridge handshake only.
- **Is an `isError: true` tool result a failed Task?** No. The Task is `completed`.
- **Resume a dropped stream with `Last-Event-ID`?** No. Open a new listen request and refetch.
Code et livrables 1
Ce que vous en retenez

Enregistré dans votre carnet sur cet appareil.

Sources et limites

Les repères de lecture sont déclaratifs. Les résultats de tests sont enregistrés séparément ; ils valent pour les cas exécutés.

AI Engineering from Scratch · 3f9715c · MIT

Télécharger ce fichier ↓