refactor: replace RequestHandlerExtra with structured context types (#1467)

This commit is contained in:
Felix Weinberger 2026-02-03 20:35:02 +00:00 • committed by GitHub
commit 7bb79ebbbb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 1224 additions and 770 deletions

View file

@ -144,7 +144,8 @@ When a request arrives from the remote side:
2. **`Protocol.connect()`** routes to `_onrequest()`, `_onresponse()`, or `_onnotification()`
3. **`Protocol._onrequest()`**:
- Looks up handler in `_requestHandlers` map (keyed by method name)
- Creates `RequestHandlerExtra` with `signal`, `sessionId`, `sendNotification`, `sendRequest`
- Creates `BaseContext` with `signal`, `sessionId`, `sendNotification`, `sendRequest`, etc.
- Calls `buildContext()` to let subclasses enrich the context (e.g., Server adds `requestInfo`)
- Invokes handler, sends JSON-RPC response back via transport
4. **Handler** was registered via `setRequestHandler('method', handler)`
@ -152,29 +153,42 @@ When a request arrives from the remote side:
```typescript
// In Client (for server→client requests like sampling, elicitation)
client.setRequestHandler('sampling/createMessage', async (request, extra) => {
client.setRequestHandler('sampling/createMessage', async (request, ctx) => {
// Handle sampling request from server
return { role: "assistant", content: {...}, model: "..." };
});
// In Server (for client→server requests like tools/call)
server.setRequestHandler('tools/call', async (request, extra) => {
server.setRequestHandler('tools/call', async (request, ctx) => {
// Handle tool call from client
return { content: [...] };
});
```
### Request Handler Extra
### Request Handler Context
The `extra` parameter in handlers (`RequestHandlerExtra`) provides:
The `ctx` parameter in handlers provides a structured context:
- `signal`: AbortSignal for cancellation
- `sessionId`: Transport session identifier
- `authInfo`: Validated auth token info (if authenticated)
- `requestId`: JSON-RPC message ID
- `sendNotification(notification)`: Send related notification back
- `sendRequest(request, schema)`: Send related request (for bidirectional flows)
- `taskStore`: Task storage interface (if tasks enabled)
**`BaseContext`** (common to both Server and Client), fields organized into nested groups:
- `sessionId?`: Transport session identifier
- `mcpReq`: Request-level concerns
- `id`: JSON-RPC message ID
- `method`: Request method string (e.g., 'tools/call')
- `_meta?`: Request metadata
- `signal`: AbortSignal for cancellation
- `send(request, schema, options?)`: Send related request (for bidirectional flows)
- `notify(notification)`: Send related notification back
- `http?`: HTTP transport info (undefined for stdio)
- `authInfo?`: Validated auth token info
- `task?`: Task context (`{ id?, store, requestedTtl? }`) when task storage is configured
**`ServerContext`** extends `BaseContext.mcpReq` and `BaseContext.http?` via type intersection:
- `mcpReq` adds: `log(level, data, logger?)`, `elicitInput(params, options?)`, `requestSampling(params, options?)`
- `http?` adds: `req?` (HTTP request info), `closeSSE?`, `closeStandaloneSSE?`
**`ClientContext`** is currently identical to `BaseContext`.
### Capability Checking

View file

@ -297,7 +297,7 @@ extra.requestInfo?.headers['mcp-session-id']
// v2: Headers object, .get() access
headers: new Headers({ 'Authorization': 'Bearer token' })
extra.requestInfo?.headers.get('mcp-session-id')
ctx.http?.req?.headers.get('mcp-session-id')
```
## 8. Removed Server Features
@ -366,11 +366,41 @@ Schema to method string mapping:
Request/notification params remain fully typed. Remove unused schema imports after migration.
## 10. Client Behavioral Changes
## 10. Request Handler Context Types
`RequestHandlerExtra` → structured context types with nested groups. Rename `extra` → `ctx` in all handler callbacks.
| v1 | v2 |
|----|-----|
| `RequestHandlerExtra` | `ServerContext` (server) / `ClientContext` (client) / `BaseContext` (base) |
| `extra` (param name) | `ctx` |
| `extra.signal` | `ctx.mcpReq.signal` |
| `extra.requestId` | `ctx.mcpReq.id` |
| `extra._meta` | `ctx.mcpReq._meta` |
| `extra.sendRequest(...)` | `ctx.mcpReq.send(...)` |
| `extra.sendNotification(...)` | `ctx.mcpReq.notify(...)` |
| `extra.authInfo` | `ctx.http?.authInfo` |
| `extra.sessionId` | `ctx.sessionId` |
| `extra.requestInfo` | `ctx.http?.req` (only `ServerContext`) |
| `extra.closeSSEStream` | `ctx.http?.closeSSE` (only `ServerContext`) |
| `extra.closeStandaloneSSEStream` | `ctx.http?.closeStandaloneSSE` (only `ServerContext`) |
| `extra.taskStore` | `ctx.task?.store` |
| `extra.taskId` | `ctx.task?.id` |
| `extra.taskRequestedTtl` | `ctx.task?.requestedTtl` |
`ServerContext` convenience methods (new in v2, no v1 equivalent):
| Method | Description | Replaces |
|--------|-------------|----------|
| `ctx.mcpReq.log(level, data, logger?)` | Send log notification (respects client's level filter) | `server.sendLoggingMessage(...)` from within handler |
| `ctx.mcpReq.elicitInput(params, options?)` | Elicit user input (form or URL) | `server.elicitInput(...)` from within handler |
| `ctx.mcpReq.requestSampling(params, options?)` | Request LLM sampling from client | `server.createMessage(...)` from within handler |
## 11. Client Behavioral Changes
`Client.listPrompts()`, `listResources()`, `listResourceTemplates()`, `listTools()` now return empty results when the server lacks the corresponding capability (instead of sending the request). Set `enforceStrictCapabilities: true` in `ClientOptions` to throw an error instead.
## 11. Runtime-Specific JSON Schema Validators (Enhancement)
## 12. Runtime-Specific JSON Schema Validators (Enhancement)
The SDK now auto-selects the appropriate JSON Schema validator based on runtime:
- Node.js → `AjvJsonSchemaValidator` (no change from v1)
@ -390,7 +420,7 @@ new McpServer({ name: 'server', version: '1.0.0' }, {});
Access validators via `_shims` export: `import { DefaultJsonSchemaValidator } from '@modelcontextprotocol/server/_shims';`
## 12. Migration Steps (apply in this order)
## 13. Migration Steps (apply in this order)
1. Update `package.json`: `npm uninstall @modelcontextprotocol/sdk`, install the appropriate v2 packages
2. Replace all imports from `@modelcontextprotocol/sdk/...` using the import mapping tables (sections 3-4), including `StreamableHTTPServerTransport` → `NodeStreamableHTTPServerTransport`

View file

@ -156,7 +156,7 @@ const transport = new StreamableHTTPClientTransport(url, {
});
// Reading headers in a request handler
const sessionId = extra.requestInfo?.headers.get('mcp-session-id');
const sessionId = ctx.http?.req?.headers.get('mcp-session-id');
```
### `McpServer.tool()`, `.prompt()`, `.resource()` removed
@ -381,6 +381,83 @@ import { JSONRPCError, ResourceReference, isJSONRPCError } from '@modelcontextpr
import { JSONRPCErrorResponse, ResourceTemplateReference, isJSONRPCErrorResponse } from '@modelcontextprotocol/core';
```
### Request handler context types
The `RequestHandlerExtra` type has been replaced with a structured context type hierarchy using nested groups:
| v1 | v2 |
|----|-----|
| `RequestHandlerExtra` (flat, all fields) | `ServerContext` (server handlers) or `ClientContext` (client handlers) |
| `extra` parameter name | `ctx` parameter name |
| `extra.signal` | `ctx.mcpReq.signal` |
| `extra.requestId` | `ctx.mcpReq.id` |
| `extra._meta` | `ctx.mcpReq._meta` |
| `extra.sendRequest(...)` | `ctx.mcpReq.send(...)` |
| `extra.sendNotification(...)` | `ctx.mcpReq.notify(...)` |
| `extra.authInfo` | `ctx.http?.authInfo` |
| `extra.requestInfo` | `ctx.http?.req` (only on `ServerContext`) |
| `extra.closeSSEStream` | `ctx.http?.closeSSE` (only on `ServerContext`) |
| `extra.closeStandaloneSSEStream` | `ctx.http?.closeStandaloneSSE` (only on `ServerContext`) |
| `extra.sessionId` | `ctx.sessionId` |
| `extra.taskStore` | `ctx.task?.store` |
| `extra.taskId` | `ctx.task?.id` |
| `extra.taskRequestedTtl` | `ctx.task?.requestedTtl` |
**Before (v1):**
```typescript
server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
const headers = extra.requestInfo?.headers;
const taskStore = extra.taskStore;
await extra.sendNotification({ method: 'notifications/progress', params: { progressToken: 'abc', progress: 50, total: 100 } });
return { content: [{ type: 'text', text: 'result' }] };
});
```
**After (v2):**
```typescript
server.setRequestHandler('tools/call', async (request, ctx) => {
const headers = ctx.http?.req?.headers;
const taskStore = ctx.task?.store;
await ctx.mcpReq.notify({ method: 'notifications/progress', params: { progressToken: 'abc', progress: 50, total: 100 } });
return { content: [{ type: 'text', text: 'result' }] };
});
```
Context fields are organized into 4 groups:
- **`mcpReq`** — request-level concerns: `id`, `method`, `_meta`, `signal`, `send()`, `notify()`, plus server-only `log()`, `elicitInput()`, and `requestSampling()`
- **`http?`** — HTTP transport concerns (undefined for stdio): `authInfo`, plus server-only `req`, `closeSSE`, `closeStandaloneSSE`
- **`task?`** — task lifecycle: `id`, `store`, `requestedTtl`
`BaseContext` is the common base type shared by both `ServerContext` and `ClientContext`. `ServerContext` extends each group with server-specific additions via type intersection.
`ServerContext` also provides convenience methods for common server→client operations:
```typescript
server.setRequestHandler('tools/call', async (request, ctx) => {
// Send a log message (respects client's log level filter)
await ctx.mcpReq.log('info', 'Processing tool call', 'my-logger');
// Request client to sample an LLM
const samplingResult = await ctx.mcpReq.requestSampling({
messages: [{ role: 'user', content: { type: 'text', text: 'Hello' } }],
maxTokens: 100,
});
// Elicit user input via a form
const elicitResult = await ctx.mcpReq.elicitInput({
message: 'Please provide details',
requestedSchema: { type: 'object', properties: { name: { type: 'string' } } },
});
return { content: [{ type: 'text', text: 'done' }] };
});
```
These replace the pattern of calling `server.sendLoggingMessage()`, `server.createMessage()`, and `server.elicitInput()` from within handlers.
### Error hierarchy refactoring
The SDK now distinguishes between two types of errors:

View file

@ -46,12 +46,12 @@ const getServer = () => {
cartId: z.string().describe('The ID of the cart to confirm')
})
},
async ({ cartId }, extra): Promise<CallToolResult> => {
async ({ cartId }, ctx): Promise<CallToolResult> => {
/*
In a real world scenario, there would be some logic here to check if the user has the provided cartId.
For the purposes of this example, we'll throw an error (-> elicits the client to open a URL to confirm payment)
*/
const sessionId = extra.sessionId;
const sessionId = ctx.sessionId;
if (!sessionId) {
throw new Error('Expected a Session ID');
}
@ -79,15 +79,15 @@ const getServer = () => {
param1: z.string().describe('First parameter')
})
},
async (_, extra): Promise<CallToolResult> => {
async (_, ctx): Promise<CallToolResult> => {
/*
In a real world scenario, there would be some logic here to check if we already have a valid access token for the user.
Auth info (with a subject or `sub` claim) can be typically be found in `extra.authInfo`.
Auth info (with a subject or `sub` claim) can be typically be found in `ctx.http?.authInfo`.
If we do, we can just return the result of the tool call.
If we don't, we can throw an ElicitationRequiredError to request the user to authenticate.
For the purposes of this example, we'll throw an error (-> elicits the client to open a URL to authenticate).
*/
const sessionId = extra.sessionId;
const sessionId = ctx.sessionId;
if (!sessionId) {
throw new Error('Expected a Session ID');
}

View file

@ -51,36 +51,18 @@ const getServer = () => {
name: z.string().describe('Name to greet')
})
},
async ({ name }, extra): Promise<CallToolResult> => {
async ({ name }, ctx): Promise<CallToolResult> => {
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
await server.sendLoggingMessage(
{
level: 'debug',
data: `Starting multi-greet for ${name}`
},
extra.sessionId
);
await ctx.mcpReq.log('debug', `Starting multi-greet for ${name}`);
await sleep(1000); // Wait 1 second before first greeting
await server.sendLoggingMessage(
{
level: 'info',
data: `Sending first greeting to ${name}`
},
extra.sessionId
);
await ctx.mcpReq.log('info', `Sending first greeting to ${name}`);
await sleep(1000); // Wait another second before second greeting
await server.sendLoggingMessage(
{
level: 'info',
data: `Sending second greeting to ${name}`
},
extra.sessionId
);
await ctx.mcpReq.log('info', `Sending second greeting to ${name}`);
return {
content: [

View file

@ -49,20 +49,14 @@ const getServer = () => {
count: z.number().describe('Number of notifications to send (0 for 100)').default(10)
})
},
async ({ interval, count }, extra): Promise<CallToolResult> => {
async ({ interval, count }, ctx): Promise<CallToolResult> => {
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
let counter = 0;
while (count === 0 || counter < count) {
counter++;
try {
await server.sendLoggingMessage(
{
level: 'info',
data: `Periodic notification #${counter} at ${new Date().toISOString()}`
},
extra.sessionId
);
await ctx.mcpReq.log('info', `Periodic notification #${counter} at ${new Date().toISOString()}`);
} catch (error) {
console.error('Error sending notification:', error);
}

View file

