feat: pluggable JSON schema validator providers (#1012)

This commit is contained in:
Matt 2025-10-25 16:56:42 +01:00 • committed by GitHub
commit 5a8fb39430
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 2373 additions and 134 deletions

View file

@ -0,0 +1,642 @@
/**
* Comprehensive elicitation flow tests with validator integration
*
* These tests verify the end-to-end elicitation flow from server requesting
* input to client responding and validation of the response against schemas.
*
* Per the MCP spec, elicitation only supports object schemas, not primitives.
*/
import { Client } from '../client/index.js';
import { InMemoryTransport } from '../inMemory.js';
import { ElicitRequestSchema } from '../types.js';
import { AjvJsonSchemaValidator } from '../validation/ajv-provider.js';
import { CfWorkerJsonSchemaValidator } from '../validation/cfworker-provider.js';
import { Server } from './index.js';
const ajvProvider = new AjvJsonSchemaValidator();
const cfWorkerProvider = new CfWorkerJsonSchemaValidator();
describe('Elicitation Flow', () => {
describe('with AJV validator', () => {
testElicitationFlow(ajvProvider, 'AJV');
});
describe('with CfWorker validator', () => {
testElicitationFlow(cfWorkerProvider, 'CfWorker');
});
});
function testElicitationFlow(validatorProvider: typeof ajvProvider | typeof cfWorkerProvider, validatorName: string) {
test(`${validatorName}: should elicit simple object with string field`, async () => {
const server = new Server(
{ name: 'test-server', version: '1.0.0' },
{
capabilities: {},
jsonSchemaValidator: validatorProvider
}
);
const client = new Client({ name: 'test-client', version: '1.0.0' }, { capabilities: { elicitation: {} } });
client.setRequestHandler(ElicitRequestSchema, _request => ({
action: 'accept',
content: { name: 'John Doe' }
}));
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]);
const result = await server.elicitInput({
message: 'What is your name?',
requestedSchema: {
type: 'object',
properties: {
name: { type: 'string', minLength: 1 }
},
required: ['name']
}
});
expect(result).toEqual({
action: 'accept',
content: { name: 'John Doe' }
});
});
test(`${validatorName}: should elicit object with integer field`, async () => {
const server = new Server(
{ name: 'test-server', version: '1.0.0' },
{
capabilities: {},
jsonSchemaValidator: validatorProvider
}
);
const client = new Client({ name: 'test-client', version: '1.0.0' }, { capabilities: { elicitation: {} } });
client.setRequestHandler(ElicitRequestSchema, _request => ({
action: 'accept',
content: { age: 42 }
}));
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]);
const result = await server.elicitInput({
message: 'What is your age?',
requestedSchema: {
type: 'object',
properties: {
age: { type: 'integer', minimum: 0, maximum: 150 }
},
required: ['age']
}
});
expect(result).toEqual({
action: 'accept',
content: { age: 42 }
});
});
test(`${validatorName}: should elicit object with boolean field`, async () => {
const server = new Server(
{ name: 'test-server', version: '1.0.0' },
{
capabilities: {},
jsonSchemaValidator: validatorProvider
}
);
const client = new Client({ name: 'test-client', version: '1.0.0' }, { capabilities: { elicitation: {} } });
client.setRequestHandler(ElicitRequestSchema, _request => ({
action: 'accept',
content: { agree: true }
}));
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]);
const result = await server.elicitInput({
message: 'Do you agree?',
requestedSchema: {
type: 'object',
properties: {
agree: { type: 'boolean' }
},
required: ['agree']
}
});
expect(result).toEqual({
action: 'accept',
content: { agree: true }
});
});
test(`${validatorName}: should elicit complex object with multiple fields`, async () => {
const server = new Server(
{ name: 'test-server', version: '1.0.0' },
{
capabilities: {},
jsonSchemaValidator: validatorProvider
}
);
const client = new Client({ name: 'test-client', version: '1.0.0' }, { capabilities: { elicitation: {} } });
const userData = {
name: 'Jane Smith',
email: 'jane@example.com',
age: 28,
street: '123 Main St',
city: 'San Francisco',
zipCode: '94105',
newsletter: true,
notifications: false
};
client.setRequestHandler(ElicitRequestSchema, _request => ({
action: 'accept',
content: userData
}));
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]);
const result = await server.elicitInput({
message: 'Please provide your information',
requestedSchema: {
type: 'object',
properties: {
name: { type: 'string', minLength: 1 },
email: { type: 'string', format: 'email' },
age: { type: 'integer', minimum: 0, maximum: 150 },
street: { type: 'string' },
city: { type: 'string' },
zipCode: { type: 'string', pattern: '^[0-9]{5}$' },
newsletter: { type: 'boolean' },
notifications: { type: 'boolean' }
},
required: ['name', 'email', 'age', 'street', 'city', 'zipCode']
}
});
expect(result).toEqual({
action: 'accept',
content: userData
});
});
test(`${validatorName}: should reject invalid object (missing required field)`, async () => {
const server = new Server(
{ name: 'test-server', version: '1.0.0' },
{
capabilities: {},
jsonSchemaValidator: validatorProvider
}
);
const client = new Client({ name: 'test-client', version: '1.0.0' }, { capabilities: { elicitation: {} } });
client.setRequestHandler(ElicitRequestSchema, _request => ({
action: 'accept',
content: {
email: 'user@example.com'
// Missing required 'name' field
}
}));
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]);
await expect(
server.elicitInput({
message: 'Please provide your information',
requestedSchema: {
type: 'object',
properties: {
name: { type: 'string' },
email: { type: 'string' }
},
required: ['name', 'email']
}
})
).rejects.toThrow(/does not match requested schema/);
});
test(`${validatorName}: should reject invalid field type`, async () => {
const server = new Server(
{ name: 'test-server', version: '1.0.0' },
{
capabilities: {},
jsonSchemaValidator: validatorProvider
}
);
const client = new Client({ name: 'test-client', version: '1.0.0' }, { capabilities: { elicitation: {} } });
client.setRequestHandler(ElicitRequestSchema, _request => ({
action: 'accept',
content: {
name: 'John Doe',
age: 'thirty' // Wrong type - should be integer
}
}));
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]);
await expect(
server.elicitInput({
message: 'Please provide your information',
requestedSchema: {
type: 'object',
properties: {
name: { type: 'string' },
age: { type: 'integer' }
},
required: ['name', 'age']
}
})
).rejects.toThrow(/does not match requested schema/);
});
test(`${validatorName}: should reject invalid string (too short)`, async () => {
const server = new Server(
{ name: 'test-server', version: '1.0.0' },
{
capabilities: {},
jsonSchemaValidator: validatorProvider
}
);
const client = new Client({ name: 'test-client', version: '1.0.0' }, { capabilities: { elicitation: {} } });
client.setRequestHandler(ElicitRequestSchema, _request => ({
action: 'accept',
content: { name: '' } // Too short
}));
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]);
await expect(
server.elicitInput({
message: 'What is your name?',
requestedSchema: {
type: 'object',
properties: {
name: { type: 'string', minLength: 1 }
},
required: ['name']
}
})
).rejects.toThrow(/does not match requested schema/);
});
test(`${validatorName}: should reject invalid integer (out of range)`, async () => {
const server = new Server(
{ name: 'test-server', version: '1.0.0' },
{
capabilities: {},
jsonSchemaValidator: validatorProvider
}
);
const client = new Client({ name: 'test-client', version: '1.0.0' }, { capabilities: { elicitation: {} } });
client.setRequestHandler(ElicitRequestSchema, _request => ({
action: 'accept',
content: { age: 200 } // Too high
}));
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]);
await expect(
server.elicitInput({
message: 'What is your age?',
requestedSchema: {
type: 'object',
properties: {
age: { type: 'integer', minimum: 0, maximum: 150 }
},
required: ['age']
}
})
).rejects.toThrow(/does not match requested schema/);
});
test(`${validatorName}: should reject invalid pattern`, async () => {
const server = new Server(
{ name: 'test-server', version: '1.0.0' },
{
capabilities: {},
jsonSchemaValidator: validatorProvider
}
);
const client = new Client({ name: 'test-client', version: '1.0.0' }, { capabilities: { elicitation: {} } });
client.setRequestHandler(ElicitRequestSchema, _request => ({
action: 'accept',
content: { zipCode: 'ABC123' } // Doesn't match pattern
}));
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]);
await expect(
server.elicitInput({
message: 'Enter a 5-digit zip code',
requestedSchema: {
type: 'object',
properties: {
zipCode: { type: 'string', pattern: '^[0-9]{5}$' }
},
required: ['zipCode']
}
})
).rejects.toThrow(/does not match requested schema/);
});
test(`${validatorName}: should allow decline action without validation`, async () => {
const server = new Server(
{ name: 'test-server', version: '1.0.0' },
{
capabilities: {},
jsonSchemaValidator: validatorProvider
}
);
const client = new Client({ name: 'test-client', version: '1.0.0' }, { capabilities: { elicitation: {} } });
client.setRequestHandler(ElicitRequestSchema, _request => ({
action: 'decline'
}));
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]);
const result = await server.elicitInput({
message: 'Please provide your information',
requestedSchema: {
type: 'object',
properties: {
name: { type: 'string' }
},
required: ['name']
}
});
expect(result).toEqual({
action: 'decline'
});
});
test(`${validatorName}: should allow cancel action without validation`, async () => {
const server = new Server(
{ name: 'test-server', version: '1.0.0' },
{
capabilities: {},
jsonSchemaValidator: validatorProvider
}
);
const client = new Client({ name: 'test-client', version: '1.0.0' }, { capabilities: { elicitation: {} } });
client.setRequestHandler(ElicitRequestSchema, _request => ({
action: 'cancel'
}));
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]);
const result = await server.elicitInput({
message: 'Please provide your information',
requestedSchema: {
type: 'object',
properties: {
name: { type: 'string' }
},
required: ['name']
}
});
expect(result).toEqual({
action: 'cancel'
});
});
test(`${validatorName}: should handle multiple sequential elicitation requests`, async () => {
const server = new Server(
{ name: 'test-server', version: '1.0.0' },
{
capabilities: {},
jsonSchemaValidator: validatorProvider
}
);
const client = new Client({ name: 'test-client', version: '1.0.0' }, { capabilities: { elicitation: {} } });
let requestCount = 0;
client.setRequestHandler(ElicitRequestSchema, request => {
requestCount++;
if (request.params.message.includes('name')) {
return { action: 'accept', content: { name: 'Alice' } };
} else if (request.params.message.includes('age')) {
return { action: 'accept', content: { age: 30 } };
} else if (request.params.message.includes('city')) {
return { action: 'accept', content: { city: 'New York' } };
}
return { action: 'decline' };
});
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]);
const nameResult = await server.elicitInput({
message: 'What is your name?',
requestedSchema: {
type: 'object',
properties: { name: { type: 'string', minLength: 1 } },
required: ['name']
}
});
const ageResult = await server.elicitInput({
message: 'What is your age?',
requestedSchema: {
type: 'object',
properties: { age: { type: 'integer', minimum: 0 } },
required: ['age']
}
});
const cityResult = await server.elicitInput({
message: 'What is your city?',
requestedSchema: {
type: 'object',
properties: { city: { type: 'string', minLength: 1 } },
required: ['city']
}
});
expect(requestCount).toBe(3);
expect(nameResult).toEqual({
action: 'accept',
content: { name: 'Alice' }
});
expect(ageResult).toEqual({ action: 'accept', content: { age: 30 } });
expect(cityResult).toEqual({
action: 'accept',
content: { city: 'New York' }
});
});
test(`${validatorName}: should validate with optional fields present`, async () => {
const server = new Server(
{ name: 'test-server', version: '1.0.0' },
{
capabilities: {},
jsonSchemaValidator: validatorProvider
}
);
const client = new Client({ name: 'test-client', version: '1.0.0' }, { capabilities: { elicitation: {} } });
client.setRequestHandler(ElicitRequestSchema, _request => ({
action: 'accept',
content: { name: 'John', nickname: 'Johnny' }
}));
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]);
const result = await server.elicitInput({
message: 'Enter your name',
requestedSchema: {
type: 'object',
properties: {
name: { type: 'string', minLength: 1 },
nickname: { type: 'string' }
},
required: ['name']
}
});
expect(result).toEqual({
action: 'accept',
content: { name: 'John', nickname: 'Johnny' }
});
});
test(`${validatorName}: should validate with optional fields absent`, async () => {
const server = new Server(
{ name: 'test-server', version: '1.0.0' },
{
capabilities: {},
jsonSchemaValidator: validatorProvider
}
);
const client = new Client({ name: 'test-client', version: '1.0.0' }, { capabilities: { elicitation: {} } });
client.setRequestHandler(ElicitRequestSchema, _request => ({
action: 'accept',
content: { name: 'John' }
}));
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]);
const result = await server.elicitInput({
message: 'Enter your name',
requestedSchema: {
type: 'object',
properties: {
name: { type: 'string', minLength: 1 },
nickname: { type: 'string' }
},
required: ['name']
}
});
expect(result).toEqual({
action: 'accept',
content: { name: 'John' }
});
});
test(`${validatorName}: should validate email format`, async () => {
const server = new Server(
{ name: 'test-server', version: '1.0.0' },
{
capabilities: {},
jsonSchemaValidator: validatorProvider
}
);
const client = new Client({ name: 'test-client', version: '1.0.0' }, { capabilities: { elicitation: {} } });
client.setRequestHandler(ElicitRequestSchema, _request => ({
action: 'accept',
content: { email: 'user@example.com' }
}));
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]);
const result = await server.elicitInput({
message: 'Enter your email',
requestedSchema: {
type: 'object',
properties: {
email: { type: 'string', format: 'email' }
},
required: ['email']
}
});
expect(result).toEqual({
action: 'accept',
content: { email: 'user@example.com' }
});
});
test(`${validatorName}: should reject invalid email format`, async () => {
const server = new Server(
{ name: 'test-server', version: '1.0.0' },
{
capabilities: {},
jsonSchemaValidator: validatorProvider
}
);
const client = new Client({ name: 'test-client', version: '1.0.0' }, { capabilities: { elicitation: {} } });
client.setRequestHandler(ElicitRequestSchema, _request => ({
action: 'accept',
content: { email: 'not-an-email' }
}));
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]);
await expect(
server.elicitInput({
message: 'Enter your email',
requestedSchema: {
type: 'object',
properties: {
email: { type: 'string', format: 'email' }
},
required: ['email']
}
})
).rejects.toThrow(/does not match requested schema/);
});
}

