* 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>
This commit is contained in:
parent
2259b1a3e1
commit
e70a11360c
1 changed files with 1610 additions and 86 deletions
|
|
@ -114,11 +114,13 @@ A streamable-HTTP server can also serve the **modern (2026-07-28) protocol era**
|
|||
|
||||
`test-servers/configs/modern-network-http.json` is the **Network-tab showcase** for the standardized HTTP headers and new error taxonomy (SEP-2243 / SEP-2575). It serves a `get_weather` tool whose `city` argument carries an `x-mcp-header: "City"` annotation (so a modern client mirrors it to `Mcp-Param-City`), plus four `trigger_*` tools that the modern leg's spec-error injector (`transport.modern.injectSpecErrors: true`) answers with a real HTTP status + JSON-RPC error body: `trigger_header_mismatch` → `400 / -32020`, `trigger_missing_capability` → `400 / -32021`, `trigger_unsupported_version` → `400 / -32022` (with `data.supported`), `trigger_method_not_found` → `404 / -32601`. Connect to it with **Protocol Era = Modern** and open the Network tab to see the mirrored `Mcp-*` headers highlighted, sentinel values decoded, and each error rendered distinctly. Note: `Mcp-Param-*` mirroring is **skipped by the SDK in the browser** (`detectProbeEnvironment() !== "browser"`), so calling `get_weather` from the **web** client omits `Mcp-Param-City` and the strict server answers `-32020` — the same tool is callable from the Node CLI/TUI, where mirroring is active.
|
||||
|
||||
`test-servers/configs/xmcpheader-modern-http.json` is the **`x-mcp-header` Tools-tab showcase** (#1632). It serves `echo`, a `get_weather` tool with a **valid** `x-mcp-header: "City"` annotation on its `city` argument, an `invalid_header_tool` whose annotation uses the header name `"Bad Header"` (a space makes it an invalid RFC 9110 token, so the whole tool definition is invalid), and a `trigger_invalid_params` tool that the modern leg's spec-error injector (`transport.modern.injectSpecErrors: true`) answers with a real `-32602 Invalid params` JSON-RPC error whose message is not about a missing tool. Connect with **Protocol Era = Modern** and open the Tools tab: `get_weather`'s detail panel shows a **"Mirrored request headers (SEP-2243)"** section (`city → Mcp-Param-City`), and `invalid_header_tool` appears struck-through under an **"Excluded (SEP-2243)"** divider in the sidebar with the reason on hover (a conforming Streamable HTTP client MUST drop it from `tools/list`; the Inspector surfaces _why_). Under SDK v2 a `tools/call` that rejects with **`-32602`** now renders as a distinct error panel rather than an `isError` result — headed **"Unknown Tool"** when the message names a missing tool (reproduce by calling a tool the server dropped from its list), or **"Invalid Parameters"** for any other `-32602` (run `trigger_invalid_params`).
|
||||
|
||||
`test-servers/configs/pagination-http.json` is the **page-by-page fetch showcase** (#1721). It serves 12 tools, 12 resources, and 12 prompts (presets `numbered_tools` / `numbered_resources` / `numbered_prompts`, `count: 12`) with `maxPageSize` of 4 for each, so every list paginates into three pages. Turn on **"Fetch Lists One Page at a Time"** (Server Settings — the `paginatedLists` setting, or the **Paginated** switch in a list sidebar) and the Tools/Resources/Prompts lists load page 1 only (4 items) with a **Load next page** control and an _N pages loaded_ status; each click fetches the next 4 and appends them, and Refresh resets to page 1. With the switch off (the default), the same lists auto-aggregate all three pages on connect.
|
||||
|
||||
`test-servers/configs/logging-legacy-http.json` and `test-servers/configs/logging-modern-http.json` are the **logging era-fork showcase** (#1629). Both serve `logging: true` plus a `send_notification` tool that emits a `notifications/message` at a chosen level; the legacy one is a plain streamable-HTTP server (`logging/setLevel` era) and the modern one sets `transport.modern: true`. Connect to the legacy server and open the **Logs** tab to get the session-scoped **Set Active Level** selector + **Set** button; calling `send_notification` streams the log into the panel. Connect to the modern one with **Protocol Era = Modern** and the same tab instead shows the **Log Level per Request** control — pick a level to opt in and the client stamps `_meta["io.modelcontextprotocol/logLevel"]` on every subsequent request (verify in the Network tab's request body); calling `send_notification` then streams the log into the panel over the request's SSE response. Set the control back to **Off** and the same call is silently gated — the request omits the `logLevel` key, so the log never arrives. That gating is faithful to the spec ("a server MUST NOT emit `notifications/message` for a request that didn't opt in") because `send_notification` emits through the SDK's request-scoped, threshold-aware `extra.log` (`ctx.mcpReq.log`): on the modern leg it reads the per-request `logLevel` opt-in from the request envelope and drops the message when the client didn't opt in or the level is below the requested severity; on legacy it honors the session level from `logging/setLevel`. Because it emits through the request's `notify`, the modern response upgrades to SSE and the log rides the originating request's stream.
|
||||
|
||||
`test-servers/configs/subscriptions-legacy-http.json` and `test-servers/configs/subscriptions-modern-http.json` are the **resource-subscription era-fork showcase** (#1630). Both serve three `numbered_resources` with `subscriptions: true`; the legacy one also serves an `update_resource` tool, and the modern one sets `transport.modern: true`. Connect to the **legacy** server, open a resource in the **Resources** tab and click **Subscribe** — the client sends `resources/subscribe` (Network/Protocol view) and the Subscriptions section lists the URI with no stream chrome; call `update_resource` with that URI and the server updates the content and emits `notifications/resources/updated`, stamping the subscribed tile's last-updated time. Connect to the **modern** server with **Protocol Era = Modern** and the same Subscribe instead sends **`subscriptions/listen`** (its filter carries `resourceSubscriptions` + the `resourcesListChanged` opt-in) and resolves on `notifications/subscriptions/acknowledged`; the Subscriptions section then shows the stream-status badge (`Connecting…` → `Listening`) in its header, and if the long-lived stream drops it reconnects by re-listing. The modern config deliberately **omits** `update_resource`: the SDK's modern leg is stateless/per-request (`createMcpHandler(() => createMcpServer(config))`), so the tool would run against a throwaway server instance — the content change wouldn't persist for the next `resources/read`, and its `resources/updated` wouldn't reach the (separate) listen stream — which is more confusing than useful. The live update-notification round-trip is therefore demonstrated on the legacy (stateful-session) server; the modern server is for the subscribe/listen/badge behavior. (The Inspector's *receive* path is era-transparent, so a real stateful modern server that routes `resources/updated` onto the listen stream drives the subscribed tile the same way.)
|
||||
`test-servers/configs/subscriptions-legacy-http.json` and `test-servers/configs/subscriptions-modern-http.json` are the **resource-subscription era-fork showcase** (#1630). Both serve three `numbered_resources` with `subscriptions: true`; the legacy one also serves an `update_resource` tool, and the modern one sets `transport.modern: true`. Connect to the **legacy** server, open a resource in the **Resources** tab and click **Subscribe** — the client sends `resources/subscribe` (Network/Protocol view) and the Subscriptions section lists the URI with no stream chrome; call `update_resource` with that URI and the server updates the content and emits `notifications/resources/updated`, stamping the subscribed tile's last-updated time. Connect to the **modern** server with **Protocol Era = Modern** and the same Subscribe instead sends **`subscriptions/listen`** (its filter carries `resourceSubscriptions` + the `resourcesListChanged` opt-in) and resolves on `notifications/subscriptions/acknowledged`; the Subscriptions section then shows the stream-status badge (`Connecting…` → `Listening`) in its header, and if the long-lived stream drops it reconnects by re-listing. The modern config deliberately **omits** `update_resource`: the SDK's modern leg is stateless/per-request (`createMcpHandler(() => createMcpServer(config))`), so the tool would run against a throwaway server instance — the content change wouldn't persist for the next `resources/read`, and its `resources/updated` wouldn't reach the (separate) listen stream — which is more confusing than useful. The live update-notification round-trip is therefore demonstrated on the legacy (stateful-session) server; the modern server is for the subscribe/listen/badge behavior. (The Inspector's _receive_ path is era-transparent, so a real stateful modern server that routes `resources/updated` onto the listen stream drives the subscribed tile the same way.)
|
||||
|
||||
`test-servers/configs/tasks-legacy-http.json` and `test-servers/configs/tasks-modern-http.json` are the **Tasks era-fork showcase** (#1631). The legacy server advertises `capabilities.tasks` (`tasks: { list, cancel }`) with the `simple_task` / `progress_task` / `elicitation_task` presets — connect to it, run one of those tools with **Run as task** on, and the **Tasks** tab lists it (populated via `tasks/list`), polls `tasks/get`, fetches the payload with the blocking `tasks/result`, and cancels with `tasks/cancel`. The modern server sets `transport.modern: true` and `tasksExtension: true`, advertising the `io.modelcontextprotocol/tasks` extension (SEP-2663) and serving the `modern_task` / `modern_input_task` tools. Connect with **Protocol Era = Modern**: the **Tasks** tab is now gated on the negotiated extension (not `capabilities.tasks`). Run `modern_task` as a task — the `tools/call` returns a `CreateTaskResult` (`resultType: "task"`, visible in the Protocol/Network tabs), the client polls **`tasks/get`** (no `tasks/list`), and the completed task inlines its result (no blocking `tasks/result`). Run `modern_input_task` and the task moves to `input_required`, surfacing an embedded elicitation through the pending-request modal; answering it sends **`tasks/update`** with the `inputResponses`, and the next poll completes. SDK v2 removed all tasks support **and** era-gates the `tasks/*` spec methods out of the modern era on both sides — so the Inspector drives the extension itself (the `resultType: "task"` frame is rewritten at the transport into a `CallToolResult` carrying the handle; `tasks/get`/`update`/`cancel` ride a raw-wire request channel with the full modern envelope), and the test server serves `tasks/*` from an Express interceptor ahead of the SDK handler (the SDK's modern leg would answer them `-32601`). The Tasks tab's **Refresh** re-polls the handles already known to the client (modern has no server-side task list).
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue