feat(server): Tasks extension at @modelcontextprotocol/server/ext/tasks with a pluggable execution engine - #2782
Draft
mattzcarey wants to merge 5 commits into
Draft
feat(server): Tasks extension at @modelcontextprotocol/server/ext/tasks with a pluggable execution engine#2782mattzcarey wants to merge 5 commits into
mattzcarey wants to merge 5 commits into
Conversation
…-universe gate The inbound and outbound era gates rejected any method name that ever appeared in a past protocol revision's registry but is absent from the current era's registry, even when the consumer explicitly registered a handler (or supplied a schema on send) for it. This made extension methods that reuse a historical core method name unreachable: the Tasks extension (SEP-2663) defines `tasks/get` and `tasks/cancel`, both of which the 2025-11-25 revision used for now-removed core methods, so a 2026-era server could never serve them and a 2026-era client could never send them — every attempt answered -32601 or threw MethodNotSupportedByProtocolVersion before the handler or the transport were ever consulted. Both gates now only apply to TYPED dispatch (setRequestHandler(method, handler) inbound, request(method, options) outbound) — exactly the path the SDK's own built-ins (initialize, ping, logging/setLevel) use, which correctly stays era-gated. A method registered or sent with an EXPLICIT schema (setRequestHandler(method, schemas, handler) / request(request, resultSchema, options)) is the extension-authoring path: the consumer supplied their own validation, so a historical registry collision no longer blocks it. Fixes #2598
…cution engine
Adds @modelcontextprotocol/tasks: installTasks(server, { engine }) serves
tasks/get, tasks/update and tasks/cancel as explicit-schema custom methods
and returns registerTask, which runs a tool handler as a replayable
workflow against a Step API (do, sleep, sleepUntil, elicit, offer,
checkInput, status).
Two interfaces keep handlers engine-invariant: TaskEngine (create / get /
update / cancel) and StepJournal (what the step API drives). The package
ships InMemoryTaskEngine as the in-process reference; durable engines
implement the same two seams outside the SDK.
Stacked on #2599 (explicit-schema handlers escape the era gate), which
tasks/get and tasks/cancel need on the 2026-07-28 era.
🦋 Changeset detectedLatest commit: 774c3f9 The changes in this PR will be included in the next version bump. This PR includes changesets to release 6 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
@modelcontextprotocol/client
@modelcontextprotocol/codemod
@modelcontextprotocol/core
@modelcontextprotocol/server
@modelcontextprotocol/server-legacy
@modelcontextprotocol/express
@modelcontextprotocol/fastify
@modelcontextprotocol/hono
@modelcontextprotocol/node
commit: |
2 tasks
…server/ext/tasks The extension is a subpath export of the server package rather than a separate package: src/ext/tasks, built as dist/ext/tasks/index.*, exported as @modelcontextprotocol/server/ext/tasks. Tests move to packages/server/test/ext/tasks; the package README becomes docs/servers/tasks.md.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this adds
A new subpath export,
@modelcontextprotocol/server/ext/tasks: the server side of the Tasks extension (io.modelcontextprotocol/tasks, SEP-2663) for@modelcontextprotocol/server, with the execution engine pluggable.installTasks(server, { engine })servestasks/get,tasks/update,tasks/cancelas explicit-schema custom methods (server.server.setRequestHandler(method, { params }, …)), advertises the extension capability once a task exists, and returnsregisterTask.registerTaskisregisterToolfor long-running work: the tool answers a flatCreateTaskResult(resultType: "task") and the handler runs as a replayable workflow against aStepAPI:do(journaled closure with retries and timeout),sleep/sleepUntil,elicit(→input_required, answered bytasks/update, optional deadline),offer+checkInput(a standing non-blocking input channel),status.TaskEngineis the control seam (create/get/update/cancel, plus an optionalattach(executor)for engines that run handlers in-process).StepJournalis the step seam (whatReplayStepdrives:beginStep/completeStep/failStep/recordSleep/recordElicit/recordOffer/checkInput/setStatus/checkCancel, generation-fenced withStaleLeaseError).InMemoryTaskEngineis the in-process reference implementation: records and journals in aMap, one timer per task computed from the rows, TTL purge, elicit deadlines, principal binding. Nothing durable ships in the SDK. A durable engine (a database plus workers, Durable Objects, a durable-execution runtime) implements the same two interfaces and lives outside the SDK; swapping engines changes theinstallTaskscall and nothing else.The step and journal layers are ported from
durable-mcp-server(Cloudflare Durable Objects engine, on npm asdurable-mcp-server@0.1.0), which would become the first external engine. The wire types and zod schemas are authored against ext-tasks2026-07-28(dcc8d2b).Tests
packages/server/test/ext/tasks: 15 tests (the full server suite is 497 green).tasks.e2e.test.ts— a realClientagainst the statelesscreateMcpHandler(freshMcpServerper request, the engine as the only shared state): task handle ontools/call,do/sleep/statuswith replay verified (the closure runs once across the suspend),input_required→tasks/update→ result, step retries with the task policy,NonRetryableError, handler throw →isErrorresult, cooperative cancel,-32602unknown task,-32021without the extension capability.inMemoryEngine.test.ts— journal semantics a durable engine must reproduce, against a scripted executor: durable create, replay hits and thelatestsuspension boundary,StaleLeaseErrorfencing, blocking input, elicit timeout, offers (never block, answer cuts a pending sleep, consume-once), cancel idle vs running, TTL purge, principal fail-closed.SDK interactions worth knowing about
tasks/getandtasks/cancelwere 2025-11-25 core methods, so the era gate answers-32601before an explicit-schema handler is consulted (v2: extension methods shadowed by legacy spec-method registry — custom tasks/get & tasks/cancel handlers unreachable (-32601 before handler lookup) #2598). This branch merges fix(core-internal): let explicit-schema handlers/calls escape the era-universe gate #2599 so the handlers are reachable; the first two commits are that PR.inputResponsesis a reserved MRTR name. The protocol layer liftsinputResponsesout of every client request's params on the 2026-07-28 era and surfaces it atctx.mcpReq.inputResponses.tasks/updateuses the same name, so the handler reads it back from the context (its handler-side schema makes the field optional; the wire schema keeps it required). Documented indocs/servers/tasks.md; possibly worth a note in the extension spec.tools/callthrows becomeisErrorresults. The-32021refusal for a non-declaring client therefore reaches the caller as a tool error rather than a JSON-RPC error. A router in front of the SDK can enforce the spec'd-32021/ HTTP 400 (durable-mcp-server does); the SDK-level option would be to letMissingRequiredClientCapabilityErrorpass through tool dispatch the wayUrlElicitationRequiredErrordoes.ClientrejectsresultType: "task"ontools/call([v2] Tasks extension: tools/call rejects CreateTaskResult but accepts an omitted discriminator as complete #2637). The e2e test poststools/callraw for that reason; every other method goes through the realClient. The requester half of the extension is ext-tasks#21 (@modelcontextprotocol/ext-tasks), which has no server side — this package is the complementary half.notifications/tasksoversubscriptions/listenis not implemented (blocked by subscriptions/listen cannot carry extension notifications (blocks notifications/tasks) #2569); polling only.Open questions
@modelcontextprotocol/server/ext/tasks, source underpackages/server/src/ext/tasks) so extensions have a home next to the server they extend without a new package per extension. The alternative is aserversub-package of@modelcontextprotocol/ext-tasksin the ext-tasks repo onceparamsrequired to be specified even when empty #21 lands. The SDK's owntasksPluginsketch onfweinberger/f3-tasks-replacesuggests there is appetite for an SDK-side hook.Stepsurface should be narrower for a first release (do/sleep/elicit/status, withoffer/checkInputheld back).Refs #2189, #2598, #2637, #2569.