Skip to content

Commit 8afe461

Browse files
Merge branch 'main' into feature/sep-1046-client-credentials
2 parents 6822457 + b9538a2 commit 8afe461

30 files changed

+16171
-246
lines changed

README.md

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
- [Improving Network Efficiency with Notification Debouncing](#improving-network-efficiency-with-notification-debouncing)
2727
- [Low-Level Server](#low-level-server)
2828
- [Eliciting User Input](#eliciting-user-input)
29+
- [Task-Based Execution](#task-based-execution)
2930
- [Writing MCP Clients](#writing-mcp-clients)
3031
- [Proxy Authorization Requests Upstream](#proxy-authorization-requests-upstream)
3132
- [Backwards Compatibility](#backwards-compatibility)
@@ -625,6 +626,11 @@ app.post('/mcp', async (req, res) => {
625626
}
626627
});
627628

629+
// Handle GET requests when session management is not supported - the server must return an HTTP 405 status code in this case
630+
app.get('/mcp', (req, res) => {
631+
res.status(405).end();
632+
});
633+
628634
const port = parseInt(process.env.PORT || '3000');
629635
app.listen(port, () => {
630636
console.log(`MCP Server running on http://localhost:${port}/mcp`);
@@ -1406,6 +1412,206 @@ const client = new Client(
14061412
);
14071413
```
14081414

1415+
### Task-Based Execution
1416+
1417+
> **⚠️ Experimental API**: Task-based execution is an experimental feature and may change without notice. Access these APIs via the `.experimental.tasks` namespace.
1418+
1419+
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.
1420+
1421+
Common use cases include:
1422+
1423+
- Long-running data processing or analysis
1424+
- Code migration or refactoring operations
1425+
- Complex computational tasks
1426+
- Operations that require periodic status updates
1427+
1428+
#### Server-Side: Implementing Task Support
1429+
1430+
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:
1431+
1432+
```typescript
1433+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
1434+
import { TaskStore } from '@modelcontextprotocol/sdk/experimental';
1435+
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
1436+
1437+
// Implement TaskStore backed by your database (e.g., PostgreSQL, Redis, etc.)
1438+
class MyTaskStore implements TaskStore {
1439+
async createTask(taskParams, requestId, request, sessionId?): Promise<Task> {
1440+
// Generate unique taskId and lastUpdatedAt/createdAt timestamps
1441+
// Store task in your database, using the session ID as a proxy to restrict unauthorized access
1442+
// Return final Task object
1443+
}
1444+
1445+
async getTask(taskId): Promise<Task | null> {
1446+
// Retrieve task from your database
1447+
}
1448+
1449+
async updateTaskStatus(taskId, status, statusMessage?): Promise<void> {
1450+
// Update task status in your database
1451+
}
1452+
1453+
async storeTaskResult(taskId, result): Promise<void> {
1454+
// Store task result in your database
1455+
}
1456+
1457+
async getTaskResult(taskId): Promise<Result> {
1458+
// Retrieve task result from your database
1459+
}
1460+
1461+
async listTasks(cursor?, sessionId?): Promise<{ tasks: Task[]; nextCursor?: string }> {
1462+
// List tasks with pagination support
1463+
}
1464+
}
1465+
1466+
const taskStore = new MyTaskStore();
1467+
1468+
const server = new Server(
1469+
{
1470+
name: 'task-enabled-server',
1471+
version: '1.0.0'
1472+
},
1473+
{
1474+
capabilities: {
1475+
tools: {},
1476+
// Declare capabilities
1477+
tasks: {
1478+
list: {},
1479+
cancel: {},
1480+
requests: {
1481+
tools: {
1482+
// Declares support for tasks on tools/call
1483+
call: {}
1484+
}
1485+
}
1486+
}
1487+
},
1488+
taskStore // Enable task support
1489+
}
1490+
);
1491+
1492+
// Register a tool that supports tasks using the experimental API
1493+
server.experimental.tasks.registerToolTask(
1494+
'my-echo-tool',
1495+
{
1496+
title: 'My Echo Tool',
1497+
description: 'A simple task-based echo tool.',
1498+
inputSchema: {
1499+
message: z.string().describe('Message to send')
1500+
}
1501+
},
1502+
{
1503+
async createTask({ message }, { taskStore, taskRequestedTtl, requestId }) {
1504+
// Create the task
1505+
const task = await taskStore.createTask({
1506+
ttl: taskRequestedTtl
1507+
});
1508+
1509+
// Simulate out-of-band work
1510+
(async () => {
1511+
await new Promise(resolve => setTimeout(resolve, 5000));
1512+
await taskStore.storeTaskResult(task.taskId, 'completed', {
1513+
content: [
1514+
{
1515+
type: 'text',
1516+
text: message
1517+
}
1518+
]
1519+
});
1520+
})();
1521+
1522+
// Return CreateTaskResult with the created task
1523+
return { task };
1524+
},
1525+
async getTask(_args, { taskId, taskStore }) {
1526+
// Retrieve the task
1527+
return await taskStore.getTask(taskId);
1528+
},
1529+
async getTaskResult(_args, { taskId, taskStore }) {
1530+
// Retrieve the result of the task
1531+
const result = await taskStore.getTaskResult(taskId);
1532+
return result as CallToolResult;
1533+
}
1534+
}
1535+
);
1536+
```
1537+
1538+
**Note**: See `src/examples/shared/inMemoryTaskStore.ts` in the SDK source for a reference task store implementation suitable for development and testing.
1539+
1540+
#### Client-Side: Using Task-Based Execution
1541+
1542+
Clients use `experimental.tasks.callToolStream()` to initiate task-augmented tool calls. The returned `AsyncGenerator` abstracts automatic polling and status updates:
1543+
1544+
```typescript
1545+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
1546+
import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';
1547+
1548+
const client = new Client({
1549+
name: 'task-client',
1550+
version: '1.0.0'
1551+
});
1552+
1553+
// ... connect to server ...
1554+
1555+
// Call the tool with task metadata using the experimental streaming API
1556+
const stream = client.experimental.tasks.callToolStream(
1557+
{
1558+
name: 'my-echo-tool',
1559+
arguments: { message: 'Hello, world!' }
1560+
},
1561+
CallToolResultSchema
1562+
);
1563+
1564+
// Iterate the stream and handle stream events
1565+
let taskId = '';
1566+
for await (const message of stream) {
1567+
switch (message.type) {
1568+
case 'taskCreated':
1569+
console.log('Task created successfully with ID:', message.task.taskId);
1570+
taskId = message.task.taskId;
1571+
break;
1572+
case 'taskStatus':
1573+
console.log(` ${message.task.status}${message.task.statusMessage ?? ''}`);
1574+
break;
1575+
case 'result':
1576+
console.log('Task completed! Tool result:');
1577+
message.result.content.forEach(item => {
1578+
if (item.type === 'text') {
1579+
console.log(` ${item.text}`);
1580+
}
1581+
});
1582+
break;
1583+
case 'error':
1584+
throw message.error;
1585+
}
1586+
}
1587+
1588+
// Optional: Fire and forget - disconnect and reconnect later
1589+
// (useful when you don't want to wait for long-running tasks)
1590+
// Later, after disconnecting and reconnecting to the server:
1591+
const taskStatus = await client.getTask({ taskId });
1592+
console.log('Task status:', taskStatus.status);
1593+
1594+
if (taskStatus.status === 'completed') {
1595+
const taskResult = await client.getTaskResult({ taskId }, CallToolResultSchema);
1596+
console.log('Retrieved result after reconnect:', taskResult);
1597+
}
1598+
```
1599+
1600+
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.
1601+
1602+
#### Task Status Lifecycle
1603+
1604+
Tasks transition through the following states:
1605+
1606+
- **working**: Task is actively being processed
1607+
- **input_required**: Task is waiting for additional input (e.g., from elicitation)
1608+
- **completed**: Task finished successfully
1609+
- **failed**: Task encountered an error
1610+
- **cancelled**: Task was cancelled by the client
1611+
1612+
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
1613+
`taskCreated` and `taskStatus` events.
1614+
14091615
### Writing MCP Clients
14101616

14111617
The SDK provides a high-level client interface:

package.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,14 @@
4343
"import": "./dist/esm/validation/cfworker-provider.js",
4444
"require": "./dist/cjs/validation/cfworker-provider.js"
4545
},
46+
"./experimental": {
47+
"import": "./dist/esm/experimental/index.js",
48+
"require": "./dist/cjs/experimental/index.js"
49+
},
50+
"./experimental/tasks": {
51+
"import": "./dist/esm/experimental/tasks/index.js",
52+
"require": "./dist/cjs/experimental/tasks/index.js"
53+
},
4654
"./*": {
4755
"import": "./dist/esm/*",
4856
"require": "./dist/cjs/*"

0 commit comments

Comments
 (0)