v2 refactor!: remove Zod schema parameter from public-facing APIs (#1606)

This commit is contained in:
Konstantin Konstantinov 2026-03-04 20:34:47 +02:00 • committed by GitHub
commit f88c376c52
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 1483 additions and 1708 deletions

View file

@ -2,11 +2,9 @@ import { Client, getSupportedElicitationModes } from '@modelcontextprotocol/clie
import type { Prompt, Resource, Tool, Transport } from '@modelcontextprotocol/core';
import {
CallToolResultSchema,
CreateTaskResultSchema,
ElicitResultSchema,
InMemoryTransport,
LATEST_PROTOCOL_VERSION,
ListToolsResultSchema,
ProtocolErrorCode,
SdkError,
SdkErrorCode,
@ -857,23 +855,20 @@ test('should reject missing-mode elicitation when client only supports URL mode'
await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]);
await expect(
server.request(
{
method: 'elicitation/create',
params: {
message: 'Please provide data',
requestedSchema: {
type: 'object',
properties: {
username: {
type: 'string'
}
server.request({
method: 'elicitation/create',
params: {
message: 'Please provide data',
requestedSchema: {
type: 'object',
properties: {
username: {
type: 'string'
}
}
}
},
ElicitResultSchema
)
}
})
).rejects.toThrow('Client does not support form-mode elicitation requests');
expect(handler).not.toHaveBeenCalled();
@ -2305,11 +2300,14 @@ describe('Task-based execution', () => {
await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]);
// Client creates task on server via tool call
await client.callTool({ name: 'test-tool', arguments: {} }, CallToolResultSchema, {
task: {
ttl: 60_000
await client.callTool(
{ name: 'test-tool', arguments: {} },
{
task: {
ttl: 60_000
}
}
});
);
// Verify task was created successfully by listing tasks
const taskList = await client.experimental.tasks.listTasks();
@ -2381,9 +2379,12 @@ describe('Task-based execution', () => {
await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]);
// Create a task
await client.callTool({ name: 'test-tool', arguments: {} }, CallToolResultSchema, {
task: { ttl: 60_000 }
});
await client.callTool(
{ name: 'test-tool', arguments: {} },
{
task: { ttl: 60_000 }
}
);
// Query task status by listing tasks and getting the first one
const taskList = await client.experimental.tasks.listTasks();
@ -2545,9 +2546,12 @@ describe('Task-based execution', () => {
const createdTaskIds: string[] = [];
for (let i = 0; i < 2; i++) {
await client.callTool({ name: 'test-tool', arguments: {} }, CallToolResultSchema, {
task: { ttl: 60_000 }
});
await client.callTool(
{ name: 'test-tool', arguments: {} },
{
task: { ttl: 60_000 }
}
);
// Get the task ID from the task list
const taskList = await client.experimental.tasks.listTasks();
@ -2661,7 +2665,6 @@ describe('Task-based execution', () => {
}
}
},
CreateTaskResultSchema,
{ task: { ttl: 60_000 } }
);
@ -2751,7 +2754,6 @@ describe('Task-based execution', () => {
}
}
},
CreateTaskResultSchema,
{ task: { ttl: 60_000 } }
);
@ -2843,7 +2845,6 @@ describe('Task-based execution', () => {
}
}
},
CreateTaskResultSchema,
{ task: { ttl: 60_000 } }
);
@ -2936,7 +2937,6 @@ describe('Task-based execution', () => {
}
}
},
CreateTaskResultSchema,
{ task: { ttl: 60_000 } }
);
@ -3043,9 +3043,12 @@ describe('Task-based execution', () => {
const createdTaskIds: string[] = [];
for (let i = 0; i < 3; i++) {
await client.callTool({ name: 'test-tool', arguments: { id: `task-${i + 1}` } }, CallToolResultSchema, {
task: { ttl: 60_000 }
});
await client.callTool(
{ name: 'test-tool', arguments: { id: `task-${i + 1}` } },
{
task: { ttl: 60_000 }
}
);
// Get the task ID from the task list
const taskList = await client.experimental.tasks.listTasks();
@ -3314,21 +3317,21 @@ 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: 60_000 }
})
client.callTool(
{ name: 'test-tool', arguments: {} },
{
task: { ttl: 60_000 }
}
)
).resolves.not.toThrow();
await expect(client.experimental.tasks.listTasks()).resolves.not.toThrow();
// tools/list doesn't support task creation, but it shouldn't throw - it should just ignore the task metadata
await expect(
client.request(
{
method: 'tools/list',
params: {}
},
ListToolsResultSchema
)
client.request({
method: 'tools/list',
params: {}
})
).resolves.not.toThrow();
serverTaskStore.cleanup();
@ -3375,13 +3378,10 @@ test('should expose requestStream() method for streaming responses', async () =>
expect(regularResult.content).toEqual([{ type: 'text', text: 'Tool result' }]);
// Test requestStream with non-task request (should yield only result)
const stream = client.experimental.tasks.requestStream(
{
method: 'tools/call',
params: { name: 'test-tool', arguments: {} }
},
CallToolResultSchema
);
const stream = client.experimental.tasks.requestStream({
method: 'tools/call',
params: { name: 'test-tool', arguments: {} }
});
const messages = [];
for await (const message of stream) {

View file

@ -7,7 +7,7 @@
*/
import { Client } from '@modelcontextprotocol/client';
import { InMemoryTransport, ListPromptsResultSchema } from '@modelcontextprotocol/core';
import { InMemoryTransport } from '@modelcontextprotocol/core';
import { McpServer } from '@modelcontextprotocol/server';
import * as z from 'zod/v4';
@ -47,12 +47,9 @@ describe('Issue #1277: Zod v4', () => {
await Promise.all([client.connect(clientTransport), mcpServer.server.connect(serverTransport)]);
const result = await client.request(
{
method: 'prompts/list'
},
ListPromptsResultSchema
);
const result = await client.request({
method: 'prompts/list'
});
expect(result.prompts).toHaveLength(1);
expect(result.prompts[0]!.name).toBe('test');

View file

@ -7,7 +7,7 @@
*/
import { Client } from '@modelcontextprotocol/client';
import { CallToolResultSchema, InMemoryTransport } from '@modelcontextprotocol/core';
import { InMemoryTransport } from '@modelcontextprotocol/core';
import { McpServer } from '@modelcontextprotocol/server';
import * as z from 'zod/v4';
@ -45,16 +45,13 @@ describe('Issue #400: Zod v4', () => {
await Promise.all([client.connect(clientTransport), mcpServer.server.connect(serverTransport)]);
// Call tool without arguments (arguments is undefined)
const result = await client.request(
{
method: 'tools/call',
params: {
name: 'optional-params-tool'
// arguments is intentionally omitted (undefined)
}
},
CallToolResultSchema
);
const result = await client.request({
method: 'tools/call',
params: {
name: 'optional-params-tool'
// arguments is intentionally omitted (undefined)
}
});
expect(result.isError).toBeUndefined();
expect(result.content).toEqual([

View file

@ -14,9 +14,6 @@ import type {
} from '@modelcontextprotocol/core';
import {
CallToolResultSchema,
CreateMessageRequestSchema,
CreateMessageResultSchema,
CreateTaskResultSchema,
ElicitResultSchema,
InMemoryTransport,
LATEST_PROTOCOL_VERSION,
@ -2613,23 +2610,20 @@ describe('Task-based execution', () => {
// Perform async work that makes a nested request
(async () => {
// During tool execution, make a nested request to the client using ctx.mcpReq.send
const elicitResult = await ctx.mcpReq.send(
{
method: 'elicitation/create',
params: {
mode: 'form',
message: 'Please provide your username',
requestedSchema: {
type: 'object',
properties: {
username: { type: 'string' }
},
required: ['username']
}
const elicitResult = await ctx.mcpReq.send({
method: 'elicitation/create',
params: {
mode: 'form',
message: 'Please provide your username',
requestedSchema: {
type: 'object',
properties: {
username: { type: 'string' }
},
required: ['username']
}
},
ElicitResultSchema
);
}
});
const result = {
content: [
@ -2778,7 +2772,6 @@ describe('Task-based execution', () => {
}
}
},
CreateTaskResultSchema,
{ task: { ttl: 60_000 } }
);
@ -2855,7 +2848,6 @@ describe('Task-based execution', () => {
}
}
},
CreateTaskResultSchema,
{ task: { ttl: 60_000 } }
);
@ -2937,7 +2929,6 @@ describe('Task-based execution', () => {
}
}
},
CreateTaskResultSchema,
{ task: { ttl: 60_000 } }
);
@ -3030,7 +3021,6 @@ describe('Task-based execution', () => {
}
}
},
CreateTaskResultSchema,
{ task: { ttl: 60_000 } }
);
@ -3141,9 +3131,12 @@ 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: 60_000 }
})
client.callTool(
{ name: 'async-tool', arguments: { delay: 10 + index * 5, taskNum: index + 1 } },
{
task: { ttl: 60_000 }
}
)
);
// Wait for all tasks to complete
@ -3386,7 +3379,6 @@ test('should respect client task capabilities', async () => {
}
}
},
CreateTaskResultSchema,
{ task: { ttl: 60_000 } }
);
@ -3408,7 +3400,6 @@ test('should respect client task capabilities', async () => {
maxTokens: 10
}
},
CreateMessageResultSchema,
{ task: { taskId: 'test-task-2', keepAlive: 60_000 } }
)
).rejects.toThrow('Client does not support task creation for sampling/createMessage');
@ -3532,7 +3523,6 @@ describe('elicitInputStream', () => {
method: 'elicitation/create',
params: expect.objectContaining({ mode: 'form' })
}),
ElicitResultSchema,
undefined
);
});

