diff --git a/src/server/elicitation.test.ts b/src/server/elicitation.test.ts index 67361fc10..79ed6b801 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: { @@ -192,16 +175,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: { @@ -210,9 +183,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', @@ -229,16 +199,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: { @@ -247,9 +207,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', @@ -266,24 +223,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?', @@ -299,24 +243,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?', @@ -332,24 +263,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', @@ -366,23 +284,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: { @@ -400,23 +305,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: { @@ -434,16 +326,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++; @@ -457,9 +339,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: { @@ -500,24 +379,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: { @@ -537,24 +403,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: { @@ -574,24 +427,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: { @@ -610,24 +450,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', @@ -641,4 +468,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: { + colors: ['Red', 'Blue'] + } + })); + + // 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: { + type: 'string', + enum: ['Red', 'Green', 'Blue'] + } + } + }, + required: ['colors'] + } + }) + ).resolves.toEqual({ + action: 'accept', + content: { + colors: ['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/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 8b695a73b..be181c144 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1227,16 +1227,98 @@ export const NumberSchemaSchema = z.object({ }); /** - * Primitive schema definition for enum fields. + * Schema for single-selection enumeration without display titles for options. */ -export const EnumSchemaSchema = z.object({ +export const UntitledSingleSelectEnumSchemaSchema = z.object({ type: z.literal('string'), - title: z.optional(z.string()), - description: z.optional(z.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 TitledSingleSelectEnumSchemaSchema = 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 LegacyTitledEnumSchemaSchema = z.object({ + type: z.literal('string'), + title: z.string().optional(), + description: z.string().optional(), enum: z.array(z.string()), - enumNames: z.optional(z.array(z.string())) + enumNames: z.array(z.string()).optional(), + default: z.string().optional() +}); + +// Combined single selection enumeration +export const SingleSelectEnumSchemaSchema = z.union([ + UntitledSingleSelectEnumSchemaSchema, + TitledSingleSelectEnumSchemaSchema, + LegacyTitledEnumSchemaSchema +]); + +/** + * Schema for multiple-selection enumeration without display titles for options. + */ +export const UntitledMultiSelectEnumSchemaSchema = 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.array(z.string()).optional() +}); + +/** + * Schema for multiple-selection enumeration with display titles for each option. + */ +export const TitledMultiSelectEnumSchemaSchema = 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.array(z.string()).optional() }); +/** + * Combined schema for multiple-selection enumeration + */ +export const MultiSelectEnumSchemaSchema = z.union([UntitledMultiSelectEnumSchemaSchema, TitledMultiSelectEnumSchemaSchema]); + +/** + * Primitive schema definition for enum fields. + */ +export const EnumSchemaSchema = z.union([SingleSelectEnumSchemaSchema, MultiSelectEnumSchemaSchema]); + /** * Union of all primitive schema definitions. */ @@ -1285,7 +1367,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 */ @@ -1654,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;