diff --git a/src/client/index.test.ts b/src/client/index.test.ts index bbfa80fa..abd0c34e 100644 --- a/src/client/index.test.ts +++ b/src/client/index.test.ts @@ -14,6 +14,7 @@ import { ListToolsRequestSchema, CallToolRequestSchema, CreateMessageRequestSchema, + ElicitRequestSchema, ListRootsRequestSchema, ErrorCode, } from "../types.js"; @@ -597,6 +598,43 @@ test("should only allow setRequestHandler for declared capabilities", () => { }).toThrow("Client does not support roots capability"); }); +test("should allow setRequestHandler for declared elicitation capability", () => { + const client = new Client( + { + name: "test-client", + version: "1.0.0", + }, + { + capabilities: { + elicitation: {}, + }, + }, + ); + + // This should work because elicitation is a declared capability + expect(() => { + client.setRequestHandler(ElicitRequestSchema, () => ({ + action: "accept", + content: { + username: "test-user", + confirmed: true, + }, + })); + }).not.toThrow(); + + // This should throw because sampling is not a declared capability + expect(() => { + client.setRequestHandler(CreateMessageRequestSchema, () => ({ + model: "test-model", + role: "assistant", + content: { + type: "text", + text: "Test response", + }, + })); + }).toThrow("Client does not support sampling capability"); +}); + /*** * Test: Type Checking * Test that custom request/notification/result schemas can be used with the Client class. diff --git a/src/client/index.ts b/src/client/index.ts index 98618a17..53ef8c52 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -303,6 +303,14 @@ export class Client< } break; + case "elicitation/create": + if (!this._capabilities.elicitation) { + throw new Error( + `Client does not support elicitation capability (required for ${method})`, + ); + } + break; + case "roots/list": if (!this._capabilities.roots) { throw new Error( diff --git a/src/examples/client/simpleStreamableHttp.ts b/src/examples/client/simpleStreamableHttp.ts index 0328f0d2..4bcaf94c 100644 --- a/src/examples/client/simpleStreamableHttp.ts +++ b/src/examples/client/simpleStreamableHttp.ts @@ -14,7 +14,9 @@ import { ListResourcesResultSchema, LoggingMessageNotificationSchema, ResourceListChangedNotificationSchema, + ElicitRequestSchema, } from '../../types.js'; +import Ajv from "ajv"; // Create readline interface for user input const readline = createInterface({ @@ -54,6 +56,7 @@ function printHelp(): void { console.log(' call-tool [args] - Call a tool with optional JSON arguments'); console.log(' greet [name] - Call the greet tool'); console.log(' multi-greet [name] - Call the multi-greet tool with notifications'); + console.log(' collect-info [type] - Test elicitation with collect-user-info tool (contact/preferences/feedback)'); console.log(' start-notifications [interval] [count] - Start periodic notifications'); console.log(' list-prompts - List available prompts'); console.log(' get-prompt [name] [args] - Get a prompt with optional JSON arguments'); @@ -114,6 +117,10 @@ function commandLoop(): void { await callMultiGreetTool(args[1] || 'MCP User'); break; + case 'collect-info': + await callCollectInfoTool(args[1] || 'contact'); + break; + case 'start-notifications': { const interval = args[1] ? parseInt(args[1], 10) : 2000; const count = args[2] ? parseInt(args[2], 10) : 10; @@ -183,15 +190,212 @@ async function connect(url?: string): Promise { console.log(`Connecting to ${serverUrl}...`); try { - // Create a new client + // Create a new client with elicitation capability client = new Client({ name: 'example-client', version: '1.0.0' + }, { + capabilities: { + elicitation: {}, + }, }); client.onerror = (error) => { console.error('\x1b[31mClient error:', error, '\x1b[0m'); } + // Set up elicitation request handler with proper validation + client.setRequestHandler(ElicitRequestSchema, async (request) => { + console.log('\nšŸ”” Elicitation Request Received:'); + console.log(`Message: ${request.params.message}`); + console.log('Requested Schema:'); + console.log(JSON.stringify(request.params.requestedSchema, null, 2)); + + const schema = request.params.requestedSchema; + const properties = schema.properties; + const required = schema.required || []; + + // Set up AJV validator for the requested schema + const ajv = new Ajv(); + const validate = ajv.compile(schema); + + let attempts = 0; + const maxAttempts = 3; + + while (attempts < maxAttempts) { + attempts++; + console.log(`\nPlease provide the following information (attempt ${attempts}/${maxAttempts}):`); + + const content: Record = {}; + let inputCancelled = false; + + // Collect input for each field + for (const [fieldName, fieldSchema] of Object.entries(properties)) { + const field = fieldSchema as { + type?: string; + title?: string; + description?: string; + default?: unknown; + enum?: string[]; + minimum?: number; + maximum?: number; + minLength?: number; + maxLength?: number; + format?: string; + }; + + const isRequired = required.includes(fieldName); + let prompt = `${field.title || fieldName}`; + + // Add helpful information to the prompt + if (field.description) { + prompt += ` (${field.description})`; + } + if (field.enum) { + prompt += ` [options: ${field.enum.join(', ')}]`; + } + if (field.type === 'number' || field.type === 'integer') { + if (field.minimum !== undefined && field.maximum !== undefined) { + prompt += ` [${field.minimum}-${field.maximum}]`; + } else if (field.minimum !== undefined) { + prompt += ` [min: ${field.minimum}]`; + } else if (field.maximum !== undefined) { + prompt += ` [max: ${field.maximum}]`; + } + } + if (field.type === 'string' && field.format) { + prompt += ` [format: ${field.format}]`; + } + if (isRequired) { + prompt += ' *required*'; + } + if (field.default !== undefined) { + prompt += ` [default: ${field.default}]`; + } + + prompt += ': '; + + const answer = await new Promise((resolve) => { + readline.question(prompt, (input) => { + resolve(input.trim()); + }); + }); + + // Check for cancellation + if (answer.toLowerCase() === 'cancel' || answer.toLowerCase() === 'c') { + inputCancelled = true; + break; + } + + // Parse and validate the input + try { + if (answer === '' && field.default !== undefined) { + content[fieldName] = field.default; + } else if (answer === '' && !isRequired) { + // Skip optional empty fields + continue; + } else if (answer === '') { + throw new Error(`${fieldName} is required`); + } else { + // Parse the value based on type + let parsedValue: unknown; + + if (field.type === 'boolean') { + parsedValue = answer.toLowerCase() === 'true' || answer.toLowerCase() === 'yes' || answer === '1'; + } else if (field.type === 'number') { + parsedValue = parseFloat(answer); + if (isNaN(parsedValue as number)) { + throw new Error(`${fieldName} must be a valid number`); + } + } else if (field.type === 'integer') { + parsedValue = parseInt(answer, 10); + if (isNaN(parsedValue as number)) { + throw new Error(`${fieldName} must be a valid integer`); + } + } else if (field.enum) { + if (!field.enum.includes(answer)) { + throw new Error(`${fieldName} must be one of: ${field.enum.join(', ')}`); + } + parsedValue = answer; + } else { + parsedValue = answer; + } + + content[fieldName] = parsedValue; + } + } catch (error) { + console.log(`āŒ Error: ${error}`); + // Continue to next attempt + break; + } + } + + if (inputCancelled) { + return { action: 'cancel' }; + } + + // If we didn't complete all fields due to an error, try again + if (Object.keys(content).length !== Object.keys(properties).filter(name => + required.includes(name) || content[name] !== undefined + ).length) { + if (attempts < maxAttempts) { + console.log('Please try again...'); + continue; + } else { + console.log('Maximum attempts reached. Declining request.'); + return { action: 'decline' }; + } + } + + // Validate the complete object against the schema + const isValid = validate(content); + + if (!isValid) { + console.log('āŒ Validation errors:'); + validate.errors?.forEach(error => { + console.log(` - ${error.dataPath || 'root'}: ${error.message}`); + }); + + if (attempts < maxAttempts) { + console.log('Please correct the errors and try again...'); + continue; + } else { + console.log('Maximum attempts reached. Declining request.'); + return { action: 'decline' }; + } + } + + // Show the collected data and ask for confirmation + console.log('\nāœ… Collected data:'); + console.log(JSON.stringify(content, null, 2)); + + const confirmAnswer = await new Promise((resolve) => { + readline.question('\nSubmit this information? (yes/no/cancel): ', (input) => { + resolve(input.trim().toLowerCase()); + }); + }); + + + if (confirmAnswer === 'yes' || confirmAnswer === 'y') { + return { + action: 'accept', + content, + }; + } else if (confirmAnswer === 'cancel' || confirmAnswer === 'c') { + return { action: 'cancel' }; + } else if (confirmAnswer === 'no' || confirmAnswer === 'n') { + if (attempts < maxAttempts) { + console.log('Please re-enter the information...'); + continue; + } else { + return { action: 'decline' }; + } + } + } + + console.log('Maximum attempts reached. Declining request.'); + return { action: 'decline' }; + }); + transport = new StreamableHTTPClientTransport( new URL(serverUrl), { @@ -362,6 +566,11 @@ async function callMultiGreetTool(name: string): Promise { await callTool('multi-greet', { name }); } +async function callCollectInfoTool(infoType: string): Promise { + console.log(`Testing elicitation with collect-user-info tool (${infoType})...`); + await callTool('collect-user-info', { infoType }); +} + async function startNotifications(interval: number, count: number): Promise { console.log(`Starting notification stream: interval=${interval}ms, count=${count || 'unlimited'}`); await callTool('start-notification-stream', { interval, count }); diff --git a/src/examples/server/simpleStreamableHttp.ts b/src/examples/server/simpleStreamableHttp.ts index 6c331192..4ef50446 100644 --- a/src/examples/server/simpleStreamableHttp.ts +++ b/src/examples/server/simpleStreamableHttp.ts @@ -5,7 +5,7 @@ import { McpServer } from '../../server/mcp.js'; import { StreamableHTTPServerTransport } from '../../server/streamableHttp.js'; import { getOAuthProtectedResourceMetadataUrl, mcpAuthMetadataRouter } from '../../server/auth/router.js'; import { requireBearerAuth } from '../../server/auth/middleware/bearerAuth.js'; -import { CallToolResult, GetPromptResult, isInitializeRequest, ReadResourceResult } from '../../types.js'; +import { CallToolResult, GetPromptResult, isInitializeRequest, PrimitiveSchemaDefinition, ReadResourceResult } from '../../types.js'; import { InMemoryEventStore } from '../shared/inMemoryEventStore.js'; import { setupAuthServer } from './demoInMemoryOAuthProvider.js'; import { OAuthMetadata } from 'src/shared/auth.js'; @@ -84,6 +84,157 @@ const getServer = () => { } ); + // Register a tool that demonstrates elicitation (user input collection) + // This creates a closure that captures the server instance + server.tool( + 'collect-user-info', + 'A tool that collects user information through elicitation', + { + infoType: z.enum(['contact', 'preferences', 'feedback']).describe('Type of information to collect'), + }, + async ({ infoType }): Promise => { + let message: string; + let requestedSchema: { + type: 'object'; + properties: Record; + required?: string[]; + }; + + switch (infoType) { + case 'contact': + message = 'Please provide your contact information'; + requestedSchema = { + type: 'object', + properties: { + name: { + type: 'string', + title: 'Full Name', + description: 'Your full name', + }, + email: { + type: 'string', + title: 'Email Address', + description: 'Your email address', + format: 'email', + }, + phone: { + type: 'string', + title: 'Phone Number', + description: 'Your phone number (optional)', + }, + }, + required: ['name', 'email'], + }; + break; + case 'preferences': + message = 'Please set your preferences'; + requestedSchema = { + type: 'object', + properties: { + theme: { + type: 'string', + title: 'Theme', + description: 'Choose your preferred theme', + enum: ['light', 'dark', 'auto'], + enumNames: ['Light', 'Dark', 'Auto'], + }, + notifications: { + type: 'boolean', + title: 'Enable Notifications', + description: 'Would you like to receive notifications?', + default: true, + }, + frequency: { + type: 'string', + title: 'Notification Frequency', + description: 'How often would you like notifications?', + enum: ['daily', 'weekly', 'monthly'], + enumNames: ['Daily', 'Weekly', 'Monthly'], + }, + }, + required: ['theme'], + }; + break; + case 'feedback': + message = 'Please provide your feedback'; + requestedSchema = { + type: 'object', + properties: { + rating: { + type: 'integer', + title: 'Rating', + description: 'Rate your experience (1-5)', + minimum: 1, + maximum: 5, + }, + comments: { + type: 'string', + title: 'Comments', + description: 'Additional comments (optional)', + maxLength: 500, + }, + recommend: { + type: 'boolean', + title: 'Would you recommend this?', + description: 'Would you recommend this to others?', + }, + }, + required: ['rating', 'recommend'], + }; + break; + default: + throw new Error(`Unknown info type: ${infoType}`); + } + + try { + // Use the underlying server instance to elicit input from the client + const result = await server.server.elicitInput({ + message, + requestedSchema, + }); + + if (result.action === 'accept') { + return { + content: [ + { + type: 'text', + text: `Thank you! Collected ${infoType} information: ${JSON.stringify(result.content, null, 2)}`, + }, + ], + }; + } else if (result.action === 'decline') { + return { + content: [ + { + type: 'text', + text: `No information was collected. User declined to provide ${infoType} information.`, + }, + ], + }; + } else { + return { + content: [ + { + type: 'text', + text: `Information collection was cancelled by the user.`, + }, + ], + }; + } + } catch (error) { + return { + content: [ + { + type: 'text', + text: `Error collecting ${infoType} information: ${error}`, + }, + ], + }; + } + } + ); + + // Register a simple prompt server.prompt( 'greeting-template', diff --git a/src/server/index.test.ts b/src/server/index.test.ts index 7c0fbc51..ce54247a 100644 --- a/src/server/index.test.ts +++ b/src/server/index.test.ts @@ -10,6 +10,7 @@ import { LATEST_PROTOCOL_VERSION, SUPPORTED_PROTOCOL_VERSIONS, CreateMessageRequestSchema, + ElicitRequestSchema, ListPromptsRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, @@ -267,6 +268,318 @@ test("should respect client capabilities", async () => { await expect(server.listRoots()).rejects.toThrow(/^Client does not support/); }); +test("should respect client elicitation capabilities", async () => { + const server = new Server( + { + name: "test server", + version: "1.0", + }, + { + capabilities: { + prompts: {}, + resources: {}, + tools: {}, + logging: {}, + }, + enforceStrictCapabilities: true, + }, + ); + + const client = new Client( + { + name: "test client", + version: "1.0", + }, + { + capabilities: { + elicitation: {}, + }, + }, + ); + + client.setRequestHandler(ElicitRequestSchema, (params) => ({ + action: "accept", + content: { + username: params.params.message.includes("username") ? "test-user" : undefined, + confirmed: true, + }, + })); + + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + + await Promise.all([ + client.connect(clientTransport), + server.connect(serverTransport), + ]); + + expect(server.getClientCapabilities()).toEqual({ elicitation: {} }); + + // This should work because elicitation is supported by the client + await expect( + server.elicitInput({ + message: "Please provide your username", + requestedSchema: { + type: "object", + properties: { + username: { + type: "string", + title: "Username", + description: "Your username", + }, + confirmed: { + type: "boolean", + title: "Confirm", + description: "Please confirm", + default: false, + }, + }, + required: ["username"], + }, + }), + ).resolves.toEqual({ + action: "accept", + content: { + username: "test-user", + confirmed: true, + }, + }); + + // This should still throw because sampling is not supported by the client + await expect( + server.createMessage({ + messages: [], + maxTokens: 10, + }), + ).rejects.toThrow(/^Client does not support/); +}); + +test("should validate elicitation response against requested schema", async () => { + const server = new Server( + { + name: "test server", + version: "1.0", + }, + { + capabilities: { + prompts: {}, + resources: {}, + tools: {}, + logging: {}, + }, + enforceStrictCapabilities: true, + }, + ); + + const client = new Client( + { + name: "test client", + version: "1.0", + }, + { + capabilities: { + elicitation: {}, + }, + }, + ); + + // Set up client to return valid response + client.setRequestHandler(ElicitRequestSchema, (request) => ({ + action: "accept", + content: { + name: "John Doe", + email: "john@example.com", + age: 30, + }, + })); + + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + + await Promise.all([ + client.connect(clientTransport), + server.connect(serverTransport), + ]); + + // Test with valid response + await expect( + server.elicitInput({ + message: "Please provide your information", + requestedSchema: { + type: "object", + properties: { + name: { + type: "string", + minLength: 1, + }, + email: { + type: "string", + minLength: 1, + }, + age: { + type: "integer", + minimum: 0, + maximum: 150, + }, + }, + required: ["name", "email"], + }, + }), + ).resolves.toEqual({ + action: "accept", + content: { + name: "John Doe", + email: "john@example.com", + age: 30, + }, + }); +}); + +test("should reject elicitation response with invalid data", async () => { + const server = new Server( + { + name: "test server", + version: "1.0", + }, + { + capabilities: { + prompts: {}, + resources: {}, + tools: {}, + logging: {}, + }, + enforceStrictCapabilities: true, + }, + ); + + const client = new Client( + { + name: "test client", + version: "1.0", + }, + { + capabilities: { + elicitation: {}, + }, + }, + ); + + // Set up client to return invalid response (missing required field, invalid age) + client.setRequestHandler(ElicitRequestSchema, (request) => ({ + action: "accept", + content: { + email: "", // Invalid - too short + age: -5, // Invalid age + }, + })); + + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + + await Promise.all([ + client.connect(clientTransport), + server.connect(serverTransport), + ]); + + // Test with invalid response + await expect( + server.elicitInput({ + message: "Please provide your information", + requestedSchema: { + type: "object", + properties: { + name: { + type: "string", + minLength: 1, + }, + email: { + type: "string", + minLength: 1, + }, + age: { + type: "integer", + minimum: 0, + maximum: 150, + }, + }, + required: ["name", "email"], + }, + }), + ).rejects.toThrow(/does not match requested schema/); +}); + +test("should allow elicitation decline and cancel without validation", async () => { + const server = new Server( + { + name: "test server", + version: "1.0", + }, + { + capabilities: { + prompts: {}, + resources: {}, + tools: {}, + logging: {}, + }, + enforceStrictCapabilities: true, + }, + ); + + const client = new Client( + { + name: "test client", + version: "1.0", + }, + { + capabilities: { + elicitation: {}, + }, + }, + ); + + let requestCount = 0; + client.setRequestHandler(ElicitRequestSchema, (request) => { + requestCount++; + if (requestCount === 1) { + return { action: "decline" }; + } else { + return { action: "cancel" }; + } + }); + + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + + await Promise.all([ + client.connect(clientTransport), + server.connect(serverTransport), + ]); + + const schema = { + type: "object" as const, + properties: { + name: { type: "string" as const }, + }, + required: ["name"], + }; + + // Test decline - should not validate + await expect( + server.elicitInput({ + message: "Please provide your name", + requestedSchema: schema, + }), + ).resolves.toEqual({ + action: "decline", + }); + + // Test cancel - should not validate + await expect( + server.elicitInput({ + message: "Please provide your name", + requestedSchema: schema, + }), + ).resolves.toEqual({ + action: "cancel", + }); +}); + test("should respect server notification capabilities", async () => { const server = new Server( { diff --git a/src/server/index.ts b/src/server/index.ts index 3901099e..5d482d32 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -8,6 +8,9 @@ import { ClientCapabilities, CreateMessageRequest, CreateMessageResultSchema, + ElicitRequest, + ElicitResult, + ElicitResultSchema, EmptyResultSchema, Implementation, InitializedNotificationSchema, @@ -18,6 +21,8 @@ import { ListRootsRequest, ListRootsResultSchema, LoggingMessageNotification, + McpError, + ErrorCode, Notification, Request, ResourceUpdatedNotification, @@ -28,6 +33,7 @@ import { ServerResult, SUPPORTED_PROTOCOL_VERSIONS, } from "../types.js"; +import Ajv from "ajv"; export type ServerOptions = ProtocolOptions & { /** @@ -129,6 +135,14 @@ export class Server< } break; + case "elicitation/create": + if (!this._clientCapabilities?.elicitation) { + throw new Error( + `Client does not support elicitation (required for ${method})`, + ); + } + break; + case "roots/list": if (!this._clientCapabilities?.roots) { throw new Error( @@ -294,6 +308,44 @@ export class Server< ); } + async elicitInput( + params: ElicitRequest["params"], + options?: RequestOptions, + ): Promise { + 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) { + try { + const ajv = new Ajv(); + + const validate = ajv.compile(params.requestedSchema); + const isValid = validate(result.content); + + if (!isValid) { + throw new McpError( + ErrorCode.InvalidParams, + `Elicitation response content does not match requested schema: ${ajv.errorsText(validate.errors)}`, + ); + } + } catch (error) { + if (error instanceof McpError) { + throw error; + } + throw new McpError( + ErrorCode.InternalError, + `Error validating elicitation response: ${error}`, + ); + } + } + + return result; + } + async listRoots( params?: ListRootsRequest["params"], options?: RequestOptions, diff --git a/src/shared/protocol.test.ts b/src/shared/protocol.test.ts index e0141da1..ac453b17 100644 --- a/src/shared/protocol.test.ts +++ b/src/shared/protocol.test.ts @@ -339,6 +339,7 @@ describe("mergeCapabilities", () => { experimental: { feature: true, }, + elicitation: {}, roots: { newProp: true, }, @@ -347,6 +348,7 @@ describe("mergeCapabilities", () => { const merged = mergeCapabilities(base, additional); expect(merged).toEqual({ sampling: {}, + elicitation: {}, roots: { listChanged: true, newProp: true, diff --git a/src/types.ts b/src/types.ts index ae25848e..e5a544b5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -218,6 +218,10 @@ export const ClientCapabilitiesSchema = z * Present if the client supports sampling from an LLM. */ sampling: z.optional(z.object({}).passthrough()), + /** + * Present if the client supports eliciting user input. + */ + elicitation: z.optional(z.object({}).passthrough()), /** * Present if the client supports listing roots. */ @@ -1088,6 +1092,107 @@ export const CreateMessageResultSchema = ResultSchema.extend({ ]), }); +/* Elicitation */ +/** + * Primitive schema definition for boolean fields. + */ +export const BooleanSchemaSchema = z + .object({ + type: z.literal("boolean"), + title: z.optional(z.string()), + description: z.optional(z.string()), + default: z.optional(z.boolean()), + }) + .passthrough(); + +/** + * Primitive schema definition for string fields. + */ +export const StringSchemaSchema = z + .object({ + type: z.literal("string"), + title: z.optional(z.string()), + description: z.optional(z.string()), + minLength: z.optional(z.number()), + maxLength: z.optional(z.number()), + format: z.optional(z.enum(["email", "uri", "date", "date-time"])), + }) + .passthrough(); + +/** + * Primitive schema definition for number fields. + */ +export const NumberSchemaSchema = z + .object({ + type: z.enum(["number", "integer"]), + title: z.optional(z.string()), + description: z.optional(z.string()), + minimum: z.optional(z.number()), + maximum: z.optional(z.number()), + }) + .passthrough(); + +/** + * Primitive schema definition for enum fields. + */ +export const EnumSchemaSchema = 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())), + }) + .passthrough(); + +/** + * Union of all primitive schema definitions. + */ +export const PrimitiveSchemaDefinitionSchema = z.union([ + BooleanSchemaSchema, + StringSchemaSchema, + NumberSchemaSchema, + EnumSchemaSchema, +]); + +/** + * A request from the server to elicit user input via the client. + * The client should present the message and form fields to the user. + */ +export const ElicitRequestSchema = RequestSchema.extend({ + method: z.literal("elicitation/create"), + params: BaseRequestParamsSchema.extend({ + /** + * The message to present to the user. + */ + message: z.string(), + /** + * The schema for the requested user input. + */ + requestedSchema: z + .object({ + type: z.literal("object"), + properties: z.record(z.string(), PrimitiveSchemaDefinitionSchema), + required: z.optional(z.array(z.string())), + }) + .passthrough(), + }), +}); + +/** + * The client's response to an elicitation/create request from the server. + */ +export const ElicitResultSchema = ResultSchema.extend({ + /** + * The user's response action. + */ + action: z.enum(["accept", "decline", "cancel"]), + /** + * The collected user input content (only present if action is "accept"). + */ + content: z.optional(z.record(z.string(), z.unknown())), +}); + /* Autocomplete */ /** * A reference to a resource or resource template definition. @@ -1227,6 +1332,7 @@ export const ClientNotificationSchema = z.union([ export const ClientResultSchema = z.union([ EmptyResultSchema, CreateMessageResultSchema, + ElicitResultSchema, ListRootsResultSchema, ]); @@ -1234,6 +1340,7 @@ export const ClientResultSchema = z.union([ export const ServerRequestSchema = z.union([ PingRequestSchema, CreateMessageRequestSchema, + ElicitRequestSchema, ListRootsRequestSchema, ]); @@ -1376,6 +1483,15 @@ export type SamplingMessage = Infer; export type CreateMessageRequest = Infer; export type CreateMessageResult = Infer; +/* Elicitation */ +export type BooleanSchema = Infer; +export type StringSchema = Infer; +export type NumberSchema = Infer; +export type EnumSchema = Infer; +export type PrimitiveSchemaDefinition = Infer; +export type ElicitRequest = Infer; +export type ElicitResult = Infer; + /* Autocomplete */ export type ResourceReference = Infer; export type PromptReference = Infer;