Skip to content

Commit 71adf3e

Browse files
authored
Merge branch 'main' into fix/use-streamable-http-error-in-send
2 parents 01e62f3 + 5e97e1a commit 71adf3e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+17622
-268
lines changed

README.md

Lines changed: 207 additions & 1 deletion
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`);
@@ -1382,6 +1388,206 @@ const client = new Client(
13821388
);
13831389
```
13841390

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

13871593
The SDK provides a high-level client interface:
@@ -1499,7 +1705,7 @@ try {
14991705
name: 'streamable-http-client',
15001706
version: '1.0.0'
15011707
});
1502-
const transport = new StreamableHTTPClientTransport(new URL(baseUrl));
1708+
const transport = new StreamableHTTPClientTransport(baseUrl);
15031709
await client.connect(transport);
15041710
console.log('Connected using Streamable HTTP transport');
15051711
} catch (error) {

eslint.config.mjs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import eslint from '@eslint/js';
44
import tseslint from 'typescript-eslint';
55
import eslintConfigPrettier from 'eslint-config-prettier/flat';
6+
import nodePlugin from 'eslint-plugin-n';
67

78
export default tseslint.config(
89
eslint.configs.recommended,
@@ -11,8 +12,12 @@ export default tseslint.config(
1112
linterOptions: {
1213
reportUnusedDisableDirectives: false
1314
},
15+
plugins: {
16+
n: nodePlugin
17+
},
1418
rules: {
15-
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }]
19+
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
20+
'n/prefer-node-protocol': 'error'
1621
}
1722
},
1823
{

0 commit comments

Comments
 (0)