Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 0 additions & 10 deletions .changeset/extract-task-manager.md

This file was deleted.

5 changes: 0 additions & 5 deletions .changeset/fix-failed-task-result-retrieval.md

This file was deleted.

5 changes: 0 additions & 5 deletions .changeset/fix-task-session-isolation.md

This file was deleted.

3 changes: 0 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,11 +103,9 @@
The repo also ships “middleware” packages under `packages/middleware/` (e.g. `@modelcontextprotocol/express`, `@modelcontextprotocol/hono`, `@modelcontextprotocol/node`). These are thin integration layers for specific frameworks/runtimes and should not add new MCP functionality.

### Experimental Features

Located in `packages/*/src/experimental/`:

Check warning on line 108 in CLAUDE.md

View check run for this annotation

Claude / Claude Code Review

CLAUDE.md Experimental Features section left dangling

The `### Experimental Features` section in CLAUDE.md is left dangling — its only bullet (Tasks) was removed, but the heading and the `Located in packages/*/src/experimental/:` lead-in remain, now pointing at directories this PR deletes. Suggest removing the section entirely (lines 105–108).
Comment on lines 106 to 108
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 ### Experimental Features section in CLAUDE.md is left dangling — its only bullet (Tasks) was removed, but the heading and the Located in packages/*/src/experimental/: lead-in remain, now pointing at directories this PR deletes. Suggest removing the section entirely (lines 105–108).

Extended reasoning...

What the issue is

This PR removes the experimental Tasks API in its entirety, including deleting packages/core/src/experimental/, packages/client/src/experimental/, and packages/server/src/experimental/. In CLAUDE.md, the diff removes the single bullet under Experimental Features:

 ### Experimental Features

 Located in `packages/*/src/experimental/`:

-- **Tasks**: Long-running task support with polling/resumption (`packages/core/src/experimental/tasks/`)
-
 ### Zod Schemas

After this change, lines 105–108 of CLAUDE.md read:

### Experimental Features

Located in `packages/*/src/experimental/`:

### Zod Schemas

— a heading followed by a lead-in sentence ending in a colon, with no list items, immediately followed by the next heading.

Why this is a problem

There are two issues with the leftover prose:

  1. Empty section: The lead-in Located in packages/*/src/experimental/: syntactically introduces a list, but there are no items. This reads as broken/incomplete documentation.
  2. Stale path reference: The PR deletes packages/core/src/experimental/index.ts, packages/client/src/experimental/index.ts, and packages/server/src/experimental/index.ts (and everything under them). The glob packages/*/src/experimental/ therefore points at directories that no longer exist, which will mislead anyone using CLAUDE.md to navigate the codebase.

Step-by-step proof

  1. Before this PR, CLAUDE.md §Experimental Features had exactly one bullet: - **Tasks**: Long-running task support….
  2. The diff hunk for CLAUDE.md (@@ -106,8 +106,6 @@) removes that bullet and the trailing blank line, but leaves the ### Experimental Features heading and the Located in packages/*/src/experimental/: line untouched.
  3. The same PR deletes packages/{core,client,server}/src/experimental/index.ts and all files under those directories (visible in the changed-files list and the diff).
  4. Result: CLAUDE.md now contains a section header + path reference with zero content, pointing at paths that were removed in the same PR.

Impact

This is purely a documentation-consistency issue with no runtime effect. CLAUDE.md is the repo's onboarding/architecture doc, so leaving it referencing deleted directories degrades its accuracy but doesn't break anything.

Suggested fix

Delete the ### Experimental Features heading and the Located in packages/*/src/experimental/: line so the doc goes straight from the middleware paragraph to ### Zod Schemas. If experimental features are reintroduced later, the section can be re-added at that time.

- **Tasks**: Long-running task support with polling/resumption (`packages/core/src/experimental/tasks/`)

### Zod Schemas

The SDK uses `zod/v4` internally. Schema utilities live in:
Expand Down Expand Up @@ -201,7 +199,6 @@
- `notify(notification)`: Send related notification back
- `http?`: HTTP transport info (undefined for stdio)
- `authInfo?`: Validated auth token info
- `task?`: Task context (`{ id?, store, requestedTtl? }`) when task storage is configured

**`ServerContext`** extends `BaseContext.mcpReq` and `BaseContext.http?` via type intersection:

Expand Down
28 changes: 3 additions & 25 deletions docs/migration-SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -420,9 +420,6 @@ Request/notification params remain fully typed. Remove unused schema imports aft
| `extra.requestInfo` | `ctx.http?.req` (standard Web `Request`, only `ServerContext`) |
| `extra.closeSSEStream` | `ctx.http?.closeSSE` (only `ServerContext`) |
| `extra.closeStandaloneSSEStream` | `ctx.http?.closeStandaloneSSE` (only `ServerContext`) |
| `extra.taskStore` | `ctx.task?.store` |
| `extra.taskId` | `ctx.task?.id` |
| `extra.taskRequestedTtl` | `ctx.task?.requestedTtl` |

