Skip to content

Commit e91c862

Browse files
committed
Added auto-refreshing tool list notification handler to client
Added handler for server's notifications/tools/list_changed notification Implemented auto-refresh for tools list when notification is received Added onToolListChanged callback property to provide updated tools to clients Added setToolListChangedCallback convenience method for callback registration Added capability validation to ensure tools support before refreshing Fixed type definitions for proper TypeScript compliance Added error handling for failed refreshes
1 parent 592c91f commit e91c862

File tree

1 file changed

+90
-40
lines changed

1 file changed

+90
-40
lines changed

Diff for: src/client/index.ts

+90-40
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
ListResourceTemplatesRequest,
2929
ListResourceTemplatesResultSchema,
3030
ListToolsRequest,
31+
ListToolsResult,
3132
ListToolsResultSchema,
3233
LoggingLevel,
3334
Notification,
@@ -76,7 +77,7 @@ export type ClientOptions = ProtocolOptions & {
7677
export class Client<
7778
RequestT extends Request = Request,
7879
NotificationT extends Notification = Notification,
79-
ResultT extends Result = Result,
80+
ResultT extends Result = Result
8081
> extends Protocol<
8182
ClientRequest | RequestT,
8283
ClientNotification | NotificationT,
@@ -87,15 +88,38 @@ export class Client<
8788
private _capabilities: ClientCapabilities;
8889
private _instructions?: string;
8990

91+
/**
92+
* Callback for when the server indicates that the tools list has changed.
93+
* Client should typically refresh its list of tools in response.
94+
*/
95+
onToolListChanged?: (tools?: ListToolsResult["tools"]) => void;
96+
9097
/**
9198
* Initializes this client with the given name and version information.
9299
*/
93-
constructor(
94-
private _clientInfo: Implementation,
95-
options?: ClientOptions,
96-
) {
100+
constructor(private _clientInfo: Implementation, options?: ClientOptions) {
97101
super(options);
98102
this._capabilities = options?.capabilities ?? {};
103+
104+
// Set up notification handlers
105+
this.setNotificationHandler(
106+
"notifications/tools/list_changed",
107+
async () => {
108+
// Automatically refresh the tools list when the server indicates a change
109+
try {
110+
// Only refresh if the server supports tools
111+
if (this._serverCapabilities?.tools) {
112+
const result = await this.listTools();
113+
// Call the user's callback with the updated tools list
114+
this.onToolListChanged?.(result.tools);
115+
}
116+
} catch (error) {
117+
console.error("Failed to refresh tools list:", error);
118+
// Still call the callback even if refresh failed
119+
this.onToolListChanged?.(undefined);
120+
}
121+
}
122+
);
99123
}
100124

101125
/**
@@ -106,7 +130,7 @@ export class Client<
106130
public registerCapabilities(capabilities: ClientCapabilities): void {
107131
if (this.transport) {
108132
throw new Error(
109-
"Cannot register capabilities after connecting to transport",
133+
"Cannot register capabilities after connecting to transport"
110134
);
111135
}
112136

@@ -115,11 +139,11 @@ export class Client<
115139

116140
protected assertCapability(
117141
capability: keyof ServerCapabilities,
118-
method: string,
142+
method: string
119143
): void {
120144
if (!this._serverCapabilities?.[capability]) {
121145
throw new Error(
122-
`Server does not support ${capability} (required for ${method})`,
146+
`Server does not support ${String(capability)} (required for ${method})`
123147
);
124148
}
125149
}
@@ -137,7 +161,7 @@ export class Client<
137161
clientInfo: this._clientInfo,
138162
},
139163
},
140-
InitializeResultSchema,
164+
InitializeResultSchema
141165
);
142166

143167
if (result === undefined) {
@@ -146,7 +170,7 @@ export class Client<
146170

147171
if (!SUPPORTED_PROTOCOL_VERSIONS.includes(result.protocolVersion)) {
148172
throw new Error(
149-
`Server's protocol version is not supported: ${result.protocolVersion}`,
173+
`Server's protocol version is not supported: ${result.protocolVersion}`
150174
);
151175
}
152176

@@ -191,7 +215,7 @@ export class Client<
191215
case "logging/setLevel":
192216
if (!this._serverCapabilities?.logging) {
193217
throw new Error(
194-
`Server does not support logging (required for ${method})`,
218+
`Server does not support logging (required for ${method})`
195219
);
196220
}
197221
break;
@@ -200,7 +224,7 @@ export class Client<
200224
case "prompts/list":
201225
if (!this._serverCapabilities?.prompts) {
202226
throw new Error(
203-
`Server does not support prompts (required for ${method})`,
227+
`Server does not support prompts (required for ${method})`
204228
);
205229
}
206230
break;
@@ -212,7 +236,7 @@ export class Client<
212236
case "resources/unsubscribe":
213237
if (!this._serverCapabilities?.resources) {
214238
throw new Error(
215-
`Server does not support resources (required for ${method})`,
239+
`Server does not support resources (required for ${method})`
216240
);
217241
}
218242

@@ -221,7 +245,7 @@ export class Client<
221245
!this._serverCapabilities.resources.subscribe
222246
) {
223247
throw new Error(
224-
`Server does not support resource subscriptions (required for ${method})`,
248+
`Server does not support resource subscriptions (required for ${method})`
225249
);
226250
}
227251

@@ -231,15 +255,15 @@ export class Client<
231255
case "tools/list":
232256
if (!this._serverCapabilities?.tools) {
233257
throw new Error(
234-
`Server does not support tools (required for ${method})`,
258+
`Server does not support tools (required for ${method})`
235259
);
236260
}
237261
break;
238262

239263
case "completion/complete":
240264
if (!this._serverCapabilities?.prompts) {
241265
throw new Error(
242-
`Server does not support prompts (required for ${method})`,
266+
`Server does not support prompts (required for ${method})`
243267
);
244268
}
245269
break;
@@ -255,13 +279,23 @@ export class Client<
255279
}
256280

257281
protected assertNotificationCapability(
258-
method: NotificationT["method"],
282+
method: NotificationT["method"]
259283
): void {
260284
switch (method as ClientNotification["method"]) {
261285
case "notifications/roots/list_changed":
262286
if (!this._capabilities.roots?.listChanged) {
263287
throw new Error(
264-
`Client does not support roots list changed notifications (required for ${method})`,
288+
`Client does not support roots list changed notifications (required for ${method})`
289+
);
290+
}
291+
break;
292+
293+
case "notifications/tools/list_changed":
294+
if (!this._capabilities.tools?.listChanged) {
295+
throw new Error(
296+
`Client does not support tools capability (required for ${String(
297+
method
298+
)})`
265299
);
266300
}
267301
break;
@@ -285,15 +319,15 @@ export class Client<
285319
case "sampling/createMessage":
286320
if (!this._capabilities.sampling) {
287321
throw new Error(
288-
`Client does not support sampling capability (required for ${method})`,
322+
`Client does not support sampling capability (required for ${method})`
289323
);
290324
}
291325
break;
292326

293327
case "roots/list":
294328
if (!this._capabilities.roots) {
295329
throw new Error(
296-
`Client does not support roots capability (required for ${method})`,
330+
`Client does not support roots capability (required for ${method})`
297331
);
298332
}
299333
break;
@@ -312,92 +346,92 @@ export class Client<
312346
return this.request(
313347
{ method: "completion/complete", params },
314348
CompleteResultSchema,
315-
options,
349+
options
316350
);
317351
}
318352

319353
async setLoggingLevel(level: LoggingLevel, options?: RequestOptions) {
320354
return this.request(
321355
{ method: "logging/setLevel", params: { level } },
322356
EmptyResultSchema,
323-
options,
357+
options
324358
);
325359
}
326360

327361
async getPrompt(
328362
params: GetPromptRequest["params"],
329-
options?: RequestOptions,
363+
options?: RequestOptions
330364
) {
331365
return this.request(
332366
{ method: "prompts/get", params },
333367
GetPromptResultSchema,
334-
options,
368+
options
335369
);
336370
}
337371

338372
async listPrompts(
339373
params?: ListPromptsRequest["params"],
340-
options?: RequestOptions,
374+
options?: RequestOptions
341375
) {
342376
return this.request(
343377
{ method: "prompts/list", params },
344378
ListPromptsResultSchema,
345-
options,
379+
options
346380
);
347381
}
348382

349383
async listResources(
350384
params?: ListResourcesRequest["params"],
351-
options?: RequestOptions,
385+
options?: RequestOptions
352386
) {
353387
return this.request(
354388
{ method: "resources/list", params },
355389
ListResourcesResultSchema,
356-
options,
390+
options
357391
);
358392
}
359393

360394
async listResourceTemplates(
361395
params?: ListResourceTemplatesRequest["params"],
362-
options?: RequestOptions,
396+
options?: RequestOptions
363397
) {
364398
return this.request(
365399
{ method: "resources/templates/list", params },
366400
ListResourceTemplatesResultSchema,
367-
options,
401+
options
368402
);
369403
}
370404

371405
async readResource(
372406
params: ReadResourceRequest["params"],
373-
options?: RequestOptions,
407+
options?: RequestOptions
374408
) {
375409
return this.request(
376410
{ method: "resources/read", params },
377411
ReadResourceResultSchema,
378-
options,
412+
options
379413
);
380414
}
381415

382416
async subscribeResource(
383417
params: SubscribeRequest["params"],
384-
options?: RequestOptions,
418+
options?: RequestOptions
385419
) {
386420
return this.request(
387421
{ method: "resources/subscribe", params },
388422
EmptyResultSchema,
389-
options,
423+
options
390424
);
391425
}
392426

393427
async unsubscribeResource(
394428
params: UnsubscribeRequest["params"],
395-
options?: RequestOptions,
429+
options?: RequestOptions
396430
) {
397431
return this.request(
398432
{ method: "resources/unsubscribe", params },
399433
EmptyResultSchema,
400-
options,
434+
options
401435
);
402436
}
403437

@@ -406,27 +440,43 @@ export class Client<
406440
resultSchema:
407441
| typeof CallToolResultSchema
408442
| typeof CompatibilityCallToolResultSchema = CallToolResultSchema,
409-
options?: RequestOptions,
443+
options?: RequestOptions
410444
) {
411445
return this.request(
412446
{ method: "tools/call", params },
413447
resultSchema,
414-
options,
448+
options
415449
);
416450
}
417451

418452
async listTools(
419453
params?: ListToolsRequest["params"],
420-
options?: RequestOptions,
454+
options?: RequestOptions
421455
) {
422456
return this.request(
423457
{ method: "tools/list", params },
424458
ListToolsResultSchema,
425-
options,
459+
options
426460
);
427461
}
428462

463+
/**
464+
* Registers a callback to be called when the server indicates that
465+
* the tools list has changed. The callback should typically refresh the tools list.
466+
*
467+
* @param callback Function to call when tools list changes
468+
*/
469+
setToolListChangedCallback(
470+
callback: (tools?: ListToolsResult["tools"]) => void
471+
): void {
472+
this.onToolListChanged = callback;
473+
}
474+
429475
async sendRootsListChanged() {
430476
return this.notification({ method: "notifications/roots/list_changed" });
431477
}
478+
479+
async sendToolListChanged() {
480+
return this.notification({ method: "notifications/tools/list_changed" });
481+
}
432482
}

0 commit comments

Comments
 (0)