Closes #2248. Completes SEP-2640 support across all three clients, and closes the one obligation #2234 deliberately left open. - `checkSkillFrontmatterMatch` compares a served SKILL.md's own YAML frontmatter against the entry the listing advertised, field by field. No digest can cover this: a digest is taken over the bytes the server served, so it proves the file was not altered in transit and says nothing about whether the listing described it honestly. Needs a real YAML parser; `yaml` was already a root dependency, so this adds no package — but it does newly put it on core/'s import graph, hence the three bundler `external` lists. - `resources/directory/read`: result schemas defined against the normative text, `InspectorClient.readResourceDirectory`, a Directory section on the Skills screen, and `--method resources/directory/read` in the CLI. The client refuses the call locally when the server did not declare `directoryRead`, which is the SEP's MUST NOT. A child the directory lists but the entry does not is marked `not listed` rather than merged into the manifest — the SEP calls a directory result a live observation and forbids treating it as extending the manifest. - CLI: `skills/list`, `skills/get` and `resources/directory/read`, plus `--verify` — one NDJSON report per skill, a summary on stderr, exit 7 on a violation. Reads are sequential; a conforming manifest may declare 512 entries. - TUI: a Skills pane, shown only when the server declares the extension. Each row carries its conformance verdict as a glyph as well as a colour, since the pane is read over ssh and through script(1). Enter verifies. - `verifySkills` re-throws `AuthRecoveryRequiredError` rather than recording it per file: it says the session's authorization expired, so absorbing it would report N identical read failures and swallow the error the TUI and the web commands key off to reauthorize. Two open questions settled, both in code comments where they will be found: `skills/get` carries no caching attributes because SEP-2640 leaves the question open in as many words, so requiring them would fail a conforming server; and there is no `PagedSkillsState` because every consumer of this list is a whole-catalog verdict — computed over page one of three, "this server's skills conform" is not merely partial but wrong. The fixture grows two skills, both for checks that were otherwise undemonstrable: `lying-listing` (listing and file disagree, digest still verifies) and `stale-manifest` (serves a file its manifest does not declare). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ND46Ci64DC3tzBy9aG81Nc Signed-off-by: cliffhall <cliff@futurescale.com>
215 lines
8.5 KiB
TypeScript
215 lines
8.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,
|
|
InspectorServerSettings,
|
|
ResourceReadInvocation,
|
|
ResourceTemplateReadInvocation,
|
|
PromptGetInvocation,
|
|
ToolCallInvocation,
|
|
ResourceSubscriptionStreamState,
|
|
ExcludedTool,
|
|
RequestMetadata,
|
|
} 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 { 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";
|
|
|
|
/**
|
|
* 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;
|
|
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;
|
|
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 }>;
|
|
}
|