Skip to content

Commit 39e10ab

Browse files
refactor(core)!: remove experimental tasks interception from Protocol
Removes the 2025-11 task side-channel from Protocol (no compatibility owed; surface was experimental). SEP-2663's server-directed model has no equivalent to processInbound*/processOutbound* interception: a handler returns a CreateTaskResult and the client polls tasks/* methods. Test handling: - DELETED (~121, protocol.test.ts): mechanism tests for processInbound*/processOutbound*/relatedTask/NullTaskManager. Mechanism removed; no SEP-2663 equivalent. Per-suite reasons in the comment block at the deletion site. - SKIPPED (~85, integration): behavior tests (create/poll/result). TODO(F3) markers; rewritten via tasksPlugin in F3. - KEPT: storage-layer tests (InMemoryTaskStore etc.). Interfaces unchanged. Tasks comes back as tasksPlugin() in F3.
1 parent 2c0c481 commit 39e10ab

23 files changed

Lines changed: 191 additions & 5401 deletions

File tree

docs/migration-SKILL.md

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -420,9 +420,7 @@ Request/notification params remain fully typed. Remove unused schema imports aft
420420
| `extra.requestInfo` | `ctx.http?.req` (standard Web `Request`, only `ServerContext`) |
421421
| `extra.closeSSEStream` | `ctx.http?.closeSSE` (only `ServerContext`) |
422422
| `extra.closeStandaloneSSEStream` | `ctx.http?.closeStandaloneSSE` (only `ServerContext`) |
423-
| `extra.taskStore` | `ctx.task?.store` |
424-
| `extra.taskId` | `ctx.task?.id` |
425-
| `extra.taskRequestedTtl` | `ctx.task?.requestedTtl` |
423+
| `extra.taskStore` / `taskId` / `taskRequestedTtl` | _removed; see §12_ |
426424

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

