* 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
1 changed files with 6404 additions and 5 deletions
219
core/mcp/__tests__/fakeInspectorClient.ts
Normal file
219
core/mcp/__tests__/fakeInspectorClient.ts
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
/**
|
||||
* Test-only fake InspectorClient: an InspectorClientEventTarget subclass
|
||||
* implementing InspectorClientProtocol with stubbed methods. State and hook
|
||||
* tests inject this so they can drive events, queue paginated list responses,
|
||||
* and assert subscriber wiring without a real transport or SDK client.
|
||||
*/
|
||||
|
||||
import { vi } from "vitest";
|
||||
import type {
|
||||
Implementation,
|
||||
LoggingLevel,
|
||||
Prompt,
|
||||
Resource,
|
||||
ResourceTemplate,
|
||||
ServerCapabilities,
|
||||
Task,
|
||||
Tool,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
import { InspectorClientEventTarget } from "../inspectorClientEventTarget.js";
|
||||
import type {
|
||||
AppRendererClient,
|
||||
InspectorClientProtocol,
|
||||
} from "../inspectorClientProtocol.js";
|
||||
import type {
|
||||
ConnectionStatus,
|
||||
PromptGetInvocation,
|
||||
ResourceReadInvocation,
|
||||
ResourceTemplateReadInvocation,
|
||||
ToolCallInvocation,
|
||||
} from "../types.js";
|
||||
import type { JsonValue } from "../../json/jsonUtils.js";
|
||||
|
||||
type ListResult<TKey extends string, TItem> = {
|
||||
[K in TKey]: TItem[];
|
||||
} & { nextCursor?: string };
|
||||
|
||||
export interface FakeInspectorClientOptions {
|
||||
status?: ConnectionStatus;
|
||||
capabilities?: ServerCapabilities;
|
||||
serverInfo?: Implementation;
|
||||
instructions?: string;
|
||||
}
|
||||
|
||||
export class FakeInspectorClient
|
||||
extends InspectorClientEventTarget
|
||||
implements InspectorClientProtocol
|
||||
{
|
||||
private status: ConnectionStatus;
|
||||
private capabilities: ServerCapabilities | undefined;
|
||||
private serverInfo: Implementation | undefined;
|
||||
private instructions: string | undefined;
|
||||
private appRendererClient: AppRendererClient | null = null;
|
||||
private sessionId: string | undefined;
|
||||
|
||||
// Each paginated method pulls from a queue of pre-canned pages. Tests push
|
||||
// pages with `queueToolPages(...)` etc. so they can assert pagination
|
||||
// accumulation without wiring a real server.
|
||||
toolPages: Array<ListResult<"tools", Tool>> = [];
|
||||
promptPages: Array<ListResult<"prompts", Prompt>> = [];
|
||||
resourcePages: Array<ListResult<"resources", Resource>> = [];
|
||||
resourceTemplatePages: Array<
|
||||
ListResult<"resourceTemplates", ResourceTemplate>
|
||||
> = [];
|
||||
taskPages: Array<ListResult<"tasks", Task>> = [];
|
||||
|
||||
listTools = vi.fn(async () => this.toolPages.shift() ?? { tools: [] });
|
||||
listPrompts = vi.fn(async () => this.promptPages.shift() ?? { prompts: [] });
|
||||
listResources = vi.fn(
|
||||
async () => this.resourcePages.shift() ?? { resources: [] },
|
||||
);
|
||||
listResourceTemplates = vi.fn(
|
||||
async () =>
|
||||
this.resourceTemplatePages.shift() ?? { resourceTemplates: [] },
|
||||
);
|
||||
listRequestorTasks = vi.fn(
|
||||
async () => this.taskPages.shift() ?? { tasks: [] },
|
||||
);
|
||||
|
||||
callTool = vi.fn(
|
||||
async (
|
||||
tool: Tool,
|
||||
params: Record<string, JsonValue>,
|
||||
): Promise<ToolCallInvocation> => ({
|
||||
toolName: tool.name,
|
||||
params,
|
||||
result: null,
|
||||
timestamp: new Date(),
|
||||
success: true,
|
||||
}),
|
||||
);
|
||||
|
||||
readResource = vi.fn(
|
||||
async (uri: string): Promise<ResourceReadInvocation> => ({
|
||||
result: { contents: [] },
|
||||
timestamp: new Date(),
|
||||
uri,
|
||||
}),
|
||||
);
|
||||
|
||||
readResourceFromTemplate = vi.fn(
|
||||
async (
|
||||
uriTemplate: string,
|
||||
params: Record<string, string>,
|
||||
): Promise<ResourceTemplateReadInvocation> => ({
|
||||
uriTemplate,
|
||||
expandedUri: uriTemplate,
|
||||
result: { contents: [] },
|
||||
timestamp: new Date(),
|
||||
params,
|
||||
}),
|
||||
);
|
||||
|
||||
getPrompt = vi.fn(
|
||||
async (name: string): Promise<PromptGetInvocation> => ({
|
||||
result: { messages: [] },
|
||||
timestamp: new Date(),
|
||||
name,
|
||||
}),
|
||||
);
|
||||
|
||||
setLoggingLevel = vi.fn(async (_level: LoggingLevel) => {});
|
||||
|
||||
constructor(options: FakeInspectorClientOptions = {}) {
|
||||
super();
|
||||
this.status = options.status ?? "disconnected";
|
||||
this.capabilities = options.capabilities;
|
||||
this.serverInfo = options.serverInfo;
|
||||
this.instructions = options.instructions;
|
||||
}
|
||||
|
||||
getStatus(): ConnectionStatus {
|
||||
return this.status;
|
||||
}
|
||||
|
||||
getCapabilities(): ServerCapabilities | undefined {
|
||||
return this.capabilities;
|
||||
}
|
||||
|
||||
getServerInfo(): Implementation | undefined {
|
||||
return this.serverInfo;
|
||||
}
|
||||
|
||||
getInstructions(): string | undefined {
|
||||
return this.instructions;
|
||||
}
|
||||
|
||||
getAppRendererClient(): AppRendererClient | null {
|
||||
return this.appRendererClient;
|
||||
}
|
||||
|
||||
async connect(): Promise<void> {
|
||||
this.setStatus("connecting");
|
||||
this.setStatus("connected");
|
||||
this.dispatchTypedEvent("connect");
|
||||
}
|
||||
|
||||
async disconnect(): Promise<void> {
|
||||
this.setStatus("disconnected");
|
||||
this.dispatchTypedEvent("disconnect");
|
||||
}
|
||||
|
||||
getSessionId(): string | undefined {
|
||||
return this.sessionId;
|
||||
}
|
||||
|
||||
// ---- test helpers ----
|
||||
|
||||
setStatus(status: ConnectionStatus): void {
|
||||
this.status = status;
|
||||
this.dispatchTypedEvent("statusChange", status);
|
||||
}
|
||||
|
||||
setCapabilities(capabilities: ServerCapabilities | undefined): void {
|
||||
this.capabilities = capabilities;
|
||||
this.dispatchTypedEvent("capabilitiesChange", capabilities);
|
||||
}
|
||||
|
||||
setServerInfo(info: Implementation | undefined): void {
|
||||
this.serverInfo = info;
|
||||
this.dispatchTypedEvent("serverInfoChange", info);
|
||||
}
|
||||
|
||||
setInstructions(instructions: string | undefined): void {
|
||||
this.instructions = instructions;
|
||||
this.dispatchTypedEvent("instructionsChange", instructions);
|
||||
}
|
||||
|
||||
setAppRendererClient(client: AppRendererClient | null): void {
|
||||
this.appRendererClient = client;
|
||||
}
|
||||
|
||||
setSessionId(id: string | undefined): void {
|
||||
this.sessionId = id;
|
||||
}
|
||||
|
||||
queueToolPages(...pages: Array<ListResult<"tools", Tool>>): void {
|
||||
this.toolPages.push(...pages);
|
||||
}
|
||||
|
||||
queuePromptPages(...pages: Array<ListResult<"prompts", Prompt>>): void {
|
||||
this.promptPages.push(...pages);
|
||||
}
|
||||
|
||||
queueResourcePages(
|
||||
...pages: Array<ListResult<"resources", Resource>>
|
||||
): void {
|
||||
this.resourcePages.push(...pages);
|
||||
}
|
||||
|
||||
queueResourceTemplatePages(
|
||||
...pages: Array<ListResult<"resourceTemplates", ResourceTemplate>>
|
||||
): void {
|
||||
this.resourceTemplatePages.push(...pages);
|
||||
}
|
||||
|
||||
queueTaskPages(...pages: Array<ListResult<"tasks", Task>>): void {
|
||||
this.taskPages.push(...pages);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue