Skip to content

Commit b9538a2

Browse files
SEP-1686: Tasks (#1041)
Co-authored-by: Felix Weinberger <3823880+felixweinberger@users.noreply.github.com> Co-authored-by: Felix Weinberger <fweinberger@anthropic.com>
1 parent 2aa697c commit b9538a2

30 files changed

+16166
-246
lines changed

README.md

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
- [Improving Network Efficiency with Notification Debouncing](#improving-network-efficiency-with-notification-debouncing)
2727
- [Low-Level Server](#low-level-server)
2828
- [Eliciting User Input](#eliciting-user-input)
29+
- [Task-Based Execution](#task-based-execution)
2930
- [Writing MCP Clients](#writing-mcp-clients)
3031
- [Proxy Authorization Requests Upstream](#proxy-authorization-requests-upstream)
3132
- [Backwards Compatibility](#backwards-compatibility)
@@ -1387,6 +1388,206 @@ const client = new Client(
13871388
);
13881389
```
13891390

1391+
### Task-Based Execution
1392+
1393+
> **⚠️ Experimental API**: Task-based execution is an experimental feature and may change without notice. Access these APIs via the `.experimental.tasks` namespace.
1394+
1395+
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.
1396+
1397+
Common use cases include:
1398+
1399+
- Long-running data processing or analysis
1400+
- Code migration or refactoring operations
1401+
- Complex computational tasks
1402+
- Operations that require periodic status updates
1403+
1404+
#### Server-Side: Implementing Task Support
1405+
1406+
To enable task-based execution, configure your server with a `TaskStore` implementation. The SDK doesn't provide a built-in TaskStore—you'll need to implement one backed by your database of choice:
1407+
1408+
```typescript
1409+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
1410+
import { TaskStore } from '@modelcontextprotocol/sdk/experimental';
1411+
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
1412+
1413+
// Implement TaskStore backed by your database (e.g., PostgreSQL, Redis, etc.)
1414+
class MyTaskStore implements TaskStore {
1415+
async createTask(taskParams, requestId, request, sessionId?): Promise<Task> {
1416+
// Generate unique taskId and lastUpdatedAt/createdAt timestamps
1417+
// Store task in your database, using the session ID as a proxy to restrict unauthorized access
1418+
// Return final Task object
1419+
}
1420+
1421+
async getTask(taskId): Promise<Task | null> {
1422+
// Retrieve task from your database
1423+
}
1424+
1425+
async updateTaskStatus(taskId, status, statusMessage?): Promise<void> {
1426+
// Update task status in your database
1427+
}
1428+
1429+
async storeTaskResult(taskId, result): Promise<void> {
1430+
// Store task result in your database
1431+
}
1432+
1433+
async getTaskResult(taskId): Promise<Result> {
1434+
// Retrieve task result from your database
1435+
}
1436+
1437+
async listTasks(cursor?, sessionId?): Promise<{ tasks: Task[]; nextCursor?: string }> {
1438+
// List tasks with pagination support
1439+
}
1440+
}
1441+
1442+
const taskStore = new MyTaskStore();
1443+
1444+
const server = new Server(
1445+
{
1446+
name: 'task-enabled-server',
1447+
version: '1.0.0'
1448+
},
1449+
{
1450+
capabilities: {
1451+
tools: {},
1452+
// Declare capabilities
1453+
tasks: {
1454+
list: {},
1455+
cancel: {},
1456+
requests: {
1457+
tools: {
1458+
// Declares support for tasks on tools/call
1459+
call: {}
1460+
}
1461+
}
1462+
}
1463+
},
1464+
taskStore // Enable task support
1465+
}
1466+
);
1467+
1468+
// Register a tool that supports tasks using the experimental API
1469+
server.experimental.tasks.registerToolTask(
1470+
'my-echo-tool',
1471+
{
1472+
title: 'My Echo Tool',
1473+
description: 'A simple task-based echo tool.',
1474+
inputSchema: {
1475+
message: z.string().describe('Message to send')
1476+
}
1477+
},
1478+
{
1479+
async createTask({ message }, { taskStore, taskRequestedTtl, requestId }) {
1480+
// Create the task
1481+
const task = await taskStore.createTask({
1482+
ttl: taskRequestedTtl
1483+
});
1484+
1485+
// Simulate out-of-band work
1486+
(async () => {
1487+
await new Promise(resolve => setTimeout(resolve, 5000));
1488+
await taskStore.storeTaskResult(task.taskId, 'completed', {
1489+
content: [
1490+
{
1491+
type: 'text',
1492+
text: message
1493+
}
1494+
]
1495+
});
1496+
})();
1497+
1498+
// Return CreateTaskResult with the created task
1499+
return { task };
1500+
},
1501+
async getTask(_args, { taskId, taskStore }) {
1502+
// Retrieve the task
1503+
return await taskStore.getTask(taskId);
1504+
},
1505+
async getTaskResult(_args, { taskId, taskStore }) {
1506+
// Retrieve the result of the task
1507+
const result = await taskStore.getTaskResult(taskId);
1508+
return result as CallToolResult;
1509+
}
1510+
}
1511+
);
1512+
```
1513+
1514+
**Note**: See `src/examples/shared/inMemoryTaskStore.ts` in the SDK source for a reference task store implementation suitable for development and testing.
1515+
1516+
#### Client-Side: Using Task-Based Execution
1517+
1518+
Clients use `experimental.tasks.callToolStream()` to initiate task-augmented tool calls. The returned `AsyncGenerator` abstracts automatic polling and status updates:
1519+
1520+
```typescript
1521+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
1522+
import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';
1523+
1524+
const client = new Client({
1525+
name: 'task-client',
1526+
version: '1.0.0'
1527+
});
1528+
1529+
// ... connect to server ...
1530+
1531+
// Call the tool with task metadata using the experimental streaming API
1532+
const stream = client.experimental.tasks.callToolStream(
1533+
{
1534+
name: 'my-echo-tool',
1535+
arguments: { message: 'Hello, world!' }
1536+
},
1537+
CallToolResultSchema
1538+
);
1539+
1540+
// Iterate the stream and handle stream events
1541+
let taskId = '';
1542+
for await (const message of stream) {
1543+
switch (message.type) {
1544+
case 'taskCreated':
1545+
console.log('Task created successfully with ID:', message.task.taskId);
1546+
taskId = message.task.taskId;
1547+
break;
1548+
case 'taskStatus':
1549+
console.log(` ${message.task.status}${message.task.statusMessage ?? ''}`);
1550+
break;
1551+
case 'result':
1552+
console.log('Task completed! Tool result:');
1553+
message.result.content.forEach(item => {
1554+
if (item.type === 'text') {
1555+
console.log(` ${item.text}`);
1556+
}
1557+
});
1558+
break;
1559+
case 'error':
1560+
throw message.error;
1561+
}
1562+
}
1563+
1564+
// Optional: Fire and forget - disconnect and reconnect later
1565+
// (useful when you don't want to wait for long-running tasks)
1566+
// Later, after disconnecting and reconnecting to the server:
1567+
const taskStatus = await client.getTask({ taskId });
1568+
console.log('Task status:', taskStatus.status);
1569+
1570+
if (taskStatus.status === 'completed') {
1571+
const taskResult = await client.getTaskResult({ taskId }, CallToolResultSchema);
1572+
console.log('Retrieved result after reconnect:', taskResult);
1573+
}
1574+
```
1575+
1576+
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.
1577+
1578+
#### Task Status Lifecycle
1579+
1580+
Tasks transition through the following states:
1581+
1582+
- **working**: Task is actively being processed
1583+
- **input_required**: Task is waiting for additional input (e.g., from elicitation)
1584+
- **completed**: Task finished successfully
1585+
- **failed**: Task encountered an error
1586+
- **cancelled**: Task was cancelled by the client
1587+
1588+
The `ttl` parameter suggests how long the server will manage the task for. If the task duration exceeds this, the server may delete the task prematurely. The client's suggested value may be overridden by the server, and the final TTL will be provided in `Task.ttl` in
1589+
`taskCreated` and `taskStatus` events.
1590+
13901591
### Writing MCP Clients
13911592

13921593
The SDK provides a high-level client interface:

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/*"

0 commit comments

Comments
 (0)