Closes#1323. Adds an `error`-event-driven `lastError` on useInspectorClient and a mid-session failure toast in App.tsx; the transport onerror is scoped to non-handshake (status !== connecting) so handshake failures stay on the connect() rejection path. Follow-up: #1490.
* feat(web): show negotiated MCP protocol version on ServerCard (#1324)
Add optional protocolVersion to ConnectionState and render it as a
dimmed "MCP <version>" label at the end of the transport-badge/
description row on ServerCard, shown only when connected. The value
is plumbed through useInspectorClient as part of #1324.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(core,web): surface negotiated protocolVersion end-to-end (#1324)
Plumb the MCP protocol version negotiated during initialize from the
InspectorClient through to the ServerCard so the active connected card
displays "MCP <version>".
The SDK Client doesn't expose protocolVersion; it stamps it onto the
transport via setProtocolVersion. MessageTrackingTransport now captures
it (and forwards to the base transport when present), so it works for
stdio too — whose base transport has no setProtocolVersion.
- core: MessageTrackingTransport captures + exposes protocolVersion;
InspectorClient stores it, exposes getProtocolVersion(), dispatches a
protocolVersionChange event, and clears it on disconnect.
- core: useInspectorClient subscribes and returns protocolVersion.
- web: App reads it from the hook (drops the hard-coded "2025-06-18")
and InspectorView splices it onto the active server's connection.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(web): decouple initializeResult from protocolVersion (#1324 review)
Gate the initializeResult memo on serverInfo only, not protocolVersion.
initializeResult also drives the connected ViewHeader and Connection Info
modal, so requiring the version risked hiding the whole connected surface
in the edge case where it's absent. protocolVersion now flows through as
the optional field it already is everywhere downstream ('' reads as
unknown; the ServerCard label and modal value both tolerate it).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(web): drop redundant connected-check in InspectorView splice (#1324 review)
initializeResult is derived from connectionStatus in App's memo, so a
truthy initializeResult.protocolVersion already implies a live
connection — the extra connectionStatus === 'connected' guard in the
splice was redundant. ServerCard keeps its own status guard (dumb-
component robustness). No behavior change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(server-info): wire Server Info modal to the connection handshake
Closes#1364
- Add ServerInfoModal wrapping the existing ServerInfoContent, mirroring the
ServerSettingsModal layout.
- Gate the Server Info button on ServerCard to render only when the
connection is "connected".
- Expose `getClientCapabilities()` on InspectorClient (and protocol +
FakeInspectorClient), and surface it through useInspectorClient so the
modal can render the client side of the capability handshake without
poking the SDK Client's private state.
- App.tsx: open the modal from the gated Server Info button, derive the
active server's transport and OAuth details (synchronously, from the
guided-OAuth state snapshot + persisted settings), and reset the open
flag on InspectorClient `disconnect` so the modal does not auto-reopen
on a future reconnect.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(server-info): dedupe modal header and render instructions in ContentViewer
- Drop the in-content "Server Information" Title from ServerInfoContent and
rename the modal title to "Server Information" so the modal renders one
heading instead of two stacked ones.
- Swap the instructions Blockquote for ContentViewer (copyable, with the
existing wrapping Code variant) so long instructions wrap and the user
can copy the full payload.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(server-info): rename ServerInfo -> ConnectionInfo
The popup covers server identity AND client capabilities AND OAuth state —
it's a snapshot of the live connection, not just the server. Rename to match
what it actually shows. Also add a "Server Implementation" heading above
the top section (name/version/protocol/transport) to balance the existing
capability headings.
- ServerInfoContent -> ConnectionInfoContent (directory, file, exports)
- ServerInfoModal -> ConnectionInfoModal (directory, file, exports)
- ServerCard button label "Server Info" -> "Connection Info";
prop onServerInfo -> onConnectionInfo
- Modal title -> "Connection Info"
- App.tsx / ServerListScreen / InspectorView wiring updated to match
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(connection-info): cap instructions block height so it scrolls in place
A long server prompt (e.g. an Everything-style instruction document) was
pushing the OAuth section and the modal chrome off-screen — users had to
scroll the whole modal to reach the close button. Wrap the ContentViewer
in ScrollArea.Autosize with mah=280 so the instructions section scrolls
internally and the rest of the modal stays put.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(connection-info): add subtle background to section headings
Add a `section` variant for Title that paints the heading in the existing
`--inspector-surface-subtle` token (same surface token already used for
modal call-out panels). Apply it to each section heading in
ConnectionInfoContent (Server Implementation, Server Capabilities, Client
Capabilities, Server Instructions, OAuth Details) via a SectionHeading
subcomponent constant, so the sections read as distinct cards against the
modal background instead of bleeding into one stream of text.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(stories): wrap long import line in ConnectionInfoModal.stories.tsx
Prettier flagged the import in CI. The line exceeded printWidth=80 and
needed to wrap onto multiple lines.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(agents): require `npm run format` before committing
Add a pre-commit instruction to run the auto-fixing `npm run format` ahead
of `npm run validate`. `validate` runs `format:check` (non-fixing) which
will fail CI on any unformatted file — running the auto-fixer first
eliminates that class of failure entirely.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(favicon): use theme/assets/MCP.svg as the favicon source of truth
Point index.html's icon link at /src/theme/assets/MCP.svg so the same MCP
logo asset already imported by ViewHeader is also the favicon. Delete the
now-unused clients/web/public/favicon.svg. Vite resolves the source path
during build and emits a hashed copy under dist/assets/.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(server-card): surface failed-handshake errors with a 5s auto-dismiss
Two related fixes so the user actually sees what went wrong when a
connect attempt fails (e.g. OAuth-backed server, bad URL, missing dev
auth token):
- App.tsx: add `connectInFlightRef` and skip clearing `activeServerId`
in the InspectorClient `disconnect` listener while a connect attempt
is in flight. The transport closes during a failing handshake and
dispatches `disconnect` before the catch block sets `errorMessage`;
without this guard InspectorView would map the card to plain
"disconnected" and the InlineError would never render.
- InlineError: opt-in `autoDismissMs` prop that runs a Mantine `slide-up`
Transition exit animation after the message has been visible for the
given duration. Visibility is derived from a "dismissed message"
state (rather than a mounted boolean + setMounted-in-effect), so the
timer re-arms whenever `error.message` changes without tripping the
react-hooks/set-state-in-effect rule. ServerCard passes
autoDismissMs={5000}.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(server-card): own InlineError visibility + animate parent-driven removal
Move the 5s auto-dismiss timer and visibility state out of InlineError
and into ServerCard so the slide-up exit animation runs in both cases:
- Timer-driven: after 5s the alert animates out (existing behavior).
- Parent-driven: when `connection.error` is cleared mid-flight (e.g. a
successful reconnect), the alert ALSO slides up instead of vanishing.
To keep the message painted during the parent-driven exit, ServerCard
tracks a `lastError` state that mirrors `connection.error` only on its
truthy edges. The conditional setState during render is the supported
React pattern for "remember the last seen value of a prop"; gated on
message inequality so it can't loop. The `useRef` version of this
pattern failed `react-hooks/refs` (no ref writes/reads during render).
InlineError is now a pure presentational component: `mounted` prop
controls a Mantine `slide-up` Transition. The internal `autoDismissMs`
prop is gone — its single caller has moved to the controlled `mounted`
pattern.
Closes the "error vanishes the moment the parent removes it" half of #1364
(the timer half landed in 6d147e0f).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(server-card): surface connection errors as a toast, drop InlineError
Move handshake-failure messaging out of the ServerCard body and onto a
Mantine toast (`notifications.show`). The inline alert had a 5-second
visibility contract entangled with InspectorView's status pipeline, and
the active card was flipping through error → disconnected → error during
a failing handshake; surfacing the error as a toast sidesteps that whole
timing problem and keeps the card focused on the live status indicator.
- App.tsx: toast on the connect-catch path; drop `errorMessage` state +
`connectInFlightRef` (the latter was only there to keep the inline
alert visible past the transport's close event).
- InspectorView: drop the `errorMessage` prop and the
`connectionStatus === "error" && errorMessage` mapping branch.
- ServerCard: drop the InlineError rendering + the displayedError /
errorVisible visibility state machine.
- Delete the InlineError component — no other consumers.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(notifications): anchor toasts to the bottom right
Move the Notifications portal from top-right to bottom-right so a
connection-failure toast doesn't overlap the ViewHeader / tabs row.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* review: address PR feedback on the Connection Info modal
1. Hoist `EMPTY_CLIENT_CAPABILITIES = Object.freeze({})` to module scope
in `useInspectorClient` so the fallback returns a stable reference
(avoids invalidating downstream `useMemo`/`useEffect` deps when no
client is attached).
4. Drop the `?? "stdio"` silent fallback on `connectionInfoTransport`
in the JSX — gate the modal render on `activeServer` too. The
"stdio" default still lives at the derivation site (config.type is
schema-optional and `ServerType` is required by the modal prop),
but the gate makes the invariant explicit.
6. Replace the raw `document.querySelector("button.mantine-CloseButton-root")`
in `ConnectionInfoModal.test.tsx` with `screen.getByRole("button",
{ name: /close/i })`. Add `aria-label="Close"` to the
CloseButton so the assertion (and screen readers) have a name to
match on.
7. Switch the OAuth scope splitter from `/\s+/` to the literal `" "`
per OAuth 2.1 §3.3.
Also clarify in comments:
2. Note that `connectionInfoOAuth` uses snapshot-at-open semantics on
purpose — token refreshes mid-session won't re-render the modal.
5. Note that we prefer the AS-advertised `authorization_endpoint`
over `authorizationUrl` because the latter is per-flight noise
(state, code_challenge) rather than a stable identifier.
Finding 3 (modal exit animation short-circuited by the `{initializeResult
&& …}` guard) left as intentional — we want the snap-shut on disconnect
so a stale info dialog can't keep rendering with no live client behind it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(connection-info): tighten the OAuth memo comment per re-review
The reviewer noted that "snapshot-at-open" was imprecise — the memo
re-runs on any of its deps (connectionStatus, inspectorClient,
activeServer), which includes settings edits that re-reference the
active server. Reframe as "snapshot-at-last-derivation" and call out
the natural triggers (server switch, reconnect, settings edit) so a
future reader doesn't conclude the memo is open-only.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(core): port v1.5 hook layer (useInspectorClient + useManaged*/usePaged* + log hooks)
Wholesale port of v1.5's React hook layer and the EventTarget-based state machinery the hooks subscribe to (#1243). Hooks decouple from the concrete InspectorClient via a new `InspectorClientProtocol` interface so this layer can land before the runtime class is ported.
Added under `core/`:
- `core/react/` — useInspectorClient + 5 useManaged* + 5 usePaged* + 3 log hooks
- `core/mcp/state/` — 5 managed*State + 5 paged*State + messageLogState + stderrLogState + fetchRequestLogState
- `core/mcp/inspectorClientProtocol.ts`, `inspectorClientEventTarget.ts`, `typedEventTarget.ts`, `samplingCreateMessage.ts`, `sessionStorage.ts`
- v2-only wrapper types in `core/mcp/types.ts`: InspectorUrlElicitRequest, InspectorPendingRequest, InspectorRequestHistoryItem
Tests live under `clients/web/src/test/core/` (28 new test files, 264 new tests). Test fake: `core/mcp/__tests__/fakeInspectorClient.ts`.
Build wiring:
- `vite.config.ts`: dedupe react/react-dom so core/ files resolve them via clients/web/node_modules.
- `tsconfig.app.json` / `tsconfig.test.json`: paths map react/react-dom/vitest to clients/web/node_modules so `tsc -b` resolves bare imports from core/ source files (which sit above the package's node_modules). Exclude core/__tests__ from app config; include in test config.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* review: address PR #1300 review findings
Bugs:
- fetchRequestLogState: add .catch() on sessionStorage.loadSession so a corrupt
or unreadable session doesn't surface as an unhandled rejection (matches the
saveSession swallow pattern).
- messageLogState: sync pendingRequestEntries when clearMessages() evicts a
pending request entry, so a later response with the same id falls through
to the append branch instead of mutating an unreachable map entry. Add tests
for both predicate and no-predicate paths.
Cleanups:
- Extract mergeTaskIntoList helper to core/mcp/state/mergeTaskIntoList.ts so
managed/paged RequestorTasks state classes share one definition.
- Add a one-line JSDoc note on each PagedTools/Prompts/Resources/Templates
state explaining why they deliberately do not subscribe to *ListChanged
(cursor invalidation mid-pagination).
- Document on useInspectorClient that appRendererClient is read lazily on
every render and is not subscribed (changes once at connect time).
Carry-overs from v1.5 deliberately left for the follow-up InspectorClient
runtime port:
- Refresh/status-change race in Managed*State.refresh() (status check is only
at entry; a disconnect during await can re-clear before refresh resumes).
- nextCursor exposure asymmetry across usePaged* (only usePagedRequestorTasks
surfaces it via state field; others return it from loadPage() only).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>