@ -88,36 +88,18 @@ const getServer = () => {
openWorldHint: false
}
},
async ({ name }, extra): Promise<CallToolResult> => {
async ({ name }, ctx): Promise<CallToolResult> => {
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
await server.sendLoggingMessage(
{
level: 'debug',
data: `Starting multi-greet for ${name}`
},
extra.sessionId
);
await ctx.mcpReq.log('debug', `Starting multi-greet for ${name}`);
await sleep(1000); // Wait 1 second before first greeting
await server.sendLoggingMessage(
{
level: 'info',
data: `Sending first greeting to ${name}`
},
extra.sessionId
);
await ctx.mcpReq.log('info', `Sending first greeting to ${name}`);
await sleep(1000); // Wait another second before second greeting
await server.sendLoggingMessage(
{
level: 'info',
data: `Sending second greeting to ${name}`
},
extra.sessionId
);
await ctx.mcpReq.log('info', `Sending second greeting to ${name}`);
return {
content: [
@ -139,7 +121,7 @@ const getServer = () => {
infoType: z.enum(['contact', 'preferences', 'feedback']).describe('Type of information to collect')
})
},
async ({ infoType }, extra): Promise<CallToolResult> => {
async ({ infoType }, ctx): Promise<CallToolResult> => {
let message: string;
let requestedSchema: {
type: 'object';
@ -238,8 +220,8 @@ const getServer = () => {
}
try {
// Use sendRequest through the extra parameter to elicit input
const result = await extra.sendRequest(
// Use sendRequest through the ctx parameter to elicit input
const result = await ctx.mcpReq.send(
{
method: 'elicitation/create',
params: {
@ -327,20 +309,14 @@ const getServer = () => {
count: z.number().describe('Number of notifications to send (0 for 100)').default(50)
})
},
async ({ interval, count }, extra): Promise<CallToolResult> => {
async ({ interval, count }, ctx): Promise<CallToolResult> => {
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
let counter = 0;
while (count === 0 || counter < count) {
counter++;
try {
await server.sendLoggingMessage(
{
level: 'info',
data: `Periodic notification #${counter} at ${new Date().toISOString()}`
},
extra.sessionId
);
await ctx.mcpReq.log('info', `Periodic notification #${counter} at ${new Date().toISOString()}`);
} catch (error) {
console.error('Error sending notification:', error);
}
@ -484,16 +460,16 @@ const getServer = () => {
})
},
{
async createTask({ duration }, { taskStore, taskRequestedTtl }) {
async createTask({ duration }, ctx) {
// Create the task
const task = await taskStore.createTask({
ttl: taskRequestedTtl
const task = await ctx.task.store.createTask({
ttl: ctx.task.requestedTtl
});
// Simulate out-of-band work
(async () => {
await new Promise(resolve => setTimeout(resolve, duration));
await taskStore.storeTaskResult(task.taskId, 'completed', {
await ctx.task.store.storeTaskResult(task.taskId, 'completed', {
content: [
{
type: 'text',
@ -508,11 +484,11 @@ const getServer = () => {
task
};
},
async getTask(_args, { taskId, taskStore }) {
return await taskStore.getTask(taskId);
async getTask(_args, ctx) {
return await ctx.task.store.getTask(ctx.task.id);
},
async getTaskResult(_args, { taskId, taskStore }) {
const result = await taskStore.getTaskResult(taskId);
async getTaskResult(_args, ctx) {
const result = await ctx.task.store.getTaskResult(ctx.task.id);
return result as CallToolResult;
}
}

View file

@ -507,7 +507,7 @@ const createServer = (): Server => {
});
// Handle tool calls
server.setRequestHandler('tools/call', async (request, extra): Promise<CallToolResult | CreateTaskResult> => {
server.setRequestHandler('tools/call', async (request, ctx): Promise<CallToolResult | CreateTaskResult> => {
const { name, arguments: args } = request.params;
const taskParams = (request.params._meta?.task || request.params.task) as { ttl?: number; pollInterval?: number } | undefined;
@ -522,7 +522,7 @@ const createServer = (): Server => {
pollInterval: taskParams.pollInterval ?? 1000
};
const task = await taskStore.createTask(taskOptions, extra.requestId, request, extra.sessionId);
const task = await taskStore.createTask(taskOptions, ctx.mcpReq.id, request, ctx.sessionId);
console.log(`\n[Server] ${name} called, task created: ${task.taskId}`);
@ -600,7 +600,7 @@ const createServer = (): Server => {
activeTaskExecutions.set(task.taskId, {
promise: taskExecution,
server,
sessionId: extra.sessionId ?? ''
sessionId: ctx.sessionId ?? ''
});
return { task };
@ -617,10 +617,10 @@ const createServer = (): Server => {
});
// Handle tasks/result
server.setRequestHandler('tasks/result', async (request, extra): Promise<GetTaskPayloadResult> => {
server.setRequestHandler('tasks/result', async (request, ctx): Promise<GetTaskPayloadResult> => {
const { taskId } = request.params;
console.log(`[Server] tasks/result called for task ${taskId}`);
return taskResultHandler.handle(taskId, server, extra.sessionId ?? '');
return taskResultHandler.handle(taskId, server, ctx.sessionId ?? '');
});
return server;

View file

@ -7,7 +7,7 @@
* Key features:
* - Configures `retryInterval` to tell clients how long to wait before reconnecting
* - Uses `eventStore` to persist events for replay after reconnection
* - Uses `extra.closeSSEStream()` callback to gracefully disconnect clients mid-operation
* - Uses `ctx.http?.closeSSE()` callback to gracefully disconnect clients mid-operation
*
* Run with: pnpm tsx src/ssePollingExample.ts
* Test with: curl or the MCP Inspector
@ -40,60 +40,36 @@ server.registerTool(
{
description: 'A long-running task that sends progress updates. Server will disconnect mid-task to demonstrate polling.'
},
async (extra): Promise<CallToolResult> => {
async (ctx): Promise<CallToolResult> => {
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
console.log(`[${extra.sessionId}] Starting long-task...`);
console.log(`[${ctx.sessionId}] Starting long-task...`);
// Send first progress notification
await server.sendLoggingMessage(
{
level: 'info',
data: 'Progress: 25% - Starting work...'
},
extra.sessionId
);
await ctx.mcpReq.log('info', 'Progress: 25% - Starting work...');
await sleep(1000);
// Send second progress notification
await server.sendLoggingMessage(
{
level: 'info',
data: 'Progress: 50% - Halfway there...'
},
extra.sessionId
);
await ctx.mcpReq.log('info', 'Progress: 50% - Halfway there...');
await sleep(1000);
// Server decides to disconnect the client to free resources
// Client will reconnect via GET with Last-Event-ID after the transport's retryInterval
// Use extra.closeSSEStream callback - available when eventStore is configured
if (extra.closeSSEStream) {
console.log(`[${extra.sessionId}] Closing SSE stream to trigger client polling...`);
extra.closeSSEStream();
// Use ctx.http?.closeSSE callback - available when eventStore is configured
if (ctx.http?.closeSSE) {
console.log(`[${ctx.sessionId}] Closing SSE stream to trigger client polling...`);
ctx.http?.closeSSE();
}
// Continue processing while client is disconnected
// Events are stored in eventStore and will be replayed on reconnect
await sleep(500);
await server.sendLoggingMessage(
{
level: 'info',
data: 'Progress: 75% - Almost done (sent while client disconnected)...'
},
extra.sessionId
);
await ctx.mcpReq.log('info', 'Progress: 75% - Almost done (sent while client disconnected)...');
await sleep(500);
await server.sendLoggingMessage(
{
level: 'info',
data: 'Progress: 100% - Complete!'
},
extra.sessionId
);
await ctx.mcpReq.log('info', 'Progress: 100% - Complete!');
console.log(`[${extra.sessionId}] Task complete`);
console.log(`[${ctx.sessionId}] Task complete`);
return {
content: [

View file

@ -1,7 +1,9 @@
import { DefaultJsonSchemaValidator } from '@modelcontextprotocol/client/_shims';
import type {
BaseContext,
CallToolRequest,
ClientCapabilities,
ClientContext,
ClientNotification,
ClientRequest,
ClientResult,
@ -19,12 +21,12 @@ import type {
ListResourceTemplatesRequest,
ListToolsRequest,
LoggingLevel,
MessageExtraInfo,
Notification,
NotificationMethod,
ProtocolOptions,
ReadResourceRequest,
Request,
RequestHandlerExtra,
RequestMethod,
RequestOptions,
RequestTypeMap,
@ -215,7 +217,12 @@ export class Client<
RequestT extends Request = Request,
NotificationT extends Notification = Notification,
ResultT extends Result = Result
> extends Protocol<ClientRequest | RequestT, ClientNotification | NotificationT, ClientResult | ResultT> {
> extends Protocol<
ClientRequest | RequestT,
ClientNotification | NotificationT,
ClientResult | ResultT,
ClientContext<ClientRequest | RequestT, ClientNotification | NotificationT>
> {
private _serverCapabilities?: ServerCapabilities;
private _serverVersion?: Implementation;
private _capabilities: ClientCapabilities;
@ -247,6 +254,13 @@ export class Client<
}
}
protected override buildContext(
ctx: BaseContext<ClientRequest | RequestT, ClientNotification | NotificationT>,
_transportInfo?: MessageExtraInfo
): ClientContext<ClientRequest | RequestT, ClientNotification | NotificationT> {
return ctx;
}
/**
* Set up handlers for list changed notifications based on config and server capabilities.
* This should only be called after initialization when server capabilities are known.
@ -312,13 +326,13 @@ export class Client<
method: M,
handler: (
request: RequestTypeMap[M],
extra: RequestHandlerExtra<ClientRequest | RequestT, ClientNotification | NotificationT>
ctx: ClientContext<ClientRequest | RequestT, ClientNotification | NotificationT>
) => ClientResult | ResultT | Promise<ClientResult | ResultT>
): void {
if (method === 'elicitation/create') {
const wrappedHandler = async (
request: RequestTypeMap[M],
extra: RequestHandlerExtra<ClientRequest | RequestT, ClientNotification | NotificationT>
ctx: ClientContext<ClientRequest | RequestT, ClientNotification | NotificationT>
): Promise<ClientResult | ResultT> => {
const validatedRequest = parseSchema(ElicitRequestSchema, request);
if (!validatedRequest.success) {
@ -340,7 +354,7 @@ export class Client<
throw new ProtocolError(ProtocolErrorCode.InvalidParams, 'Client does not support URL-mode elicitation requests');
}
const result = await Promise.resolve(handler(request, extra));
const result = await Promise.resolve(handler(request, ctx));
// When task creation is requested, validate and return CreateTaskResult
if (params.task) {
@ -385,13 +399,13 @@ export class Client<
};
// Install the wrapped handler
return super.setRequestHandler(method, wrappedHandler as unknown as typeof handler);
return super.setRequestHandler(method, wrappedHandler);
}
if (method === 'sampling/createMessage') {
const wrappedHandler = async (
request: RequestTypeMap[M],
extra: RequestHandlerExtra<ClientRequest | RequestT, ClientNotification | NotificationT>
ctx: ClientContext<ClientRequest | RequestT, ClientNotification | NotificationT>
): Promise<ClientResult | ResultT> => {
const validatedRequest = parseSchema(CreateMessageRequestSchema, request);
if (!validatedRequest.success) {
@ -402,7 +416,7 @@ export class Client<
const { params } = validatedRequest.data;
const result = await Promise.resolve(handler(request, extra));
const result = await Promise.resolve(handler(request, ctx));
// When task creation is requested, validate and return CreateTaskResult
if (params.task) {
@ -431,7 +445,7 @@ export class Client<
};
// Install the wrapped handler
return super.setRequestHandler(method, wrappedHandler as unknown as typeof handler);
return super.setRequestHandler(method, wrappedHandler);
}
// Other handlers use default behavior

View file

@ -3,7 +3,7 @@
* WARNING: These APIs are experimental and may change without notice.
*/
import type { RequestHandlerExtra, RequestTaskStore } from '../../shared/protocol.js';
import type { RequestTaskStore, ServerContext } from '../../shared/protocol.js';
import type {
JSONRPCErrorResponse,
JSONRPCNotification,
@ -12,8 +12,6 @@ import type {
Request,
RequestId,
Result,
ServerNotification,
ServerRequest,
Task,
ToolExecution
} from '../../types/types.js';
@ -23,21 +21,20 @@ import type {
// ============================================================================
/**
* Extended handler extra with task store for task creation.
* Server context with guaranteed task store for task creation.
* @experimental
*/
export interface CreateTaskRequestHandlerExtra extends RequestHandlerExtra<ServerRequest, ServerNotification> {
taskStore: RequestTaskStore;
}
export type CreateTaskServerContext = ServerContext & {
task: { store: RequestTaskStore; requestedTtl?: number | null };
};
/**
* Extended handler extra with task ID and store for task operations.
* Server context with guaranteed task ID and store for task operations.
* @experimental
*/
export interface TaskRequestHandlerExtra extends RequestHandlerExtra<ServerRequest, ServerNotification> {
taskId: string;
taskStore: RequestTaskStore;
}
export type TaskServerContext = ServerContext & {
task: { id: string; store: RequestTaskStore; requestedTtl?: number | null };
};
/**
* Task-specific execution configuration.

View file

@ -5,6 +5,14 @@ import type {
AuthInfo,
CancelledNotification,
ClientCapabilities,
ClientNotification,
ClientRequest,
CreateMessageRequest,
CreateMessageResult,
CreateMessageResultWithTools,
ElicitRequestFormParams,
ElicitRequestURLParams,
ElicitResult,
GetTaskPayloadRequest,
GetTaskRequest,
GetTaskResult,
@ -13,6 +21,7 @@ import type {
JSONRPCRequest,
JSONRPCResponse,
JSONRPCResultResponse,
LoggingLevel,
MessageExtraInfo,
Notification,
NotificationMethod,
@ -28,6 +37,8 @@ import type {
RequestTypeMap,
Result,
ServerCapabilities,
ServerNotification,
ServerRequest,
Task,
TaskCreationParams,
TaskStatusNotification
@ -244,75 +255,134 @@ export interface RequestTaskStore {
}
/**
* Extra data given to request handlers.
* Task context provided to request handlers when task storage is configured.
*/
export type RequestHandlerExtra<SendRequestT extends Request, SendNotificationT extends Notification> = {
/**
* An abort signal used to communicate if the request was cancelled from the sender's side.
*/
signal: AbortSignal;
/**
* Information about a validated access token, provided to request handlers.
*/
authInfo?: AuthInfo;
export type TaskContext = {
id?: string;
store: RequestTaskStore;
requestedTtl?: number | null;
};
/**
* Base context provided to all request handlers.
*/
export type BaseContext<SendRequestT extends Request, SendNotificationT extends Notification> = {
/**
* The session ID from the transport, if available.
*/
sessionId?: string;
/**
* Metadata from the original request.
* Information about the MCP request being handled.
*/
_meta?: RequestMeta;
mcpReq: {
/**
* The JSON-RPC ID of the request being handled.
*/
id: RequestId;
/**
* The method name of the request (e.g., 'tools/call', 'ping').
*/
method: string;
/**
* Metadata from the original request.
*/
_meta?: RequestMeta;
/**
* An abort signal used to communicate if the request was cancelled from the sender's side.
*/
signal: AbortSignal;
/**
* Sends a request that relates to the current request being handled.
*
* This is used by certain transports to correctly associate related messages.
*/
send: <U extends AnySchema>(request: SendRequestT, resultSchema: U, options?: TaskRequestOptions) => Promise<SchemaOutput<U>>;
/**
* Sends a notification that relates to the current request being handled.
*
* This is used by certain transports to correctly associate related messages.
*/
notify: (notification: SendNotificationT) => Promise<void>;
};
/**
* The JSON-RPC ID of the request being handled.
* This can be useful for tracking or logging purposes.
* HTTP transport information, only available when using an HTTP-based transport.
*/
requestId: RequestId;
taskId?: string;
taskStore?: RequestTaskStore;
taskRequestedTtl?: number | null;
http?: {
/**
* Information about a validated access token, provided to request handlers.
*/
authInfo?: AuthInfo;
};
/**
* The original HTTP request.
* Task context, available when task storage is configured.
*/
requestInfo?: RequestInfo;
/**
* Sends a notification that relates to the current request being handled.
*
* This is used by certain transports to correctly associate related messages.
*/
sendNotification: (notification: SendNotificationT) => Promise<void>;
/**
* Sends a request that relates to the current request being handled.
*
* This is used by certain transports to correctly associate related messages.
*/
sendRequest: <U extends AnySchema>(request: SendRequestT, resultSchema: U, options?: TaskRequestOptions) => Promise<SchemaOutput<U>>;
/**
* Closes the SSE stream for this request, triggering client reconnection.
* Only available when using a StreamableHTTPServerTransport with eventStore configured.
* Use this to implement polling behavior during long-running operations.
*/
closeSSEStream?: () => void;
/**
* Closes the standalone GET SSE stream, triggering client reconnection.
* Only available when using a StreamableHTTPServerTransport with eventStore configured.
* Use this to implement polling behavior for server-initiated notifications.
*/
closeStandaloneSSEStream?: () => void;
task?: TaskContext;
};
/**
* Context provided to server-side request handlers, extending BaseContext with server-specific fields.
*/
export type ServerContext<
SendRequestT extends Request = ServerRequest,
SendNotificationT extends Notification = ServerNotification
> = BaseContext<SendRequestT, SendNotificationT> & {
mcpReq: {
/**
* Send a log message notification to the client.
* Respects the client's log level filter set via logging/setLevel.
*/
log: (level: LoggingLevel, data: unknown, logger?: string) => Promise<void>;
/**
* Send an elicitation request to the client, requesting user input.
*/
elicitInput: (params: ElicitRequestFormParams | ElicitRequestURLParams, options?: RequestOptions) => Promise<ElicitResult>;
/**
* Request LLM sampling from the client.
*/
requestSampling: (
params: CreateMessageRequest['params'],
options?: RequestOptions
) => Promise<CreateMessageResult | CreateMessageResultWithTools>;
};
http?: {
/**
* The original HTTP request information.
*/
req?: RequestInfo;
/**
* Closes the SSE stream for this request, triggering client reconnection.
* Only available when using a StreamableHTTPServerTransport with eventStore configured.
*/
closeSSE?: () => void;
/**
* Closes the standalone GET SSE stream, triggering client reconnection.
* Only available when using a StreamableHTTPServerTransport with eventStore configured.
*/
closeStandaloneSSE?: () => void;
};
};
/**
* Context provided to client-side request handlers.
*/
export type ClientContext<
SendRequestT extends Request = ClientRequest,
SendNotificationT extends Notification = ClientNotification
> = BaseContext<SendRequestT, SendNotificationT>;
/**
* Information about a request's timeout state
*/
@ -329,13 +399,15 @@ type TimeoutInfo = {
* Implements MCP protocol framing on top of a pluggable transport, including
* features like request/response linking, notifications, and progress.
*/
export abstract class Protocol<SendRequestT extends Request, SendNotificationT extends Notification, SendResultT extends Result> {
export abstract class Protocol<
SendRequestT extends Request,
SendNotificationT extends Notification,
SendResultT extends Result,
ContextT extends BaseContext<SendRequestT, SendNotificationT>
> {
private _transport?: Transport;
private _requestMessageId = 0;
private _requestHandlers: Map<
string,
(request: JSONRPCRequest, extra: RequestHandlerExtra<SendRequestT, SendNotificationT>) => Promise<SendResultT>
> = new Map();
private _requestHandlers: Map<string, (request: JSONRPCRequest, ctx: ContextT) => Promise<SendResultT>> = new Map();
private _requestHandlerAbortControllers: Map<RequestId, AbortController> = new Map();
private _notificationHandlers: Map<string, (notification: JSONRPCNotification) => Promise<void>> = new Map();
private _responseHandlers: Map<number, (response: JSONRPCResultResponse | Error) => void> = new Map();
@ -370,7 +442,7 @@ export abstract class Protocol<SendRequestT extends Request, SendNotificationT e
/**
* A handler to invoke for any request types that do not have their own handler installed.
*/
fallbackRequestHandler?: (request: JSONRPCRequest, extra: RequestHandlerExtra<SendRequestT, SendNotificationT>) => Promise<SendResultT>;
fallbackRequestHandler?: (request: JSONRPCRequest, ctx: ContextT) => Promise<SendResultT>;
/**
* A handler to invoke for any notification types that do not have their own handler installed.
@ -398,8 +470,8 @@ export abstract class Protocol<SendRequestT extends Request, SendNotificationT e
this._taskStore = _options?.taskStore;
this._taskMessageQueue = _options?.taskMessageQueue;
if (this._taskStore) {
this.setRequestHandler('tasks/get', async (request, extra) => {
const task = await this._taskStore!.getTask(request.params.taskId, extra.sessionId);
this.setRequestHandler('tasks/get', async (request, ctx) => {
const task = await this._taskStore!.getTask(request.params.taskId, ctx.sessionId);
if (!task) {
throw new ProtocolError(ProtocolErrorCode.InvalidParams, 'Failed to retrieve task: Task not found');
}
@ -411,14 +483,14 @@ export abstract class Protocol<SendRequestT extends Request, SendNotificationT e
} as unknown as SendResultT;
});
this.setRequestHandler('tasks/result', async (request, extra) => {
this.setRequestHandler('tasks/result', async (request, ctx) => {
const handleTaskResult = async (): Promise<SendResultT> => {
const taskId = request.params.taskId;
// Deliver queued messages
if (this._taskMessageQueue) {
let queuedMessage: QueuedMessage | undefined;
while ((queuedMessage = await this._taskMessageQueue.dequeue(taskId, extra.sessionId))) {
while ((queuedMessage = await this._taskMessageQueue.dequeue(taskId, ctx.sessionId))) {
// Handle response and error messages by routing them to the appropriate resolver
if (queuedMessage.type === 'response' || queuedMessage.type === 'error') {
const message = queuedMessage.message;
@ -456,12 +528,12 @@ export abstract class Protocol<SendRequestT extends Request, SendNotificationT e
// Send the message on the response stream by passing the relatedRequestId
// This tells the transport to write the message to the tasks/result response stream
await this._transport?.send(queuedMessage.message, { relatedRequestId: extra.requestId });
await this._transport?.send(queuedMessage.message, { relatedRequestId: ctx.mcpReq.id });
}
}
// Now check task status
const task = await this._taskStore!.getTask(taskId, extra.sessionId);
const task = await this._taskStore!.getTask(taskId, ctx.sessionId);
if (!task) {
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Task not found: ${taskId}`);
}
@ -469,7 +541,7 @@ export abstract class Protocol<SendRequestT extends Request, SendNotificationT e
// Block if task is not terminal (we've already delivered all queued messages above)
if (!isTerminal(task.status)) {
// Wait for status change or new messages
await this._waitForTaskUpdate(taskId, extra.signal);
await this._waitForTaskUpdate(taskId, ctx.mcpReq.signal);
// After waking up, recursively call to deliver any new messages or result
return await handleTaskResult();
@ -477,7 +549,7 @@ export abstract class Protocol<SendRequestT extends Request, SendNotificationT e
// If task is terminal, return the result
if (isTerminal(task.status)) {
const result = await this._taskStore!.getTaskResult(taskId, extra.sessionId);
const result = await this._taskStore!.getTaskResult(taskId, ctx.sessionId);
this._clearTaskQueue(taskId);
@ -498,9 +570,9 @@ export abstract class Protocol<SendRequestT extends Request, SendNotificationT e
return await handleTaskResult();
});
this.setRequestHandler('tasks/list', async (request, extra) => {
this.setRequestHandler('tasks/list', async (request, ctx) => {
try {
const { tasks, nextCursor } = await this._taskStore!.listTasks(request.params?.cursor, extra.sessionId);
const { tasks, nextCursor } = await this._taskStore!.listTasks(request.params?.cursor, ctx.sessionId);
return {
tasks,
nextCursor,
@ -514,10 +586,10 @@ export abstract class Protocol<SendRequestT extends Request, SendNotificationT e
}
});
this.setRequestHandler('tasks/cancel', async (request, extra) => {
this.setRequestHandler('tasks/cancel', async (request, ctx) => {
try {
// Get the current task to check if it's in a terminal state, in case the implementation is not atomic
const task = await this._taskStore!.getTask(request.params.taskId, extra.sessionId);
const task = await this._taskStore!.getTask(request.params.taskId, ctx.sessionId);
if (!task) {
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Task not found: ${request.params.taskId}`);
@ -532,12 +604,12 @@ export abstract class Protocol<SendRequestT extends Request, SendNotificationT e
request.params.taskId,
'cancelled',
'Client cancelled task execution.',
extra.sessionId
ctx.sessionId
);
this._clearTaskQueue(request.params.taskId);
const cancelledTask = await this._taskStore!.getTask(request.params.taskId, extra.sessionId);
const cancelledTask = await this._taskStore!.getTask(request.params.taskId, ctx.sessionId);
if (!cancelledTask) {
// Task was deleted during cancellation (e.g., cleanup happened)
throw new ProtocolError(
@ -564,6 +636,12 @@ export abstract class Protocol<SendRequestT extends Request, SendNotificationT e
}
}
/**
* Builds the context object for request handlers. Subclasses must override
* to return the appropriate context type (e.g., ServerContext adds requestInfo).
*/
protected abstract buildContext(ctx: BaseContext<SendRequestT, SendNotificationT>, transportInfo?: MessageExtraInfo): ContextT;
private async _oncancel(notification: CancelledNotification): Promise<void> {
if (!notification.params.requestId) {
return;
@ -734,43 +812,40 @@ export abstract class Protocol<SendRequestT extends Request, SendNotificationT e
const taskCreationParams = isTaskAugmentedRequestParams(request.params) ? request.params.task : undefined;
const taskStore = this._taskStore ? this.requestTaskStore(request, capturedTransport?.sessionId) : undefined;
const fullExtra: RequestHandlerExtra<SendRequestT, SendNotificationT> = {
signal: abortController.signal,
const task: TaskContext | undefined = taskStore
? { id: relatedTaskId, store: taskStore, requestedTtl: taskCreationParams?.ttl }
: undefined;
const baseCtx: BaseContext<SendRequestT, SendNotificationT> = {
sessionId: capturedTransport?.sessionId,
_meta: request.params?._meta,
sendNotification: async notification => {
// Include related-task metadata if this request is part of a task
const notificationOptions: NotificationOptions = { relatedRequestId: request.id };
if (relatedTaskId) {
notificationOptions.relatedTask = { taskId: relatedTaskId };
mcpReq: {
id: request.id,
method: request.method,
_meta: request.params?._meta,
signal: abortController.signal,
send: async (r, resultSchema, options?) => {
const requestOptions: RequestOptions = { ...options, relatedRequestId: request.id };
if (relatedTaskId && !requestOptions.relatedTask) {
requestOptions.relatedTask = { taskId: relatedTaskId };
}
const effectiveTaskId = requestOptions.relatedTask?.taskId ?? relatedTaskId;
if (effectiveTaskId && taskStore) {
await taskStore.updateTaskStatus(effectiveTaskId, 'input_required');
}
return await this.request(r, resultSchema, requestOptions);
},
notify: async notification => {
const notificationOptions: NotificationOptions = { relatedRequestId: request.id };
if (relatedTaskId) {
notificationOptions.relatedTask = { taskId: relatedTaskId };
}
await this.notification(notification, notificationOptions);
}
await this.notification(notification, notificationOptions);
},
sendRequest: async (r, resultSchema, options?) => {
// Include related-task metadata if this request is part of a task
const requestOptions: RequestOptions = { ...options, relatedRequestId: request.id };
if (relatedTaskId && !requestOptions.relatedTask) {
requestOptions.relatedTask = { taskId: relatedTaskId };
}
// Set task status to input_required when sending a request within a task context
// Use the taskId from options (explicit) or fall back to relatedTaskId (inherited)
const effectiveTaskId = requestOptions.relatedTask?.taskId ?? relatedTaskId;
if (effectiveTaskId && taskStore) {
await taskStore.updateTaskStatus(effectiveTaskId, 'input_required');
}
return await this.request(r, resultSchema, requestOptions);
},
authInfo: extra?.authInfo,
requestId: request.id,
requestInfo: extra?.requestInfo,
taskId: relatedTaskId,
taskStore: taskStore,
taskRequestedTtl: taskCreationParams?.ttl,
closeSSEStream: extra?.closeSSEStream,
closeStandaloneSSEStream: extra?.closeStandaloneSSEStream
http: extra?.authInfo ? { authInfo: extra.authInfo } : undefined,
task
};
const ctx = this.buildContext(baseCtx, extra);
// Starting with Promise.resolve() puts any synchronous errors into the monad as well.
Promise.resolve()
@ -781,7 +856,7 @@ export abstract class Protocol<SendRequestT extends Request, SendNotificationT e
this.assertTaskHandlerCapability(request.method);
}
})
.then(() => handler(request, fullExtra))
.then(() => handler(request, ctx))
.then(
async result => {
if (abortController.signal.aborted) {
@ -1418,17 +1493,14 @@ export abstract class Protocol<SendRequestT extends Request, SendNotificationT e
*/
setRequestHandler<M extends RequestMethod>(
method: M,
handler: (
request: RequestTypeMap[M],
extra: RequestHandlerExtra<SendRequestT, SendNotificationT>
) => SendResultT | Promise<SendResultT>
handler: (request: RequestTypeMap[M], ctx: ContextT) => SendResultT | Promise<SendResultT>
): void {
this.assertRequestHandlerCapability(method);
const schema = getRequestSchema(method);
this._requestHandlers.set(method, (request, extra) => {
const parsed = schema.parse(request);
return Promise.resolve(handler(parsed, extra));
this._requestHandlers.set(method, (request, ctx) => {
const parsed = schema.parse(request) as RequestTypeMap[M];
return Promise.resolve(handler(parsed, ctx));
});
}

View file

@ -11,6 +11,7 @@ import type {
TaskStore
} from '../../src/experimental/tasks/interfaces.js';
import { InMemoryTaskMessageQueue } from '../../src/experimental/tasks/stores/inMemory.js';
import type { BaseContext } from '../../src/shared/protocol.js';
import { mergeCapabilities, Protocol } from '../../src/shared/protocol.js';
import type { ErrorMessage, ResponseMessage } from '../../src/shared/responseMessage.js';
import { toArrayAsync } from '../../src/shared/responseMessage.js';
@ -149,18 +150,21 @@ function assertQueuedRequest(o?: QueuedMessage): asserts o is QueuedRequest {
}
describe('protocol tests', () => {
let protocol: Protocol<Request, Notification, Result>;
let protocol: Protocol<Request, Notification, Result, BaseContext<Request, Notification>>;
let transport: MockTransport;
let sendSpy: MockInstance;
beforeEach(() => {
transport = new MockTransport();
sendSpy = vi.spyOn(transport, 'send');
protocol = new (class extends Protocol<Request, Notification, Result> {
protocol = new (class extends Protocol<Request, Notification, Result, BaseContext<Request, Notification>> {
protected assertCapabilityForMethod(): void {}
protected assertNotificationCapability(): void {}
protected assertRequestHandlerCapability(): void {}
protected assertTaskCapability(): void {}
protected buildContext(ctx: any) {
return ctx;
}
protected assertTaskHandlerCapability(): void {}
})();
});
@ -630,11 +634,14 @@ describe('protocol tests', () => {
it('should NOT debounce a notification that has parameters', async () => {
// ARRANGE
protocol = new (class extends Protocol<Request, Notification, Result> {
protocol = new (class extends Protocol<Request, Notification, Result, BaseContext<Request, Notification>> {
protected assertCapabilityForMethod(): void {}
protected assertNotificationCapability(): void {}
protected assertRequestHandlerCapability(): void {}
protected assertTaskCapability(): void {}
protected buildContext(ctx: any) {
return ctx;
}
protected assertTaskHandlerCapability(): void {}
})({ debouncedNotificationMethods: ['test/debounced_with_params'] });
await protocol.connect(transport);
@ -653,11 +660,14 @@ describe('protocol tests', () => {
it('should NOT debounce a notification that has a relatedRequestId', async () => {
// ARRANGE
protocol = new (class extends Protocol<Request, Notification, Result> {
protocol = new (class extends Protocol<Request, Notification, Result, BaseContext<Request, Notification>> {
protected assertCapabilityForMethod(): void {}
protected assertNotificationCapability(): void {}
protected assertRequestHandlerCapability(): void {}
protected assertTaskCapability(): void {}
protected buildContext(ctx: any) {
return ctx;
}
protected assertTaskHandlerCapability(): void {}
})({ debouncedNotificationMethods: ['test/debounced_with_options'] });
await protocol.connect(transport);
@ -674,11 +684,14 @@ describe('protocol tests', () => {
it('should clear pending debounced notifications on connection close', async () => {
// ARRANGE
protocol = new (class extends Protocol<Request, Notification, Result> {
protocol = new (class extends Protocol<Request, Notification, Result, BaseContext<Request, Notification>> {
protected assertCapabilityForMethod(): void {}
protected assertNotificationCapability(): void {}
protected assertRequestHandlerCapability(): void {}
protected assertTaskCapability(): void {}
protected buildContext(ctx: any) {
return ctx;
}
protected assertTaskHandlerCapability(): void {}
})({ debouncedNotificationMethods: ['test/debounced'] });
await protocol.connect(transport);
@ -700,11 +713,14 @@ describe('protocol tests', () => {
it('should debounce multiple synchronous calls when params property is omitted', async () => {
// ARRANGE
protocol = new (class extends Protocol<Request, Notification, Result> {
protocol = new (class extends Protocol<Request, Notification, Result, BaseContext<Request, Notification>> {
protected assertCapabilityForMethod(): void {}
protected assertNotificationCapability(): void {}
protected assertRequestHandlerCapability(): void {}
protected assertTaskCapability(): void {}
protected buildContext(ctx: any) {
return ctx;
}
protected assertTaskHandlerCapability(): void {}
})({ debouncedNotificationMethods: ['test/debounced'] });
await protocol.connect(transport);
@ -729,11 +745,14 @@ describe('protocol tests', () => {
it('should debounce calls when params is explicitly undefined', async () => {
// ARRANGE
protocol = new (class extends Protocol<Request, Notification, Result> {
protocol = new (class extends Protocol<Request, Notification, Result, BaseContext<Request, Notification>> {
protected assertCapabilityForMethod(): void {}
protected assertNotificationCapability(): void {}
protected assertRequestHandlerCapability(): void {}
protected assertTaskCapability(): void {}
protected buildContext(ctx: any) {
return ctx;
}
protected assertTaskHandlerCapability(): void {}
})({ debouncedNotificationMethods: ['test/debounced'] });
await protocol.connect(transport);
@ -756,11 +775,14 @@ describe('protocol tests', () => {
it('should send non-debounced notifications immediately and multiple times', async () => {
// ARRANGE
protocol = new (class extends Protocol<Request, Notification, Result> {
protocol = new (class extends Protocol<Request, Notification, Result, BaseContext<Request, Notification>> {
protected assertCapabilityForMethod(): void {}
protected assertNotificationCapability(): void {}
protected assertRequestHandlerCapability(): void {}
protected assertTaskCapability(): void {}
protected buildContext(ctx: any) {
return ctx;
}
protected assertTaskHandlerCapability(): void {}
})({ debouncedNotificationMethods: ['test/debounced'] }); // Configure for a different method
await protocol.connect(transport);
@ -791,11 +813,14 @@ describe('protocol tests', () => {
it('should handle sequential batches of debounced notifications correctly', async () => {
// ARRANGE
protocol = new (class extends Protocol<Request, Notification, Result> {
protocol = new (class extends Protocol<Request, Notification, Result, BaseContext<Request, Notification>> {
protected assertCapabilityForMethod(): void {}
protected assertNotificationCapability(): void {}
protected assertRequestHandlerCapability(): void {}
protected assertTaskCapability(): void {}
protected buildContext(ctx: any) {
return ctx;
}
protected assertTaskHandlerCapability(): void {}
})({ debouncedNotificationMethods: ['test/debounced'] });
await protocol.connect(transport);
@ -1003,18 +1028,21 @@ describe('mergeCapabilities', () => {
});
describe('Task-based execution', () => {
let protocol: Protocol<Request, Notification, Result>;
let protocol: Protocol<Request, Notification, Result, BaseContext<Request, Notification>>;
let transport: MockTransport;
let sendSpy: MockInstance;
beforeEach(() => {
transport = new MockTransport();
sendSpy = vi.spyOn(transport, 'send');
protocol = new (class extends Protocol<Request, Notification, Result> {
protocol = new (class extends Protocol<Request, Notification, Result, BaseContext<Request, Notification>> {
protected assertCapabilityForMethod(): void {}
protected assertNotificationCapability(): void {}
protected assertRequestHandlerCapability(): void {}
protected assertTaskCapability(): void {}
protected buildContext(ctx: any) {
return ctx;
}
protected assertTaskHandlerCapability(): void {}
})({ taskStore: createMockTaskStore(), taskMessageQueue: new InMemoryTaskMessageQueue() });
});
@ -1257,11 +1285,14 @@ describe('Task-based execution', () => {
// rather than in _meta, and that task management is handled by tool implementors
const mockTaskStore = createMockTaskStore();
protocol = new (class extends Protocol<Request, Notification, Result> {
protocol = new (class extends Protocol<Request, Notification, Result, BaseContext<Request, Notification>> {
protected assertCapabilityForMethod(): void {}
protected assertNotificationCapability(): void {}
protected assertRequestHandlerCapability(): void {}
protected assertTaskCapability(): void {}
protected buildContext(ctx: any) {
return ctx;
}
protected assertTaskHandlerCapability(): void {}
})({ taskStore: mockTaskStore });
@ -1326,11 +1357,14 @@ describe('Task-based execution', () => {
}
);
protocol = new (class extends Protocol<Request, Notification, Result> {
protocol = new (class extends Protocol<Request, Notification, Result, BaseContext<Request, Notification>> {
protected assertCapabilityForMethod(): void {}
protected assertNotificationCapability(): void {}
protected assertRequestHandlerCapability(): void {}
protected assertTaskCapability(): void {}
protected buildContext(ctx: any) {
return ctx;
}
protected assertTaskHandlerCapability(): void {}
})({ taskStore: mockTaskStore });
@ -1387,11 +1421,14 @@ describe('Task-based execution', () => {
}
);
protocol = new (class extends Protocol<Request, Notification, Result> {
protocol = new (class extends Protocol<Request, Notification, Result, BaseContext<Request, Notification>> {
protected assertCapabilityForMethod(): void {}
protected assertNotificationCapability(): void {}
protected assertRequestHandlerCapability(): void {}
protected assertTaskCapability(): void {}
protected buildContext(ctx: any) {
return ctx;
}
protected assertTaskHandlerCapability(): void {}
})({ taskStore: mockTaskStore });
@ -1433,11 +1470,14 @@ describe('Task-based execution', () => {
onList: () => listedTasks.releaseLatch()
});
protocol = new (class extends Protocol<Request, Notification, Result> {
protocol = new (class extends Protocol<Request, Notification, Result, BaseContext<Request, Notification>> {
protected assertCapabilityForMethod(): void {}
protected assertNotificationCapability(): void {}
protected assertRequestHandlerCapability(): void {}
protected assertTaskCapability(): void {}
protected buildContext(ctx: any) {
return ctx;
}
protected assertTaskHandlerCapability(): void {}
})({ taskStore: mockTaskStore });
@ -1466,11 +1506,14 @@ describe('Task-based execution', () => {
const mockTaskStore = createMockTaskStore();
mockTaskStore.listTasks.mockRejectedValue(new Error('Invalid cursor: bad-cursor'));
protocol = new (class extends Protocol<Request, Notification, Result> {
protocol = new (class extends Protocol<Request, Notification, Result, BaseContext<Request, Notification>> {
protected assertCapabilityForMethod(): void {}
protected assertNotificationCapability(): void {}
protected assertRequestHandlerCapability(): void {}
protected assertTaskCapability(): void {}
protected buildContext(ctx: any) {
return ctx;
}
protected assertTaskHandlerCapability(): void {}
})({ taskStore: mockTaskStore });
@ -1600,11 +1643,14 @@ describe('Task-based execution', () => {
throw new Error('Task not found');
});
const serverProtocol = new (class extends Protocol<Request, Notification, Result> {
const serverProtocol = new (class extends Protocol<Request, Notification, Result, BaseContext<Request, Notification>> {
protected assertCapabilityForMethod(): void {}
protected assertNotificationCapability(): void {}
protected assertRequestHandlerCapability(): void {}
protected assertTaskCapability(): void {}
protected buildContext(ctx: any) {
return ctx;
}
protected assertTaskHandlerCapability(): void {}
})({ taskStore: mockTaskStore });
const serverTransport = new MockTransport();
@ -1642,11 +1688,14 @@ describe('Task-based execution', () => {
mockTaskStore.getTask.mockResolvedValue(null);
const serverProtocol = new (class extends Protocol<Request, Notification, Result> {
const serverProtocol = new (class extends Protocol<Request, Notification, Result, BaseContext<Request, Notification>> {
protected assertCapabilityForMethod(): void {}
protected assertNotificationCapability(): void {}
protected assertRequestHandlerCapability(): void {}
protected assertTaskCapability(): void {}
protected buildContext(ctx: any) {
return ctx;
}
protected assertTaskHandlerCapability(): void {}
})({ taskStore: mockTaskStore });
const serverTransport = new MockTransport();
@ -1690,11 +1739,14 @@ describe('Task-based execution', () => {
mockTaskStore.updateTaskStatus.mockClear();
mockTaskStore.getTask.mockResolvedValue(completedTask);
const serverProtocol = new (class extends Protocol<Request, Notification, Result> {
const serverProtocol = new (class extends Protocol<Request, Notification, Result, BaseContext<Request, Notification>> {
protected assertCapabilityForMethod(): void {}
protected assertNotificationCapability(): void {}
protected assertRequestHandlerCapability(): void {}
protected assertTaskCapability(): void {}
protected buildContext(ctx: any) {
return ctx;
}
protected assertTaskHandlerCapability(): void {}
})({ taskStore: mockTaskStore });
const serverTransport = new MockTransport();
@ -1772,11 +1824,14 @@ describe('Task-based execution', () => {
params: {}
});
const serverProtocol = new (class extends Protocol<Request, Notification, Result> {
const serverProtocol = new (class extends Protocol<Request, Notification, Result, BaseContext<Request, Notification>> {
protected assertCapabilityForMethod(): void {}
protected assertNotificationCapability(): void {}
protected assertRequestHandlerCapability(): void {}
protected assertTaskCapability(): void {}
protected buildContext(ctx: any) {
return ctx;
}
protected assertTaskHandlerCapability(): void {}
})({ taskStore: mockTaskStore });
const serverTransport = new MockTransport();
@ -1822,11 +1877,14 @@ describe('Task-based execution', () => {
params: {}
});
const serverProtocol = new (class extends Protocol<Request, Notification, Result> {
const serverProtocol = new (class extends Protocol<Request, Notification, Result, BaseContext<Request, Notification>> {
protected assertCapabilityForMethod(): void {}
protected assertNotificationCapability(): void {}
protected assertRequestHandlerCapability(): void {}
protected assertTaskCapability(): void {}
protected buildContext(ctx: any) {
return ctx;
}
protected assertTaskHandlerCapability(): void {}
})({ taskStore: mockTaskStore });
const serverTransport = new MockTransport();
@ -1871,11 +1929,14 @@ describe('Task-based execution', () => {
params: {}
});
const serverProtocol = new (class extends Protocol<Request, Notification, Result> {
const serverProtocol = new (class extends Protocol<Request, Notification, Result, BaseContext<Request, Notification>> {
protected assertCapabilityForMethod(): void {}
protected assertNotificationCapability(): void {}
protected assertRequestHandlerCapability(): void {}
protected assertTaskCapability(): void {}
protected buildContext(ctx: any) {
return ctx;
}
protected assertTaskHandlerCapability(): void {}
})({ taskStore: mockTaskStore });
const serverTransport = new MockTransport();
@ -1908,11 +1969,14 @@ describe('Task-based execution', () => {
params: {}
});
const serverProtocol = new (class extends Protocol<Request, Notification, Result> {
const serverProtocol = new (class extends Protocol<Request, Notification, Result, BaseContext<Request, Notification>> {
protected assertCapabilityForMethod(): void {}
protected assertNotificationCapability(): void {}
protected assertRequestHandlerCapability(): void {}
protected assertTaskCapability(): void {}
protected buildContext(ctx: any) {
return ctx;
}
protected assertTaskHandlerCapability(): void {}
})({ taskStore: mockTaskStore });
const serverTransport = new MockTransport();
@ -1953,11 +2017,14 @@ describe('Task-based execution', () => {
await mockTaskStore.storeTaskResult(task.taskId, 'completed', testResult);
const serverProtocol = new (class extends Protocol<Request, Notification, Result> {
const serverProtocol = new (class extends Protocol<Request, Notification, Result, BaseContext<Request, Notification>> {
protected assertCapabilityForMethod(): void {}
protected assertNotificationCapability(): void {}
protected assertRequestHandlerCapability(): void {}
protected assertTaskCapability(): void {}
protected buildContext(ctx: any) {
return ctx;
}
protected assertTaskHandlerCapability(): void {}
})({ taskStore: mockTaskStore });
const serverTransport = new MockTransport();
@ -1996,11 +2063,14 @@ describe('Task-based execution', () => {
it('should propagate related-task metadata to handler sendRequest and sendNotification', async () => {
const mockTaskStore = createMockTaskStore();
const serverProtocol = new (class extends Protocol<Request, Notification, Result> {
const serverProtocol = new (class extends Protocol<Request, Notification, Result, BaseContext<Request, Notification>> {
protected assertCapabilityForMethod(): void {}
protected assertNotificationCapability(): void {}
protected assertRequestHandlerCapability(): void {}
protected assertTaskCapability(): void {}
protected buildContext(ctx: any) {
return ctx;
}
protected assertTaskHandlerCapability(): void {}
})({ taskStore: mockTaskStore, taskMessageQueue: new InMemoryTaskMessageQueue() });
@ -2010,9 +2080,9 @@ describe('Task-based execution', () => {
await serverProtocol.connect(serverTransport);
// Set up a handler that uses sendRequest and sendNotification
serverProtocol.setRequestHandler('tools/call', async (_request, extra) => {
// Send a notification using the extra.sendNotification
await extra.sendNotification({
serverProtocol.setRequestHandler('tools/call', async (_request, ctx) => {
// Send a notification using the ctx.mcpReq.notify
await ctx.mcpReq.notify({
method: 'notifications/message',
params: { level: 'info', data: 'test' }
});
@ -2072,18 +2142,21 @@ describe('Task-based execution', () => {
});
describe('Request Cancellation vs Task Cancellation', () => {
let protocol: Protocol<Request, Notification, Result>;
let protocol: Protocol<Request, Notification, Result, BaseContext<Request, Notification>>;
let transport: MockTransport;
let taskStore: TaskStore;
beforeEach(() => {
transport = new MockTransport();
taskStore = createMockTaskStore();
protocol = new (class extends Protocol<Request, Notification, Result> {
protocol = new (class extends Protocol<Request, Notification, Result, BaseContext<Request, Notification>> {
protected assertCapabilityForMethod(): void {}
protected assertNotificationCapability(): void {}
protected assertRequestHandlerCapability(): void {}
protected assertTaskCapability(): void {}
protected buildContext(ctx: any) {
return ctx;
}
protected assertTaskHandlerCapability(): void {}
})({ taskStore });
});
@ -2094,10 +2167,10 @@ describe('Request Cancellation vs Task Cancellation', () => {
// Set up a request handler that checks if it was aborted
let wasAborted = false;
protocol.setRequestHandler('ping', async (_request, extra) => {
protocol.setRequestHandler('ping', async (_request, ctx) => {
// Simulate a long-running operation
await new Promise(resolve => setTimeout(resolve, 100));
wasAborted = extra.signal.aborted;
wasAborted = ctx.mcpReq.signal.aborted;
return {};
});
@ -2357,29 +2430,35 @@ describe('Request Cancellation vs Task Cancellation', () => {
});
describe('Progress notification support for tasks', () => {
let protocol: Protocol<Request, Notification, Result>;
let protocol: Protocol<Request, Notification, Result, BaseContext<Request, Notification>>;
let transport: MockTransport;
let sendSpy: MockInstance;
beforeEach(() => {
transport = new MockTransport();
sendSpy = vi.spyOn(transport, 'send');
protocol = new (class extends Protocol<Request, Notification, Result> {
protocol = new (class extends Protocol<Request, Notification, Result, BaseContext<Request, Notification>> {
protected assertCapabilityForMethod(): void {}
protected assertNotificationCapability(): void {}
protected assertRequestHandlerCapability(): void {}
protected assertTaskCapability(): void {}
protected buildContext(ctx: any) {
return ctx;
}
protected assertTaskHandlerCapability(): void {}
})();
});
it('should maintain progress token association after CreateTaskResult is returned', async () => {
const taskStore = createMockTaskStore();
const protocol = new (class extends Protocol<Request, Notification, Result> {
const protocol = new (class extends Protocol<Request, Notification, Result, BaseContext<Request, Notification>> {
protected assertCapabilityForMethod(): void {}
protected assertNotificationCapability(): void {}
protected assertRequestHandlerCapability(): void {}
protected assertTaskCapability(): void {}
protected buildContext(ctx: any) {
return ctx;
}
protected assertTaskHandlerCapability(): void {}
})({ taskStore });
@ -2468,11 +2547,14 @@ describe('Progress notification support for tasks', () => {
it('should stop progress notifications when task reaches terminal status (completed)', async () => {
const taskStore = createMockTaskStore();
const protocol = new (class extends Protocol<Request, Notification, Result> {
const protocol = new (class extends Protocol<Request, Notification, Result, BaseContext<Request, Notification>> {
protected assertCapabilityForMethod(): void {}
protected assertNotificationCapability(): void {}
protected assertRequestHandlerCapability(): void {}
protected assertTaskCapability(): void {}
protected buildContext(ctx: any) {
return ctx;
}
protected assertTaskHandlerCapability(): void {}
})({ taskStore });
@ -2481,13 +2563,14 @@ describe('Progress notification support for tasks', () => {
await protocol.connect(transport);
// Set up a request handler that will complete the task
protocol.setRequestHandler('tools/call', async (_request, extra) => {
if (extra.taskStore) {
const task = await extra.taskStore.createTask({ ttl: 60000 });
protocol.setRequestHandler('tools/call', async (_request, ctx) => {
if (ctx.task?.store) {
const task = await ctx.task.store.createTask({ ttl: 60000 });
// Simulate async work then complete the task
const taskStore = ctx.task.store;
setTimeout(async () => {
await extra.taskStore!.storeTaskResult(task.taskId, 'completed', {
await taskStore.storeTaskResult(task.taskId, 'completed', {
content: [{ type: 'text', text: 'Done' }]
});
}, 50);
@ -2603,11 +2686,14 @@ describe('Progress notification support for tasks', () => {
it('should stop progress notifications when task reaches terminal status (failed)', async () => {
const taskStore = createMockTaskStore();
const protocol = new (class extends Protocol<Request, Notification, Result> {
const protocol = new (class extends Protocol<Request, Notification, Result, BaseContext<Request, Notification>> {
protected assertCapabilityForMethod(): void {}
protected assertNotificationCapability(): void {}
protected assertRequestHandlerCapability(): void {}
protected assertTaskCapability(): void {}
protected buildContext(ctx: any) {
return ctx;
}
protected assertTaskHandlerCapability(): void {}
})({ taskStore });
@ -2701,11 +2787,14 @@ describe('Progress notification support for tasks', () => {
it('should stop progress notifications when task is cancelled', async () => {
const taskStore = createMockTaskStore();
const protocol = new (class extends Protocol<Request, Notification, Result> {
const protocol = new (class extends Protocol<Request, Notification, Result, BaseContext<Request, Notification>> {
protected assertCapabilityForMethod(): void {}
protected assertNotificationCapability(): void {}
protected assertRequestHandlerCapability(): void {}
protected assertTaskCapability(): void {}
protected buildContext(ctx: any) {
return ctx;
}
protected assertTaskHandlerCapability(): void {}
})({ taskStore });
@ -2796,11 +2885,14 @@ describe('Progress notification support for tasks', () => {
it('should use the same progressToken throughout task lifetime', async () => {
const taskStore = createMockTaskStore();
const protocol = new (class extends Protocol<Request, Notification, Result> {
const protocol = new (class extends Protocol<Request, Notification, Result, BaseContext<Request, Notification>> {
protected assertCapabilityForMethod(): void {}
protected assertNotificationCapability(): void {}
protected assertRequestHandlerCapability(): void {}
protected assertTaskCapability(): void {}
protected buildContext(ctx: any) {
return ctx;
}
protected assertTaskHandlerCapability(): void {}
})({ taskStore });
@ -3065,11 +3157,14 @@ describe('Message interception for task-related notifications', () => {
it('should queue notifications with io.modelcontextprotocol/related-task metadata', async () => {
const taskStore = createMockTaskStore();
const transport = new MockTransport();
const server = new (class extends Protocol<Request, Notification, Result> {
const server = new (class extends Protocol<Request, Notification, Result, BaseContext<Request, Notification>> {
protected assertCapabilityForMethod(_method: string): void {}
protected assertNotificationCapability(_method: string): void {}
protected assertRequestHandlerCapability(_method: string): void {}
protected assertTaskCapability(_method: string): void {}
protected buildContext(ctx: any) {
return ctx;
}
protected assertTaskHandlerCapability(_method: string): void {}
})({ taskStore, taskMessageQueue: new InMemoryTaskMessageQueue() });
@ -3102,11 +3197,14 @@ describe('Message interception for task-related notifications', () => {
it('should not queue notifications without related-task metadata', async () => {
const taskStore = createMockTaskStore();
const transport = new MockTransport();
const server = new (class extends Protocol<Request, Notification, Result> {
const server = new (class extends Protocol<Request, Notification, Result, BaseContext<Request, Notification>> {
protected assertCapabilityForMethod(_method: string): void {}
protected assertNotificationCapability(_method: string): void {}
protected assertRequestHandlerCapability(_method: string): void {}
protected assertTaskCapability(_method: string): void {}
protected buildContext(ctx: any) {
return ctx;
}
protected assertTaskHandlerCapability(_method: string): void {}
})({ taskStore, taskMessageQueue: new InMemoryTaskMessageQueue() });
@ -3129,11 +3227,14 @@ describe('Message interception for task-related notifications', () => {
it('should propagate queue overflow errors without failing the task', async () => {
const taskStore = createMockTaskStore();
const transport = new MockTransport();
const server = new (class extends Protocol<Request, Notification, Result> {
const server = new (class extends Protocol<Request, Notification, Result, BaseContext<Request, Notification>> {
protected assertCapabilityForMethod(_method: string): void {}
protected assertNotificationCapability(_method: string): void {}
protected assertRequestHandlerCapability(_method: string): void {}
protected assertTaskCapability(_method: string): void {}
protected buildContext(ctx: any) {
return ctx;
}
protected assertTaskHandlerCapability(_method: string): void {}
})({ taskStore, taskMessageQueue: new InMemoryTaskMessageQueue(), maxTaskQueueSize: 100 });
@ -3176,11 +3277,14 @@ describe('Message interception for task-related notifications', () => {
it('should extract task ID correctly from metadata', async () => {
const taskStore = createMockTaskStore();
const transport = new MockTransport();
const server = new (class extends Protocol<Request, Notification, Result> {
const server = new (class extends Protocol<Request, Notification, Result, BaseContext<Request, Notification>> {
protected assertCapabilityForMethod(_method: string): void {}
protected assertNotificationCapability(_method: string): void {}
protected assertRequestHandlerCapability(_method: string): void {}
protected assertTaskCapability(_method: string): void {}
protected buildContext(ctx: any) {
return ctx;
}
protected assertTaskHandlerCapability(_method: string): void {}
})({ taskStore, taskMessageQueue: new InMemoryTaskMessageQueue() });
@ -3209,11 +3313,14 @@ describe('Message interception for task-related notifications', () => {
it('should preserve message order when queuing multiple notifications', async () => {
const taskStore = createMockTaskStore();
const transport = new MockTransport();
const server = new (class extends Protocol<Request, Notification, Result> {
const server = new (class extends Protocol<Request, Notification, Result, BaseContext<Request, Notification>> {
protected assertCapabilityForMethod(_method: string): void {}
protected assertNotificationCapability(_method: string): void {}
protected assertRequestHandlerCapability(_method: string): void {}
protected assertTaskCapability(_method: string): void {}
protected buildContext(ctx: any) {
return ctx;
}
protected assertTaskHandlerCapability(_method: string): void {}
})({ taskStore, taskMessageQueue: new InMemoryTaskMessageQueue() });
@ -3251,11 +3358,14 @@ describe('Message interception for task-related requests', () => {
it('should queue requests with io.modelcontextprotocol/related-task metadata', async () => {
const taskStore = createMockTaskStore();
const transport = new MockTransport();
const server = new (class extends Protocol<Request, Notification, Result> {
const server = new (class extends Protocol<Request, Notification, Result, BaseContext<Request, Notification>> {
protected assertCapabilityForMethod(_method: string): void {}
protected assertNotificationCapability(_method: string): void {}
protected assertRequestHandlerCapability(_method: string): void {}
protected assertTaskCapability(_method: string): void {}
protected buildContext(ctx: any) {
return ctx;
}
protected assertTaskHandlerCapability(_method: string): void {}
})({ taskStore, taskMessageQueue: new InMemoryTaskMessageQueue() });
@ -3303,11 +3413,14 @@ describe('Message interception for task-related requests', () => {
it('should not queue requests without related-task metadata', async () => {
const taskStore = createMockTaskStore();
const transport = new MockTransport();
const server = new (class extends Protocol<Request, Notification, Result> {
const server = new (class extends Protocol<Request, Notification, Result, BaseContext<Request, Notification>> {
protected assertCapabilityForMethod(_method: string): void {}
protected assertNotificationCapability(_method: string): void {}
protected assertRequestHandlerCapability(_method: string): void {}
protected assertTaskCapability(_method: string): void {}
protected buildContext(ctx: any) {
return ctx;
}
protected assertTaskHandlerCapability(_method: string): void {}
})({ taskStore, taskMessageQueue: new InMemoryTaskMessageQueue() });
@ -3342,11 +3455,14 @@ describe('Message interception for task-related requests', () => {
it('should store request resolver for response routing', async () => {
const taskStore = createMockTaskStore();
const transport = new MockTransport();
const server = new (class extends Protocol<Request, Notification, Result> {
const server = new (class extends Protocol<Request, Notification, Result, BaseContext<Request, Notification>> {
protected assertCapabilityForMethod(_method: string): void {}
protected assertNotificationCapability(_method: string): void {}
protected assertRequestHandlerCapability(_method: string): void {}
protected assertTaskCapability(_method: string): void {}
protected buildContext(ctx: any) {
return ctx;
}
protected assertTaskHandlerCapability(_method: string): void {}
})({ taskStore, taskMessageQueue: new InMemoryTaskMessageQueue() });
@ -3395,11 +3511,14 @@ describe('Message interception for task-related requests', () => {
const taskStore = createMockTaskStore();
const transport = new MockTransport();
const queue = new InMemoryTaskMessageQueue();
const server = new (class extends Protocol<Request, Notification, Result> {
const server = new (class extends Protocol<Request, Notification, Result, BaseContext<Request, Notification>> {
protected assertCapabilityForMethod(_method: string): void {}
protected assertNotificationCapability(_method: string): void {}
protected assertRequestHandlerCapability(_method: string): void {}
protected assertTaskCapability(_method: string): void {}
protected buildContext(ctx: any) {
return ctx;
}
protected assertTaskHandlerCapability(_method: string): void {}
})({ taskStore, taskMessageQueue: queue });
@ -3460,11 +3579,14 @@ describe('Message interception for task-related requests', () => {
it('should log error when resolver is missing for side-channeled request', async () => {
const taskStore = createMockTaskStore();
const transport = new MockTransport();
const server = new (class extends Protocol<Request, Notification, Result> {
const server = new (class extends Protocol<Request, Notification, Result, BaseContext<Request, Notification>> {
protected assertCapabilityForMethod(_method: string): void {}
protected assertNotificationCapability(_method: string): void {}
protected assertRequestHandlerCapability(_method: string): void {}
protected assertTaskCapability(_method: string): void {}
protected buildContext(ctx: any) {
return ctx;
}
protected assertTaskHandlerCapability(_method: string): void {}
})({ taskStore, taskMessageQueue: new InMemoryTaskMessageQueue() });
@ -3536,11 +3658,14 @@ describe('Message interception for task-related requests', () => {
it('should propagate queue overflow errors for requests without failing the task', async () => {
const taskStore = createMockTaskStore();
const transport = new MockTransport();
const server = new (class extends Protocol<Request, Notification, Result> {
const server = new (class extends Protocol<Request, Notification, Result, BaseContext<Request, Notification>> {
protected assertCapabilityForMethod(_method: string): void {}
protected assertNotificationCapability(_method: string): void {}
protected assertRequestHandlerCapability(_method: string): void {}
protected assertTaskCapability(_method: string): void {}
protected buildContext(ctx: any) {
return ctx;
}
protected assertTaskHandlerCapability(_method: string): void {}
})({ taskStore, taskMessageQueue: new InMemoryTaskMessageQueue(), maxTaskQueueSize: 100 });
@ -3590,18 +3715,21 @@ describe('Message interception for task-related requests', () => {
});
describe('Message Interception', () => {
let protocol: Protocol<Request, Notification, Result>;
let protocol: Protocol<Request, Notification, Result, BaseContext<Request, Notification>>;
let transport: MockTransport;
let mockTaskStore: TaskStore & { [K in keyof TaskStore]: MockInstance };
beforeEach(() => {
transport = new MockTransport();
mockTaskStore = createMockTaskStore();
protocol = new (class extends Protocol<Request, Notification, Result> {
protocol = new (class extends Protocol<Request, Notification, Result, BaseContext<Request, Notification>> {
protected assertCapabilityForMethod(): void {}
protected assertNotificationCapability(): void {}
protected assertRequestHandlerCapability(): void {}
protected assertTaskCapability(): void {}
protected buildContext(ctx: any) {
return ctx;
}
protected assertTaskHandlerCapability(): void {}
})({ taskStore: mockTaskStore, taskMessageQueue: new InMemoryTaskMessageQueue() });
});
@ -4108,18 +4236,21 @@ describe('Message Interception', () => {
});
describe('Queue lifecycle management', () => {
let protocol: Protocol<Request, Notification, Result>;
let protocol: Protocol<Request, Notification, Result, BaseContext<Request, Notification>>;
let transport: MockTransport;
let mockTaskStore: TaskStore & { [K in keyof TaskStore]: MockInstance };
beforeEach(() => {
transport = new MockTransport();
mockTaskStore = createMockTaskStore();
protocol = new (class extends Protocol<Request, Notification, Result> {
protocol = new (class extends Protocol<Request, Notification, Result, BaseContext<Request, Notification>> {
protected assertCapabilityForMethod(): void {}
protected assertNotificationCapability(): void {}
protected assertRequestHandlerCapability(): void {}
protected assertTaskCapability(): void {}
protected buildContext(ctx: any) {
return ctx;
}
protected assertTaskHandlerCapability(): void {}
})({ taskStore: mockTaskStore, taskMessageQueue: new InMemoryTaskMessageQueue() });
});
@ -4429,11 +4560,14 @@ describe('requestStream() method', () => {
test('should yield result immediately for non-task requests', async () => {
const transport = new MockTransport();
const protocol = new (class extends Protocol<Request, Notification, Result> {
const protocol = new (class extends Protocol<Request, Notification, Result, BaseContext<Request, Notification>> {
protected assertCapabilityForMethod(): void {}
protected assertNotificationCapability(): void {}
protected assertRequestHandlerCapability(): void {}
protected assertTaskCapability(): void {}
protected buildContext(ctx: any) {
return ctx;
}
protected assertTaskHandlerCapability(): void {}
})();
await protocol.connect(transport);
@ -4472,11 +4606,14 @@ describe('requestStream() method', () => {
test('should yield error message on request failure', async () => {
const transport = new MockTransport();
const protocol = new (class extends Protocol<Request, Notification, Result> {
const protocol = new (class extends Protocol<Request, Notification, Result, BaseContext<Request, Notification>> {
protected assertCapabilityForMethod(): void {}
protected assertNotificationCapability(): void {}
protected assertRequestHandlerCapability(): void {}
protected assertTaskCapability(): void {}
protected buildContext(ctx: any) {
return ctx;
}
protected assertTaskHandlerCapability(): void {}
})();
await protocol.connect(transport);
@ -4518,11 +4655,14 @@ describe('requestStream() method', () => {
test('should handle cancellation via AbortSignal', async () => {
const transport = new MockTransport();
const protocol = new (class extends Protocol<Request, Notification, Result> {
const protocol = new (class extends Protocol<Request, Notification, Result, BaseContext<Request, Notification>> {
protected assertCapabilityForMethod(): void {}
protected assertNotificationCapability(): void {}
protected assertRequestHandlerCapability(): void {}
protected assertTaskCapability(): void {}
protected buildContext(ctx: any) {
return ctx;
}
protected assertTaskHandlerCapability(): void {}
})();
await protocol.connect(transport);
@ -4556,11 +4696,14 @@ describe('requestStream() method', () => {
describe('Error responses', () => {
test('should yield error as terminal message for server error response', async () => {
const transport = new MockTransport();
const protocol = new (class extends Protocol<Request, Notification, Result> {
const protocol = new (class extends Protocol<Request, Notification, Result, BaseContext<Request, Notification>> {
protected assertCapabilityForMethod(): void {}
protected assertNotificationCapability(): void {}
protected assertRequestHandlerCapability(): void {}
protected assertTaskCapability(): void {}
protected buildContext(ctx: any) {
return ctx;
}
protected assertTaskHandlerCapability(): void {}
})();
await protocol.connect(transport);
@ -4598,11 +4741,14 @@ describe('requestStream() method', () => {
vi.useFakeTimers();
try {
const transport = new MockTransport();
const protocol = new (class extends Protocol<Request, Notification, Result> {
const protocol = new (class extends Protocol<Request, Notification, Result, BaseContext<Request, Notification>> {
protected assertCapabilityForMethod(): void {}
protected assertNotificationCapability(): void {}
protected assertRequestHandlerCapability(): void {}
protected assertTaskCapability(): void {}
protected buildContext(ctx: any) {
return ctx;
}
protected assertTaskHandlerCapability(): void {}
})();
await protocol.connect(transport);
@ -4637,11 +4783,14 @@ describe('requestStream() method', () => {
test('should yield error as terminal message for cancellation', async () => {
const transport = new MockTransport();
const protocol = new (class extends Protocol<Request, Notification, Result> {
const protocol = new (class extends Protocol<Request, Notification, Result, BaseContext<Request, Notification>> {
protected assertCapabilityForMethod(): void {}
protected assertNotificationCapability(): void {}
protected assertRequestHandlerCapability(): void {}
protected assertTaskCapability(): void {}
protected buildContext(ctx: any) {
return ctx;
}
protected assertTaskHandlerCapability(): void {}
})();
await protocol.connect(transport);
@ -4670,11 +4819,14 @@ describe('requestStream() method', () => {
test('should not yield any messages after error message', async () => {
const transport = new MockTransport();
const protocol = new (class extends Protocol<Request, Notification, Result> {
const protocol = new (class extends Protocol<Request, Notification, Result, BaseContext<Request, Notification>> {
protected assertCapabilityForMethod(): void {}
protected assertNotificationCapability(): void {}
protected assertRequestHandlerCapability(): void {}
protected assertTaskCapability(): void {}
protected buildContext(ctx: any) {
return ctx;
}
protected assertTaskHandlerCapability(): void {}
})();
await protocol.connect(transport);
@ -4722,11 +4874,14 @@ describe('requestStream() method', () => {
test('should yield error as terminal message for task failure', async () => {
const transport = new MockTransport();
const mockTaskStore = createMockTaskStore();
const protocol = new (class extends Protocol<Request, Notification, Result> {
const protocol = new (class extends Protocol<Request, Notification, Result, BaseContext<Request, Notification>> {
protected assertCapabilityForMethod(): void {}
protected assertNotificationCapability(): void {}
protected assertRequestHandlerCapability(): void {}
protected assertTaskCapability(): void {}
protected buildContext(ctx: any) {
return ctx;
}
protected assertTaskHandlerCapability(): void {}
})({ taskStore: mockTaskStore });
await protocol.connect(transport);
@ -4782,11 +4937,14 @@ describe('requestStream() method', () => {
test('should yield error as terminal message for network error', async () => {
const transport = new MockTransport();
const protocol = new (class extends Protocol<Request, Notification, Result> {
const protocol = new (class extends Protocol<Request, Notification, Result, BaseContext<Request, Notification>> {
protected assertCapabilityForMethod(): void {}
protected assertNotificationCapability(): void {}
protected assertRequestHandlerCapability(): void {}
protected assertTaskCapability(): void {}
protected buildContext(ctx: any) {
return ctx;
}
protected assertTaskHandlerCapability(): void {}
})();
await protocol.connect(transport);
@ -4810,11 +4968,14 @@ describe('requestStream() method', () => {
test('should ensure error is always the final message', async () => {
const transport = new MockTransport();
const protocol = new (class extends Protocol<Request, Notification, Result> {
const protocol = new (class extends Protocol<Request, Notification, Result, BaseContext<Request, Notification>> {
protected assertCapabilityForMethod(): void {}
protected assertNotificationCapability(): void {}
protected assertRequestHandlerCapability(): void {}
protected assertTaskCapability(): void {}
protected buildContext(ctx: any) {
return ctx;
}
protected assertTaskHandlerCapability(): void {}
})();
await protocol.connect(transport);
@ -4855,7 +5016,7 @@ describe('requestStream() method', () => {
});
describe('Error handling for missing resolvers', () => {
let protocol: Protocol<Request, Notification, Result>;
let protocol: Protocol<Request, Notification, Result, BaseContext<Request, Notification>>;
let transport: MockTransport;
let taskStore: TaskStore & { [K in keyof TaskStore]: MockInstance };
let taskMessageQueue: TaskMessageQueue;
@ -4866,11 +5027,14 @@ describe('Error handling for missing resolvers', () => {
taskMessageQueue = new InMemoryTaskMessageQueue();
errorHandler = vi.fn();
protocol = new (class extends Protocol<Request, Notification, Result> {
protocol = new (class extends Protocol<Request, Notification, Result, BaseContext<Request, Notification>> {
protected assertCapabilityForMethod(_method: string): void {}
protected assertNotificationCapability(_method: string): void {}
protected assertRequestHandlerCapability(_method: string): void {}
protected assertTaskCapability(_method: string): void {}
protected buildContext(ctx: any) {
return ctx;
}
protected assertTaskHandlerCapability(_method: string): void {}
})({
taskStore,

View file

@ -1,5 +1,6 @@
import { beforeEach, describe, expect, test } from 'vitest';
import type { BaseContext } from '../../src/shared/protocol.js';
import { Protocol } from '../../src/shared/protocol.js';
import type { Transport } from '../../src/shared/transport.js';
import type { EmptyResult, JSONRPCMessage, Notification, Request, Result } from '../../src/types/types.js';
@ -28,16 +29,19 @@ class MockTransport implements Transport {
}
describe('Protocol transport handling bug', () => {
let protocol: Protocol<Request, Notification, Result>;
let protocol: Protocol<Request, Notification, Result, BaseContext<Request, Notification>>;
let transportA: MockTransport;
let transportB: MockTransport;
beforeEach(() => {
protocol = new (class extends Protocol<Request, Notification, Result> {
protocol = new (class extends Protocol<Request, Notification, Result, BaseContext<Request, Notification>> {
protected assertCapabilityForMethod(): void {}
protected assertNotificationCapability(): void {}
protected assertRequestHandlerCapability(): void {}
protected assertTaskCapability(): void {}
protected buildContext(ctx: any) {
return ctx;
}
protected assertTaskHandlerCapability(): void {}
})();
@ -86,8 +90,8 @@ describe('Protocol transport handling bug', () => {
const results: { transport: string; response: JSONRPCMessage[] }[] = [];
// Set up handler with variable delay based on request id
protocol.setRequestHandler('ping', async (_request, extra) => {
const delay = extra.requestId === 1 ? 50 : 10;
protocol.setRequestHandler('ping', async (_request, ctx) => {
const delay = ctx.mcpReq.id === 1 ? 50 : 10;
await new Promise(resolve => setTimeout(resolve, delay));
return {};
});

View file

@ -214,8 +214,10 @@ describe('Zod v4', () => {
description: 'A user profile data tool',
inputSchema: z.object({ active: z.boolean().describe('Profile status') })
},
async ({ active }, { authInfo }): Promise<CallToolResult> => {
return { content: [{ type: 'text', text: `${active ? 'Active' : 'Inactive'} profile from token: ${authInfo?.token}!` }] };
async ({ active }, ctx): Promise<CallToolResult> => {
return {
content: [{ type: 'text', text: `${active ? 'Active' : 'Inactive'} profile from token: ${ctx.http?.authInfo?.token}!` }]
};
}
);
@ -403,11 +405,11 @@ describe('Zod v4', () => {
description: 'A simple test tool with request info',
inputSchema: z.object({ name: z.string().describe('Name to greet') })
},
async ({ name }, { requestInfo }): Promise<CallToolResult> => {
async ({ name }, ctx): Promise<CallToolResult> => {
// Convert Headers object to plain object for JSON serialization
// Headers is a Web API class that doesn't serialize with JSON.stringify
const serializedRequestInfo = {
headers: Object.fromEntries(requestInfo?.headers ?? new Headers())
headers: Object.fromEntries(ctx.http?.req?.headers ?? new Headers())
};
return {
content: [
@ -1831,7 +1833,7 @@ describe('Zod v4', () => {
expect(text).not.toContain('retry:');
});
it('should close POST SSE stream when extra.closeSSEStream is called', async () => {
it('should close POST SSE stream when ctx.http?.closeSSE is called', async () => {
const result = await createTestServer({
sessionIdGenerator: () => randomUUID(),
eventStore: createEventStore(),
@ -1849,10 +1851,10 @@ describe('Zod v4', () => {
toolResolve = resolve;
});
// Register a tool that closes its own SSE stream via extra callback
mcpServer.registerTool('close-stream-tool', { description: 'Closes its own stream' }, async extra => {
// Register a tool that closes its own SSE stream via ctx callback
mcpServer.registerTool('close-stream-tool', { description: 'Closes its own stream' }, async ctx => {
// Close the SSE stream for this request
extra.closeSSEStream?.();
ctx.http?.closeSSE?.();
streamCloseCalled = true;
// Wait before returning so we can observe the stream closure
@ -1903,7 +1905,7 @@ describe('Zod v4', () => {
toolResolve!();
});
it('should provide closeSSEStream callback in extra when eventStore is configured', async () => {
it('should provide closeSSEStream callback in ctx when eventStore is configured', async () => {
const result = await createTestServer({
sessionIdGenerator: () => randomUUID(),
eventStore: createEventStore(),
@ -1917,9 +1919,9 @@ describe('Zod v4', () => {
// Track whether closeSSEStream callback was provided
let receivedCloseSSEStream: (() => void) | undefined;
// Register a tool that captures the extra.closeSSEStream callback
mcpServer.registerTool('test-callback-tool', { description: 'Test tool' }, async extra => {
receivedCloseSSEStream = extra.closeSSEStream;
// Register a tool that captures the ctx.http?.closeSSE callback
mcpServer.registerTool('test-callback-tool', { description: 'Test tool' }, async ctx => {
receivedCloseSSEStream = ctx.http?.closeSSE;
return { content: [{ type: 'text', text: 'Done' }] };
});
@ -1976,10 +1978,10 @@ describe('Zod v4', () => {
let receivedCloseSSEStream: (() => void) | undefined;
let receivedCloseStandaloneSSEStream: (() => void) | undefined;
// Register a tool that captures the extra.closeSSEStream callback
mcpServer.registerTool('test-old-version-tool', { description: 'Test tool' }, async extra => {
receivedCloseSSEStream = extra.closeSSEStream;
receivedCloseStandaloneSSEStream = extra.closeStandaloneSSEStream;
// Register a tool that captures the ctx.http?.closeSSE callback
mcpServer.registerTool('test-old-version-tool', { description: 'Test tool' }, async ctx => {
receivedCloseSSEStream = ctx.http?.closeSSE;
receivedCloseStandaloneSSEStream = ctx.http?.closeStandaloneSSE;
return { content: [{ type: 'text', text: 'Done' }] };
});
@ -2035,9 +2037,9 @@ describe('Zod v4', () => {
// Track whether closeSSEStream callback was provided
let receivedCloseSSEStream: (() => void) | undefined;
// Register a tool that captures the extra.closeSSEStream callback
mcpServer.registerTool('test-no-callback-tool', { description: 'Test tool' }, async extra => {
receivedCloseSSEStream = extra.closeSSEStream;
// Register a tool that captures the ctx.http?.closeSSE callback
mcpServer.registerTool('test-no-callback-tool', { description: 'Test tool' }, async ctx => {
receivedCloseSSEStream = ctx.http?.closeSSE;
return { content: [{ type: 'text', text: 'Done' }] };
});
@ -2078,7 +2080,7 @@ describe('Zod v4', () => {
expect(receivedCloseSSEStream).toBeUndefined();
});
it('should provide closeStandaloneSSEStream callback in extra when eventStore is configured', async () => {
it('should provide closeStandaloneSSEStream callback in ctx when eventStore is configured', async () => {
const result = await createTestServer({
sessionIdGenerator: () => randomUUID(),
eventStore: createEventStore(),
@ -2092,9 +2094,9 @@ describe('Zod v4', () => {
// Track whether closeStandaloneSSEStream callback was provided
let receivedCloseStandaloneSSEStream: (() => void) | undefined;
// Register a tool that captures the extra.closeStandaloneSSEStream callback
mcpServer.registerTool('test-standalone-callback-tool', { description: 'Test tool' }, async extra => {
receivedCloseStandaloneSSEStream = extra.closeStandaloneSSEStream;
// Register a tool that captures the ctx.http?.closeStandaloneSSE callback
mcpServer.registerTool('test-standalone-callback-tool', { description: 'Test tool' }, async ctx => {
receivedCloseStandaloneSSEStream = ctx.http?.closeStandaloneSSE;
return { content: [{ type: 'text', text: 'Done' }] };
});
@ -2136,7 +2138,7 @@ describe('Zod v4', () => {
expect(typeof receivedCloseStandaloneSSEStream).toBe('function');
});
it('should close standalone GET SSE stream when extra.closeStandaloneSSEStream is called', async () => {
it('should close standalone GET SSE stream when ctx.http?.closeStandaloneSSE is called', async () => {
const result = await createTestServer({
sessionIdGenerator: () => randomUUID(),
eventStore: createEventStore(),
@ -2147,9 +2149,9 @@ describe('Zod v4', () => {
baseUrl = result.baseUrl;
mcpServer = result.mcpServer;
// Register a tool that closes the standalone SSE stream via extra callback
mcpServer.registerTool('close-standalone-stream-tool', { description: 'Closes standalone stream' }, async extra => {
extra.closeStandaloneSSEStream?.();
// Register a tool that closes the standalone SSE stream via ctx callback
mcpServer.registerTool('close-standalone-stream-tool', { description: 'Closes standalone stream' }, async ctx => {
ctx.http?.closeStandaloneSSE?.();
return { content: [{ type: 'text', text: 'Stream closed' }] };
});
@ -2217,7 +2219,7 @@ describe('Zod v4', () => {
expect(done).toBe(true);
});
it('should allow client to reconnect after standalone SSE stream is closed via extra.closeStandaloneSSEStream', async () => {
it('should allow client to reconnect after standalone SSE stream is closed via ctx.http?.closeStandaloneSSE', async () => {
const result = await createTestServer({
sessionIdGenerator: () => randomUUID(),
eventStore: createEventStore(),
@ -2229,8 +2231,8 @@ describe('Zod v4', () => {
mcpServer = result.mcpServer;
// Register a tool that closes the standalone SSE stream
mcpServer.registerTool('close-standalone-for-reconnect', { description: 'Closes standalone stream' }, async extra => {
extra.closeStandaloneSSEStream?.();
mcpServer.registerTool('close-standalone-for-reconnect', { description: 'Closes standalone stream' }, async ctx => {
ctx.http?.closeStandaloneSSE?.();
return { content: [{ type: 'text', text: 'Stream closed' }] };
});

View file

@ -6,11 +6,11 @@
import type {
AnySchema,
CallToolResult,
CreateTaskRequestHandlerExtra,
CreateTaskResult,
CreateTaskServerContext,
GetTaskResult,
Result,
TaskRequestHandlerExtra
TaskServerContext
} from '@modelcontextprotocol/core';
import type { BaseToolCallback } from '../../server/mcp.js';
@ -25,7 +25,7 @@ import type { BaseToolCallback } from '../../server/mcp.js';
*/
export type CreateTaskRequestHandler<SendResultT extends Result, Args extends AnySchema | undefined = undefined> = BaseToolCallback<
SendResultT,
CreateTaskRequestHandlerExtra,
CreateTaskServerContext,
Args
>;
@ -35,7 +35,7 @@ export type CreateTaskRequestHandler<SendResultT extends Result, Args extends An
*/
export type TaskRequestHandler<SendResultT extends Result, Args extends AnySchema | undefined = undefined> = BaseToolCallback<
SendResultT,
TaskRequestHandlerExtra,
TaskServerContext,
Args
>;

View file

@ -55,16 +55,16 @@ export class ExperimentalMcpServerTasks {
* inputSchema: z.object({ input: z.string() }),
* execution: { taskSupport: 'required' }
* }, {
* createTask: async (args, extra) => {
* const task = await extra.taskStore.createTask({ ttl: 300000 });
* createTask: async (args, ctx) => {
* const task = await ctx.task.store.createTask({ ttl: 300000 });
* startBackgroundWork(task.taskId, args);
* return { task };
* },
* getTask: async (args, extra) => {
* return extra.taskStore.getTask(extra.taskId);
* getTask: async (args, ctx) => {
* return ctx.task.store.getTask(ctx.task.id);
* },
* getTaskResult: async (args, extra) => {
* return extra.taskStore.getTaskResult(extra.taskId);
* getTaskResult: async (args, ctx) => {
* return ctx.task.store.getTaskResult(ctx.task.id);
* }
* });
* ```

View file

@ -6,8 +6,8 @@ import type {
CompleteRequestPrompt,
CompleteRequestResourceTemplate,
CompleteResult,
CreateTaskRequestHandlerExtra,
CreateTaskResult,
CreateTaskServerContext,
GetPromptResult,
Implementation,
ListPromptsResult,
@ -18,13 +18,11 @@ import type {
PromptArgument,
PromptReference,
ReadResourceResult,
RequestHandlerExtra,
Resource,
ResourceTemplateReference,
Result,
SchemaOutput,
ServerNotification,
ServerRequest,
ServerContext,
Tool,
ToolAnnotations,
ToolExecution,
@ -152,7 +150,7 @@ export class McpServer {
})
);
this.server.setRequestHandler('tools/call', async (request, extra): Promise<CallToolResult | CreateTaskResult> => {
this.server.setRequestHandler('tools/call', async (request, ctx): Promise<CallToolResult | CreateTaskResult> => {
try {
const tool = this._registeredTools[request.params.name];
if (!tool) {
@ -184,12 +182,12 @@ export class McpServer {
// Handle taskSupport 'optional' without task augmentation - automatic polling
if (taskSupport === 'optional' && !isTaskRequest && isTaskHandler) {
return await this.handleAutomaticTaskPolling(tool, request, extra);
return await this.handleAutomaticTaskPolling(tool, request, ctx);
}
// Normal execution path
const args = await this.validateToolInput(tool, request.params.arguments, request.params.name);
const result = await this.executeToolHandler(tool, args, extra);
const result = await this.executeToolHandler(tool, args, ctx);
// Return CreateTaskResult immediately for task requests
if (isTaskRequest) {
@ -293,13 +291,9 @@ export class McpServer {
/**
* Executes a tool handler (either regular or task-based).
*/
private async executeToolHandler(
tool: RegisteredTool,
args: unknown,
extra: RequestHandlerExtra<ServerRequest, ServerNotification>
): Promise<CallToolResult | CreateTaskResult> {
private async executeToolHandler(tool: RegisteredTool, args: unknown, ctx: ServerContext): Promise<CallToolResult | CreateTaskResult> {
// Executor encapsulates handler invocation with proper types
return tool.executor(args, extra);
return tool.executor(args, ctx);
}
/**
@ -308,15 +302,15 @@ export class McpServer {
private async handleAutomaticTaskPolling<RequestT extends CallToolRequest>(
tool: RegisteredTool,
request: RequestT,
extra: RequestHandlerExtra<ServerRequest, ServerNotification>
ctx: ServerContext
): Promise<CallToolResult> {
if (!extra.taskStore) {
if (!ctx.task?.store) {
throw new Error('No task store provided for task-capable tool.');
}
// Validate input and create task using the executor
const args = await this.validateToolInput(tool, request.params.arguments, request.params.name);
const createTaskResult = (await tool.executor(args, extra)) as CreateTaskResult;
const createTaskResult = (await tool.executor(args, ctx)) as CreateTaskResult;
// Poll until completion
const taskId = createTaskResult.task.taskId;
@ -325,7 +319,7 @@ export class McpServer {
while (task.status !== 'completed' && task.status !== 'failed' && task.status !== 'cancelled') {
await new Promise(resolve => setTimeout(resolve, pollInterval));
const updatedTask = await extra.taskStore.getTask(taskId);
const updatedTask = await ctx.task.store.getTask(taskId);
if (!updatedTask) {
throw new ProtocolError(ProtocolErrorCode.InternalError, `Task ${taskId} not found during polling`);
}
@ -333,7 +327,7 @@ export class McpServer {
}
// Return the final result
return (await extra.taskStore.getTaskResult(taskId)) as CallToolResult;
return (await ctx.task.store.getTaskResult(taskId)) as CallToolResult;
}
private _completionHandlerInitialized = false;
@ -439,7 +433,7 @@ export class McpServer {
}
});
this.server.setRequestHandler('resources/list', async (_request, extra) => {
this.server.setRequestHandler('resources/list', async (_request, ctx) => {
const resources = Object.entries(this._registeredResources)
.filter(([_, resource]) => resource.enabled)
.map(([uri, resource]) => ({
@ -454,7 +448,7 @@ export class McpServer {
continue;
}
const result = await template.resourceTemplate.listCallback(extra);
const result = await template.resourceTemplate.listCallback(ctx);
for (const resource of result.resources) {
templateResources.push({
...template.metadata,
@ -477,7 +471,7 @@ export class McpServer {
return { resourceTemplates };
});
this.server.setRequestHandler('resources/read', async (request, extra) => {
this.server.setRequestHandler('resources/read', async (request, ctx) => {
const uri = new URL(request.params.uri);
// First check for exact resource match
@ -486,14 +480,14 @@ export class McpServer {
if (!resource.enabled) {
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Resource ${uri} disabled`);
}
return resource.readCallback(uri, extra);
return resource.readCallback(uri, ctx);
}
// Then check templates
for (const template of Object.values(this._registeredResourceTemplates)) {
const variables = template.resourceTemplate.uriTemplate.match(uri.toString());
if (variables) {
return template.readCallback(uri, variables, extra);
return template.readCallback(uri, variables, ctx);
}
}
@ -535,7 +529,7 @@ export class McpServer {
})
);
this.server.setRequestHandler('prompts/get', async (request, extra): Promise<GetPromptResult> => {
this.server.setRequestHandler('prompts/get', async (request, ctx): Promise<GetPromptResult> => {
const prompt = this._registeredPrompts[request.params.name];
if (!prompt) {
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Prompt ${request.params.name} not found`);
@ -546,7 +540,7 @@ export class McpServer {
}
// Handler encapsulates parsing and callback invocation with proper types
return prompt.handler(request.params.arguments, extra);
return prompt.handler(request.params.arguments, ctx);
});
this._promptHandlersInitialized = true;
@ -988,20 +982,16 @@ export class ResourceTemplate {
export type BaseToolCallback<
SendResultT extends Result,
Extra extends RequestHandlerExtra<ServerRequest, ServerNotification>,
Ctx extends ServerContext,
Args extends AnySchema | undefined
> = Args extends AnySchema
? (args: SchemaOutput<Args>, extra: Extra) => SendResultT | Promise<SendResultT>
: (extra: Extra) => SendResultT | Promise<SendResultT>;
? (args: SchemaOutput<Args>, ctx: Ctx) => SendResultT | Promise<SendResultT>
: (ctx: Ctx) => SendResultT | Promise<SendResultT>;
/**
* Callback for a tool handler registered with Server.tool().
*/
export type ToolCallback<Args extends AnySchema | undefined = undefined> = BaseToolCallback<
CallToolResult,
RequestHandlerExtra<ServerRequest, ServerNotification>,
Args
>;
export type ToolCallback<Args extends AnySchema | undefined = undefined> = BaseToolCallback<CallToolResult, ServerContext, Args>;
/**
* Supertype that can handle both regular tools (simple callback) and task-based tools (task handler object).
@ -1011,10 +1001,7 @@ export type AnyToolHandler<Args extends AnySchema | undefined = undefined> = Too
/**
* Internal executor type that encapsulates handler invocation with proper types.
*/
type ToolExecutor = (
args: unknown,
extra: RequestHandlerExtra<ServerRequest, ServerNotification>
) => Promise<CallToolResult | CreateTaskResult>;
type ToolExecutor = (args: unknown, ctx: ServerContext) => Promise<CallToolResult | CreateTaskResult>;
export type RegisteredTool = {
title?: string;
@ -1046,37 +1033,35 @@ export type RegisteredTool = {
/**
* Creates an executor that invokes the handler with the appropriate arguments.
* When inputSchema is defined, the handler is called with (args, extra).
* When inputSchema is undefined, the handler is called with just (extra).
* When inputSchema is defined, the handler is called with (args, ctx).
* When inputSchema is undefined, the handler is called with just (ctx).
*/
function createToolExecutor(inputSchema: AnySchema | undefined, handler: AnyToolHandler<AnySchema | undefined>): ToolExecutor {
const isTaskHandler = 'createTask' in handler;
if (isTaskHandler) {
const taskHandler = handler as TaskHandlerInternal;
return async (args, extra) => {
if (!extra.taskStore) {
return async (args, ctx) => {
if (!ctx.task?.store) {
throw new Error('No task store provided.');
}
const taskExtra: CreateTaskRequestHandlerExtra = { ...extra, taskStore: extra.taskStore };
const taskCtx: CreateTaskServerContext = { ...ctx, task: { store: ctx.task.store, requestedTtl: ctx.task?.requestedTtl } };
if (inputSchema) {
return taskHandler.createTask(args, taskExtra);
return taskHandler.createTask(args, taskCtx);
}
// When no inputSchema, call with just extra (the handler expects (extra) signature)
return (taskHandler.createTask as (extra: CreateTaskRequestHandlerExtra) => CreateTaskResult | Promise<CreateTaskResult>)(
taskExtra
);
// When no inputSchema, call with just ctx (the handler expects (ctx) signature)
return (taskHandler.createTask as (ctx: CreateTaskServerContext) => CreateTaskResult | Promise<CreateTaskResult>)(taskCtx);
};
}
if (inputSchema) {
const callback = handler as ToolCallbackInternal;
return async (args, extra) => callback(args, extra);
return async (args, ctx) => callback(args, ctx);
}
// When no inputSchema, call with just extra (the handler expects (extra) signature)
const callback = handler as (extra: RequestHandlerExtra<ServerRequest, ServerNotification>) => CallToolResult | Promise<CallToolResult>;
return async (_args, extra) => callback(extra);
// When no inputSchema, call with just ctx (the handler expects (ctx) signature)
const callback = handler as (ctx: ServerContext) => CallToolResult | Promise<CallToolResult>;
return async (_args, ctx) => callback(ctx);
}
const EMPTY_OBJECT_JSON_SCHEMA = {
@ -1092,17 +1077,12 @@ export type ResourceMetadata = Omit<Resource, 'uri' | 'name'>;
/**
* Callback to list all resources matching a given template.
*/
export type ListResourcesCallback = (
extra: RequestHandlerExtra<ServerRequest, ServerNotification>
) => ListResourcesResult | Promise<ListResourcesResult>;
export type ListResourcesCallback = (ctx: ServerContext) => ListResourcesResult | Promise<ListResourcesResult>;
/**
* Callback to read a resource at a given URI.
*/
export type ReadResourceCallback = (
uri: URL,
extra: RequestHandlerExtra<ServerRequest, ServerNotification>
) => ReadResourceResult | Promise<ReadResourceResult>;
export type ReadResourceCallback = (uri: URL, ctx: ServerContext) => ReadResourceResult | Promise<ReadResourceResult>;
export type RegisteredResource = {
name: string;
@ -1129,7 +1109,7 @@ export type RegisteredResource = {
export type ReadResourceTemplateCallback = (
uri: URL,
variables: Variables,
extra: RequestHandlerExtra<ServerRequest, ServerNotification>
ctx: ServerContext
) => ReadResourceResult | Promise<ReadResourceResult>;
export type RegisteredResourceTemplate = {
@ -1152,28 +1132,19 @@ export type RegisteredResourceTemplate = {
};
export type PromptCallback<Args extends AnySchema | undefined = undefined> = Args extends AnySchema
? (
args: SchemaOutput<Args>,
extra: RequestHandlerExtra<ServerRequest, ServerNotification>
) => GetPromptResult | Promise<GetPromptResult>
: (extra: RequestHandlerExtra<ServerRequest, ServerNotification>) => GetPromptResult | Promise<GetPromptResult>;
? (args: SchemaOutput<Args>, ctx: ServerContext) => GetPromptResult | Promise<GetPromptResult>
: (ctx: ServerContext) => GetPromptResult | Promise<GetPromptResult>;
/**
* Internal handler type that encapsulates parsing and callback invocation.
* This allows type-safe handling without runtime type assertions.
*/
type PromptHandler = (
args: Record<string, unknown> | undefined,
extra: RequestHandlerExtra<ServerRequest, ServerNotification>
) => Promise<GetPromptResult>;
type PromptHandler = (args: Record<string, unknown> | undefined, ctx: ServerContext) => Promise<GetPromptResult>;
type ToolCallbackInternal = (
args: unknown,
extra: RequestHandlerExtra<ServerRequest, ServerNotification>
) => CallToolResult | Promise<CallToolResult>;
type ToolCallbackInternal = (args: unknown, ctx: ServerContext) => CallToolResult | Promise<CallToolResult>;
type TaskHandlerInternal = {
createTask: (args: unknown, extra: CreateTaskRequestHandlerExtra) => CreateTaskResult | Promise<CreateTaskResult>;
createTask: (args: unknown, ctx: CreateTaskServerContext) => CreateTaskResult | Promise<CreateTaskResult>;
};
export type RegisteredPrompt = {
@ -1206,26 +1177,21 @@ function createPromptHandler(
callback: PromptCallback<AnySchema | undefined>
): PromptHandler {
if (argsSchema) {
const typedCallback = callback as (
args: SchemaOutput<AnySchema>,
extra: RequestHandlerExtra<ServerRequest, ServerNotification>
) => GetPromptResult | Promise<GetPromptResult>;
const typedCallback = callback as (args: SchemaOutput<AnySchema>, ctx: ServerContext) => GetPromptResult | Promise<GetPromptResult>;
return async (args, extra) => {
return async (args, ctx) => {
const parseResult = await parseSchemaAsync(argsSchema, args);
if (!parseResult.success) {
const errorMessage = parseResult.error.issues.map((i: { message: string }) => i.message).join(', ');
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Invalid arguments for prompt ${name}: ${errorMessage}`);
}
return typedCallback(parseResult.data as SchemaOutput<AnySchema>, extra);
return typedCallback(parseResult.data as SchemaOutput<AnySchema>, ctx);
};
} else {
const typedCallback = callback as (
extra: RequestHandlerExtra<ServerRequest, ServerNotification>
) => GetPromptResult | Promise<GetPromptResult>;
const typedCallback = callback as (ctx: ServerContext) => GetPromptResult | Promise<GetPromptResult>;
return async (_args, extra) => {
return typedCallback(extra);
return async (_args, ctx) => {
return typedCallback(ctx);
};
}
}

View file

@ -1,4 +1,5 @@
import type {
BaseContext,
ClientCapabilities,
CreateMessageRequest,
CreateMessageRequestParamsBase,
@ -16,17 +17,18 @@ import type {
ListRootsRequest,
LoggingLevel,
LoggingMessageNotification,
MessageExtraInfo,
Notification,
NotificationOptions,
ProtocolOptions,
Request,
RequestHandlerExtra,
RequestMethod,
RequestOptions,
RequestTypeMap,
ResourceUpdatedNotification,
Result,
ServerCapabilities,
ServerContext,
ServerNotification,
ServerRequest,
ServerResult,
@ -110,7 +112,12 @@ export class Server<
RequestT extends Request = Request,
NotificationT extends Notification = Notification,
ResultT extends Result = Result
> extends Protocol<ServerRequest | RequestT, ServerNotification | NotificationT, ServerResult | ResultT> {
> extends Protocol<
ServerRequest | RequestT,
ServerNotification | NotificationT,
ServerResult | ResultT,
ServerContext<ServerRequest | RequestT, ServerNotification | NotificationT>
> {
private _clientCapabilities?: ClientCapabilities;
private _clientVersion?: Implementation;
private _capabilities: ServerCapabilities;
@ -139,9 +146,9 @@ export class Server<
this.setNotificationHandler('notifications/initialized', () => this.oninitialized?.());
if (this._capabilities.logging) {
this.setRequestHandler('logging/setLevel', async (request, extra) => {
this.setRequestHandler('logging/setLevel', async (request, ctx) => {
const transportSessionId: string | undefined =
extra.sessionId || (extra.requestInfo?.headers.get('mcp-session-id') as string) || undefined;
ctx.sessionId || (ctx.http?.req?.headers.get('mcp-session-id') as string) || undefined;
const { level } = request.params;
const parseResult = parseSchema(LoggingLevelSchema, level);
if (parseResult.success) {
@ -152,6 +159,32 @@ export class Server<
}
}
protected override buildContext(
ctx: BaseContext<ServerRequest | RequestT, ServerNotification | NotificationT>,
transportInfo?: MessageExtraInfo
): ServerContext<ServerRequest | RequestT, ServerNotification | NotificationT> {
// Only create http when there's actual HTTP transport info or auth info
const hasHttpInfo =
ctx.http || transportInfo?.requestInfo || transportInfo?.closeSSEStream || transportInfo?.closeStandaloneSSEStream;
return {
...ctx,
mcpReq: {
...ctx.mcpReq,
log: (level, data, logger) => this.sendLoggingMessage({ level, data, logger }),
elicitInput: (params, options) => this.elicitInput(params, options),
requestSampling: (params, options) => this.createMessage(params, options)
},
http: hasHttpInfo
? {
...ctx.http,
req: transportInfo?.requestInfo,
closeSSE: transportInfo?.closeSSEStream,
closeStandaloneSSE: transportInfo?.closeStandaloneSSEStream
}
: undefined
};
}
/**
* Access experimental features.
*
@ -199,13 +232,13 @@ export class Server<
method: M,
handler: (
request: RequestTypeMap[M],
extra: RequestHandlerExtra<ServerRequest | RequestT, ServerNotification | NotificationT>
ctx: ServerContext<ServerRequest | RequestT, ServerNotification | NotificationT>
) => ServerResult | ResultT | Promise<ServerResult | ResultT>
): void {
if (method === 'tools/call') {
const wrappedHandler = async (
request: RequestTypeMap[M],
extra: RequestHandlerExtra<ServerRequest | RequestT, ServerNotification | NotificationT>
ctx: ServerContext<ServerRequest | RequestT, ServerNotification | NotificationT>
): Promise<ServerResult | ResultT> => {
const validatedRequest = parseSchema(CallToolRequestSchema, request);
if (!validatedRequest.success) {
@ -216,7 +249,7 @@ export class Server<
const { params } = validatedRequest.data;
const result = await Promise.resolve(handler(request, extra));
const result = await Promise.resolve(handler(request, ctx));
// When task creation is requested, validate and return CreateTaskResult
if (params.task) {
@ -243,7 +276,7 @@ export class Server<
};
// Install the wrapped handler
return super.setRequestHandler(method, wrappedHandler as unknown as typeof handler);
return super.setRequestHandler(method, wrappedHandler);
}
// Other handlers use default behavior

View file

@ -202,8 +202,8 @@ function createMcpServer() {
description: 'Tests tool that emits log messages during execution',
inputSchema: z.object({})
},
async (_args, extra): Promise<CallToolResult> => {
await extra.sendNotification({
async (_args, ctx): Promise<CallToolResult> => {
await ctx.mcpReq.notify({
method: 'notifications/message',
params: {
level: 'info',
@ -212,7 +212,7 @@ function createMcpServer() {
});
await new Promise(resolve => setTimeout(resolve, 50));
await extra.sendNotification({
await ctx.mcpReq.notify({
method: 'notifications/message',
params: {
level: 'info',
@ -221,7 +221,7 @@ function createMcpServer() {
});
await new Promise(resolve => setTimeout(resolve, 50));
await extra.sendNotification({
await ctx.mcpReq.notify({
method: 'notifications/message',
params: {
level: 'info',
@ -241,10 +241,10 @@ function createMcpServer() {
description: 'Tests tool that reports progress notifications',
inputSchema: z.object({})
},
async (_args, extra): Promise<CallToolResult> => {
const progressToken = extra._meta?.progressToken ?? 0;
async (_args, ctx): Promise<CallToolResult> => {
const progressToken = ctx.mcpReq._meta?.progressToken ?? 0;
console.log('Progress token:', progressToken);
await extra.sendNotification({
await ctx.mcpReq.notify({
method: 'notifications/progress',
params: {
progressToken,
@ -255,7 +255,7 @@ function createMcpServer() {
});
await new Promise(resolve => setTimeout(resolve, 50));
await extra.sendNotification({
await ctx.mcpReq.notify({
method: 'notifications/progress',
params: {
progressToken,
@ -266,7 +266,7 @@ function createMcpServer() {
});
await new Promise(resolve => setTimeout(resolve, 50));
await extra.sendNotification({
await ctx.mcpReq.notify({
method: 'notifications/progress',
params: {
progressToken,
@ -301,23 +301,23 @@ function createMcpServer() {
'Tests SSE stream disconnection and client reconnection (SEP-1699). Server will close the stream mid-call and send the result after client reconnects.',
inputSchema: z.object({})
},
async (_args, extra): Promise<CallToolResult> => {
async (_args, ctx): Promise<CallToolResult> => {
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
console.log(`[${extra.sessionId}] Starting test_reconnection tool...`);
console.log(`[${ctx.sessionId}] Starting test_reconnection tool...`);
// Get the transport for this session
const transport = extra.sessionId ? transports[extra.sessionId] : undefined;
if (transport && extra.requestId) {
const transport = ctx.sessionId ? transports[ctx.sessionId] : undefined;
if (transport && ctx.mcpReq.id) {
// Close the SSE stream to trigger client reconnection
console.log(`[${extra.sessionId}] Closing SSE stream to trigger client polling...`);
transport.closeSSEStream(extra.requestId);
console.log(`[${ctx.sessionId}] Closing SSE stream to trigger client polling...`);
transport.closeSSEStream(ctx.mcpReq.id);
}
// Wait for client to reconnect (should respect retry field)
await sleep(100);
console.log(`[${extra.sessionId}] test_reconnection tool complete`);
console.log(`[${ctx.sessionId}] test_reconnection tool complete`);
return {
content: [
@ -339,10 +339,10 @@ function createMcpServer() {
prompt: z.string().describe('The prompt to send to the LLM')
})
},
async (args: { prompt: string }, extra): Promise<CallToolResult> => {
async (args: { prompt: string }, ctx): Promise<CallToolResult> => {
try {
// Request sampling from client
const result = (await extra.sendRequest(
const result = (await ctx.mcpReq.send(
{
method: 'sampling/createMessage',
params: {
@ -393,10 +393,10 @@ function createMcpServer() {
message: z.string().describe('The message to show the user')
})
},
async (args: { message: string }, extra): Promise<CallToolResult> => {
async (args: { message: string }, ctx): Promise<CallToolResult> => {
try {
// Request user input from client
const result = await extra.sendRequest(
const result = await ctx.mcpReq.send(
{
method: 'elicitation/create',
params: {
@ -445,10 +445,10 @@ function createMcpServer() {
description: 'Tests elicitation with default values per SEP-1034',
inputSchema: z.object({})
},
async (_args, extra): Promise<CallToolResult> => {
async (_args, ctx): Promise<CallToolResult> => {
try {
// Request user input with default values for all primitive types
const result = await extra.sendRequest(
const result = await ctx.mcpReq.send(
{
method: 'elicitation/create',
params: {
@ -519,10 +519,10 @@ function createMcpServer() {
description: 'Tests elicitation with enum schema improvements per SEP-1330',
inputSchema: z.object({})
},
async (_args, extra): Promise<CallToolResult> => {
async (_args, ctx): Promise<CallToolResult> => {
try {
// Request user input with all 5 enum schema variants
const result = await extra.sendRequest(
const result = await ctx.mcpReq.send(
{
method: 'elicitation/create',
params: {

Some files were not shown because too many files have changed in this diff Show more