@@ -42,7 +42,13 @@ import {
4242 ElicitRequestSchema ,
4343 CreateTaskResultSchema ,
4444 CreateMessageRequestSchema ,
45- CreateMessageResultSchema
45+ CreateMessageResultSchema ,
46+ ToolListChangedNotificationSchema ,
47+ PromptListChangedNotificationSchema ,
48+ ResourceListChangedNotificationSchema ,
49+ ListChangedOptions ,
50+ ListChangedOptionsBaseSchema ,
51+ type ListChangedHandlers
4652} from '../types.js' ;
4753import { AjvJsonSchemaValidator } from '../validation/ajv-provider.js' ;
4854import type { JsonSchemaType , JsonSchemaValidator , jsonSchemaValidator } from '../validation/types.js' ;
@@ -87,14 +93,20 @@ function applyElicitationDefaults(schema: JsonSchemaType | undefined, data: unkn
8793
8894 if ( Array . isArray ( schema . anyOf ) ) {
8995 for ( const sub of schema . anyOf ) {
90- applyElicitationDefaults ( sub , data ) ;
96+ // Skip boolean schemas (true/false are valid JSON Schemas but have no defaults)
97+ if ( typeof sub !== 'boolean' ) {
98+ applyElicitationDefaults ( sub , data ) ;
99+ }
91100 }
92101 }
93102
94103 // Combine schemas
95104 if ( Array . isArray ( schema . oneOf ) ) {
96105 for ( const sub of schema . oneOf ) {
97- applyElicitationDefaults ( sub , data ) ;
106+ // Skip boolean schemas (true/false are valid JSON Schemas but have no defaults)
107+ if ( typeof sub !== 'boolean' ) {
108+ applyElicitationDefaults ( sub , data ) ;
109+ }
98110 }
99111 }
100112}
@@ -163,6 +175,34 @@ export type ClientOptions = ProtocolOptions & {
163175 * ```
164176 */
165177 jsonSchemaValidator ?: jsonSchemaValidator ;
178+
179+ /**
180+ * Configure handlers for list changed notifications (tools, prompts, resources).
181+ *
182+ * @example
183+ * ```typescript
184+ * const client = new Client(
185+ * { name: 'my-client', version: '1.0.0' },
186+ * {
187+ * listChanged: {
188+ * tools: {
189+ * onChanged: (error, tools) => {
190+ * if (error) {
191+ * console.error('Failed to refresh tools:', error);
192+ * return;
193+ * }
194+ * console.log('Tools updated:', tools);
195+ * }
196+ * },
197+ * prompts: {
198+ * onChanged: (error, prompts) => console.log('Prompts updated:', prompts)
199+ * }
200+ * }
201+ * }
202+ * );
203+ * ```
204+ */
205+ listChanged ?: ListChangedHandlers ;
166206} ;
167207
168208/**
@@ -204,6 +244,8 @@ export class Client<
204244 private _cachedKnownTaskTools : Set < string > = new Set ( ) ;
205245 private _cachedRequiredTaskTools : Set < string > = new Set ( ) ;
206246 private _experimental ?: { tasks : ExperimentalClientTasks < RequestT , NotificationT , ResultT > } ;
247+ private _listChangedDebounceTimers : Map < string , ReturnType < typeof setTimeout > > = new Map ( ) ;
248+ private _pendingListChangedConfig ?: ListChangedHandlers ;
207249
208250 /**
209251 * Initializes this client with the given name and version information.
@@ -215,6 +257,40 @@ export class Client<
215257 super ( options ) ;
216258 this . _capabilities = options ?. capabilities ?? { } ;
217259 this . _jsonSchemaValidator = options ?. jsonSchemaValidator ?? new AjvJsonSchemaValidator ( ) ;
260+
261+ // Store list changed config for setup after connection (when we know server capabilities)
262+ if ( options ?. listChanged ) {
263+ this . _pendingListChangedConfig = options . listChanged ;
264+ }
265+ }
266+
267+ /**
268+ * Set up handlers for list changed notifications based on config and server capabilities.
269+ * This should only be called after initialization when server capabilities are known.
270+ * Handlers are silently skipped if the server doesn't advertise the corresponding listChanged capability.
271+ * @internal
272+ */
273+ private _setupListChangedHandlers ( config : ListChangedHandlers ) : void {
274+ if ( config . tools && this . _serverCapabilities ?. tools ?. listChanged ) {
275+ this . _setupListChangedHandler ( 'tools' , ToolListChangedNotificationSchema , config . tools , async ( ) => {
276+ const result = await this . listTools ( ) ;
277+ return result . tools ;
278+ } ) ;
279+ }
280+
281+ if ( config . prompts && this . _serverCapabilities ?. prompts ?. listChanged ) {
282+ this . _setupListChangedHandler ( 'prompts' , PromptListChangedNotificationSchema , config . prompts , async ( ) => {
283+ const result = await this . listPrompts ( ) ;
284+ return result . prompts ;
285+ } ) ;
286+ }
287+
288+ if ( config . resources && this . _serverCapabilities ?. resources ?. listChanged ) {
289+ this . _setupListChangedHandler ( 'resources' , ResourceListChangedNotificationSchema , config . resources , async ( ) => {
290+ const result = await this . listResources ( ) ;
291+ return result . resources ;
292+ } ) ;
293+ }
218294 }
219295
220296 /**
@@ -442,6 +518,12 @@ export class Client<
442518 await this . notification ( {
443519 method : 'notifications/initialized'
444520 } ) ;
521+
522+ // Set up list changed handlers now that we know server capabilities
523+ if ( this . _pendingListChangedConfig ) {
524+ this . _setupListChangedHandlers ( this . _pendingListChangedConfig ) ;
525+ this . _pendingListChangedConfig = undefined ;
526+ }
445527 } catch ( error ) {
446528 // Disconnect if initialization fails.
447529 void this . close ( ) ;
@@ -757,6 +839,66 @@ export class Client<
757839 return result ;
758840 }
759841
842+ /**
843+ * Set up a single list changed handler.
844+ * @internal
845+ */
846+ private _setupListChangedHandler < T > (
847+ listType : string ,
848+ notificationSchema : { shape : { method : { value : string } } } ,
849+ options : ListChangedOptions < T > ,
850+ fetcher : ( ) => Promise < T [ ] >
851+ ) : void {
852+ // Validate options using Zod schema (validates autoRefresh and debounceMs)
853+ const parseResult = ListChangedOptionsBaseSchema . safeParse ( options ) ;
854+ if ( ! parseResult . success ) {
855+ throw new Error ( `Invalid ${ listType } listChanged options: ${ parseResult . error . message } ` ) ;
856+ }
857+
858+ // Validate callback
859+ if ( typeof options . onChanged !== 'function' ) {
860+ throw new Error ( `Invalid ${ listType } listChanged options: onChanged must be a function` ) ;
861+ }
862+
863+ const { autoRefresh, debounceMs } = parseResult . data ;
864+ const { onChanged } = options ;
865+
866+ const refresh = async ( ) => {
867+ if ( ! autoRefresh ) {
868+ onChanged ( null , null ) ;
869+ return ;
870+ }
871+
872+ try {
873+ const items = await fetcher ( ) ;
874+ onChanged ( null , items ) ;
875+ } catch ( e ) {
876+ const error = e instanceof Error ? e : new Error ( String ( e ) ) ;
877+ onChanged ( error , null ) ;
878+ }
879+ } ;
880+
881+ const handler = ( ) => {
882+ if ( debounceMs ) {
883+ // Clear any pending debounce timer for this list type
884+ const existingTimer = this . _listChangedDebounceTimers . get ( listType ) ;
885+ if ( existingTimer ) {
886+ clearTimeout ( existingTimer ) ;
887+ }
888+
889+ // Set up debounced refresh
890+ const timer = setTimeout ( refresh , debounceMs ) ;
891+ this . _listChangedDebounceTimers . set ( listType , timer ) ;
892+ } else {
893+ // No debounce, refresh immediately
894+ refresh ( ) ;
895+ }
896+ } ;
897+
898+ // Register notification handler
899+ this . setNotificationHandler ( notificationSchema as AnyObjectSchema , handler ) ;
900+ }
901+
760902 async sendRootsListChanged ( ) {
761903 return this . notification ( { method : 'notifications/roots/list_changed' } ) ;
762904 }
0 commit comments