Skip to content

Commit 00a522d

Browse files
feat(experimental): Create experimental/tasks module structure
Phase 1-2 of tasks experimental isolation: - Create src/experimental/tasks/ directory structure - Move TaskStore, TaskMessageQueue, and related interfaces to experimental/tasks/interfaces.ts - Add experimental/tasks/types.ts for re-exporting spec types - Update shared/task.ts to re-export from experimental for backward compatibility - Add barrel exports for experimental module All tests pass (1399 tests).
1 parent 191e749 commit 00a522d

26 files changed

+1074
-1442
lines changed

README.md

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1385,6 +1385,8 @@ const client = new Client(
13851385

13861386
### Task-Based Execution
13871387

1388+
> **⚠️ Experimental API**: Task-based execution is an experimental feature and may change without notice. Access these APIs via the `.experimental.tasks` namespace.
1389+
13881390
Task-based execution enables "call-now, fetch-later" patterns for long-running operations. This is useful for tools that take significant time to complete, where clients may want to disconnect and check on progress or retrieve results later.
13891391

13901392
Common use cases include:
@@ -1400,7 +1402,7 @@ To enable task-based execution, configure your server with a `TaskStore` impleme
14001402

14011403
```typescript
14021404
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
1403-
import { TaskStore } from '@modelcontextprotocol/sdk/shared/task.js';
1405+
import { TaskStore } from '@modelcontextprotocol/sdk/experimental';
14041406
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
14051407

14061408
// Implement TaskStore backed by your database (e.g., PostgreSQL, Redis, etc.)
@@ -1458,8 +1460,8 @@ const server = new Server(
14581460
}
14591461
);
14601462

1461-
// Register a tool that supports tasks
1462-
server.registerToolTask(
1463+
// Register a tool that supports tasks using the experimental API
1464+
server.experimental.tasks.registerToolTask(
14631465
'my-echo-tool',
14641466
{
14651467
title: 'My Echo Tool',
@@ -1508,7 +1510,7 @@ server.registerToolTask(
15081510

15091511
#### Client-Side: Using Task-Based Execution
15101512

1511-
Clients use `callToolStream()` to initiate task-augmented tool calls. The returned `AsyncGenerator` abstracts automatic polling and status updates:
1513+
Clients use `experimental.tasks.callToolStream()` to initiate task-augmented tool calls. The returned `AsyncGenerator` abstracts automatic polling and status updates:
15121514

15131515
```typescript
15141516
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
@@ -1521,8 +1523,8 @@ const client = new Client({
15211523

15221524
// ... connect to server ...
15231525

1524-
// Call the tool with task metadata using streaming API
1525-
const stream = client.callToolStream(
1526+
// Call the tool with task metadata using the experimental streaming API
1527+
const stream = client.experimental.tasks.callToolStream(
15261528
{
15271529
name: 'my-echo-tool',
15281530
arguments: { message: 'Hello, world!' }
@@ -1566,7 +1568,7 @@ if (taskStatus.status === 'completed') {
15661568
}
15671569
```
15681570

1569-
The `callToolStream()` method also works with non-task tools, making it a drop-in replacement for `callTool()` in applications that support it. When used to invoke a tool that doesn't support tasks, the `taskCreated` and `taskStatus` events will not be emitted.
1571+
The `experimental.tasks.callToolStream()` method also works with non-task tools, making it a drop-in replacement for `callTool()` in applications that support it. When used to invoke a tool that doesn't support tasks, the `taskCreated` and `taskStatus` events will not be emitted.
15701572

15711573
#### Task Status Lifecycle
15721574

package.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,14 @@
4343
"import": "./dist/esm/validation/cfworker-provider.js",
4444
"require": "./dist/cjs/validation/cfworker-provider.js"
4545
},
46+
"./experimental": {
47+
"import": "./dist/esm/experimental/index.js",
48+
"require": "./dist/cjs/experimental/index.js"
49+
},
50+
"./experimental/tasks": {
51+
"import": "./dist/esm/experimental/tasks/index.js",
52+
"require": "./dist/cjs/experimental/tasks/index.js"
53+
},
4654
"./*": {
4755
"import": "./dist/esm/*",
4856
"require": "./dist/cjs/*"

src/client/index.test.ts

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import { Transport } from '../shared/transport.js';
2626
import { Server } from '../server/index.js';
2727
import { McpServer } from '../server/mcp.js';
2828
import { InMemoryTransport } from '../inMemory.js';
29-
import { InMemoryTaskStore, InMemoryTaskMessageQueue } from '../examples/shared/inMemoryTaskStore.js';
29+
import { InMemoryTaskStore, InMemoryTaskMessageQueue } from '../experimental/tasks/stores/in-memory.js';
3030
import * as z3 from 'zod/v3';
3131
import * as z4 from 'zod/v4';
3232

@@ -1818,7 +1818,7 @@ describe('Task-based execution', () => {
18181818
}
18191819
);
18201820

1821-
server.registerToolTask(
1821+
server.experimental.tasks.registerToolTask(
18221822
'test-tool',
18231823
{
18241824
description: 'A test tool',
@@ -1868,7 +1868,7 @@ describe('Task-based execution', () => {
18681868
});
18691869

18701870
// Verify task was created successfully by listing tasks
1871-
const taskList = await client.listTasks();
1871+
const taskList = await client.experimental.tasks.listTasks();
18721872
expect(taskList.tasks.length).toBeGreaterThan(0);
18731873
const task = taskList.tasks[0];
18741874
expect(task.status).toBe('completed');
@@ -1894,7 +1894,7 @@ describe('Task-based execution', () => {
18941894
}
18951895
);
18961896

1897-
server.registerToolTask(
1897+
server.experimental.tasks.registerToolTask(
18981898
'test-tool',
18991899
{
19001900
description: 'A test tool',
@@ -1942,7 +1942,7 @@ describe('Task-based execution', () => {
19421942
});
19431943

19441944
// Query task status by listing tasks and getting the first one
1945-
const taskList = await client.listTasks();
1945+
const taskList = await client.experimental.tasks.listTasks();
19461946
expect(taskList.tasks.length).toBeGreaterThan(0);
19471947
const task = taskList.tasks[0];
19481948
expect(task).toBeDefined();
@@ -1971,7 +1971,7 @@ describe('Task-based execution', () => {
19711971
}
19721972
);
19731973

1974-
server.registerToolTask(
1974+
server.experimental.tasks.registerToolTask(
19751975
'test-tool',
19761976
{
19771977
description: 'A test tool',
@@ -2015,7 +2015,7 @@ describe('Task-based execution', () => {
20152015

20162016
// Create a task using callToolStream to capture the task ID
20172017
let taskId: string | undefined;
2018-
const stream = client.callToolStream({ name: 'test-tool', arguments: {} }, CallToolResultSchema, {
2018+
const stream = client.experimental.tasks.callToolStream({ name: 'test-tool', arguments: {} }, CallToolResultSchema, {
20192019
task: { ttl: 60000 }
20202020
});
20212021

@@ -2028,7 +2028,7 @@ describe('Task-based execution', () => {
20282028
expect(taskId).toBeDefined();
20292029

20302030
// Query task result using the captured task ID
2031-
const result = await client.getTaskResult({ taskId: taskId! }, CallToolResultSchema);
2031+
const result = await client.experimental.tasks.getTaskResult(taskId!, CallToolResultSchema);
20322032
expect(result.content).toEqual([{ type: 'text', text: 'Result data!' }]);
20332033
});
20342034

@@ -2052,7 +2052,7 @@ describe('Task-based execution', () => {
20522052
}
20532053
);
20542054

2055-
server.registerToolTask(
2055+
server.experimental.tasks.registerToolTask(
20562056
'test-tool',
20572057
{
20582058
description: 'A test tool',
@@ -2103,15 +2103,15 @@ describe('Task-based execution', () => {
21032103
});
21042104

21052105
// Get the task ID from the task list
2106-
const taskList = await client.listTasks();
2106+
const taskList = await client.experimental.tasks.listTasks();
21072107
const newTask = taskList.tasks.find(t => !createdTaskIds.includes(t.taskId));
21082108
if (newTask) {
21092109
createdTaskIds.push(newTask.taskId);
21102110
}
21112111
}
21122112

21132113
// Query task list
2114-
const taskList = await client.listTasks();
2114+
const taskList = await client.experimental.tasks.listTasks();
21152115
expect(taskList.tasks.length).toBeGreaterThanOrEqual(2);
21162116
for (const taskId of createdTaskIds) {
21172117
expect(taskList.tasks).toContainEqual(
@@ -2224,7 +2224,7 @@ describe('Task-based execution', () => {
22242224
const taskId = createTaskResult.task.taskId;
22252225

22262226
// Verify task was created
2227-
const task = await server.getTask({ taskId });
2227+
const task = await server.experimental.tasks.getTask(taskId);
22282228
expect(task.status).toBe('completed');
22292229
});
22302230

@@ -2314,7 +2314,7 @@ describe('Task-based execution', () => {
23142314
const taskId = createTaskResult.task.taskId;
23152315

23162316
// Query task status
2317-
const task = await server.getTask({ taskId });
2317+
const task = await server.experimental.tasks.getTask(taskId);
23182318
expect(task).toBeDefined();
23192319
expect(task.taskId).toBe(taskId);
23202320
expect(task.status).toBe('completed');
@@ -2406,7 +2406,7 @@ describe('Task-based execution', () => {
24062406
const taskId = createTaskResult.task.taskId;
24072407

24082408
// Query task result using getTaskResult
2409-
const taskResult = await server.getTaskResult({ taskId }, ElicitResultSchema);
2409+
const taskResult = await server.experimental.tasks.getTaskResult(taskId, ElicitResultSchema);
24102410
expect(taskResult.action).toBe('accept');
24112411
expect(taskResult.content).toEqual({ username: 'result-user' });
24122412
});
@@ -2500,7 +2500,7 @@ describe('Task-based execution', () => {
25002500
}
25012501

25022502
// Query task list
2503-
const taskList = await server.listTasks();
2503+
const taskList = await server.experimental.tasks.listTasks();
25042504
expect(taskList.tasks.length).toBeGreaterThanOrEqual(2);
25052505
for (const taskId of createdTaskIds) {
25062506
expect(taskList.tasks).toContainEqual(
@@ -2535,7 +2535,7 @@ describe('Task-based execution', () => {
25352535
}
25362536
);
25372537

2538-
server.registerToolTask(
2538+
server.experimental.tasks.registerToolTask(
25392539
'test-tool',
25402540
{
25412541
description: 'A test tool',
@@ -2601,21 +2601,21 @@ describe('Task-based execution', () => {
26012601
});
26022602

26032603
// Get the task ID from the task list
2604-
const taskList = await client.listTasks();
2604+
const taskList = await client.experimental.tasks.listTasks();
26052605
const newTask = taskList.tasks.find(t => !createdTaskIds.includes(t.taskId));
26062606
if (newTask) {
26072607
createdTaskIds.push(newTask.taskId);
26082608
}
26092609
}
26102610

26112611
// List all tasks without cursor
2612-
const firstPage = await client.listTasks();
2612+
const firstPage = await client.experimental.tasks.listTasks();
26132613
expect(firstPage.tasks.length).toBeGreaterThan(0);
26142614
expect(firstPage.tasks.map(t => t.taskId)).toEqual(expect.arrayContaining(createdTaskIds));
26152615

26162616
// If there's a cursor, test pagination
26172617
if (firstPage.nextCursor) {
2618-
const secondPage = await client.listTasks({ cursor: firstPage.nextCursor });
2618+
const secondPage = await client.experimental.tasks.listTasks(firstPage.nextCursor);
26192619
expect(secondPage.tasks).toBeDefined();
26202620
}
26212621

@@ -2680,7 +2680,7 @@ describe('Task-based execution', () => {
26802680
await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]);
26812681

26822682
// Try to get a task that doesn't exist
2683-
await expect(client.getTask({ taskId: 'non-existent-task' })).rejects.toThrow();
2683+
await expect(client.experimental.tasks.getTask('non-existent-task')).rejects.toThrow();
26842684
});
26852685

26862686
test('should throw error when querying result of non-existent task from server', async () => {
@@ -2727,7 +2727,7 @@ describe('Task-based execution', () => {
27272727
await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]);
27282728

27292729
// Try to get result of a task that doesn't exist
2730-
await expect(client.getTaskResult({ taskId: 'non-existent-task' }, CallToolResultSchema)).rejects.toThrow();
2730+
await expect(client.experimental.tasks.getTaskResult('non-existent-task', CallToolResultSchema)).rejects.toThrow();
27312731
});
27322732

27332733
test('should throw error when server queries non-existent task from client', async () => {
@@ -2779,7 +2779,7 @@ describe('Task-based execution', () => {
27792779
await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]);
27802780

27812781
// Try to query a task that doesn't exist on client
2782-
await expect(server.getTask({ taskId: 'non-existent-task' })).rejects.toThrow();
2782+
await expect(server.experimental.tasks.getTask('non-existent-task')).rejects.toThrow();
27832783
});
27842784
});
27852785
});
@@ -2805,7 +2805,7 @@ test('should respect server task capabilities', async () => {
28052805
}
28062806
);
28072807

2808-
server.registerToolTask(
2808+
server.experimental.tasks.registerToolTask(
28092809
'test-tool',
28102810
{
28112811
description: 'A test tool',
@@ -2871,7 +2871,7 @@ test('should respect server task capabilities', async () => {
28712871
task: { ttl: 60000 }
28722872
})
28732873
).resolves.not.toThrow();
2874-
await expect(client.listTasks()).resolves.not.toThrow();
2874+
await expect(client.experimental.tasks.listTasks()).resolves.not.toThrow();
28752875

28762876
// tools/list doesn't support task creation, but it shouldn't throw - it should just ignore the task metadata
28772877
await expect(
@@ -2928,7 +2928,7 @@ test('should expose requestStream() method for streaming responses', async () =>
29282928
expect(regularResult.content).toEqual([{ type: 'text', text: 'Tool result' }]);
29292929

29302930
// Test requestStream with non-task request (should yield only result)
2931-
const stream = client.requestStream(
2931+
const stream = client.experimental.tasks.requestStream(
29322932
{
29332933
method: 'tools/call',
29342934
params: { name: 'test-tool', arguments: {} }
@@ -2989,7 +2989,7 @@ test('should expose callToolStream() method for streaming tool calls', async ()
29892989
await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]);
29902990

29912991
// Test callToolStream
2992-
const stream = client.callToolStream({ name: 'test-tool', arguments: {} });
2992+
const stream = client.experimental.tasks.callToolStream({ name: 'test-tool', arguments: {} });
29932993

29942994
const messages = [];
29952995
for await (const message of stream) {
@@ -3070,7 +3070,7 @@ test('should validate structured output in callToolStream()', async () => {
30703070
await client.listTools();
30713071

30723072
// Test callToolStream with valid structured output
3073-
const stream = client.callToolStream({ name: 'structured-tool', arguments: {} });
3073+
const stream = client.experimental.tasks.callToolStream({ name: 'structured-tool', arguments: {} });
30743074

30753075
const messages = [];
30763076
for await (const message of stream) {

0 commit comments

Comments
 (0)