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
6 changed files with 1224 additions and 770 deletions

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: [