Skip to content
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
71 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
723bc7d
Add session ID to TaskStore methods
LucaButBoring Nov 5, 2025
3789080
Implement tasks/delete
LucaButBoring Nov 5, 2025
9ae5f84
Rename pollFrequency to pollInterval
LucaButBoring Nov 5, 2025
719675a
Implement capabilities for tasks
LucaButBoring Nov 5, 2025
7a4f52b
Add taskHint for tool-level signposting
LucaButBoring Nov 5, 2025
01be32d
Only auto-add task ID if server capability is set
LucaButBoring Nov 6, 2025
0b8ced2
Correctly check peer task capabilities on receiving end
LucaButBoring Nov 6, 2025
0db292a
Merge branch 'main' of https://github.com/modelcontextprotocol/typesc…
LucaButBoring Nov 10, 2025
b4207b9
Introduce shim task interface for tools
LucaButBoring Nov 10, 2025
b24ea0f
Remove most capabilities and automatic task management
LucaButBoring Nov 11, 2025
4d570d3
Support customizing task pollInterval on server
LucaButBoring Nov 11, 2025
2382df4
Remove submitted and unknown statuses
LucaButBoring Nov 11, 2025
e4c2695
Merge branch 'main' of upstream into feat/tasks
LucaButBoring Nov 11, 2025
388e603
Fix spec types test compatibility after upstream merge
LucaButBoring Nov 11, 2025
b1cf3cf
Merge branch 'main' of https://github.com/modelcontextprotocol/typesc…
LucaButBoring Nov 17, 2025
8d06d4f
chore: commit new lockfile
LucaButBoring Nov 17, 2025
5975661
Introduce CreateTaskResult to request flow in tools
LucaButBoring Nov 18, 2025
0af275f
Implement tasks/cancel
LucaButBoring Nov 18, 2025
25d0b14
Update TaskStore interface
LucaButBoring Nov 18, 2025
064568b
Preliminary tasks/result implementation updates
LucaButBoring Nov 19, 2025
9296ea8
Handle input_required state in PendingRequest
LucaButBoring Nov 19, 2025
ffc1282
Implement task status notifications
LucaButBoring Nov 19, 2025
0bf0d70
Remove task creation notification
LucaButBoring Nov 19, 2025
b945454
Remove types for task creation notification
LucaButBoring Nov 19, 2025
1c7b332
Use actual notification schema
LucaButBoring Nov 19, 2025
dcd761b
Remove related-task metadata from messages that already include it as…
LucaButBoring Nov 19, 2025
ad220e0
Update taskHint implementation on server
LucaButBoring Nov 19, 2025
a3ebe62
Add tests for task cancellation vs request cancellation
LucaButBoring Nov 19, 2025
901ec9c
Validate task status transitions; allow failure result
LucaButBoring Nov 19, 2025
92dfcf8
Fix registerToolTask type inference with overloads
LucaButBoring Nov 19, 2025
5af038d
Clean up TTL handling
LucaButBoring Nov 19, 2025
5996224
Add task-listing tests
LucaButBoring Nov 19, 2025
b1e1401
Implement cross-request progress
LucaButBoring Nov 20, 2025
f668117
Backfill unit tests for task changes
LucaButBoring Nov 20, 2025
370fd07
Implement end to end tests for tasks
LucaButBoring Nov 20, 2025
304ff0a
Add elicitation integration tests, fix type issues
LucaButBoring Nov 20, 2025
b562a09
Implement SSE side-channeling on tasks/result
LucaButBoring Nov 20, 2025
dba843b
Replace PendingRequest with async generator
LucaButBoring Nov 20, 2025
57ab83b
Remove unneeded casts in requestStream()
LucaButBoring Nov 20, 2025
a279a45
Reuse callToolStream inside of callTool
LucaButBoring Nov 20, 2025
94681c7
Merge branch 'main' of https://github.com/modelcontextprotocol/typesc…
LucaButBoring Nov 21, 2025
e2f6e89
Add missing schema
LucaButBoring Nov 21, 2025
b9c3054
Remove now-unused isomorphic UUID dependency
LucaButBoring Nov 21, 2025
cd89e76
Use same polling interval in waitForTaskUpdate as caller would use
LucaButBoring Nov 21, 2025
70803d8
Make queue implementation swappable
LucaButBoring Nov 21, 2025
702d077
Merge branch 'main' into feat/tasks
LucaButBoring Nov 21, 2025
86cc5cc
Refactor TaskMessageQueue to not store response closures
LucaButBoring Nov 21, 2025
8cf6675
Move annotations.taskHint to execution.taskSupport
LucaButBoring Nov 21, 2025
8acb942
Remove now-unneeded _taskResultWaiters map
LucaButBoring Nov 22, 2025
e7143f1
Don't fail the task or dump the queue on a failed enqueue operation
LucaButBoring Nov 22, 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
32 changes: 23 additions & 9 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
25 changes: 25 additions & 0 deletions src/client/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { mergeCapabilities, Protocol, type ProtocolOptions, type RequestOptions } from '../shared/protocol.js';
import type { Transport } from '../shared/transport.js';
import { PendingRequest } from '../shared/request.js';
import { v4 as uuidv4 } from '@lukeed/uuid';
import {
type CallToolRequest,
CallToolResultSchema,
Expand Down Expand Up @@ -357,6 +359,29 @@ export class Client<
return this.request({ method: 'resources/unsubscribe', params }, EmptyResultSchema, options);
}

