Skip to content

[SEP-2663] refactor!: remove 2025-11 experimental tasks#2128

Draft
felixweinberger wants to merge 1 commit into
mainfrom
fweinberger/v2-tasks-delete
Draft

[SEP-2663] refactor!: remove 2025-11 experimental tasks#2128
felixweinberger wants to merge 1 commit into
mainfrom
fweinberger/v2-tasks-delete

Conversation

@felixweinberger
Copy link
Copy Markdown
Contributor

@felixweinberger felixweinberger commented May 20, 2026

2026-06 stateless stack (v2-stateless label):

# PR
1 #2128 tasks-delete (mechanical)
2 #2129 schema-sync (mechanical)
3 #2130 Dispatcher extraction (zero-Δ refactor)
4 #2131 HTTP-stateless (the substantive review)
5 #2132 stdio/InMemory transports (additive)
6 #2133 docs + changeset
7 #2134 LegacyServer/LegacyClient extraction

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 in Protocol, and experimental.tasks.* accessors.

How Has This Been Tested?

pnpm test:all (1241 tests). Mechanical deletion; grep audit confirms no task references remain in packages/** except microtask/platformBackgroundTask (unrelated JS terms) and taskSupport in ToolExecutionSchema (removed in next PR alongside spec regen).

Breaking Changes

Yes — experimental.tasks.* removed. See docs/migration.md.

Types of changes

  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added or updated documentation as needed

…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).
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 20, 2026

⚠️ No Changeset found

Latest commit: 54bbe7b

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 20, 2026

Open in StackBlitz

@modelcontextprotocol/client

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

@modelcontextprotocol/server

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

@modelcontextprotocol/express

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

@modelcontextprotocol/fastify

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

@modelcontextprotocol/hono

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

@modelcontextprotocol/node

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

commit: 54bbe7b

@felixweinberger
Copy link
Copy Markdown
Contributor Author

@claude review

Comment on lines 38 to +47
// 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...`);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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 the tools/call request 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

  1. pnpm tsx examples/server/src/ssePollingExample.ts — server starts and registers long-operation (line 40 after this PR).
  2. pnpm tsx examples/client/src/ssePollingClient.ts — client connects, then at line 77 sends { method: 'tools/call', params: { name: 'long-task', ... } }.
  3. Server's tools/call handler does this._registeredTools[request.params.name]undefined → throws Tool long-task not found.
  4. 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.

Comment thread docs/migration.md
Comment on lines +869 to +875
**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.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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 defined TaskStore, CreateTaskOptions, and isTerminal.
  • Deletes packages/core/src/experimental/tasks/stores/inMemory.ts — which defined InMemoryTaskStore (and InMemoryTaskMessageQueue).
  • Deletes the experimental barrel packages/core/src/experimental/index.ts.
  • Removes the re-exports from packages/core/src/exports/public/index.ts (the export type { ... TaskStore ... }, export { isTerminal }, and export { InMemoryTaskMessageQueue, InMemoryTaskStore } lines).
  • Removes the export * from './experimental/index.js'; line from packages/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

  1. A v1 user has import { InMemoryTaskStore, isTerminal } from '@modelcontextprotocol/core'; in their server.
  2. They upgrade to v2 and read the migration guide. docs/migration.md line ~869 says these are "Unchanged."
  3. They leave that import alone.
  4. tsc fails: Module '"@modelcontextprotocol/core"' has no exported member 'InMemoryTaskStore'. — because packages/core/src/exports/public/index.ts no longer re-exports it and the source file no longer exists.
  5. 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 TaskCreationParams were 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.

Comment on lines +326 to 329
.optional() /**
* Extensions that the client supports. Keys are extension identifiers (vendor-prefix/extension-name).
*/
*/,
extensions: z.record(z.string(), JSONObjectSchema).optional()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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)

  1. 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()
  2. The deletion removed the tasks: line and its JSDoc, but the closing , that used to follow .optional() on the roots block now precedes the extensions JSDoc on the same logical line, producing .optional() /** ... */,.
  3. Hover over extensions in an editor → no description, because the comment belongs to the roots property 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

v2-stateless 2026-06 SDK: Protocol decomposition + SEP alignment (request-first / stateless)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant