import { useState, useEffect, useCallback } from "react"; import type { InspectorClientProtocol } from "../mcp/inspectorClientProtocol.js"; import type { RequestMetadata } from "../mcp/types.js"; import type { PagedResourcesState, PagedResourcesStateEventMap, LoadPageResult, } from "../mcp/state/pagedResourcesState.js"; import type { Resource } from "@modelcontextprotocol/client"; import type { TypedEventGeneric } from "../mcp/typedEventTarget.js"; import { useListError } from "./useListError.js"; export interface UsePagedResourcesResult { resources: Resource[]; /** The server's `nextCursor` from the last page (undefined = at the end). */ nextCursor?: string; /** Pages loaded since the last reset (page 1 = 1). */ pageCount: number; /** * The last page load's failure, or `null` when it succeeded. In paginated * mode this store is the display source, so this — not the managed * store's error — is what the panel renders (#1998). */ error: Error | null; loadPage: ( cursor?: string, metadata?: RequestMetadata, ) => Promise; clear: () => void; } /** * React hook that subscribes to PagedResourcesState and returns resources + * pagination progress + loadPage. The state store owns loading; this mirrors * its observable state. */ export function usePagedResources( client: InspectorClientProtocol | null, pagedResourcesState: PagedResourcesState | null, ): UsePagedResourcesResult { const [resources, setResources] = useState( pagedResourcesState?.getResources() ?? [], ); const [nextCursor, setNextCursor] = useState( pagedResourcesState?.getPagination().nextCursor, ); const [pageCount, setPageCount] = useState( pagedResourcesState?.getPagination().pageCount ?? 0, ); useEffect(() => { if (!pagedResourcesState) { setResources([]); setNextCursor(undefined); setPageCount(0); return; } setResources(pagedResourcesState.getResources()); setNextCursor(pagedResourcesState.getPagination().nextCursor); setPageCount(pagedResourcesState.getPagination().pageCount); const onResourcesChange = ( event: TypedEventGeneric, ) => { setResources(event.detail); }; const onPaginationChange = ( event: TypedEventGeneric, ) => { setNextCursor(event.detail.nextCursor); setPageCount(event.detail.pageCount); }; pagedResourcesState.addEventListener("resourcesChange", onResourcesChange); pagedResourcesState.addEventListener( "paginationChange", onPaginationChange, ); return () => { pagedResourcesState.removeEventListener( "resourcesChange", onResourcesChange, ); pagedResourcesState.removeEventListener( "paginationChange", onPaginationChange, ); }; }, [pagedResourcesState]); const error = useListError(pagedResourcesState); const loadPage = useCallback( async ( cursor?: string, metadata?: RequestMetadata, ): Promise => { if (!pagedResourcesState || !client) { return { resources: [], nextCursor: undefined }; } return pagedResourcesState.loadPage(cursor, metadata); }, [client, pagedResourcesState], ); const clear = useCallback(() => { pagedResourcesState?.clear(); }, [pagedResourcesState]); return { resources, nextCursor, pageCount, error, loadPage, clear }; }