/**
* Begins a tool call and returns a PendingRequest for granular control over task-based execution.
*
* This is useful when you want to create a task for a long-running tool call and poll for results later.
*/
beginCallTool(
params: CallToolRequest['params'],
resultSchema: typeof CallToolResultSchema | typeof CompatibilityCallToolResultSchema = CallToolResultSchema,
options?: RequestOptions
): PendingRequest<ClientRequest | RequestT, ClientNotification | NotificationT, ClientResult | ResultT> {
// Automatically add task metadata if not provided
const optionsWithTask = {
...options,
task: options?.task ?? { taskId: uuidv4() }
};
return this.beginRequest({ method: 'tools/call', params }, resultSchema, optionsWithTask);
}

/**
* Calls a tool and waits for the result. Automatically validates structured output if the tool has an outputSchema.
*
* For task-based execution with granular control, use beginCallTool() instead.
*/
async callTool(
params: CallToolRequest['params'],
resultSchema: typeof CallToolResultSchema | typeof CompatibilityCallToolResultSchema = CallToolResultSchema,
Expand Down
83 changes: 82 additions & 1 deletion src/examples/client/simpleStreamableHttp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ import {
ElicitRequestSchema,
ResourceLink,
ReadResourceRequest,
ReadResourceResultSchema
ReadResourceResultSchema,
RELATED_TASK_META_KEY
} from '../../types.js';
import { getDisplayName } from '../../shared/metadataUtils.js';
import { Ajv } from 'ajv';
Expand Down Expand Up @@ -58,6 +59,7 @@ function printHelp(): void {
console.log(' reconnect - Reconnect to the server');
console.log(' list-tools - List available tools');
console.log(' call-tool <name> [args] - Call a tool with optional JSON arguments');
console.log(' call-tool-task <name> [args] - Call a tool with task-based execution (example: call-tool-task delay {"duration":3000})');
console.log(' greet [name] - Call the greet tool');
console.log(' multi-greet [name] - Call the multi-greet tool with notifications');
console.log(' collect-info [type] - Test elicitation with collect-user-info tool (contact/preferences/feedback)');
Expand Down Expand Up @@ -141,6 +143,23 @@ function commandLoop(): void {
break;
}

case 'call-tool-task':
if (args.length < 2) {
console.log('Usage: call-tool-task <name> [args]');
} else {
const toolName = args[1];
let toolArgs = {};
if (args.length > 2) {
try {
toolArgs = JSON.parse(args.slice(2).join(' '));
} catch {
console.log('Invalid JSON arguments. Using empty args.');
}
}
await callToolTask(toolName, toolArgs);
}
break;

case 'list-prompts':
await listPrompts();
break;
Expand Down Expand Up @@ -231,6 +250,7 @@ async function connect(url?: string): Promise<void> {
client.setRequestHandler(ElicitRequestSchema, async request => {
console.log('\n🔔 Elicitation Request Received:');
console.log(`Message: ${request.params.message}`);
console.log(`Related Task: ${request.params._meta?.[RELATED_TASK_META_KEY]?.taskId}`);
console.log('Requested Schema:');
console.log(JSON.stringify(request.params.requestedSchema, null, 2));

Expand Down Expand Up @@ -777,6 +797,67 @@ async function readResource(uri: string): Promise<void> {
}
}

async function callToolTask(name: string, args: Record<string, unknown>): Promise<void> {
if (!client) {
console.log('Not connected to server.');
return;
}

console.log(`Calling tool '${name}' with task-based execution...`);
console.log('Arguments:', args);

// Use task-based execution - call now, fetch later
const taskId = `task-${Date.now()}`;
console.log(`Task ID: ${taskId}`);
console.log('This will return immediately while processing continues in the background...');

try {
// Begin the tool call with task metadata
const pendingRequest = client.beginCallTool(
{
name,
arguments: args
},
CallToolResultSchema,
{
task: {
taskId,
keepAlive: 60000 // Keep results for 60 seconds
}
}
);

console.log('Waiting for task completion...');

let lastStatus = '';
await pendingRequest.result({
onTaskCreated: () => {
console.log('Task created successfully');
},
onTaskStatus: task => {
if (lastStatus !== task.status) {
console.log(` ${task.status}${task.error ? ` - ${task.error}` : ''}`);
}
lastStatus = task.status;
}
});

console.log('Task completed! Fetching result...');

// Get the actual result
const result = await client.getTaskResult({ taskId }, CallToolResultSchema);

console.log('Tool result:');
result.content.forEach(item => {
if (item.type === 'text') {
console.log(` ${item.text}`);
}
});
} catch (error) {
console.log(`Error with task-based execution: ${error}`);
}
}

async function cleanup(): Promise<void> {
if (client && transport) {
try {
Expand Down
Loading