|
26 | 26 | - [Improving Network Efficiency with Notification Debouncing](#improving-network-efficiency-with-notification-debouncing) |
27 | 27 | - [Low-Level Server](#low-level-server) |
28 | 28 | - [Eliciting User Input](#eliciting-user-input) |
| 29 | + - [Task-Based Execution](#task-based-execution) |
29 | 30 | - [Writing MCP Clients](#writing-mcp-clients) |
30 | 31 | - [Proxy Authorization Requests Upstream](#proxy-authorization-requests-upstream) |
31 | 32 | - [Backwards Compatibility](#backwards-compatibility) |
@@ -1387,6 +1388,206 @@ const client = new Client( |
1387 | 1388 | ); |
1388 | 1389 | ``` |
1389 | 1390 |
|
| 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 | + |
1390 | 1591 | ### Writing MCP Clients |
1391 | 1592 |
|
1392 | 1593 | The SDK provides a high-level client interface: |
|
0 commit comments