From c23c80d9d5b61871d330bcdf24f1528cbb6ad618 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Mon, 3 Nov 2025 17:30:30 -0500 Subject: [PATCH 1/6] Update EnumSchema and extend Elicitation result to support string array return type. See [SEP-1330](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1330) * In types.ts - add UntitledSingleSelectEnumSchema, TitledSingleSelectEnumSchema, LegacyTitledEnumSchema - add SingleSelectEnumSchema that is a union of these three - add UntitledMultiSelectEnumSchema, TitledMultiSelectEnumSchema - add MultiSelectEnumSchema that is a union of these two - refactor EnumSchemaSchema to be a union of SingleSelectEnumSchema and MultiSelectEnumSchema * In elicitation.test.ts - simplify tests by extracting the duplicated client and server creation and connection into a beforeEach run in the describe for each flow - add positive/negative tests for all enum permutations (single-select, multi-select, titled, untitled, and legacy - "should succeed with valid selection in single-select untitled enum" - "should succeed with valid selection in single-select titled enum" - "should succeed with valid selection in single-select titled legacy enum" - "should succeed with valid selection in multi-select untitled enum" - "should succeed with valid selection in multi-select titled enum" - "should reject invalid selection in single-select untitled enum" - "should reject invalid selection in single-select titled enum" - "should reject invalid selection in single-select titled legacy enum" - "should reject invalid selection in multi-select untitled enum" - "should reject invalid selection in multi-select titled enum" --- src/server/elicitation.test.ts | 608 ++++++++++++++++++++++----------- src/types.ts | 84 ++++- 2 files changed, 481 insertions(+), 211 deletions(-) diff --git a/src/server/elicitation.test.ts b/src/server/elicitation.test.ts index 845a08cb2..7f0794e62 100644 --- a/src/server/elicitation.test.ts +++ b/src/server/elicitation.test.ts @@ -17,36 +17,58 @@ import { Server } from './index.js'; const ajvProvider = new AjvJsonSchemaValidator(); const cfWorkerProvider = new CfWorkerJsonSchemaValidator(); +let server: Server; +let client: Client; + describe('Elicitation Flow', () => { describe('with AJV validator', () => { + beforeEach(async () => { + server = new Server( + { name: 'test-server', version: '1.0.0' }, + { + capabilities: {}, + jsonSchemaValidator: ajvProvider + } + ); + + client = new Client({ name: 'test-client', version: '1.0.0' }, { capabilities: { elicitation: {} } }); + + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + + await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]); + }); + testElicitationFlow(ajvProvider, 'AJV'); }); describe('with CfWorker validator', () => { + beforeEach(async () => { + server = new Server( + { name: 'test-server', version: '1.0.0' }, + { + capabilities: {}, + jsonSchemaValidator: cfWorkerProvider + } + ); + + client = new Client({ name: 'test-client', version: '1.0.0' }, { capabilities: { elicitation: {} } }); + + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + + await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]); + }); + 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: { @@ -65,24 +87,11 @@ function testElicitationFlow(validatorProvider: typeof ajvProvider | typeof cfWo }); 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: { @@ -101,24 +110,11 @@ function testElicitationFlow(validatorProvider: typeof ajvProvider | typeof cfWo }); 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: { @@ -137,16 +133,6 @@ function testElicitationFlow(validatorProvider: typeof ajvProvider | typeof cfWo }); 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', @@ -163,9 +149,6 @@ function testElicitationFlow(validatorProvider: typeof ajvProvider | typeof cfWo 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: { @@ -191,16 +174,6 @@ function testElicitationFlow(validatorProvider: typeof ajvProvider | typeof cfWo }); 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: { @@ -209,9 +182,6 @@ function testElicitationFlow(validatorProvider: typeof ajvProvider | typeof cfWo } })); - const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); - await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]); - await expect( server.elicitInput({ message: 'Please provide your information', @@ -228,16 +198,6 @@ function testElicitationFlow(validatorProvider: typeof ajvProvider | typeof cfWo }); 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: { @@ -246,9 +206,6 @@ function testElicitationFlow(validatorProvider: typeof ajvProvider | typeof cfWo } })); - const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); - await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]); - await expect( server.elicitInput({ message: 'Please provide your information', @@ -265,24 +222,11 @@ function testElicitationFlow(validatorProvider: typeof ajvProvider | typeof cfWo }); 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?', @@ -298,24 +242,11 @@ function testElicitationFlow(validatorProvider: typeof ajvProvider | typeof cfWo }); 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?', @@ -331,24 +262,11 @@ function testElicitationFlow(validatorProvider: typeof ajvProvider | typeof cfWo }); 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', @@ -364,23 +282,10 @@ function testElicitationFlow(validatorProvider: typeof ajvProvider | typeof cfWo }); 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: { @@ -398,23 +303,10 @@ function testElicitationFlow(validatorProvider: typeof ajvProvider | typeof cfWo }); 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: { @@ -432,16 +324,6 @@ function testElicitationFlow(validatorProvider: typeof ajvProvider | typeof cfWo }); 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++; @@ -455,9 +337,6 @@ function testElicitationFlow(validatorProvider: typeof ajvProvider | typeof cfWo 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: { @@ -498,24 +377,11 @@ function testElicitationFlow(validatorProvider: typeof ajvProvider | typeof cfWo }); 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: { @@ -535,24 +401,11 @@ function testElicitationFlow(validatorProvider: typeof ajvProvider | typeof cfWo }); 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: { @@ -572,24 +425,11 @@ function testElicitationFlow(validatorProvider: typeof ajvProvider | typeof cfWo }); 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: { @@ -608,24 +448,11 @@ function testElicitationFlow(validatorProvider: typeof ajvProvider | typeof cfWo }); 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', @@ -639,4 +466,369 @@ function testElicitationFlow(validatorProvider: typeof ajvProvider | typeof cfWo }) ).rejects.toThrow(/does not match requested schema/); }); + + // Enums - Valid - Single Select - Untitled / Titled + + test(`${validatorName}: should succeed with valid selection in single-select untitled enum`, async () => { + // Set up client to return valid response + client.setRequestHandler(ElicitRequestSchema, _request => ({ + action: 'accept', + content: { + color: 'Red' + } + })); + + // Test with valid response + await expect( + server.elicitInput({ + message: 'Please provide your information', + requestedSchema: { + type: 'object', + properties: { + color: { + type: 'string', + title: 'Color Selection', + description: 'Choose your favorite color', + enum: ['Red', 'Green', 'Blue'], + default: 'Green' + } + }, + required: ['color'] + } + }) + ).resolves.toEqual({ + action: 'accept', + content: { + color: 'Red' + } + }); + }); + + test(`${validatorName}: should succeed with valid selection in single-select titled enum`, async () => { + // Set up client to return valid response + client.setRequestHandler(ElicitRequestSchema, _request => ({ + action: 'accept', + content: { + color: '#FF0000' + } + })); + + // Test with valid response + await expect( + server.elicitInput({ + message: 'Please provide your information', + requestedSchema: { + type: 'object', + properties: { + color: { + type: 'string', + title: 'Color Selection', + description: 'Choose your favorite color', + oneOf: [ + { const: '#FF0000', title: 'Red' }, + { const: '#00FF00', title: 'Green' }, + { const: '#0000FF', title: 'Blue' } + ], + default: ['#00FF00'] + } + }, + required: ['color'] + } + }) + ).resolves.toEqual({ + action: 'accept', + content: { + color: '#FF0000' + } + }); + }); + + 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 => ({ + action: 'accept', + content: { + color: '#FF0000' + } + })); + + // Test with valid response + await expect( + server.elicitInput({ + message: 'Please provide your information', + requestedSchema: { + type: 'object', + properties: { + color: { + type: 'string', + title: 'Color Selection', + description: 'Choose your favorite color', + enum: ['#FF0000', '#00FF00', '#0000FF'], + enumNames: ['Red', 'Green', 'Blue'], + default: '#00FF00' + } + }, + required: ['color'] + } + }) + ).resolves.toEqual({ + action: 'accept', + content: { + color: '#FF0000' + } + }); + }); + + // Enums - Valid - Multi Select - Untitled / Titled + + test(`${validatorName}: should succeed with valid selection in multi-select untitled enum`, async () => { + // Set up client to return valid response + client.setRequestHandler(ElicitRequestSchema, _request => ({ + action: 'accept', + content: { + color: ['Red', 'Blue'] + } + })); + + // Test with valid response + await expect( + server.elicitInput({ + message: 'Please provide your information', + requestedSchema: { + type: 'object', + properties: { + color: { + type: 'array', + title: 'Color Selection', + description: 'Choose your favorite colors', + minItems: 1, + maxItems: 3, + items: { + type: 'string', + enum: ['Red', 'Green', 'Blue'] + } + } + }, + required: ['color'] + } + }) + ).resolves.toEqual({ + action: 'accept', + content: { + color: ['Red', 'Blue'] + } + }); + }); + + test(`${validatorName}: should succeed with valid selection in multi-select titled enum`, async () => { + // Set up client to return valid response + client.setRequestHandler(ElicitRequestSchema, _request => ({ + action: 'accept', + content: { + colors: ['#FF0000', '#0000FF'] + } + })); + + // Test with valid response + await expect( + server.elicitInput({ + message: 'Please provide your information', + requestedSchema: { + type: 'object', + properties: { + colors: { + type: 'array', + title: 'Color Selection', + description: 'Choose your favorite colors', + minItems: 1, + maxItems: 3, + items: { + anyOf: [ + { const: '#FF0000', title: 'Red' }, + { const: '#00FF00', title: 'Green' }, + { const: '#0000FF', title: 'Blue' } + ] + } + } + }, + required: ['colors'] + } + }) + ).resolves.toEqual({ + action: 'accept', + content: { + colors: ['#FF0000', '#0000FF'] + } + }); + }); + + // Enums - Invalid - Single Select - Untitled / Titled + + test(`${validatorName}: should reject invalid selection in single-select untitled enum`, async () => { + // Set up client to return valid response + client.setRequestHandler(ElicitRequestSchema, _request => ({ + action: 'accept', + content: { + color: 'Black' // Color not in enum list + } + })); + + // Test with valid response + await expect( + server.elicitInput({ + message: 'Please provide your information', + requestedSchema: { + type: 'object', + properties: { + color: { + type: 'string', + title: 'Color Selection', + description: 'Choose your favorite color', + enum: ['Red', 'Green', 'Blue'], + default: 'Green' + } + }, + required: ['color'] + } + }) + ).rejects.toThrow(/^MCP error -32602: Elicitation response content does not match requested schema/); + }); + + test(`${validatorName}: should reject invalid selection in single-select titled enum`, async () => { + // Set up client to return valid response + client.setRequestHandler(ElicitRequestSchema, _request => ({ + action: 'accept', + content: { + color: 'Red' // Should be "#FF0000" (const not title) + } + })); + + // Test with valid response + await expect( + server.elicitInput({ + message: 'Please provide your information', + requestedSchema: { + type: 'object', + properties: { + color: { + type: 'string', + title: 'Color Selection', + description: 'Choose your favorite color', + oneOf: [ + { const: '#FF0000', title: 'Red' }, + { const: '#00FF00', title: 'Green' }, + { const: '#0000FF', title: 'Blue' } + ], + default: ['#00FF00'] + } + }, + required: ['color'] + } + }) + ).rejects.toThrow(/^MCP error -32602: Elicitation response content does not match requested schema/); + }); + + test(`${validatorName}: should reject invalid selection in single-select titled legacy enum`, async () => { + // Set up client to return valid response + client.setRequestHandler(ElicitRequestSchema, _request => ({ + action: 'accept', + content: { + color: 'Red' // Should be "#FF0000" (enum not enumNames) + } + })); + + // Test with valid response + await expect( + server.elicitInput({ + message: 'Please provide your information', + requestedSchema: { + type: 'object', + properties: { + color: { + type: 'string', + title: 'Color Selection', + description: 'Choose your favorite color', + enum: ['#FF0000', '#00FF00', '#0000FF'], + enumNames: ['Red', 'Green', 'Blue'], + default: '#00FF00' + } + }, + required: ['color'] + } + }) + ).rejects.toThrow(/^MCP error -32602: Elicitation response content does not match requested schema/); + }); + + // Enums - Invalid - Multi Select - Untitled / Titled + + test(`${validatorName}: should reject invalid selection in multi-select untitled enum`, async () => { + // Set up client to return valid response + client.setRequestHandler(ElicitRequestSchema, _request => ({ + action: 'accept', + content: { + color: 'Red' // Should be array, not string + } + })); + + // Test with valid response + await expect( + server.elicitInput({ + message: 'Please provide your information', + requestedSchema: { + type: 'object', + properties: { + color: { + type: 'array', + title: 'Color Selection', + description: 'Choose your favorite colors', + minItems: 1, + maxItems: 3, + items: { + type: 'string', + enum: ['Red', 'Green', 'Blue'] + } + } + }, + required: ['color'] + } + }) + ).rejects.toThrow(/^MCP error -32602: Elicitation response content does not match requested schema/); + }); + + test(`${validatorName}: should reject invalid selection in multi-select titled enum`, async () => { + // Set up client to return valid response + client.setRequestHandler(ElicitRequestSchema, _request => ({ + action: 'accept', + content: { + colors: ['Red', 'Blue'] // Should be ["#FF0000", "#0000FF"] (const not title) + } + })); + + // Test with valid response + await expect( + server.elicitInput({ + message: 'Please provide your information', + requestedSchema: { + type: 'object', + properties: { + colors: { + type: 'array', + title: 'Color Selection', + description: 'Choose your favorite colors', + minItems: 1, + maxItems: 3, + items: { + anyOf: [ + { const: '#FF0000', title: 'Red' }, + { const: '#00FF00', title: 'Green' }, + { const: '#0000FF', title: 'Blue' } + ] + } + } + }, + required: ['colors'] + } + }) + ).rejects.toThrow(/^MCP error -32602: Elicitation response content does not match requested schema/); + }); } diff --git a/src/types.ts b/src/types.ts index e6d3fe46e..67ee45e30 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1216,18 +1216,96 @@ export const NumberSchemaSchema = z .passthrough(); /** - * Primitive schema definition for enum fields. + * Schema for single-selection enumeration without display titles for options. */ -export const EnumSchemaSchema = z +export const UntitledSingleSelectEnumSchema = z .object({ type: z.literal('string'), title: z.optional(z.string()), description: z.optional(z.string()), enum: z.array(z.string()), - enumNames: z.optional(z.array(z.string())) + default: z.string().optional() }) .passthrough(); +/** + * Schema for single-selection enumeration with display titles for each option. + */ +export const TitledSingleSelectEnumSchema = z.object({ + type: z.literal('string'), + title: z.string().optional(), + description: z.string().optional(), + oneOf: z.array( + z.object({ + const: z.string(), + title: z.string() + }) + ), + default: z.string().optional() +}); + +/** + * Use TitledSingleSelectEnumSchema instead. + * This interface will be removed in a future version. + */ +export const LegacyTitledEnumSchema = z.object({ + type: z.literal('string'), + title: z.string().optional(), + description: z.string().optional(), + enum: z.array(z.string()), + enumNames: z.array(z.string()).optional(), + default: z.string().optional() +}); + +// Combined single selection enumeration +export const SingleSelectEnumSchema = z.union([UntitledSingleSelectEnumSchema, TitledSingleSelectEnumSchema, LegacyTitledEnumSchema]); + +/** + * Schema for multiple-selection enumeration without display titles for options. + */ +export const UntitledMultiSelectEnumSchema = z.object({ + type: z.literal('array'), + title: z.string().optional(), + description: z.string().optional(), + minItems: z.number().optional(), + maxItems: z.number().optional(), + items: z.object({ + type: z.literal('string'), + enum: z.array(z.string()) + }), + default: z.string().optional() +}); + +/** + * Schema for multiple-selection enumeration with display titles for each option. + */ +export const TitledMultiSelectEnumSchema = z.object({ + type: z.literal('array'), + title: z.string().optional(), + description: z.string().optional(), + minItems: z.number().optional(), + maxItems: z.number().optional(), + items: z.object({ + anyOf: z.array( + z.object({ + const: z.string(), + title: z.string() + }) + ) + }), + default: z.string().optional() +}); + +/** + * Combined schema for multiple-selection enumeration + */ +export const MultiSelectEnumSchema = z.union([UntitledMultiSelectEnumSchema, TitledMultiSelectEnumSchema]); + +/** + * Primitive schema definition for enum fields. + */ +export const EnumSchemaSchema = z.union([SingleSelectEnumSchema, MultiSelectEnumSchema]); + /** * Union of all primitive schema definitions. */ From 58bbe4706d85c555af5d3b42548f7fba289cd5d5 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Sat, 8 Nov 2025 15:18:56 -0500 Subject: [PATCH 2/6] Update EnumSchema with string array default for multi-select enums type. See [SEP-1330](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1330) * In types.ts - Set UntitledMultiSelectEnumSchema and TitledMultiSelectEnumSchema - set default to z.array(z.string()).optional() --- src/types.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/types.ts b/src/types.ts index 67ee45e30..ecdff093d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1273,7 +1273,7 @@ export const UntitledMultiSelectEnumSchema = z.object({ type: z.literal('string'), enum: z.array(z.string()) }), - default: z.string().optional() + default: z.array(z.string()).optional() }); /** @@ -1293,7 +1293,7 @@ export const TitledMultiSelectEnumSchema = z.object({ }) ) }), - default: z.string().optional() + default: z.array(z.string()).optional() }); /** From 73e33a9a71d499af93836c57fcf29f98148b61cb Mon Sep 17 00:00:00 2001 From: cliffhall Date: Sat, 8 Nov 2025 15:26:32 -0500 Subject: [PATCH 3/6] * In elicitation.test.ts - removed @ts-expect-error instances --- src/server/elicitation.test.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/server/elicitation.test.ts b/src/server/elicitation.test.ts index d192ce1a9..7f0794e62 100644 --- a/src/server/elicitation.test.ts +++ b/src/server/elicitation.test.ts @@ -159,7 +159,6 @@ function testElicitationFlow(validatorProvider: typeof ajvProvider | typeof cfWo age: { type: 'integer', minimum: 0, maximum: 150 }, street: { type: 'string' }, city: { type: 'string' }, - // @ts-expect-error - pattern is not a valid property by MCP spec, however it is making use of the Ajv validator zipCode: { type: 'string', pattern: '^[0-9]{5}$' }, newsletter: { type: 'boolean' }, notifications: { type: 'boolean' } @@ -274,7 +273,6 @@ function testElicitationFlow(validatorProvider: typeof ajvProvider | typeof cfWo requestedSchema: { type: 'object', properties: { - // @ts-expect-error - pattern is not a valid property by MCP spec, however it is making use of the Ajv validator zipCode: { type: 'string', pattern: '^[0-9]{5}$' } }, required: ['zipCode'] From 2f0cb5711782b0ffa2b4e9333f7003170468130c Mon Sep 17 00:00:00 2001 From: cliffhall Date: Sat, 8 Nov 2025 15:28:45 -0500 Subject: [PATCH 4/6] * In elicitation.test.ts - fix default return types on single-select with titles --- src/server/elicitation.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/server/elicitation.test.ts b/src/server/elicitation.test.ts index 7f0794e62..caac9a409 100644 --- a/src/server/elicitation.test.ts +++ b/src/server/elicitation.test.ts @@ -529,7 +529,7 @@ function testElicitationFlow(validatorProvider: typeof ajvProvider | typeof cfWo { const: '#00FF00', title: 'Green' }, { const: '#0000FF', title: 'Blue' } ], - default: ['#00FF00'] + default: '#00FF00' } }, required: ['color'] @@ -719,7 +719,7 @@ function testElicitationFlow(validatorProvider: typeof ajvProvider | typeof cfWo { const: '#00FF00', title: 'Green' }, { const: '#0000FF', title: 'Blue' } ], - default: ['#00FF00'] + default: '#00FF00' } }, required: ['color'] From 7b83590d34e935648276b60c6f4cc4b27f6a3075 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Sat, 8 Nov 2025 15:42:45 -0500 Subject: [PATCH 5/6] * In elicitation.test.ts - In "should succeed with valid selection in multi-select untitled enum" - rename prop to colors * In types.ts - add string array to ElicitResultSchema --- src/server/elicitation.test.ts | 8 ++++---- src/types.ts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/server/elicitation.test.ts b/src/server/elicitation.test.ts index caac9a409..7b0378b36 100644 --- a/src/server/elicitation.test.ts +++ b/src/server/elicitation.test.ts @@ -586,7 +586,7 @@ function testElicitationFlow(validatorProvider: typeof ajvProvider | typeof cfWo client.setRequestHandler(ElicitRequestSchema, _request => ({ action: 'accept', content: { - color: ['Red', 'Blue'] + colors: ['Red', 'Blue'] } })); @@ -597,7 +597,7 @@ function testElicitationFlow(validatorProvider: typeof ajvProvider | typeof cfWo requestedSchema: { type: 'object', properties: { - color: { + colors: { type: 'array', title: 'Color Selection', description: 'Choose your favorite colors', @@ -609,13 +609,13 @@ function testElicitationFlow(validatorProvider: typeof ajvProvider | typeof cfWo } } }, - required: ['color'] + required: ['colors'] } }) ).resolves.toEqual({ action: 'accept', content: { - color: ['Red', 'Blue'] + colors: ['Red', 'Blue'] } }); }); diff --git a/src/types.ts b/src/types.ts index 4fd0a455c..091f6a201 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1366,7 +1366,7 @@ export const ElicitResultSchema = ResultSchema.extend({ * The submitted form data, only present when action is "accept". * Contains values matching the requested schema. */ - content: z.record(z.union([z.string(), z.number(), z.boolean()])).optional() + content: z.record(z.union([z.string(), z.number(), z.boolean(), z.array(z.string())])).optional() }); /* Autocomplete */ From 86e367f96a8bb2081c40a2b3d7526eff28bae6c6 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Sat, 8 Nov 2025 19:06:13 -0500 Subject: [PATCH 6/6] * In elicitation.test.ts - Replace @ts-expect-error erroneously removed for "pattern" types * In types.ts - follow the pattern of the other primitive types - name the zod schemas with SchemaSchema at the end - export types inferred from enum schemas with just Schema at the end * In spec.types.test.ts - add tests for enum types --- src/server/elicitation.test.ts | 2 ++ src/spec.types.test.ts | 30 ++++++++++++++++++++++- src/types.ts | 44 +++++++++++++++++++++------------- 3 files changed, 58 insertions(+), 18 deletions(-) diff --git a/src/server/elicitation.test.ts b/src/server/elicitation.test.ts index 7b0378b36..79ed6b801 100644 --- a/src/server/elicitation.test.ts +++ b/src/server/elicitation.test.ts @@ -159,6 +159,7 @@ function testElicitationFlow(validatorProvider: typeof ajvProvider | typeof cfWo age: { type: 'integer', minimum: 0, maximum: 150 }, street: { type: 'string' }, city: { type: 'string' }, + // @ts-expect-error - pattern is not a valid property by MCP spec, however it is making use of the Ajv validator zipCode: { type: 'string', pattern: '^[0-9]{5}$' }, newsletter: { type: 'boolean' }, notifications: { type: 'boolean' } @@ -273,6 +274,7 @@ function testElicitationFlow(validatorProvider: typeof ajvProvider | typeof cfWo requestedSchema: { type: 'object', properties: { + // @ts-expect-error - pattern is not a valid property by MCP spec, however it is making use of the Ajv validator zipCode: { type: 'string', pattern: '^[0-9]{5}$' } }, required: ['zipCode'] diff --git a/src/spec.types.test.ts b/src/spec.types.test.ts index 653e9522c..81e1a88c4 100644 --- a/src/spec.types.test.ts +++ b/src/spec.types.test.ts @@ -464,6 +464,34 @@ const sdkTypeChecks = { sdk = spec; spec = sdk; }, + UntitledSingleSelectEnumSchema: (sdk: SDKTypes.UntitledSingleSelectEnumSchema, spec: SpecTypes.UntitledSingleSelectEnumSchema) => { + sdk = spec; + spec = sdk; + }, + TitledSingleSelectEnumSchema: (sdk: SDKTypes.TitledSingleSelectEnumSchema, spec: SpecTypes.TitledSingleSelectEnumSchema) => { + sdk = spec; + spec = sdk; + }, + SingleSelectEnumSchema: (sdk: SDKTypes.SingleSelectEnumSchema, spec: SpecTypes.SingleSelectEnumSchema) => { + sdk = spec; + spec = sdk; + }, + UntitledMultiSelectEnumSchema: (sdk: SDKTypes.UntitledMultiSelectEnumSchema, spec: SpecTypes.UntitledMultiSelectEnumSchema) => { + sdk = spec; + spec = sdk; + }, + TitledMultiSelectEnumSchema: (sdk: SDKTypes.TitledMultiSelectEnumSchema, spec: SpecTypes.TitledMultiSelectEnumSchema) => { + sdk = spec; + spec = sdk; + }, + MultiSelectEnumSchema: (sdk: SDKTypes.MultiSelectEnumSchema, spec: SpecTypes.MultiSelectEnumSchema) => { + sdk = spec; + spec = sdk; + }, + LegacyTitledEnumSchema: (sdk: SDKTypes.LegacyTitledEnumSchema, spec: SpecTypes.LegacyTitledEnumSchema) => { + sdk = spec; + spec = sdk; + }, PrimitiveSchemaDefinition: (sdk: SDKTypes.PrimitiveSchemaDefinition, spec: SpecTypes.PrimitiveSchemaDefinition) => { sdk = spec; spec = sdk; @@ -565,7 +593,7 @@ describe('Spec Types', () => { it('should define some expected types', () => { expect(specTypes).toContain('JSONRPCNotification'); expect(specTypes).toContain('ElicitResult'); - expect(specTypes).toHaveLength(112); + expect(specTypes).toHaveLength(119); }); it('should have up to date list of missing sdk types', () => { diff --git a/src/types.ts b/src/types.ts index 091f6a201..be181c144 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1229,21 +1229,18 @@ export const NumberSchemaSchema = z.object({ /** * Schema for single-selection enumeration without display titles for options. */ -export const UntitledSingleSelectEnumSchema = z - .object({ - type: z.literal('string'), - title: z.optional(z.string()), - description: z.optional(z.string()), - enum: z.array(z.string()), - default: z.string().optional() - }) - .passthrough(); - +export const UntitledSingleSelectEnumSchemaSchema = z.object({ + type: z.literal('string'), + title: z.string().optional(), + description: z.string().optional(), + enum: z.array(z.string()), + default: z.string().optional() +}); /** * Schema for single-selection enumeration with display titles for each option. */ -export const TitledSingleSelectEnumSchema = z.object({ +export const TitledSingleSelectEnumSchemaSchema = z.object({ type: z.literal('string'), title: z.string().optional(), description: z.string().optional(), @@ -1260,7 +1257,7 @@ export const TitledSingleSelectEnumSchema = z.object({ * Use TitledSingleSelectEnumSchema instead. * This interface will be removed in a future version. */ -export const LegacyTitledEnumSchema = z.object({ +export const LegacyTitledEnumSchemaSchema = z.object({ type: z.literal('string'), title: z.string().optional(), description: z.string().optional(), @@ -1270,12 +1267,16 @@ export const LegacyTitledEnumSchema = z.object({ }); // Combined single selection enumeration -export const SingleSelectEnumSchema = z.union([UntitledSingleSelectEnumSchema, TitledSingleSelectEnumSchema, LegacyTitledEnumSchema]); +export const SingleSelectEnumSchemaSchema = z.union([ + UntitledSingleSelectEnumSchemaSchema, + TitledSingleSelectEnumSchemaSchema, + LegacyTitledEnumSchemaSchema +]); /** * Schema for multiple-selection enumeration without display titles for options. */ -export const UntitledMultiSelectEnumSchema = z.object({ +export const UntitledMultiSelectEnumSchemaSchema = z.object({ type: z.literal('array'), title: z.string().optional(), description: z.string().optional(), @@ -1291,7 +1292,7 @@ export const UntitledMultiSelectEnumSchema = z.object({ /** * Schema for multiple-selection enumeration with display titles for each option. */ -export const TitledMultiSelectEnumSchema = z.object({ +export const TitledMultiSelectEnumSchemaSchema = z.object({ type: z.literal('array'), title: z.string().optional(), description: z.string().optional(), @@ -1311,12 +1312,12 @@ export const TitledMultiSelectEnumSchema = z.object({ /** * Combined schema for multiple-selection enumeration */ -export const MultiSelectEnumSchema = z.union([UntitledMultiSelectEnumSchema, TitledMultiSelectEnumSchema]); +export const MultiSelectEnumSchemaSchema = z.union([UntitledMultiSelectEnumSchemaSchema, TitledMultiSelectEnumSchemaSchema]); /** * Primitive schema definition for enum fields. */ -export const EnumSchemaSchema = z.union([SingleSelectEnumSchema, MultiSelectEnumSchema]); +export const EnumSchemaSchema = z.union([SingleSelectEnumSchemaSchema, MultiSelectEnumSchemaSchema]); /** * Union of all primitive schema definitions. @@ -1735,7 +1736,16 @@ export type CreateMessageResult = Infer; export type BooleanSchema = Infer; export type StringSchema = Infer; export type NumberSchema = Infer; + export type EnumSchema = Infer; +export type UntitledSingleSelectEnumSchema = Infer; +export type TitledSingleSelectEnumSchema = Infer; +export type LegacyTitledEnumSchema = Infer; +export type UntitledMultiSelectEnumSchema = Infer; +export type TitledMultiSelectEnumSchema = Infer; +export type SingleSelectEnumSchema = Infer; +export type MultiSelectEnumSchema = Infer; + export type PrimitiveSchemaDefinition = Infer; export type ElicitRequestParams = Infer; export type ElicitRequest = Infer;