refactor: replace RequestHandlerExtra with structured context types (#1467)
This commit is contained in:
parent
be5670f4fa
commit
7bb79ebbbb
6 changed files with 1224 additions and 770 deletions
|
|
@ -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');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: [
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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: [
|
||||
|
|
|
|||
Loading…
Reference in a new issue