View file

@ -1,26 +1,24 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable no-constant-binary-expression */
/* eslint-disable @typescript-eslint/no-unused-expressions */
import { Server } from './index.js';
import { z } from 'zod';
import { Client } from '../client/index.js';
import { InMemoryTransport } from '../inMemory.js';
import type { Transport } from '../shared/transport.js';
import {
RequestSchema,
NotificationSchema,
ResultSchema,
LATEST_PROTOCOL_VERSION,
SUPPORTED_PROTOCOL_VERSIONS,
CreateMessageRequestSchema,
ElicitRequestSchema,
ErrorCode,
LATEST_PROTOCOL_VERSION,
ListPromptsRequestSchema,
ListResourcesRequestSchema,
ListToolsRequestSchema,
type LoggingMessageNotification,
NotificationSchema,
RequestSchema,
ResultSchema,
SetLevelRequestSchema,
ErrorCode,
LoggingMessageNotification
SUPPORTED_PROTOCOL_VERSIONS
} from '../types.js';
import { Transport } from '../shared/transport.js';
import { InMemoryTransport } from '../inMemory.js';
import { Client } from '../client/index.js';
import { Server } from './index.js';
test('should accept latest protocol version', async () => {
let sendPromiseResolve: (value: unknown) => void;
@ -235,7 +233,7 @@ test('should respect client capabilities', async () => {
);
// Implement request handler for sampling/createMessage
client.setRequestHandler(CreateMessageRequestSchema, async request => {
client.setRequestHandler(CreateMessageRequestSchema, async _request => {
// Mock implementation of createMessage
return {
model: 'test-model',
@ -377,7 +375,7 @@ test('should validate elicitation response against requested schema', async () =
);
// Set up client to return valid response
client.setRequestHandler(ElicitRequestSchema, request => ({
client.setRequestHandler(ElicitRequestSchema, _request => ({
action: 'accept',
content: {
name: 'John Doe',
@ -454,7 +452,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(ElicitRequestSchema, _request => ({
action: 'accept',
content: {
email: '', // Invalid - too short
@ -523,7 +521,7 @@ test('should allow elicitation reject and cancel without validation', async () =
);
let requestCount = 0;
client.setRequestHandler(ElicitRequestSchema, request => {
client.setRequestHandler(ElicitRequestSchema, _request => {
requestCount++;
if (requestCount === 1) {
return { action: 'decline' };
@ -579,7 +577,7 @@ test('should respect server notification capabilities', async () => {
}
);
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
const [_clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await server.connect(serverTransport);
@ -685,7 +683,7 @@ test('should typecheck', () => {
);
// Typecheck that only valid weather requests/notifications/results are allowed
weatherServer.setRequestHandler(GetWeatherRequestSchema, request => {
weatherServer.setRequestHandler(GetWeatherRequestSchema, _request => {
return {
temperature: 72,
conditions: 'sunny'
@ -723,7 +721,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(CreateMessageRequestSchema, async (_request, _extra) => {
await new Promise(resolve => setTimeout(resolve, 1000));
return {
model: 'test',

View file

@ -1,37 +1,38 @@
import { mergeCapabilities, Protocol, ProtocolOptions, RequestOptions } from '../shared/protocol.js';
import { mergeCapabilities, Protocol, type ProtocolOptions, type RequestOptions } from '../shared/protocol.js';
import {
ClientCapabilities,
CreateMessageRequest,
type ClientCapabilities,
type CreateMessageRequest,
CreateMessageResultSchema,
ElicitRequest,
ElicitResult,
type ElicitRequest,
type ElicitResult,
ElicitResultSchema,
EmptyResultSchema,
Implementation,
InitializedNotificationSchema,
InitializeRequest,
InitializeRequestSchema,
InitializeResult,
LATEST_PROTOCOL_VERSION,
ListRootsRequest,
ListRootsResultSchema,
LoggingMessageNotification,
McpError,
ErrorCode,
Notification,
Request,
ResourceUpdatedNotification,
Result,
ServerCapabilities,
ServerNotification,
ServerRequest,
ServerResult,
SUPPORTED_PROTOCOL_VERSIONS,
LoggingLevel,
type Implementation,
InitializedNotificationSchema,
type InitializeRequest,
InitializeRequestSchema,
type InitializeResult,
LATEST_PROTOCOL_VERSION,
type ListRootsRequest,
ListRootsResultSchema,
type LoggingLevel,
LoggingLevelSchema,
type LoggingMessageNotification,
McpError,
type Notification,
type Request,
type ResourceUpdatedNotification,
type Result,
type ServerCapabilities,
type ServerNotification,
type ServerRequest,
type ServerResult,
SetLevelRequestSchema,
LoggingLevelSchema
SUPPORTED_PROTOCOL_VERSIONS
} from '../types.js';
import Ajv from 'ajv';
import { AjvJsonSchemaValidator } from '../validation/ajv-provider.js';
import type { JsonSchemaType, jsonSchemaValidator } from '../validation/types.js';
export type ServerOptions = ProtocolOptions & {
/**
@ -43,6 +44,37 @@ export type ServerOptions = ProtocolOptions & {
* Optional instructions describing how to use the server and its features.
*/
instructions?: string;
/**
* JSON Schema validator for elicitation response validation.
*
* The validator is used to validate user input returned from elicitation
* requests against the requested schema.
*
* @default AjvJsonSchemaValidator
*
* @example
* ```typescript
* // ajv (default)
* const server = new Server(
* { name: 'my-server', version: '1.0.0' },
* {
* capabilities: {}
* jsonSchemaValidator: new AjvJsonSchemaValidator()
* }
* );
*
* // @cfworker/json-schema
* const server = new Server(
* { name: 'my-server', version: '1.0.0' },
* {
* capabilities: {},
* jsonSchemaValidator: new CfWorkerJsonSchemaValidator()
* }
* );
* ```
*/
jsonSchemaValidator?: jsonSchemaValidator;
};
/**
@ -79,6 +111,7 @@ export class Server<
private _clientVersion?: Implementation;
private _capabilities: ServerCapabilities;
private _instructions?: string;
private _jsonSchemaValidator: jsonSchemaValidator;
/**
* Callback for when initialization has fully completed (i.e., the client has sent an `initialized` notification).
@ -95,6 +128,7 @@ export class Server<
super(options);
this._capabilities = options?.capabilities ?? {};
this._instructions = options?.instructions;
this._jsonSchemaValidator = options?.jsonSchemaValidator ?? new AjvJsonSchemaValidator();
this.setRequestHandler(InitializeRequestSchema, request => this._oninitialize(request));
this.setNotificationHandler(InitializedNotificationSchema, () => this.oninitialized?.());
@ -289,24 +323,25 @@ export class Server<
const result = await this.request({ method: 'elicitation/create', params }, ElicitResultSchema, options);
// Validate the response content against the requested schema if action is "accept"
if (result.action === 'accept' && result.content) {
if (result.action === 'accept' && result.content && params.requestedSchema) {
try {
const ajv = new Ajv();
const validator = this._jsonSchemaValidator.getValidator(params.requestedSchema as JsonSchemaType);
const validationResult = validator(result.content);
const validate = ajv.compile(params.requestedSchema);
const isValid = validate(result.content);
if (!isValid) {
if (!validationResult.valid) {
throw new McpError(
ErrorCode.InvalidParams,
`Elicitation response content does not match requested schema: ${ajv.errorsText(validate.errors)}`
`Elicitation response content does not match requested schema: ${validationResult.errorMessage}`
);
}
} catch (error) {
if (error instanceof McpError) {
throw error;
}
throw new McpError(ErrorCode.InternalError, `Error validating elicitation response: ${error}`);
throw new McpError(
ErrorCode.InternalError,
`Error validating elicitation response: ${error instanceof Error ? error.message : String(error)}`
);
}
}

View file

@ -1,25 +1,24 @@
import { McpServer } from './mcp.js';
import { z } from 'zod';
import { Client } from '../client/index.js';
import { InMemoryTransport } from '../inMemory.js';
import { z } from 'zod';
import { getDisplayName } from '../shared/metadataUtils.js';
import { UriTemplate } from '../shared/uriTemplate.js';
import {
ListToolsResultSchema,
CallToolResultSchema,
CompleteResultSchema,
ElicitRequestSchema,
GetPromptResultSchema,
ListPromptsResultSchema,
ListResourcesResultSchema,
ListResourceTemplatesResultSchema,
ReadResourceResultSchema,
ListPromptsResultSchema,
GetPromptResultSchema,
CompleteResultSchema,
ListToolsResultSchema,
LoggingMessageNotificationSchema,
Notification,
TextContent,
ElicitRequestSchema
type Notification,
ReadResourceResultSchema,
type TextContent
} from '../types.js';
import { ResourceTemplate } from './mcp.js';
import { completable } from './completable.js';
import { UriTemplate } from '../shared/uriTemplate.js';
import { getDisplayName } from '../shared/metadataUtils.js';
import { McpServer, ResourceTemplate } from './mcp.js';
describe('McpServer', () => {
/***
@ -112,11 +111,22 @@ describe('McpServer', () => {
}
}
return { content: [{ type: 'text' as const, text: `Operation completed with ${steps} steps` }] };
return {
content: [
{
type: 'text' as const,
text: `Operation completed with ${steps} steps`
}
]
};
}
);
const progressUpdates: Array<{ progress: number; total?: number; message?: string }> = [];
const progressUpdates: Array<{
progress: number;
total?: number;
message?: string;
}> = [];
const client = new Client({
name: 'test client',
@ -677,9 +687,15 @@ describe('tool()', () => {
expect(result.tools).toHaveLength(2);
expect(result.tools[0].name).toBe('test');
expect(result.tools[0].annotations).toEqual({ title: 'Test Tool', readOnlyHint: true });
expect(result.tools[0].annotations).toEqual({
title: 'Test Tool',
readOnlyHint: true
});
expect(result.tools[1].name).toBe('test (new api)');
expect(result.tools[1].annotations).toEqual({ title: 'Test Tool', readOnlyHint: true });
expect(result.tools[1].annotations).toEqual({
title: 'Test Tool',
readOnlyHint: true
});
});
/***
@ -722,7 +738,10 @@ describe('tool()', () => {
type: 'object',
properties: { name: { type: 'string' } }
});
expect(result.tools[0].annotations).toEqual({ title: 'Test Tool', readOnlyHint: true });
expect(result.tools[0].annotations).toEqual({
title: 'Test Tool',
readOnlyHint: true
});
expect(result.tools[1].name).toBe('test (new api)');
expect(result.tools[1].inputSchema).toEqual(result.tools[0].inputSchema);
expect(result.tools[1].annotations).toEqual(result.tools[0].annotations);
@ -756,7 +775,11 @@ describe('tool()', () => {
{
description: 'A tool with everything',
inputSchema: { name: z.string() },
annotations: { title: 'Complete Test Tool', readOnlyHint: true, openWorldHint: false }
annotations: {
title: 'Complete Test Tool',
readOnlyHint: true,
openWorldHint: false
}
},
async ({ name }) => ({
content: [{ type: 'text', text: `Hello, ${name}!` }]
@ -804,7 +827,11 @@ describe('tool()', () => {
'test',
'A tool with everything but empty params',
{},
{ title: 'Complete Test Tool with empty params', readOnlyHint: true, openWorldHint: false },
{
title: 'Complete Test Tool with empty params',
readOnlyHint: true,
openWorldHint: false
},
async () => ({
content: [{ type: 'text', text: 'Test response' }]
})
@ -815,7 +842,11 @@ describe('tool()', () => {
{
description: 'A tool with everything but empty params',
inputSchema: {},
annotations: { title: 'Complete Test Tool with empty params', readOnlyHint: true, openWorldHint: false }
annotations: {
title: 'Complete Test Tool with empty params',
readOnlyHint: true,
openWorldHint: false
}
},
async () => ({
content: [{ type: 'text' as const, text: 'Test response' }]
@ -1342,7 +1373,7 @@ describe('tool()', () => {
expect(receivedRequestId).toBeDefined();
expect(typeof receivedRequestId === 'string' || typeof receivedRequestId === 'number').toBe(true);
expect(result.content && result.content[0].text).toContain('Received request ID:');
expect(result.content?.[0].text).toContain('Received request ID:');
});
/***
@ -1370,7 +1401,10 @@ describe('tool()', () => {
});
mcpServer.tool('test-tool', async ({ sendNotification }) => {
await sendNotification({ method: 'notifications/message', params: { level: 'debug', data: loggingMessage } });
await sendNotification({
method: 'notifications/message',
params: { level: 'debug', data: loggingMessage }
});
return {
content: [
{