* feat(core): port v1.5 hook layer (useInspectorClient + useManaged*/usePaged* + log hooks) Wholesale port of v1.5's React hook layer and the EventTarget-based state machinery the hooks subscribe to (#1243). Hooks decouple from the concrete InspectorClient via a new `InspectorClientProtocol` interface so this layer can land before the runtime class is ported. Added under `core/`: - `core/react/` — useInspectorClient + 5 useManaged* + 5 usePaged* + 3 log hooks - `core/mcp/state/` — 5 managed*State + 5 paged*State + messageLogState + stderrLogState + fetchRequestLogState - `core/mcp/inspectorClientProtocol.ts`, `inspectorClientEventTarget.ts`, `typedEventTarget.ts`, `samplingCreateMessage.ts`, `sessionStorage.ts` - v2-only wrapper types in `core/mcp/types.ts`: InspectorUrlElicitRequest, InspectorPendingRequest, InspectorRequestHistoryItem Tests live under `clients/web/src/test/core/` (28 new test files, 264 new tests). Test fake: `core/mcp/__tests__/fakeInspectorClient.ts`. Build wiring: - `vite.config.ts`: dedupe react/react-dom so core/ files resolve them via clients/web/node_modules. - `tsconfig.app.json` / `tsconfig.test.json`: paths map react/react-dom/vitest to clients/web/node_modules so `tsc -b` resolves bare imports from core/ source files (which sit above the package's node_modules). Exclude core/__tests__ from app config; include in test config. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * review: address PR #1300 review findings Bugs: - fetchRequestLogState: add .catch() on sessionStorage.loadSession so a corrupt or unreadable session doesn't surface as an unhandled rejection (matches the saveSession swallow pattern). - messageLogState: sync pendingRequestEntries when clearMessages() evicts a pending request entry, so a later response with the same id falls through to the append branch instead of mutating an unreachable map entry. Add tests for both predicate and no-predicate paths. Cleanups: - Extract mergeTaskIntoList helper to core/mcp/state/mergeTaskIntoList.ts so managed/paged RequestorTasks state classes share one definition. - Add a one-line JSDoc note on each PagedTools/Prompts/Resources/Templates state explaining why they deliberately do not subscribe to *ListChanged (cursor invalidation mid-pagination). - Document on useInspectorClient that appRendererClient is read lazily on every render and is not subscribed (changes once at connect time). Carry-overs from v1.5 deliberately left for the follow-up InspectorClient runtime port: - Refresh/status-change race in Managed*State.refresh() (status check is only at entry; a disconnect during await can re-clear before refresh resumes). - nextCursor exposure asymmetry across usePaged* (only usePagedRequestorTasks surfaces it via state field; others return it from loadPage() only). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a4215e57b9
commit
e3e70470b5
14 changed files with 6404 additions and 5 deletions
51
core/react/useFetchRequestLog.ts
Normal file
51
core/react/useFetchRequestLog.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import { useState, useEffect } from "react";
|
||||
import type { FetchRequestEntry } from "../mcp/types.js";
|
||||
import type {
|
||||
FetchRequestLogState,
|
||||
FetchRequestLogStateEventMap,
|
||||
} from "../mcp/state/fetchRequestLogState.js";
|
||||
import type { TypedEventGeneric } from "../mcp/typedEventTarget.js";
|
||||
|
||||
export interface UseFetchRequestLogResult {
|
||||
fetchRequests: FetchRequestEntry[];
|
||||
}
|
||||
|
||||
/**
|
||||
* React hook that subscribes to FetchRequestLogState and returns the fetch
|
||||
* request list.
|
||||
*/
|
||||
export function useFetchRequestLog(
|
||||
fetchRequestLogState: FetchRequestLogState | null,
|
||||
): UseFetchRequestLogResult {
|
||||
const [fetchRequests, setFetchRequests] = useState<FetchRequestEntry[]>(
|
||||
fetchRequestLogState?.getFetchRequests() ?? [],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!fetchRequestLogState) {
|
||||
setFetchRequests([]);
|
||||
return;
|
||||
}
|
||||
setFetchRequests(fetchRequestLogState.getFetchRequests());
|
||||
const onFetchRequestsChange = (
|
||||
event: TypedEventGeneric<
|
||||
FetchRequestLogStateEventMap,
|
||||
"fetchRequestsChange"
|
||||
>,
|
||||
) => {
|
||||
setFetchRequests(event.detail);
|
||||
};
|
||||
fetchRequestLogState.addEventListener(
|
||||
"fetchRequestsChange",
|
||||
onFetchRequestsChange,
|
||||
);
|
||||
return () => {
|
||||
fetchRequestLogState.removeEventListener(
|
||||
"fetchRequestsChange",
|
||||
onFetchRequestsChange,
|
||||
);
|
||||
};
|
||||
}, [fetchRequestLogState]);
|
||||
|
||||
return { fetchRequests };
|
||||
}
|
||||
124
core/react/useInspectorClient.ts
Normal file
124
core/react/useInspectorClient.ts
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
import { useState, useEffect, useCallback } from "react";
|
||||
import type { InspectorClientProtocol } from "../mcp/inspectorClientProtocol.js";
|
||||
import type { AppRendererClient } from "../mcp/inspectorClientProtocol.js";
|
||||
import type { TypedEvent } from "../mcp/inspectorClientEventTarget.js";
|
||||
import type { ConnectionStatus } from "../mcp/types.js";
|
||||
import type {
|
||||
ServerCapabilities,
|
||||
Implementation,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
|
||||
export interface UseInspectorClientResult {
|
||||
status: ConnectionStatus;
|
||||
capabilities?: ServerCapabilities;
|
||||
serverInfo?: Implementation;
|
||||
instructions?: string;
|
||||
appRendererClient: AppRendererClient | null;
|
||||
connect: () => Promise<void>;
|
||||
disconnect: () => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* React hook that subscribes to InspectorClient events and provides reactive
|
||||
* connection state. Log lists (message / stderr / fetch) live in dedicated
|
||||
* state managers consumed via useMessageLog / useStderrLog / useFetchRequestLog.
|
||||
*
|
||||
* Note: `appRendererClient` is read lazily from the client on every render
|
||||
* and is NOT subscribed. It changes once at connect time and is not expected
|
||||
* to change again during a session, so callers will see the current value
|
||||
* on any rerender triggered by status / capabilities / serverInfo / instructions.
|
||||
* If a future use case requires autonomous updates when the renderer attaches,
|
||||
* add an `appRendererClientChange` event to `InspectorClientEventMap` and
|
||||
* subscribe here.
|
||||
*/
|
||||
export function useInspectorClient(
|
||||
inspectorClient: InspectorClientProtocol | null,
|
||||
): UseInspectorClientResult {
|
||||
const [status, setStatus] = useState<ConnectionStatus>(
|
||||
inspectorClient?.getStatus() ?? "disconnected",
|
||||
);
|
||||
const [capabilities, setCapabilities] = useState<
|
||||
ServerCapabilities | undefined
|
||||
>(inspectorClient?.getCapabilities());
|
||||
const [serverInfo, setServerInfo] = useState<Implementation | undefined>(
|
||||
inspectorClient?.getServerInfo(),
|
||||
);
|
||||
const [instructions, setInstructions] = useState<string | undefined>(
|
||||
inspectorClient?.getInstructions(),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!inspectorClient) {
|
||||
setStatus("disconnected");
|
||||
setCapabilities(undefined);
|
||||
setServerInfo(undefined);
|
||||
setInstructions(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
setStatus(inspectorClient.getStatus());
|
||||
setCapabilities(inspectorClient.getCapabilities());
|
||||
setServerInfo(inspectorClient.getServerInfo());
|
||||
setInstructions(inspectorClient.getInstructions());
|
||||
|
||||
const onStatusChange = (event: TypedEvent<"statusChange">) => {
|
||||
setStatus(event.detail);
|
||||
};
|
||||
const onCapabilitiesChange = (event: TypedEvent<"capabilitiesChange">) => {
|
||||
setCapabilities(event.detail);
|
||||
};
|
||||
const onServerInfoChange = (event: TypedEvent<"serverInfoChange">) => {
|
||||
setServerInfo(event.detail);
|
||||
};
|
||||
const onInstructionsChange = (event: TypedEvent<"instructionsChange">) => {
|
||||
setInstructions(event.detail);
|
||||
};
|
||||
|
||||
inspectorClient.addEventListener("statusChange", onStatusChange);
|
||||
inspectorClient.addEventListener(
|
||||
"capabilitiesChange",
|
||||
onCapabilitiesChange,
|
||||
);
|
||||
inspectorClient.addEventListener("serverInfoChange", onServerInfoChange);
|
||||
inspectorClient.addEventListener(
|
||||
"instructionsChange",
|
||||
onInstructionsChange,
|
||||
);
|
||||
|
||||
return () => {
|
||||
inspectorClient.removeEventListener("statusChange", onStatusChange);
|
||||
inspectorClient.removeEventListener(
|
||||
"capabilitiesChange",
|
||||
onCapabilitiesChange,
|
||||
);
|
||||
inspectorClient.removeEventListener(
|
||||
"serverInfoChange",
|
||||
onServerInfoChange,
|
||||
);
|
||||
inspectorClient.removeEventListener(
|
||||
"instructionsChange",
|
||||
onInstructionsChange,
|
||||
);
|
||||
};
|
||||
}, [inspectorClient]);
|
||||
|
||||
const connect = useCallback(async () => {
|
||||
if (!inspectorClient) return;
|
||||
await inspectorClient.connect();
|
||||
}, [inspectorClient]);
|
||||
|
||||
const disconnect = useCallback(async () => {
|
||||
if (!inspectorClient) return;
|
||||
await inspectorClient.disconnect();
|
||||
}, [inspectorClient]);
|
||||
|
||||
return {
|
||||
status,
|
||||
capabilities,
|
||||
serverInfo,
|
||||
instructions,
|
||||
appRendererClient: inspectorClient?.getAppRendererClient() ?? null,
|
||||
connect,
|
||||
disconnect,
|
||||
};
|
||||
}
|
||||
54
core/react/useManagedPrompts.ts
Normal file
54
core/react/useManagedPrompts.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
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/sdk/types.js";
|
||||
import type { TypedEventGeneric } from "../mcp/typedEventTarget.js";
|
||||
|
||||
export interface UseManagedPromptsResult {
|
||||
prompts: Prompt[];
|
||||
refresh: () => Promise<Prompt[]>;
|
||||
}
|
||||
|
||||
/**
|
||||
* React hook that subscribes to ManagedPromptsState and returns prompts + refresh.
|
||||
*/
|
||||
export function useManagedPrompts(
|
||||
client: InspectorClientProtocol | null,
|
||||
managedPromptsState: ManagedPromptsState | null,
|
||||
): UseManagedPromptsResult {
|
||||
const [prompts, setPrompts] = useState<Prompt[]>(
|
||||
managedPromptsState?.getPrompts() ?? [],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!managedPromptsState) {
|
||||
setPrompts([]);
|
||||
return;
|
||||
}
|
||||
setPrompts(managedPromptsState.getPrompts());
|
||||
const onPromptsChange = (
|
||||
event: TypedEventGeneric<ManagedPromptsStateEventMap, "promptsChange">,
|
||||
) => {
|
||||
setPrompts(event.detail);
|
||||
};
|
||||
managedPromptsState.addEventListener("promptsChange", onPromptsChange);
|
||||
return () => {
|
||||
managedPromptsState.removeEventListener(
|
||||
"promptsChange",
|
||||
onPromptsChange,
|
||||
);
|
||||
};
|
||||
}, [managedPromptsState]);
|
||||
|
||||
const refresh = useCallback(async (): Promise<Prompt[]> => {
|
||||
if (!managedPromptsState || !client) return [];
|
||||
const next = await managedPromptsState.refresh();
|
||||
setPrompts(next);
|
||||
return next;
|
||||
}, [client, managedPromptsState]);
|
||||
|
||||
return { prompts, refresh };
|
||||
}
|
||||
58
core/react/useManagedRequestorTasks.ts
Normal file
58
core/react/useManagedRequestorTasks.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import { useState, useEffect, useCallback } from "react";
|
||||
import type { InspectorClientProtocol } from "../mcp/inspectorClientProtocol.js";
|
||||
import type {
|
||||
ManagedRequestorTasksState,
|
||||
ManagedRequestorTasksStateEventMap,
|
||||
} from "../mcp/state/managedRequestorTasksState.js";
|
||||
import type { Task } from "@modelcontextprotocol/sdk/types.js";
|
||||
import type { TypedEventGeneric } from "../mcp/typedEventTarget.js";
|
||||
|
||||
export interface UseManagedRequestorTasksResult {
|
||||
tasks: Task[];
|
||||
refresh: () => Promise<Task[]>;
|
||||
}
|
||||
|
||||
/**
|
||||
* React hook that subscribes to ManagedRequestorTasksState and returns
|
||||
* requestor tasks + refresh.
|
||||
*/
|
||||
export function useManagedRequestorTasks(
|
||||
client: InspectorClientProtocol | null,
|
||||
managedRequestorTasksState: ManagedRequestorTasksState | null,
|
||||
): UseManagedRequestorTasksResult {
|
||||
const [tasks, setTasks] = useState<Task[]>(
|
||||
managedRequestorTasksState?.getTasks() ?? [],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!managedRequestorTasksState) {
|
||||
setTasks([]);
|
||||
return;
|
||||
}
|
||||
setTasks(managedRequestorTasksState.getTasks());
|
||||
const onTasksChange = (
|
||||
event: TypedEventGeneric<
|
||||
ManagedRequestorTasksStateEventMap,
|
||||
"tasksChange"
|
||||
>,
|
||||
) => {
|
||||
setTasks(event.detail);
|
||||
};
|
||||
managedRequestorTasksState.addEventListener("tasksChange", onTasksChange);
|
||||
return () => {
|
||||
managedRequestorTasksState.removeEventListener(
|
||||
"tasksChange",
|
||||
onTasksChange,
|
||||
);
|
||||
};
|
||||
}, [managedRequestorTasksState]);
|
||||
|
||||
const refresh = useCallback(async (): Promise<Task[]> => {
|
||||
if (!managedRequestorTasksState || !client) return [];
|
||||
const next = await managedRequestorTasksState.refresh();
|
||||
setTasks(next);
|
||||
return next;
|
||||
}, [client, managedRequestorTasksState]);
|
||||
|
||||
return { tasks, refresh };
|
||||
}
|
||||
61
core/react/useManagedResourceTemplates.ts
Normal file
61
core/react/useManagedResourceTemplates.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import { useState, useEffect, useCallback } from "react";
|
||||
import type { InspectorClientProtocol } from "../mcp/inspectorClientProtocol.js";
|
||||
import type {
|
||||
ManagedResourceTemplatesState,
|
||||
ManagedResourceTemplatesStateEventMap,
|
||||
} from "../mcp/state/managedResourceTemplatesState.js";
|
||||
import type { ResourceTemplate } from "@modelcontextprotocol/sdk/types.js";
|
||||
import type { TypedEventGeneric } from "../mcp/typedEventTarget.js";
|
||||
|
||||
export interface UseManagedResourceTemplatesResult {
|
||||
resourceTemplates: ResourceTemplate[];
|
||||
refresh: () => Promise<ResourceTemplate[]>;
|
||||
}
|
||||
|
||||
/**
|
||||
* React hook that subscribes to ManagedResourceTemplatesState and returns
|
||||
* resource templates + refresh.
|
||||
*/
|
||||
export function useManagedResourceTemplates(
|
||||
client: InspectorClientProtocol | null,
|
||||
managedResourceTemplatesState: ManagedResourceTemplatesState | null,
|
||||
): UseManagedResourceTemplatesResult {
|
||||
const [resourceTemplates, setResourceTemplates] = useState<
|
||||
ResourceTemplate[]
|
||||
>(managedResourceTemplatesState?.getResourceTemplates() ?? []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!managedResourceTemplatesState) {
|
||||
setResourceTemplates([]);
|
||||
return;
|
||||
}
|
||||
setResourceTemplates(managedResourceTemplatesState.getResourceTemplates());
|
||||
const onResourceTemplatesChange = (
|
||||
event: TypedEventGeneric<
|
||||
ManagedResourceTemplatesStateEventMap,
|
||||
"resourceTemplatesChange"
|
||||
>,
|
||||
) => {
|
||||
setResourceTemplates(event.detail);
|
||||
};
|
||||
managedResourceTemplatesState.addEventListener(
|
||||
"resourceTemplatesChange",
|
||||
onResourceTemplatesChange,
|
||||
);
|
||||
return () => {
|
||||
managedResourceTemplatesState.removeEventListener(
|
||||
"resourceTemplatesChange",
|
||||
onResourceTemplatesChange,
|
||||
);
|
||||
};
|
||||
}, [managedResourceTemplatesState]);
|
||||
|
||||
const refresh = useCallback(async (): Promise<ResourceTemplate[]> => {
|
||||
if (!managedResourceTemplatesState || !client) return [];
|
||||
const next = await managedResourceTemplatesState.refresh();
|
||||
setResourceTemplates(next);
|
||||
return next;
|
||||
}, [client, managedResourceTemplatesState]);
|
||||
|
||||
return { resourceTemplates, refresh };
|
||||
}
|
||||
60
core/react/useManagedResources.ts
Normal file
60
core/react/useManagedResources.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import { useState, useEffect, useCallback } from "react";
|
||||
import type { InspectorClientProtocol } from "../mcp/inspectorClientProtocol.js";
|
||||
import type {
|
||||
ManagedResourcesState,
|
||||
ManagedResourcesStateEventMap,
|
||||
} from "../mcp/state/managedResourcesState.js";
|
||||
import type { Resource } from "@modelcontextprotocol/sdk/types.js";
|
||||
import type { TypedEventGeneric } from "../mcp/typedEventTarget.js";
|
||||
|
||||
export interface UseManagedResourcesResult {
|
||||
resources: Resource[];
|
||||
refresh: () => Promise<Resource[]>;
|
||||
}
|
||||
|
||||
/**
|
||||
* React hook that subscribes to ManagedResourcesState and returns resources + refresh.
|
||||
*/
|
||||
export function useManagedResources(
|
||||
client: InspectorClientProtocol | null,
|
||||
managedResourcesState: ManagedResourcesState | null,
|
||||
): UseManagedResourcesResult {
|
||||
const [resources, setResources] = useState<Resource[]>(
|
||||
managedResourcesState?.getResources() ?? [],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!managedResourcesState) {
|
||||
setResources([]);
|
||||
return;
|
||||
}
|
||||
setResources(managedResourcesState.getResources());
|
||||
const onResourcesChange = (
|
||||
event: TypedEventGeneric<
|
||||
ManagedResourcesStateEventMap,
|
||||
"resourcesChange"
|
||||
>,
|
||||
) => {
|
||||
setResources(event.detail);
|
||||
};
|
||||
managedResourcesState.addEventListener(
|
||||
"resourcesChange",
|
||||
onResourcesChange,
|
||||
);
|
||||
return () => {
|
||||
managedResourcesState.removeEventListener(
|
||||
"resourcesChange",
|
||||
onResourcesChange,
|
||||
);
|
||||
};
|
||||
}, [managedResourcesState]);
|
||||
|
||||
const refresh = useCallback(async (): Promise<Resource[]> => {
|
||||
if (!managedResourcesState || !client) return [];
|
||||
const next = await managedResourcesState.refresh();
|
||||
setResources(next);
|
||||
return next;
|
||||
}, [client, managedResourcesState]);
|
||||
|
||||
return { resources, refresh };
|
||||
}
|
||||
49
core/react/useManagedTools.ts
Normal file
49
core/react/useManagedTools.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
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/sdk/types.js";
|
||||
import type { TypedEventGeneric } from "../mcp/typedEventTarget.js";
|
||||
|
||||
export interface UseManagedToolsResult {
|
||||
tools: Tool[];
|
||||
refresh: () => Promise<Tool[]>;
|
||||
}
|
||||
|
||||
/**
|
||||
* React hook that subscribes to ManagedToolsState and returns tools + refresh.
|
||||
*/
|
||||
export function useManagedTools(
|
||||
client: InspectorClientProtocol | null,
|
||||
managedToolsState: ManagedToolsState | null,
|
||||
): UseManagedToolsResult {
|
||||
const [tools, setTools] = useState<Tool[]>(
|
||||
managedToolsState?.getTools() ?? [],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!managedToolsState) {
|
||||
setTools([]);
|
||||
return;
|
||||
}
|
||||
setTools(managedToolsState.getTools());
|
||||
const onToolsChange = (
|
||||
event: TypedEventGeneric<ManagedToolsStateEventMap, "toolsChange">,
|
||||
) => {
|
||||
setTools(event.detail);
|
||||
};
|
||||
managedToolsState.addEventListener("toolsChange", onToolsChange);
|
||||
return () => {
|
||||
managedToolsState.removeEventListener("toolsChange", onToolsChange);
|
||||
};
|
||||
}, [managedToolsState]);
|
||||
|
||||
const refresh = useCallback(async (): Promise<Tool[]> => {
|
||||
if (!managedToolsState || !client) return [];
|
||||
const next = await managedToolsState.refresh();
|
||||
setTools(next);
|
||||
return next;
|
||||
}, [client, managedToolsState]);
|
||||
|
||||
return { tools, refresh };
|
||||
}
|
||||
41
core/react/useMessageLog.ts
Normal file
41
core/react/useMessageLog.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import { useState, useEffect } from "react";
|
||||
import type { MessageEntry } from "../mcp/types.js";
|
||||
import type {
|
||||
MessageLogState,
|
||||
MessageLogStateEventMap,
|
||||
} from "../mcp/state/messageLogState.js";
|
||||
import type { TypedEventGeneric } from "../mcp/typedEventTarget.js";
|
||||
|
||||
export interface UseMessageLogResult {
|
||||
messages: MessageEntry[];
|
||||
}
|
||||
|
||||
/**
|
||||
* React hook that subscribes to MessageLogState and returns the message list.
|
||||
*/
|
||||
export function useMessageLog(
|
||||
messageLogState: MessageLogState | null,
|
||||
): UseMessageLogResult {
|
||||
const [messages, setMessages] = useState<MessageEntry[]>(
|
||||
messageLogState?.getMessages() ?? [],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!messageLogState) {
|
||||
setMessages([]);
|
||||
return;
|
||||
}
|
||||
setMessages(messageLogState.getMessages());
|
||||
const onMessagesChange = (
|
||||
event: TypedEventGeneric<MessageLogStateEventMap, "messagesChange">,
|
||||
) => {
|
||||
setMessages(event.detail);
|
||||
};
|
||||
messageLogState.addEventListener("messagesChange", onMessagesChange);
|
||||
return () => {
|
||||
messageLogState.removeEventListener("messagesChange", onMessagesChange);
|
||||
};
|
||||
}, [messageLogState]);
|
||||
|
||||
return { messages };
|
||||
}
|
||||
68
core/react/usePagedPrompts.ts
Normal file
68
core/react/usePagedPrompts.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import { useState, useEffect, useCallback } from "react";
|
||||
import type { InspectorClientProtocol } from "../mcp/inspectorClientProtocol.js";
|
||||
import type {
|
||||
PagedPromptsState,
|
||||
PagedPromptsStateEventMap,
|
||||
LoadPageResult,
|
||||
} from "../mcp/state/pagedPromptsState.js";
|
||||
import type { Prompt } from "@modelcontextprotocol/sdk/types.js";
|
||||
import type { TypedEventGeneric } from "../mcp/typedEventTarget.js";
|
||||
|
||||
export interface UsePagedPromptsResult {
|
||||
prompts: Prompt[];
|
||||
loadPage: (
|
||||
cursor?: string,
|
||||
metadata?: Record<string, string>,
|
||||
) => Promise<LoadPageResult>;
|
||||
clear: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* React hook that subscribes to PagedPromptsState and returns prompts + loadPage.
|
||||
*/
|
||||
export function usePagedPrompts(
|
||||
client: InspectorClientProtocol | null,
|
||||
pagedPromptsState: PagedPromptsState | null,
|
||||
): UsePagedPromptsResult {
|
||||
const [prompts, setPrompts] = useState<Prompt[]>(
|
||||
pagedPromptsState?.getPrompts() ?? [],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!pagedPromptsState) {
|
||||
setPrompts([]);
|
||||
return;
|
||||
}
|
||||
setPrompts(pagedPromptsState.getPrompts());
|
||||
const onPromptsChange = (
|
||||
event: TypedEventGeneric<PagedPromptsStateEventMap, "promptsChange">,
|
||||
) => {
|
||||
setPrompts(event.detail);
|
||||
};
|
||||
pagedPromptsState.addEventListener("promptsChange", onPromptsChange);
|
||||
return () => {
|
||||
pagedPromptsState.removeEventListener("promptsChange", onPromptsChange);
|
||||
};
|
||||
}, [pagedPromptsState]);
|
||||
|
||||
const loadPage = useCallback(
|
||||
async (
|
||||
cursor?: string,
|
||||
metadata?: Record<string, string>,
|
||||
): Promise<LoadPageResult> => {
|
||||
if (!pagedPromptsState || !client) {
|
||||
return { prompts: [], nextCursor: undefined };
|
||||
}
|
||||
const result = await pagedPromptsState.loadPage(cursor, metadata);
|
||||
setPrompts(pagedPromptsState.getPrompts());
|
||||
return result;
|
||||
},
|
||||
[client, pagedPromptsState],
|
||||
);
|
||||
|
||||
const clear = useCallback(() => {
|
||||
pagedPromptsState?.clear();
|
||||
}, [pagedPromptsState]);
|
||||
|
||||
return { prompts, loadPage, clear };
|
||||
}
|
||||
77
core/react/usePagedRequestorTasks.ts
Normal file
77
core/react/usePagedRequestorTasks.ts
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import { useState, useEffect, useCallback } from "react";
|
||||
import type { InspectorClientProtocol } from "../mcp/inspectorClientProtocol.js";
|
||||
import type {
|
||||
PagedRequestorTasksState,
|
||||
PagedRequestorTasksStateEventMap,
|
||||
LoadPageResult,
|
||||
} from "../mcp/state/pagedRequestorTasksState.js";
|
||||
import type { Task } from "@modelcontextprotocol/sdk/types.js";
|
||||
import type { TypedEventGeneric } from "../mcp/typedEventTarget.js";
|
||||
|
||||
export interface UsePagedRequestorTasksResult {
|
||||
tasks: Task[];
|
||||
loadPage: (cursor?: string) => Promise<LoadPageResult>;
|
||||
clear: () => void;
|
||||
nextCursor: string | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* React hook that subscribes to PagedRequestorTasksState and returns tasks,
|
||||
* loadPage, clear, and nextCursor.
|
||||
*/
|
||||
export function usePagedRequestorTasks(
|
||||
client: InspectorClientProtocol | null,
|
||||
pagedRequestorTasksState: PagedRequestorTasksState | null,
|
||||
): UsePagedRequestorTasksResult {
|
||||
const [tasks, setTasks] = useState<Task[]>(
|
||||
pagedRequestorTasksState?.getTasks() ?? [],
|
||||
);
|
||||
const [nextCursor, setNextCursor] = useState<string | undefined>(
|
||||
pagedRequestorTasksState?.getNextCursor() ?? undefined,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!pagedRequestorTasksState) {
|
||||
setTasks([]);
|
||||
setNextCursor(undefined);
|
||||
return;
|
||||
}
|
||||
setTasks(pagedRequestorTasksState.getTasks());
|
||||
setNextCursor(pagedRequestorTasksState.getNextCursor() ?? undefined);
|
||||
const onTasksChange = (
|
||||
event: TypedEventGeneric<
|
||||
PagedRequestorTasksStateEventMap,
|
||||
"tasksChange"
|
||||
>,
|
||||
) => {
|
||||
setTasks(event.detail);
|
||||
setNextCursor(pagedRequestorTasksState.getNextCursor() ?? undefined);
|
||||
};
|
||||
pagedRequestorTasksState.addEventListener("tasksChange", onTasksChange);
|
||||
return () => {
|
||||
pagedRequestorTasksState.removeEventListener(
|
||||
"tasksChange",
|
||||
onTasksChange,
|
||||
);
|
||||
};
|
||||
}, [pagedRequestorTasksState]);
|
||||
|
||||
const loadPage = useCallback(
|
||||
async (cursor?: string): Promise<LoadPageResult> => {
|
||||
if (!pagedRequestorTasksState || !client) {
|
||||
return { tasks: [], nextCursor: undefined };
|
||||
}
|
||||
const result = await pagedRequestorTasksState.loadPage(cursor);
|
||||
setTasks(pagedRequestorTasksState.getTasks());
|
||||
setNextCursor(pagedRequestorTasksState.getNextCursor() ?? undefined);
|
||||
return result;
|
||||
},
|
||||
[client, pagedRequestorTasksState],
|
||||
);
|
||||
|
||||
const clear = useCallback(() => {
|
||||
pagedRequestorTasksState?.clear();
|
||||
}, [pagedRequestorTasksState]);
|
||||
|
||||
return { tasks, loadPage, clear, nextCursor };
|
||||
}
|
||||
81
core/react/usePagedResourceTemplates.ts
Normal file
81
core/react/usePagedResourceTemplates.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import { useState, useEffect, useCallback } from "react";
|
||||
import type { InspectorClientProtocol } from "../mcp/inspectorClientProtocol.js";
|
||||
import type {
|
||||
PagedResourceTemplatesState,
|
||||
PagedResourceTemplatesStateEventMap,
|
||||
LoadPageResult,
|
||||
} from "../mcp/state/pagedResourceTemplatesState.js";
|
||||
import type { ResourceTemplate } from "@modelcontextprotocol/sdk/types.js";
|
||||
import type { TypedEventGeneric } from "../mcp/typedEventTarget.js";
|
||||
|
||||
export interface UsePagedResourceTemplatesResult {
|
||||
resourceTemplates: ResourceTemplate[];
|
||||
loadPage: (
|
||||
cursor?: string,
|
||||
metadata?: Record<string, string>,
|
||||
) => Promise<LoadPageResult>;
|
||||
clear: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* React hook that subscribes to PagedResourceTemplatesState and returns
|
||||
* resource templates + loadPage.
|
||||
*/
|
||||
export function usePagedResourceTemplates(
|
||||
client: InspectorClientProtocol | null,
|
||||
pagedResourceTemplatesState: PagedResourceTemplatesState | null,
|
||||
): UsePagedResourceTemplatesResult {
|
||||
const [resourceTemplates, setResourceTemplates] = useState<
|
||||
ResourceTemplate[]
|
||||
>(pagedResourceTemplatesState?.getResourceTemplates() ?? []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!pagedResourceTemplatesState) {
|
||||
setResourceTemplates([]);
|
||||
return;
|
||||
}
|
||||
setResourceTemplates(pagedResourceTemplatesState.getResourceTemplates());
|
||||
const onResourceTemplatesChange = (
|
||||
event: TypedEventGeneric<
|
||||
PagedResourceTemplatesStateEventMap,
|
||||
"resourceTemplatesChange"
|
||||
>,
|
||||
) => {
|
||||
setResourceTemplates(event.detail);
|
||||
};
|
||||
pagedResourceTemplatesState.addEventListener(
|
||||
"resourceTemplatesChange",
|
||||
onResourceTemplatesChange,
|
||||
);
|
||||
return () => {
|
||||
pagedResourceTemplatesState.removeEventListener(
|
||||
"resourceTemplatesChange",
|
||||
onResourceTemplatesChange,
|
||||
);
|
||||
};
|
||||
}, [pagedResourceTemplatesState]);
|
||||
|
||||
const loadPage = useCallback(
|
||||
async (
|
||||
cursor?: string,
|
||||
metadata?: Record<string, string>,
|
||||
): Promise<LoadPageResult> => {
|
||||
if (!pagedResourceTemplatesState || !client) {
|
||||
return { resourceTemplates: [], nextCursor: undefined };
|
||||
}
|
||||
const result = await pagedResourceTemplatesState.loadPage(
|
||||
cursor,
|
||||
metadata,
|
||||
);
|
||||
setResourceTemplates(pagedResourceTemplatesState.getResourceTemplates());
|
||||
return result;
|
||||
},
|
||||
[client, pagedResourceTemplatesState],
|
||||
);
|
||||
|
||||
const clear = useCallback(() => {
|
||||
pagedResourceTemplatesState?.clear();
|
||||
}, [pagedResourceTemplatesState]);
|
||||
|
||||
return { resourceTemplates, loadPage, clear };
|
||||
}
|
||||
71
core/react/usePagedResources.ts
Normal file
71
core/react/usePagedResources.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import { useState, useEffect, useCallback } from "react";
|
||||
import type { InspectorClientProtocol } from "../mcp/inspectorClientProtocol.js";
|
||||
import type {
|
||||
PagedResourcesState,
|
||||
PagedResourcesStateEventMap,
|
||||
LoadPageResult,
|
||||
} from "../mcp/state/pagedResourcesState.js";
|
||||
import type { Resource } from "@modelcontextprotocol/sdk/types.js";
|
||||
import type { TypedEventGeneric } from "../mcp/typedEventTarget.js";
|
||||
|
||||
export interface UsePagedResourcesResult {
|
||||
resources: Resource[];
|
||||
loadPage: (
|
||||
cursor?: string,
|
||||
metadata?: Record<string, string>,
|
||||
) => Promise<LoadPageResult>;
|
||||
clear: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* React hook that subscribes to PagedResourcesState and returns resources + loadPage.
|
||||
*/
|
||||
export function usePagedResources(
|
||||
client: InspectorClientProtocol | null,
|
||||
pagedResourcesState: PagedResourcesState | null,
|
||||
): UsePagedResourcesResult {
|
||||
const [resources, setResources] = useState<Resource[]>(
|
||||
pagedResourcesState?.getResources() ?? [],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!pagedResourcesState) {
|
||||
setResources([]);
|
||||
return;
|
||||
}
|
||||
setResources(pagedResourcesState.getResources());
|
||||
const onResourcesChange = (
|
||||
event: TypedEventGeneric<PagedResourcesStateEventMap, "resourcesChange">,
|
||||
) => {
|
||||
setResources(event.detail);
|
||||
};
|
||||
pagedResourcesState.addEventListener("resourcesChange", onResourcesChange);
|
||||
return () => {
|
||||
pagedResourcesState.removeEventListener(
|
||||
"resourcesChange",
|
||||
onResourcesChange,
|
||||
);
|
||||
};
|
||||
}, [pagedResourcesState]);
|
||||
|
||||
const loadPage = useCallback(
|
||||
async (
|
||||
cursor?: string,
|
||||
metadata?: Record<string, string>,
|
||||
): Promise<LoadPageResult> => {
|
||||
if (!pagedResourcesState || !client) {
|
||||
return { resources: [], nextCursor: undefined };
|
||||
}
|
||||
const result = await pagedResourcesState.loadPage(cursor, metadata);
|
||||
setResources(pagedResourcesState.getResources());
|
||||
return result;
|
||||
},
|
||||
[client, pagedResourcesState],
|
||||
);
|
||||
|
||||
const clear = useCallback(() => {
|
||||
pagedResourcesState?.clear();
|
||||
}, [pagedResourcesState]);
|
||||
|
||||
return { resources, loadPage, clear };
|
||||
}
|
||||
62
core/react/usePagedTools.ts
Normal file
62
core/react/usePagedTools.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import { useState, useEffect, useCallback } from "react";
|
||||
import type { InspectorClientProtocol } from "../mcp/inspectorClientProtocol.js";
|
||||
import type {
|
||||
PagedToolsState,
|
||||
PagedToolsStateEventMap,
|
||||
LoadPageResult,
|
||||
} from "../mcp/state/pagedToolsState.js";
|
||||
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
|
||||
import type { TypedEventGeneric } from "../mcp/typedEventTarget.js";
|
||||
|
||||
export interface UsePagedToolsResult {
|
||||
tools: Tool[];
|
||||
loadPage: (cursor?: string) => Promise<LoadPageResult>;
|
||||
clear: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* React hook that subscribes to PagedToolsState and returns tools + loadPage.
|
||||
*/
|
||||
export function usePagedTools(
|
||||
client: InspectorClientProtocol | null,
|
||||
pagedToolsState: PagedToolsState | null,
|
||||
): UsePagedToolsResult {
|
||||
const [tools, setTools] = useState<Tool[]>(
|
||||
pagedToolsState?.getTools() ?? [],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!pagedToolsState) {
|
||||
setTools([]);
|
||||
return;
|
||||
}
|
||||
setTools(pagedToolsState.getTools());
|
||||
const onToolsChange = (
|
||||
event: TypedEventGeneric<PagedToolsStateEventMap, "toolsChange">,
|
||||
) => {
|
||||
setTools(event.detail);
|
||||
};
|
||||
pagedToolsState.addEventListener("toolsChange", onToolsChange);
|
||||
return () => {
|
||||
pagedToolsState.removeEventListener("toolsChange", onToolsChange);
|
||||
};
|
||||
}, [pagedToolsState]);
|
||||
|
||||
const loadPage = useCallback(
|
||||
async (cursor?: string): Promise<LoadPageResult> => {
|
||||
if (!pagedToolsState || !client) {
|
||||
return { tools: [], nextCursor: undefined };
|
||||
}
|
||||
const result = await pagedToolsState.loadPage(cursor);
|
||||
setTools(pagedToolsState.getTools());
|
||||
return result;
|
||||
},
|
||||
[client, pagedToolsState],
|
||||
);
|
||||
|
||||
const clear = useCallback(() => {
|
||||
pagedToolsState?.clear();
|
||||
}, [pagedToolsState]);
|
||||
|
||||
return { tools, loadPage, clear };
|
||||
}
|
||||
44
core/react/useStderrLog.ts
Normal file
44
core/react/useStderrLog.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import { useState, useEffect } from "react";
|
||||
import type { StderrLogEntry } from "../mcp/types.js";
|
||||
import type {
|
||||
StderrLogState,
|
||||
StderrLogStateEventMap,
|
||||
} from "../mcp/state/stderrLogState.js";
|
||||
import type { TypedEventGeneric } from "../mcp/typedEventTarget.js";
|
||||
|
||||
export interface UseStderrLogResult {
|
||||
stderrLogs: StderrLogEntry[];
|
||||
}
|
||||
|
||||
/**
|
||||
* React hook that subscribes to StderrLogState and returns the stderr log list.
|
||||
*/
|
||||
export function useStderrLog(
|
||||
stderrLogState: StderrLogState | null,
|
||||
): UseStderrLogResult {
|
||||
const [stderrLogs, setStderrLogs] = useState<StderrLogEntry[]>(
|
||||
stderrLogState?.getStderrLogs() ?? [],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!stderrLogState) {
|
||||
setStderrLogs([]);
|
||||
return;
|
||||
}
|
||||
setStderrLogs(stderrLogState.getStderrLogs());
|
||||
const onStderrLogsChange = (
|
||||
event: TypedEventGeneric<StderrLogStateEventMap, "stderrLogsChange">,
|
||||
) => {
|
||||
setStderrLogs(event.detail);
|
||||
};
|
||||
stderrLogState.addEventListener("stderrLogsChange", onStderrLogsChange);
|
||||
return () => {
|
||||
stderrLogState.removeEventListener(
|
||||
"stderrLogsChange",
|
||||
onStderrLogsChange,
|
||||
);
|
||||
};
|
||||
}, [stderrLogState]);
|
||||
|
||||
return { stderrLogs };
|
||||
}
|
||||
Loading…
Reference in a new issue