Adds detection, enumeration, and conformance checking for the Skills extension (`io.modelcontextprotocol/skills`) — phases 1 and 2 of #2234, which is everything its Acceptance list names. Skills is a *server*-declared extension, read off the connecting server's `capabilities.extensions`, so it is deliberately absent from `ADVERTISABLE_EXTENSIONS` — that registry is what the Inspector advertises and the user toggles, and an entry there would be a meaningless toggle. `skills/list` and `skills/get` are consumer-owned extension methods that neither era codec defines, so the SDK's era gate skips them and they go out as ordinary `client.request` calls with explicit result schemas. The raw-wire channel modern `tasks/*` needs is not used, and the Skills tab is not era-gated: a legacy-era server that declares the extension is serving it. The checks are the point, not the list view. `core/mcp/skills.ts` reports each obligation SEP-2640 states — the name/path invariant, the digest format, the 512-entry and 16 MiB limits, and `resources: "dynamic"`, which means integrity cannot be verified at all. Digest verification hashes the fetched bytes with WebCrypto and returns a mismatch with both digests attached rather than throwing, because showing a mismatch loudly is the whole value proposition. Files are fetched on demand: SEP-2640 is explicit that a `resources/read` of a `SKILL.md` is not a load and confers no standing, so none of the SEP's host machinery is implemented. The `skills-http` fixture serves four skills over two pages, three of them deliberately non-conforming — without those the verification code is untestable. Phase 3 (CLI, TUI, `resources/directory/read`, paged mode) is #2248. Closes #2234 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018kfrH3uRNUM4JAk1reKtyY Signed-off-by: cliffhall <cliff@futurescale.com>
201 lines
7.9 KiB
TypeScript
201 lines
7.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,
|
|
} 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 { 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>;
|
|
|
|
/**
|
|
* 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 }>;
|
|
}
|