fix: replace schema-based handlers with string method names (#1446)

This commit is contained in:
Matt 2026-02-02 23:57:07 +00:00 • committed by GitHub
commit f6e8204b86
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 490 additions and 878 deletions

View file

@ -1,191 +1,19 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable no-constant-binary-expression */
/* eslint-disable @typescript-eslint/no-unused-expressions */
import { Client, getSupportedElicitationModes } from '@modelcontextprotocol/client';
import type { Prompt, Resource, Tool, Transport } from '@modelcontextprotocol/core';
import {
CallToolRequestSchema,
CallToolResultSchema,
CreateMessageRequestSchema,
CreateTaskResultSchema,
ElicitRequestSchema,
ElicitResultSchema,
ErrorCode,
InitializeRequestSchema,
InMemoryTransport,
LATEST_PROTOCOL_VERSION,
ListPromptsRequestSchema,
ListResourcesRequestSchema,
ListRootsRequestSchema,
ListToolsRequestSchema,
ListToolsResultSchema,
McpError,
NotificationSchema,
RequestSchema,
ResultSchema,
SUPPORTED_PROTOCOL_VERSIONS
} from '@modelcontextprotocol/core';
import { InMemoryTaskStore, McpServer, Server } from '@modelcontextprotocol/server';
import * as z3 from 'zod/v3';
import * as z4 from 'zod/v4';
describe('Zod v4', () => {
/***
* Test: Type Checking
* Test that custom request/notification/result schemas can be used with the Client class.
*/
test('should typecheck', () => {
const GetWeatherRequestSchema = RequestSchema.extend({
method: z4.literal('weather/get'),
params: z4.object({
city: z4.string()
})
});
const GetForecastRequestSchema = RequestSchema.extend({
method: z4.literal('weather/forecast'),
params: z4.object({
city: z4.string(),
days: z4.number()
})
});
const WeatherForecastNotificationSchema = NotificationSchema.extend({
method: z4.literal('weather/alert'),
params: z4.object({
severity: z4.enum(['warning', 'watch']),
message: z4.string()
})
});
const WeatherRequestSchema = GetWeatherRequestSchema.or(GetForecastRequestSchema);
const WeatherNotificationSchema = WeatherForecastNotificationSchema;
const WeatherResultSchema = ResultSchema.extend({
temperature: z4.number(),
conditions: z4.string()
});
type WeatherRequest = z4.infer<typeof WeatherRequestSchema>;
type WeatherNotification = z4.infer<typeof WeatherNotificationSchema>;
type WeatherResult = z4.infer<typeof WeatherResultSchema>;
// Create a typed Client for weather data
const weatherClient = new Client<WeatherRequest, WeatherNotification, WeatherResult>(
{
name: 'WeatherClient',
version: '1.0.0'
},
{
capabilities: {
sampling: {}
}
}
);
// Typecheck that only valid weather requests/notifications/results are allowed
false &&
weatherClient.request(
{
method: 'weather/get',
params: {
city: 'Seattle'
}
},
WeatherResultSchema
);
false &&
weatherClient.notification({
method: 'weather/alert',
params: {
severity: 'warning',
message: 'Storm approaching'
}
});
});
});
describe('Zod v3', () => {
/***
* Test: Type Checking
* Test that custom request/notification/result schemas can be used with the Client class.
*/
test('should typecheck', () => {
const GetWeatherRequestSchema = z3.object({
...RequestSchema.shape,
method: z3.literal('weather/get'),
params: z3.object({
city: z3.string()
})
});
const GetForecastRequestSchema = z3.object({
...RequestSchema.shape,
method: z3.literal('weather/forecast'),
params: z3.object({
city: z3.string(),
days: z3.number()
})
});
const WeatherForecastNotificationSchema = z3.object({
...NotificationSchema.shape,
method: z3.literal('weather/alert'),
params: z3.object({
severity: z3.enum(['warning', 'watch']),
message: z3.string()
})
});
const WeatherRequestSchema = GetWeatherRequestSchema.or(GetForecastRequestSchema);
const WeatherNotificationSchema = WeatherForecastNotificationSchema;
const WeatherResultSchema = z3.object({
...ResultSchema.shape,
_meta: z3.record(z3.string(), z3.unknown()).optional(),
temperature: z3.number(),
conditions: z3.string()
});
type WeatherRequest = z3.infer<typeof WeatherRequestSchema>;
type WeatherNotification = z3.infer<typeof WeatherNotificationSchema>;
type WeatherResult = z3.infer<typeof WeatherResultSchema>;
// Create a typed Client for weather data
const weatherClient = new Client<WeatherRequest, WeatherNotification, WeatherResult>(
{
name: 'WeatherClient',
version: '1.0.0'
},
{
capabilities: {
sampling: {}
}
}
);
// Typecheck that only valid weather requests/notifications/results are allowed
false &&
weatherClient.request(
{
method: 'weather/get',
params: {
city: 'Seattle'
}
},
WeatherResultSchema
);
false &&
weatherClient.notification({
method: 'weather/alert',
params: {
severity: 'warning',
message: 'Storm approaching'
}
});
});
});
/***
* Test: Initialize with Matching Protocol Version
*/
@ -356,7 +184,7 @@ test('should connect new client to old, supported server version', async () => {
}
);
server.setRequestHandler(InitializeRequestSchema, _request => ({
server.setRequestHandler('initialize', _request => ({
protocolVersion: OLD_VERSION,
capabilities: {
resources: {},
@ -368,11 +196,11 @@ test('should connect new client to old, supported server version', async () => {
}
}));
server.setRequestHandler(ListResourcesRequestSchema, () => ({
server.setRequestHandler('resources/list', () => ({
resources: []
}));
server.setRequestHandler(ListToolsRequestSchema, () => ({
server.setRequestHandler('tools/list', () => ({
tools: []
}));
@ -403,7 +231,6 @@ test('should connect new client to old, supported server version', async () => {
* Test: Version Negotiation with Old Client and Newer Server
*/
test('should negotiate version when client is old, and newer server supports its version', async () => {
const OLD_VERSION = SUPPORTED_PROTOCOL_VERSIONS[1];
const server = new Server(
{
name: 'new server',
@ -417,7 +244,7 @@ test('should negotiate version when client is old, and newer server supports its
}
);
server.setRequestHandler(InitializeRequestSchema, _request => ({
server.setRequestHandler('initialize', _request => ({
protocolVersion: LATEST_PROTOCOL_VERSION,
capabilities: {
resources: {},
@ -429,11 +256,11 @@ test('should negotiate version when client is old, and newer server supports its
}
}));
server.setRequestHandler(ListResourcesRequestSchema, () => ({
server.setRequestHandler('resources/list', () => ({
resources: []
}));
server.setRequestHandler(ListToolsRequestSchema, () => ({
server.setRequestHandler('tools/list', () => ({
tools: []
}));
@ -464,7 +291,6 @@ test('should negotiate version when client is old, and newer server supports its
* Test: Throw when Old Client and Server Version Mismatch
*/
test("should throw when client is old, and server doesn't support its version", async () => {
const OLD_VERSION = SUPPORTED_PROTOCOL_VERSIONS[1];
const FUTURE_VERSION = 'FUTURE_VERSION';
const server = new Server(
{
@ -479,7 +305,7 @@ test("should throw when client is old, and server doesn't support its version",
}
);
server.setRequestHandler(InitializeRequestSchema, _request => ({
server.setRequestHandler('initialize', _request => ({
protocolVersion: FUTURE_VERSION,
capabilities: {
resources: {},
@ -491,11 +317,11 @@ test("should throw when client is old, and server doesn't support its version",
}
}));
server.setRequestHandler(ListResourcesRequestSchema, () => ({
server.setRequestHandler('resources/list', () => ({
resources: []
}));
server.setRequestHandler(ListToolsRequestSchema, () => ({
server.setRequestHandler('tools/list', () => ({
tools: []
}));
@ -537,7 +363,7 @@ test('should respect server capabilities', async () => {
}
);
server.setRequestHandler(InitializeRequestSchema, _request => ({
server.setRequestHandler('initialize', _request => ({
protocolVersion: LATEST_PROTOCOL_VERSION,
capabilities: {
resources: {},
@ -549,11 +375,11 @@ test('should respect server capabilities', async () => {
}
}));
server.setRequestHandler(ListResourcesRequestSchema, () => ({
server.setRequestHandler('resources/list', () => ({
resources: []
}));
server.setRequestHandler(ListToolsRequestSchema, () => ({
server.setRequestHandler('tools/list', () => ({
tools: []
}));
@ -615,7 +441,7 @@ test('should return empty lists for missing capabilities by default', async () =
}
);
server.setRequestHandler(InitializeRequestSchema, _request => ({
server.setRequestHandler('initialize', _request => ({
protocolVersion: LATEST_PROTOCOL_VERSION,
capabilities: {
tools: {}
@ -626,7 +452,7 @@ test('should return empty lists for missing capabilities by default', async () =
}
}));
server.setRequestHandler(ListToolsRequestSchema, () => ({
server.setRequestHandler('tools/list', () => ({
tools: [{ name: 'test-tool', inputSchema: { type: 'object' } }]
}));
@ -653,7 +479,7 @@ test('should return empty lists for missing capabilities by default', async () =
// listTools should work and return actual tools
const toolsResult = await client.listTools();
expect(toolsResult.tools).toHaveLength(1);
expect(toolsResult.tools[0].name).toBe('test-tool');
expect(toolsResult.tools[0]!.name).toBe('test-tool');
// listPrompts should return empty list without sending request
const promptsResult = await client.listPrompts();
@ -780,7 +606,7 @@ test('should only allow setRequestHandler for declared capabilities', () => {
// This should work because sampling is a declared capability
expect(() => {
client.setRequestHandler(CreateMessageRequestSchema, () => ({
client.setRequestHandler('sampling/createMessage', () => ({
model: 'test-model',
role: 'assistant',
content: {
@ -792,7 +618,7 @@ test('should only allow setRequestHandler for declared capabilities', () => {
// This should throw because roots listing is not a declared capability
expect(() => {
client.setRequestHandler(ListRootsRequestSchema, () => ({}));
client.setRequestHandler('roots/list', () => ({}));
}).toThrow('Client does not support roots capability');
});
@ -811,7 +637,7 @@ test('should allow setRequestHandler for declared elicitation capability', () =>
// This should work because elicitation is a declared capability
expect(() => {
client.setRequestHandler(ElicitRequestSchema, () => ({
client.setRequestHandler('elicitation/create', () => ({
action: 'accept',
content: {
username: 'test-user',
@ -822,7 +648,7 @@ test('should allow setRequestHandler for declared elicitation capability', () =>
// This should throw because sampling is not a declared capability
expect(() => {
client.setRequestHandler(CreateMessageRequestSchema, () => ({
client.setRequestHandler('sampling/createMessage', () => ({
model: 'test-model',
role: 'assistant',
content: {
@ -862,7 +688,7 @@ test('should accept form-mode elicitation request when client advertises empty e
);
// Set up client handler for form-mode elicitation
client.setRequestHandler(ElicitRequestSchema, request => {
client.setRequestHandler('elicitation/create', request => {
expect(request.params.mode).toBe('form');
return {
action: 'accept',
@ -927,7 +753,7 @@ test('should reject form-mode elicitation when client only supports URL mode', a
const handler = vi.fn().mockResolvedValue({
action: 'cancel'
});
client.setRequestHandler(ElicitRequestSchema, handler);
client.setRequestHandler('elicitation/create', handler);
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
@ -1024,7 +850,7 @@ test('should reject missing-mode elicitation when client only supports URL mode'
const handler = vi.fn().mockResolvedValue({
action: 'cancel'
});
client.setRequestHandler(ElicitRequestSchema, handler);
client.setRequestHandler('elicitation/create', handler);
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]);
@ -1072,7 +898,7 @@ test('should reject URL-mode elicitation when client only supports form mode', a
const handler = vi.fn().mockResolvedValue({
action: 'cancel'
});
client.setRequestHandler(ElicitRequestSchema, handler);
client.setRequestHandler('elicitation/create', handler);
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
@ -1167,7 +993,7 @@ test('should apply defaults for form-mode elicitation when applyDefaults is enab
}
);
client.setRequestHandler(ElicitRequestSchema, request => {
client.setRequestHandler('elicitation/create', request => {
expect(request.params.mode).toBe('form');
return {
action: 'accept',
@ -1218,7 +1044,7 @@ test('should handle client cancelling a request', async () => {
);
// Set up server to delay responding to listResources
server.setRequestHandler(ListResourcesRequestSchema, async (request, extra) => {
server.setRequestHandler('resources/list', async () => {
await new Promise(resolve => setTimeout(resolve, 1000));
return {
resources: []
@ -1269,7 +1095,7 @@ test('should handle request timeout', async () => {
);
// Set up server with a delayed response
server.setRequestHandler(ListResourcesRequestSchema, async (_request, extra) => {
server.setRequestHandler('resources/list', async (_request, extra) => {
const timer = new Promise(resolve => {
const timeout = setTimeout(resolve, 100);
extra.signal.addEventListener('abort', () => clearTimeout(timeout));
@ -1636,13 +1462,13 @@ test('should not activate listChanged handler when server does not advertise cap
// Server with tools capability but WITHOUT listChanged
const server = new Server({ name: 'test-server', version: '1.0.0' }, { capabilities: { tools: {} } });
server.setRequestHandler(InitializeRequestSchema, async request => ({
server.setRequestHandler('initialize', async request => ({
protocolVersion: request.params.protocolVersion,
capabilities: { tools: {} }, // No listChanged: true
serverInfo: { name: 'test-server', version: '1.0.0' }
}));
server.setRequestHandler(ListToolsRequestSchema, async () => ({
server.setRequestHandler('tools/list', async () => ({
tools: [{ name: 'test-tool', inputSchema: { type: 'object' } }]
}));
@ -1685,13 +1511,13 @@ test('should activate listChanged handler when server advertises capability', as
// Server with tools.listChanged: true capability
const server = new Server({ name: 'test-server', version: '1.0.0' }, { capabilities: { tools: { listChanged: true } } });
server.setRequestHandler(InitializeRequestSchema, async request => ({
server.setRequestHandler('initialize', async request => ({
protocolVersion: request.params.protocolVersion,
capabilities: { tools: { listChanged: true } },
serverInfo: { name: 'test-server', version: '1.0.0' }
}));
server.setRequestHandler(ListToolsRequestSchema, async () => ({
server.setRequestHandler('tools/list', async () => ({
tools: [{ name: 'test-tool', inputSchema: { type: 'object' } }]
}));
@ -1738,7 +1564,7 @@ test('should not activate any handlers when server has no listChanged capabiliti
// Server with capabilities but NO listChanged for any
const server = new Server({ name: 'test-server', version: '1.0.0' }, { capabilities: { tools: {}, prompts: {}, resources: {} } });
server.setRequestHandler(InitializeRequestSchema, async request => ({
server.setRequestHandler('initialize', async request => ({
protocolVersion: request.params.protocolVersion,
capabilities: { tools: {}, prompts: {}, resources: {} },
serverInfo: { name: 'test-server', version: '1.0.0' }
@ -1797,17 +1623,17 @@ test('should handle partial listChanged capability support', async () => {
// Server with tools.listChanged: true but prompts without listChanged
const server = new Server({ name: 'test-server', version: '1.0.0' }, { capabilities: { tools: { listChanged: true }, prompts: {} } });
server.setRequestHandler(InitializeRequestSchema, async request => ({
server.setRequestHandler('initialize', async request => ({
protocolVersion: request.params.protocolVersion,
capabilities: { tools: { listChanged: true }, prompts: {} },
serverInfo: { name: 'test-server', version: '1.0.0' }
}));
server.setRequestHandler(ListToolsRequestSchema, async () => ({
server.setRequestHandler('tools/list', async () => ({
tools: [{ name: 'tool-1', inputSchema: { type: 'object' } }]
}));
server.setRequestHandler(ListPromptsRequestSchema, async () => ({
server.setRequestHandler('prompts/list', async () => ({
prompts: [{ name: 'prompt-1' }]
}));
@ -1864,7 +1690,7 @@ describe('outputSchema validation', () => {
);
// Set up server handlers
server.setRequestHandler(InitializeRequestSchema, async request => ({
server.setRequestHandler('initialize', async request => ({
protocolVersion: request.params.protocolVersion,
capabilities: {},
serverInfo: {
@ -1873,7 +1699,7 @@ describe('outputSchema validation', () => {
}
}));
server.setRequestHandler(ListToolsRequestSchema, async () => ({
server.setRequestHandler('tools/list', async () => ({
tools: [
{
name: 'test-tool',
@ -1895,7 +1721,7 @@ describe('outputSchema validation', () => {
]
}));
server.setRequestHandler(CallToolRequestSchema, async request => {
server.setRequestHandler('tools/call', async request => {
if (request.params.name === 'test-tool') {
return {
structuredContent: { result: 'success', count: 42 }
@ -1956,7 +1782,7 @@ describe('outputSchema validation', () => {
);
// Set up server handlers
server.setRequestHandler(InitializeRequestSchema, async request => ({
server.setRequestHandler('initialize', async request => ({
protocolVersion: request.params.protocolVersion,
capabilities: { tools: {} },
serverInfo: {
@ -1965,7 +1791,7 @@ describe('outputSchema validation', () => {
}
}));
server.setRequestHandler(ListToolsRequestSchema, async () => ({
server.setRequestHandler('tools/list', async () => ({
tools: [
{
name: 'test-tool',
@ -1987,7 +1813,7 @@ describe('outputSchema validation', () => {
]
}));
server.setRequestHandler(CallToolRequestSchema, async request => {
server.setRequestHandler('tools/call', async request => {
if (request.params.name === 'test-tool') {
// Return invalid structured content (count is string instead of number)
return {
@ -2048,7 +1874,7 @@ describe('outputSchema validation', () => {
);
// Set up server handlers
server.setRequestHandler(InitializeRequestSchema, async request => ({
server.setRequestHandler('initialize', async request => ({
protocolVersion: request.params.protocolVersion,
capabilities: { tools: {} },
serverInfo: {
@ -2057,7 +1883,7 @@ describe('outputSchema validation', () => {
}
}));
server.setRequestHandler(ListToolsRequestSchema, async () => ({
server.setRequestHandler('tools/list', async () => ({
tools: [
{
name: 'test-tool',
@ -2077,7 +1903,7 @@ describe('outputSchema validation', () => {
]
}));
server.setRequestHandler(CallToolRequestSchema, async request => {
server.setRequestHandler('tools/call', async request => {
if (request.params.name === 'test-tool') {
// Return content instead of structuredContent
return {
@ -2140,7 +1966,7 @@ describe('outputSchema validation', () => {
);
// Set up server handlers
server.setRequestHandler(InitializeRequestSchema, async request => ({
server.setRequestHandler('initialize', async request => ({
protocolVersion: request.params.protocolVersion,
capabilities: {},
serverInfo: {
@ -2149,7 +1975,7 @@ describe('outputSchema validation', () => {
}
}));
server.setRequestHandler(ListToolsRequestSchema, async () => ({
server.setRequestHandler('tools/list', async () => ({
tools: [
{
name: 'test-tool',
@ -2163,7 +1989,7 @@ describe('outputSchema validation', () => {
]
}));
server.setRequestHandler(CallToolRequestSchema, async request => {
server.setRequestHandler('tools/call', async request => {
if (request.params.name === 'test-tool') {
// Return regular content
return {
@ -2225,7 +2051,7 @@ describe('outputSchema validation', () => {
);
// Set up server handlers
server.setRequestHandler(InitializeRequestSchema, async request => ({
server.setRequestHandler('initialize', async request => ({
protocolVersion: request.params.protocolVersion,
capabilities: {},
serverInfo: {
@ -2234,7 +2060,7 @@ describe('outputSchema validation', () => {
}
}));
server.setRequestHandler(ListToolsRequestSchema, async () => ({
server.setRequestHandler('tools/list', async () => ({
tools: [
{
name: 'complex-tool',
@ -2269,7 +2095,7 @@ describe('outputSchema validation', () => {
]
}));
server.setRequestHandler(CallToolRequestSchema, async request => {
server.setRequestHandler('tools/call', async request => {
if (request.params.name === 'complex-tool') {
return {
structuredContent: {
@ -2341,7 +2167,7 @@ describe('outputSchema validation', () => {
);
// Set up server handlers
server.setRequestHandler(InitializeRequestSchema, async request => ({
server.setRequestHandler('initialize', async request => ({
protocolVersion: request.params.protocolVersion,
capabilities: { tools: {} },
serverInfo: {
@ -2350,7 +2176,7 @@ describe('outputSchema validation', () => {
}
}));
server.setRequestHandler(ListToolsRequestSchema, async () => ({
server.setRequestHandler('tools/list', async () => ({
tools: [
{
name: 'strict-tool',
@ -2371,7 +2197,7 @@ describe('outputSchema validation', () => {
]
}));
server.setRequestHandler(CallToolRequestSchema, async request => {
server.setRequestHandler('tools/call', async request => {
if (request.params.name === 'strict-tool') {
// Return structured content with extra property
return {
@ -2773,7 +2599,7 @@ describe('Task-based execution', () => {
}
);
client.setRequestHandler(ElicitRequestSchema, async (request, extra) => {
client.setRequestHandler('elicitation/create', async (request, extra) => {
const result = {
action: 'accept',
content: { username: 'list-user' }
@ -2866,7 +2692,7 @@ describe('Task-based execution', () => {
}
);
client.setRequestHandler(ElicitRequestSchema, async (request, extra) => {
client.setRequestHandler('elicitation/create', async (request, extra) => {
const result = {
action: 'accept',
content: { username: 'list-user' }
@ -2958,7 +2784,7 @@ describe('Task-based execution', () => {
}
);
client.setRequestHandler(ElicitRequestSchema, async (request, extra) => {
client.setRequestHandler('elicitation/create', async (request, extra) => {
const result = {
action: 'accept',
content: { username: 'result-user' }
@ -3049,7 +2875,7 @@ describe('Task-based execution', () => {
}
);
client.setRequestHandler(ElicitRequestSchema, async (request, extra) => {
client.setRequestHandler('elicitation/create', async (request, extra) => {
const result = {
action: 'accept',
content: { username: 'list-user' }
@ -3368,7 +3194,7 @@ describe('Task-based execution', () => {
}
);
client.setRequestHandler(ElicitRequestSchema, async () => ({
client.setRequestHandler('elicitation/create', async () => ({
action: 'accept',
content: { username: 'test' }
}));
@ -3520,7 +3346,7 @@ test('should expose requestStream() method for streaming responses', async () =>
}
);
server.setRequestHandler(CallToolRequestSchema, async () => {
server.setRequestHandler('tools/call', async () => {
return {
content: [{ type: 'text', text: 'Tool result' }]
};
@ -3585,7 +3411,7 @@ test('should expose callToolStream() method for streaming tool calls', async ()
}
);
server.setRequestHandler(CallToolRequestSchema, async () => {
server.setRequestHandler('tools/call', async () => {
return {
content: [{ type: 'text', text: 'Tool result' }]
};
@ -3640,7 +3466,7 @@ test('should validate structured output in callToolStream()', async () => {
}
);
server.setRequestHandler(ListToolsRequestSchema, async () => {
server.setRequestHandler('tools/list', async () => {
return {
tools: [
{
@ -3662,7 +3488,7 @@ test('should validate structured output in callToolStream()', async () => {
};
});
server.setRequestHandler(CallToolRequestSchema, async () => {
server.setRequestHandler('tools/call', async () => {
return {
content: [{ type: 'text', text: 'Result' }],
structuredContent: { value: 42 }
@ -3718,7 +3544,7 @@ test('callToolStream() should yield error when structuredContent does not match
}
);
server.setRequestHandler(ListToolsRequestSchema, async () => ({
server.setRequestHandler('tools/list', async () => ({
tools: [
{
name: 'test-tool',
@ -3740,7 +3566,7 @@ test('callToolStream() should yield error when structuredContent does not match
]
}));
server.setRequestHandler(CallToolRequestSchema, async () => {
server.setRequestHandler('tools/call', async () => {
// Return invalid structured content (count is string instead of number)
return {
structuredContent: { result: 'success', count: 'not a number' }
@ -3794,7 +3620,7 @@ test('callToolStream() should yield error when tool with outputSchema returns no
}
);
server.setRequestHandler(ListToolsRequestSchema, async () => ({
server.setRequestHandler('tools/list', async () => ({
tools: [
{
name: 'test-tool',
@ -3814,7 +3640,7 @@ test('callToolStream() should yield error when tool with outputSchema returns no
]
}));
server.setRequestHandler(CallToolRequestSchema, async () => {
server.setRequestHandler('tools/call', async () => {
return {
content: [{ type: 'text', text: 'This should be structured content' }]
};
@ -3866,7 +3692,7 @@ test('callToolStream() should handle tools without outputSchema normally', async
}
);
server.setRequestHandler(ListToolsRequestSchema, async () => ({
server.setRequestHandler('tools/list', async () => ({
tools: [
{
name: 'test-tool',
@ -3879,7 +3705,7 @@ test('callToolStream() should handle tools without outputSchema normally', async
]
}));
server.setRequestHandler(CallToolRequestSchema, async () => {
server.setRequestHandler('tools/call', async () => {
return {
content: [{ type: 'text', text: 'Normal response' }]
};
@ -3931,7 +3757,7 @@ test('callToolStream() should handle complex JSON schema validation', async () =
}
);
server.setRequestHandler(ListToolsRequestSchema, async () => ({
server.setRequestHandler('tools/list', async () => ({
tools: [
{
name: 'complex-tool',
@ -3966,7 +3792,7 @@ test('callToolStream() should handle complex JSON schema validation', async () =
]
}));
server.setRequestHandler(CallToolRequestSchema, async () => {
server.setRequestHandler('tools/call', async () => {
return {
structuredContent: {
name: 'John Doe',
@ -4029,7 +3855,7 @@ test('callToolStream() should yield error with additional properties when not al
}
);
server.setRequestHandler(ListToolsRequestSchema, async () => ({
server.setRequestHandler('tools/list', async () => ({
tools: [
{
name: 'strict-tool',
@ -4050,7 +3876,7 @@ test('callToolStream() should yield error with additional properties when not al
]
}));
server.setRequestHandler(CallToolRequestSchema, async () => {
server.setRequestHandler('tools/call', async () => {
return {
structuredContent: {
name: 'John',
@ -4105,7 +3931,7 @@ test('callToolStream() should not validate structuredContent when isError is tru
}
);
server.setRequestHandler(ListToolsRequestSchema, async () => ({
server.setRequestHandler('tools/list', async () => ({
tools: [
{
name: 'test-tool',
@ -4125,7 +3951,7 @@ test('callToolStream() should not validate structuredContent when isError is tru
]
}));
server.setRequestHandler(CallToolRequestSchema, async () => {
server.setRequestHandler('tools/call', async () => {
// Return isError with content (no structuredContent) - should NOT trigger validation error
return {
isError: true,
@ -4213,7 +4039,7 @@ describe('Client sampling validation with tools', () => {
const client = new Client({ name: 'test client', version: '1.0' }, { capabilities: { sampling: { tools: {} } } });
// Handler returns array content with tool_use - should validate with CreateMessageResultWithToolsSchema
client.setRequestHandler(CreateMessageRequestSchema, async () => ({
client.setRequestHandler('sampling/createMessage', async () => ({
model: 'test-model',
role: 'assistant',
stopReason: 'toolUse',
@ -4231,7 +4057,7 @@ describe('Client sampling validation with tools', () => {
expect(result.stopReason).toBe('toolUse');
expect(Array.isArray(result.content)).toBe(true);
expect((result.content as Array<{ type: string }>)[0].type).toBe('tool_use');
expect((result.content as Array<{ type: string }>)[0]!.type).toBe('tool_use');
});
test('should validate single content when request includes tools', async () => {
@ -4240,7 +4066,7 @@ describe('Client sampling validation with tools', () => {
const client = new Client({ name: 'test client', version: '1.0' }, { capabilities: { sampling: { tools: {} } } });
// Handler returns single content (text) - should still validate with CreateMessageResultWithToolsSchema
client.setRequestHandler(CreateMessageRequestSchema, async () => ({
client.setRequestHandler('sampling/createMessage', async () => ({
model: 'test-model',
role: 'assistant',
content: { type: 'text', text: 'No tool needed' }
@ -4264,7 +4090,7 @@ describe('Client sampling validation with tools', () => {
const client = new Client({ name: 'test client', version: '1.0' }, { capabilities: { sampling: {} } });
// Handler returns single content - should validate with CreateMessageResultSchema
client.setRequestHandler(CreateMessageRequestSchema, async () => ({
client.setRequestHandler('sampling/createMessage', async () => ({
model: 'test-model',
role: 'assistant',
content: { type: 'text', text: 'Response' }
@ -4287,7 +4113,7 @@ describe('Client sampling validation with tools', () => {
const client = new Client({ name: 'test client', version: '1.0' }, { capabilities: { sampling: {} } });
// Handler returns array content - should fail validation with CreateMessageResultSchema
client.setRequestHandler(CreateMessageRequestSchema, async () => ({
client.setRequestHandler('sampling/createMessage', async () => ({
model: 'test-model',
role: 'assistant',
content: [{ type: 'text', text: 'Array response' }]
@ -4310,7 +4136,7 @@ describe('Client sampling validation with tools', () => {
const client = new Client({ name: 'test client', version: '1.0' }, { capabilities: { sampling: { tools: {} } } });
// Handler returns array content with tool_use
client.setRequestHandler(CreateMessageRequestSchema, async () => ({
client.setRequestHandler('sampling/createMessage', async () => ({
model: 'test-model',
role: 'assistant',
stopReason: 'toolUse',

View file

@ -2,7 +2,7 @@ import path from 'node:path';
import { Readable, Writable } from 'node:stream';
import { Client, StdioClientTransport } from '@modelcontextprotocol/client';
import { LoggingMessageNotificationSchema, Server, StdioServerTransport } from '@modelcontextprotocol/server';
import { Server, StdioServerTransport } from '@modelcontextprotocol/server';
// Use the local fixtures directory alongside this test file
const FIXTURES_DIR = path.resolve(__dirname, './__fixtures__');
@ -89,7 +89,7 @@ describe('Process cleanup', () => {
await client.connect(transport);
await client.setLoggingLevel('debug');
client.setNotificationHandler(LoggingMessageNotificationSchema, notification => {
client.setNotificationHandler('notifications/message', notification => {
console.debug('server log: ' + notification.params.data);
});
const serverPid = transport.pid!;

View file

@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { Client } from '@modelcontextprotocol/client';
import type {
AnyObjectSchema,
ElicitRequestSchema,
JsonSchemaType,
JsonSchemaValidator,
jsonSchemaValidator,
@ -9,87 +9,31 @@ import type {
Transport
} from '@modelcontextprotocol/core';
import {
CallToolRequestSchema,
CallToolResultSchema,
CreateMessageRequestSchema,
CreateMessageResultSchema,
CreateTaskResultSchema,
ElicitationCompleteNotificationSchema,
ElicitRequestSchema,
ElicitResultSchema,
ErrorCode,
InMemoryTransport,
LATEST_PROTOCOL_VERSION,
ListPromptsRequestSchema,
ListResourcesRequestSchema,
ListToolsRequestSchema,
McpError,
NotificationSchema,
RequestSchema,
ResultSchema,
SetLevelRequestSchema,
SUPPORTED_PROTOCOL_VERSIONS
} from '@modelcontextprotocol/core';
import { createMcpExpressApp } from '@modelcontextprotocol/express';
import { InMemoryTaskStore, McpServer, Server } from '@modelcontextprotocol/server';
import type { Request, Response } from 'express';
import supertest from 'supertest';
import * as z3 from 'zod/v3';
import * as z4 from 'zod/v4';
describe('Zod v3', () => {
describe('Server with standard protocol methods', () => {
/*
Test that custom request/notification/result schemas can be used with the Server class.
Test that Server class works with standard protocol method handlers.
*/
test('should typecheck', () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const GetWeatherRequestSchema = (RequestSchema as unknown as z3.ZodObject<any>).extend({
method: z3.literal('weather/get'),
params: z3.object({
city: z3.string()
})
}) as AnyObjectSchema;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const GetForecastRequestSchema = (RequestSchema as unknown as z3.ZodObject<any>).extend({
method: z3.literal('weather/forecast'),
params: z3.object({
city: z3.string(),
days: z3.number()
})
}) as AnyObjectSchema;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const WeatherForecastNotificationSchema = (NotificationSchema as unknown as z3.ZodObject<any>).extend({
method: z3.literal('weather/alert'),
params: z3.object({
severity: z3.enum(['warning', 'watch']),
message: z3.string()
})
}) as AnyObjectSchema;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const WeatherRequestSchema = (GetWeatherRequestSchema as unknown as z3.ZodObject<any>).or(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
GetForecastRequestSchema as unknown as z3.ZodObject<any>
) as AnyObjectSchema;
const WeatherNotificationSchema = WeatherForecastNotificationSchema as AnyObjectSchema;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const WeatherResultSchema = (ResultSchema as unknown as z3.ZodObject<any>).extend({
temperature: z3.number(),
conditions: z3.string()
}) as AnyObjectSchema;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type InferSchema<T> = T extends z3.ZodType<infer Output, any, any> ? Output : never;
type WeatherRequest = InferSchema<typeof WeatherRequestSchema>;
type WeatherNotification = InferSchema<typeof WeatherNotificationSchema>;
type WeatherResult = InferSchema<typeof WeatherResultSchema>;
// Create a typed Server for weather data
const weatherServer = new Server<WeatherRequest, WeatherNotification, WeatherResult>(
test('should typecheck with standard protocol methods', () => {
// Create a Server with default types
const server = new Server(
{
name: 'WeatherServer',
name: 'TestServer',
version: '1.0.0'
},
{
@ -102,85 +46,37 @@ describe('Zod v3', () => {
}
);
// Typecheck that only valid weather requests/notifications/results are allowed
weatherServer.setRequestHandler(GetWeatherRequestSchema, _request => {
return {
temperature: 72,
conditions: 'sunny'
};
// Register handlers using method strings
server.setRequestHandler('ping', _request => {
return {};
});
weatherServer.setNotificationHandler(WeatherForecastNotificationSchema, notification => {
// Type assertion needed for v3/v4 schema mixing
const params = notification.params as { message: string; severity: 'warning' | 'watch' };
console.log(`Weather alert: ${params.message}`);
server.setNotificationHandler('notifications/initialized', () => {
console.log('Client initialized');
});
});
});
describe('Zod v4', () => {
test('should typecheck', () => {
const GetWeatherRequestSchema = RequestSchema.extend({
method: z4.literal('weather/get'),
params: z4.object({
city: z4.string()
})
});
const GetForecastRequestSchema = RequestSchema.extend({
method: z4.literal('weather/forecast'),
params: z4.object({
city: z4.string(),
days: z4.number()
})
});
const WeatherForecastNotificationSchema = NotificationSchema.extend({
method: z4.literal('weather/alert'),
params: z4.object({
severity: z4.enum(['warning', 'watch']),
message: z4.string()
})
});
const WeatherRequestSchema = GetWeatherRequestSchema.or(GetForecastRequestSchema);
const WeatherNotificationSchema = WeatherForecastNotificationSchema;
const WeatherResultSchema = ResultSchema.extend({
temperature: z4.number(),
conditions: z4.string()
});
type WeatherRequest = z4.infer<typeof WeatherRequestSchema>;
type WeatherNotification = z4.infer<typeof WeatherNotificationSchema>;
type WeatherResult = z4.infer<typeof WeatherResultSchema>;
// Create a typed Server for weather data
const weatherServer = new Server<WeatherRequest, WeatherNotification, WeatherResult>(
describe('Server with tools capability', () => {
test('should register tools/list handler', () => {
const server = new Server(
{
name: 'WeatherServer',
name: 'ToolServer',
version: '1.0.0'
},
{
capabilities: {
prompts: {},
resources: {},
tools: {},
logging: {}
tools: {}
}
}
);
// Typecheck that only valid weather requests/notifications/results are allowed
weatherServer.setRequestHandler(GetWeatherRequestSchema, _request => {
// Register handler using method string
server.setRequestHandler('tools/list', _request => {
return {
temperature: 72,
conditions: 'sunny'
tools: []
};
});
weatherServer.setNotificationHandler(WeatherForecastNotificationSchema, notification => {
console.log(`Weather alert: ${notification.params.message}`);
});
});
});
@ -397,7 +293,7 @@ test('should respect client capabilities', async () => {
);
// Implement request handler for sampling/createMessage
client.setRequestHandler(CreateMessageRequestSchema, async _request => {
client.setRequestHandler('sampling/createMessage', async _request => {
// Mock implementation of createMessage
return {
model: 'test-model',
@ -456,7 +352,7 @@ test('should respect client elicitation capabilities', async () => {
}
);
client.setRequestHandler(ElicitRequestSchema, params => ({
client.setRequestHandler('elicitation/create', params => ({
action: 'accept',
content: {
username: params.params.message.includes('username') ? 'test-user' : undefined,
@ -540,7 +436,7 @@ test('should use elicitInput with mode: "form" by default for backwards compatib
}
);
client.setRequestHandler(ElicitRequestSchema, params => ({
client.setRequestHandler('elicitation/create', params => ({
action: 'accept',
content: {
username: params.params.message.includes('username') ? 'test-user' : undefined,
@ -619,7 +515,7 @@ test('should throw when elicitInput is called without client form capability', a
}
);
client.setRequestHandler(ElicitRequestSchema, () => ({
client.setRequestHandler('elicitation/create', () => ({
action: 'cancel'
}));
@ -668,7 +564,7 @@ test('should throw when elicitInput is called without client URL capability', as
}
);
client.setRequestHandler(ElicitRequestSchema, () => ({
client.setRequestHandler('elicitation/create', () => ({
action: 'cancel'
}));
@ -712,7 +608,7 @@ test('should include form mode when sending elicitation form requests', async ()
);
const receivedModes: string[] = [];
client.setRequestHandler(ElicitRequestSchema, request => {
client.setRequestHandler('elicitation/create', request => {
receivedModes.push(request.params.mode ?? '');
return {
action: 'accept',
@ -776,7 +672,7 @@ test('should include url mode when sending elicitation URL requests', async () =
const receivedModes: string[] = [];
const receivedIds: string[] = [];
client.setRequestHandler(ElicitRequestSchema, request => {
client.setRequestHandler('elicitation/create', request => {
receivedModes.push(request.params.mode ?? '');
if (request.params.mode === 'url') {
receivedIds.push(request.params.elicitationId);
@ -830,7 +726,7 @@ test('should reject elicitInput when client response violates requested schema',
}
);
client.setRequestHandler(ElicitRequestSchema, () => ({
client.setRequestHandler('elicitation/create', () => ({
action: 'accept',
// Bad response: missing required field `username`
@ -889,7 +785,7 @@ test('should wrap unexpected validator errors during elicitInput', async () => {
}
);
client.setRequestHandler(ElicitRequestSchema, () => ({
client.setRequestHandler('elicitation/create', () => ({
action: 'accept',
content: {
username: 'ignored'
@ -938,7 +834,7 @@ test('should forward notification options when using elicitation completion noti
}
);
client.setNotificationHandler(ElicitationCompleteNotificationSchema, () => {});
client.setNotificationHandler('notifications/elicitation/complete', () => {});
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
@ -986,7 +882,7 @@ test('should create notifier that emits elicitation completion notification', as
);
const receivedIds: string[] = [];
client.setNotificationHandler(ElicitationCompleteNotificationSchema, notification => {
client.setNotificationHandler('notifications/elicitation/complete', notification => {
receivedIds.push(notification.params.elicitationId);
});
@ -1152,7 +1048,7 @@ test('should validate elicitation response against requested schema', async () =
);
// Set up client to return valid response
client.setRequestHandler(ElicitRequestSchema, _request => ({
client.setRequestHandler('elicitation/create', _request => ({
action: 'accept',
content: {
name: 'John Doe',
@ -1230,7 +1126,7 @@ test('should reject elicitation response with invalid data', async () => {
);
// Set up client to return invalid response (missing required field, invalid age)
client.setRequestHandler(ElicitRequestSchema, _request => ({
client.setRequestHandler('elicitation/create', _request => ({
action: 'accept',
content: {
email: '', // Invalid - too short
@ -1300,7 +1196,7 @@ test('should allow elicitation reject and cancel without validation', async () =
);
let requestCount = 0;
client.setRequestHandler(ElicitRequestSchema, _request => {
client.setRequestHandler('elicitation/create', _request => {
requestCount++;
return requestCount === 1 ? { action: 'decline' } : { action: 'cancel' };
});
@ -1386,22 +1282,22 @@ test('should only allow setRequestHandler for declared capabilities', () => {
// These should work because the capabilities are declared
expect(() => {
server.setRequestHandler(ListPromptsRequestSchema, () => ({ prompts: [] }));
server.setRequestHandler('prompts/list', () => ({ prompts: [] }));
}).not.toThrow();
expect(() => {
server.setRequestHandler(ListResourcesRequestSchema, () => ({
server.setRequestHandler('resources/list', () => ({
resources: []
}));
}).not.toThrow();
// These should throw because the capabilities are not declared
expect(() => {
server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: [] }));
server.setRequestHandler('tools/list', () => ({ tools: [] }));
}).toThrow(/^Server does not support tools/);
expect(() => {
server.setRequestHandler(SetLevelRequestSchema, () => ({}));
server.setRequestHandler('logging/setLevel', () => ({}));
}).toThrow(/^Server does not support logging/);
});
@ -1429,7 +1325,7 @@ test('should handle server cancelling a request', async () => {
);
// Set up client to delay responding to createMessage
client.setRequestHandler(CreateMessageRequestSchema, async (_request, _extra) => {
client.setRequestHandler('sampling/createMessage', async (_request, _extra) => {
await new Promise(resolve => setTimeout(resolve, 1000));
return {
model: 'test',
@ -1488,7 +1384,7 @@ test('should handle request timeout', async () => {
}
);
client.setRequestHandler(CreateMessageRequestSchema, async (_request, extra) => {
client.setRequestHandler('sampling/createMessage', async (_request, extra) => {
await new Promise((resolve, reject) => {
const timeout = setTimeout(resolve, 100);
extra.signal.addEventListener('abort', () => {
@ -1600,7 +1496,7 @@ describe('createMessage validation', () => {
{ capabilities: { sampling: {} } } // No tools capability
);
client.setRequestHandler(CreateMessageRequestSchema, async () => ({
client.setRequestHandler('sampling/createMessage', async () => ({
model: 'test-model',
role: 'assistant',
content: { type: 'text', text: 'Response' }
@ -1626,7 +1522,7 @@ describe('createMessage validation', () => {
{ capabilities: { sampling: {} } } // No tools capability
);
client.setRequestHandler(CreateMessageRequestSchema, async () => ({
client.setRequestHandler('sampling/createMessage', async () => ({
model: 'test-model',
role: 'assistant',
content: { type: 'text', text: 'Response' }
@ -1649,7 +1545,7 @@ describe('createMessage validation', () => {
const client = new Client({ name: 'test client', version: '1.0' }, { capabilities: { sampling: { tools: {} } } });
client.setRequestHandler(CreateMessageRequestSchema, async () => ({
client.setRequestHandler('sampling/createMessage', async () => ({
model: 'test-model',
role: 'assistant',
content: { type: 'text', text: 'Response' }
@ -1682,7 +1578,7 @@ describe('createMessage validation', () => {
const client = new Client({ name: 'test client', version: '1.0' }, { capabilities: { sampling: { tools: {} } } });
client.setRequestHandler(CreateMessageRequestSchema, async () => ({
client.setRequestHandler('sampling/createMessage', async () => ({
model: 'test-model',
role: 'assistant',
content: { type: 'text', text: 'Response' }
@ -1709,7 +1605,7 @@ describe('createMessage validation', () => {
const client = new Client({ name: 'test client', version: '1.0' }, { capabilities: { sampling: { tools: {} } } });
client.setRequestHandler(CreateMessageRequestSchema, async () => ({
client.setRequestHandler('sampling/createMessage', async () => ({
model: 'test-model',
role: 'assistant',
content: { type: 'text', text: 'Response' }
@ -1736,7 +1632,7 @@ describe('createMessage validation', () => {
const client = new Client({ name: 'test client', version: '1.0' }, { capabilities: { sampling: { tools: {} } } });
client.setRequestHandler(CreateMessageRequestSchema, async () => ({
client.setRequestHandler('sampling/createMessage', async () => ({
model: 'test-model',
role: 'assistant',
content: { type: 'text', text: 'Response' }
@ -1759,7 +1655,7 @@ describe('createMessage validation', () => {
const client = new Client({ name: 'test client', version: '1.0' }, { capabilities: { sampling: { tools: {} } } });
client.setRequestHandler(CreateMessageRequestSchema, async () => ({
client.setRequestHandler('sampling/createMessage', async () => ({
model: 'test-model',
role: 'assistant',
content: { type: 'text', text: 'Response' }
@ -1786,7 +1682,7 @@ describe('createMessage validation', () => {
const client = new Client({ name: 'test client', version: '1.0' }, { capabilities: { sampling: { tools: {} } } });
client.setRequestHandler(CreateMessageRequestSchema, async () => ({
client.setRequestHandler('sampling/createMessage', async () => ({
model: 'test-model',
role: 'assistant',
content: { type: 'text', text: 'Response' }
@ -1814,7 +1710,7 @@ describe('createMessage validation', () => {
const client = new Client({ name: 'test client', version: '1.0' }, { capabilities: { sampling: { tools: {} } } });
client.setRequestHandler(CreateMessageRequestSchema, async () => ({
client.setRequestHandler('sampling/createMessage', async () => ({
model: 'test-model',
role: 'assistant',
content: { type: 'text', text: 'Response' }
@ -1851,7 +1747,7 @@ describe('createMessage validation', () => {
const client = new Client({ name: 'test client', version: '1.0' }, { capabilities: { sampling: { tools: {} } } });
client.setRequestHandler(CreateMessageRequestSchema, async () => ({
client.setRequestHandler('sampling/createMessage', async () => ({
model: 'test-model',
role: 'assistant',
content: { type: 'text', text: 'Response' }
@ -1879,7 +1775,7 @@ describe('createMessage validation', () => {
const client = new Client({ name: 'test client', version: '1.0' }, { capabilities: { sampling: { tools: {} } } });
client.setRequestHandler(CreateMessageRequestSchema, async () => ({
client.setRequestHandler('sampling/createMessage', async () => ({
model: 'test-model',
role: 'assistant',
content: { type: 'text', text: 'Response' }
@ -1907,7 +1803,7 @@ describe('createMessage validation', () => {
const client = new Client({ name: 'test client', version: '1.0' }, { capabilities: { sampling: {} } });
client.setRequestHandler(CreateMessageRequestSchema, async () => ({
client.setRequestHandler('sampling/createMessage', async () => ({
model: 'test-model',
role: 'assistant',
content: { type: 'text', text: 'Response' }
@ -1933,7 +1829,7 @@ describe('createMessage backwards compatibility', () => {
const client = new Client({ name: 'test client', version: '1.0' }, { capabilities: { sampling: {} } });
// Mock client returns single text content
client.setRequestHandler(CreateMessageRequestSchema, async () => ({
client.setRequestHandler('sampling/createMessage', async () => ({
model: 'test-model',
role: 'assistant',
content: { type: 'text', text: 'Hello from LLM' }
@ -1963,7 +1859,7 @@ describe('createMessage backwards compatibility', () => {
const client = new Client({ name: 'test client', version: '1.0' }, { capabilities: { sampling: { tools: {} } } });
// Mock client returns text content (tool_use schema validation is tested in types.test.ts)
client.setRequestHandler(CreateMessageRequestSchema, async () => ({
client.setRequestHandler('sampling/createMessage', async () => ({
model: 'test-model',
role: 'assistant',
content: { type: 'text', text: 'I will use the weather tool' },
@ -2346,7 +2242,7 @@ describe('Task-based execution', () => {
}
);
server.setRequestHandler(CallToolRequestSchema, async request => {
server.setRequestHandler('tools/call', async request => {
if (request.params.name === 'test-tool') {
return {
content: [{ type: 'text', text: 'Success!' }]
@ -2355,7 +2251,7 @@ describe('Task-based execution', () => {
throw new Error('Unknown tool');
});
server.setRequestHandler(ListToolsRequestSchema, async () => ({
server.setRequestHandler('tools/list', async () => ({
tools: [
{
name: 'test-tool',
@ -2440,7 +2336,7 @@ describe('Task-based execution', () => {
let capturedElicitRequest: z4.infer<typeof ElicitRequestSchema> | null = null;
// Set up client elicitation handler
client.setRequestHandler(ElicitRequestSchema, async (request, extra) => {
client.setRequestHandler('elicitation/create', async (request, extra) => {
let taskId: string | undefined;
// Check if task creation is requested
@ -2594,7 +2490,7 @@ describe('Task-based execution', () => {
}
);
client.setRequestHandler(ElicitRequestSchema, async (request, extra) => {
client.setRequestHandler('elicitation/create', async (request, extra) => {
const result = {
action: 'accept',
content: { username: 'server-test-user', confirmed: true }
@ -2675,7 +2571,7 @@ describe('Task-based execution', () => {
}
);
client.setRequestHandler(ElicitRequestSchema, async (request, extra) => {
client.setRequestHandler('elicitation/create', async (request, extra) => {
const result = {
action: 'accept',
content: { username: 'list-user' }
@ -2754,7 +2650,7 @@ describe('Task-based execution', () => {
}
);
client.setRequestHandler(ElicitRequestSchema, async (request, extra) => {
client.setRequestHandler('elicitation/create', async (request, extra) => {
const result = {
action: 'accept',
content: { username: 'result-user', confirmed: true }
@ -2835,7 +2731,7 @@ describe('Task-based execution', () => {
}
);
client.setRequestHandler(ElicitRequestSchema, async (request, extra) => {
client.setRequestHandler('elicitation/create', async (request, extra) => {
const result = {
action: 'accept',
content: { username: 'list-user' }
@ -3121,7 +3017,7 @@ describe('Task-based execution', () => {
}
);
client.setRequestHandler(ElicitRequestSchema, async () => ({
client.setRequestHandler('elicitation/create', async () => ({
action: 'accept',
content: { username: 'test' }
}));
@ -3178,7 +3074,7 @@ test('should respect client task capabilities', async () => {
}
);
client.setRequestHandler(ElicitRequestSchema, async (request, extra) => {
client.setRequestHandler('elicitation/create', async (request, extra) => {
const result = {
action: 'accept',
content: { username: 'test-user' }

View file

@ -9,7 +9,7 @@
import { Client } from '@modelcontextprotocol/client';
import type { ElicitRequestFormParams } from '@modelcontextprotocol/core';
import { AjvJsonSchemaValidator, CfWorkerJsonSchemaValidator, ElicitRequestSchema, InMemoryTransport } from '@modelcontextprotocol/core';
import { AjvJsonSchemaValidator, CfWorkerJsonSchemaValidator, InMemoryTransport } from '@modelcontextprotocol/core';
import { Server } from '@modelcontextprotocol/server';
const ajvProvider = new AjvJsonSchemaValidator();
@ -62,7 +62,7 @@ describe('Elicitation Flow', () => {
function testElicitationFlow(validatorProvider: typeof ajvProvider | typeof cfWorkerProvider, validatorName: string) {
test(`${validatorName}: should elicit simple object with string field`, async () => {
client.setRequestHandler(ElicitRequestSchema, _request => ({
client.setRequestHandler('elicitation/create', _request => ({
action: 'accept',
content: { name: 'John Doe' }
}));
@ -86,7 +86,7 @@ function testElicitationFlow(validatorProvider: typeof ajvProvider | typeof cfWo
});
test(`${validatorName}: should elicit object with integer field`, async () => {
client.setRequestHandler(ElicitRequestSchema, _request => ({
client.setRequestHandler('elicitation/create', _request => ({
action: 'accept',
content: { age: 42 }
}));
@ -110,7 +110,7 @@ function testElicitationFlow(validatorProvider: typeof ajvProvider | typeof cfWo
});
test(`${validatorName}: should elicit object with boolean field`, async () => {
client.setRequestHandler(ElicitRequestSchema, _request => ({
client.setRequestHandler('elicitation/create', _request => ({
action: 'accept',
content: { agree: true }
}));
@ -145,7 +145,7 @@ function testElicitationFlow(validatorProvider: typeof ajvProvider | typeof cfWo
notifications: false
};
client.setRequestHandler(ElicitRequestSchema, _request => ({
client.setRequestHandler('elicitation/create', _request => ({
action: 'accept',
content: userData
}));
@ -178,7 +178,7 @@ function testElicitationFlow(validatorProvider: typeof ajvProvider | typeof cfWo
});
test(`${validatorName}: should reject invalid object (missing required field)`, async () => {
client.setRequestHandler(ElicitRequestSchema, _request => ({
client.setRequestHandler('elicitation/create', _request => ({
action: 'accept',
content: {
email: 'user@example.com'
@ -203,7 +203,7 @@ function testElicitationFlow(validatorProvider: typeof ajvProvider | typeof cfWo
});
test(`${validatorName}: should reject invalid field type`, async () => {
client.setRequestHandler(ElicitRequestSchema, _request => ({
client.setRequestHandler('elicitation/create', _request => ({
action: 'accept',
content: {
name: 'John Doe',
@ -228,7 +228,7 @@ function testElicitationFlow(validatorProvider: typeof ajvProvider | typeof cfWo
});
test(`${validatorName}: should reject invalid string (too short)`, async () => {
client.setRequestHandler(ElicitRequestSchema, _request => ({
client.setRequestHandler('elicitation/create', _request => ({
action: 'accept',
content: { name: '' } // Too short
}));
@ -248,7 +248,7 @@ function testElicitationFlow(validatorProvider: typeof ajvProvider | typeof cfWo
});
test(`${validatorName}: should reject invalid integer (out of range)`, async () => {
client.setRequestHandler(ElicitRequestSchema, _request => ({
client.setRequestHandler('elicitation/create', _request => ({
action: 'accept',
content: { age: 200 } // Too high
}));
@ -269,7 +269,7 @@ function testElicitationFlow(validatorProvider: typeof ajvProvider | typeof cfWo
});
test(`${validatorName}: should reject invalid pattern`, async () => {
client.setRequestHandler(ElicitRequestSchema, _request => ({
client.setRequestHandler('elicitation/create', _request => ({
action: 'accept',
content: { zipCode: 'ABC123' } // Doesn't match pattern
}));
@ -291,7 +291,7 @@ function testElicitationFlow(validatorProvider: typeof ajvProvider | typeof cfWo
});
test(`${validatorName}: should allow decline action without validation`, async () => {
client.setRequestHandler(ElicitRequestSchema, _request => ({
client.setRequestHandler('elicitation/create', _request => ({
action: 'decline'
}));
@ -313,7 +313,7 @@ function testElicitationFlow(validatorProvider: typeof ajvProvider | typeof cfWo
});
test(`${validatorName}: should allow cancel action without validation`, async () => {
client.setRequestHandler(ElicitRequestSchema, _request => ({
client.setRequestHandler('elicitation/create', _request => ({
action: 'cancel'
}));
@ -336,7 +336,7 @@ function testElicitationFlow(validatorProvider: typeof ajvProvider | typeof cfWo
test(`${validatorName}: should handle multiple sequential elicitation requests`, async () => {
let requestCount = 0;
client.setRequestHandler(ElicitRequestSchema, request => {
client.setRequestHandler('elicitation/create', request => {
requestCount++;
if (request.params.message.includes('name')) {
return { action: 'accept', content: { name: 'Alice' } };
@ -389,7 +389,7 @@ function testElicitationFlow(validatorProvider: typeof ajvProvider | typeof cfWo
});
test(`${validatorName}: should validate with optional fields present`, async () => {
client.setRequestHandler(ElicitRequestSchema, _request => ({
client.setRequestHandler('elicitation/create', _request => ({
action: 'accept',
content: { name: 'John', nickname: 'Johnny' }
}));
@ -414,7 +414,7 @@ function testElicitationFlow(validatorProvider: typeof ajvProvider | typeof cfWo
});
test(`${validatorName}: should validate with optional fields absent`, async () => {
client.setRequestHandler(ElicitRequestSchema, _request => ({
client.setRequestHandler('elicitation/create', _request => ({
action: 'accept',
content: { name: 'John' }
}));
@ -439,7 +439,7 @@ function testElicitationFlow(validatorProvider: typeof ajvProvider | typeof cfWo
});
test(`${validatorName}: should validate email format`, async () => {
client.setRequestHandler(ElicitRequestSchema, _request => ({
client.setRequestHandler('elicitation/create', _request => ({
action: 'accept',
content: { email: 'user@example.com' }
}));
@ -556,7 +556,7 @@ function testElicitationFlow(validatorProvider: typeof ajvProvider | typeof cfWo
};
// Client returns no values; SDK should apply defaults automatically (and validate)
client.setRequestHandler(ElicitRequestSchema, request => {
client.setRequestHandler('elicitation/create', request => {
expect(request.params.mode).toEqual('form');
expect((request.params as ElicitRequestFormParams).requestedSchema).toEqual(testSchemaProperties);
return {
@ -592,7 +592,7 @@ function testElicitationFlow(validatorProvider: typeof ajvProvider | typeof cfWo
});
test(`${validatorName}: should reject invalid email format`, async () => {
client.setRequestHandler(ElicitRequestSchema, _request => ({
client.setRequestHandler('elicitation/create', _request => ({
action: 'accept',
content: { email: 'not-an-email' }
}));
@ -616,7 +616,7 @@ function testElicitationFlow(validatorProvider: typeof ajvProvider | typeof cfWo
test(`${validatorName}: should succeed with valid selection in single-select untitled enum`, async () => {
// Set up client to return valid response
client.setRequestHandler(ElicitRequestSchema, _request => ({
client.setRequestHandler('elicitation/create', _request => ({
action: 'accept',
content: {
color: 'Red'
@ -652,7 +652,7 @@ function testElicitationFlow(validatorProvider: typeof ajvProvider | typeof cfWo
test(`${validatorName}: should succeed with valid selection in single-select titled enum`, async () => {
// Set up client to return valid response
client.setRequestHandler(ElicitRequestSchema, _request => ({
client.setRequestHandler('elicitation/create', _request => ({
action: 'accept',
content: {
color: '#FF0000'
@ -692,7 +692,7 @@ function testElicitationFlow(validatorProvider: typeof ajvProvider | typeof cfWo
test(`${validatorName}: should succeed with valid selection in single-select titled legacy enum`, async () => {
// Set up client to return valid response
client.setRequestHandler(ElicitRequestSchema, _request => ({
client.setRequestHandler('elicitation/create', _request => ({
action: 'accept',
content: {
color: '#FF0000'
@ -730,7 +730,7 @@ function testElicitationFlow(validatorProvider: typeof ajvProvider | typeof cfWo
test(`${validatorName}: should succeed with valid selection in multi-select untitled enum`, async () => {
// Set up client to return valid response
client.setRequestHandler(ElicitRequestSchema, _request => ({
client.setRequestHandler('elicitation/create', _request => ({
action: 'accept',
content: {
colors: ['Red', 'Blue']
@ -770,7 +770,7 @@ function testElicitationFlow(validatorProvider: typeof ajvProvider | typeof cfWo
test(`${validatorName}: should succeed with valid selection in multi-select titled enum`, async () => {
// Set up client to return valid response
client.setRequestHandler(ElicitRequestSchema, _request => ({
client.setRequestHandler('elicitation/create', _request => ({
action: 'accept',
content: {
colors: ['#FF0000', '#0000FF']
@ -815,7 +815,7 @@ function testElicitationFlow(validatorProvider: typeof ajvProvider | typeof cfWo
test(`${validatorName}: should reject invalid selection in single-select untitled enum`, async () => {
// Set up client to return valid response
client.setRequestHandler(ElicitRequestSchema, _request => ({
client.setRequestHandler('elicitation/create', _request => ({
action: 'accept',
content: {
color: 'Black' // Color not in enum list
@ -846,7 +846,7 @@ function testElicitationFlow(validatorProvider: typeof ajvProvider | typeof cfWo
test(`${validatorName}: should reject invalid selection in single-select titled enum`, async () => {
// Set up client to return valid response
client.setRequestHandler(ElicitRequestSchema, _request => ({
client.setRequestHandler('elicitation/create', _request => ({
action: 'accept',
content: {
color: 'Red' // Should be "#FF0000" (const not title)
@ -880,7 +880,7 @@ function testElicitationFlow(validatorProvider: typeof ajvProvider | typeof cfWo
test(`${validatorName}: should reject invalid selection in single-select titled legacy enum`, async () => {
// Set up client to return valid response
client.setRequestHandler(ElicitRequestSchema, _request => ({
client.setRequestHandler('elicitation/create', _request => ({
action: 'accept',
content: {
color: 'Red' // Should be "#FF0000" (enum not enumNames)
@ -913,7 +913,7 @@ function testElicitationFlow(validatorProvider: typeof ajvProvider | typeof cfWo
test(`${validatorName}: should reject invalid selection in multi-select untitled enum`, async () => {
// Set up client to return valid response
client.setRequestHandler(ElicitRequestSchema, _request => ({
client.setRequestHandler('elicitation/create', _request => ({
action: 'accept',
content: {
color: 'Red' // Should be array, not string
@ -948,7 +948,7 @@ function testElicitationFlow(validatorProvider: typeof ajvProvider | typeof cfWo
test(`${validatorName}: should reject invalid selection in multi-select titled enum`, async () => {
// Set up client to return valid response
client.setRequestHandler(ElicitRequestSchema, _request => ({
client.setRequestHandler('elicitation/create', _request => ({
action: 'accept',
content: {
colors: ['Red', 'Blue'] // Should be ["#FF0000", "#0000FF"] (const not title)

View file

@ -3,7 +3,6 @@ import type { CallToolResult, Notification, TextContent } from '@modelcontextpro
import {
CallToolResultSchema,
CompleteResultSchema,
ElicitRequestSchema,
ErrorCode,
getDisplayName,
GetPromptResultSchema,
@ -13,7 +12,6 @@ import {
ListResourcesResultSchema,
ListResourceTemplatesResultSchema,
ListToolsResultSchema,
LoggingMessageNotificationSchema,
ReadResourceResultSchema,
UriTemplate,
UrlElicitationRequiredError
@ -1441,7 +1439,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {
let receivedLogMessage: string | undefined;
const loggingMessage = 'hello here is log message 1';
client.setNotificationHandler(LoggingMessageNotificationSchema, notification => {
client.setNotificationHandler('notifications/message', notification => {
receivedLogMessage = notification.params.data as string;
});
@ -4539,7 +4537,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {
findAlternatives.mockResolvedValue(['2024-12-26', '2024-12-27', '2024-12-28']);
// Set up client to accept alternative date checking
client.setRequestHandler(ElicitRequestSchema, async request => {
client.setRequestHandler('elicitation/create', async request => {
expect(request.params.message).toContain('No tables available at ABC Restaurant on 2024-12-25');
return {
action: 'accept',
@ -4579,7 +4577,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {
checkAvailability.mockResolvedValue(false);
// Set up client to reject alternative date checking
client.setRequestHandler(ElicitRequestSchema, async () => {
client.setRequestHandler('elicitation/create', async () => {
return {
action: 'accept',
content: {
@ -4617,7 +4615,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {
checkAvailability.mockResolvedValue(false);
// Set up client to cancel the elicitation
client.setRequestHandler(ElicitRequestSchema, async () => {
client.setRequestHandler('elicitation/create', async () => {
return {
action: 'cancel'
};
@ -5830,7 +5828,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {
findAlternatives.mockResolvedValue(['2024-12-26', '2024-12-27', '2024-12-28']);
// Set up client to accept alternative date checking
client.setRequestHandler(ElicitRequestSchema, async request => {
client.setRequestHandler('elicitation/create', async request => {
expect(request.params.message).toContain('No tables available at ABC Restaurant on 2024-12-25');
return {
action: 'accept',
@ -5870,7 +5868,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {
checkAvailability.mockResolvedValue(false);
// Set up client to reject alternative date checking
client.setRequestHandler(ElicitRequestSchema, async () => {
client.setRequestHandler('elicitation/create', async () => {
return {
action: 'accept',
content: {
@ -5908,7 +5906,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {
checkAvailability.mockResolvedValue(false);
// Set up client to cancel the elicitation
client.setRequestHandler(ElicitRequestSchema, async () => {
client.setRequestHandler('elicitation/create', async () => {
return {
action: 'cancel'
};

View file

@ -8,7 +8,6 @@ import type { TaskRequestOptions } from '@modelcontextprotocol/server';
import {
CallToolResultSchema,
CreateTaskResultSchema,
ElicitRequestSchema,
ElicitResultSchema,
ErrorCode,
InMemoryTaskMessageQueue,
@ -498,7 +497,7 @@ describe('Task Lifecycle Integration Tests', () => {
const receivedMessages: Array<{ method: string; message: string }> = [];
// Set up elicitation handler on client to track message order
client.setRequestHandler(ElicitRequestSchema, async request => {
client.setRequestHandler('elicitation/create', async request => {
// Track the message
receivedMessages.push({
method: request.method,
@ -604,7 +603,7 @@ describe('Task Lifecycle Integration Tests', () => {
let elicitationRequestMeta: Record<string, unknown> | undefined;
// Set up elicitation handler on client
elicitClient.setRequestHandler(ElicitRequestSchema, async request => {
elicitClient.setRequestHandler('elicitation/create', async request => {
elicitationReceived = true;
elicitationRequestMeta = request.params._meta;
@ -987,7 +986,7 @@ describe('Task Lifecycle Integration Tests', () => {
let elicitationCallCount = 0;
// Set up elicitation handler to track if any messages are delivered
client.setRequestHandler(ElicitRequestSchema, async () => {
client.setRequestHandler('elicitation/create', async () => {
elicitationCallCount++;
return {
action: 'accept' as const,
@ -1204,7 +1203,7 @@ describe('Task Lifecycle Integration Tests', () => {
let tasksResultStartTime = 0;
// Set up elicitation handler to track when messages arrive
client.setRequestHandler(ElicitRequestSchema, async request => {
client.setRequestHandler('elicitation/create', async request => {
const timestamp = Date.now();
receivedMessages.push({
message: request.params.message,
@ -1412,7 +1411,7 @@ describe('Task Lifecycle Integration Tests', () => {
const receivedMessages: Array<{ type: string; message?: string; content?: unknown }> = [];
// Set up elicitation handler to track message order
client.setRequestHandler(ElicitRequestSchema, async request => {
client.setRequestHandler('elicitation/create', async request => {
receivedMessages.push({
type: 'elicitation',
message: request.params.message
@ -1607,7 +1606,7 @@ describe('Task Lifecycle Integration Tests', () => {
let elicitationMessage = '';
// Set up elicitation handler on client
client.setRequestHandler(ElicitRequestSchema, async request => {
client.setRequestHandler('elicitation/create', async request => {
elicitationReceived = true;
elicitationMessage = request.params.message;

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, LoggingMessageNotificationSchema, McpServer } from '@modelcontextprotocol/server';
import { CallToolResultSchema, McpServer } from '@modelcontextprotocol/server';
import type { ZodMatrixEntry } from '@modelcontextprotocol/test-helpers';
import { listenOnRandomPort, zodTestMatrix } from '@modelcontextprotocol/test-helpers';
@ -192,7 +192,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {
});
// Set up notification handler for first client
client1.setNotificationHandler(LoggingMessageNotificationSchema, notification => {
client1.setNotificationHandler('notifications/message', notification => {
if (notification.method === 'notifications/message') {
notifications.push(notification.params);
}
@ -271,7 +271,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {
// Track replayed notifications separately
const replayedNotifications: unknown[] = [];
client2.setNotificationHandler(LoggingMessageNotificationSchema, notification => {
client2.setNotificationHandler('notifications/message', notification => {
if (notification.method === 'notifications/message') {
replayedNotifications.push(notification.params);
}