Skip to content
186 changes: 184 additions & 2 deletions src/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
ListResourceTemplatesRequest,
ListResourceTemplatesResultSchema,
ListToolsRequest,
ListToolsResult,
ListToolsResultSchema,
LoggingLevel,
Notification,
Expand All @@ -46,6 +47,27 @@
* Capabilities to advertise as being supported by this client.
*/
capabilities?: ClientCapabilities;
/**
* Configure automatic refresh behavior for tool list changes
*/
toolRefreshOptions?: {
/**
* Whether to automatically refresh the tools list when a change notification is received.
* Default: true
*/
autoRefresh?: boolean;
/**
* Debounce time in milliseconds for tool list refresh operations.
* Multiple notifications received within this timeframe will only trigger one refresh.
* Default: 300
*/
debounceMs?: number;
/**
* Optional callback for handling tool list refresh errors.
* When provided, this will be called instead of logging to console.
*/
onError?: (error: Error) => void;
};
};

/**
Expand Down Expand Up @@ -86,6 +108,18 @@
private _serverVersion?: Implementation;
private _capabilities: ClientCapabilities;
private _instructions?: string;
private _toolRefreshOptions: {
autoRefresh: boolean;
debounceMs: number;
onError?: (error: Error) => void;
};
private _toolRefreshDebounceTimer?: ReturnType<typeof setTimeout>;

/**
* Callback for when the server indicates that the tools list has changed.
* Client should typically refresh its list of tools in response.
*/
onToolListChanged?: (tools?: ListToolsResult["tools"]) => void;

/**
* Initializes this client with the given name and version information.
Expand All @@ -96,6 +130,64 @@
) {
super(options);
this._capabilities = options?.capabilities ?? {};
this._toolRefreshOptions = {
autoRefresh: options?.toolRefreshOptions?.autoRefresh ?? true,
debounceMs: options?.toolRefreshOptions?.debounceMs ?? 500,
onError: options?.toolRefreshOptions?.onError,
};

// Set up notification handlers
this.setNotificationHandler(
"notifications/tools/list_changed",

Check failure on line 141 in src/client/index.ts

View workflow job for this annotation

GitHub Actions / build

Argument of type 'string' is not assignable to parameter of type 'ZodObject<{ method: ZodLiteral<string>; }, UnknownKeysParam, ZodTypeAny, { method: string; }, { method: string; }>'.
async () => {
// Only proceed with refresh if auto-refresh is enabled
if (!this._toolRefreshOptions.autoRefresh) {
// Still call callback to notify about the change, but without tools data
this.onToolListChanged?.(undefined);
return;
}

// Clear any pending refresh timer
if (this._toolRefreshDebounceTimer) {
clearTimeout(this._toolRefreshDebounceTimer);
}

// Set up debounced refresh
this._toolRefreshDebounceTimer = setTimeout(() => {
this._refreshToolsList().catch((error) => {
// Use error callback if provided, otherwise log to console
if (this._toolRefreshOptions.onError) {
this._toolRefreshOptions.onError(error instanceof Error ? error : new Error(String(error)));
} else {
console.error("Failed to refresh tools list:", error);
}
});
}, this._toolRefreshOptions.debounceMs);
}
);
}

/**
* Private method to handle tools list refresh
*/
private async _refreshToolsList(): Promise<void> {
try {
// Only refresh if the server supports tools
if (this._serverCapabilities?.tools) {
const result = await this.listTools();
// Call the user's callback with the updated tools list
this.onToolListChanged?.(result.tools);
}
} catch (error) {
// Use error callback if provided, otherwise log to console
if (this._toolRefreshOptions.onError) {
this._toolRefreshOptions.onError(error instanceof Error ? error : new Error(String(error)));
} else {
console.error("Failed to refresh tools list:", error);
}
// Still call the callback even if refresh failed
this.onToolListChanged?.(undefined);
}
}

