@@ -15,13 +15,15 @@ import {
1515 LoggingMessageNotificationSchema ,
1616 ResourceListChangedNotificationSchema ,
1717 ElicitRequestSchema ,
18+ ElicitResult ,
1819 ResourceLink ,
1920 ReadResourceRequest ,
2021 ReadResourceResultSchema ,
2122 RELATED_TASK_META_KEY ,
2223 ErrorCode ,
2324 McpError
2425} from '../../types.js' ;
26+ import { InMemoryTaskStore } from '../../experimental/tasks/stores/in-memory.js' ;
2527import { getDisplayName } from '../../shared/metadataUtils.js' ;
2628import { Ajv } from 'ajv' ;
2729
@@ -65,6 +67,7 @@ function printHelp(): void {
6567 console . log ( ' greet [name] - Call the greet tool' ) ;
6668 console . log ( ' multi-greet [name] - Call the multi-greet tool with notifications' ) ;
6769 console . log ( ' collect-info [type] - Test form elicitation with collect-user-info tool (contact/preferences/feedback)' ) ;
70+ console . log ( ' collect-info-task [type] - Test bidirectional task support (server+client tasks) with elicitation' ) ;
6871 console . log ( ' start-notifications [interval] [count] - Start periodic notifications' ) ;
6972 console . log ( ' run-notifications-tool-with-resumability [interval] [count] - Run notification tool with resumability' ) ;
7073 console . log ( ' list-prompts - List available prompts' ) ;
@@ -131,6 +134,11 @@ function commandLoop(): void {
131134 await callCollectInfoTool ( args [ 1 ] || 'contact' ) ;
132135 break ;
133136
137+ case 'collect-info-task' : {
138+ await callCollectInfoWithTask ( args [ 1 ] || 'contact' ) ;
139+ break ;
140+ }
141+
134142 case 'start-notifications' : {
135143 const interval = args [ 1 ] ? parseInt ( args [ 1 ] , 10 ) : 2000 ;
136144 const count = args [ 2 ] ? parseInt ( args [ 2 ] , 10 ) : 10 ;
@@ -232,7 +240,10 @@ async function connect(url?: string): Promise<void> {
232240 console . log ( `Connecting to ${ serverUrl } ...` ) ;
233241
234242 try {
235- // Create a new client with form elicitation capability
243+ // Create task store for client-side task support
244+ const clientTaskStore = new InMemoryTaskStore ( ) ;
245+
246+ // Create a new client with form elicitation capability and task support
236247 client = new Client (
237248 {
238249 name : 'example-client' ,
@@ -242,25 +253,46 @@ async function connect(url?: string): Promise<void> {
242253 capabilities : {
243254 elicitation : {
244255 form : { }
256+ } ,
257+ tasks : {
258+ requests : {
259+ elicitation : {
260+ create : { }
261+ }
262+ }
245263 }
246- }
264+ } ,
265+ taskStore : clientTaskStore
247266 }
248267 ) ;
249268 client . onerror = error => {
250269 console . error ( '\x1b[31mClient error:' , error , '\x1b[0m' ) ;
251270 } ;
252271
253- // Set up elicitation request handler with proper validation
254- client . setRequestHandler ( ElicitRequestSchema , async request => {
272+ // Set up elicitation request handler with proper validation and task support
273+ client . setRequestHandler ( ElicitRequestSchema , async ( request , extra ) => {
255274 if ( request . params . mode !== 'form' ) {
256275 throw new McpError ( ErrorCode . InvalidParams , `Unsupported elicitation mode: ${ request . params . mode } ` ) ;
257276 }
258277 console . log ( '\n🔔 Elicitation (form) Request Received:' ) ;
259278 console . log ( `Message: ${ request . params . message } ` ) ;
260279 console . log ( `Related Task: ${ request . params . _meta ?. [ RELATED_TASK_META_KEY ] ?. taskId } ` ) ;
280+ console . log ( `Task Creation Requested: ${ request . params . task ? 'yes' : 'no' } ` ) ;
261281 console . log ( 'Requested Schema:' ) ;
262282 console . log ( JSON . stringify ( request . params . requestedSchema , null , 2 ) ) ;
263283
284+ // Helper to return result, optionally creating a task if requested
285+ const returnResult = async ( result : ElicitResult ) => {
286+ if ( request . params . task && extra . taskStore ) {
287+ // Create a task and store the result
288+ const task = await extra . taskStore . createTask ( { ttl : extra . taskRequestedTtl } ) ;
289+ await extra . taskStore . storeTaskResult ( task . taskId , 'completed' , result ) ;
290+ console . log ( `📋 Created client-side task: ${ task . taskId } ` ) ;
291+ return { task } ;
292+ }
293+ return result ;
294+ } ;
295+
264296 const schema = request . params . requestedSchema ;
265297 const properties = schema . properties ;
266298 const required = schema . required || [ ] ;
@@ -381,7 +413,7 @@ async function connect(url?: string): Promise<void> {
381413 }
382414
383415 if ( inputCancelled ) {
384- return { action : 'cancel' } ;
416+ return returnResult ( { action : 'cancel' } ) ;
385417 }
386418
387419 // If we didn't complete all fields due to an error, try again
@@ -394,7 +426,7 @@ async function connect(url?: string): Promise<void> {
394426 continue ;
395427 } else {
396428 console . log ( 'Maximum attempts reached. Declining request.' ) ;
397- return { action : 'decline' } ;
429+ return returnResult ( { action : 'decline' } ) ;
398430 }
399431 }
400432
@@ -412,7 +444,7 @@ async function connect(url?: string): Promise<void> {
412444 continue ;
413445 } else {
414446 console . log ( 'Maximum attempts reached. Declining request.' ) ;
415- return { action : 'decline' } ;
447+ return returnResult ( { action : 'decline' } ) ;
416448 }
417449 }
418450
@@ -426,25 +458,34 @@ async function connect(url?: string): Promise<void> {
426458 } ) ;
427459 } ) ;
428460
429- if ( confirmAnswer === 'yes' || confirmAnswer === 'y' ) {
430- return {
431- action : 'accept' ,
432- content
433- } ;
434- } else if ( confirmAnswer === 'cancel' || confirmAnswer === 'c' ) {
435- return { action : 'cancel' } ;
436- } else if ( confirmAnswer === 'no' || confirmAnswer === 'n' ) {
437- if ( attempts < maxAttempts ) {
438- console . log ( 'Please re-enter the information...' ) ;
439- continue ;
440- } else {
441- return { action : 'decline' } ;
461+ switch ( confirmAnswer ) {
462+ case 'yes' :
463+ case 'y' : {
464+ return returnResult ( {
465+ action : 'accept' ,
466+ content : content as ElicitResult [ 'content' ]
467+ } ) ;
468+ }
469+ case 'cancel' :
470+ case 'c' : {
471+ return returnResult ( { action : 'cancel' } ) ;
472+ }
473+ case 'no' :
474+ case 'n' : {
475+ if ( attempts < maxAttempts ) {
476+ console . log ( 'Please re-enter the information...' ) ;
477+ continue ;
478+ } else {
479+ return returnResult ( { action : 'decline' } ) ;
480+ }
481+
482+ break ;
442483 }
443484 }
444485 }
445486
446487 console . log ( 'Maximum attempts reached. Declining request.' ) ;
447- return { action : 'decline' } ;
488+ return returnResult ( { action : 'decline' } ) ;
448489 } ) ;
449490
450491 transport = new StreamableHTTPClientTransport ( new URL ( serverUrl ) , {
@@ -641,6 +682,12 @@ async function callCollectInfoTool(infoType: string): Promise<void> {
641682 await callTool ( 'collect-user-info' , { infoType } ) ;
642683}
643684
685+ async function callCollectInfoWithTask ( infoType : string ) : Promise < void > {
686+ console . log ( `\n🔄 Testing bidirectional task support with collect-user-info-task tool (${ infoType } )...` ) ;
687+ console . log ( 'This will create a task on the server, which will elicit input and create a task on the client.\n' ) ;
688+ await callToolTask ( 'collect-user-info-task' , { infoType } ) ;
689+ }
690+
644691async function startNotifications ( interval : number , count : number ) : Promise < void > {
645692 console . log ( `Starting notification stream: interval=${ interval } ms, count=${ count || 'unlimited' } ` ) ;
646693 await callTool ( 'start-notification-stream' , { interval, count } ) ;
0 commit comments