fix: replace schema-based handlers with string method names (#1446)
This commit is contained in:
parent
a5670de388
commit
f6e8204b86
7 changed files with 490 additions and 878 deletions
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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!;
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue