@@ -14,7 +14,9 @@ import {
14
14
ListResourcesResultSchema ,
15
15
LoggingMessageNotificationSchema ,
16
16
ResourceListChangedNotificationSchema ,
17
+ ElicitRequestSchema ,
17
18
} from '../../types.js' ;
19
+ import { Ajv } from 'ajv' ;
18
20
19
21
// Create readline interface for user input
20
22
const readline = createInterface ( {
@@ -54,6 +56,7 @@ function printHelp(): void {
54
56
console . log ( ' call-tool <name> [args] - Call a tool with optional JSON arguments' ) ;
55
57
console . log ( ' greet [name] - Call the greet tool' ) ;
56
58
console . log ( ' multi-greet [name] - Call the multi-greet tool with notifications' ) ;
59
+ console . log ( ' collect-info [type] - Test elicitation with collect-user-info tool (contact/preferences/feedback)' ) ;
57
60
console . log ( ' start-notifications [interval] [count] - Start periodic notifications' ) ;
58
61
console . log ( ' list-prompts - List available prompts' ) ;
59
62
console . log ( ' get-prompt [name] [args] - Get a prompt with optional JSON arguments' ) ;
@@ -114,6 +117,10 @@ function commandLoop(): void {
114
117
await callMultiGreetTool ( args [ 1 ] || 'MCP User' ) ;
115
118
break ;
116
119
120
+ case 'collect-info' :
121
+ await callCollectInfoTool ( args [ 1 ] || 'contact' ) ;
122
+ break ;
123
+
117
124
case 'start-notifications' : {
118
125
const interval = args [ 1 ] ? parseInt ( args [ 1 ] , 10 ) : 2000 ;
119
126
const count = args [ 2 ] ? parseInt ( args [ 2 ] , 10 ) : 10 ;
@@ -183,15 +190,212 @@ async function connect(url?: string): Promise<void> {
183
190
console . log ( `Connecting to ${ serverUrl } ...` ) ;
184
191
185
192
try {
186
- // Create a new client
193
+ // Create a new client with elicitation capability
187
194
client = new Client ( {
188
195
name : 'example-client' ,
189
196
version : '1.0.0'
197
+ } , {
198
+ capabilities : {
199
+ elicitation : { } ,
200
+ } ,
190
201
} ) ;
191
202
client . onerror = ( error ) => {
192
203
console . error ( '\x1b[31mClient error:' , error , '\x1b[0m' ) ;
193
204
}
194
205
206
+ // Set up elicitation request handler with proper validation
207
+ client . setRequestHandler ( ElicitRequestSchema , async ( request ) => {
208
+ console . log ( '\n🔔 Elicitation Request Received:' ) ;
209
+ console . log ( `Message: ${ request . params . message } ` ) ;
210
+ console . log ( 'Requested Schema:' ) ;
211
+ console . log ( JSON . stringify ( request . params . requestedSchema , null , 2 ) ) ;
212
+
213
+ const schema = request . params . requestedSchema ;
214
+ const properties = schema . properties ;
215
+ const required = schema . required || [ ] ;
216
+
217
+ // Set up AJV validator for the requested schema
218
+ const ajv = new Ajv ( { strict : false , validateFormats : true } ) ;
219
+ const validate = ajv . compile ( schema ) ;
220
+
221
+ let attempts = 0 ;
222
+ const maxAttempts = 3 ;
223
+
224
+ while ( attempts < maxAttempts ) {
225
+ attempts ++ ;
226
+ console . log ( `\nPlease provide the following information (attempt ${ attempts } /${ maxAttempts } ):` ) ;
227
+
228
+ const content : Record < string , unknown > = { } ;
229
+ let inputCancelled = false ;
230
+
231
+ // Collect input for each field
232
+ for ( const [ fieldName , fieldSchema ] of Object . entries ( properties ) ) {
233
+ const field = fieldSchema as {
234
+ type ?: string ;
235
+ title ?: string ;
236
+ description ?: string ;
237
+ default ?: unknown ;
238
+ enum ?: string [ ] ;
239
+ minimum ?: number ;
240
+ maximum ?: number ;
241
+ minLength ?: number ;
242
+ maxLength ?: number ;
243
+ format ?: string ;
244
+ } ;
245
+
246
+ const isRequired = required . includes ( fieldName ) ;
247
+ let prompt = `${ field . title || fieldName } ` ;
248
+
249
+ // Add helpful information to the prompt
250
+ if ( field . description ) {
251
+ prompt += ` (${ field . description } )` ;
252
+ }
253
+ if ( field . enum ) {
254
+ prompt += ` [options: ${ field . enum . join ( ', ' ) } ]` ;
255
+ }
256
+ if ( field . type === 'number' || field . type === 'integer' ) {
257
+ if ( field . minimum !== undefined && field . maximum !== undefined ) {
258
+ prompt += ` [${ field . minimum } -${ field . maximum } ]` ;
259
+ } else if ( field . minimum !== undefined ) {
260
+ prompt += ` [min: ${ field . minimum } ]` ;
261
+ } else if ( field . maximum !== undefined ) {
262
+ prompt += ` [max: ${ field . maximum } ]` ;
263
+ }
264
+ }
265
+ if ( field . type === 'string' && field . format ) {
266
+ prompt += ` [format: ${ field . format } ]` ;
267
+ }
268
+ if ( isRequired ) {
269
+ prompt += ' *required*' ;
270
+ }
271
+ if ( field . default !== undefined ) {
272
+ prompt += ` [default: ${ field . default } ]` ;
273
+ }
274
+
275
+ prompt += ': ' ;
276
+
277
+ const answer = await new Promise < string > ( ( resolve ) => {
278
+ readline . question ( prompt , ( input ) => {
279
+ resolve ( input . trim ( ) ) ;
280
+ } ) ;
281
+ } ) ;
282
+
283
+ // Check for cancellation
284
+ if ( answer . toLowerCase ( ) === 'cancel' || answer . toLowerCase ( ) === 'c' ) {
285
+ inputCancelled = true ;
286
+ break ;
287
+ }
288
+
289
+ // Parse and validate the input
290
+ try {
291
+ if ( answer === '' && field . default !== undefined ) {
292
+ content [ fieldName ] = field . default ;
293
+ } else if ( answer === '' && ! isRequired ) {
294
+ // Skip optional empty fields
295
+ continue ;
296
+ } else if ( answer === '' ) {
297
+ throw new Error ( `${ fieldName } is required` ) ;
298
+ } else {
299
+ // Parse the value based on type
300
+ let parsedValue : unknown ;
301
+
302
+ if ( field . type === 'boolean' ) {
303
+ parsedValue = answer . toLowerCase ( ) === 'true' || answer . toLowerCase ( ) === 'yes' || answer === '1' ;
304
+ } else if ( field . type === 'number' ) {
305
+ parsedValue = parseFloat ( answer ) ;
306
+ if ( isNaN ( parsedValue as number ) ) {
307
+ throw new Error ( `${ fieldName } must be a valid number` ) ;
308
+ }
309
+ } else if ( field . type === 'integer' ) {
310
+ parsedValue = parseInt ( answer , 10 ) ;
311
+ if ( isNaN ( parsedValue as number ) ) {
312
+ throw new Error ( `${ fieldName } must be a valid integer` ) ;
313
+ }
314
+ } else if ( field . enum ) {
315
+ if ( ! field . enum . includes ( answer ) ) {
316
+ throw new Error ( `${ fieldName } must be one of: ${ field . enum . join ( ', ' ) } ` ) ;
317
+ }
318
+ parsedValue = answer ;
319
+ } else {
320
+ parsedValue = answer ;
321
+ }
322
+
323
+ content [ fieldName ] = parsedValue ;
324
+ }
325
+ } catch ( error ) {
326
+ console . log ( `❌ Error: ${ error } ` ) ;
327
+ // Continue to next attempt
328
+ break ;
329
+ }
330
+ }
331
+
332
+ if ( inputCancelled ) {
333
+ return { action : 'cancel' } ;
334
+ }
335
+
336
+ // If we didn't complete all fields due to an error, try again
337
+ if ( Object . keys ( content ) . length !== Object . keys ( properties ) . filter ( name =>
338
+ required . includes ( name ) || content [ name ] !== undefined
339
+ ) . length ) {
340
+ if ( attempts < maxAttempts ) {
341
+ console . log ( 'Please try again...' ) ;
342
+ continue ;
343
+ } else {
344
+ console . log ( 'Maximum attempts reached. Declining request.' ) ;
345
+ return { action : 'decline' } ;
346
+ }
347
+ }
348
+
349
+ // Validate the complete object against the schema
350
+ const isValid = validate ( content ) ;
351
+
352
+ if ( ! isValid ) {
353
+ console . log ( '❌ Validation errors:' ) ;
354
+ validate . errors ?. forEach ( error => {
355
+ console . log ( ` - ${ error . instancePath || 'root' } : ${ error . message } ` ) ;
356
+ } ) ;
357
+
358
+ if ( attempts < maxAttempts ) {
359
+ console . log ( 'Please correct the errors and try again...' ) ;
360
+ continue ;
361
+ } else {
362
+ console . log ( 'Maximum attempts reached. Declining request.' ) ;
363
+ return { action : 'decline' } ;
364
+ }
365
+ }
366
+
367
+ // Show the collected data and ask for confirmation
368
+ console . log ( '\n✅ Collected data:' ) ;
369
+ console . log ( JSON . stringify ( content , null , 2 ) ) ;
370
+
371
+ const confirmAnswer = await new Promise < string > ( ( resolve ) => {
372
+ readline . question ( '\nSubmit this information? (yes/no/cancel): ' , ( input ) => {
373
+ resolve ( input . trim ( ) . toLowerCase ( ) ) ;
374
+ } ) ;
375
+ } ) ;
376
+
377
+
378
+ if ( confirmAnswer === 'yes' || confirmAnswer === 'y' ) {
379
+ return {
380
+ action : 'accept' ,
381
+ content,
382
+ } ;
383
+ } else if ( confirmAnswer === 'cancel' || confirmAnswer === 'c' ) {
384
+ return { action : 'cancel' } ;
385
+ } else if ( confirmAnswer === 'no' || confirmAnswer === 'n' ) {
386
+ if ( attempts < maxAttempts ) {
387
+ console . log ( 'Please re-enter the information...' ) ;
388
+ continue ;
389
+ } else {
390
+ return { action : 'decline' } ;
391
+ }
392
+ }
393
+ }
394
+
395
+ console . log ( 'Maximum attempts reached. Declining request.' ) ;
396
+ return { action : 'decline' } ;
397
+ } ) ;
398
+
195
399
transport = new StreamableHTTPClientTransport (
196
400
new URL ( serverUrl ) ,
197
401
{
@@ -362,6 +566,11 @@ async function callMultiGreetTool(name: string): Promise<void> {
362
566
await callTool ( 'multi-greet' , { name } ) ;
363
567
}
364
568
569
+ async function callCollectInfoTool ( infoType : string ) : Promise < void > {
570
+ console . log ( `Testing elicitation with collect-user-info tool (${ infoType } )...` ) ;
571
+ await callTool ( 'collect-user-info' , { infoType } ) ;
572
+ }
573
+
365
574
async function startNotifications ( interval : number , count : number ) : Promise < void > {
366
575
console . log ( `Starting notification stream: interval=${ interval } ms, count=${ count || 'unlimited' } ` ) ;
367
576
await callTool ( 'start-notification-stream' , { interval, count } ) ;
0 commit comments