v2 ESlint fixes (#1403)

Co-authored-by: Matt <77928207+mattzcarey@users.noreply.github.com>
This commit is contained in:
Konstantin Konstantinov 2026-01-20 14:21:53 +02:00 • committed by GitHub
commit 9fc7e28f48
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 1672 additions and 1346 deletions

View file

@ -11,6 +11,7 @@ await server.connect(transport);
const exit = async () => {
await server.close();
// eslint-disable-next-line unicorn/no-process-exit
process.exit(0);
};

View file

@ -2407,7 +2407,7 @@ describe('Task-based execution', () => {
// Client creates task on server via tool call
await client.callTool({ name: 'test-tool', arguments: {} }, CallToolResultSchema, {
task: {
ttl: 60000
ttl: 60_000
}
});
@ -2482,7 +2482,7 @@ describe('Task-based execution', () => {
// Create a task
await client.callTool({ name: 'test-tool', arguments: {} }, CallToolResultSchema, {
task: { ttl: 60000 }
task: { ttl: 60_000 }
});
// Query task status by listing tasks and getting the first one
@ -2560,7 +2560,7 @@ describe('Task-based execution', () => {
// Create a task using callToolStream to capture the task ID
let taskId: string | undefined;
const stream = client.experimental.tasks.callToolStream({ name: 'test-tool', arguments: {} }, CallToolResultSchema, {
task: { ttl: 60000 }
task: { ttl: 60_000 }
});
for await (const message of stream) {
@ -2643,7 +2643,7 @@ describe('Task-based execution', () => {
for (let i = 0; i < 2; i++) {
await client.callTool({ name: 'test-tool', arguments: {} }, CallToolResultSchema, {
task: { ttl: 60000 }
task: { ttl: 60_000 }
});
// Get the task ID from the task list
@ -2759,7 +2759,7 @@ describe('Task-based execution', () => {
}
},
CreateTaskResultSchema,
{ task: { ttl: 60000 } }
{ task: { ttl: 60_000 } }
);
// Verify CreateTaskResult structure
@ -2849,7 +2849,7 @@ describe('Task-based execution', () => {
}
},
CreateTaskResultSchema,
{ task: { ttl: 60000 } }
{ task: { ttl: 60_000 } }
);
// Verify CreateTaskResult structure
@ -2941,7 +2941,7 @@ describe('Task-based execution', () => {
}
},
CreateTaskResultSchema,
{ task: { ttl: 60000 } }
{ task: { ttl: 60_000 } }
);
// Verify CreateTaskResult structure
@ -3034,7 +3034,7 @@ describe('Task-based execution', () => {
}
},
CreateTaskResultSchema,
{ task: { ttl: 60000 } }
{ task: { ttl: 60_000 } }
);
// Verify CreateTaskResult structure and capture taskId
@ -3141,7 +3141,7 @@ describe('Task-based execution', () => {
for (let i = 0; i < 3; i++) {
await client.callTool({ name: 'test-tool', arguments: { id: `task-${i + 1}` } }, CallToolResultSchema, {
task: { ttl: 60000 }
task: { ttl: 60_000 }
});
// Get the task ID from the task list
@ -3412,7 +3412,7 @@ test('should respect server task capabilities', async () => {
// These should work because server supports tasks
await expect(
client.callTool({ name: 'test-tool', arguments: {} }, CallToolResultSchema, {
task: { ttl: 60000 }
task: { ttl: 60_000 }
})
).resolves.not.toThrow();
await expect(client.experimental.tasks.listTasks()).resolves.not.toThrow();

View file

@ -32,13 +32,13 @@ describe('Task Schema Validation', () => {
const task: Task = {
taskId: 'test-123',
status: 'working',
ttl: 60000,
ttl: 60_000,
createdAt,
lastUpdatedAt: createdAt,
pollInterval: 1000
};
expect(task.ttl).toBe(60000);
expect(task.ttl).toBe(60_000);
expect(task.createdAt).toBeDefined();
expect(typeof task.createdAt).toBe('string');
});
@ -76,7 +76,7 @@ describe('Task Schema Validation', () => {
const task: Task = {
taskId: 'test-iso',
status: 'working',
ttl: 30000,
ttl: 30_000,
createdAt,
lastUpdatedAt: createdAt
};
@ -91,7 +91,7 @@ describe('Task Schema Validation', () => {
const task: Task = {
taskId: 'test-iso',
status: 'working',
ttl: 30000,
ttl: 30_000,
createdAt,
lastUpdatedAt: createdAt
};
@ -103,7 +103,7 @@ describe('Task Schema Validation', () => {
const statuses: Task['status'][] = ['working', 'input_required', 'completed', 'failed', 'cancelled'];
const createdAt = new Date().toISOString();
statuses.forEach(status => {
for (const status of statuses) {
const task: Task = {
taskId: `test-${status}`,
status,
@ -112,6 +112,6 @@ describe('Task Schema Validation', () => {
lastUpdatedAt: createdAt
};
expect(task.status).toBe(status);
});
}
});
});

View file

@ -0,0 +1,129 @@
import { ErrorCode, McpError } from '@modelcontextprotocol/core';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { createInMemoryTaskEnvironment } from '../../helpers/mcp.js';
describe('Task Listing with Pagination', () => {
let client: Awaited<ReturnType<typeof createInMemoryTaskEnvironment>>['client'];
let server: Awaited<ReturnType<typeof createInMemoryTaskEnvironment>>['server'];
let taskStore: Awaited<ReturnType<typeof createInMemoryTaskEnvironment>>['taskStore'];
beforeEach(async () => {
const env = await createInMemoryTaskEnvironment();
client = env.client;
server = env.server;
taskStore = env.taskStore;
});
afterEach(async () => {
taskStore.cleanup();
await client.close();
await server.close();
});
it('should return empty list when no tasks exist', async () => {
const result = await client.experimental.tasks.listTasks();
expect(result.tasks).toEqual([]);
expect(result.nextCursor).toBeUndefined();
});
it('should return all tasks when less than page size', async () => {
// Create 3 tasks
for (let i = 0; i < 3; i++) {
await taskStore.createTask({}, i, {
method: 'tools/call',
params: { name: 'test-tool' }
});
}
const result = await client.experimental.tasks.listTasks();
expect(result.tasks).toHaveLength(3);
expect(result.nextCursor).toBeUndefined();
});
it('should paginate when more than page size exists', async () => {
// Create 15 tasks (page size is 10 in InMemoryTaskStore)
for (let i = 0; i < 15; i++) {
await taskStore.createTask({}, i, {
method: 'tools/call',
params: { name: 'test-tool' }
});
}
// Get first page
const page1 = await client.experimental.tasks.listTasks();
expect(page1.tasks).toHaveLength(10);
expect(page1.nextCursor).toBeDefined();
// Get second page using cursor
const page2 = await client.experimental.tasks.listTasks(page1.nextCursor);
expect(page2.tasks).toHaveLength(5);
expect(page2.nextCursor).toBeUndefined();
});
it('should treat cursor as opaque token', async () => {
// Create 5 tasks
for (let i = 0; i < 5; i++) {
await taskStore.createTask({}, i, {
method: 'tools/call',
params: { name: 'test-tool' }
});
}
// Get all tasks to get a valid cursor
const allTasks = taskStore.getAllTasks();
const validCursor = allTasks[2]!.taskId;
// Use the cursor - should work even though we don't know its internal structure
const result = await client.experimental.tasks.listTasks(validCursor);
expect(result.tasks).toHaveLength(2);
});
it('should return error code -32602 for invalid cursor', async () => {
await taskStore.createTask({}, 1, {
method: 'tools/call',
params: { name: 'test-tool' }
});
// Try to use an invalid cursor - should return -32602 (Invalid params) per MCP spec
await expect(client.experimental.tasks.listTasks('invalid-cursor')).rejects.toSatisfy((error: McpError) => {
expect(error).toBeInstanceOf(McpError);
expect(error.code).toBe(ErrorCode.InvalidParams);
expect(error.message).toContain('Invalid cursor');
return true;
});
});
it('should ensure tasks accessible via tasks/get are also accessible via tasks/list', async () => {
// Create a task
const task = await taskStore.createTask({}, 1, {
method: 'tools/call',
params: { name: 'test-tool' }
});
// Verify it's accessible via tasks/get
const getResult = await client.experimental.tasks.getTask(task.taskId);
expect(getResult.taskId).toBe(task.taskId);
// Verify it's also accessible via tasks/list
const listResult = await client.experimental.tasks.listTasks();
expect(listResult.tasks).toHaveLength(1);
expect(listResult.tasks[0]!.taskId).toBe(task.taskId);
});
it('should not include related-task metadata in list response', async () => {
// Create a task
await taskStore.createTask({}, 1, {
method: 'tools/call',
params: { name: 'test-tool' }
});
const result = await client.experimental.tasks.listTasks();
// The response should have _meta but not include related-task metadata
expect(result._meta).toBeDefined();
expect(result._meta?.['io.modelcontextprotocol/related-task']).toBeUndefined();
});
});

View file

@ -0,0 +1,65 @@
/**
* Regression test for https://github.com/modelcontextprotocol/typescript-sdk/issues/1277
*
* Zod v4 stores `.describe()` descriptions directly on the schema object,
* not in `._zod.def.description`. This test verifies that descriptions are
* correctly extracted for prompt arguments.
*/
import { Client } from '@modelcontextprotocol/client';
import { InMemoryTransport, ListPromptsResultSchema } from '@modelcontextprotocol/core';
import { McpServer } from '@modelcontextprotocol/server';
import type { ZodMatrixEntry } from '@modelcontextprotocol/test-helpers';
import { zodTestMatrix } from '@modelcontextprotocol/test-helpers';
describe.each(zodTestMatrix)('Issue #1277: $zodVersionLabel', (entry: ZodMatrixEntry) => {
const { z } = entry;
test('should preserve argument descriptions from .describe()', async () => {
const mcpServer = new McpServer({
name: 'test server',
version: '1.0'
});
const client = new Client({
name: 'test client',
version: '1.0'
});
mcpServer.prompt(
'test',
{
name: z.string().describe('The user name'),
value: z.string().describe('The value to set')
},
async ({ name, value }) => ({
messages: [
{
role: 'assistant',
content: {
type: 'text',
text: `${name}: ${value}`
}
}
]
})
);
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await Promise.all([client.connect(clientTransport), mcpServer.server.connect(serverTransport)]);
const result = await client.request(
{
method: 'prompts/list'
},
ListPromptsResultSchema
);
expect(result.prompts).toHaveLength(1);
expect(result.prompts[0]!.name).toBe('test');
expect(result.prompts[0]!.arguments).toEqual([
{ name: 'name', required: true, description: 'The user name' },
{ name: 'value', required: true, description: 'The value to set' }
]);
});
});

View file

@ -102,11 +102,11 @@ describe('Process cleanup', () => {
try {
process.kill(serverPid, 9);
throw new Error('Expected server to be dead but it is alive');
} catch (err: unknown) {
} catch (error: unknown) {
// 'ESRCH' the process doesn't exist
if (err && typeof err === 'object' && 'code' in err && err.code === 'ESRCH') {
if (error && typeof error === 'object' && 'code' in error && error.code === 'ESRCH') {
// success
} else throw err;
} else throw error;
}
});
});

View file

@ -1302,11 +1302,7 @@ test('should allow elicitation reject and cancel without validation', async () =
let requestCount = 0;
client.setRequestHandler(ElicitRequestSchema, _request => {
requestCount++;
if (requestCount === 1) {
return { action: 'decline' };
} else {
return { action: 'cancel' };
}
return requestCount === 1 ? { action: 'decline' } : { action: 'cancel' };
});
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
@ -2090,7 +2086,7 @@ describe('createMcpExpressApp', () => {
expect(response.body).toEqual({
jsonrpc: '2.0',
error: {
code: -32000,
code: -32_000,
message: 'Invalid Host: evil.com'
},
id: null
@ -2308,7 +2304,7 @@ describe('Task-based execution', () => {
let taskId: string | undefined;
const stream = client.experimental.tasks.callToolStream({ name: 'test-tool', arguments: {} }, CallToolResultSchema, {
task: {
ttl: 60000
ttl: 60_000
}
});
@ -2535,7 +2531,7 @@ describe('Task-based execution', () => {
let taskId: string | undefined;
const stream = client.experimental.tasks.callToolStream({ name: 'collect-info', arguments: {} }, CallToolResultSchema, {
task: {
ttl: 60000
ttl: 60_000
}
});
@ -2645,7 +2641,7 @@ describe('Task-based execution', () => {
}
},
CreateTaskResultSchema,
{ task: { ttl: 60000 } }
{ task: { ttl: 60_000 } }
);
// Verify CreateTaskResult structure
@ -2722,7 +2718,7 @@ describe('Task-based execution', () => {
}
},
CreateTaskResultSchema,
{ task: { ttl: 60000 } }
{ task: { ttl: 60_000 } }
);
// Verify CreateTaskResult structure
@ -2804,7 +2800,7 @@ describe('Task-based execution', () => {
}
},
CreateTaskResultSchema,
{ task: { ttl: 60000 } }
{ task: { ttl: 60_000 } }
);
// Verify CreateTaskResult structure
@ -2897,7 +2893,7 @@ describe('Task-based execution', () => {
}
},
CreateTaskResultSchema,
{ task: { ttl: 60000 } }
{ task: { ttl: 60_000 } }
);
// Verify CreateTaskResult structure and capture taskId
@ -3008,7 +3004,7 @@ describe('Task-based execution', () => {
// Create multiple tasks concurrently
const pendingRequests = Array.from({ length: 4 }, (_, index) =>
client.callTool({ name: 'async-tool', arguments: { delay: 10 + index * 5, taskNum: index + 1 } }, CallToolResultSchema, {
task: { ttl: 60000 }
task: { ttl: 60_000 }
})
);
@ -3024,12 +3020,12 @@ describe('Task-based execution', () => {
const taskIds = taskList.tasks.map(t => t.taskId);
// Verify all tasks completed successfully
for (let i = 0; i < taskIds.length; i++) {
const task = await client.experimental.tasks.getTask(taskIds[i]!);
for (const [i, taskId] of taskIds.entries()) {
const task = await client.experimental.tasks.getTask(taskId!);
expect(task.status).toBe('completed');
expect(task.taskId).toBe(taskIds[i]!);
expect(task.taskId).toBe(taskId!);
const result = await client.experimental.tasks.getTaskResult(taskIds[i]!, CallToolResultSchema);
const result = await client.experimental.tasks.getTaskResult(taskId!, CallToolResultSchema);
expect(result.content).toEqual([{ type: 'text', text: `Completed task ${i + 1}` }]);
}
@ -3253,7 +3249,7 @@ test('should respect client task capabilities', async () => {
}
},
CreateTaskResultSchema,
{ task: { ttl: 60000 } }
{ task: { ttl: 60_000 } }
);
// Verify CreateTaskResult structure
@ -3275,7 +3271,7 @@ test('should respect client task capabilities', async () => {
}
},
CreateMessageResultSchema,
{ task: { taskId: 'test-task-2', keepAlive: 60000 } }
{ task: { taskId: 'test-task-2', keepAlive: 60_000 } }
)
).rejects.toThrow('Client does not support task creation for sampling/createMessage');

View file

@ -1902,7 +1902,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {
},
{
createTask: async (_args, extra) => {
const task = await extra.taskStore.createTask({ ttl: 60000 });
const task = await extra.taskStore.createTask({ ttl: 60_000 });
return { task };
},
getTask: async (_args, extra) => {
@ -1971,7 +1971,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {
},
{
createTask: async (_args, extra) => {
const task = await extra.taskStore.createTask({ ttl: 60000 });
const task = await extra.taskStore.createTask({ ttl: 60_000 });
return { task };
},
getTask: async (_args, extra) => {
@ -3184,7 +3184,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {
);
expect(listResult.prompts[0]!.arguments).toHaveLength(2);
expect(listResult.prompts[0]!.arguments!.map(a => a.name).sort()).toEqual(['name', 'value']);
expect(listResult.prompts[0]!.arguments!.map(a => a.name).toSorted()).toEqual(['name', 'value']);
// Call the prompt with the new schema
const getResult = await client.request(
@ -3325,7 +3325,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {
let result = await client.request({ method: 'prompts/list' }, ListPromptsResultSchema);
expect(result.prompts).toHaveLength(2);
expect(result.prompts.map(p => p.name).sort()).toEqual(['prompt1', 'prompt2']);
expect(result.prompts.map(p => p.name).toSorted()).toEqual(['prompt1', 'prompt2']);
expect(notifications).toHaveLength(0);
@ -4375,12 +4375,17 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {
}),
name: completable(z.string(), (value, context) => {
const department = context?.arguments?.['department'];
if (department === 'engineering') {
return ['Alice', 'Bob', 'Charlie'].filter(n => n.startsWith(value));
} else if (department === 'sales') {
return ['David', 'Eve', 'Frank'].filter(n => n.startsWith(value));
} else if (department === 'marketing') {
return ['Grace', 'Henry', 'Iris'].filter(n => n.startsWith(value));
switch (department) {
case 'engineering': {
return ['Alice', 'Bob', 'Charlie'].filter(n => n.startsWith(value));
}
case 'sales': {
return ['David', 'Eve', 'Frank'].filter(n => n.startsWith(value));
}
case 'marketing': {
return ['Grace', 'Henry', 'Iris'].filter(n => n.startsWith(value));
}
// No default
}
return ['Guest'].filter(n => n.startsWith(value));
})
@ -4739,15 +4744,13 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {
]);
server.registerTool('contact', { inputSchema: unionSchema }, async args => {
if (args.type === 'email') {
return {
content: [{ type: 'text' as const, text: `Email contact: ${args.email}` }]
};
} else {
return {
content: [{ type: 'text' as const, text: `Phone contact: ${args.phone}` }]
};
}
return args.type === 'email'
? {
content: [{ type: 'text' as const, text: `Email contact: ${args.email}` }]
}
: {
content: [{ type: 'text' as const, text: `Phone contact: ${args.phone}` }]
};
});
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
@ -4854,11 +4857,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {
server.registerTool('process', { inputSchema: schema }, async args => {
const processed = args.items.map(item => {
if (item.type === 'text') {
return item.content.toUpperCase();
} else {
return item.value * 2;
}
return item.type === 'text' ? item.content.toUpperCase() : item.value * 2;
});
return {
content: [
@ -5093,7 +5092,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {
const obj = input as Record<string, unknown>;
// Convert string numbers to actual numbers
if (typeof obj.count === 'string') {
return { ...obj, count: parseInt(obj.count, 10) };
return { ...obj, count: Number.parseInt(obj.count, 10) };
}
}
return input;
@ -5662,12 +5661,17 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {
}),
name: completable(z.string(), (value, context) => {
const department = context?.arguments?.['department'];
if (department === 'engineering') {
return ['Alice', 'Bob', 'Charlie'].filter(n => n.startsWith(value));
} else if (department === 'sales') {
return ['David', 'Eve', 'Frank'].filter(n => n.startsWith(value));
} else if (department === 'marketing') {
return ['Grace', 'Henry', 'Iris'].filter(n => n.startsWith(value));
switch (department) {
case 'engineering': {
return ['Alice', 'Bob', 'Charlie'].filter(n => n.startsWith(value));
}
case 'sales': {
return ['David', 'Eve', 'Frank'].filter(n => n.startsWith(value));
}
case 'marketing': {
return ['Grace', 'Henry', 'Iris'].filter(n => n.startsWith(value));
}
// No default
}
return ['Guest'].filter(n => n.startsWith(value));
})
@ -6027,15 +6031,13 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {
]);
server.registerTool('contact', { inputSchema: unionSchema }, async args => {
if (args.type === 'email') {
return {
content: [{ type: 'text', text: `Email contact: ${args.email}` }]
};
} else {
return {
content: [{ type: 'text', text: `Phone contact: ${args.phone}` }]
};
}
return args.type === 'email'
? {
content: [{ type: 'text', text: `Email contact: ${args.email}` }]
}
: {
content: [{ type: 'text', text: `Phone contact: ${args.phone}` }]
};
});
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
@ -6142,11 +6144,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {
server.registerTool('process', { inputSchema: schema }, async args => {
const processed = args.items.map(item => {
if (item.type === 'text') {
return item.content.toUpperCase();
} else {
return item.value * 2;
}
return item.type === 'text' ? item.content.toUpperCase() : item.value * 2;
});
return {
content: [
@ -6301,7 +6299,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {
},
{
createTask: async ({ input }, extra) => {
const task = await extra.taskStore.createTask({ ttl: 60000, pollInterval: 100 });
const task = await extra.taskStore.createTask({ ttl: 60_000, pollInterval: 100 });
// Capture taskStore for use in setTimeout
const store = extra.taskStore;
@ -6406,7 +6404,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {
},
{
createTask: async ({ value }, extra) => {
const task = await extra.taskStore.createTask({ ttl: 60000, pollInterval: 100 });
const task = await extra.taskStore.createTask({ ttl: 60_000, pollInterval: 100 });
// Capture taskStore for use in setTimeout
const store = extra.taskStore;
@ -6514,7 +6512,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {
},
{
createTask: async ({ data }, extra) => {
const task = await extra.taskStore.createTask({ ttl: 60000, pollInterval: 100 });
const task = await extra.taskStore.createTask({ ttl: 60_000, pollInterval: 100 });
// Capture taskStore for use in setTimeout
const store = extra.taskStore;
@ -6554,7 +6552,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {
params: {
name: 'task-tool',
arguments: { data: 'test' },
task: { ttl: 60000 }
task: { ttl: 60_000 }
}
},
z.object({
@ -6631,7 +6629,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {
},
{
createTask: async extra => {
const task = await extra.taskStore.createTask({ ttl: 60000, pollInterval: 100 });
const task = await extra.taskStore.createTask({ ttl: 60_000, pollInterval: 100 });
// Capture taskStore for use in setTimeout
const store = extra.taskStore;
@ -6737,7 +6735,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {
},
{
createTask: async extra => {
const task = await extra.taskStore.createTask({ ttl: 60000, pollInterval: 100 });
const task = await extra.taskStore.createTask({ ttl: 60_000, pollInterval: 100 });
// Capture taskStore for use in setTimeout
const store = extra.taskStore;
@ -6824,7 +6822,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {
},
{
createTask: async (_args, extra) => {
const task = await extra.taskStore.createTask({ ttl: 60000, pollInterval: 100 });
const task = await extra.taskStore.createTask({ ttl: 60_000, pollInterval: 100 });
return { task };
},
getTask: async (_args, extra) => {

View file

@ -12,81 +12,81 @@ import {
ListToolsResultSchema,
McpServer
} from '@modelcontextprotocol/server';
import type { ZodMatrixEntry } from '@modelcontextprotocol/test-helpers';
import type { ZNamespace, ZodMatrixEntry } from '@modelcontextprotocol/test-helpers';
import { listenOnRandomPort, zodTestMatrix } from '@modelcontextprotocol/test-helpers';
async function setupServer(withSessionManagement: boolean, z: ZNamespace) {
const server: Server = createServer();
const mcpServer = new McpServer(
{ name: 'test-server', version: '1.0.0' },
{
capabilities: {
logging: {},
tools: {},
resources: {},
prompts: {}
}
}
);
// Add a simple resource
mcpServer.resource('test-resource', '/test', { description: 'A test resource' }, async () => ({
contents: [
{
uri: '/test',
text: 'This is a test resource content'
}
]
}));
mcpServer.prompt('test-prompt', 'A test prompt', async () => ({
messages: [
{
role: 'user',
content: {
type: 'text',
text: 'This is a test prompt'
}
}
]
}));
mcpServer.tool(
'greet',
'A simple greeting tool',
{
name: z.string().describe('Name to greet').default('World')
},
async ({ name }) => {
return {
content: [{ type: 'text', text: `Hello, ${name}!` }]
};
}
);
// Create transport with or without session management
const serverTransport = new NodeStreamableHTTPServerTransport({
sessionIdGenerator: withSessionManagement
? () => randomUUID() // With session management, generate UUID
: undefined // Without session management, return undefined
});
await mcpServer.connect(serverTransport);
server.on('request', async (req, res) => {
await serverTransport.handleRequest(req, res);
});
// Start the server on a random port
const baseUrl = await listenOnRandomPort(server);
return { server, mcpServer, serverTransport, baseUrl };
}
describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {
const { z } = entry;
describe('Streamable HTTP Transport Session Management', () => {
// Function to set up the server with optional session management
async function setupServer(withSessionManagement: boolean) {
const server: Server = createServer();
const mcpServer = new McpServer(
{ name: 'test-server', version: '1.0.0' },
{
capabilities: {
logging: {},
tools: {},
resources: {},
prompts: {}
}
}
);
// Add a simple resource
mcpServer.resource('test-resource', '/test', { description: 'A test resource' }, async () => ({
contents: [
{
uri: '/test',
text: 'This is a test resource content'
}
]
}));
mcpServer.prompt('test-prompt', 'A test prompt', async () => ({
messages: [
{
role: 'user',
content: {
type: 'text',
text: 'This is a test prompt'
}
}
]
}));
mcpServer.tool(
'greet',
'A simple greeting tool',
{
name: z.string().describe('Name to greet').default('World')
},
async ({ name }) => {
return {
content: [{ type: 'text', text: `Hello, ${name}!` }]
};
}
);
// Create transport with or without session management
const serverTransport = new NodeStreamableHTTPServerTransport({
sessionIdGenerator: withSessionManagement
? () => randomUUID() // With session management, generate UUID
: undefined // Without session management, return undefined
});
await mcpServer.connect(serverTransport);
server.on('request', async (req, res) => {
await serverTransport.handleRequest(req, res);
});
// Start the server on a random port
const baseUrl = await listenOnRandomPort(server);
return { server, mcpServer, serverTransport, baseUrl };
}
describe('Stateless Mode', () => {
let server: Server;
let mcpServer: McpServer;
@ -94,7 +94,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {
let baseUrl: URL;
beforeEach(async () => {
const setup = await setupServer(false);
const setup = await setupServer(false, z);
server = setup.server;
mcpServer = setup.mcpServer;
serverTransport = setup.serverTransport;
@ -258,7 +258,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {
let baseUrl: URL;
beforeEach(async () => {
const setup = await setupServer(true);
const setup = await setupServer(true, z);
server = setup.server;
mcpServer = setup.mcpServer;
serverTransport = setup.serverTransport;

View file

@ -66,7 +66,7 @@ describe('Task Lifecycle Integration Tests', () => {
{
async createTask({ duration, shouldFail }, extra) {
const task = await extra.taskStore.createTask({
ttl: 60000,
ttl: 60_000,
pollInterval: 100
});
@ -75,16 +75,14 @@ describe('Task Lifecycle Integration Tests', () => {
await new Promise(resolve => setTimeout(resolve, duration));
try {
if (shouldFail) {
await extra.taskStore.storeTaskResult(task.taskId, 'failed', {
content: [{ type: 'text', text: 'Task failed as requested' }],
isError: true
});
} else {
await extra.taskStore.storeTaskResult(task.taskId, 'completed', {
content: [{ type: 'text', text: `Completed after ${duration}ms` }]
});
}
await (shouldFail
? extra.taskStore.storeTaskResult(task.taskId, 'failed', {
content: [{ type: 'text', text: 'Task failed as requested' }],
isError: true
})
: extra.taskStore.storeTaskResult(task.taskId, 'completed', {
content: [{ type: 'text', text: `Completed after ${duration}ms` }]
}));
} catch {
// Task may have been cleaned up if test ended
}
@ -119,7 +117,7 @@ describe('Task Lifecycle Integration Tests', () => {
{
async createTask({ userName }, extra) {
const task = await extra.taskStore.createTask({
ttl: 60000,
ttl: 60_000,
pollInterval: 100
});
@ -128,7 +126,16 @@ describe('Task Lifecycle Integration Tests', () => {
await new Promise(resolve => setTimeout(resolve, 100));
// If userName not provided, request it via elicitation
if (!userName) {
if (userName) {
// Complete immediately if userName was provided
try {
await extra.taskStore.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(
{
method: 'elicitation/create',
@ -160,15 +167,6 @@ describe('Task Lifecycle Integration Tests', () => {
} catch {
// Task may have been cleaned up if test ended
}
} else {
// Complete immediately if userName was provided
try {
await extra.taskStore.storeTaskResult(task.taskId, 'completed', {
content: [{ type: 'text', text: `Hello, ${userName}!` }]
});
} catch {
// Task may have been cleaned up if test ended
}
}
})();
@ -232,7 +230,7 @@ describe('Task Lifecycle Integration Tests', () => {
shouldFail: false
},
task: {
ttl: 60000
ttl: 60_000
}
}
},
@ -243,7 +241,7 @@ describe('Task Lifecycle Integration Tests', () => {
expect(createResult).toHaveProperty('task');
expect(createResult.task).toHaveProperty('taskId');
expect(createResult.task.status).toBe('working');
expect(createResult.task.ttl).toBe(60000);
expect(createResult.task.ttl).toBe(60_000);
expect(createResult.task.createdAt).toBeDefined();
expect(createResult.task.pollInterval).toBe(100);
@ -288,7 +286,7 @@ describe('Task Lifecycle Integration Tests', () => {
shouldFail: true
},
task: {
ttl: 60000
ttl: 60_000
}
}
},
@ -332,7 +330,7 @@ describe('Task Lifecycle Integration Tests', () => {
duration: 5000
},
task: {
ttl: 60000
ttl: 60_000
}
}
},
@ -381,7 +379,7 @@ describe('Task Lifecycle Integration Tests', () => {
duration: 100
},
task: {
ttl: 60000
ttl: 60_000
}
}
},
@ -423,7 +421,7 @@ describe('Task Lifecycle Integration Tests', () => {
{
async createTask({ requestCount }, extra) {
const task = await extra.taskStore.createTask({
ttl: 60000,
ttl: 60_000,
pollInterval: 100
});
@ -533,7 +531,7 @@ describe('Task Lifecycle Integration Tests', () => {
requestCount: 3
},
task: {
ttl: 60000
ttl: 60_000
}
}
},
@ -575,7 +573,7 @@ describe('Task Lifecycle Integration Tests', () => {
expect(task.status).toBe('completed');
await transport.close();
}, 10000);
}, 10_000);
});
describe('Input Required Flow', () => {
@ -629,7 +627,7 @@ describe('Task Lifecycle Integration Tests', () => {
name: 'input-task',
arguments: {},
task: {
ttl: 60000
ttl: 60_000
}
}
},
@ -694,7 +692,7 @@ describe('Task Lifecycle Integration Tests', () => {
expect(finalTask.status).toBe('completed');
await transport.close();
}, 15000);
}, 15_000);
});
describe('Task Listing and Pagination', () => {
@ -719,7 +717,7 @@ describe('Task Lifecycle Integration Tests', () => {
duration: 1000
},
task: {
ttl: 60000
ttl: 60_000
}
}
},
@ -757,7 +755,7 @@ describe('Task Lifecycle Integration Tests', () => {
duration: 5000
},
task: {
ttl: 60000
ttl: 60_000
}
}
},
@ -880,7 +878,7 @@ describe('Task Lifecycle Integration Tests', () => {
const taskId = createResult.task.taskId;
// Verify TTL is set correctly
expect(createResult.task.ttl).toBe(60000); // The task store uses 60000 as default
expect(createResult.task.ttl).toBe(60_000); // The task store uses 60000 as default
// Task should exist
const task = await client.request(
@ -891,7 +889,7 @@ describe('Task Lifecycle Integration Tests', () => {
TaskSchema
);
expect(task).toBeDefined();
expect(task.ttl).toBe(60000);
expect(task.ttl).toBe(60_000);
await transport.close();
});
@ -912,7 +910,7 @@ describe('Task Lifecycle Integration Tests', () => {
{
async createTask({ messageCount }, extra) {
const task = await extra.taskStore.createTask({
ttl: 60000,
ttl: 60_000,
pollInterval: 100
});
@ -1012,7 +1010,7 @@ describe('Task Lifecycle Integration Tests', () => {
messageCount: 2
},
task: {
ttl: 60000
ttl: 60_000
}
}
},
@ -1091,7 +1089,7 @@ describe('Task Lifecycle Integration Tests', () => {
expect(elicitationCallCount).toBe(0);
await transport.close();
}, 10000);
}, 10_000);
});
describe('Continuous Message Delivery', () => {
@ -1110,7 +1108,7 @@ describe('Task Lifecycle Integration Tests', () => {
{
async createTask({ messageCount, delayBetweenMessages }, extra) {
const task = await extra.taskStore.createTask({
ttl: 60000,
ttl: 60_000,
pollInterval: 100
});
@ -1240,7 +1238,7 @@ describe('Task Lifecycle Integration Tests', () => {
delayBetweenMessages: 300
},
task: {
ttl: 60000
ttl: 60_000
}
}
},
@ -1308,7 +1306,7 @@ describe('Task Lifecycle Integration Tests', () => {
expect(task.status).toBe('completed');
await transport.close();
}, 15000); // Increase timeout to 15 seconds to allow for message delays
}, 15_000); // Increase timeout to 15 seconds to allow for message delays
});
describe('Terminal Task with Queued Messages', () => {
@ -1326,7 +1324,7 @@ describe('Task Lifecycle Integration Tests', () => {
{
async createTask({ messageCount }, extra) {
const task = await extra.taskStore.createTask({
ttl: 60000,
ttl: 60_000,
pollInterval: 100
});
@ -1445,7 +1443,7 @@ describe('Task Lifecycle Integration Tests', () => {
messageCount: 2
},
task: {
ttl: 60000
ttl: 60_000
}
}
},
@ -1493,7 +1491,7 @@ describe('Task Lifecycle Integration Tests', () => {
expect(result2.content).toEqual([{ type: 'text', text: 'Task completed quickly' }]);
await transport.close();
}, 10000);
}, 10_000);
});
describe('Concurrent Operations', () => {
@ -1517,7 +1515,7 @@ describe('Task Lifecycle Integration Tests', () => {
duration: 500
},
task: {
ttl: 60000
ttl: 60_000
}
}
},
@ -1558,7 +1556,7 @@ describe('Task Lifecycle Integration Tests', () => {
duration: 2000
},
task: {
ttl: 60000
ttl: 60_000
}
}
},
@ -1626,7 +1624,7 @@ describe('Task Lifecycle Integration Tests', () => {
// Use callToolStream instead of raw request()
const stream = client.experimental.tasks.callToolStream({ name: 'input-task', arguments: {} }, CallToolResultSchema, {
task: { ttl: 60000 }
task: { ttl: 60_000 }
});
// Collect all stream messages
@ -1647,7 +1645,7 @@ describe('Task Lifecycle Integration Tests', () => {
expect(statusMessages.length).toBeGreaterThanOrEqual(1);
// Last message should be result
const lastMessage = messages[messages.length - 1]!;
const lastMessage = messages.at(-1)!;
expect(lastMessage.type).toBe('result');
expect(lastMessage.result).toBeDefined();
@ -1660,6 +1658,6 @@ describe('Task Lifecycle Integration Tests', () => {
expect(result.content).toEqual([{ type: 'text', text: 'Hello, StreamUser!' }]);
await transport.close();
}, 15000);
}, 15_000);
});
});

View file

@ -30,7 +30,7 @@ class InMemoryEventStore implements EventStore {
if (!streamId) return '';
let found = false;
const sorted = [...this.events.entries()].sort((a, b) => a[0].localeCompare(b[0]));
const sorted = [...this.events.entries()].toSorted((a, b) => a[0].localeCompare(b[0]));
for (const [eventId, { streamId: sid, message }] of sorted) {
if (sid !== streamId) continue;
if (eventId === lastEventId) {
@ -244,11 +244,11 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {
// any in-progress requests, which is expected behavior
await transport1.close();
// Save the promise so we can catch it after closing
const catchPromise = toolPromise.catch(err => {
const catchPromise = toolPromise.catch(error => {
// This error is expected - the connection was intentionally closed
if (err?.code !== -32000) {
if (error?.code !== -32_000) {
// ConnectionClosed error code
console.error('Unexpected error type during transport close:', err);
console.error('Unexpected error type during transport close:', error);
}
});