A request timeout used to surface as a bare `Request timed out`. Every fact needed to explain it was already in-process: what the client was still waiting on, when it last heard back, and the state of the standalone GET notification stream. The SDK's timeout now rejects with all three, and the same snapshot is shown live in Connection Info and on the Network entry for the stream. - core/mcp/connectionDiagnostics.ts: the snapshot type, its one-line description, and `annotateRequestTimeout`, which rebuilds the SDK's per-request timeout with the state at that moment. - InspectorClient decorates the SDK Client's `request` (the one funnel every verb goes through) so the annotation covers the whole surface; tracks outstanding requests with their send time, the last response, and the most recent long-lived transport stream; exposes `getConnectionDiagnostics()` and `connectionDiagnosticsChange`. - fetchTracking watches a long-lived stream through a tee'd clone, counting SSE events and reporting the close, via a new `updateStream` callback wired through the node transport, the remote session (`fetch_stream_update`), and the browser transport. - FetchRequestLogState patches `entry.stream`; the Network entry's placeholder reports the event count and open/closed state. - Connection Info gains a Connection Activity section, ticking while open (`useTickingClock`). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01APKSYHGxXBUpnR7ErFiLJn Signed-off-by: cliffhall <cliff@futurescale.com>
225 lines
9 KiB
TypeScript
225 lines
9 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,
|
|
InspectorServerSettings,
|
|
ResourceReadInvocation,
|
|
ResourceTemplateReadInvocation,
|
|
PromptGetInvocation,
|
|
ToolCallInvocation,
|
|
ResourceSubscriptionStreamState,
|
|
ExcludedTool,
|
|
RequestMetadata,
|
|
AppRendererClient,
|
|
} from "./types.js";
|
|
import type {
|
|
CacheMode,
|
|
ClientCapabilities,
|
|
DiscoverResult,
|
|
Implementation,
|
|
LoggingLevel,
|
|
Prompt,
|
|
ProtocolEra,
|
|
Resource,
|
|
ResourceTemplateType as ResourceTemplate,
|
|
ServerCapabilities,
|
|
Task,
|
|
Tool,
|
|
} from "@modelcontextprotocol/client";
|
|
import type { JsonValue } from "../json/jsonUtils.js";
|
|
import type { MalformedListItem } from "./listSalvage.js";
|
|
import type { ConnectionDiagnostics } from "./connectionDiagnostics.js";
|
|
import type { InspectorClientEventTarget } from "./inspectorClientEventTarget.js";
|
|
import type { SkillEntry } from "./skillsSchemas.js";
|
|
import type { SkillsExtensionSupport } from "./skills.js";
|
|
import type { DirectoryReadResult } from "./skillsSchemas.js";
|
|
import type { SamplingCreateMessage } from "./samplingCreateMessage.js";
|
|
import type { ElicitationCreateMessage } from "./elicitationCreateMessage.js";
|
|
|
|
/**
|
|
* Re-exported from `./types.js`, where it is defined and documented, so hook
|
|
* code that types against this protocol (`core/react/useInspectorClient.ts`)
|
|
* can import it here without a second, drift-prone definition (#1745).
|
|
*/
|
|
export type { AppRendererClient } from "./types.js";
|
|
|
|
/**
|
|
* 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;
|
|
getClientCapabilities(): ClientCapabilities;
|
|
getServerInfo(): Implementation | undefined;
|
|
getInstructions(): string | undefined;
|
|
getProtocolVersion(): string | undefined;
|
|
getProtocolEra(): ProtocolEra | undefined;
|
|
/** Tools excluded from `tools/list` for invalid `x-mcp-header` annotations
|
|
* (SEP-2243); empty on legacy/stdio connections (#1632). */
|
|
getExcludedTools(): ExcludedTool[];
|
|
/** Entries dropped from a list result as malformed, across every list method;
|
|
* empty against a conforming server (#1909). */
|
|
getMalformedListItems(): MalformedListItem[];
|
|
getResourceSubscriptionStreamState(): ResourceSubscriptionStreamState;
|
|
getDiscoverResult(): DiscoverResult | undefined;
|
|
getServerSettings(): InspectorServerSettings | undefined;
|
|
setServerSettings(settings: InspectorServerSettings): void;
|
|
/**
|
|
* What the client is still waiting on, when it last heard back, and the
|
|
* state of the notification stream — the snapshot a request timeout is
|
|
* annotated with (#2318). Optional so existing test doubles satisfy the
|
|
* interface without implementing it; `connectionDiagnosticsChange` fires
|
|
* whenever it changes.
|
|
*/
|
|
getConnectionDiagnostics?(): ConnectionDiagnostics;
|
|
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?: RequestMetadata,
|
|
): Promise<{ tools: Tool[]; nextCursor?: string }>;
|
|
listPrompts(
|
|
cursor?: string,
|
|
metadata?: RequestMetadata,
|
|
): Promise<{ prompts: Prompt[]; nextCursor?: string }>;
|
|
listResources(
|
|
cursor?: string,
|
|
metadata?: RequestMetadata,
|
|
): Promise<{ resources: Resource[]; nextCursor?: string }>;
|
|
listResourceTemplates(
|
|
cursor?: string,
|
|
metadata?: RequestMetadata,
|
|
): Promise<{ resourceTemplates: ResourceTemplate[]; nextCursor?: string }>;
|
|
listRequestorTasks(
|
|
cursor?: string,
|
|
): Promise<{ tasks: Task[]; nextCursor?: string }>;
|
|
/** Poll one requestor task's current status (era-aware: modern `DetailedTask`
|
|
* via `tasks/get`, or the legacy flattened task). Dispatches
|
|
* `requestorTaskUpdated`. Used by the modern task store's refresh (no
|
|
* `tasks/list`). */
|
|
getRequestorTask(taskId: string): Promise<Task>;
|
|
/** True when a modern (2026-07-28) connection negotiated the
|
|
* `io.modelcontextprotocol/tasks` extension (SEP-2663). Gates the Tasks tab
|
|
* and the modern task store's poll-based refresh. */
|
|
isTasksExtensionNegotiated(): boolean;
|
|
|
|
/** The Skills extension (SEP-2640) the server declared, or `undefined`.
|
|
* Gates the Skills tab and the managed skills store (#2234). */
|
|
getSkillsExtension(): SkillsExtensionSupport | undefined;
|
|
/** One page of `skills/list`; the managed skills store walks the cursor. */
|
|
listSkills(
|
|
cursor?: string,
|
|
metadata?: RequestMetadata,
|
|
): Promise<{ skills: SkillEntry[]; nextCursor?: string }>;
|
|
/** One skill entry by URI (`skills/get`). */
|
|
getSkill(uri: string, metadata?: RequestMetadata): Promise<SkillEntry>;
|
|
/**
|
|
* One page of `resources/directory/read` — the direct children of a directory
|
|
* resource (#2248). Optional on this interface, unlike the two methods above:
|
|
* declaring the extension commits a server to `skills/list` and `skills/get`,
|
|
* while this one is separately gated on `directoryRead`, so a caller has to
|
|
* check for it anyway and the many test doubles that satisfy this interface
|
|
* should not all have to grow a method most of them never reach.
|
|
*/
|
|
readResourceDirectory?(
|
|
uri: string,
|
|
cursor?: string,
|
|
metadata?: RequestMetadata,
|
|
): Promise<DirectoryReadResult>;
|
|
|
|
/**
|
|
* Mark the response that most recently answered `method` as rejected by the
|
|
* client, so its Protocol entry shows the reason instead of rendering as a
|
|
* clean success (#1953). Optional so existing test doubles satisfy the
|
|
* interface without implementing it; see `InspectorClient` for how the id is
|
|
* correlated and why that correlation is exact.
|
|
*/
|
|
markResponseRejected?(method: string, reason: string): void;
|
|
|
|
// Aggregate (all-page) list methods used by the managed state stores on
|
|
// refresh. Unlike the single-page methods above, these route through the
|
|
// SDK's cache-aware high-level verbs so `cacheMode` ('use' | 'refresh' |
|
|
// 'bypass') is honored — a manual refresh forces a cache-bypassing round trip
|
|
// on modern servers that send `ttlMs` hints. Options are optional; omitting
|
|
// `cacheMode` uses the SDK default ('use').
|
|
listAllTools(options?: {
|
|
cacheMode?: CacheMode;
|
|
metadata?: RequestMetadata;
|
|
}): Promise<{ tools: Tool[] }>;
|
|
listAllPrompts(options?: {
|
|
cacheMode?: CacheMode;
|
|
metadata?: RequestMetadata;
|
|
}): Promise<{ prompts: Prompt[] }>;
|
|
listAllResources(options?: {
|
|
cacheMode?: CacheMode;
|
|
metadata?: RequestMetadata;
|
|
}): Promise<{ resources: Resource[] }>;
|
|
listAllResourceTemplates(options?: {
|
|
cacheMode?: CacheMode;
|
|
metadata?: RequestMetadata;
|
|
}): Promise<{ resourceTemplates: ResourceTemplate[] }>;
|
|
|
|
// Invocation methods used by paged result panels
|
|
callTool(
|
|
tool: Tool,
|
|
args: Record<string, JsonValue>,
|
|
generalMetadata?: RequestMetadata,
|
|
toolSpecificMetadata?: RequestMetadata,
|
|
): Promise<ToolCallInvocation>;
|
|
readResource(
|
|
uri: string,
|
|
metadata?: RequestMetadata,
|
|
): Promise<ResourceReadInvocation>;
|
|
readResourceFromTemplate(
|
|
uriTemplate: string,
|
|
params: Record<string, string>,
|
|
metadata?: RequestMetadata,
|
|
): Promise<ResourceTemplateReadInvocation>;
|
|
getPrompt(
|
|
name: string,
|
|
params?: Record<string, string>,
|
|
metadata?: RequestMetadata,
|
|
): Promise<PromptGetInvocation>;
|
|
|
|
// Misc surface required by hooks/state
|
|
setLoggingLevel(level: LoggingLevel): Promise<void>;
|
|
getSessionId(): string | undefined;
|
|
|
|
// Server-initiated request queues. Each entry exposes `respond()` / `reject()`
|
|
// (and `remove()`), which resolve the handler Promise that left the originating
|
|
// call (e.g. a tool execution) in flight. The `pendingSamplesChange` /
|
|
// `pendingElicitationsChange` events fire whenever these arrays mutate.
|
|
getPendingSamples(): SamplingCreateMessage[];
|
|
getPendingElicitations(): ElicitationCreateMessage[];
|
|
|
|
// Completions (resource templates / prompt arguments)
|
|
getCompletions(
|
|
ref:
|
|
| { type: "ref/resource"; uri: string }
|
|
| { type: "ref/prompt"; name: string },
|
|
argumentName: string,
|
|
argumentValue: string,
|
|
context?: Record<string, string>,
|
|
metadata?: RequestMetadata,
|
|
): Promise<{ values: string[]; total?: number; hasMore?: boolean }>;
|
|
}
|