File diff suppressed because it is too large Load diff

View file

@ -4,14 +4,7 @@ import { createServer } from 'node:http';
import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';
import {
CallToolResultSchema,
LATEST_PROTOCOL_VERSION,
ListPromptsResultSchema,
ListResourcesResultSchema,
ListToolsResultSchema,
McpServer
} from '@modelcontextprotocol/server';
import { LATEST_PROTOCOL_VERSION, McpServer } from '@modelcontextprotocol/server';
import { listenOnRandomPort } from '@modelcontextprotocol/test-helpers';
import * as z from 'zod/v4';
@ -123,13 +116,10 @@ describe('Zod v4', () => {
expect(transport1.sessionId).toBeUndefined();
// List available tools
await client1.request(
{
method: 'tools/list',
params: {}
},
ListToolsResultSchema
);
await client1.request({
method: 'tools/list',
params: {}
});
const client2 = new Client({
name: 'test-client',
@ -143,13 +133,10 @@ describe('Zod v4', () => {
expect(transport2.sessionId).toBeUndefined();
// List available tools
await client2.request(
{
method: 'tools/list',
params: {}
},
ListToolsResultSchema
);
await client2.request({
method: 'tools/list',
params: {}
});
});
it('should operate without session management', async () => {
// Create and connect a client
@ -165,13 +152,10 @@ describe('Zod v4', () => {
expect(transport.sessionId).toBeUndefined();
// List available tools
const toolsResult = await client.request(
{
method: 'tools/list',
params: {}
},
ListToolsResultSchema
);
const toolsResult = await client.request({
method: 'tools/list',
params: {}
});
// Verify tools are accessible
expect(toolsResult.tools).toContainEqual(
@ -181,25 +165,19 @@ describe('Zod v4', () => {
);
// List available resources
const resourcesResult = await client.request(
{
method: 'resources/list',
params: {}
},
ListResourcesResultSchema
);
const resourcesResult = await client.request({
method: 'resources/list',
params: {}
});
// Verify resources result structure
expect(resourcesResult).toHaveProperty('resources');
// List available prompts
const promptsResult = await client.request(
{
method: 'prompts/list',
params: {}
},
ListPromptsResultSchema
);
const promptsResult = await client.request({
method: 'prompts/list',
params: {}
});
// Verify prompts result structure
expect(promptsResult).toHaveProperty('prompts');
@ -210,18 +188,15 @@ describe('Zod v4', () => {
);
// Call the greeting tool
const greetingResult = await client.request(
{
method: 'tools/call',
params: {
name: 'greet',
arguments: {
name: 'Stateless Transport'
}
const greetingResult = await client.request({
method: 'tools/call',
params: {
name: 'greet',
arguments: {
name: 'Stateless Transport'
}
},
CallToolResultSchema
);
}
});
// Verify tool result
expect(greetingResult.content).toEqual([{ type: 'text', text: 'Hello, Stateless Transport!' }]);
@ -288,13 +263,10 @@ describe('Zod v4', () => {
expect(typeof transport.sessionId).toBe('string');
// List available tools
const toolsResult = await client.request(
{
method: 'tools/list',
params: {}
},
ListToolsResultSchema
);
const toolsResult = await client.request({
method: 'tools/list',
params: {}
});
// Verify tools are accessible
expect(toolsResult.tools).toContainEqual(
@ -304,25 +276,19 @@ describe('Zod v4', () => {
);
// List available resources
const resourcesResult = await client.request(
{
method: 'resources/list',
params: {}
},
ListResourcesResultSchema
);
const resourcesResult = await client.request({
method: 'resources/list',
params: {}
});
// Verify resources result structure
expect(resourcesResult).toHaveProperty('resources');
// List available prompts
const promptsResult = await client.request(
{
method: 'prompts/list',
params: {}
},
ListPromptsResultSchema
);
const promptsResult = await client.request({
method: 'prompts/list',
params: {}
});
// Verify prompts result structure
expect(promptsResult).toHaveProperty('prompts');
@ -333,18 +299,15 @@ describe('Zod v4', () => {
);
// Call the greeting tool
const greetingResult = await client.request(
{
method: 'tools/call',
params: {
name: 'greet',
arguments: {
name: 'Stateful Transport'
}
const greetingResult = await client.request({
method: 'tools/call',
params: {
name: 'greet',
arguments: {
name: 'Stateful Transport'
}
},
CallToolResultSchema
);
}
});
// Verify tool result
expect(greetingResult.content).toEqual([{ type: 'text', text: 'Hello, Stateful Transport!' }]);

View file

@ -6,16 +6,12 @@ import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/cli
import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';
import type { TaskRequestOptions } from '@modelcontextprotocol/server';
import {
CallToolResultSchema,
CreateTaskResultSchema,
ElicitResultSchema,
InMemoryTaskMessageQueue,
InMemoryTaskStore,
McpServer,
ProtocolError,
ProtocolErrorCode,
RELATED_TASK_META_KEY,
TaskSchema
RELATED_TASK_META_KEY
} from '@modelcontextprotocol/server';
import { listenOnRandomPort, waitForTaskStatus } from '@modelcontextprotocol/test-helpers';
import * as z from 'zod/v4';
@ -150,7 +146,6 @@ describe('Task Lifecycle Integration Tests', () => {
}
}
},
ElicitResultSchema,
{ relatedTask: { taskId: task.taskId } } as unknown as TaskRequestOptions
);
@ -219,22 +214,19 @@ describe('Task Lifecycle Integration Tests', () => {
await client.connect(transport);
// Create a task
const createResult = await client.request(
{
method: 'tools/call',
params: {
name: 'long-task',
arguments: {
duration: 500,
shouldFail: false
},
task: {
ttl: 60_000
}
const createResult = await client.request({
method: 'tools/call',
params: {
name: 'long-task',
arguments: {
duration: 500,
shouldFail: false
},
task: {
ttl: 60_000
}
},
CreateTaskResultSchema
);
}
});
// Verify CreateTaskResult structure
expect(createResult).toHaveProperty('task');
@ -275,22 +267,19 @@ describe('Task Lifecycle Integration Tests', () => {
await client.connect(transport);
// Create a task that will fail
const createResult = await client.request(
{
method: 'tools/call',
params: {
name: 'long-task',
arguments: {
duration: 300,
shouldFail: true
},
task: {
ttl: 60_000
}
const createResult = await client.request({
method: 'tools/call',
params: {
name: 'long-task',
arguments: {
duration: 300,
shouldFail: true
},
task: {
ttl: 60_000
}
},
CreateTaskResultSchema
);
}
});
const taskId = createResult.task.taskId;
@ -320,21 +309,18 @@ describe('Task Lifecycle Integration Tests', () => {
await client.connect(transport);
// Create a long-running task
const createResult = await client.request(
{
method: 'tools/call',
params: {
name: 'long-task',
arguments: {
duration: 5000
},
task: {
ttl: 60_000
}
const createResult = await client.request({
method: 'tools/call',
params: {
name: 'long-task',
arguments: {
duration: 5000
},
task: {
ttl: 60_000
}
},
CreateTaskResultSchema
);
}
});
const taskId = createResult.task.taskId;
@ -369,21 +355,18 @@ describe('Task Lifecycle Integration Tests', () => {
await client.connect(transport);
// Create a quick task
const createResult = await client.request(
{
method: 'tools/call',
params: {
name: 'long-task',
arguments: {
duration: 100
},
task: {
ttl: 60_000
}
const createResult = await client.request({
method: 'tools/call',
params: {
name: 'long-task',
arguments: {
duration: 100
},
task: {
ttl: 60_000
}
},
CreateTaskResultSchema
);
}
});
const taskId = createResult.task.taskId;
@ -447,7 +430,6 @@ describe('Task Lifecycle Integration Tests', () => {
}
}
},
ElicitResultSchema,
{ relatedTask: { taskId: task.taskId } } as unknown as TaskRequestOptions
);
@ -521,21 +503,18 @@ describe('Task Lifecycle Integration Tests', () => {
await client.connect(transport);
// Create a task that will send 3 requests
const createResult = await client.request(
{
method: 'tools/call',
params: {
name: 'multi-request-task',
arguments: {
requestCount: 3
},
task: {
ttl: 60_000
}
const createResult = await client.request({
method: 'tools/call',
params: {
name: 'multi-request-task',
arguments: {
requestCount: 3
},
task: {
ttl: 60_000
}
},
CreateTaskResultSchema
);
}
});
const taskId = createResult.task.taskId;
@ -544,13 +523,10 @@ describe('Task Lifecycle Integration Tests', () => {
// Call tasks/result to receive all queued messages
// This should deliver all 3 elicitation requests in order
const result = await client.request(
{
method: 'tasks/result',
params: { taskId }
},
CallToolResultSchema
);
const result = await client.request({
method: 'tasks/result',
params: { taskId }
});
// Verify all messages were delivered in order
expect(receivedMessages.length).toBe(3);
@ -562,13 +538,10 @@ describe('Task Lifecycle Integration Tests', () => {
expect(result.content).toEqual([{ type: 'text', text: 'Received responses: Response 1, Response 2, Response 3' }]);
// Verify task is completed
const task = await client.request(
{
method: 'tasks/get',
params: { taskId }
},
TaskSchema
);
const task = await client.request({
method: 'tasks/get',
params: { taskId }
});
expect(task.status).toBe('completed');
await transport.close();
@ -619,19 +592,16 @@ describe('Task Lifecycle Integration Tests', () => {
await elicitClient.connect(transport);
// Phase 1: Create task
const createResult = await elicitClient.request(
{
method: 'tools/call',
params: {
name: 'input-task',
arguments: {},
task: {
ttl: 60_000
}
const createResult = await elicitClient.request({
method: 'tools/call',
params: {
name: 'input-task',
arguments: {},
task: {
ttl: 60_000
}
},
CreateTaskResultSchema
);
}
});
const taskId = createResult.task.taskId;
expect(createResult.task.status).toBe('working');
@ -639,13 +609,10 @@ describe('Task Lifecycle Integration Tests', () => {
// Phase 2: Wait for server to queue elicitation and update status
const task = await waitForTaskStatus(
id =>
elicitClient.request(
{
method: 'tasks/get',
params: { taskId: id }
},
TaskSchema
),
elicitClient.request({
method: 'tasks/get',
params: { taskId: id }
}),
taskId,
'input_required',
{
@ -662,13 +629,10 @@ describe('Task Lifecycle Integration Tests', () => {
// - Client handler responds
// - Server receives response, completes task
// - Return final result
const result = await elicitClient.request(
{
method: 'tasks/result',
params: { taskId }
},
CallToolResultSchema
);
const result = await elicitClient.request({
method: 'tasks/result',
params: { taskId }
});
// Verify elicitation was received and processed
expect(elicitationReceived).toBe(true);
@ -681,13 +645,10 @@ describe('Task Lifecycle Integration Tests', () => {
expect(result.content).toEqual([{ type: 'text', text: 'Hello, TestUser!' }]);
// Verify task is now completed
const finalTask = await elicitClient.request(
{
method: 'tasks/get',
params: { taskId }
},
TaskSchema
);
const finalTask = await elicitClient.request({
method: 'tasks/get',
params: { taskId }
});
expect(finalTask.status).toBe('completed');
await transport.close();
@ -707,21 +668,18 @@ describe('Task Lifecycle Integration Tests', () => {
// Create multiple tasks
const taskIds: string[] = [];
for (let i = 0; i < 3; i++) {
const createResult = await client.request(
{
method: 'tools/call',
params: {
name: 'long-task',
arguments: {
duration: 1000
},
task: {
ttl: 60_000
}
const createResult = await client.request({
method: 'tools/call',
params: {
name: 'long-task',
arguments: {
duration: 1000
},
task: {
ttl: 60_000
}
},
CreateTaskResultSchema
);
}
});
taskIds.push(createResult.task.taskId);
}
@ -745,21 +703,18 @@ describe('Task Lifecycle Integration Tests', () => {
// Create 15 tasks (more than page size of 10)
for (let i = 0; i < 15; i++) {
await client.request(
{
method: 'tools/call',
params: {
name: 'long-task',
arguments: {
duration: 5000
},
task: {
ttl: 60_000
}
await client.request({
method: 'tools/call',
params: {
name: 'long-task',
arguments: {
duration: 5000
},
task: {
ttl: 60_000
}
},
CreateTaskResultSchema
);
}
});
}
// Get first page using taskStore
@ -829,13 +784,10 @@ describe('Task Lifecycle Integration Tests', () => {
// Try to get result of non-existent task via tasks/result request
await expect(
client.request(
{
method: 'tasks/result',
params: { taskId: 'non-existent-task-id' }
},
CallToolResultSchema
)
client.request({
method: 'tasks/result',
params: { taskId: 'non-existent-task-id' }
})
).rejects.toSatisfy((error: ProtocolError) => {
expect(error).toBeInstanceOf(ProtocolError);
expect(error.code).toBe(ProtocolErrorCode.InvalidParams);
@ -858,21 +810,18 @@ describe('Task Lifecycle Integration Tests', () => {
await client.connect(transport);
// Create a task with specific TTL
const createResult = await client.request(
{
method: 'tools/call',
params: {
name: 'long-task',
arguments: {
duration: 100
},
task: {
ttl: 5000
}
const createResult = await client.request({
method: 'tools/call',
params: {
name: 'long-task',
arguments: {
duration: 100
},
task: {
ttl: 5000
}
},
CreateTaskResultSchema
);
}
});
const taskId = createResult.task.taskId;
@ -880,13 +829,10 @@ describe('Task Lifecycle Integration Tests', () => {
expect(createResult.task.ttl).toBe(60_000); // The task store uses 60000 as default
// Task should exist
const task = await client.request(
{
method: 'tasks/get',
params: { taskId }
},
TaskSchema
);
const task = await client.request({
method: 'tasks/get',
params: { taskId }
});
expect(task).toBeDefined();
expect(task.ttl).toBe(60_000);
@ -937,7 +883,6 @@ describe('Task Lifecycle Integration Tests', () => {
}
}
},
ElicitResultSchema,
{ relatedTask: { taskId: task.taskId } } as unknown as TaskRequestOptions
)
.catch(() => {
@ -1000,21 +945,18 @@ describe('Task Lifecycle Integration Tests', () => {
await client.connect(transport);
// Create a task that will queue messages
const createResult = await client.request(
{
method: 'tools/call',
params: {
name: 'cancellable-task',
arguments: {
messageCount: 2
},
task: {
ttl: 60_000
}
const createResult = await client.request({
method: 'tools/call',
params: {
name: 'cancellable-task',
arguments: {
messageCount: 2
},
task: {
ttl: 60_000
}
},
CreateTaskResultSchema
);
}
});
const taskId = createResult.task.taskId;
@ -1022,33 +964,24 @@ describe('Task Lifecycle Integration Tests', () => {
await new Promise(resolve => setTimeout(resolve, 200));
// Verify task is in input_required state and messages are queued
let task = await client.request(
{
method: 'tasks/get',
params: { taskId }
},
TaskSchema
);
let task = await client.request({
method: 'tasks/get',
params: { taskId }
});
expect(task.status).toBe('input_required');
// Cancel the task before calling tasks/result using the proper tasks/cancel request
// This will trigger queue cleanup via _clearTaskQueue in the handler
await client.request(
{
method: 'tasks/cancel',
params: { taskId }
},
z.object({ _meta: z.record(z.string(), z.unknown()).optional() })
);
await client.request({
method: 'tasks/cancel',
params: { taskId }
});
// Verify task is cancelled
task = await client.request(
{
method: 'tasks/get',
params: { taskId }
},
TaskSchema
);
task = await client.request({
method: 'tasks/get',
params: { taskId }
});
expect(task.status).toBe('cancelled');
// Attempt to call tasks/result
@ -1056,13 +989,10 @@ describe('Task Lifecycle Integration Tests', () => {
// and reject any pending message delivery promises, meaning no further
// messages should be delivered for a cancelled task.
try {
await client.request(
{
method: 'tasks/result',
params: { taskId }
},
CallToolResultSchema
);
await client.request({
method: 'tasks/result',
params: { taskId }
});
} catch {
// tasks/result might throw an error for cancelled tasks without a result
// This is acceptable behavior
@ -1073,13 +1003,10 @@ describe('Task Lifecycle Integration Tests', () => {
// Verify queue remains cleared on subsequent calls
try {
await client.request(
{
method: 'tasks/result',
params: { taskId }
},
CallToolResultSchema
);
await client.request({
method: 'tasks/result',
params: { taskId }
});
} catch {
// Expected - task is cancelled
}
@ -1136,7 +1063,6 @@ describe('Task Lifecycle Integration Tests', () => {
}
}
},
ElicitResultSchema,
{ relatedTask: { taskId: task.taskId } } as unknown as TaskRequestOptions
);
@ -1227,45 +1153,36 @@ describe('Task Lifecycle Integration Tests', () => {
await client.connect(transport);
// Create a task that will send messages over time
const createResult = await client.request(
{
method: 'tools/call',
params: {
name: 'streaming-task',
arguments: {
messageCount: 3,
delayBetweenMessages: 300
},
task: {
ttl: 60_000
}
const createResult = await client.request({
method: 'tools/call',
params: {
name: 'streaming-task',
arguments: {
messageCount: 3,
delayBetweenMessages: 300
},
task: {
ttl: 60_000
}
},
CreateTaskResultSchema
);
}
});
const taskId = createResult.task.taskId;
// Verify task is in working status
let task = await client.request(
{
method: 'tasks/get',
params: { taskId }
},
TaskSchema
);
let task = await client.request({
method: 'tasks/get',
params: { taskId }
});
expect(task.status).toBe('working');
// Call tasks/result immediately (before messages are queued)
// This should block and deliver messages as they arrive
tasksResultStartTime = Date.now();
const resultPromise = client.request(
{
method: 'tasks/result',
params: { taskId }
},
CallToolResultSchema
);
const resultPromise = client.request({
method: 'tasks/result',
params: { taskId }
});
// Wait for the task to complete and get the result
const result = await resultPromise;
@ -1295,13 +1212,10 @@ describe('Task Lifecycle Integration Tests', () => {
expect(result.content).toEqual([{ type: 'text', text: 'Received all responses: Response 1, Response 2, Response 3' }]);
// Verify task is now completed
task = await client.request(
{
method: 'tasks/get',
params: { taskId }
},
TaskSchema
);
task = await client.request({
method: 'tasks/get',
params: { taskId }
});
expect(task.status).toBe('completed');
await transport.close();
@ -1351,7 +1265,6 @@ describe('Task Lifecycle Integration Tests', () => {
}
}
},
ElicitResultSchema,
{ relatedTask: { taskId: task.taskId } } as unknown as TaskRequestOptions
)
.catch(() => {});
@ -1433,21 +1346,18 @@ describe('Task Lifecycle Integration Tests', () => {
await client.connect(transport);
// Create a task that will complete quickly with queued messages
const createResult = await client.request(
{
method: 'tools/call',
params: {
name: 'quick-complete-task',
arguments: {
messageCount: 2
},
task: {
ttl: 60_000
}
const createResult = await client.request({
method: 'tools/call',
params: {
name: 'quick-complete-task',
arguments: {
messageCount: 2
},
task: {
ttl: 60_000
}
},
CreateTaskResultSchema
);
}
});
const taskId = createResult.task.taskId;
@ -1458,13 +1368,10 @@ describe('Task Lifecycle Integration Tests', () => {
expect(task.status).toBe('completed');
// Call tasks/result - should deliver queued messages followed by final result
const result = await client.request(
{
method: 'tasks/result',
params: { taskId }
},
CallToolResultSchema
);
const result = await client.request({
method: 'tasks/result',
params: { taskId }
});
// Verify all queued messages were delivered before the final result
expect(receivedMessages.length).toBe(2);
@ -1477,13 +1384,10 @@ describe('Task Lifecycle Integration Tests', () => {
// Verify queue is cleaned up - calling tasks/result again should only return the result
receivedMessages.length = 0; // Clear the array
const result2 = await client.request(
{
method: 'tasks/result',
params: { taskId }
},
CallToolResultSchema
);
const result2 = await client.request({
method: 'tasks/result',
params: { taskId }
});
// No messages should be delivered on second call (queue was cleaned up)
expect(receivedMessages.length).toBe(0);
@ -1505,21 +1409,18 @@ describe('Task Lifecycle Integration Tests', () => {
// Create multiple tasks concurrently
const promises = Array.from({ length: 5 }, () =>
client.request(
{
method: 'tools/call',
params: {
name: 'long-task',
arguments: {
duration: 500
},
task: {
ttl: 60_000
}
client.request({
method: 'tools/call',
params: {
name: 'long-task',
arguments: {
duration: 500
},
task: {
ttl: 60_000
}
},
CreateTaskResultSchema
)
}
})
);
const results = await Promise.all(promises);
@ -1546,33 +1447,27 @@ describe('Task Lifecycle Integration Tests', () => {
await client.connect(transport);
// Create a task
const createResult = await client.request(
{
method: 'tools/call',
params: {
name: 'long-task',
arguments: {
duration: 2000
},
task: {
ttl: 60_000
}
const createResult = await client.request({
method: 'tools/call',
params: {
name: 'long-task',
arguments: {
duration: 2000
},
task: {
ttl: 60_000
}
},
CreateTaskResultSchema
);
}
});
const taskId = createResult.task.taskId;
// Perform multiple concurrent gets
const getPromises = Array.from({ length: 5 }, () =>
client.request(
{
method: 'tasks/get',
params: { taskId }
},
TaskSchema
)
client.request({
method: 'tasks/get',
params: { taskId }
})
);
const tasks = await Promise.all(getPromises);

View file

@ -5,7 +5,7 @@ import { createServer } from 'node:http';
import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';
import type { EventStore, JSONRPCMessage } from '@modelcontextprotocol/server';
import { CallToolResultSchema, McpServer } from '@modelcontextprotocol/server';
import { McpServer } from '@modelcontextprotocol/server';
import { listenOnRandomPort } from '@modelcontextprotocol/test-helpers';
import * as z from 'zod/v4';
@ -220,7 +220,6 @@ describe('Zod v4', () => {
}
}
},
CallToolResultSchema,
{
resumptionToken: lastEventId,
onresumptiontoken: onLastEventIdUpdate