feat: Tasks extension for server and client (ext/tasks subpaths) - #2782
Draft
mattzcarey wants to merge 6 commits into
Draft
mattzcarey wants to merge 6 commits into
mattzcarey wants to merge 6 commits into
Conversation
🦋 Changeset detectedLatest commit: 100decd 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
9 tasks
mattzcarey
force-pushed
the
feat/pluggable-task-workflow
branch
from
September 17, 2026 07:24
774c3f9 to
b04649e
Compare
mattzcarey
added this pull request to stack #2826
September 17, 2026 14:18
mattzcarey
force-pushed
the
feat/pluggable-task-workflow
branch
from
September 17, 2026 14:19
46c22c6 to
b6f148b
Compare
…-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
TasksExtension is the server side of io.modelcontextprotocol/tasks as a ServerExtension: it advertises the capability, serves tasks/get, tasks/update and tasks/cancel, gates task handles on the client capability (-32021, including through a tools/call override for handles minted outside the extension), and offers tasks.create(ctx) for a tool handler to answer with a task handle. TaskStore (create / get / update / cancel over JSON) is the seam a server implements over its own state and execution; how the work runs is the server's own. InMemoryTaskStore is the in-process reference with a writer handle (status, requireInput, complete, fail, cancel signal). Wire types and zod schemas for the extension's 2026-07-28 schema are exported. Stacked on the server extensions seam and on #2599 (explicit-schema handlers escape the era gate), which tasks/get and tasks/cancel need.
TasksClientExtension declares io.modelcontextprotocol/tasks on every request, accepts task handles on tools/call through acceptResultType, and wraps the extension's methods: callTool (a task handle or the plain result), get, update, cancel, and waitFor, which polls at the server's suggested interval to a terminal snapshot. The wire types and zod schemas move to @modelcontextprotocol/core-internal/ext/tasks, shared by both halves and re-exported from each subpath. The server e2e test now drives tools/call through the client extension instead of a raw POST.
mattzcarey
force-pushed
the
feat/pluggable-task-workflow
branch
from
September 17, 2026 14:23
b6f148b to
100decd
Compare
This branch has not been deployed
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
The Tasks extension (
io.modelcontextprotocol/tasks, SEP-2663) as a pair of extensions:@modelcontextprotocol/server/ext/tasks(aServerExtension) and@modelcontextprotocol/client/ext/tasks(aClientExtension).The SDK owns the wire, nothing else.
TasksExtensionadvertises the capability, servestasks/get/tasks/update/tasks/cancelas explicit-schema custom methods, refuses requests that did not declare the extension (-32021), binds tasks to the request principal, and gives a tool handlertasks.create(ctx)to answer with a flatCreateTaskResult(resultType: "task", forwarded verbatim by the 2026 result encoder).tools/callmiddleware gates task handles minted some other way (an external engine's own create) on the same capability.The server owns the task.
TaskStoreis four methods over JSON —create,get,update,cancel— that a server implements over whatever holds its task state. How the work behind a task runs (a queue, a workflow engine, a durable-execution runtime) is invisible to the SDK and deliberately unspecified.InMemoryTaskStoreis the in-process reference; it adds a writerhandle(taskId)(status,requireInput,complete,fail, an abortsignal) so a plain async function can drive a task, and records an opaquecontextper task for an execution to pick up.The client half.
new TasksClientExtension()inClientOptions.extensionsdeclares the capability on every request, acceptsresultType: "task"ontools/call(viaacceptResultTypefrom #2820), and wraps the extension's methods:callToolyields{ kind: 'task', task }or{ kind: 'result', result };get/update/cancelare the three methods one to one;waitForpollstasks/getat the server'spollIntervalMsuntil a terminal snapshot, reporting every snapshot throughonUpdate, and stops on abort without touching the task.Wire types and zod schemas are authored against ext-tasks
2026-07-28, live at@modelcontextprotocol/core-internal/ext/tasks, and are re-exported from both subpaths.Tests
packages/server/test/ext/tasks(10) andpackages/client/test/ext/tasks(4); full suites core-internal 1464, server 497, client 887, all green.tasks.e2e.test.ts— a realClientwithTasksClientExtensionagainst the statelesscreateMcpHandler(freshMcpServerper request, the store as the only shared state): capability advertised, task handle ontools/call,waitForto completion with status,input_required→tasks/update→ result, failure, cooperative cancel via the handle signal,-32602unknown task,-32021as a JSON-RPC error fromtasks/*, fromtasks.create, and from thetools/callmiddleware.tasksClientExtension.test.ts— one instance per client, the capability on every request, task handle vs plain result split,waitForpolling at the task interval throughworking→input_required→completed, abort stops polling without cancelling.inMemoryStore.test.ts— durable create with context, partial answers accumulate and the first answer wins, cancel rejects a pending input wait and later writes are ignored, TTL purge, principal fail-closed, argument validation.SDK interactions worth knowing about
inputResponsesis a reserved multi-round-trip name. The protocol layer lifts it out of every client request's params on the 2026 era and surfaces it atctx.mcpReq.inputResponses.tasks/updateuses the same name, so the handler reads it back from the context (handler-side schema optional, wire schema required). Possibly worth a note in the extension spec.ClientrejectsresultType: "task"ontools/call([v2] Tasks extension: tools/call rejects CreateTaskResult but accepts an omitted discriminator as complete #2637) unless an extension declared it withacceptResultType, which the client extension does. The plainclient.callToolstill rejects a task handle, correctly: aCallToolResultschema cannot describe one. ext-tasks#21 (@modelcontextprotocol/ext-tasks) is a separate, richer requester; this client half is the minimal one that pairs with the server half.notifications/tasksoversubscriptions/listenis not implemented (subscriptions/listen cannot carry extension notifications (blocks notifications/tasks) #2569); polling only.Design notes
registerTaskwith a replayablestep.do/sleep/elicitAPI and an in-memory execution engine). That is out: execution is the server's concern, and the spec-level story is only the API shape. The workflow layer lives on indurable-mcp-server, which becomes oneTaskStoreimplementation among many.@modelcontextprotocol/serverso extensions have a home next to the server they extend.Refs #2189, #2598, #2637, #2569. Requires #2820 and #2599.