import { useState, useEffect, useCallback } from "react"; import type { InspectorClientProtocol } from "../mcp/inspectorClientProtocol.js"; import type { ManagedToolsState } from "../mcp/state/managedToolsState.js"; import type { ManagedToolsStateEventMap } from "../mcp/state/managedToolsState.js"; import type { Tool } from "@modelcontextprotocol/client"; import type { TypedEventGeneric } from "../mcp/typedEventTarget.js"; import { useListError } from "./useListError.js"; export interface UseManagedToolsResult { /** * The last fetch's failure (transport error, or a result the SDK codec * rejected), or `null` when it succeeded. Includes the connect-time load, * whose failure has no caller to surface it (#1953). */ error: Error | null; tools: Tool[]; /** True when a `tools/list_changed` arrived since the last user refresh. */ listChanged: boolean; refresh: () => Promise; /** * Acknowledge the list-changed indicator without fetching the aggregate. * Used by the paginated Refresh path, which reloads page 1 of the paged * store (bypassing this hook's `refresh`) but must still clear the indicator * the managed state lit on `list_changed` (#1721). */ clearListChanged: () => void; } /** * React hook that subscribes to ManagedToolsState and returns tools + refresh. */ export function useManagedTools( client: InspectorClientProtocol | null, managedToolsState: ManagedToolsState | null, ): UseManagedToolsResult { const [tools, setTools] = useState( managedToolsState?.getTools() ?? [], ); const [listChanged, setListChanged] = useState( managedToolsState?.getListChanged() ?? false, ); useEffect(() => { if (!managedToolsState) { setTools([]); setListChanged(false); return; } setTools(managedToolsState.getTools()); setListChanged(managedToolsState.getListChanged()); const onToolsChange = ( event: TypedEventGeneric, ) => { setTools(event.detail); }; const onListChangedChange = ( event: TypedEventGeneric, ) => { setListChanged(event.detail); }; managedToolsState.addEventListener("toolsChange", onToolsChange); managedToolsState.addEventListener( "listChangedChange", onListChangedChange, ); return () => { managedToolsState.removeEventListener("toolsChange", onToolsChange); managedToolsState.removeEventListener( "listChangedChange", onListChangedChange, ); }; }, [managedToolsState]); const error = useListError(managedToolsState); const refresh = useCallback(async (): Promise => { if (!managedToolsState || !client) return []; // A user-initiated refresh acknowledges the change — clear the indicator // BEFORE awaiting the fetch, not after. If a `tools/list_changed` arrives // mid-fetch, the state re-sets the flag (and auto-refreshes); clearing // afterward would wipe that genuinely-new signal and the user would miss // it. Clearing up front acknowledges only the change in hand. managedToolsState.clearListChanged(); // A user-initiated refresh forces a cache-bypassing round trip // (`cacheMode: "refresh"`) so a modern server's `ttlMs`-cached list can't // return stale — and re-stores the fresh aggregate. const next = await managedToolsState.refresh(undefined, "refresh"); setTools(next); return next; }, [client, managedToolsState]); const clearListChanged = useCallback(() => { managedToolsState?.clearListChanged(); }, [managedToolsState]); return { tools, error, listChanged, refresh, clearListChanged }; }