`ServerContext` convenience methods (new in v2, no v1 equivalent):

Expand Down Expand Up @@ -473,30 +470,11 @@ If a `*Schema` constant was used for **runtime validation** (not just as a `requ

`isCallToolResult(value)` still works, but `isSpecType` covers every spec type by name.

## 12. Experimental: `TaskCreationParams.ttl` no longer accepts `null`

`TaskCreationParams.ttl` changed from `z.union([z.number(), z.null()]).optional()` to `z.number().optional()`. Per the MCP spec, `null` TTL (unlimited lifetime) is only valid in server responses (`Task.ttl`), not in client requests. Omit `ttl` to let the server decide.

| v1 | v2 |
| ---------------------- | ---------------------------------- |
| `task: { ttl: null }` | `task: {}` (omit ttl) |
| `task: { ttl: 60000 }` | `task: { ttl: 60000 }` (unchanged) |

Type changes in handler context:

| Type | v1 | v2 |
| ------------------------------------------- | ----------------------------- | --------------------- |
| `TaskContext.requestedTtl` | `number \| null \| undefined` | `number \| undefined` |
| `CreateTaskServerContext.task.requestedTtl` | `number \| null \| undefined` | `number \| undefined` |
| `TaskServerContext.task.requestedTtl` | `number \| null \| undefined` | `number \| undefined` |

> These task APIs are `@experimental` and may change without notice.

## 13. Client Behavioral Changes
## 12. Client Behavioral Changes

`Client.listPrompts()`, `listResources()`, `listResourceTemplates()`, `listTools()` now return empty results when the server lacks the corresponding capability (instead of sending the request). Set `enforceStrictCapabilities: true` in `ClientOptions` to throw an error instead.

## 14. Runtime-Specific JSON Schema Validators (Enhancement)
## 13. Runtime-Specific JSON Schema Validators (Enhancement)

The SDK now auto-selects the appropriate JSON Schema validator based on runtime:

Expand Down Expand Up @@ -524,7 +502,7 @@ Access validators explicitly:
- AJV (Node.js): `import { AjvJsonSchemaValidator } from '@modelcontextprotocol/server';`
- CF Worker: `import { CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/server/validators/cf-worker';`

## 15. Migration Steps (apply in this order)
## 14. Migration Steps (apply in this order)

1. Update `package.json`: `npm uninstall @modelcontextprotocol/sdk`, install the appropriate v2 packages
2. Replace all imports from `@modelcontextprotocol/sdk/...` using the import mapping tables (sections 3-4), including `StreamableHTTPServerTransport` → `NodeStreamableHTTPServerTransport`
Expand Down
51 changes: 2 additions & 49 deletions docs/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -485,7 +485,7 @@ const result = await client.callTool({ name: 'my-tool', arguments: {} }, Compati
const result = await client.callTool({ name: 'my-tool', arguments: {} });
```

The return type is now inferred from the method name via `ResultTypeMap`. For example, `client.request({ method: 'tools/call', ... })` returns `Promise<CallToolResult | CreateTaskResult>`.
The return type is now inferred from the method name via `ResultTypeMap`. For example, `client.request({ method: 'tools/call', ... })` returns `Promise<CallToolResult>`.

For **custom (non-spec)** methods, keep the result-schema argument — see [Sending custom-method requests](#sending-custom-method-requests). Only drop the schema when calling a spec method.

Expand Down Expand Up @@ -591,16 +591,12 @@ The `RequestHandlerExtra` type has been replaced with a structured context type
| `extra.closeSSEStream` | `ctx.http?.closeSSE` (only on `ServerContext`) |
| `extra.closeStandaloneSSEStream` | `ctx.http?.closeStandaloneSSE` (only on `ServerContext`) |
| `extra.sessionId` | `ctx.sessionId` |
| `extra.taskStore` | `ctx.task?.store` |
| `extra.taskId` | `ctx.task?.id` |
| `extra.taskRequestedTtl` | `ctx.task?.requestedTtl` |

**Before (v1):**

```typescript
server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
const headers = extra.requestInfo?.headers;
const taskStore = extra.taskStore;
await extra.sendNotification({ method: 'notifications/progress', params: { progressToken: 'abc', progress: 50, total: 100 } });
return { content: [{ type: 'text', text: 'result' }] };
});
Expand All @@ -611,17 +607,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
```typescript
server.setRequestHandler('tools/call', async (request, ctx) => {
const headers = ctx.http?.req?.headers; // standard Web Request object
const taskStore = ctx.task?.store;
await ctx.mcpReq.notify({ method: 'notifications/progress', params: { progressToken: 'abc', progress: 50, total: 100 } });
return { content: [{ type: 'text', text: 'result' }] };
});
```

Context fields are organized into 4 groups:
Context fields are organized into 2 groups:

- **`mcpReq`** — request-level concerns: `id`, `method`, `_meta`, `signal`, `send()`, `notify()`, plus server-only `log()`, `elicitInput()`, and `requestSampling()`
- **`http?`** — HTTP transport concerns (undefined for stdio): `authInfo`, plus server-only `req`, `closeSSE`, `closeStandaloneSSE`
- **`task?`** — task lifecycle: `id`, `store`, `requestedTtl`

`BaseContext` is the common base type shared by both `ServerContext` and `ClientContext`. `ServerContext` extends each group with server-specific additions via type intersection.

Expand Down Expand Up @@ -853,47 +847,6 @@ try {
}
```

### Experimental: `TaskCreationParams.ttl` no longer accepts `null`

The `ttl` field in `TaskCreationParams` (used when requesting the server to create a task) no longer accepts `null`. Per the MCP spec, `null` TTL (meaning unlimited lifetime) is only valid in server responses (`Task.ttl`), not in client requests. Clients should omit `ttl` to let
the server decide the lifetime.

This also narrows the type of `requestedTtl` in `TaskContext`, `CreateTaskServerContext`, and `TaskServerContext` from `number | null | undefined` to `number | undefined`.

**Before (v1):**

```typescript
// Requesting unlimited lifetime by passing null
const result = await client.callTool({
name: 'long-task',
arguments: {},
task: { ttl: null }
});

// Handler context had number | null | undefined
server.setRequestHandler('tools/call', async (request, ctx) => {
const ttl: number | null | undefined = ctx.task?.requestedTtl;
});
```

**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;
});
```

> **Note:** These task APIs are marked `@experimental` and may change without notice.

## Enhancements

### Automatic JSON Schema validator selection by runtime
Expand Down
95 changes: 2 additions & 93 deletions examples/client/src/simpleOAuthClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { createServer } from 'node:http';
import { createInterface } from 'node:readline';
import { URL } from 'node:url';

import type { CallToolResult, ListToolsRequest, OAuthClientMetadata } from '@modelcontextprotocol/client';
import type { ListToolsRequest, OAuthClientMetadata } from '@modelcontextprotocol/client';
import { Client, StreamableHTTPClientTransport, UnauthorizedError } from '@modelcontextprotocol/client';
import open from 'open';

Expand Down Expand Up @@ -209,7 +209,6 @@ class InteractiveOAuthClient {
console.log('Commands:');
console.log(' list - List available tools');
console.log(' call <tool_name> [args] - Call a tool');
console.log(' stream <tool_name> [args] - Call a tool with streaming (shows task status)');
console.log(' quit - Exit the client');
console.log();

Expand All @@ -229,10 +228,8 @@ class InteractiveOAuthClient {
await this.listTools();
} else if (command.startsWith('call ')) {
await this.handleCallTool(command);
} else if (command.startsWith('stream ')) {
await this.handleStreamTool(command);
} else {
console.log("❌ Unknown command. Try 'list', 'call <tool_name>', 'stream <tool_name>', or 'quit'");
console.log("❌ Unknown command. Try 'list', 'call <tool_name>', or 'quit'");
}
} catch (error) {
if (error instanceof Error && error.message === 'SIGINT') {
Expand Down Expand Up @@ -328,94 +325,6 @@ class InteractiveOAuthClient {
}
}

private async handleStreamTool(command: string): Promise<void> {
const parts = command.split(/\s+/);
const toolName = parts[1];

if (!toolName) {
console.log('❌ Please specify a tool name');
return;
}

// Parse arguments (simple JSON-like format)
let toolArgs: Record<string, unknown> = {};
if (parts.length > 2) {
const argsString = parts.slice(2).join(' ');
try {
toolArgs = JSON.parse(argsString);
} catch {
console.log('❌ Invalid arguments format (expected JSON)');
return;
}
}

await this.streamTool(toolName, toolArgs);
}

private async streamTool(toolName: string, toolArgs: Record<string, unknown>): Promise<void> {
if (!this.client) {
console.log('❌ Not connected to server');
return;
}

try {
// Using the experimental tasks API - WARNING: may change without notice
console.log(`\n🔧 Streaming tool '${toolName}'...`);

const stream = this.client.experimental.tasks.callToolStream(
{
name: toolName,
arguments: toolArgs
},
{
task: {
taskId: `task-${Date.now()}`,
ttl: 60_000
}
}
);

// Iterate through all messages yielded by the generator
for await (const message of stream) {
switch (message.type) {
case 'taskCreated': {
console.log(`✓ Task created: ${message.task.taskId}`);
break;
}

case 'taskStatus': {
console.log(`⟳ Status: ${message.task.status}`);
if (message.task.statusMessage) {
console.log(` ${message.task.statusMessage}`);
}
break;
}

case 'result': {
console.log('✓ Completed!');
const toolResult = message.result as CallToolResult;
for (const content of toolResult.content) {
if (content.type === 'text') {
console.log(content.text);
} else {
console.log(content);
}
}
break;
}

case 'error': {
console.log('✗ Error:');
console.log(` ${message.error.message}`);
break;
}
}
}
} catch (error) {
console.error(`❌ Failed to stream tool '${toolName}':`, error);
}
}

close(): void {
this.rl.close();
if (this.client) {
Expand Down
Loading
Loading