[SEP-2663] refactor!: remove 2025-11 experimental tasks#2128
[SEP-2663] refactor!: remove 2025-11 experimental tasks#2128felixweinberger wants to merge 1 commit into
Conversation
…ences) Removes the 2025-11 experimental tasks side-channel through Protocol: TaskManager, processInbound*/processOutbound*, task interception, the experimental.tasks.* client/server accessors, and all task-augmented request handling. Also extends beyond the implementation deletion to scrub remaining references in examples, docs, and comments. The only task-related symbol remaining in packages/*.ts is `taskSupport` in ToolExecutionSchema, kept solely to match spec.types.ts (which still declares it); both are removed together in the next commit (spec regen). CHANGELOG entries are preserved (historical record). Migration docs retain a brief removal note. `microtask`/`platformBackgroundTask` are JS/platform terminology, not MCP tasks. Satisfies: SEP-2663 (core-removal half; tasks are now Extensions Track).
|
@modelcontextprotocol/client
@modelcontextprotocol/server
@modelcontextprotocol/express
@modelcontextprotocol/fastify
@modelcontextprotocol/hono
@modelcontextprotocol/node
commit: |
|
@claude review |
| // Register a long-running tool that demonstrates server-initiated disconnect | ||
| server.registerTool( | ||
| 'long-task', | ||
| 'long-operation', | ||
| { | ||
| description: 'A long-running task that sends progress updates. Server will disconnect mid-task to demonstrate polling.' | ||
| description: 'A long-running operation that sends progress updates. Server will disconnect mid-stream to demonstrate polling.' | ||
| }, | ||
| async (ctx): Promise<CallToolResult> => { | ||
| const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); | ||
|
|
||
| console.log(`[${ctx.sessionId}] Starting long-task...`); | ||
| console.log(`[${ctx.sessionId}] Starting long-operation...`); |
There was a problem hiding this comment.
🔴 This PR renames the tool registered in ssePollingExample.ts from long-task to long-operation (and updates clientGuide.examples.ts to match), but the paired demo client examples/client/src/ssePollingClient.ts still calls tools/call with name: 'long-task' (line 77, plus stale log/comment text at lines 68–69). Running the client against the server now fails with a tool-not-found error; update ssePollingClient.ts to use long-operation to keep the example pair working.
Extended reasoning...
What changed
examples/server/src/ssePollingExample.ts previously registered a tool named long-task. This PR renames it to long-operation (line 40), updates the description and log strings in that file, and also updates the unrelated snippet in examples/client/src/clientGuide.examples.ts:223 from long-task to long-operation — confirming the rename was intended to be repo-wide.
What was missed
examples/client/src/ssePollingClient.ts is the documented runnable companion to ssePollingExample.ts (both example READMEs list them as the SSE-polling demo pair: "SSE polling client (legacy)" ↔ "SSE polling demo server"). That file is not touched by this PR and still references the old tool name:
- Line 68:
// Call the long-task tool - Line 69:
console.log('[Client] Calling long-task tool...'); - Line 77:
name: 'long-task',inside thetools/callrequest params
Why this breaks the demo
After this PR, the server registers only long-operation. When the client sends tools/call with name: 'long-task', McpServer's tools/call handler (packages/server/src/server/mcp.ts) looks up this._registeredTools['long-task'], finds nothing, and throws ProtocolError(InvalidParams, 'Tool long-task not found'). The whole point of this paired demo — showing server-initiated SSE disconnect and resumable polling mid-tool-call — never gets exercised.
Step-by-step proof
pnpm tsx examples/server/src/ssePollingExample.ts— server starts and registerslong-operation(line 40 after this PR).pnpm tsx examples/client/src/ssePollingClient.ts— client connects, then at line 77 sends{ method: 'tools/call', params: { name: 'long-task', ... } }.- Server's
tools/callhandler doesthis._registeredTools[request.params.name]→undefined→ throwsTool long-task not found. - Client receives a JSON-RPC error and the demo terminates without ever demonstrating the SSE disconnect/reconnect flow it exists to show.
Why nothing else catches it
The example clients are not type-checked against the example servers' tool registries, and there is no integration test that runs ssePollingClient.ts against ssePollingExample.ts. pnpm test:all passing (per the PR description) does not exercise the example pair.
Fix
Update examples/client/src/ssePollingClient.ts lines 68–69 (comment + log text) and line 77 (name: 'long-task' → name: 'long-operation') to match the renamed tool.
| **Unchanged:** the storage layer (`TaskStore`, `InMemoryTaskStore`, `CreateTaskOptions`, `isTerminal`). It will be consumed by the SEP-2663 server-directed plugin in a follow-up. | ||
|
|
||
| // Handler context had number | null | undefined | ||
| server.setRequestHandler('tools/call', async (request, ctx) => { | ||
| const ttl: number | null | undefined = ctx.task?.requestedTtl; | ||
| }); | ||
| ``` | ||
| There is no migration path for the removed surface; it was always `@experimental`. Under SEP-2663, tasks reattach via a `DispatchMiddleware` (`mcp.use(tasksPlugin({ store }))`) and handlers read task context from `ctx.ext.task` instead of `ctx.task`. | ||
|
|
||
| **After (v2):** | ||
|
|
||
| ```typescript | ||
| // Omit ttl to let the server decide (server may return null for unlimited) | ||
| const result = await client.callTool({ | ||
| name: 'long-task', | ||
| arguments: {}, | ||
| task: {} | ||
| }); | ||
|
|
||
| // Handler context is now number | undefined | ||
| server.setRequestHandler('tools/call', async (request, ctx) => { | ||
| const ttl: number | undefined = ctx.task?.requestedTtl; | ||
| }); | ||
| ``` | ||
| #### `TaskCreationParams.ttl` no longer accepts `null` | ||
|
|
||
| > **Note:** These task APIs are marked `@experimental` and may change without notice. | ||
| `TaskCreationParams.ttl` (the storage-layer creation parameter) is now `number | undefined`; `null` is no longer accepted. Per the MCP spec, `null` TTL (unlimited lifetime) is only valid in server responses (`Task.ttl`), not in creation requests. Omit `ttl` to let the store decide. This is a storage-interface change and is independent of the Protocol-level removals above. |
There was a problem hiding this comment.
🔴 The migration guide tells v1 users that the storage layer (TaskStore, InMemoryTaskStore, CreateTaskOptions, isTerminal) is "Unchanged" and will be consumed by a SEP-2663 follow-up — but this same PR deletes all of those symbols (packages/core/src/experimental/tasks/interfaces.ts, stores/inMemory.ts) and removes their re-exports from packages/core/src/exports/public/index.ts and packages/core/src/index.ts. The same applies to docs/migration-SKILL.md:491, and the adjacent TaskCreationParams.ttl subsection documents a type (TaskCreationParams) that is also deleted here. The docs should say these were removed (and will return with the SEP-2663 plugin), or the storage-layer types should be retained.
Extended reasoning...
What the docs claim vs. what the diff ships
docs/migration.md (around line 869) and docs/migration-SKILL.md (around line 491), both rewritten in this PR, contain:
Unchanged: the storage layer (
TaskStore,InMemoryTaskStore,CreateTaskOptions,isTerminal). It will be consumed by the SEP-2663 server-directed plugin in a follow-up.
But the same diff:
- Deletes
packages/core/src/experimental/tasks/interfaces.ts— which definedTaskStore,CreateTaskOptions, andisTerminal. - Deletes
packages/core/src/experimental/tasks/stores/inMemory.ts— which definedInMemoryTaskStore(andInMemoryTaskMessageQueue). - Deletes the experimental barrel
packages/core/src/experimental/index.ts. - Removes the re-exports from
packages/core/src/exports/public/index.ts(theexport type { ... TaskStore ... },export { isTerminal }, andexport { InMemoryTaskMessageQueue, InMemoryTaskStore }lines). - Removes the
export * from './experimental/index.js';line frompackages/core/src/index.ts.
After this PR, packages/core/src/experimental/ does not exist, and a grep for TaskStore|InMemoryTaskStore|CreateTaskOptions|isTerminal across packages/core/src returns nothing. The PR description itself states the grep audit confirms no task references remain in packages/**.
Why this matters
The migration guide is the surface a v1 user reads to learn what is and isn't still importable. "Unchanged" is an explicit, load-bearing claim — it tells the reader they do not need to touch any code that depends on these symbols. A user with import { InMemoryTaskStore } from '@modelcontextprotocol/core' (or /server) who reads this section will conclude their import still resolves, ship the change, and then hit a build failure that contradicts the docs they just followed.
Concrete proof
- A v1 user has
import { InMemoryTaskStore, isTerminal } from '@modelcontextprotocol/core';in their server. - They upgrade to v2 and read the migration guide.
docs/migration.mdline ~869 says these are "Unchanged." - They leave that import alone.
tscfails:Module '"@modelcontextprotocol/core"' has no exported member 'InMemoryTaskStore'.— becausepackages/core/src/exports/public/index.tsno longer re-exports it and the source file no longer exists.- The user now believes the docs are wrong (correctly) and has to grep the package to figure out what actually shipped.
Adjacent issue: the TaskCreationParams.ttl subsection
Immediately after the "Unchanged" claim, migration.md keeps a subsection titled "TaskCreationParams.ttl no longer accepts null" that describes a type narrowing as if TaskCreationParams still exists. But TaskCreationParamsSchema is deleted from packages/core/src/types/schemas.ts and TaskCreationParams is removed from packages/core/src/types/types.ts (it now only survives as a MISSING_SDK_TYPES entry in spec.types.test.ts). Documenting a type narrowing for a type that no longer exists compounds the confusion.
How to fix
Either:
- Update both docs to say the storage-layer types and
TaskCreationParamswere removed in this PR and will return as part of the SEP-2663 server-directed plugin (a one-line edit in each file), or - If the intent really was to keep the storage layer importable for the follow-up, restore
interfaces.ts,stores/inMemory.ts, and their re-exports from the public barrels.
Given the PR description ("grep audit confirms no task references remain in packages/**"), the first option matches the implementation; the docs are what needs to change.
| .optional() /** | ||
| * Extensions that the client supports. Keys are extension identifiers (vendor-prefix/extension-name). | ||
| */ | ||
| */, | ||
| extensions: z.record(z.string(), JSONObjectSchema).optional() |
There was a problem hiding this comment.
🟡 Removing the tasks: ...Schema.optional(), entries collapsed the JSDoc block for the extensions field onto the previous property's .optional() call in both ClientCapabilitiesSchema (~line 326) and ServerCapabilitiesSchema (~line 401), so it's now trailing trivia on the wrong declaration. Move the /** Extensions that the ... */ comment back onto its own line immediately before extensions: in both schemas so editors and typedoc associate it with the right key.
Extended reasoning...
What happened
When the tasks: ClientTasksCapabilitySchema.optional(), (and ServerTasksCapabilitySchema.optional(),) entries were deleted from ClientCapabilitiesSchema and ServerCapabilitiesSchema, the JSDoc block for the next field (extensions) got reflowed onto the same logical statement as the previous field's .optional() call. The result in both schemas now looks like:
.optional() /**
* Extensions that the client supports. Keys are extension identifiers (vendor-prefix/extension-name).
*/,
extensions: z.record(z.string(), JSONObjectSchema).optional()Why it matters
In TypeScript, JSDoc attaches to a declaration via leading trivia — the comment must appear immediately before the declaration. After this reflow, the /** Extensions ... */ block is sandwiched between .optional() and the comma terminating the previous property assignment (roots in ClientCapabilitiesSchema, tools in ServerCapabilitiesSchema), so it parses as trailing trivia on that property rather than leading trivia on extensions:. Editors (hover/quick-info) and typedoc will no longer surface this description for ClientCapabilities.extensions / ServerCapabilities.extensions. These are public-API schema definitions, so the lost docs are externally visible.
Why nothing else catches it
The Zod schema is structurally unchanged — extensions still parses identically — so no test fails. Prettier accepts a comment in this position and won't move it. Only doc generation and editor tooling silently degrade.
Concrete walk-through (ClientCapabilitiesSchema)
- Before the change, lines were:
.optional(), /** * Present if the client supports task creation. */ tasks: ClientTasksCapabilitySchema.optional(), /** * Extensions that the client supports. ... */ extensions: z.record(z.string(), JSONObjectSchema).optional()
- The deletion removed the
tasks:line and its JSDoc, but the closing,that used to follow.optional()on therootsblock now precedes theextensionsJSDoc on the same logical line, producing.optional() /** ... */,. - Hover over
extensionsin an editor → no description, because the comment belongs to therootsproperty assignment.
The identical sequence happened in ServerCapabilitiesSchema (~line 401) with the tools property and ServerTasksCapabilitySchema.
Fix
Restore the comment to its own lines immediately before extensions: in both schemas:
.optional(),
/**
* Extensions that the client supports. Keys are extension identifiers (vendor-prefix/extension-name).
*/
extensions: z.record(z.string(), JSONObjectSchema).optional()(and the equivalent for ServerCapabilitiesSchema). No runtime behavior change; purely a documentation-attachment fix.
Removes the 2025-11 experimental tasks implementation per SEP-2663 (tasks moved to Extensions Track).
Motivation and Context
SEP-2663 removes tasks from the core protocol. This deletes
TaskManager, the task interception hooks inProtocol, andexperimental.tasks.*accessors.How Has This Been Tested?
pnpm test:all(1241 tests). Mechanical deletion; grep audit confirms notaskreferences remain inpackages/**exceptmicrotask/platformBackgroundTask(unrelated JS terms) andtaskSupportinToolExecutionSchema(removed in next PR alongside spec regen).Breaking Changes
Yes —
experimental.tasks.*removed. Seedocs/migration.md.Types of changes
Checklist