ADR-0037 — mocapi-tasks: the MCP Tasks extension and its execution model¶
- Status: Amended by ADR-0040
- Date: 2026-08-02
Context¶
MCP Tasks (SEP-2663, modelcontextprotocol/ext-tasks) was declared
declined in ADR-0022:
extensions are opt-in, and the task lifecycle looked like a substantial
state machine mocapi's synchronous handler model didn't need. A
2026-07-31 design (commit 309d01fa, unmerged branch
feat/mcp-tasks-extension-design) revisited that and proposed an
explicit task API: a task-eligible tool declared a TaskContext
parameter, returned CreateTaskResult, and called task.elicit(...),
which blocked the background thread parked on the store. Task tools
were task-only — -32003 for non-capable clients, no synchronous
degrade. That reintroduced exactly the parked-continuation liabilities
ADR-0021 rejected for wire
elicitation: a thread that dies with its node, no idempotency contract,
and a bespoke elicit API duplicating what MRTR replay already does.
Meanwhile ADR-0031
(ServerCapabilitiesCustomizer) and the MCP Apps precedent
(ADR-0033) established that
an optional module can add capability and behavior with zero changes to
a stateless core — the seam the 07-31 design anticipated has since
shipped. This ADR supersedes the 07-31 design's surface and the Tasks
entry in ADR-0022.
Decision¶
Add an optional module, mocapi-tasks, implementing the extension's
polling model over tools/call only.
- Opt-in is one annotation.
@McpTaskon an unmodified@McpToolmethod is the entire task-enabling surface; the tool body never knows whether it is running as a task (transparency contract). - Decision rule: no
@McpTask→ always synchronous.@McpTask+ client declares thetaskscapability →CreateTaskResult, body runs as a task.@McpTask+ non-capable client → synchronous execution unlessrequired = true, which rejects instead of degrading. - Resume is MRTR replay through the store, not a parked thread.
tasks/updatemergesinputResponsesinto the ledger and, iff the mutation itself observedinput_requiredwith outstanding keys consumed, flips toworkingand spawns the next execution — a duplicate concurrenttasks/updateobservesworkingand spawns nothing. Single-resume falls out of the store's atomicity, not a check-then-act race. The ADR-0021 idempotency contract applies unchanged; only the ledger's carrier differs — see ADR-0038 for the shared execution core this depends on. - Guards re-run every execution under a fresh
ContextSnapshotcaptured from the triggering dispatch thread (tools/callfor execution #1,tasks/updatefor #2+). Both are live, authenticated requests from the bound principal, so the six-stratum chain — guards included — evaluates under a legitimate security context every time. No authorization special case, and resume works cross-node because auth arrives with each triggering request rather than being stored. - Progress routes to
statusMessage, notnotifications/progress/notifications/message— a task'sMcpProgressSourcewrites a formatted string via store mutation. Monotonicity validation (ADR-0025) runs identically in both modes. - Cancel sticks; terminal states are final.
tasks/cancelatomically flips any non-terminal status tocancelled; an in-flight execution is not interrupted (consistent with the ADR-0022 cancellation stance) — its output is discarded by construction once the record is terminal (TaskRecord's transition helpers no-op). - Errors:
failedis reserved for JSON-RPC-level errors (engine failure, e.g. a replay ledger fingerprint mismatch →-32602, the same code the wire carrier uses for the identical idempotency violation). A tool-level error (isError = true) surfaces ascompletedwith the error inresult, per spec. Unknown, expired, or foreign-principaltaskIdall report the identical-32602"Unknown task" — no existence leak. TaskStoreSPI: atomic-mutation contract, in-memory default, no new dependencies. All engine semantics (single-resume, terminal finality, no-resurrection) derive from decisions made insideupdate's mutation and from what the returned record shows actually happened.InMemoryTaskStoreis the shipped default (@ConditionalOnMissingBean) and logs a prominent WARN when it activates (mirroring themocapi.mrtr.secretwarning): process-local, not multi-node safe, dies on restart.TaskStoreContractTestships in a test-jar so any external implementation proves itself against the same bar.- v1 scope: polling only (no
notifications/tasks, nosubscriptions/listen— declined globally per ADR-0022);tools/callonly; elicitation form-mode only; finite TTLs (defaultPT1H, poll defaultPT2S, overridable per-tool or viamocapi.tasks.default-*). - Extension model types live in
mocapi-tasks, notmocapi-model— I7 scopes the model to the coreschema.ts, and ADR-0033 already established module-local extension types. - Multi-node deployment requires a shared
TaskStore. Plain hash-on-Mcp-Nameload balancing does not give create→poll affinity (creatingtools/callhashes on tool name; the follow-uptasks/gethashes ontaskId), and the spec is silent on taskId→instance affinity. Seedocs/design/tasks.md. - Constitution I1 gains a scoped exception: core's request model
stays stateless;
mocapi-tasksconfines all task state behind theTaskStoreSPI (updated inconstitution.md).
Error code: -32021, not the extension draft's -32003¶
The extension's draft text still says -32003 for a server that cannot
serve a request without a task. That's stale: -32003 sits in mocapi's
own implementation-defined sub-range
(I9), while the core
2026-07-28 registry defines MissingRequiredClientCapabilityErrorData.CODE
as -32021 for exactly this case — the same translator elicitation
already uses. mocapi follows the core registry:
TaskRequiredExceptionTranslator emits -32021.
Conformance verification (2026-08-02): exercised against
@modelcontextprotocol/conformance@0.2.0-alpha.10's tasks-required-task-error
and tasks-capability-negotiation scenarios (run individually with
--scenario <name> --force — the suite's 10 tasks scenarios are tagged
[extension] and excluded from --suite all regardless of
--spec-version; see mocapi-conformance/README.md). The suite confirms
-32021 is correct: both scenarios assert code === -32021 explicitly and
both pass. This closes the open question — mocapi's choice to follow the
core registry over the extension draft's stale -32003 stands.
Reconciling the full tasks-scenario run surfaced two real bugs, both fixed
in mocapi-tasks: (1) tasks/get/update/cancel had no capability
check at all, so a non-capable caller got -32602 "Unknown task" instead
of -32021, fixed in McpTasksService.requireTaskCapable; (2) a
malformed tasks/update inputResponses entry failed Jackson's
deduction-based InputResponse typing and errored the whole request
instead of being ignored per SEP-2322's SHOULD — fixed by typing
UpdateTaskParams.inputResponses as Map<String, JsonNode> and
converting each outstanding entry leniently in McpTasksService, with no
mocapi-model change. Two architectural v1-scope limitations remain
waived in conformance-expected-failures.yaml — simultaneous multi-key
inputRequests (tasks-mrtr-input:tasks-mrtr-partial-fulfillment; see
the non-goals below) and tasks-mrtr-composition's
MRTR-then-escalate-to-task shape — see the conformance README for detail.
Rejected alternatives¶
- The 07-31 blocking-
TaskContextdesign — parked threads reintroduce exactly the liabilities MRTR replay was adopted to avoid. - Substrate dependency in mocapi — a
TaskStoreadapter belongs on the Substrate side; mocapi adds zero new dependencies for tasks.
Amended (ADR-0040, 2026-08-03): Reversed. Substrate 0.8.0 shipped token
compareAndSetacross all nine backends, which is exactly the primitiveTaskStore.update's atomicity contract needs; with that primitive available, mocapi ships the adapter itself (mocapi-tasks-substrate) rather than asking Substrate to depend on mocapi'sTaskRecord/TaskStoretypes instead. - Hand-rolled JDBC/JPA/Redis stores in-tree — rebuilding a backend fleet mocapi has no business maintaining; users bring their own bean. - Event-sourced store — deterministic conflict resolution, but the fold/dedup/retention machinery is overkill for a five-state lifecycle; recorded as the fallback if tasks ever need real history. - Client-echoed version numbers ontasks/update/cancel— not spec-viable (params are fixed) and wouldn't help the worst race anyway (cancel vs. the task's own executing thread).
Consequences¶
What this buys us. A tool author adds one annotation and gets dual-mode execution for free. The replay core, idempotency contract, and context-injection model are shared with wire MRTR, so there is exactly one place ordinals, fingerprints, and error mapping can drift. Core mocapi is unaffected when the module is absent — see ADR-0038 for the seams it sits on.
Costs. Multi-node deployments must supply a shared TaskStore. A
working execution whose node dies is orphaned until TTL expiry —
arbitrary Java compute is not checkpointable, a documented limitation,
not a bug. -32021 is confirmed correct against the conformance suite
(above); -32003 was never emitted.
Non-goals. notifications/tasks push, task-augmenting
prompts/get/resources/read, sampling/roots inputRequests, URL-mode
elicitation, pollIntervalMs-based rate limiting, shipped cluster-store
adapters, and mid-execution thread interruption on cancel are out of
scope for v1. So is more than one simultaneously-pending inputRequests
key per task: the replay engine's single-pending-key-per-round model
(decision 3, ADR-0021) surfaces at most one outstanding input-required
exception per execution by construction — confirmed against the
conformance suite's tasks-mrtr-input:tasks-mrtr-partial-fulfillment
check (waived in conformance-expected-failures.yaml), not a bug.
This ADR flips the Tasks entry in ADR-0022 from declined to accepted-and-implemented.
Code anchors:
mocapi-tasks/src/main/java/com/callibrity/mocapi/tasks/McpTask.javamocapi-tasks/src/main/java/com/callibrity/mocapi/tasks/McpTasksService.javamocapi-tasks/src/main/java/com/callibrity/mocapi/tasks/TaskToolCallDispatcher.javamocapi-tasks/src/main/java/com/callibrity/mocapi/tasks/store/TaskStore.javamocapi-tasks/src/main/java/com/callibrity/mocapi/tasks/store/InMemoryTaskStore.javamocapi-tasks/src/main/java/com/callibrity/mocapi/tasks/engine/TaskExecutionEngine.javamocapi-tasks/src/main/java/com/callibrity/mocapi/tasks/TaskRequiredExceptionTranslator.javamocapi-autoconfigure/src/main/java/com/callibrity/mocapi/tasks/MocapiTasksAutoConfiguration.java