Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
201 changes: 201 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 @@ -1387,6 +1388,206 @@ const client = new Client(
);
```

### Task-Based Execution

> **⚠️ Experimental API**: Task-based execution is an experimental feature and may change without notice. Access these APIs via the `.experimental.tasks` namespace.

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/experimental';
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(taskParams, requestId, request, sessionId?): Promise<Task> {
// Generate unique taskId and lastUpdatedAt/createdAt timestamps
// Store task in your database, using the session ID as a proxy to restrict unauthorized access
// Return final Task object
}

async getTask(taskId): Promise<Task | null> {
// Retrieve task from your database
}

async updateTaskStatus(taskId, status, statusMessage?): Promise<void> {
// Update task status in your database
}

async storeTaskResult(taskId, result): Promise<void> {
// Store task result in your database
}

async getTaskResult(taskId): Promise<Result> {
// Retrieve task result from your database
}

async listTasks(cursor?, sessionId?): Promise<{ tasks: Task[]; nextCursor?: string }> {
// List tasks with pagination support
}
}

const taskStore = new MyTaskStore();

const server = new Server(
{
name: 'task-enabled-server',
version: '1.0.0'
},
{
capabilities: {
tools: {},
// Declare capabilities
tasks: {
list: {},
cancel: {},
requests: {
tools: {
// Declares support for tasks on tools/call
call: {}
}
}
}
},
taskStore // Enable task support
}
);

// Register a tool that supports tasks using the experimental API
server.experimental.tasks.registerToolTask(
'my-echo-tool',
{
title: 'My Echo Tool',
description: 'A simple task-based echo tool.',
inputSchema: {
message: z.string().describe('Message to send')
}
},
{
async createTask({ message }, { taskStore, taskRequestedTtl, requestId }) {
// Create the task
const task = await taskStore.createTask({
ttl: taskRequestedTtl
});

// Simulate out-of-band work
(async () => {
await new Promise(resolve => setTimeout(resolve, 5000));
await taskStore.storeTaskResult(task.taskId, 'completed', {
content: [
{
type: 'text',
text: message
}
]
});
})();

// Return CreateTaskResult with the created task
return { task };
},
async getTask(_args, { taskId, taskStore }) {
// Retrieve the task
return await taskStore.getTask(taskId);
},
async getTaskResult(_args, { taskId, taskStore }) {
// Retrieve the result of the task
const result = await taskStore.getTaskResult(taskId);
return result as CallToolResult;
}
}
);
```

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

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

Clients use `experimental.tasks.callToolStream()` to initiate task-augmented tool calls. The returned `AsyncGenerator` abstracts automatic polling and status updates:

```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 ...

// Call the tool with task metadata using the experimental streaming API
const stream = client.experimental.tasks.callToolStream(
{
name: 'my-echo-tool',
arguments: { message: 'Hello, world!' }
},
CallToolResultSchema
);

// Iterate the stream and handle stream events
let taskId = '';
for await (const message of stream) {
switch (message.type) {
case 'taskCreated':
console.log('Task created successfully with ID:', message.task.taskId);
taskId = message.task.taskId;
break;
case 'taskStatus':
console.log(` ${message.task.status}${message.task.statusMessage ?? ''}`);
break;
case 'result':
console.log('Task completed! Tool result:');
message.result.content.forEach(item => {
if (item.type === 'text') {
console.log(` ${item.text}`);
}
});
break;
case 'error':
throw message.error;
}
}

// Optional: 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);
}
```

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.

#### Task Status Lifecycle

Tasks transition through the following states:

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

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
`taskCreated` and `taskStatus` events.

### Writing MCP Clients

The SDK provides a high-level client interface:
Expand Down
8 changes: 8 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@
"import": "./dist/esm/validation/cfworker-provider.js",
"require": "./dist/cjs/validation/cfworker-provider.js"
},
"./experimental": {
"import": "./dist/esm/experimental/index.js",
"require": "./dist/cjs/experimental/index.js"
},
"./experimental/tasks": {
"import": "./dist/esm/experimental/tasks/index.js",
"require": "./dist/cjs/experimental/tasks/index.js"
},
"./*": {
"import": "./dist/esm/*",
"require": "./dist/cjs/*"
Expand Down
Loading
Loading