|
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) |
@@ -1301,6 +1302,169 @@ client.setRequestHandler(ElicitRequestSchema, async request => { |
1301 | 1302 |
|
1302 | 1303 | **Note**: Elicitation requires client support. Clients must declare the `elicitation` capability during initialization. |
1303 | 1304 |
|
| 1305 | +### Task-Based Execution |
| 1306 | + |
| 1307 | +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. |
| 1308 | + |
| 1309 | +Common use cases include: |
| 1310 | + |
| 1311 | +- Long-running data processing or analysis |
| 1312 | +- Code migration or refactoring operations |
| 1313 | +- Complex computational tasks |
| 1314 | +- Operations that require periodic status updates |
| 1315 | + |
| 1316 | +#### Server-Side: Implementing Task Support |
| 1317 | + |
| 1318 | +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: |
| 1319 | + |
| 1320 | +```typescript |
| 1321 | +import { Server } from '@modelcontextprotocol/sdk/server/index.js'; |
| 1322 | +import { TaskStore } from '@modelcontextprotocol/sdk/shared/task.js'; |
| 1323 | +import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'; |
| 1324 | + |
| 1325 | +// Implement TaskStore backed by your database (e.g., PostgreSQL, Redis, etc.) |
| 1326 | +class MyTaskStore implements TaskStore { |
| 1327 | + async createTask(metadata, requestId, request) { |
| 1328 | + // Store task in your database |
| 1329 | + } |
| 1330 | + |
| 1331 | + async getTask(taskId) { |
| 1332 | + // Retrieve task from your database |
| 1333 | + } |
| 1334 | + |
| 1335 | + async updateTaskStatus(taskId, status, errorMessage?) { |
| 1336 | + // Update task status in your database |
| 1337 | + } |
| 1338 | + |
| 1339 | + async storeTaskResult(taskId, result) { |
| 1340 | + // Store task result in your database |
| 1341 | + } |
| 1342 | + |
| 1343 | + async getTaskResult(taskId) { |
| 1344 | + // Retrieve task result from your database |
| 1345 | + } |
| 1346 | +} |
| 1347 | + |
| 1348 | +const taskStore = new MyTaskStore(); |
| 1349 | + |
| 1350 | +const server = new Server( |
| 1351 | + { |
| 1352 | + name: 'task-enabled-server', |
| 1353 | + version: '1.0.0' |
| 1354 | + }, |
| 1355 | + { |
| 1356 | + capabilities: { |
| 1357 | + tools: {} |
| 1358 | + }, |
| 1359 | + taskStore // Enable task support |
| 1360 | + } |
| 1361 | +); |
| 1362 | + |
| 1363 | +// Set up a long-running tool handler as usual |
| 1364 | +server.setRequestHandler(CallToolRequestSchema, async request => { |
| 1365 | + if (request.params.name === 'analyze-data') { |
| 1366 | + // Simulate long-running analysis |
| 1367 | + await new Promise(resolve => setTimeout(resolve, 30000)); |
| 1368 | + |
| 1369 | + return { |
| 1370 | + content: [ |
| 1371 | + { |
| 1372 | + type: 'text', |
| 1373 | + text: 'Analysis complete!' |
| 1374 | + } |
| 1375 | + ] |
| 1376 | + }; |
| 1377 | + } |
| 1378 | + throw new Error('Unknown tool'); |
| 1379 | +}); |
| 1380 | + |
| 1381 | +server.setRequestHandler(ListToolsRequestSchema, async () => ({ |
| 1382 | + tools: [ |
| 1383 | + { |
| 1384 | + name: 'analyze-data', |
| 1385 | + description: 'Perform data analysis (long-running)', |
| 1386 | + inputSchema: { |
| 1387 | + type: 'object', |
| 1388 | + properties: { |
| 1389 | + dataset: { type: 'string' } |
| 1390 | + } |
| 1391 | + } |
| 1392 | + } |
| 1393 | + ] |
| 1394 | +})); |
| 1395 | +``` |
| 1396 | + |
| 1397 | +**Note**: See `src/examples/shared/inMemoryTaskStore.ts` in the SDK source for a reference implementation suitable for development and testing. |
| 1398 | + |
| 1399 | +#### Client-Side: Using Task-Based Execution |
| 1400 | + |
| 1401 | +Clients use `beginCallTool()` to initiate task-based operations. The returned `PendingRequest` object provides automatic polling and status tracking: |
| 1402 | + |
| 1403 | +```typescript |
| 1404 | +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; |
| 1405 | +import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js'; |
| 1406 | + |
| 1407 | +const client = new Client({ |
| 1408 | + name: 'task-client', |
| 1409 | + version: '1.0.0' |
| 1410 | +}); |
| 1411 | + |
| 1412 | +// ... connect to server ... |
| 1413 | + |
| 1414 | +// Initiate a task-based tool call |
| 1415 | +const taskId = 'analysis-task-123'; |
| 1416 | +const pendingRequest = client.beginCallTool( |
| 1417 | + { |
| 1418 | + name: 'analyze-data', |
| 1419 | + arguments: { dataset: 'user-data.csv' } |
| 1420 | + }, |
| 1421 | + CallToolResultSchema, |
| 1422 | + { |
| 1423 | + task: { |
| 1424 | + taskId, |
| 1425 | + keepAlive: 300000 // Keep results for 5 minutes after completion |
| 1426 | + } |
| 1427 | + } |
| 1428 | +); |
| 1429 | + |
| 1430 | +// Option 1: Wait for completion with status callbacks |
| 1431 | +const result = await pendingRequest.result({ |
| 1432 | + onTaskCreated: () => { |
| 1433 | + console.log('Task created successfully'); |
| 1434 | + }, |
| 1435 | + onTaskStatus: task => { |
| 1436 | + console.log(`Task status: ${task.status}`); |
| 1437 | + // Status can be: 'submitted', 'working', 'input_required', 'completed', 'failed', or 'cancelled' |
| 1438 | + } |
| 1439 | +}); |
| 1440 | +console.log('Task completed:', result); |
| 1441 | + |
| 1442 | +// Option 2: Fire and forget - disconnect and reconnect later |
| 1443 | +// (useful when you don't want to wait for long-running tasks) |
| 1444 | +// Later, after disconnecting and reconnecting to the server: |
| 1445 | +const taskStatus = await client.getTask({ taskId }); |
| 1446 | +console.log('Task status:', taskStatus.status); |
| 1447 | + |
| 1448 | +if (taskStatus.status === 'completed') { |
| 1449 | + const taskResult = await client.getTaskResult({ taskId }, CallToolResultSchema); |
| 1450 | + console.log('Retrieved result after reconnect:', taskResult); |
| 1451 | +} |
| 1452 | +``` |
| 1453 | + |
| 1454 | +#### Task Status Lifecycle |
| 1455 | + |
| 1456 | +Tasks transition through the following states: |
| 1457 | + |
| 1458 | +- **submitted**: Task has been created and queued |
| 1459 | +- **working**: Task is actively being processed |
| 1460 | +- **input_required**: Task is waiting for additional input (e.g., from elicitation) |
| 1461 | +- **completed**: Task finished successfully |
| 1462 | +- **failed**: Task encountered an error |
| 1463 | +- **cancelled**: Task was cancelled by the client |
| 1464 | +- **unknown**: Task status could not be determined (terminal state, rarely occurs) |
| 1465 | + |
| 1466 | +The `keepAlive` parameter determines how long the server retains task results after completion. This allows clients to retrieve results even after disconnecting and reconnecting. |
| 1467 | + |
1304 | 1468 | ### Writing MCP Clients |
1305 | 1469 |
|
1306 | 1470 | The SDK provides a high-level client interface: |
|
0 commit comments