Skip to content

Commit 3822a51

Browse files
Merge branch 'main' into improvement/handle-string-expires_in
2 parents 289f007 + b9538a2 commit 3822a51

Some content is hidden

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

43 files changed

+17224
-338
lines changed

README.md

Lines changed: 210 additions & 4 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)
@@ -38,7 +39,7 @@
3839
## Overview
3940

4041
The Model Context Protocol allows applications to provide context for LLMs in a standardized way, separating the concerns of providing context from the actual LLM interaction. This TypeScript SDK implements
41-
[the full MCP specification](https://modelcontextprotocol.io/specification/latest), making it easy to:
42+
[the full MCP specification](https://modelcontextprotocol.io/specification/draft), making it easy to:
4243

4344
- Create MCP servers that expose resources, prompts and tools
4445
- Build MCP clients that can connect to any MCP server
@@ -158,7 +159,7 @@ const server = new McpServer({
158159

159160
### Tools
160161

161-
[Tools](https://modelcontextprotocol.io/specification/latest/server/tools) let LLMs take actions through your server. Tools can perform computation, fetch data and have side effects. Tools should be designed to be model-controlled - i.e. AI models will decide which tools to call,
162+
[Tools](https://modelcontextprotocol.io/specification/draft/server/tools) let LLMs take actions through your server. Tools can perform computation, fetch data and have side effects. Tools should be designed to be model-controlled - i.e. AI models will decide which tools to call,
162163
and the arguments.
163164

164165
```typescript
@@ -259,7 +260,7 @@ Tools can return `ResourceLink` objects to reference resources without embedding
259260

260261
### Resources
261262

262-
[Resources](https://modelcontextprotocol.io/specification/latest/server/resources) can also expose data to LLMs, but unlike tools shouldn't perform significant computation or have side effects.
263+
[Resources](https://modelcontextprotocol.io/specification/draft/server/resources) can also expose data to LLMs, but unlike tools shouldn't perform significant computation or have side effects.
263264

264265
Resources are designed to be used in an application-driven way, meaning MCP client applications can decide how to expose them. For example, a client could expose a resource picker to the human, or could expose them to the model directly.
265266

@@ -333,7 +334,7 @@ server.registerResource(
333334

334335
### Prompts
335336

336-
[Prompts](https://modelcontextprotocol.io/specification/latest/server/prompts) are reusable templates that help humans prompt models to interact with your server. They're designed to be user-driven, and might appear as slash commands in a chat interface.
337+
[Prompts](https://modelcontextprotocol.io/specification/draft/server/prompts) are reusable templates that help humans prompt models to interact with your server. They're designed to be user-driven, and might appear as slash commands in a chat interface.
337338

338339
```typescript
339340
import { completable } from '@modelcontextprotocol/sdk/server/completable.js';
@@ -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:

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@modelcontextprotocol/sdk",
3-
"version": "1.23.0-beta.0",
3+
"version": "1.23.0",
44
"description": "Model Context Protocol implementation for TypeScript",
55
"license": "MIT",
66
"author": "Anthropic, PBC (https://anthropic.com)",
@@ -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/*"

src/client/auth.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -625,10 +625,12 @@ export async function discoverOAuthProtectedResourceMetadata(
625625
});
626626

627627
if (!response || response.status === 404) {
628+
await response?.body?.cancel();
628629
throw new Error(`Resource server does not implement OAuth 2.0 Protected Resource Metadata.`);
629630
}
630631

631632
if (!response.ok) {
633+
await response.body?.cancel();
632634
throw new Error(`HTTP ${response.status} trying to load well-known OAuth protected resource metadata.`);
633635
}
634636
return OAuthProtectedResourceMetadataSchema.parse(await response.json());
@@ -756,10 +758,12 @@ export async function discoverOAuthMetadata(
756758
});
757759

758760
if (!response || response.status === 404) {
761+
await response?.body?.cancel();
759762
return undefined;
760763
}
761764

762765
if (!response.ok) {
766+
await response.body?.cancel();
763767
throw new Error(`HTTP ${response.status} trying to load well-known OAuth metadata`);
764768
}
765769

@@ -869,6 +873,7 @@ export async function discoverAuthorizationServerMetadata(
869873
}
870874

871875
if (!response.ok) {
876+
await response.body?.cancel();
872877
// Continue looking for any 4xx response code.
873878
if (response.status >= 400 && response.status < 500) {
874879
continue; // Try next URL

0 commit comments

Comments
 (0)