@@ -473,24 +471,22 @@ If a `*Schema` constant was used for **runtime validation** (not just as a `requ
473471

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

476-
## 12. Experimental: `TaskCreationParams.ttl` no longer accepts `null`
474+
## 12. Experimental tasks interception removed
477475

478-
`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.
476+
The 2025-11 task side-channel through `Protocol` is removed (was always `@experimental`; SEP-2663 reattaches via `tasksPlugin()` in a follow-up). No mechanical migration; remove usages.
479477

480-
| v1 | v2 |
481-
| ---------------------- | ---------------------------------- |
482-
| `task: { ttl: null }` | `task: {}` (omit ttl) |
483-
| `task: { ttl: 60000 }` | `task: { ttl: 60000 }` (unchanged) |
478+
| Removed | Notes |
479+
| --- | --- |
480+
| `ProtocolOptions.tasks` | drop the option |
481+
| `protocol.taskManager` | gone |
482+
| `RequestOptions.task` / `.relatedTask`, `NotificationOptions.relatedTask` | drop the option |
483+
| `BaseContext.task` (`ctx.task?.*`) | gone; future: `ctx.ext.task` via `tasksPlugin()` |
484+
| `assertTaskCapability` / `assertTaskHandlerCapability` overrides | delete the override |
485+
| `*.experimental.tasks.{requestStream,callToolStream,createMessageStream,elicitInputStream}` | still defined; throw `CapabilityNotSupported` until `tasksPlugin()` |
484486

485-
Type changes in handler context:
487+
`TaskStore` / `InMemoryTaskStore` / `TaskMetadata` / `TaskMessageQueue` (storage interfaces) are unchanged.
486488

487-
| Type | v1 | v2 |
488-
| ------------------------------------------- | ----------------------------- | --------------------- |
489-
| `TaskContext.requestedTtl` | `number \| null \| undefined` | `number \| undefined` |
490-
| `CreateTaskServerContext.task.requestedTtl` | `number \| null \| undefined` | `number \| undefined` |
491-
| `TaskServerContext.task.requestedTtl` | `number \| null \| undefined` | `number \| undefined` |
492-
493-
> These task APIs are `@experimental` and may change without notice.
489+
`TaskCreationParams.ttl` also no longer accepts `null` (`number | undefined` only); omit `ttl` to let the server decide.
494490

495491
## 13. Client Behavioral Changes
496492

docs/migration.md

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -591,9 +591,7 @@ The `RequestHandlerExtra` type has been replaced with a structured context type
591591
| `extra.closeSSEStream` | `ctx.http?.closeSSE` (only on `ServerContext`) |
592592
| `extra.closeStandaloneSSEStream` | `ctx.http?.closeStandaloneSSE` (only on `ServerContext`) |
593593
| `extra.sessionId` | `ctx.sessionId` |
594-
| `extra.taskStore` | `ctx.task?.store` |
595-
| `extra.taskId` | `ctx.task?.id` |
596-
| `extra.taskRequestedTtl` | `ctx.task?.requestedTtl` |
594+
| `extra.taskStore` / `taskId` / `taskRequestedTtl` | _removed — see "Experimental tasks interception removed" below_ |
597595

598596
**Before (v1):**
599597

@@ -853,7 +851,23 @@ try {
853851
}
854852
```
855853

856-
### Experimental: `TaskCreationParams.ttl` no longer accepts `null`
854+
### Experimental tasks interception removed
855+
856+
The 2025-11 experimental tasks side-channel woven through `Protocol` has been removed in preparation for the SEP-2663 Tasks Extension. The following are gone with no in-place replacement:
857+
858+
- `ProtocolOptions.tasks` (the `{ taskStore, taskMessageQueue }` constructor option)
859+
- `protocol.taskManager` getter, `Protocol#_bindTaskManager`
860+
- `RequestOptions.task` / `RequestOptions.relatedTask`, `NotificationOptions.relatedTask`
861+
- `BaseContext.task` (`ctx.task?.store` / `ctx.task?.id` / `ctx.task?.requestedTtl`)
862+
- abstract `assertTaskCapability` / `assertTaskHandlerCapability`
863+
864+
The `*.experimental.tasks.*` accessor methods (`requestStream`, `callToolStream`, `createMessageStream`, `elicitInputStream`) are still defined but now throw `SdkError(CapabilityNotSupported)` until `tasksPlugin()` lands.
865+
866+
**Unchanged:** the storage interfaces in `experimental/tasks/` (`TaskStore`, `InMemoryTaskStore`, `TaskMetadata`, `TaskMessageQueue`). These will be consumed by `tasksPlugin()` in a follow-up.
867+
868+
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`.
869+
870+
#### `TaskCreationParams.ttl` no longer accepts `null`
857871

858872
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
859873
the server decide the lifetime.

examples/client/src/simpleOAuthClient.ts

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -362,18 +362,10 @@ class InteractiveOAuthClient {
362362
// Using the experimental tasks API - WARNING: may change without notice
363363
console.log(`\n🔧 Streaming tool '${toolName}'...`);
364364

365-
const stream = this.client.experimental.tasks.callToolStream(
366-
{
367-
name: toolName,
368-
arguments: toolArgs
369-
},
370-
{
371-
task: {
372-
taskId: `task-${Date.now()}`,
373-
ttl: 60_000
374-
}
375-
}
376-
);
365+
const stream = this.client.experimental.tasks.callToolStream({
366+
name: toolName,
367+
arguments: toolArgs
368+
});
377369

378370
// Iterate through all messages yielded by the generator
379371
for await (const message of stream) {

examples/client/src/simpleStreamableHttp.ts

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -296,13 +296,8 @@ async function connect(url?: string): Promise<void> {
296296
action: 'accept' | 'decline' | 'cancel';
297297
content?: Record<string, string | number | boolean | string[]>;
298298
}) => {
299-
if (request.params.task && extra.task?.store) {
300-
// Create a task and store the result
301-
const task = await extra.task.store.createTask({ ttl: extra.task.requestedTtl });
302-
await extra.task.store.storeTaskResult(task.taskId, 'completed', result);
303-
console.log(`📋 Created client-side task: ${task.taskId}`);
304-
return { task };
305-
}
299+
void request;
300+
void extra;
306301
return result;
307302
};
308303

@@ -895,17 +890,10 @@ async function callToolTask(name: string, args: Record<string, unknown>): Promis
895890

896891
try {
897892
// Call the tool with task metadata using streaming API
898-
const stream = client.experimental.tasks.callToolStream(
899-
{
900-
name,
901-
arguments: args
902-
},
903-
{
904-
task: {
905-
ttl: 60_000 // Keep results for 60 seconds
906-
}
907-
}
908-
);
893+
const stream = client.experimental.tasks.callToolStream({
894+
name,
895+
arguments: args
896+
});
909897

910898
console.log('Waiting for task completion...');
911899

examples/client/src/simpleTaskInteractiveClient.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ async function run(url: string): Promise<void> {
121121

122122
const confirmStream = client.experimental.tasks.callToolStream(
123123
{ name: 'confirm_delete', arguments: { filename: 'important.txt' } },
124-
{ task: { ttl: 60_000 } }
124+
{}
125125
);
126126

127127
for await (const message of confirmStream) {
@@ -150,10 +150,7 @@ async function run(url: string): Promise<void> {
150150
console.log('\n--- Demo 2: Sampling ---');
151151
console.log('Calling write_haiku tool...');
152152

153-
const haikuStream = client.experimental.tasks.callToolStream(
154-
{ name: 'write_haiku', arguments: { topic: 'autumn leaves' } },
155-
{ task: { ttl: 60_000 } }
156-
);
153+
const haikuStream = client.experimental.tasks.callToolStream({ name: 'write_haiku', arguments: { topic: 'autumn leaves' } }, {});
157154

158155
for await (const message of haikuStream) {
159156
switch (message.type) {

examples/server/src/simpleStreamableHttp.ts

Lines changed: 2 additions & 154 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { createMcpExpressApp, getOAuthProtectedResourceMetadataUrl, requireBeare
55
import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';
66
import type {
77
CallToolResult,
8-
ElicitResult,
98
GetPromptResult,
109
PrimitiveSchemaDefinition,
1110
ReadResourceResult,
@@ -439,159 +438,8 @@ const getServer = () => {
439438
}
440439
);
441440

442-
// Register a long-running tool that demonstrates task execution
443-
// Using the experimental tasks API - WARNING: may change without notice
444-
server.experimental.tasks.registerToolTask(
445-
'delay',
446-
{
447-
title: 'Delay',
448-
description: 'A simple tool that delays for a specified duration, useful for testing task execution',
449-
inputSchema: z.object({
450-
duration: z.number().describe('Duration in milliseconds').default(5000)
451-
})
452-
},
453-
{
454-
async createTask({ duration }, ctx) {
455-
// Create the task
456-
const task = await ctx.task.store.createTask({
457-
ttl: ctx.task.requestedTtl
458-
});
459-
460-
// Simulate out-of-band work
461-
(async () => {
462-
await new Promise(resolve => setTimeout(resolve, duration));
463-
await ctx.task.store.storeTaskResult(task.taskId, 'completed', {
464-
content: [
465-
{
466-
type: 'text',
467-
text: `Completed ${duration}ms delay`
468-
}
469-
]
470-
});
471-
})();
472-
473-
// Return CreateTaskResult with the created task
474-
return {
475-
task
476-
};
477-
},
478-
async getTask(_args, ctx) {
479-
return await ctx.task.store.getTask(ctx.task.id);
480-
},
481-
async getTaskResult(_args, ctx) {
482-
const result = await ctx.task.store.getTaskResult(ctx.task.id);
483-
return result as CallToolResult;
484-
}
485-
}
486-
);
487-
488-
// Register a tool that demonstrates bidirectional task support:
489-
// Server creates a task, then elicits input from client using elicitInputStream
490-
// Using the experimental tasks API - WARNING: may change without notice
491-
server.experimental.tasks.registerToolTask(
492-
'collect-user-info-task',
493-
{
494-
title: 'Collect Info with Task',
495-
description: 'Collects user info via elicitation with task support using elicitInputStream',
496-
inputSchema: z.object({
497-
infoType: z.enum(['contact', 'preferences']).describe('Type of information to collect').default('contact')
498-
})
499-
},
500-
{
501-
async createTask({ infoType }, ctx) {
502-
// Create the server-side task
503-
const task = await ctx.task.store.createTask({
504-
ttl: ctx.task.requestedTtl
505-
});
506-
507-
// Perform async work that makes a nested elicitation request using elicitInputStream
508-
(async () => {
509-
try {
510-
const message = infoType === 'contact' ? 'Please provide your contact information' : 'Please set your preferences';
511-
512-
// Define schemas with proper typing for PrimitiveSchemaDefinition
513-
const contactSchema: {
514-
type: 'object';
515-
properties: Record<string, PrimitiveSchemaDefinition>;
516-
required: string[];
517-
} = {
518-
type: 'object',
519-
properties: {
520-
name: { type: 'string', title: 'Full Name', description: 'Your full name' },
521-
email: { type: 'string', title: 'Email', description: 'Your email address' }
522-
},
523-
required: ['name', 'email']
524-
};
525-
526-
const preferencesSchema: {
527-
type: 'object';
528-
properties: Record<string, PrimitiveSchemaDefinition>;
529-
required: string[];
530-
} = {
531-
type: 'object',
532-
properties: {
533-
theme: { type: 'string', title: 'Theme', enum: ['light', 'dark', 'auto'] },
534-
notifications: { type: 'boolean', title: 'Enable Notifications', default: true }
535-
},
536-
required: ['theme']
537-
};
538-
539-
const requestedSchema = infoType === 'contact' ? contactSchema : preferencesSchema;
540-
541-
// Use elicitInputStream to elicit input from client
542-
// This demonstrates the streaming elicitation API
543-
// Access via server.server to get the underlying Server instance
544-
const stream = server.server.experimental.tasks.elicitInputStream({
545-
mode: 'form',
546-
message,
547-
requestedSchema
548-
});
549-
550-
let elicitResult: ElicitResult | undefined;
551-
for await (const msg of stream) {
552-
if (msg.type === 'result') {
553-
elicitResult = msg.result as ElicitResult;
554-
} else if (msg.type === 'error') {
555-
throw msg.error;
556-
}
557-
}
558-
559-
if (!elicitResult) {
560-
throw new Error('No result received from elicitation');
561-
}
562-
563-
let resultText: string;
564-
if (elicitResult.action === 'accept') {
565-
resultText = `Collected ${infoType} info: ${JSON.stringify(elicitResult.content, null, 2)}`;
566-
} else if (elicitResult.action === 'decline') {
567-
resultText = `User declined to provide ${infoType} information`;
568-
} else {
569-
resultText = 'User cancelled the request';
570-
}
571-
572-
await taskStore.storeTaskResult(task.taskId, 'completed', {
573-
content: [{ type: 'text', text: resultText }]
574-
});
575-
} catch (error) {
576-
console.error('Error in collect-user-info-task:', error);
577-
await taskStore.storeTaskResult(task.taskId, 'failed', {
578-
content: [{ type: 'text', text: `Error: ${error}` }],
579-
isError: true
580-
});
581-
}
582-
})();
583-
584-
return { task };
585-
},
586-
async getTask(_args, ctx) {
587-
return await ctx.task.store.getTask(ctx.task.id);
588-
},
589-
async getTaskResult(_args, ctx) {
590-
const result = await ctx.task.store.getTaskResult(ctx.task.id);
591-
return result as CallToolResult;
592-
}
593-
}
594-
);
441+
// TODO(F3): re-add task tool examples (delay, collect-user-info-task) via tasksPlugin (SEP-2663).
442+
// The experimental tasks interception was removed in R0; see docs/migration.md.
595443

596444
return server;
597445
};

0 commit comments

Comments
 (0)