* Extensions: advertised-extensions toggle in Server Settings (Phase 2 of #1633)
Adds the debugging knob from #1633: users choose which extensions the
Inspector advertises to a server, and a server may register different tools
in response. Builds on Phase 1's registry + advertisedExtensions option (#1738).
Settings + persistence (core):
- InspectorServerSettings.advertisedExtensions + StoredMCPServer field, wired
through serverList's read/write converters and INSPECTOR_FIELD_KEY_MAP
(omit-when-empty, byte-stable round-trip).
- validateSettings (remote PUT route): isBooleanRecord guard + carry a
non-empty map; unknown/malformed rejected.
- App.tsx threads savedSettings.advertisedExtensions into the client options.
UI:
- ServerSettingsForm renders an "Advertised Extensions" group of checkboxes
driven by ADVERTISABLE_EXTENSIONS; a per-server override wins over the
registry default. Modal folds a toggle into settings.advertisedExtensions.
Test server + proof:
- composable-test-server extensionGatedTools: a tool registered disabled and
enabled on `initialized` iff the client declared the mapped extension
(getClientCapabilities). Legacy stateful leg. Threaded through
load-config/resolve-config; advertised-extensions-http.json showcase + README.
- Integration test connects legacy with/without the tasks extension advertised
and asserts the gated tool appears/disappears in tools/list — the acceptance
criterion, proven end to end, not asserted.
Tests: serverList round-trip (non-empty/absent/empty), ServerSettingsForm
toggle rows, ServerSettingsModal fold, validateSettings accept/reject, and the
integration gating test. npm run ci green (per-file ≥90 gate held).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* Extensions: add #1739 proof screenshots (advertised-extensions toggle)
End-to-end smoke test in the web client against the legacy
advertised-extensions-http.json test server (port 3220): connecting with the
Tasks extension advertised (default) lists echo + get_weather, unchecking the
Advertised Extensions → Tasks toggle and reconnecting lists only echo — the
server's tool registration changing on the client-declared extension.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* Extensions: reconverge advertisedExtensions to no-override on default (review)
Address Phase 2 review observation #1: toggling an extension back to its
registry default now deletes the override key (and drops the whole map to
undefined when it empties), so the on-disk `advertisedExtensions` reconverges
to the byte-stable "no override" state instead of persisting a redundant
`{ key: <default> }`. Matches the omit-when-default policy the other settings
use. Only a value that differs from the default is persisted.
Adds a modal test asserting the reconvergence (re-checking the sole disabling
override drops advertisedExtensions to undefined).
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>
* 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>
* Tasks: modern io.modelcontextprotocol/tasks extension (era fork core)
Core + test-server implementation of the SEP-2663 tasks extension for modern
(2026-07-28) connections, forked from the legacy 2025-11-25 tasks path.
SDK v2 removed all tasks support AND era-gates the tasks/* spec methods out of
the modern era on both the client (outbound) and server (inbound) sides, and its
codec rejects a resultType:"task" result outright. So the Inspector drives the
extension itself: task creation rides tools/call (a modern method) with the
resultType:"task" frame rewritten at the transport to a CallToolResult carrying
the handle in _meta; tasks/get/update/cancel go over a raw-wire request channel
that bypasses the era gate (full modern envelope, string ids, consumed by the
transport). input_required tasks reuse the existing pending-request modal via
fulfilInputRequests, answered with tasks/update. No tasks/list, no tasks/result.
Tab + run-as-task gate on the negotiated extension; the managed task store's
refresh re-polls known handles on modern (no server list). Test server serves
the extension via an Express interceptor (SDK can't) + a shared poll-driven
runtime. Full integration coverage of both eras.
Refs #1631
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* Tasks modern era: tests + coverage (raw-wire, schemas, era integration)
Adds unit coverage for modernTaskSchemas and the raw-wire request channel
(transport-null guard, send failure, timeout, error response, teardown), plus
end-to-end era integration tests (create/poll/input-required/update/cancel,
synchronous passthrough, unknown-id error, modern store re-poll). Fixes the App
and constructor-capabilities tests for the always-on tasks extension.
Refs #1631
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* Tasks modern era: docs + share test servers per era to de-flake coverage
Document the tasks era-fork test configs in README and the new core/test-server
files in AGENTS.md. Share one modern + one legacy test server across the era
integration block (started lazily, stopped in afterAll) — starting a fresh
Express server per test made connection negotiation flaky under the heavy
concurrent coverage run.
Refs #1631
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* Restore pr-screenshots deleted inadvertently
The logging/subscriptions PR screenshots + README were staged-deleted in the
working tree before this branch; restore them from v2/main so this PR doesn't
remove other PRs' artifacts.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* Tasks modern era: offer Run-as-task for any tool + PR screenshots
On modern (SEP-2663) task creation is server-directed — the client declares the
extension once and any tool may return a task — so the Tools screen now offers
'Run as task' for every tool on a modern connection (threaded via a modernTasks
prop through InspectorView → ToolsScreen → ToolDetailPanel), rather than gating
on the legacy per-tool taskSupport. Verified end-to-end in the web UI (remote
proxy): create → poll tasks/get → input_required elicitation → tasks/update →
inline result, plus the unchanged legacy tasks/list/result flow. Adds proof
screenshots.
Refs #1631
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* Tasks modern era: address two follow-ups (task-input note + ordinary-path auto-poll)
1. The pending-request note now distinguishes a modern task round from an MRTR
round: a new 'task-input-required' PendingRequestOrigin threads through
fulfilInputRequests → the pending-request modal → MrtrOriginNote, which shows
'submitted via a tasks/update request (SEP-2663), not a retry' instead of the
MRTR retry copy.
2. The ordinary (non-run-as-task) modern callTool path now auto-polls an
unsolicited task handle: attemptToolCall detects the rewritten handle in the
result _meta and drives it to termination (get → input_required/update →
inline result), dispatching task events so the Tasks tab + toasts track it and
the tool result resolves to the task's final result. The poll loop is shared
with the streaming path via extracted helpers (submitModernTaskInput /
modernTaskTerminalOutcome / modernTaskPollInterval).
Refs #1631
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* Tasks modern era: address review (negotiated envelope version, input-round cap, clarifying comment) + fresh smoke screenshots
Review of #1731:
1. withModernTaskEnvelope now stamps the NEGOTIATED protocol version
(getProtocolVersion() ?? MODERN_PROTOCOL_VERSION) so the envelope matches the
MCP-Protocol-Version header instead of a hardcoded constant.
2. A never-completing input_required task is now bounded by MRTR_MAX_ROUNDS in the
shared submitModernTaskInput (threaded through both poll loops), so a
non-conformant server can't re-prompt the user forever. New modern_loop_task
test-server tool + integration test cover the cap.
3. ToolDetailPanel: comment clarifying that per-tool taskSupport:"forbidden" is
deliberately ignored on modern (task creation is server-directed under SEP-2663).
(The abort-during-poll observation is addressed in the PR reply: the cancel path
uses tasks/cancel and disconnect rejects the in-flight raw request, so the poll
stops; matches legacy.)
Fresh web-UI smoke screenshots (modern connected, task completed, input_required
with the corrected tasks/update note, input completed).
Refs #1631
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* Tasks modern era: fix uncancelable input-required loop + sticky cancel
Two bug fixes reported during manual testing of the tasks era-fork (#1631):
1. A modern task paused at `input_required` (e.g. `modern_loop_task`, which
never advances) could not be cancelled: the poll loop blocked on the
pending-request modal, so `cancelRequestorTask` sent `tasks/cancel` but the
modal kept re-prompting. Now each modern poll loop registers a per-task
AbortController (`registerTaskInputAbort`), `cancelRequestorTask` aborts it
to reject the pending elicitation and unblock the poll, and the embedded
input requests are tagged with their owning task id
(`RELATED_TASK_META_KEY`) so the pending-request UI's Cancel action cancels
the TASK rather than sending a "cancel" answer the server would re-prompt on.
2. On the legacy server, cancelling a task briefly showed CANCELLED then
flipped to COMPLETED when the in-flight tool call resolved. Cancellation is
a deliberate terminal decision, so `ManagedRequestorTasksState` now remembers
cancelled task ids and ignores any later non-cancelled status update.
Also: move the compact task id onto its own line in the embedded monitoring
sidebar so the status badge / Cancel Task control don't wrap; DRY the duplicated
task poll-interval logic into a shared `taskPollInterval` helper.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* Refresh tasks PR screenshots + capture both bug-fix scenarios
Recapture the five tasks flow screenshots against the current build and add
two demonstrating the post-review fixes: modern_loop_task cancelled from the
pending-request modal (uncancelable-loop fix) and a legacy task staying
CANCELLED after the in-flight call resolves (sticky-cancel fix).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* Address re-review: sticky cancel on legacy refresh + App cancel-routing test
Re-review of the cancelable-loop / sticky-cancel fix raised two non-blocking
hardening items:
1. The legacy tasks/list refresh() rebuilt the list from the server, honoring
only dismissedTaskIds — so a cooperative-cancel server that re-lists a
user-cancelled task as completed could un-stick the cancel on a manual
Refresh. The refresh path now pins a cancelledTaskIds task to "cancelled"
(keeping it visible-as-cancelled, matching the live-merge handlers) rather
than dropping it. Covered by a new state test.
2. Added an App.test.tsx case locking in the user-facing half of the fix: Cancel
on a task-input-required elicitation calls cancelRequestorTask (not respond).
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>
Fork resource subscriptions by protocol era: legacy keeps resources/subscribe; modern (2026-07-28) models subscriptions as a filter over one long-lived subscriptions/listen stream with reconnect-by-re-listen (capped backoff), an optimistic Connecting→Listening header badge, and a fix so the browser proxy no longer blocks listen() ~60s. Closes#1630.
* Logging: per-request log level on modern servers (era fork) (#1629)
On modern (2026-07-28) servers `logging/setLevel` and the session-scoped
level are gone: the client opts into logs per request via the
`io.modelcontextprotocol/logLevel` `_meta` key, and the SDK does not
attach it automatically, so server logs are silently absent until stamped.
Core (`InspectorClient`):
- `setModernLogLevel(level | undefined)` / `getModernLogLevel()` store the
opt-in level; `mergeMeta` stamps it on every outgoing request, gated on
the negotiated modern era so legacy connections are untouched.
- Cleared on disconnect so it can't leak into the next connection.
Web UI:
- `LogControls` forks on `protocolEra`: legacy keeps the `logging/setLevel`
selector + Set button; modern shows a per-request opt-in Select (Off +
levels, applied immediately) with copy explaining logs arrive on the
originating request's stream.
- Threaded `protocolEra` / `modernLogLevel` / `onSetModernLogLevel` through
LoggingScreen and InspectorView from App.
Tests: unit coverage for the modern LogControls/LoggingScreen fork;
integration coverage proving the `logLevel` `_meta` is stamped on a real
modern server once opted in (and not on legacy).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* test-servers: logging era-fork showcase configs + proof screenshots (#1629)
Add two manual-testing configs and README docs for the logging era fork:
- logging-legacy-http.json: plain streamable-HTTP server with `logging: true`
and a `send_notification` tool. The Logs tab shows the legacy session-scoped
"Set Active Level" selector + Set button; calling the tool streams logs.
- logging-modern-http.json: same, with `transport.modern: true`. Connect with
Protocol Era = Modern and the Logs tab shows the per-request opt-in control;
the client stamps `_meta["io.modelcontextprotocol/logLevel"]` on every
request (visible in the Network tab request body).
pr-screenshots/ documents the verified behavior end-to-end against both real
servers: the legacy control with logs streaming, the modern per-request
control, and the modern tools/call request body carrying the stamped logLevel
_meta.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* test-servers: send_notification emits via per-request notifier so modern logs stream (#1629)
The `send_notification` tool sent its `notifications/message` through the
global `server.server.notification()`. On the modern (2026-07-28) stateless
leg that isn't tied to the in-flight request, so the SDK answered the tool
call with plain JSON and dropped the log — nothing reached the Inspector's
Logs panel.
Prefer the per-request `extra.sendNotification` (rebuilt from
`ctx.mcpReq.notify`): scoped to the current request, it makes the SDK upgrade
the response to SSE and stream the log back on the originating request's
stream. Falls back to the global notifier (legacy path / no per-request
context). Verified end-to-end: opting in via the Logs-tab "Log Level per
Request" control and calling send_notification now streams the entry on the
modern server; legacy behavior unchanged (server-notification integration
tests green).
README documents the modern logging showcase and the caveat that this test
server streams whenever a tool emits — the SDK beta strips the per-request
envelope from the handler context, so the tool can't gate on the client's
logLevel opt-in (the client-side `_meta` stamp #1629 owns is still fully
visible in the Network tab).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* test-servers: send_notification uses SDK request-scoped log() → spec-faithful modern gating (#1629)
The previous fix routed `send_notification` through `extra.sendNotification`
(`ctx.mcpReq.notify`), which streams on the modern leg but emits
unconditionally — the log showed up even when the client hadn't opted in via
the per-request `logLevel`.
The SDK already provides the correct primitive: `ctx.mcpReq.log(level, data,
logger)` (added in `McpServer.buildContext`). It applies the era-correct
threshold gating itself — on the modern (2026-07-28) 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` — and emits through
`notify`, so the modern response still upgrades to SSE and the log rides the
originating request's stream.
Surface it as `extra.log` (`toHandlerExtra` maps `ctx.mcpReq.log`) and have
`send_notification` prefer it, falling back to the global notifier for callers
without per-request context. Verified end to end: opting in via the Logs-tab
"Log Level per Request" control stamps `logLevel` in the request `_meta` and
streams the log; setting it back to Off omits the key and the server gates the
log (plain JSON response, no notification). Legacy server-notification
integration tests still green. (No SDK change needed — the earlier apparent
gap was our test server using the wrong low-level API.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* web+core: add "Log Level per Request" as a per-server setting, defaulting to on (#1629)
Adds the modern per-request log level as a persisted Server Settings option
(Options → "Log Level per Request", below Protocol Era), so a server can opt
into logs by default instead of the Logs-tab control starting at Off.
- types.ts: `ModernLogLevel` (`"off"` | the 8 levels), `DEFAULT_MODERN_LOG_LEVEL`
= "debug" (default-on so a modern connection surfaces server logs out of the
box), `MODERN_LOG_LEVELS`, and an `isModernLogLevel` guard. Field added to
`InspectorServerSettings` and `StoredMCPServer`.
- serverList.ts: round-trip the field (lift on read, omit-on-default on write,
drop unknown literals) mirroring `protocolEra`; add to the persisted field
allowlist.
- remote/node/server.ts: validateSettings accepts/validates it and the
normalize path drops a malformed literal.
- inspectorClient.ts: the constructor seeds `modernLogLevel` from the setting
(`"off"` → not opted in), so a modern connection stamps the level from the
first request without UI interaction. mergeMeta still gates on the modern era.
- ServerSettingsForm/Modal: a Select (Off + 8 levels) with a modern-era
description. App seeds the live Logs-tab control from the setting on connect
so the two stay in sync.
Tests: form Select (default/onChange/off/reflect), modal handler, serverList
round-trips, route validation (accept/reject/drop), and constructor seeding
(seeded level stamped from the first request; "off" opts out). The prior
modern-era "stamps once set" test is updated for the new default-on behavior.
Coverage gate green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* Fix CI + review: format ServerSettingsForm, drop stray network export, broaden ignore
- Prettier-format ServerSettingsForm.tsx (the format:check failure that broke
CI — my pre-push `npm run format` from the repo root was a silent no-op since
there is no root format script; formatting is per-client).
- Remove `inspector-network-logging-modern-2026-07-19T15-11-27.013Z.json`, a
Network-tab export debugging artifact accidentally committed in ca2aa526
(flagged by review).
- Broaden the .gitignore debugging-artifacts section with
`/inspector-network-*.json` so future network exports are ignored (the
existing `/inspector-history-*.json` didn't match the network export naming).
Verified with the full `npm run ci` (validate → coverage → smoke → storybook),
green across all clients.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* test: add modern-era log delivery + gating e2e tests (#1629)
The modern-era suite covered the client-side `_meta` logLevel stamp but not
that a log actually flows back — the legacy suite already has that
(`should receive server-initiated notifications via SSE/streamable-http`), so
the modern side was the gap.
Add two live e2e tests against a modern (`createMcpHandler`) server serving the
`send_notification` tool with `logging: true`:
- opted-in (server setting = a level): calling the tool delivers a
`notifications/message` over the request's SSE stream, with the right
level/logger/data.
- not opted in (setting "off"): the tool still returns a result, but the server
gates the log — no `notifications/message` is received.
This exercises the full path end to end: the client seeding + `_meta` opt-in,
and the server's `send_notification` → `ctx.mcpReq.log` era-correct gating.
Verified with the full `npm run ci` (green across all clients).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* web: hide the modern "Log Level per Request" setting on legacy connections (#1629)
The per-request log level is meaningless on a legacy server (which uses
logging/setLevel), so hide it in Server Settings when the era is legacy:
- protocolEra pinned to "legacy" → hidden (no connection needed), or
- protocolEra "auto" that negotiated legacy at connect time → hidden.
It stays visible for a pinned "modern" server, and for an "auto" server that is
either not yet connected (era unknown) or resolved to modern.
Threads the negotiated era down: App passes `negotiatedEra` to
ServerSettingsModal only when the settings target is the live-connected server
(`connectionStatus === "connected" && settingsModalTargetId === activeServerId`);
the modal forwards it to ServerSettingsForm, which gates the control on
`configuredEra === "modern" || (configuredEra === "auto" && negotiatedEra !== "legacy")`.
Tests: visibility matrix on the form (legacy hidden; auto+legacy hidden;
auto+unconnected and auto+modern and modern shown) and a modal hide case; the
existing modern-log tests now base on a modern-era settings object. Verified in
the browser (legacy hides, switching to modern reveals it reactively) and with
the full `npm run ci` (green across all clients).
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>
* Thread cacheMode through managed list refresh via SDK cache-aware verbs (#1721)
Manual/auto refresh of tools/resources/prompts now forces a cache-bypassing
round trip on modern servers, delivering the raw-request cacheMode control
deferred from #1628.
The InspectorClient list/read path uses the SDK's single-page `client.request`
verbs (for pagination debugging), which never consult the SDK response cache —
so `cacheMode` was inert there. This adds a separate aggregate path that routes
through the SDK's cache-aware high-level verbs, where `cacheMode` is honored:
- InspectorClient: new `listAllTools/Prompts/Resources/ResourceTemplates(
{ cacheMode, metadata })` that call `client.listTools()` etc. (all-page
aggregate + SEP-2243 x-mcp-header exclusion + cache consult/write). A shared
`getCacheableRequestOptions` / `aggregateListParams` back all four.
- InspectorClientProtocol: the four `listAll*` methods added.
- ManagedListState: delegates page walking to the SDK verb (config `fetchPage`
loop → `fetchAll`); `refresh(metadata?, cacheMode?)` threads cacheMode. A
`list_changed` auto-refresh bypasses the cache (`cacheMode: "refresh"`).
- Managed hooks: user-initiated `refresh()` passes `cacheMode: "refresh"`, so
the existing per-tab Refresh buttons become genuinely cache-bypassing — no
App wiring change. Connect-time load stays default (`'use'`).
The single-page `listTools(cursor)` primitive is unchanged; the paged hooks
still surface per-page `nextCursor`.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* Single-page list pagination: sidebar toggle + server setting (#1721)
Delivers the deferred single-page fetch UI: Tools/Resources/Prompts can be
loaded one page at a time instead of auto-aggregating every page — a defensive
option for servers with very large lists.
- New `singlePageLists` server setting (persisted, server-wide), plumbed through
ServerConfig/InspectorServerSettings, serverList convert/default, the node +
remote settings paths, and the Server Settings form ("Fetch Lists One Page at
a Time").
- New `ListPaginationControls` element (a "Single page" switch + "Load next
page" button + "N pages loaded" status) rendered in each list sidebar; the
toggle edits the setting live (optimistic) and persists it.
- The paged state stores (PagedTools/Prompts/ResourcesState) now own loading:
they auto-load page 1 on connect in single-page mode and track nextCursor +
page count as observable state; the managed stores skip their all-page walk
in single-page mode (so nothing is over-fetched) while still lighting the
list-changed indicator. `usePaginatedList` selects the display source and
pagination controls per mode — pure, no data-loading in React effects.
- Refresh is mode-aware: reload page 1 in single-page mode, refetch the whole
aggregate otherwise. Sidebars widened 340→360 so the controls don't crowd
list entries.
- Test server: `test-servers/configs/pagination-http.json` (12 tools/resources/
prompts, page size 4 → 3 pages each) exercises the flow end to end.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* Add pr-screenshots proving single-page pagination (#1721)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* Address @claude review: template load, double-click guard, auth recovery (#1721)
- Fix: resource templates never loaded on connect in single-page mode. The
connect-time all-page skip now applies only to lists with a paged counterpart
(new `deferInSinglePage` flag on ManagedListConfig; false for resource
templates), so templates still aggregate on connect regardless of the setting.
- Guard `PagedTools/Prompts/ResourcesState.loadPage` against a concurrent load
(fast double-click on "Load next page") so the same page can't be appended
twice; the in-flight load owns the cursor and the second call is a no-op.
- Thread ambient auth recovery through the single-page refresh, Load-next-page,
and mode-switch loads (they previously bypassed it) — usePaginatedList's
onRefresh/onLoadMore now return their promises so App can wrap them.
- Drop the misleading "aggregated list" wording from the paged-store docs.
Tests added for each; npm run coverage green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* Review round 2: revert optimistic toggle when persisting the setting fails (#1721)
Non-blocking review follow-up. If updateServerSettings rejects, the optimistic
`singlePageListsOverride` is now cleared and the live client setting rolled back
(the effect only clears the override when the persisted value changes, which
doesn't happen on failure), so the UI and client reflect what's on disk rather
than the failed edit. Added an App test for the revert path.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* Fix single-page Refresh not clearing list-changed indicator + UI polish (#1721)
Round-3 review fix (blocking): in single-page mode a `list_changed` lights the
list-changed indicator, but the single-page Refresh reloads page 1 of the paged
store and bypassed `useManaged*.refresh()`, so the indicator was never cleared.
Expose `clearListChanged` from useManagedTools/Prompts/Resources and call it in
the single-page branch of onRefreshTools/Prompts/Resources. Tests added.
UI polish (user feedback on the sidebar control):
- Rename the switch label "Single page" → "Paginated".
- Put "Load next page" on the same row as the switch, space-between (switch
fully left, button fully right).
- Move the "N pages loaded" status to its own line below the row, centered.
- Hide the whole control once the list is known to be a single page
(single-page mode, page 1 loaded, no nextCursor) — nothing to paginate.
pr-screenshots refreshed to the new layout.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* Rename the singlePageLists setting to paginatedLists for clarity (#1721)
`singlePageLists` read backwards from the "Paginated" switch label (it sounds
like paginated=false). Rename the server setting to `paginatedLists` with the
same value meaning — true = paginated/one-page-at-a-time, false = aggregate-all
— and follow through on the related identifiers for consistency:
- Setting field `paginatedLists` across ServerConfig / InspectorServerSettings,
serverList convert/default, node + remote settings paths, and the form/modal
callbacks (onPaginatedListsChange).
- `usePaginatedList` / `ListPaginationControls` prop `singlePage` → `paginated`,
`onSinglePageChange` → `onPaginatedChange`.
- `ManagedListConfig.deferInSinglePage` → `deferWhenPaginated`.
- App state (paginatedListsOverride, onTogglePaginatedLists, …) and all
tests/fixtures + mode-referring comments.
"single-page" wording that denotes the SDK's one-page `client.request` verb
(inspectorClient) is intentionally left as-is. Purely a rename — no behavior
change; unit + integration + cli + tui + coverage all green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* Format App.test.tsx (prettier) after the paginatedLists rename (#1721)
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>
* Network: modern Mcp-* headers, new error codes, cancellation-as-abort (#1628)
Upgrade the Network tab for the modern Streamable HTTP transport
(SEP-2243 / SEP-2575):
- Recognise and highlight the mirrored headers (Mcp-Method, Mcp-Name,
Mcp-Param-*, MCP-Protocol-Version), decode =?base64?…?= sentinel values
inline, and cross-check each against the request body so a HeaderMismatch
is visible before the server rejects it.
- Show the new spec error codes distinctly: -32020 HeaderMismatch,
-32021 MissingRequiredClientCapability, -32022 UnsupportedProtocolVersion
(with the server-advertised `supported` versions), and modern -32601
MethodNotFound on HTTP 404 (distinguished from a legacy bare 404 by its
JSON-RPC body).
- Reflect cancellation as a connection abort ("Cancelled") rather than a
hard error, per SEP §7.5.
- Confirm and document browser-path header mirroring: the SDK builds the
Mcp-* headers client-side and the /api/fetch proxy forwards them verbatim
(new integration test).
New pure util `mcpNetworkHeaders.ts` holds the recognition/decode/classify
logic; new `McpErrorBadge` element renders the distinct error chips. The
optional raw-request controls (cacheMode bypass/refresh, single-page fetch)
are split to #1721 — they are cross-cutting core + feature-tab work beyond
the Network tab.
Also refreshes the AGENTS.md board Status option-ID table (the Building /
MCP Apps Extension columns were removed).
Closes#1628
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* Network smoke-test showcase + fix redaction clobbering JSON-RPC error codes
Adds a composable "modern-network" showcase server for exercising the #1628
Network-tab work end-to-end, and fixes a real bug the smoke test surfaced.
Bug fix (core): the fetch-tracking body redaction masked ANY field named
`code` (targeting the OAuth authorization code), including the numeric
JSON-RPC `error.code`. That destroyed the field the Network tab classifies
the modern spec errors on, so -32020/-32021/-32022/-32601 never rendered
against real traffic. Redaction now only masks STRING values of a sensitive
field (secrets are always strings; a numeric code is never one).
Test-server showcase (test-servers): `modern-network-http.json` serves a
`get_weather` tool with an `x-mcp-header: "City"` annotation plus four
`trigger_*` tools. A new `transport.modern.injectSpecErrors` flag installs a
middleware on the modern leg that answers the triggers with real HTTP status
+ JSON-RPC error bodies (400/-32020, 400/-32021, 400/-32022 w/ `supported`,
404/-32601), so the Inspector's error rendering can be driven live.
Mirroring finding (verified live, comment corrected in createRemoteFetch.ts):
the SDK builds the STANDARD mirrored headers (Mcp-Method / Mcp-Name /
MCP-Protocol-Version) unconditionally — they reach the server through the
web proxy — but gates `Mcp-Param-*` on a non-browser environment
(`detectProbeEnvironment() !== "browser"`), so the web client never emits
them. An x-mcp-header tool is therefore uncallable from the web client
against a strict modern server (it answers -32020); it works from the Node
CLI/TUI. The earlier "mirroring works through the proxy" note was corrected.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* Network: move the spec-error badge off the crowded compact top row
In the narrow monitoring sidebar (embedded layout) the McpErrorBadge shared
the top row with the timestamp/method/category/duration/status chips, which
truncated the method and category ("PO…", "TRANSP…") and clipped the error
name. Render the badge separately from `metaBadges` and place it on the
second row, to the right of the URL, so the top row stays readable and the
error name shows in full. The full-width layout keeps it in the top-right
cluster (where there's room).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* Network: keep the spec error name's casing in McpErrorBadge (no uppercase)
The four spec/SDK error identifiers are PascalCase (`HeaderMismatch`,
`MissingRequiredClientCapability`, `UnsupportedProtocolVersion`,
`MethodNotFound`). Mantine's Badge uppercases label text by default, which
flattened them to a hard-to-read run-together string
("UNSUPPORTEDPROTOCOLVERSION"). Set `tt="none"` so the badge shows the
spec's own casing with word boundaries intact; short badges (POST, status)
keep the default uppercase.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* Network: in the full-width layout, move the spec-error badge to the second row
The full-width entry kept the McpErrorBadge in the top-right cluster, where a
long spec name (e.g. "MissingRequiredClientCapability") crowded the top row
and truncated the method chips and URL. Move it to the start of the second
row (the expand-toggle row) so the top row stays readable; the toggle stays
at the end (`space-between` only when the badge is present). The compact
sidebar already places it on its second row beside the URL.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* Protocol: show Replay as the right-side icon in the wide layout too
The compact (sidebar) ProtocolEntry already renders Replay as an icon next to
the pin/expand icons; the wide layout used a left-aligned "Replay" text
button instead. Make the wide layout match — Replay as the ReplayButton icon
at the start of the right-side action cluster (Replay, Pin, Expand) — so the
two layouts are consistent. Drops the now-unused SubtleButton constant.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* Move the modern spec-error chip + alert from the Network tab to the Protocol tab
The Network tab is focused on the HTTP transaction; the distinct protocol-error
taxonomy (-32020/-32021/-32022/-32601) belongs with the protocol messages. So:
- ProtocolEntry now renders the McpErrorBadge chip on the top line (right after
the direction badge, both layouts) plus an explanatory alert in the expanded
detail, classified by JSON-RPC error code via `classifyProtocolSpecError`.
- NetworkEntry drops the chip + alert; it keeps the HTTP-level features (status
badge, Mcp-* header highlight/sentinel-decode, header/body mismatch marker,
cancellation-as-abort).
- The SDK delivers -32020/21/22 (HTTP 400) as JSON-RPC error responses, so they
reach the Protocol log directly. -32601 (HTTP 404) and other non-enveloped
errors are *thrown* — the request stays pending. `enrichProtocolEntries`
(App) correlates the Protocol log to the Network log by JSON-RPC id (parsed
from the transport fetch's requestBody) and folds the thrown error onto its
pending request, so it too appears in Protocol. This is a derived view; the
underlying log is not mutated.
Verified live against the modern-network showcase: both a -32020 error response
and a thrown -32601 render distinctly in the Protocol tab.
Follow-up: a "reveal in Network" link on the Protocol alert (cross-tab
scroll-to + expand of the correlated HTTP entry).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* Protocol: show the spec-error chip only in the wide layout, after the method
Refine the placement of the spec-error chip added in the previous commit:
- Wide layout: move it from after the direction badge to right after the
method chip (reads as "tools/call -32020 HeaderMismatch").
- Compact sidebar row: drop the chip entirely — the two-line row stays
uncluttered and relies on its ERROR status badge. The explanatory alert
still appears in the expanded detail in both layouts.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* Protocol: "reveal in Network" link — jump from a spec error to its HTTP entry
Adds the cross-tab navigation that was the follow-up to relocating the
spec-error rendering. The expanded Protocol alert now shows a "View the HTTP
request in the Network tab" link when the entry has a correlated Network
request; clicking it:
- correlates the Protocol message to its transport fetch by JSON-RPC id
(`revealableMessageIds` gates the link; `correlateFetchEntry` finds the
target on click),
- clears whatever filter would hide the target (network filterText +
category, and the embedded monitorSearch),
- switches to the Network view in whichever mode is active — the pinned
column (`pinMonitor`) when the sidebar is open, or the full-screen header
tab (`onActiveTabChange`) when it's closed,
- scrolls the matching NetworkEntry into view and force-expands it.
The reveal is a one-shot: a new `revealId` threads NetworkScreen →
NetworkStreamPanel → NetworkEntry, where the targeted entry scrolls itself
into view (rAF, after `useScrollMemory`'s restore) + expands, then clears the
signal via `onRevealComplete`. Verified live in both the pinned-sidebar and
closed/full-screen layouts.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* Protocol: only show the "reveal in Network" link when it adds value
Gate the link on a new `httpRelevant` flag: show it only when the error is
either thrown by the SDK (its real HTTP response lives only in the Network
log — the Protocol entry is a synthetic fold) OR tied to the HTTP request
headers. A delivered, protocol-only error gets no link, since the raw HTTP
entry adds nothing over the alert.
- -32020 HeaderMismatch → link (the mirrored headers are the problem)
- -32601 MethodNotFound → link (thrown; the 404 response is only in Network)
- -32021 MissingRequiredClientCapability → no link (delivered, protocol-only)
- -32022 UnsupportedProtocolVersion → no link (supported list is in the alert)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* Network: cite SEP-2575 for cancellation-as-abort, not the internal doc's §7.5
The cancellation note referenced "(SEP §7.5)", but §7.5 is a section of the
internal v2_new_spec_impact.md doc, not a SEP. The behavior — closing the
Streamable HTTP request stream is the cancellation signal; notifications/cancelled
is stdio-only — is defined in SEP-2575 (stateless-mcp). Cite SEP-2575 in the
user-facing alert and the two related comments.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* Network: fix reveal one-shot canceling its own scroll; review doc/comment nits
Address @claude review of #1722:
- Reveal one-shot: move onRevealComplete() inside the rAF (after scrollIntoView)
so clearing revealId (which flips revealed->false and runs the effect cleanup)
can no longer race/cancel the pending scroll. Add a flip-back test.
- Comment the intentional double-mount of revealId (pinned monitor + full-screen
ScreenStage both match; clear is idempotent).
- Doc drift after the Network->Protocol relocation: McpErrorBadge + the
mcpNetworkHeaders module header now say the spec-error chip lives in Protocol.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* Network: clarify redaction array-of-scalars edge + revealableIds superset
Address second @claude review of #1722 (both non-blocking polish notes):
- fetchTracking redaction: note that masking assumes a sensitive value is a
scalar or object; an array-of-scalars under a sensitive key would slip through
(no OAuth/token payload has that shape).
- revealableMessageIds: document it's a superset of the linked entries; the
spec-error/httpRelevant narrowing is applied downstream at render.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* Protocol: gate modern -32601 framing to a genuine 404, not in-band 200
Address third @claude review of #1722. classifyProtocolSpecError recognised
-32601 from the code alone, so an ordinary in-band 'method not found' returned
on HTTP 200 was dressed up as the modern MethodNotFound taxonomy: the spec
badge, a 404-asserting alert, and a 'reveal in Network' link that jumped to a
plain 200 (claiming diagnostic value that wasn't there).
Gate the modern -32601 treatment on the correlated fetch actually being a 404
(mirroring classifyMcpSpecError). -32020/-32021/-32022 are SEP-reserved and
unaffected. The correlated HTTP status is threaded from InspectorView (new
correlatedFetchStatusById) through ProtocolScreen/ProtocolListPanel to
ProtocolEntry; when unknown, -32601 falls back to code-only recognition (the
thrown-404 path always carries a 404). Badge, alert copy, and link now resolve
together, so a plain in-band -32601 falls through to ordinary error rendering.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* Protocol: require a genuine 404 for modern -32601 (drop unknown-status fallback)
Address fourth @claude review of #1722. The prior fix only rejected -32601 when
the status was *known* and not 404; an unknown status fell back to code-only
recognition. But the thrown-404 path always carries a 404 (folded in from a 404
fetch), so the unknown-status branch is only ever reached by an ordinary in-band
-32601 with no correlated 404 — most notably a stdio connection (no HTTP at all)
— which then wrongly got the modern MethodNotFound badge and 404-asserting copy.
Drop the escape hatch: -32601 is the modern taxonomy only on httpStatus === 404.
Nothing intended is lost (the genuine case always has its 404). Update the tests
(code-only/undefined -32601 now expects null; add a stdio no-correlated-fetch
ProtocolEntry case) and mark the MethodNotFound story with correlatedHttpStatus
404.
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>
* MRTR: drive input_required manually to keep pending-request UX (#1704)
Follow-up to #1627/#1705. Instead of letting the SDK auto-fulfil the modern
(2026-07-28) MRTR retry loop, InspectorClient now drives it itself so the
Inspector keeps its pending-request UX, owns the loop, and shows era-accurate
semantics.
Core (core/mcp/inspectorClient.ts):
- Construct the SDK Client with `inputRequired: { autoFulfill: false }`
(unconditional; a no-op on legacy, which never returns input_required).
- New `requestWithInputRequired()` driver: issues the request with
`allowInputRequired` + `withInputRequired(schema)`, and while the result is
`input_required`, fulfils each embedded request through the SAME pending
sampling/elicitation plumbing, then retries the original call with
`inputResponses` + the echoed `requestState` on a fresh id, bounded by
MRTR_MAX_ROUNDS.
- Route tools/call, prompts/get, and resources/read through the driver.
- Embedded roots/list is auto-answered from configured roots (no pending UX);
decline/cancel is echoed to the server (not treated as an abort); a tool-call
abort mid-round settles the pending request and surfaces
ToolCallCancelledError.
- Extract shared `enqueuePendingSample`/`enqueuePendingElicitation` helpers used
by both the legacy inbound handlers and the driver; add a per-request `origin`
("server-request" | "input-required") to the pending wrappers.
Web UI:
- New `MrtrOriginNote` element shows an "input_required — your answer returns as
a retry" note on a modern MRTR round; legacy panels are unchanged.
- Thread `origin` App -> PendingClientRequestModal -> the sampling/elicitation
panels (and the inline panels).
Test servers: new MRTR presets (multi-round, roots, sampling, loop, edge-case
param shaping). Integration tests cover the manual round-trip (asserting the
pending UI pauses and the retry carries inputResponses + requestState on a new
id), multi-round, roots auto-answer, sampling, decline-echoed, maxRounds bound,
abort mid-round, and requestState-only/inputRequests-only param shaping.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* Review: clarify sampling-reject and between-rounds abort comments (#1704)
Address review nits on PR #1719 (comment-only, no behavior change):
- enqueuePendingSample docstring: sampling has no decline/cancel action; the
panel either sends a result (resolve) or Rejects (reject → fails the call).
The shared "declined/cancelled resolves" wording was accurate only for the
elicitation case.
- callToolWithRetries abort comment: note the between-rounds case — when the
abort lands while an MRTR round awaits an embedded pending request there is no
in-flight SDK request, so nothing is sent on the wire; the outcome
(ToolCallCancelledError, no failed-call record) is identical.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* test-servers: add mrtr-showcase config exercising every MRTR preset (#1704)
A single modern-era streamable-HTTP server (port 3111) bundling all six MRTR
presets — mrtr_confirm, mrtr_two_step, mrtr_sample, mrtr_roots, mrtr_edge,
mrtr_loop — for manual testing of the manual-driving pending-request UX across
single-round, multi-round, sampling, roots, param-shaping, and maxRounds paths.
Document it in the README alongside modern-mrtr-http.json.
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>
* History: MRTR conversation grouping + modern message vocabulary
Teach the Protocol/History view the modern (2026-07-28) message vocabulary,
correlating it off the frames the transport already captures — no change to how
MRTR is driven (manual driving is deferred to #1704).
- Correlation helpers in protocolUtils: extractResultType, extractRequestState,
extractSubscriptionId, isModernFrame, and groupProtocolEntries — clustering the
contiguous rounds of one logical operation (original call -> input_required ->
retried call with a new id -> final result) that share a requestState token.
- New MrtrConversation group renders that cluster as one expandable unit with a
status derived from the final round, reusing ProtocolEntry for each round.
- ProtocolEntry: resultType badge (input required / complete), a "modern" frame
badge for server/discover, subscriptions/listen, and the acknowledged
notification, and a copyable subscriptionId.
- ProtocolListPanel folds each section through the grouping pass and shows an era
badge in the header. Era is threaded from the negotiated protocolEra
(App -> InspectorView -> ProtocolScreen -> panel) and never inferred from
frames, respecting the transcript-classification caveat.
- Shared EraBadge element (+ eraUtils); ConnectionInfoContent reuses it.
- test-servers/configs/modern-mrtr-http.json serves an elicitation tool over the
modern leg for eyeballing a real MRTR round-trip.
- Pre-bundle @modelcontextprotocol/client in optimizeDeps.include so the
Storybook vitest project doesn't re-optimize mid-run once protocolUtils pulls
the client barrel into the browser graph.
Closes#1627
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* test-servers: real MRTR tool + live-verified History grouping
While driving the modern flow in the browser, the elicitation-based config
proved unable to produce MRTR: the modern (2026-07-28) SDK server rejects
`server.elicitInput` — server→client requests aren't allowed on that leg; a
tool must instead return `inputRequired(...)`. So the shipped
`modern-mrtr-http.json` (and its README claim) were wrong.
- Add a genuine MRTR tool preset (`mrtr_confirm` / `createMrtrTool`): its handler
returns `inputRequired({ inputRequests: { confirm: elicit(...) } })` on the
first call and the final result once the client echoes the answer back on the
retry.
- Teach the composable test-server to support it: the tool wrapper forwards an
`InputRequiredResult` verbatim (instead of coercing it to a CallToolResult),
and `toHandlerExtra` surfaces `inputResponses`/`requestState` so a handler can
branch between the original call and the retry.
- Point `modern-mrtr-http.json` at the new preset; correct the README to
describe the real mechanism.
- Add an integration test that drives the full round-trip through InspectorClient
(auto-fulfilling the embedded elicitation) and asserts the final result is only
reachable after the input_required round + retry — regression cover that would
have caught the original config bug.
Verified live in the browser: the History view renders the round-trip as one
grouped "MRTR conversation" (Round 1 input_required → Round 2 complete), keyed by
the real requestState, under the Modern era badge.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* Address review notes: dedupe status badge, unique MRTR token, import alias
Non-blocking review feedback on #1705:
- ProtocolEntry: suppress the redundant green "OK" status badge when a
`resultType` badge already conveys the outcome (a modern success shows only
`complete`/`input required`). Errors/pending have no `resultType`, so their
status badge still shows.
- createMrtrTool: mint a unique `requestState` per original call (closure
counter) so two same-action invocations don't fold into one conversation
during manual eyeballing. Real SDK tokens are already unique per operation.
- ProtocolEntry: align the `MessageEntry` import to the `@inspector/core` alias
used by its siblings (was a deep relative path).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* Review round 2: green `complete` badge, lighter embedded MRTR container
Non-blocking observations from the second review:
- ProtocolEntry: `resultType` "complete" now renders green (was gray) so a modern
success keeps the at-a-glance green success affordance a legacy "OK" had, now
that the redundant OK badge is suppressed. `input_required` stays yellow.
- MrtrConversation: in the narrow monitoring (embedded) column, drop the outer
container border and tighten padding — the per-round entries are already
bordered cards, so this avoids a busy border-in-border nesting. The full-width
view keeps the bordered surface that sets a conversation apart.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* Review round 3: module-scope MRTR counter; revert embedded border change
- test-servers: hoist the MRTR `requestState` counter to module scope. The
modern (2026-07-28) leg is stateless — `createMcpHandler` rebuilds the server
(and every tool closure) per request — so the previous per-closure counter
reset to 0 on every request and every token ended in `:1`, defeating its
purpose. A module-level counter survives the per-request factory recreation
and increments monotonically across the process, so same-action invocations
get distinct tokens and don't fold into one conversation. (Review finding.)
- MrtrConversation: revert the embedded borderless/tighter-padding change — per
maintainer feedback from manual testing, dropping the border didn't improve
the narrow-column visual, so the group keeps its border in all layouts.
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>
* Add modern-era (2026-07-28) test-server preset (#1700)
The era-aware connection model (#1626, PR #1697) added per-server
`protocolEra` and the era-aware Connection Info UI, but no bundled test
server negotiated the modern era — the modern happy path had only
unit/Storybook fixtures, no live coverage.
Serve the modern (2026-07-28) protocol era from the HTTP test server via
the SDK's `createMcpHandler`. A `modern` flag on `ServerConfig` (and a
`transport.modern` field in the JSON config) mounts the handler with a
dual-era `legacy: "stateless"` fallback by default, or `legacy: "reject"`
for modern-only strict serving. The web-standard `fetch` handler reuses
the existing `toWebRequest`/`writeWebResponse` conversion, so no new
`@modelcontextprotocol/node` dependency is needed.
An SDK v2 client negotiating `protocolEra: "auto" | "modern"` now reaches
the modern leg end-to-end: `getProtocolEra() === "modern"`,
`getNegotiatedProtocolVersion() === "2026-07-28"`, and a populated
`getDiscoverResult()`. Verified live in the Inspector — Connection Info
shows Era Modern / Session Sessionless / the Discovery section.
- Modern serving path + `listen`/`startModernHttp` helpers in
test-server-http.ts; modern-leg teardown in stop().
- `modern` config field threaded through load-config/resolve-config with
a streamable-http-only validation guard; modern-http.json demo config.
- Integration test covering auto/modern negotiation, the dual-era legacy
fallback, a modern tools/call, and the strict `legacy: "reject"`
rejection.
Closes#1700
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* Review nit: gate transport.modern guard on truthiness
A falsy `transport.modern` (e.g. `false`) is a no-op that `resolveConfig`
normalizes to `undefined`, so it shouldn't trip the streamable-http-only
guard. Gate on truthiness instead of `!== undefined` so only *enabling*
modern on a non-HTTP transport is rejected.
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>
- Root README no longer links to docs/mcp-server-configuration.md (not yet
ported to v2); keep only the mcp-app-review.md pointer (review #3).
- Collapse the Local-fixture parenthetical onto one line so prettier no longer
renders '- resource preset …' as a mid-sentence bullet (review #5).
Findings #1/#2 (documents unmerged #1576/#1577/#1665 behavior) and #4 (the
web-README #deep-link-auto-connect anchor) resolve in the Wave 4 rollup, which
lands this doc together with those code PRs — the reviewer noted the same.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
Closes#1578
Adds docs/mcp-app-review.md — the CLI-first → one-shot-web recipe for automated
MCP App review: `--app-info` probe → deep-link navigate → rendered widget, plus
OAuth-gated servers (stored-auth handoff, now honoring refresh_token per #1665),
SSH port-forward (both ports), and HTTP proxy support.
Adapted from PR #1510's reference doc to only document what actually merged and
to match the current implementation:
- Correct driver contract: `data-testid="connection-status"` + `data-status`
/`data-error-message`/`data-deeplink`, `data-testid="open-app"`, and the
`data-app-status="ready"` app lifecycle (not the reference's stale selectors).
- The zero-click `autoOpen` path (#1577) alongside the click-to-open variant.
- `--use-stored-auth` now refreshes a stored refresh_token (#1665), so the
"token is not refreshed" caveat is replaced.
The per-client READMEs (web deep-link scheme + data-* contract, CLI handoff +
refresh + proxy) were updated in their own Wave 4 PRs; this adds the root README
pointer to docs/. Proxy/undici is already documented in the CLI README.
Part of the #1579 decomposition (Wave 4, docs — lands last).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
Follow-up on the re-review's two optional notes:
- Set `ENV HOME=/home/node` so the runtime-state dir (core/storage/store-io.ts
resolves it from HOME) is explicit, rather than relying on the "." → WORKDIR
fallback that happens to work because Docker doesn't set HOME on USER.
- Document that the HEALTHCHECK assumes the default --web mode; suggest
`--no-healthcheck` for --cli/--tui runs (no web server to probe).
Re-verified: build ok, HOME=/home/node, runs as node (uid 1000), `/` → 200,
health → healthy.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
v1 published a container image but v2 arrived with no Dockerfile (the
client/server/cli layout is gone). Add a v2 image for the single-package /
launcher architecture and wire the release-gated GHCR publish job.
- `Dockerfile`: two-stage build — stage 1 installs + `npm pack`s the publishable
tarball; stage 2 `npm install -g`s that tarball, so the image ships the exact
same artifact as npm with a clean `mcp-inspector` bin. Defaults to `--web` on
`0.0.0.0:6274`, browser auto-open off. `.dockerignore` keeps the context lean
and forces a clean in-container install/build.
- `publish-github-container-registry` job in main.yml (ported from v1):
release-gated, multi-arch (amd64/arm64) build+push to GHCR with a provenance
attestation. Independent of the npm `publish` job.
- README: document the image + how to run it; the release section now covers
both publish jobs.
Verified locally: `docker build` succeeds, the container serves `/` (HTTP 200
with the injected token) and dispatches `--cli` / `--tui`.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
Wire the v2 publish pipeline into main.yml. On a published GitHub release, a new
`publish` job runs `npm run pack:verify` (the pre-publish gate — builds, packs
the real tarball, installs it into a clean consumer, and drives the bin) then
`npm publish --access public --provenance`. This is a single `npm publish` (v2
is not an npm workspace, so no v1-style publish-all/--workspaces), with a signed
provenance attestation via GitHub OIDC (id-token: write, environment: release,
NPM_TOKEN). Add the `release: types: [published]` trigger; the existing `build`
job is unchanged and the publish job is gated on the release event.
Document the single-version release/tag flow in the root README (one `npm
version` at the root; no per-client sync / check-version since only the root
package.json carries a version).
Docker/GHCR is deferred: v2 has no Dockerfile (the v1 client/server/cli layout
is gone), and a correct image needs a net-new, docker-build-tested Dockerfile.
Split into #1646.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
The Inspector ships as one package with one version, but four client
package.jsons each carried their own `version` and were read at runtime
(cli.ts, tui App.tsx), and core hardcoded a third, stale value (0.20.0) that
the TUI and web actually reported to servers. Consolidate onto the root
package.json as the single source of truth.
- Add core/node/version.ts: shared, Node-only readInspectorVersion() that walks
up to the root package.json (always shipped in the tarball). 100% covered by a
new integration test.
- CLI and TUI read the version via the shared reader instead of their own
manifest; TUI now also passes a real clientIdentity (was falling through to
the stale core default).
- Drop the `version` field from all four client package.jsons; the root keeps
the only version. TUI header name/description become local display constants.
- core default client identity: replace the stale 0.20.0 hardcode with a neutral
0.0.0 placeholder (real clients pass clientIdentity; web will source its
version from GET /api/config, see #1639).
- Stop shipping clients/cli/package.json and clients/tui/package.json (no longer
read at runtime) — supersedes the earlier "ship them" fix; update pack:verify
and the root README packaging invariants accordingly.
- Add core/node/** to the web coverage include so the new module is gated.
- Add the shared-code architecture diagram to the root README.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
CLAUDE.md included @./README.md, which did not exist. Add a root README
covering project layout, setup, the @inspector/core shared package, the web
"dumb components" + Storybook approach, test-servers, build, the testing/quality
gate, publishing, and how AGENTS.md/CLAUDE.md govern contributions for humans
and agents alike.
Move the publishing/packaging documentation (single-package tarball, "files"
invariants, pack:verify) from the launcher README to the root README, where
whole-repo packaging belongs; the launcher README now points there.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
- Changed MCP_SERVER_REQUEST_TIMEOUT from 10s to 5m (300000ms)
- Supports elicitation tools and other long-running operations
- Updated documentation to clarify client-side vs server-side timeouts
- Users can still customize via UI config or URL parameters
- Document --header flag usage with examples
- Explain header format requirements
- Clarify transport support (HTTP, SSE, not STDIO)
- Add authentication examples with API keys and bearer tokens
- Add examples for stdio, sse, and streamable-http transport types
- Document automatic server selection behavior
- Show how to use default-server in config files
Attempts to execute this command will fail: npx @modelcontextprotocol/inspector --cli http://127.0.0.1:8000 --transport http. The error indicates that --method is a mandatory parameter: "Method is required. Use --method to specify the method to invoke. Failed with exit code: 1". That's why the right command may look like this: npx @modelcontextprotocol/inspector --cli http://127.0.0.1:8000 --transport http --method tools/list.
- Extract duplicated URL generation code into getClientUrl() helper function in start.js
- Replace all 127.0.0.1 references with localhost for consistency across codebase
- Update server to respect HOST environment variable for URL generation
- Remove 127.0.0.1 from default allowed origins in CORS configuration
- Update documentation to use localhost instead of 127.0.0.1
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
Complete the security hardening started in e8e9909 by also binding the client to localhost only.
Previously only the server was protected while the client remained exposed to the network,
allowing attackers to access the server through the client as a proxy.
Changes:
- Add HOST environment variable support to client (prod mode)
- Configure Vite dev server to bind to localhost by default
- Update browser auto-open URLs to use actual host instead of hardcoded 127.0.0.1
- Fix missing cancelled parameter in startProdClient function
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Removed outdated statement about auto-open being disabled with authentication
- Clarified that browser automatically opens with token pre-filled in URL
- Updated MCP_AUTO_OPEN_ENABLED description to note it works with authentication
The README now accurately reflects the improved developer experience where
authentication no longer prevents the browser from auto-opening.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Auto-open browser only works when DANGEROUSLY_OMIT_AUTH=true
- When auth is enabled (default), users must use the secure URL with token
- Server displays clear message that auto-open is disabled with auth
- Updates documentation to reflect this behavior
This improves UX by preventing the browser from opening without auth token,
forcing users to use the secure pre-filled URL instead.
- Add origin validation middleware to check Origin header
- Default allowed origins respect CLIENT_PORT environment variable
- Support ALLOWED_ORIGINS environment variable for additional origins
- Apply validation to all protected endpoints before auth check
- Return 403 Forbidden with clear message for invalid origins
- Add DNS Rebinding Protection section to README
This completes the security hardening by preventing malicious websites
from making requests to the local proxy server.