/**
Expand All @@ -113,13 +205,48 @@
this._capabilities = mergeCapabilities(this._capabilities, capabilities);
}

/**
* Updates the tool refresh options
*/
public setToolRefreshOptions(
options: ClientOptions["toolRefreshOptions"]
): void {
if (options) {
if (options.autoRefresh !== undefined) {
this._toolRefreshOptions.autoRefresh = options.autoRefresh;
}
if (options.debounceMs !== undefined) {
this._toolRefreshOptions.debounceMs = options.debounceMs;
}
if (options.onError !== undefined) {
this._toolRefreshOptions.onError = options.onError;
}
}
}

/**
* Gets the current tool refresh options
*/
public getToolRefreshOptions(): typeof this._toolRefreshOptions {
return { ...this._toolRefreshOptions };
}

/**
* Sets an error handler for tool list refresh errors
*
* @param handler Function to call when a tool list refresh error occurs
*/
public setToolRefreshErrorHandler(handler: (error: Error) => void): void {
this._toolRefreshOptions.onError = handler;
}

protected assertCapability(
capability: keyof ServerCapabilities,
method: string,
): void {
if (!this._serverCapabilities?.[capability]) {
throw new Error(
`Server does not support ${capability} (required for ${method})`,
`Server does not support ${String(capability)} (required for ${method})`,
);
}
}
Expand Down Expand Up @@ -266,7 +393,17 @@
case "notifications/roots/list_changed":
if (!this._capabilities.roots?.listChanged) {
throw new Error(
`Client does not support roots list changed notifications (required for ${method})`,
`Client does not support roots list changed notifications (required for ${method})`
);
}
break;

case "notifications/tools/list_changed":

Check failure on line 401 in src/client/index.ts

View workflow job for this annotation

GitHub Actions / build

Type '"notifications/tools/list_changed"' is not comparable to type '"notifications/cancelled" | "notifications/initialized" | "notifications/progress" | "notifications/roots/list_changed"'.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same, client will not send notifications/tools/list_changed

if (!this._capabilities.tools?.listChanged) {

Check failure on line 402 in src/client/index.ts

View workflow job for this annotation

GitHub Actions / build

Property 'listChanged' does not exist on type '{}'.
throw new Error(
`Client does not support tools capability (required for ${String(
method
)})`
);
}
break;
Expand Down Expand Up @@ -420,6 +557,35 @@
);
}

/**
* Retrieves the list of available tools from the server.
*
* This method is called automatically when a tools list changed notification
* is received (if auto-refresh is enabled and after debouncing).
*
* To manually refresh the tools list:
* ```typescript
* try {
* const result = await client.listTools();
* // Use result.tools
* } catch (error) {
* // Handle error
* }
* ```
*
* Alternatively, register an error handler:
* ```typescript
* client.setToolRefreshErrorHandler((error) => {
* // Handle error
* });
*
* const result = await client.listTools();
* ```
*
* @param params Optional parameters for the list tools request
* @param options Optional request options
* @returns The list tools result containing available tools
*/
async listTools(
params?: ListToolsRequest["params"],
options?: RequestOptions,
Expand All @@ -431,7 +597,23 @@
);
}

/**
* Registers a callback to be called when the server indicates that
* the tools list has changed. The callback should typically refresh the tools list.
*
* @param callback Function to call when tools list changes
*/
setToolListChangedCallback(
callback: (tools?: ListToolsResult["tools"]) => void
): void {
this.onToolListChanged = callback;
}

async sendRootsListChanged() {
return this.notification({ method: "notifications/roots/list_changed" });
}

async sendToolListChanged() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this doesn't look right, client should receive the "notifications/tools/list_changed" notification, not send it

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@johnjjung did you have a chance to check this?

return this.notification({ method: "notifications/tools/list_changed" });

Check failure on line 617 in src/client/index.ts

View workflow job for this annotation

GitHub Actions / build

Object literal may only specify known properties, and 'method' does not exist in type 'never'.
}
}
Loading