Skip to content

feat(server): Tasks extension at @modelcontextprotocol/server/ext/tasks - #2782

Draft
mattzcarey wants to merge 5 commits into
feat/server-extensionsfrom
feat/pluggable-task-workflow
Draft

mattzcarey wants to merge 5 commits into
feat/server-extensionsfrom
feat/pluggable-task-workflow

Conversation

@mattzcarey

@mattzcarey mattzcarey commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Draft, second of a stack on #2820 (server extensions). Also carries #2599 merged in, because tasks/get and tasks/cancel were 2025-11-25 core methods and are unreachable on the 2026-07-28 era without its explicit-schema era-gate exemption. Review the last commit only; the rest is the base.

What this adds

@modelcontextprotocol/server/ext/tasks: the server side of the Tasks extension (io.modelcontextprotocol/tasks, SEP-2663) as a ServerExtension.

const store = new InMemoryTaskStore();
const tasks = new TasksExtension(store);
const server = new McpServer(info, { extensions: [tasks] });

server.registerTool('send_report', { inputSchema }, async ({ to }, ctx) => {
    const task = await tasks.create(ctx, { ttlMs: 3_600_000 });
    void sendReport(store.handle(task.taskId), to); // the server's own execution
    return task;
});

The SDK owns the wire, nothing else. TasksExtension advertises the capability, serves tasks/get / tasks/update / tasks/cancel as explicit-schema custom methods, refuses requests that did not declare the extension (-32021), binds tasks to the request principal, and gives a tool handler tasks.create(ctx) to answer with a flat CreateTaskResult (resultType: "task", forwarded verbatim by the 2026 encode seam). A tools/call override gates task handles minted some other way (an external engine's own create) on the same capability.

The server owns the task. TaskStore is 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. InMemoryTaskStore is the in-process reference; it adds a writer handle(taskId) (status, requireInput, complete, fail, an abort signal) so a plain async function can drive a task, and records an opaque context per task for an execution to pick up.

Wire types and zod schemas are authored against ext-tasks 2026-07-28 and exported.

Tests

packages/server/test/ext/tasks, 10 tests; full server suite 497 green.

  • tasks.e2e.test.ts — a real Client against the stateless createMcpHandler (fresh McpServer per request, the store as the only shared state): capability advertised, task handle on tools/call, status and completion, input_requiredtasks/update → result, failure, cooperative cancel via the handle signal, -32602 unknown task, -32021 as a JSON-RPC error from tasks/*, from tasks.create, and from the tools/call override.
  • 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

  1. inputResponses is 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 at ctx.mcpReq.inputResponses. tasks/update uses 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.
  2. The Client rejects resultType: "task" on tools/call ([v2] Tasks extension: tools/call rejects CreateTaskResult but accepts an omitted discriminator as complete #2637). The e2e test posts tools/call raw; every other method goes through the real Client. The requester half is ext-tasks#21 (@modelcontextprotocol/ext-tasks), which has no server side — this is the complementary half.
  3. notifications/tasks over subscriptions/listen is not implemented (subscriptions/listen cannot carry extension notifications (blocks notifications/tasks) #2569); polling only.

Design notes

  • An earlier revision of this PR shipped a workflow layer (registerTask with a replayable step.do / sleep / elicit API 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 in durable-mcp-server, which becomes one TaskStore implementation among many.
  • No new package: a subpath of @modelcontextprotocol/server so extensions have a home next to the server they extend.

Refs #2189, #2598, #2637, #2569. Requires #2820 and #2599.

freya0926 and others added 2 commits August 2, 2026 00:30
…-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
@changeset-bot

changeset-bot Bot commented Sep 10, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: b04649e

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 6 packages
Name Type
@modelcontextprotocol/client Minor
@modelcontextprotocol/server Minor
@modelcontextprotocol/codemod Minor
@modelcontextprotocol/core Minor
@modelcontextprotocol/server-legacy Minor
@modelcontextprotocol/core-internal Patch

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

@pkg-pr-new

pkg-pr-new Bot commented Sep 10, 2026

Copy link
Copy Markdown

Open in StackBlitz

@modelcontextprotocol/client

npm i https://pkg.pr.new/@modelcontextprotocol/client@2782

@modelcontextprotocol/codemod

npm i https://pkg.pr.new/@modelcontextprotocol/codemod@2782

@modelcontextprotocol/core

npm i https://pkg.pr.new/@modelcontextprotocol/core@2782

@modelcontextprotocol/server

npm i https://pkg.pr.new/@modelcontextprotocol/server@2782

@modelcontextprotocol/server-legacy

npm i https://pkg.pr.new/@modelcontextprotocol/server-legacy@2782

@modelcontextprotocol/express

npm i https://pkg.pr.new/@modelcontextprotocol/express@2782

@modelcontextprotocol/fastify

npm i https://pkg.pr.new/@modelcontextprotocol/fastify@2782

@modelcontextprotocol/hono

npm i https://pkg.pr.new/@modelcontextprotocol/hono@2782

@modelcontextprotocol/node

npm i https://pkg.pr.new/@modelcontextprotocol/node@2782

commit: 774c3f9

@mattzcarey mattzcarey changed the title feat(tasks): server-side Tasks extension package with a pluggable execution engine feat(server): Tasks extension at @modelcontextprotocol/server/ext/tasks with a pluggable execution engine Sep 11, 2026
# Conflicts:
#	packages/core-internal/src/shared/protocol.ts
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.
@mattzcarey
mattzcarey force-pushed the feat/pluggable-task-workflow branch from 774c3f9 to b04649e Compare September 17, 2026 07:24
@mattzcarey
mattzcarey changed the base branch from main to feat/server-extensions September 17, 2026 07:24
@mattzcarey mattzcarey changed the title feat(server): Tasks extension at @modelcontextprotocol/server/ext/tasks with a pluggable execution engine feat(server): Tasks extension at @modelcontextprotocol/server/ext/tasks Sep 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants