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

View file

@ -1096,10 +1096,10 @@ test('should handle request timeout', async () => {
);
// Set up server with a delayed response
server.setRequestHandler('resources/list', async (_request, extra) => {
server.setRequestHandler('resources/list', async (_request, ctx) => {
const timer = new Promise(resolve => {
const timeout = setTimeout(resolve, 100);
extra.signal.addEventListener('abort', () => clearTimeout(timeout));
ctx.mcpReq.signal.addEventListener('abort', () => clearTimeout(timeout));
});
await timer;
@ -2269,27 +2269,27 @@ describe('Task-based execution', () => {
inputSchema: z.object({})
},
{
async createTask(_args, extra) {
const task = await extra.taskStore.createTask({
ttl: extra.taskRequestedTtl
async createTask(_args, ctx) {
const task = await ctx.task.store.createTask({
ttl: ctx.task.requestedTtl
});
const result = {
content: [{ type: 'text', text: 'Tool executed successfully!' }]
};
await extra.taskStore.storeTaskResult(task.taskId, 'completed', result);
await ctx.task.store.storeTaskResult(task.taskId, 'completed', result);
return { task };
},
async getTask(_args, extra) {
const task = await extra.taskStore.getTask(extra.taskId);
async getTask(_args, ctx) {
const task = await ctx.task.store.getTask(ctx.task.id);
if (!task) {
throw new Error(`Task ${extra.taskId} not found`);
throw new Error(`Task ${ctx.task.id} not found`);
}
return task;
},
async getTaskResult(_args, extra) {
const result = await extra.taskStore.getTaskResult(extra.taskId);
async getTaskResult(_args, ctx) {
const result = await ctx.task.store.getTaskResult(ctx.task.id);
return result as { content: Array<{ type: 'text'; text: string }> };
}
}
@ -2345,27 +2345,27 @@ describe('Task-based execution', () => {
inputSchema: z.object({})
},
{
async createTask(_args, extra) {
const task = await extra.taskStore.createTask({
ttl: extra.taskRequestedTtl
async createTask(_args, ctx) {
const task = await ctx.task.store.createTask({
ttl: ctx.task.requestedTtl
});
const result = {
content: [{ type: 'text', text: 'Success!' }]
};
await extra.taskStore.storeTaskResult(task.taskId, 'completed', result);
await ctx.task.store.storeTaskResult(task.taskId, 'completed', result);
return { task };
},
async getTask(_args, extra) {
const task = await extra.taskStore.getTask(extra.taskId);
async getTask(_args, ctx) {
const task = await ctx.task.store.getTask(ctx.task.id);
if (!task) {
throw new Error(`Task ${extra.taskId} not found`);
throw new Error(`Task ${ctx.task.id} not found`);
}
return task;
},
async getTaskResult(_args, extra) {
const result = await extra.taskStore.getTaskResult(extra.taskId);
async getTaskResult(_args, ctx) {
const result = await ctx.task.store.getTaskResult(ctx.task.id);
return result as { content: Array<{ type: 'text'; text: string }> };
}
}
@ -2422,27 +2422,27 @@ describe('Task-based execution', () => {
inputSchema: z.object({})
},
{
async createTask(_args, extra) {
const task = await extra.taskStore.createTask({
ttl: extra.taskRequestedTtl
async createTask(_args, ctx) {
const task = await ctx.task.store.createTask({
ttl: ctx.task.requestedTtl
});
const result = {
content: [{ type: 'text', text: 'Result data!' }]
};
await extra.taskStore.storeTaskResult(task.taskId, 'completed', result);
await ctx.task.store.storeTaskResult(task.taskId, 'completed', result);
return { task };
},
async getTask(_args, extra) {
const task = await extra.taskStore.getTask(extra.taskId);
async getTask(_args, ctx) {
const task = await ctx.task.store.getTask(ctx.task.id);
if (!task) {
throw new Error(`Task ${extra.taskId} not found`);
throw new Error(`Task ${ctx.task.id} not found`);
}
return task;
},
async getTaskResult(_args, extra) {
const result = await extra.taskStore.getTaskResult(extra.taskId);
async getTaskResult(_args, ctx) {
const result = await ctx.task.store.getTaskResult(ctx.task.id);
return result as { content: Array<{ type: 'text'; text: string }> };
}
}
@ -2506,27 +2506,27 @@ describe('Task-based execution', () => {
inputSchema: z.object({})
},
{
async createTask(_args, extra) {
const task = await extra.taskStore.createTask({
ttl: extra.taskRequestedTtl
async createTask(_args, ctx) {
const task = await ctx.task.store.createTask({
ttl: ctx.task.requestedTtl
});
const result = {
content: [{ type: 'text', text: 'Success!' }]
};
await extra.taskStore.storeTaskResult(task.taskId, 'completed', result);
await ctx.task.store.storeTaskResult(task.taskId, 'completed', result);
return { task };
},
async getTask(_args, extra) {
const task = await extra.taskStore.getTask(extra.taskId);
async getTask(_args, ctx) {
const task = await ctx.task.store.getTask(ctx.task.id);
if (!task) {
throw new Error(`Task ${extra.taskId} not found`);
throw new Error(`Task ${ctx.task.id} not found`);
}
return task;
},
async getTaskResult(_args, extra) {
const result = await extra.taskStore.getTaskResult(extra.taskId);
async getTaskResult(_args, ctx) {
const result = await ctx.task.store.getTaskResult(ctx.task.id);
return result as { content: Array<{ type: 'text'; text: string }> };
}
}
@ -2603,18 +2603,18 @@ describe('Task-based execution', () => {
}
);
client.setRequestHandler('elicitation/create', async (request, extra) => {
client.setRequestHandler('elicitation/create', async (request, ctx) => {
const result = {
action: 'accept',
content: { username: 'list-user' }
};
// Check if task creation is requested
if (request.params.task && extra.taskStore) {
const task = await extra.taskStore.createTask({
ttl: extra.taskRequestedTtl
if (request.params.task && ctx.task?.store) {
const task = await ctx.task.store.createTask({
ttl: ctx.task.requestedTtl
});
await extra.taskStore.storeTaskResult(task.taskId, 'completed', result);
await ctx.task.store.storeTaskResult(task.taskId, 'completed', result);
// Return CreateTaskResult when task creation is requested
return { task };
}
@ -2696,18 +2696,18 @@ describe('Task-based execution', () => {
}
);
client.setRequestHandler('elicitation/create', async (request, extra) => {
client.setRequestHandler('elicitation/create', async (request, ctx) => {
const result = {
action: 'accept',
content: { username: 'list-user' }
};
// Check if task creation is requested
if (request.params.task && extra.taskStore) {
const task = await extra.taskStore.createTask({
ttl: extra.taskRequestedTtl
if (request.params.task && ctx.task?.store) {
const task = await ctx.task.store.createTask({
ttl: ctx.task.requestedTtl
});
await extra.taskStore.storeTaskResult(task.taskId, 'completed', result);
await ctx.task.store.storeTaskResult(task.taskId, 'completed', result);
// Return CreateTaskResult when task creation is requested
return { task };
}
@ -2788,18 +2788,18 @@ describe('Task-based execution', () => {
}
);
client.setRequestHandler('elicitation/create', async (request, extra) => {
client.setRequestHandler('elicitation/create', async (request, ctx) => {
const result = {
action: 'accept',
content: { username: 'result-user' }
};
// Check if task creation is requested
if (request.params.task && extra.taskStore) {
const task = await extra.taskStore.createTask({
ttl: extra.taskRequestedTtl
if (request.params.task && ctx.task?.store) {
const task = await ctx.task.store.createTask({
ttl: ctx.task.requestedTtl
});
await extra.taskStore.storeTaskResult(task.taskId, 'completed', result);
await ctx.task.store.storeTaskResult(task.taskId, 'completed', result);
// Return CreateTaskResult when task creation is requested
return { task };
}
@ -2879,18 +2879,18 @@ describe('Task-based execution', () => {
}
);
client.setRequestHandler('elicitation/create', async (request, extra) => {
client.setRequestHandler('elicitation/create', async (request, ctx) => {
const result = {
action: 'accept',
content: { username: 'list-user' }
};
// Check if task creation is requested
if (request.params.task && extra.taskStore) {
const task = await extra.taskStore.createTask({
ttl: extra.taskRequestedTtl
if (request.params.task && ctx.task?.store) {
const task = await ctx.task.store.createTask({
ttl: ctx.task.requestedTtl
});
await extra.taskStore.storeTaskResult(task.taskId, 'completed', result);
await ctx.task.store.storeTaskResult(task.taskId, 'completed', result);
// Return CreateTaskResult when task creation is requested
return { task };
}
@ -2991,27 +2991,27 @@ describe('Task-based execution', () => {
})
},
{
async createTask({ id }, extra) {
const task = await extra.taskStore.createTask({
ttl: extra.taskRequestedTtl
async createTask({ id }, ctx) {
const task = await ctx.task.store.createTask({
ttl: ctx.task.requestedTtl
});
const result = {
content: [{ type: 'text', text: `Result for ${id || 'unknown'}` }]
};
await extra.taskStore.storeTaskResult(task.taskId, 'completed', result);
await ctx.task.store.storeTaskResult(task.taskId, 'completed', result);
return { task };
},
async getTask(_args, extra) {
const task = await extra.taskStore.getTask(extra.taskId);
async getTask(_args, ctx) {
const task = await ctx.task.store.getTask(ctx.task.id);
if (!task) {
throw new Error(`Task ${extra.taskId} not found`);
throw new Error(`Task ${ctx.task.id} not found`);
}
return task;
},
async getTaskResult(_args, extra) {
const result = await extra.taskStore.getTaskResult(extra.taskId);
async getTaskResult(_args, ctx) {
const result = await ctx.task.store.getTaskResult(ctx.task.id);
return result as { content: Array<{ type: 'text'; text: string }> };
}
}
@ -3259,27 +3259,27 @@ test('should respect server task capabilities', async () => {
inputSchema: z.object({})
},
{
async createTask(_args, extra) {
const task = await extra.taskStore.createTask({
ttl: extra.taskRequestedTtl
async createTask(_args, ctx) {
const task = await ctx.task.store.createTask({
ttl: ctx.task.requestedTtl
});
const result = {
content: [{ type: 'text', text: 'Success!' }]
};
await extra.taskStore.storeTaskResult(task.taskId, 'completed', result);
await ctx.task.store.storeTaskResult(task.taskId, 'completed', result);
return { task };
},
async getTask(_args, extra) {
const task = await extra.taskStore.getTask(extra.taskId);
async getTask(_args, ctx) {
const task = await ctx.task.store.getTask(ctx.task.id);
if (!task) {
throw new Error(`Task ${extra.taskId} not found`);
throw new Error(`Task ${ctx.task.id} not found`);
}
return task;
},
async getTaskResult(_args, extra) {
const result = await extra.taskStore.getTaskResult(extra.taskId);
async getTaskResult(_args, ctx) {
const result = await ctx.task.store.getTaskResult(ctx.task.id);
return result as { content: Array<{ type: 'text'; text: string }> };
}
}

View file

@ -1386,12 +1386,12 @@ test('should handle request timeout', async () => {
}
);
client.setRequestHandler('sampling/createMessage', async (_request, extra) => {
client.setRequestHandler('sampling/createMessage', async (_request, ctx) => {
await new Promise((resolve, reject) => {
const timeout = setTimeout(resolve, 100);
extra.signal.addEventListener('abort', () => {
ctx.mcpReq.signal.addEventListener('abort', () => {
clearTimeout(timeout);
reject(extra.signal.reason);
reject(ctx.mcpReq.signal.reason);
});
});
@ -2146,9 +2146,9 @@ describe('Task-based execution', () => {
inputSchema: z.object({})
},
{
async createTask(_args, extra) {
const task = await extra.taskStore.createTask({
ttl: extra.taskRequestedTtl
async createTask(_args, ctx) {
const task = await ctx.task.store.createTask({
ttl: ctx.task.requestedTtl
});
// Simulate some async work
@ -2157,20 +2157,20 @@ describe('Task-based execution', () => {
const result = {
content: [{ type: 'text', text: 'Tool executed successfully!' }]
};
await extra.taskStore.storeTaskResult(task.taskId, 'completed', result);
await ctx.task.store.storeTaskResult(task.taskId, 'completed', result);
})();
return { task };
},
async getTask(_args, extra) {
const task = await extra.taskStore.getTask(extra.taskId);
async getTask(_args, ctx) {
const task = await ctx.task.store.getTask(ctx.task.id);
if (!task) {
throw new Error(`Task ${extra.taskId} not found`);
throw new Error(`Task ${ctx.task.id} not found`);
}
return task;
},
async getTaskResult(_args, extra) {
const result = await extra.taskStore.getTaskResult(extra.taskId);
async getTaskResult(_args, ctx) {
const result = await ctx.task.store.getTaskResult(ctx.task.id);
return result as { content: Array<{ type: 'text'; text: string }> };
}
}
@ -2341,13 +2341,13 @@ describe('Task-based execution', () => {
let capturedElicitRequest: z.infer<typeof ElicitRequestSchema> | null = null;
// Set up client elicitation handler
client.setRequestHandler('elicitation/create', async (request, extra) => {
client.setRequestHandler('elicitation/create', async (request, ctx) => {
let taskId: string | undefined;
// Check if task creation is requested
if (request.params.task && extra.taskStore) {
const createdTask = await extra.taskStore.createTask({
ttl: extra.taskRequestedTtl
if (request.params.task && ctx.task?.store) {
const createdTask = await ctx.task.store.createTask({
ttl: ctx.task.requestedTtl
});
taskId = createdTask.taskId;
}
@ -2371,15 +2371,15 @@ describe('Task-based execution', () => {
inputSchema: z.object({})
},
{
async createTask(_args, extra) {
const task = await extra.taskStore.createTask({
ttl: extra.taskRequestedTtl
async createTask(_args, ctx) {
const task = await ctx.task.store.createTask({
ttl: ctx.task.requestedTtl
});
// Perform async work that makes a nested request
(async () => {
// During tool execution, make a nested request to the client using extra.sendRequest
const elicitResult = await extra.sendRequest(
// During tool execution, make a nested request to the client using ctx.mcpReq.send
const elicitResult = await ctx.mcpReq.send(
{
method: 'elicitation/create',
params: {
@ -2405,20 +2405,20 @@ describe('Task-based execution', () => {
}
]
};
await extra.taskStore.storeTaskResult(task.taskId, 'completed', result);
await ctx.task.store.storeTaskResult(task.taskId, 'completed', result);
})();
return { task };
},
async getTask(_args, extra) {
const task = await extra.taskStore.getTask(extra.taskId);
async getTask(_args, ctx) {
const task = await ctx.task.store.getTask(ctx.task.id);
if (!task) {
throw new Error(`Task ${extra.taskId} not found`);
throw new Error(`Task ${ctx.task.id} not found`);
}
return task;
},
async getTaskResult(_args, extra) {
const result = await extra.taskStore.getTaskResult(extra.taskId);
async getTaskResult(_args, ctx) {
const result = await ctx.task.store.getTaskResult(ctx.task.id);
return result as { content: Array<{ type: 'text'; text: string }> };
}
}
@ -2498,18 +2498,18 @@ describe('Task-based execution', () => {
}
);
client.setRequestHandler('elicitation/create', async (request, extra) => {
client.setRequestHandler('elicitation/create', async (request, ctx) => {
const result = {
action: 'accept',
content: { username: 'server-test-user', confirmed: true }
};
// Check if task creation is requested
if (request.params.task && extra.taskStore) {
const task = await extra.taskStore.createTask({
ttl: extra.taskRequestedTtl
if (request.params.task && ctx.task?.store) {
const task = await ctx.task.store.createTask({
ttl: ctx.task.requestedTtl
});
await extra.taskStore.storeTaskResult(task.taskId, 'completed', result);
await ctx.task.store.storeTaskResult(task.taskId, 'completed', result);
// Return CreateTaskResult when task creation is requested
return { task };
}
@ -2579,18 +2579,18 @@ describe('Task-based execution', () => {
}
);
client.setRequestHandler('elicitation/create', async (request, extra) => {
client.setRequestHandler('elicitation/create', async (request, ctx) => {
const result = {
action: 'accept',
content: { username: 'list-user' }
};
// Check if task creation is requested
if (request.params.task && extra.taskStore) {
const task = await extra.taskStore.createTask({
ttl: extra.taskRequestedTtl
if (request.params.task && ctx.task?.store) {
const task = await ctx.task.store.createTask({
ttl: ctx.task.requestedTtl
});
await extra.taskStore.storeTaskResult(task.taskId, 'completed', result);
await ctx.task.store.storeTaskResult(task.taskId, 'completed', result);
// Return CreateTaskResult when task creation is requested
return { task };
}
@ -2658,18 +2658,18 @@ describe('Task-based execution', () => {
}
);
client.setRequestHandler('elicitation/create', async (request, extra) => {
client.setRequestHandler('elicitation/create', async (request, ctx) => {
const result = {
action: 'accept',
content: { username: 'result-user', confirmed: true }
};
// Check if task creation is requested
if (request.params.task && extra.taskStore) {
const task = await extra.taskStore.createTask({
ttl: extra.taskRequestedTtl
if (request.params.task && ctx.task?.store) {
const task = await ctx.task.store.createTask({
ttl: ctx.task.requestedTtl
});
await extra.taskStore.storeTaskResult(task.taskId, 'completed', result);
await ctx.task.store.storeTaskResult(task.taskId, 'completed', result);
// Return CreateTaskResult when task creation is requested
return { task };
}
@ -2739,18 +2739,18 @@ describe('Task-based execution', () => {
}
);
client.setRequestHandler('elicitation/create', async (request, extra) => {
client.setRequestHandler('elicitation/create', async (request, ctx) => {
const result = {
action: 'accept',
content: { username: 'list-user' }
};
// Check if task creation is requested
if (request.params.task && extra.taskStore) {
const task = await extra.taskStore.createTask({
ttl: extra.taskRequestedTtl
if (request.params.task && ctx.task?.store) {
const task = await ctx.task.store.createTask({
ttl: ctx.task.requestedTtl
});
await extra.taskStore.storeTaskResult(task.taskId, 'completed', result);
await ctx.task.store.storeTaskResult(task.taskId, 'completed', result);
// Return CreateTaskResult when task creation is requested
return { task };
}
@ -2853,9 +2853,9 @@ describe('Task-based execution', () => {
})
},
{
async createTask({ delay, taskNum }, extra) {
const task = await extra.taskStore.createTask({
ttl: extra.taskRequestedTtl
async createTask({ delay, taskNum }, ctx) {
const task = await ctx.task.store.createTask({
ttl: ctx.task.requestedTtl
});
// Simulate async work
@ -2864,20 +2864,20 @@ describe('Task-based execution', () => {
const result = {
content: [{ type: 'text', text: `Completed task ${taskNum || 'unknown'}` }]
};
await extra.taskStore.storeTaskResult(task.taskId, 'completed', result);
await ctx.task.store.storeTaskResult(task.taskId, 'completed', result);
})();
return { task };
},
async getTask(_args, extra) {
const task = await extra.taskStore.getTask(extra.taskId);
async getTask(_args, ctx) {
const task = await ctx.task.store.getTask(ctx.task.id);
if (!task) {
throw new Error(`Task ${extra.taskId} not found`);
throw new Error(`Task ${ctx.task.id} not found`);
}
return task;
},
async getTaskResult(_args, extra) {
const result = await extra.taskStore.getTaskResult(extra.taskId);
async getTaskResult(_args, ctx) {
const result = await ctx.task.store.getTaskResult(ctx.task.id);
return result as { content: Array<{ type: 'text'; text: string }> };
}
}
@ -3082,18 +3082,18 @@ test('should respect client task capabilities', async () => {
}
);
client.setRequestHandler('elicitation/create', async (request, extra) => {
client.setRequestHandler('elicitation/create', async (request, ctx) => {
const result = {
action: 'accept',
content: { username: 'test-user' }
};
// Check if task creation is requested
if (request.params.task && extra.taskStore) {
const task = await extra.taskStore.createTask({
ttl: extra.taskRequestedTtl
if (request.params.task && ctx.task?.store) {
const task = await ctx.task.store.createTask({
ttl: ctx.task.requestedTtl
});
await extra.taskStore.storeTaskResult(task.taskId, 'completed', result);
await ctx.task.store.storeTaskResult(task.taskId, 'completed', result);
// Return CreateTaskResult when task creation is requested
return { task };
}

View file

@ -94,6 +94,159 @@ describe('Zod v4', () => {
]);
});
/***
* Test: ctx.mcpReq.log convenience method
*/
test('should send logging messages via ctx.mcpReq.log() convenience method', async () => {
const mcpServer = new McpServer(
{
name: 'test server',
version: '1.0'
},
{ capabilities: { logging: {} } }
);
const notifications: Notification[] = [];
const client = new Client({
name: 'test client',
version: '1.0'
});
client.fallbackNotificationHandler = async notification => {
notifications.push(notification);
};
mcpServer.registerTool(
'log-test',
{
description: 'A tool that logs via ctx.mcpReq.log()'
},
async ctx => {
await ctx.mcpReq.log('info', 'Log from convenience method', 'test-logger');
return {
content: [{ type: 'text' as const, text: 'done' }]
};
}
);
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await Promise.all([client.connect(clientTransport), mcpServer.server.connect(serverTransport)]);
await client.callTool({ name: 'log-test' });
expect(notifications).toMatchObject([
{
method: 'notifications/message',
params: {
level: 'info',
data: 'Log from convenience method',
logger: 'test-logger'
}
}
]);
});
/***
* Test: ctx.mcpReq.elicitInput convenience method
*/
test('should elicit input via ctx.mcpReq.elicitInput() convenience method', async () => {
const mcpServer = new McpServer({
name: 'test server',
version: '1.0'
});
let elicitResult: unknown = null;
mcpServer.registerTool(
'elicit-test',
{
description: 'A tool that elicits input via ctx.mcpReq.elicitInput()'
},
async ctx => {
elicitResult = await ctx.mcpReq.elicitInput({
message: 'Please confirm',
requestedSchema: {
type: 'object',
properties: {
confirmed: { type: 'boolean' }
}
}
});
return {
content: [{ type: 'text' as const, text: 'done' }]
};
}
);
const client = new Client({ name: 'test client', version: '1.0' }, { capabilities: { elicitation: {} } });
client.setRequestHandler('elicitation/create', async () => ({
action: 'accept',
content: { confirmed: true }
}));
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await Promise.all([client.connect(clientTransport), mcpServer.server.connect(serverTransport)]);
await client.callTool({ name: 'elicit-test' });
expect(elicitResult).toMatchObject({
action: 'accept',
content: { confirmed: true }
});
});
/***
* Test: ctx.mcpReq.requestSampling convenience method
*/
test('should request sampling via ctx.mcpReq.requestSampling() convenience method', async () => {
const mcpServer = new McpServer({
name: 'test server',
version: '1.0'
});
let samplingResult: unknown = null;
mcpServer.registerTool(
'sampling-test',
{
description: 'A tool that requests sampling via ctx.mcpReq.requestSampling()'
},
async ctx => {
samplingResult = await ctx.mcpReq.requestSampling({
messages: [
{
role: 'user',
content: { type: 'text', text: 'Hello' }
}
],
maxTokens: 100
});
return {
content: [{ type: 'text' as const, text: 'done' }]
};
}
);
const client = new Client({ name: 'test client', version: '1.0' }, { capabilities: { sampling: {} } });
client.setRequestHandler('sampling/createMessage', async () => ({
model: 'test-model',
role: 'assistant' as const,
content: { type: 'text' as const, text: 'Hello back' }
}));
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await Promise.all([client.connect(clientTransport), mcpServer.server.connect(serverTransport)]);
await client.callTool({ name: 'sampling-test' });
expect(samplingResult).toMatchObject({
model: 'test-model',
role: 'assistant',
content: { type: 'text', text: 'Hello back' }
});
});
/***
* Test: Progress Notification with Message Field
*/
@ -112,13 +265,13 @@ describe('Zod v4', () => {
steps: z.number().min(1).describe('Number of steps to perform')
})
},
async ({ steps }, { sendNotification, _meta }) => {
const progressToken = _meta?.progressToken;
async ({ steps }, ctx) => {
const progressToken = ctx.mcpReq._meta?.progressToken;
if (progressToken) {
// Send progress notification for each step
for (let i = 1; i <= steps; i++) {
await sendNotification({
await ctx.mcpReq.notify({
method: 'notifications/progress',
params: {
progressToken,
@ -1320,7 +1473,7 @@ describe('Zod v4', () => {
/***
* Test: Pass Session ID to Tool Callback
*/
test('should pass sessionId to tool callback via RequestHandlerExtra', async () => {
test('should pass sessionId to tool callback via ServerContext', async () => {
const mcpServer = new McpServer({
name: 'test server',
version: '1.0'
@ -1332,8 +1485,8 @@ describe('Zod v4', () => {
});
let receivedSessionId: string | undefined;
mcpServer.registerTool('test-tool', {}, async extra => {
receivedSessionId = extra.sessionId;
mcpServer.registerTool('test-tool', {}, async ctx => {
receivedSessionId = ctx.sessionId;
return {
content: [
{
@ -1366,7 +1519,7 @@ describe('Zod v4', () => {
/***
* Test: Pass Request ID to Tool Callback
*/
test('should pass requestId to tool callback via RequestHandlerExtra', async () => {
test('should pass requestId to tool callback via ServerContext', async () => {
const mcpServer = new McpServer({
name: 'test server',
version: '1.0'
@ -1378,13 +1531,13 @@ describe('Zod v4', () => {
});
let receivedRequestId: string | number | undefined;
mcpServer.registerTool('request-id-test', {}, async extra => {
receivedRequestId = extra.requestId;
mcpServer.registerTool('request-id-test', {}, async ctx => {
receivedRequestId = ctx.mcpReq.id;
return {
content: [
{
type: 'text',
text: `Received request ID: ${extra.requestId}`
text: `Received request ID: ${ctx.mcpReq.id}`
}
]
};
@ -1440,8 +1593,8 @@ describe('Zod v4', () => {
receivedLogMessage = notification.params.data as string;
});
mcpServer.registerTool('test-tool', {}, async extra => {
await extra.sendNotification({
mcpServer.registerTool('test-tool', {}, async ctx => {
await ctx.mcpReq.notify({
method: 'notifications/message',
params: { level: 'debug', data: loggingMessage }
});
@ -1787,17 +1940,17 @@ describe('Zod v4', () => {
}
},
{
createTask: async (_args, extra) => {
const task = await extra.taskStore.createTask({ ttl: 60_000 });
createTask: async (_args, ctx) => {
const task = await ctx.task.store.createTask({ ttl: 60_000 });
return { task };
},
getTask: async (_args, extra) => {
const task = await extra.taskStore.getTask(extra.taskId);
getTask: async (_args, ctx) => {
const task = await ctx.task.store.getTask(ctx.task.id);
if (!task) throw new Error('Task not found');
return task;
},
getTaskResult: async (_args, extra) => {
return (await extra.taskStore.getTaskResult(extra.taskId)) as CallToolResult;
getTaskResult: async (_args, ctx) => {
return (await ctx.task.store.getTaskResult(ctx.task.id)) as CallToolResult;
}
}
);
@ -1856,17 +2009,17 @@ describe('Zod v4', () => {
}
},
{
createTask: async (_args, extra) => {
const task = await extra.taskStore.createTask({ ttl: 60_000 });
createTask: async (_args, ctx) => {
const task = await ctx.task.store.createTask({ ttl: 60_000 });
return { task };
},
getTask: async (_args, extra) => {
const task = await extra.taskStore.getTask(extra.taskId);
getTask: async (_args, ctx) => {
const task = await ctx.task.store.getTask(ctx.task.id);
if (!task) throw new Error('Task not found');
return task;
},
getTaskResult: async (_args, extra) => {
return (await extra.taskStore.getTaskResult(extra.taskId)) as CallToolResult;
getTaskResult: async (_args, ctx) => {
return (await ctx.task.store.getTaskResult(ctx.task.id)) as CallToolResult;
}
}
);
@ -2841,7 +2994,7 @@ describe('Zod v4', () => {
/***
* Test: Pass Request ID to Resource Callback
*/
test('should pass requestId to resource callback via RequestHandlerExtra', async () => {
test('should pass requestId to resource callback via ServerContext', async () => {
const mcpServer = new McpServer({
name: 'test server',
version: '1.0'
@ -2853,13 +3006,13 @@ describe('Zod v4', () => {
});
let receivedRequestId: string | number | undefined;
mcpServer.registerResource('request-id-test', 'test://resource', {}, async (_uri, extra) => {
receivedRequestId = extra.requestId;
mcpServer.registerResource('request-id-test', 'test://resource', {}, async (_uri, ctx) => {
receivedRequestId = ctx.mcpReq.id;
return {
contents: [
{
uri: 'test://resource',
text: `Received request ID: ${extra.requestId}`
text: `Received request ID: ${ctx.mcpReq.id}`
}
]
};
@ -3779,7 +3932,7 @@ describe('Zod v4', () => {
/***
* Test: Pass Request ID to Prompt Callback
*/
test('should pass requestId to prompt callback via RequestHandlerExtra', async () => {
test('should pass requestId to prompt callback via ServerContext', async () => {
const mcpServer = new McpServer({
name: 'test server',
version: '1.0'
@ -3791,15 +3944,15 @@ describe('Zod v4', () => {
});
let receivedRequestId: string | number | undefined;
mcpServer.registerPrompt('request-id-test', {}, async extra => {
receivedRequestId = extra.requestId;
mcpServer.registerPrompt('request-id-test', {}, async ctx => {
receivedRequestId = ctx.mcpReq.id;
return {
messages: [
{
role: 'assistant',
content: {
type: 'text',
text: `Received request ID: ${extra.requestId}`
text: `Received request ID: ${ctx.mcpReq.id}`
}
}
]
@ -6219,11 +6372,11 @@ describe('Zod v4', () => {
}
},
{
createTask: async ({ input }, extra) => {
const task = await extra.taskStore.createTask({ ttl: 60_000, pollInterval: 100 });
createTask: async ({ input }, ctx) => {
const task = await ctx.task.store.createTask({ ttl: 60_000, pollInterval: 100 });
// Capture taskStore for use in setTimeout
const store = extra.taskStore;
const store = ctx.task.store;
// Simulate async work
setTimeout(async () => {
@ -6234,15 +6387,15 @@ describe('Zod v4', () => {
return { task };
},
getTask: async (_args, extra) => {
const task = await extra.taskStore.getTask(extra.taskId);
getTask: async (_args, ctx) => {
const task = await ctx.task.store.getTask(ctx.task.id);
if (!task) {
throw new Error('Task not found');
}
return task;
},
getTaskResult: async (_input, extra) => {
const result = await extra.taskStore.getTaskResult(extra.taskId);
getTaskResult: async (_input, ctx) => {
const result = await ctx.task.store.getTaskResult(ctx.task.id);
return result as CallToolResult;
}
}
@ -6324,11 +6477,11 @@ describe('Zod v4', () => {
}
},
{
createTask: async ({ value }, extra) => {
const task = await extra.taskStore.createTask({ ttl: 60_000, pollInterval: 100 });
createTask: async ({ value }, ctx) => {
const task = await ctx.task.store.createTask({ ttl: 60_000, pollInterval: 100 });
// Capture taskStore for use in setTimeout
const store = extra.taskStore;
const store = ctx.task.store;
// Simulate async work
setTimeout(async () => {
@ -6340,15 +6493,15 @@ describe('Zod v4', () => {
return { task };
},
getTask: async (_args, extra) => {
const task = await extra.taskStore.getTask(extra.taskId);
getTask: async (_args, ctx) => {
const task = await ctx.task.store.getTask(ctx.task.id);
if (!task) {
throw new Error('Task not found');
}
return task;
},
getTaskResult: async (_value, extra) => {
const result = await extra.taskStore.getTaskResult(extra.taskId);
getTaskResult: async (_value, ctx) => {
const result = await ctx.task.store.getTaskResult(ctx.task.id);
return result as CallToolResult;
}
}
@ -6432,11 +6585,11 @@ describe('Zod v4', () => {
}
},
{
createTask: async ({ data }, extra) => {
const task = await extra.taskStore.createTask({ ttl: 60_000, pollInterval: 100 });
createTask: async ({ data }, ctx) => {
const task = await ctx.task.store.createTask({ ttl: 60_000, pollInterval: 100 });
// Capture taskStore for use in setTimeout
const store = extra.taskStore;
const store = ctx.task.store;
// Simulate async work
setTimeout(async () => {
@ -6448,15 +6601,15 @@ describe('Zod v4', () => {
return { task };
},
getTask: async (_args, extra) => {
const task = await extra.taskStore.getTask(extra.taskId);
getTask: async (_args, ctx) => {
const task = await ctx.task.store.getTask(ctx.task.id);
if (!task) {
throw new Error('Task not found');
}
return task;
},
getTaskResult: async (_data, extra) => {
const result = await extra.taskStore.getTaskResult(extra.taskId);
getTaskResult: async (_data, ctx) => {
const result = await ctx.task.store.getTaskResult(ctx.task.id);
return result as CallToolResult;
}
}
@ -6549,11 +6702,11 @@ describe('Zod v4', () => {
}
},
{
createTask: async extra => {
const task = await extra.taskStore.createTask({ ttl: 60_000, pollInterval: 100 });
createTask: async ctx => {
const task = await ctx.task.store.createTask({ ttl: 60_000, pollInterval: 100 });
// Capture taskStore for use in setTimeout
const store = extra.taskStore;
const store = ctx.task.store;
// Simulate async failure
setTimeout(async () => {
@ -6566,15 +6719,15 @@ describe('Zod v4', () => {
return { task };
},
getTask: async extra => {
const task = await extra.taskStore.getTask(extra.taskId);
getTask: async ctx => {
const task = await ctx.task.store.getTask(ctx.task.id);
if (!task) {
throw new Error('Task not found');
}
return task;
},
getTaskResult: async extra => {
const result = await extra.taskStore.getTaskResult(extra.taskId);
getTaskResult: async ctx => {
const result = await ctx.task.store.getTaskResult(ctx.task.id);
return result as CallToolResult;
}
}
@ -6655,11 +6808,11 @@ describe('Zod v4', () => {
}
},
{
createTask: async extra => {
const task = await extra.taskStore.createTask({ ttl: 60_000, pollInterval: 100 });
createTask: async ctx => {
const task = await ctx.task.store.createTask({ ttl: 60_000, pollInterval: 100 });
// Capture taskStore for use in setTimeout
const store = extra.taskStore;
const store = ctx.task.store;
// Simulate async cancellation
setTimeout(async () => {
@ -6669,15 +6822,15 @@ describe('Zod v4', () => {
return { task };
},
getTask: async extra => {
const task = await extra.taskStore.getTask(extra.taskId);
getTask: async ctx => {
const task = await ctx.task.store.getTask(ctx.task.id);
if (!task) {
throw new Error('Task not found');
}
return task;
},
getTaskResult: async extra => {
const result = await extra.taskStore.getTaskResult(extra.taskId);
getTaskResult: async ctx => {
const result = await ctx.task.store.getTaskResult(ctx.task.id);
return result as CallToolResult;
}
}
@ -6742,19 +6895,19 @@ describe('Zod v4', () => {
}
},
{
createTask: async (_args, extra) => {
const task = await extra.taskStore.createTask({ ttl: 60_000, pollInterval: 100 });
createTask: async (_args, ctx) => {
const task = await ctx.task.store.createTask({ ttl: 60_000, pollInterval: 100 });
return { task };
},
getTask: async (_args, extra) => {
const task = await extra.taskStore.getTask(extra.taskId);
getTask: async (_args, ctx) => {
const task = await ctx.task.store.getTask(ctx.task.id);
if (!task) {
throw new Error('Task not found');
}
return task;
},
getTaskResult: async (_args, extra) => {
const result = await extra.taskStore.getTaskResult(extra.taskId);
getTaskResult: async (_args, ctx) => {
const result = await ctx.task.store.getTaskResult(ctx.task.id);
return result as CallToolResult;
}
}

View file

@ -63,8 +63,8 @@ describe('Task Lifecycle Integration Tests', () => {
})
},
{
async createTask({ duration, shouldFail }, extra) {
const task = await extra.taskStore.createTask({
async createTask({ duration, shouldFail }, ctx) {
const task = await ctx.task.store.createTask({
ttl: 60_000,
pollInterval: 100
});
@ -75,11 +75,11 @@ describe('Task Lifecycle Integration Tests', () => {
try {
await (shouldFail
? extra.taskStore.storeTaskResult(task.taskId, 'failed', {
? ctx.task.store.storeTaskResult(task.taskId, 'failed', {
content: [{ type: 'text', text: 'Task failed as requested' }],
isError: true
})
: extra.taskStore.storeTaskResult(task.taskId, 'completed', {
: ctx.task.store.storeTaskResult(task.taskId, 'completed', {
content: [{ type: 'text', text: `Completed after ${duration}ms` }]
}));
} catch {
@ -89,15 +89,15 @@ describe('Task Lifecycle Integration Tests', () => {
return { task };
},
async getTask(_args, extra) {
const task = await extra.taskStore.getTask(extra.taskId);
async getTask(_args, ctx) {
const task = await ctx.task.store.getTask(ctx.task.id);
if (!task) {
throw new Error(`Task ${extra.taskId} not found`);
throw new Error(`Task ${ctx.task.id} not found`);
}
return task;
},
async getTaskResult(_args, extra) {
const result = await extra.taskStore.getTaskResult(extra.taskId);
async getTaskResult(_args, ctx) {
const result = await ctx.task.store.getTaskResult(ctx.task.id);
return result as { content: Array<{ type: 'text'; text: string }> };
}
}
@ -114,8 +114,8 @@ describe('Task Lifecycle Integration Tests', () => {
})
},
{
async createTask({ userName }, extra) {
const task = await extra.taskStore.createTask({
async createTask({ userName }, ctx) {
const task = await ctx.task.store.createTask({
ttl: 60_000,
pollInterval: 100
});
@ -128,14 +128,14 @@ describe('Task Lifecycle Integration Tests', () => {
if (userName) {
// Complete immediately if userName was provided
try {
await extra.taskStore.storeTaskResult(task.taskId, 'completed', {
await ctx.task.store.storeTaskResult(task.taskId, 'completed', {
content: [{ type: 'text', text: `Hello, ${userName}!` }]
});
} catch {
// Task may have been cleaned up if test ended
}
} else {
const elicitationResult = await extra.sendRequest(
const elicitationResult = await ctx.mcpReq.send(
{
method: 'elicitation/create',
params: {
@ -160,7 +160,7 @@ describe('Task Lifecycle Integration Tests', () => {
? elicitationResult.content.userName
: 'Unknown';
try {
await extra.taskStore.storeTaskResult(task.taskId, 'completed', {
await ctx.task.store.storeTaskResult(task.taskId, 'completed', {
content: [{ type: 'text', text: `Hello, ${name}!` }]
});
} catch {
@ -171,15 +171,15 @@ describe('Task Lifecycle Integration Tests', () => {
return { task };
},
async getTask(_args, extra) {
const task = await extra.taskStore.getTask(extra.taskId);
async getTask(_args, ctx) {
const task = await ctx.task.store.getTask(ctx.task.id);
if (!task) {
throw new Error(`Task ${extra.taskId} not found`);
throw new Error(`Task ${ctx.task.id} not found`);
}
return task;
},
async getTaskResult(_args, extra) {
const result = await extra.taskStore.getTaskResult(extra.taskId);
async getTaskResult(_args, ctx) {
const result = await ctx.task.store.getTaskResult(ctx.task.id);
return result as { content: Array<{ type: 'text'; text: string }> };
}
}
@ -418,8 +418,8 @@ describe('Task Lifecycle Integration Tests', () => {
})
},
{
async createTask({ requestCount }, extra) {
const task = await extra.taskStore.createTask({
async createTask({ requestCount }, ctx) {
const task = await ctx.task.store.createTask({
ttl: 60_000,
pollInterval: 100
});
@ -432,7 +432,7 @@ describe('Task Lifecycle Integration Tests', () => {
// Send multiple elicitation requests
for (let i = 0; i < requestCount; i++) {
const elicitationResult = await extra.sendRequest(
const elicitationResult = await ctx.mcpReq.send(
{
method: 'elicitation/create',
params: {
@ -458,7 +458,7 @@ describe('Task Lifecycle Integration Tests', () => {
// Complete with all responses
try {
await extra.taskStore.storeTaskResult(task.taskId, 'completed', {
await ctx.task.store.storeTaskResult(task.taskId, 'completed', {
content: [{ type: 'text', text: `Received responses: ${responses.join(', ')}` }]
});
} catch {
@ -468,15 +468,15 @@ describe('Task Lifecycle Integration Tests', () => {
return { task };
},
async getTask(_args, extra) {
const task = await extra.taskStore.getTask(extra.taskId);
async getTask(_args, ctx) {
const task = await ctx.task.store.getTask(ctx.task.id);
if (!task) {
throw new Error(`Task ${extra.taskId} not found`);
throw new Error(`Task ${ctx.task.id} not found`);
}
return task;
},
async getTaskResult(_args, extra) {
const result = await extra.taskStore.getTaskResult(extra.taskId);
async getTaskResult(_args, ctx) {
const result = await ctx.task.store.getTaskResult(ctx.task.id);
return result as { content: Array<{ type: 'text'; text: string }> };
}
}
@ -907,8 +907,8 @@ describe('Task Lifecycle Integration Tests', () => {
})
},
{
async createTask({ messageCount }, extra) {
const task = await extra.taskStore.createTask({
async createTask({ messageCount }, ctx) {
const task = await ctx.task.store.createTask({
ttl: 60_000,
pollInterval: 100
});
@ -921,8 +921,8 @@ describe('Task Lifecycle Integration Tests', () => {
// Queue multiple elicitation requests
for (let i = 0; i < messageCount; i++) {
// Send request but don't await - let it queue
extra
.sendRequest(
ctx.mcpReq
.send(
{
method: 'elicitation/create',
params: {
@ -957,15 +957,15 @@ describe('Task Lifecycle Integration Tests', () => {
return { task };
},
async getTask(_args, extra) {
const task = await extra.taskStore.getTask(extra.taskId);
async getTask(_args, ctx) {
const task = await ctx.task.store.getTask(ctx.task.id);
if (!task) {
throw new Error(`Task ${extra.taskId} not found`);
throw new Error(`Task ${ctx.task.id} not found`);
}
return task;
},
async getTaskResult(_args, extra) {
const result = await extra.taskStore.getTaskResult(extra.taskId);
async getTaskResult(_args, ctx) {
const result = await ctx.task.store.getTaskResult(ctx.task.id);
return result as { content: Array<{ type: 'text'; text: string }> };
}
}
@ -1105,8 +1105,8 @@ describe('Task Lifecycle Integration Tests', () => {
})
},
{
async createTask({ messageCount, delayBetweenMessages }, extra) {
const task = await extra.taskStore.createTask({
async createTask({ messageCount, delayBetweenMessages }, ctx) {
const task = await ctx.task.store.createTask({
ttl: 60_000,
pollInterval: 100
});
@ -1121,7 +1121,7 @@ describe('Task Lifecycle Integration Tests', () => {
// Send messages with delays between them
for (let i = 0; i < messageCount; i++) {
const elicitationResult = await extra.sendRequest(
const elicitationResult = await ctx.mcpReq.send(
{
method: 'elicitation/create',
params: {
@ -1152,7 +1152,7 @@ describe('Task Lifecycle Integration Tests', () => {
// Complete with all responses
try {
await extra.taskStore.storeTaskResult(task.taskId, 'completed', {
await ctx.task.store.storeTaskResult(task.taskId, 'completed', {
content: [{ type: 'text', text: `Received all responses: ${responses.join(', ')}` }]
});
} catch {
@ -1161,7 +1161,7 @@ describe('Task Lifecycle Integration Tests', () => {
} catch (error) {
// Handle errors
try {
await extra.taskStore.storeTaskResult(task.taskId, 'failed', {
await ctx.task.store.storeTaskResult(task.taskId, 'failed', {
content: [{ type: 'text', text: `Error: ${error}` }],
isError: true
});
@ -1173,15 +1173,15 @@ describe('Task Lifecycle Integration Tests', () => {
return { task };
},
async getTask(_args, extra) {
const task = await extra.taskStore.getTask(extra.taskId);
async getTask(_args, ctx) {
const task = await ctx.task.store.getTask(ctx.task.id);
if (!task) {
throw new Error(`Task ${extra.taskId} not found`);
throw new Error(`Task ${ctx.task.id} not found`);
}
return task;
},
async getTaskResult(_args, extra) {
const result = await extra.taskStore.getTaskResult(extra.taskId);
async getTaskResult(_args, ctx) {
const result = await ctx.task.store.getTaskResult(ctx.task.id);
return result as { content: Array<{ type: 'text'; text: string }> };
}
}
@ -1321,8 +1321,8 @@ describe('Task Lifecycle Integration Tests', () => {
})
},
{
async createTask({ messageCount }, extra) {
const task = await extra.taskStore.createTask({
async createTask({ messageCount }, ctx) {
const task = await ctx.task.store.createTask({
ttl: 60_000,
pollInterval: 100
});
@ -1335,8 +1335,8 @@ describe('Task Lifecycle Integration Tests', () => {
for (let i = 0; i < messageCount; i++) {
// Start the request but don't wait for response
// The request gets queued when sendRequest is called
extra
.sendRequest(
ctx.mcpReq
.send(
{
method: 'elicitation/create',
params: {
@ -1361,7 +1361,7 @@ describe('Task Lifecycle Integration Tests', () => {
// Complete the task after all messages are queued
try {
await extra.taskStore.storeTaskResult(task.taskId, 'completed', {
await ctx.task.store.storeTaskResult(task.taskId, 'completed', {
content: [{ type: 'text', text: 'Task completed quickly' }]
});
} catch {
@ -1370,7 +1370,7 @@ describe('Task Lifecycle Integration Tests', () => {
} catch (error) {
// Handle errors
try {
await extra.taskStore.storeTaskResult(task.taskId, 'failed', {
await ctx.task.store.storeTaskResult(task.taskId, 'failed', {
content: [{ type: 'text', text: `Error: ${error}` }],
isError: true
});
@ -1382,15 +1382,15 @@ describe('Task Lifecycle Integration Tests', () => {
return { task };
},
async getTask(_args, extra) {
const task = await extra.taskStore.getTask(extra.taskId);
async getTask(_args, ctx) {
const task = await ctx.task.store.getTask(ctx.task.id);
if (!task) {
throw new Error(`Task ${extra.taskId} not found`);
throw new Error(`Task ${ctx.task.id} not found`);
}
return task;
},
async getTaskResult(_args, extra) {
const result = await extra.taskStore.getTaskResult(extra.taskId);
async getTaskResult(_args, ctx) {
const result = await ctx.task.store.getTaskResult(ctx.task.id);
return result as { content: Array<{ type: 'text'; text: string }> };
}
}

View file

@ -67,9 +67,9 @@ describe('Zod v4', () => {
message: z.string().describe('Message to send').default('Test notification')
})
},
async ({ message }, { sendNotification }) => {
async ({ message }, ctx) => {
// Send notification immediately
await sendNotification({
await ctx.mcpReq.notify({
method: 'notifications/message',
params: {
level: 'info',
@ -93,10 +93,10 @@ describe('Zod v4', () => {
interval: z.number().describe('Interval between notifications in ms').default(50)
})
},
async ({ count, interval }, { sendNotification }) => {
async ({ count, interval }, ctx) => {
// Send notifications at specified intervals
for (let i = 0; i < count; i++) {
await sendNotification({
await ctx.mcpReq.notify({
method: 'notifications/message',
params: {
level: 'info',