inspector/core/mcp/inspectorClientProtocol.ts
Cliff Hall e70a11360c
Tools: x-mcp-header annotation tooling + -32602 unknown-tool rendering (#1632) (#1735)
* Tools: x-mcp-header annotation tooling + -32602 unknown-tool rendering (#1632)

Card 9/10 of the SDK-v2 + New Spec stack. Adds SEP-2243 `x-mcp-header`
tooling to the Tools tab and adjusts result rendering for SDK v2's
unknown-tool behavior, and bumps the SDK to 2.0.0-beta.5.

- **`core/json/xMcpHeader.ts`**: a faithful port of the SDK's (non-public)
  `scanXMcpHeaderDeclarations` — validates every SEP-2243 constraint
  (primitive types incl. `number`, RFC 9110 token, case-insensitive
  uniqueness, statically-reachable `properties`-only chain) and derives the
  mirrored `Mcp-Param-{Name}` params for a kept tool.

- **Excluded tools**: the SDK silently drops tools with invalid annotations
  from `tools/list`. InspectorClient now re-lists the RAW (unfiltered) list
  and scans it to recompute the excluded set with reasons — gated to the
  modern non-stdio era (matching the SDK's gate), recomputed on every
  aggregate refresh, surfaced via a new `excludedToolsChange` event +
  `getExcludedTools()` and `useInspectorClient().excludedTools`. The Tools
  sidebar lists them struck-through under an "Excluded (SEP-2243)" divider
  with the reason on hover.

- **Mirrored args**: ToolDetailPanel shows a "Mirrored request headers"
  section listing each arg → `Mcp-Param-{Name}` (with the browser-omits note).

- **-32602 rendering**: SDK v2 rejects an unknown-tool call with `-32602`
  instead of an `isError` result, which previously rendered nothing. New
  ToolCallErrorPanel renders a thrown tool-call error, with a targeted
  "Unknown Tool" hint for `-32602`.

Adds unit + Storybook + integration coverage (a real modern server with an
invalid-header tool fixture) for every path. `npm run ci`-equivalent checks
green.

Closes #1632

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

* test-servers: x-mcp-header modern showcase config + preset (#1632)

Register the `invalid_header_tool` fixture as a preset and add
`xmcpheader-modern-http.json` — a modern HTTP showcase serving echo, a
valid-header `get_weather`, and the invalid-header tool — so the Tools-tab
x-mcp-header tooling (mirrored headers, excluded-tools surface, -32602
rendering) can be exercised manually. Documented in the README alongside the
other per-SEP showcase configs. Smoke-tested end to end in the web client.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

* docs: add #1632 proof screenshots to pr-screenshots

End-to-end web-client verification of the x-mcp-header Tools tooling against
the modern showcase server: excluded-tools surface + reason, mirrored-headers
section, and the -32602 unknown-tool error panel.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

* Respond to review: disambiguate -32602, doc/search nits (#1632)

Addresses the review on PR #1735:

1. -32602 "Unknown Tool" mislabel: `-32602` is generic Invalid params, so a
   known tool called with bad arguments hit the same code and rendered under
   the definitive "Unknown Tool" heading. Replace `isUnknownToolError` with
   `classifyToolCallError(code, message)` → "unknown-tool" only when the
   message names it (not found / unknown tool / …), else "invalid-params"
   (new "Invalid Parameters" heading + schema hint), else "generic".

2. Double pagination walk: expand the `listAllTools` comment to document that
   the excluded-tools recompute is a deliberately un-cached second raw walk
   (doubles list round-trips on modern non-stdio, runs even on a cache-served
   aggregate) — accepted for a debugging tool, no-op on legacy/stdio.

3. Doc nit: `scanXMcpHeaderDeclarations` no longer claims to visit "$ref
   targets" — reworded to note `$ref` is never followed (refs resolve into the
   unreachable `$defs` body).

4. Search nit: the excluded-list filter now matches name AND title, matching
   the main tools filter.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

* Tighten unknown-tool matcher to be tool-scoped (#1632 re-review nit)

The re-review noted the -32602 disambiguation was symmetric: broad markers
("does not exist", "unrecognized") could inversely mislabel a genuine
invalid-params message (e.g. "property 'region' does not exist") as "Unknown
Tool". Scope the not-found/does-not-exist family to require the word "tool" in
the same clause (matching the SDK's "Tool <name> not found"); keep "unknown
tool" / "no such tool" as standalone unambiguous markers. Adds a
tool-less-"does not exist" test asserting it classifies as invalid-params.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

* Add trigger_invalid_params showcase tool + Invalid Parameters screenshot (#1632)

Adds a `trigger_invalid_params` spec-error trigger to the modern showcase
server: the injector answers its `tools/call` with a real `-32602 Invalid
params` JSON-RPC error whose message does NOT name an unknown tool. This lets
the "Invalid Parameters" branch of the tool-call error panel (added in the
review response) be exercised live, distinct from the unknown-tool `-32602`.

- `SPEC_ERROR_TRIGGERS` gains the `trigger_invalid_params` entry (HTTP 200,
  in-band -32602) + preset registration.
- Enable `injectSpecErrors` on `xmcpheader-modern-http.json` and add the tool.
- README + pr-screenshots README document it; adds the live "Invalid
  Parameters" proof screenshot.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 11:42:54 -04:00

174 lines
6.7 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,
} 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 { InspectorClientEventTarget } from "./inspectorClientEventTarget.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[];
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?: 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 }>;
/** 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;
// 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?: Record<string, string>;
}): Promise<{ tools: Tool[] }>;
listAllPrompts(options?: {
cacheMode?: CacheMode;
metadata?: Record<string, string>;
}): Promise<{ prompts: Prompt[] }>;
listAllResources(options?: {
cacheMode?: CacheMode;
metadata?: Record<string, string>;
}): Promise<{ resources: Resource[] }>;
listAllResourceTemplates(options?: {
cacheMode?: CacheMode;
metadata?: Record<string, string>;
}): Promise<{ resourceTemplates: ResourceTemplate[] }>;
// 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;
// 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?: Record<string, string>,
): Promise<{ values: string[]; total?: number; hasMore?: boolean }>;
}