inspector/core/mcp/inspectorClientProtocol.ts
Cliff Hall e3e70470b5
feat(core): port v1.5 hook layer (#1243) (#1300)
* 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>
2026-05-13 17:43:07 -04:00

105 lines
3.5 KiB
TypeScript

/**
* InspectorClientProtocol: the minimal surface that state managers (core/mcp/state/)
* and React hooks (core/react/) consume.
*
* v1.5 has a single concrete `InspectorClient` class (~2k LOC) bundling the transport,
* SDK client, OAuth flow, pending-request queues, and fetch tracking. Porting the full
* class requires first porting the auth subsystem (core/auth/, ~12 files) — that's
* deferred to a follow-up issue.
*
* This interface captures just what the state+hooks layer needs to function,
* so the layer is decoupled from how (or whether) the runtime is wired up.
* The real `InspectorClient` will implement this interface in the follow-up port.
*/
import type {
ConnectionStatus,
ResourceReadInvocation,
ResourceTemplateReadInvocation,
PromptGetInvocation,
ToolCallInvocation,
} from "./types.js";
import type {
Implementation,
LoggingLevel,
Prompt,
Resource,
ResourceTemplate,
ServerCapabilities,
Task,
Tool,
} from "@modelcontextprotocol/sdk/types.js";
import type { JsonValue } from "../json/jsonUtils.js";
import type { InspectorClientEventTarget } from "./inspectorClientEventTarget.js";
/**
* Opaque type representing the AppRendererClient surface used by @mcp-ui.
* v1.5 aliases this to the SDK `Client` type; v2 leaves it opaque until the
* real InspectorClient (or a focused App-renderer port) lands.
*/
export type AppRendererClient = unknown;
/**
* The contract every state manager and hook depends on. Anything that holds
* a reference to "an Inspector client" — including tests and fixtures —
* should depend on this protocol rather than the concrete class.
*/
export interface InspectorClientProtocol extends InspectorClientEventTarget {
// Connection state accessors
getStatus(): ConnectionStatus;
getCapabilities(): ServerCapabilities | undefined;
getServerInfo(): Implementation | undefined;
getInstructions(): string | undefined;
getAppRendererClient(): AppRendererClient | null;
// Connection control
connect(): Promise<void>;
disconnect(safeDisconnectTimeout?: number): Promise<void>;
// Paginated list methods used by managed/paged state stores
listTools(
cursor?: string,
metadata?: Record<string, string>,
): Promise<{ tools: Tool[]; nextCursor?: string }>;
listPrompts(
cursor?: string,
metadata?: Record<string, string>,
): Promise<{ prompts: Prompt[]; nextCursor?: string }>;
listResources(
cursor?: string,
metadata?: Record<string, string>,
): Promise<{ resources: Resource[]; nextCursor?: string }>;
listResourceTemplates(
cursor?: string,
metadata?: Record<string, string>,
): Promise<{ resourceTemplates: ResourceTemplate[]; nextCursor?: string }>;
listRequestorTasks(
cursor?: string,
): Promise<{ tasks: Task[]; nextCursor?: string }>;
// Invocation methods used by paged result panels
callTool(
tool: Tool,
args: Record<string, JsonValue>,
generalMetadata?: Record<string, string>,
toolSpecificMetadata?: Record<string, string>,
): Promise<ToolCallInvocation>;
readResource(
uri: string,
metadata?: Record<string, string>,
): Promise<ResourceReadInvocation>;
readResourceFromTemplate(
uriTemplate: string,
params: Record<string, string>,
metadata?: Record<string, string>,
): Promise<ResourceTemplateReadInvocation>;
getPrompt(
name: string,
params?: Record<string, string>,
metadata?: Record<string, string>,
): Promise<PromptGetInvocation>;
// Misc surface required by hooks/state
setLoggingLevel(level: LoggingLevel): Promise<void>;
getSessionId(): string | undefined;
}