Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
16ba027
Add types for tasks
LucaButBoring Oct 22, 2025
ecef231
Implement PendingRequest and basic task API
LucaButBoring Oct 22, 2025
41f2124
Implement RelatedTask metadata sends
LucaButBoring Oct 22, 2025
a8fabb6
Implement task state management
LucaButBoring Oct 22, 2025
b3420b3
Attach related task metadata to request handler
LucaButBoring Oct 22, 2025
8e17d04
Create task before calling handler
LucaButBoring Oct 23, 2025
fcd2882
Create task example
LucaButBoring Oct 23, 2025
c73b105
Implement input_required status for tasks
LucaButBoring Oct 23, 2025
b028061
Implement unit tests for task support
LucaButBoring Oct 23, 2025
d9b72f0
Add docs for task augmentation
LucaButBoring Oct 23, 2025
5dc999f
Implement tasks/list method
LucaButBoring Oct 27, 2025
a2d65df
Merge branch 'main' of https://github.com/modelcontextprotocol/typesc…
LucaButBoring Oct 27, 2025
71a9568
Automatically execute tool calls as tasks
LucaButBoring Oct 29, 2025
30043ed
Merge branch 'main' into feat/tasks
LucaButBoring Oct 31, 2025
2167b43
Implement task API tests on both the client and server
LucaButBoring Nov 1, 2025
12d0f66
Make default task polling interval configurable
LucaButBoring Nov 1, 2025
6a28003
Merge branch 'main' into feat/tasks
LucaButBoring Nov 3, 2025
bb28ef7
Exclude relatedTask from RequestHandlerExtra
LucaButBoring Nov 3, 2025
0bf2b42
Mark tasks as cancelled only after confirming abort
LucaButBoring Nov 3, 2025
486e8ed
Store task result before attempting to respond to client
LucaButBoring Nov 3, 2025
06db603
Allow task polling before creation notification arrives
LucaButBoring Nov 3, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 164 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
- [Improving Network Efficiency with Notification Debouncing](#improving-network-efficiency-with-notification-debouncing)
- [Low-Level Server](#low-level-server)
- [Eliciting User Input](#eliciting-user-input)
- [Task-Based Execution](#task-based-execution)
- [Writing MCP Clients](#writing-mcp-clients)
- [Proxy Authorization Requests Upstream](#proxy-authorization-requests-upstream)
- [Backwards Compatibility](#backwards-compatibility)
Expand Down Expand Up @@ -1301,6 +1302,169 @@ client.setRequestHandler(ElicitRequestSchema, async request => {

**Note**: Elicitation requires client support. Clients must declare the `elicitation` capability during initialization.

### Task-Based Execution

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.

Common use cases include:

- Long-running data processing or analysis
- Code migration or refactoring operations
- Complex computational tasks
- Operations that require periodic status updates

#### Server-Side: Implementing Task Support

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:

```typescript
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { TaskStore } from '@modelcontextprotocol/sdk/shared/task.js';
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';

// Implement TaskStore backed by your database (e.g., PostgreSQL, Redis, etc.)
class MyTaskStore implements TaskStore {
async createTask(metadata, requestId, request) {
// Store task in your database
}

async getTask(taskId) {
// Retrieve task from your database
}

async updateTaskStatus(taskId, status, errorMessage?) {
// Update task status in your database
}

async storeTaskResult(taskId, result) {
// Store task result in your database
}

async getTaskResult(taskId) {
// Retrieve task result from your database
}
}

const taskStore = new MyTaskStore();

const server = new Server(
{
name: 'task-enabled-server',
version: '1.0.0'
},
{
capabilities: {
tools: {}
},
taskStore // Enable task support
}
);

// Set up a long-running tool handler as usual
server.setRequestHandler(CallToolRequestSchema, async request => {
if (request.params.name === 'analyze-data') {
// Simulate long-running analysis
await new Promise(resolve => setTimeout(resolve, 30000));

return {
content: [
{
type: 'text',
text: 'Analysis complete!'
}
]
};
}
throw new Error('Unknown tool');
});

server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: 'analyze-data',
description: 'Perform data analysis (long-running)',
inputSchema: {
type: 'object',
properties: {
dataset: { type: 'string' }
}
}
}
]
}));
```

**Note**: See `src/examples/shared/inMemoryTaskStore.ts` in the SDK source for a reference implementation suitable for development and testing.

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

Clients use `beginCallTool()` to initiate task-based operations. The returned `PendingRequest` object provides automatic polling and status tracking:

```typescript
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';

const client = new Client({
name: 'task-client',
version: '1.0.0'
});

// ... connect to server ...

// Initiate a task-based tool call
const taskId = 'analysis-task-123';
const pendingRequest = client.beginCallTool(
{
name: 'analyze-data',
arguments: { dataset: 'user-data.csv' }
},
CallToolResultSchema,
{
task: {
taskId,
keepAlive: 300000 // Keep results for 5 minutes after completion
}
}
);

// Option 1: Wait for completion with status callbacks
const result = await pendingRequest.result({
onTaskCreated: () => {
console.log('Task created successfully');
},
onTaskStatus: task => {
console.log(`Task status: ${task.status}`);
// Status can be: 'submitted', 'working', 'input_required', 'completed', 'failed', or 'cancelled'
}
});
console.log('Task completed:', result);

// Option 2: Fire and forget - disconnect and reconnect later
// (useful when you don't want to wait for long-running tasks)
// Later, after disconnecting and reconnecting to the server:
const taskStatus = await client.getTask({ taskId });
console.log('Task status:', taskStatus.status);

if (taskStatus.status === 'completed') {
const taskResult = await client.getTaskResult({ taskId }, CallToolResultSchema);
console.log('Retrieved result after reconnect:', taskResult);
}
```

#### Task Status Lifecycle

Tasks transition through the following states:

- **submitted**: Task has been created and queued
- **working**: Task is actively being processed
- **input_required**: Task is waiting for additional input (e.g., from elicitation)
- **completed**: Task finished successfully
- **failed**: Task encountered an error
- **cancelled**: Task was cancelled by the client
- **unknown**: Task status could not be determined (terminal state, rarely occurs)

The `keepAlive` parameter determines how long the server retains task results after completion. This allows clients to retrieve results even after disconnecting and reconnecting.

### Writing MCP Clients

The SDK provides a high-level client interface:
Expand Down
22 changes: 22 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
"client": "tsx src/cli.ts client"
},
"dependencies": {
"@lukeed/uuid": "^2.0.1",
"ajv": "^8.17.1",
"ajv-formats": "^3.0.1",
"content-type": "^1.0.5",
Expand Down
Loading