import { useState, useEffect, useCallback } from "react"; import type { InspectorClientProtocol } from "../mcp/inspectorClientProtocol.js"; import type { ManagedPromptsState, ManagedPromptsStateEventMap, } from "../mcp/state/managedPromptsState.js"; import type { Prompt } from "@modelcontextprotocol/client"; import type { TypedEventGeneric } from "../mcp/typedEventTarget.js"; import { useManagedListError } from "./useManagedListError.js"; export interface UseManagedPromptsResult { /** * 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; prompts: Prompt[]; /** True when a `prompts/list_changed` arrived since the last user refresh. */ listChanged: boolean; refresh: () => Promise; /** Acknowledge the list-changed indicator without fetching (#1721). */ clearListChanged: () => void; } /** * React hook that subscribes to ManagedPromptsState and returns prompts + refresh. */ export function useManagedPrompts( client: InspectorClientProtocol | null, managedPromptsState: ManagedPromptsState | null, ): UseManagedPromptsResult { const [prompts, setPrompts] = useState( managedPromptsState?.getPrompts() ?? [], ); const [listChanged, setListChanged] = useState( managedPromptsState?.getListChanged() ?? false, ); useEffect(() => { if (!managedPromptsState) { setPrompts([]); setListChanged(false); return; } setPrompts(managedPromptsState.getPrompts()); setListChanged(managedPromptsState.getListChanged()); const onPromptsChange = ( event: TypedEventGeneric, ) => { setPrompts(event.detail); }; const onListChangedChange = ( event: TypedEventGeneric< ManagedPromptsStateEventMap, "listChangedChange" >, ) => { setListChanged(event.detail); }; managedPromptsState.addEventListener("promptsChange", onPromptsChange); managedPromptsState.addEventListener( "listChangedChange", onListChangedChange, ); return () => { managedPromptsState.removeEventListener("promptsChange", onPromptsChange); managedPromptsState.removeEventListener( "listChangedChange", onListChangedChange, ); }; }, [managedPromptsState]); const error = useManagedListError(managedPromptsState); const refresh = useCallback(async (): Promise => { if (!managedPromptsState || !client) return []; // A user-initiated refresh acknowledges the change — clear the indicator // BEFORE awaiting the fetch, not after. If a `prompts/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. managedPromptsState.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 managedPromptsState.refresh(undefined, "refresh"); setPrompts(next); return next; }, [client, managedPromptsState]); const clearListChanged = useCallback(() => { managedPromptsState?.clearListChanged(); }, [managedPromptsState]); return { prompts, error, listChanged, refresh, clearListChanged }; }