Commit graph inspector/clients/web/src/components/groups/ConnectionInfoContent/ConnectionInfoContent.stories.tsx
Author SHA1 Message Date
Cliff Hall
405d0f4d0d
Merge pull request #2039 from modelcontextprotocol/v2/chore/1959-no-floating-promises
chore(lint): enable @typescript-eslint/no-floating-promises in all five ESLint scopes
2026-08-16 22:35:52 -04:00
cliffhall
519ecf49ee chore(lint): enable no-floating-promises in all five ESLint scopes
A floated promise is invisible at review time — the call reads like an
awaited one minus four characters — and the unhandled rejection it
produces surfaces in a different test, in a different file, as a stack
pointing at SDK internals. In #1947 two un-held `callTool` calls made
`npm run ci` unpassable and took a full investigation to attribute; the
fix was two lines. `@typescript-eslint/no-floating-promises` catches the
class outright, so enable it at `error` everywhere.

The rule is type-aware and `tseslint.configs.recommended` is not, so each
scope's config gains a parser project. The four clients name their own
tsconfig projects (both of them — `src` and `__tests__`/`server` live in
different ones). The root scope has no tsconfig at all, so add
`tsconfig.lint.json`: a lint-only project covering `core/**`,
`test-servers/src/**`, and `vitest.shared.mts`, emitting nothing and
gating nothing. It sets `moduleResolution: bundler` deliberately — under
NodeNext `core/`'s extensionless imports fail to resolve, every import
degrades to `any`, and the rule silently stops seeing promises at all.

Measured surface, and the fixes:

- core (2) — `stream.close()` in two Hono `onAbort` listeners. Hono
  invokes subscribers with a bare `subscriber()`, so a promise returned
  from the listener is floated one layer up; `closeAbortedStream` owns
  the rejection where it can actually be owned.
- tui (5) — `handleDisconnect` had no catch of its own and its only
  caller is a key handler that cannot await, so a failing disconnect was
  a live unhandled rejection; it now surfaces the message the way
  `handleConnect` does. The rest are `void` at callers that genuinely
  cannot await, over callees that already own their failures.
- web (92) — 83 are Storybook `expect(...)`, which is instrumented and
  returns a promise, now awaited (`scrollAreaStoryAssertions` is async
  for the same reason). Two tests were asserting against a provider call
  they never awaited. `open(url)` in both web servers now warns instead
  of escaping a synchronous callback.
- cli, launcher — already clean.

Type-aware linting costs time: `clients/web`'s `eslint .` goes from ~8s
to ~19s, inside `validate`. Narrowing the loaded projects is the lever if
that needs reducing later; dropping the rule is not.

Closes #1959

Signed-off-by: cliffhall <cliff@futurescale.com>
2026-08-16 20:57:32 -04:00
cliffhall
73913ef09f fix(web): read modern tasks support from the extension map
SEP-2663 moved task support from `capabilities.tasks` to the negotiated
`capabilities.extensions["io.modelcontextprotocol/tasks"]`, but the
Connection Info modal's capability grid tested only for the top-level
key. A modern server that advertised the extension in `server/discover`
therefore showed "Tasks ✗" while the very same extension id was listed
under "Server Extensions" two sections below.

Server capability rows now fall back to an extension id for the
capabilities the modern era expresses that way (tasks is the only one
today), gated on the negotiated era so a legacy connection is still
judged purely on its `initialize` capabilities. Keyed the same way
`InspectorClient.isTasksExtensionNegotiated()` gates the Tasks tab, so
the checkmark and the tab can't disagree.

Signed-off-by: cliffhall <cliff@futurescale.com>
2026-08-16 18:11:29 -04:00
cliffhall
3a22d4eedc fix: disambiguate the two token rows' controls for screen readers (#2019)
Copilot review round 1. With an ID Token row alongside the Access Token
row, both can render a "Decode JWT" toggle and a "Copy" control, and the
four buttons shared two accessible names — button-by-button screen-reader
navigation could not tell which token a control acted on. That collision
is introduced by this PR: before it there was only ever one token row.

- `CopyButton` takes an optional `label` naming what is being copied,
  used for the accessible name only ("Copy ID Token"). The visible
  tooltip stays "Copy", and the name keeps "Copy"/"Copied" as its first
  word so it still contains the visible text (WCAG 2.5.3 Label in Name).
- `OAuthTokenField` qualifies its decode toggle the same way ("Decode JWT
  for Access Token") and passes its row label down to `CopyButton`.

The visible UI is unchanged — this is accessible-name only.

Signed-off-by: cliffhall <cliff@futurescale.com>
2026-08-15 21:13:11 -04:00
cliffhall
07cafeedbc feat: surface and decode the OAuth id_token in Connection Info (#2019)
When an authorization server returns an `id_token` alongside the access
token, the Inspector already persisted it and never showed it. Wire it
through to Connection Info as a viewer.

- Generalize `OAuthAccessTokenField` into `OAuthTokenField` (label + token),
  so the ID Token row reuses the existing copy control and "Decode JWT"
  toggle rather than duplicating them. Access-token behavior is unchanged.
- `oauthDetailsFromConnectionState` now surfaces `tokens.id_token` as
  `idToken`, omitted entirely when absent so no empty row renders.
- Docs: update the two `specification/` references to the renamed field.

This is deliberately a viewer, not a flow: no OIDC login mode, no signature
verification, and the `id_token` is never treated as a credential. The EMA
path is untouched. `id_token` is already in `maskSecrets`'
`JSON_SENSITIVE_KEYS`, so it stays masked in the Network tab — no gap.

Signed-off-by: cliffhall <cliff@futurescale.com>
2026-08-15 18:33:41 -04:00
Cliff Hall
820d38c438
fix(web): render the connected header when a modern server omits serverInfo (#1773)
A modern-era server/discover makes serverInfo optional (SHOULD, not MUST); a conforming modern server that omits it left initializeResult undefined, collapsing the entire connected header + tab bar so the server was uninspectable. Gate initializeResult on connectionStatus alone with a catalog-name fallback, and thread a serverInfoReported flag so the Connection Info modal reports the omission truthfully instead of presenting the inferred name as server-sent. Closes #1772.
2026-07-25 11:40:31 -04:00
Cliff Hall
e2c75da5ce
Extensions: Connection Info both eras + io.modelcontextprotocol/ui advertisement (Phase 3 of #1633) (#1744)
* Extensions: Connection Info both eras + io.modelcontextprotocol/ui advertisement (Phase 3 of #1633)

Completes the Extensions surfacing and advertises the MCP Apps UI extension.

core/mcp/extensions.ts:
- Add UI_EXTENSION_KEY ("io.modelcontextprotocol/ui") and MCP_APP_MIME_TYPE
  ("text/html;profile=mcp-app") — mirrors of ext-apps' EXTENSION_ID /
  RESOURCE_MIME_TYPE (hardcoded: the constants live on the package's /server
  subpath or an extensionless re-export that doesn't resolve under NodeNext).
- Registry entries gain an optional `advertisement` value; the builder stamps
  it (default {}). Add the `ui` entry, default-advertised with
  `{ mimeTypes: ["text/html;profile=mcp-app"] }` — the Inspector always renders
  App tools. No ext-apps cast needed: the extensions value type is `object`.

ConnectionInfoContent:
- Source server extensions from the negotiated `capabilities.extensions`
  (getServerCapabilities, era-transparent) instead of the modern-only
  discoverResult, so a LEGACY server advertising extensions renders them too.
- Add a two-column "Server Extensions" / "Advertised Extensions" section (the
  Inspector's own advertised map), mirroring the server/client capability
  columns. Drop the now-redundant Extensions row from the modern Discovery
  section (Supported versions stays).

Tests: extensions.test.ts covers the ui entry + advertisement value and the
per-extension enable/disable; ConnectionInfoContent.test.tsx covers legacy
server extensions (no discovery), the advertised-extensions column, and the
em-dash empty states. npm run ci green (the one InspectorView localStorage
failure is a known flake, passes on re-run).

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

* Extensions: address Phase 3 review (mime drift-guard, story, exact em-dashes)

- extensions-mimetype.test.ts (integration/node): assert MCP_APP_MIME_TYPE
  equals ext-apps' real RESOURCE_MIME_TYPE (imported) and that the client
  advertises it — a genuine drift guard, replacing the tautological literal
  check in the unit test (which now just asserts the type).
- ConnectionInfoContent Modern story: carry the tasks extension on
  initializeResult.capabilities too, so the Server Extensions row demonstrates
  it (the row now reads negotiated server capabilities, not discoverResult).
- Tighten the em-dash assertions from ">= N" to exact counts (3/3/2) now that
  the two extension sections em-dash by default.

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

* Extensions: drift-guard UI_EXTENSION_KEY against ext-apps EXTENSION_ID (review)

Address the Phase 3 re-review nit: the extension id was the other half of the
same drift risk (a server keys its Apps lookup on it). Import ext-apps'
EXTENSION_ID from the /server subpath in the node integration test (fine there;
the browser build is what avoids /server) and assert UI_EXTENSION_KEY === it.

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

* Extensions: clone the registry advertisement so builds never alias it (review)

Address the Phase 3 re-review's optional defensive nit: buildClientExtensions
returned the same object reference as the ADVERTISABLE_EXTENSIONS entry for the
ui advertisement, so an in-place mutation of a stamped value (e.g.
extensions[ui].mimeTypes) could corrupt the registry for later connections.
structuredClone the advertisement on stamp; add a test asserting a mutated
build doesn't leak into the next one. Nothing mutates it today — purely
defensive.

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

* Extensions: add #1740 Connection Info proof screenshots

End-to-end smoke test in the web client against the legacy
advertised-extensions-http.json server: the Connection Info modal shows the new
era-transparent Server Extensions / Advertised Extensions section, with
Advertised Extensions listing io.modelcontextprotocol/tasks + io.modelcontextprotocol/ui
(the new ui advertisement), and only ui after toggling Tasks off and reconnecting.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 10:11:36 -04:00
Cliff Hall
e026051b47
Era-aware connection model & capability gating (#1626) (#1697)
Some checks failed
Run install, format, lint, build, and test on every push / build (push) Has been cancelled
Run install, format, lint, build, and test on every push / publish (push) Has been cancelled
Run install, format, lint, build, and test on every push / publish-github-container-registry (push) Has been cancelled
* Era-aware connection model & capability gating (#1626)

Make protocol era (SEP §7.8) a first-class, per-server connection dimension,
orthogonal to the transport type, and drive the SDK Client's versionNegotiation
from it.

Config
- Add `ServerProtocolEra` ("legacy" | "auto" | "modern"), `DEFAULT_PROTOCOL_ERA`
  (legacy, per the SDK's don't-auto-probe guidance), `MODERN_PROTOCOL_VERSION`,
  and `eraToVersionNegotiation()` in core/mcp/types.ts.
- Persist `protocolEra` on `StoredMCPServer` / `InspectorServerSettings`,
  round-tripping through serverList.ts (omitting the default on disk) and the
  `/api/servers` read + write validators.

InspectorClient / state
- Thread `versionNegotiation` through `InspectorClientOptions` (default legacy),
  replacing the hardcoded pin. After connect, read `getProtocolEra()` /
  `getDiscoverResult()` / `getNegotiatedProtocolVersion()`, expose them via
  getters, dispatch `protocolEraChange` / `discoverResultChange`, and clear on
  disconnect. `useInspectorClient` surfaces `protocolEra` / `discoverResult`.

Web UI
- ServerSettingsForm: a Protocol Era selector bound to the per-server setting.
- ConnectionInfoContent: era-aware — Era badge, Session ("Sessionless" on
  modern), and a Discovery section (supported versions + extensions) from the
  `server/discover` result.

CLI/TUI also map the mcp.json `protocolEra` into versionNegotiation so the field
applies uniformly. Version negotiation stays fully client-side; modern connect
works over the existing remote tunnel and disconnect is sessionless (the SDK
skips DELETE when there is no session id). Mcp-* header fidelity is left to the
History/Network card.

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

* Address @claude review: era docstrings, era validation, stdio session label, AGENTS.md rule

- Fix stale docstrings/comments in inspectorClient.ts, useInspectorClient.ts,
  inspectorClientEventTarget.ts, and ConnectionInfoContent.tsx: getProtocolEra()
  returns "legacy" (not undefined) on a plain legacy connect; it is undefined
  only when not connected.
- Validate protocolEra at the storedFieldsToInspectorSettings read boundary
  (new shared isProtocolEra guard) so a hand-edited mcp.json read directly by
  the CLI/TUI drops an unknown era instead of passing it to
  eraToVersionNegotiation. Keeps the exhaustive switch intact.
- ConnectionInfoContent: the Session row is HTTP-only — show "N/A (stdio)" for
  stdio instead of the misleading "Session-based".
- AGENTS.md: add a rule against unjustified `as unknown as` double casts, and
  drop the one such cast introduced in the serverList test.

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

* Address re-review: dedupe isProtocolEra guard in remote server

Replace the local isProtocolEra in remote/node/server.ts with the shared guard
now exported from serverList.ts (already imported here), so the era literal set
lives in one place and can't drift.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 23:39:29 -04:00
cliffhall
19a6df7ad3 SDK v2 migration (behavior-neutral, versionNegotiation: 'legacy') (#1624)
Migrate all three clients + core + test-servers from
@modelcontextprotocol/sdk@1.29 to SDK v2 (2.0.0-beta.4), keeping behavior
identical against legacy (2025-11-25) servers. Foundation for the SDK V2 +
New Spec stack.

Packages
- Split @modelcontextprotocol/sdk -> @modelcontextprotocol/client (Client,
  transports, OAuth, error taxonomy, protocol types) + core (Zod schemas);
  stdio -> client/stdio, Ajv -> client/validators/ajv. Test-servers ->
  @modelcontextprotocol/server (+ server-legacy/sse). Deterministic import
  rewrite (schemas -> core, everything else -> client); the codemod misroutes
  client code to /server so it was not used for path routing.
- new Client(..., { versionNegotiation: { mode: 'legacy' } }) — nothing
  2026-era goes on the wire.

Runtime (core/mcp/inspectorClient.ts)
- Error taxonomy: McpError -> ProtocolError, ErrorCode -> ProtocolErrorCode;
  RequestTimeout -> SdkError/SdkErrorCode.
- Handlers take method strings; task methods (tasks/*, notifications/tasks/*)
  use the 3-arg custom form (excluded from v2's spec-method set).
- Legacy tasks kept working (per project requirement): requestor tasks re-
  implemented over raw client.request + explicit deprecated schemas;
  experimental.tasks.callToolStream replaced by a local pollTaskToolCall
  async generator (with _progressHandlers revival so task-execution progress
  still reaches the caller). Receiver (server-initiated) tasks: the v2 Client
  validates a sampling/elicit handler result and rejects the legacy { task }
  response with -32602 — installReceiverTaskResponseBypass swaps the wrapped
  entry in the private _requestHandlers map for the task-augmented branch so
  { task } reaches the wire (fixes server-initiated sampling-via-task).
- Pagination: list verbs use raw client.request({method}, List*ResultSchema)
  for single-page behavior (v2's client.listX() auto-aggregate all pages).

Web / auth / apps
- Rename local JsonSchemaType -> InspectorFormSchema + add toFormSchema()
  structural narrowing at the SDK-schema -> form boundary.
- ext-apps still peers on SDK v1: one compat boundary with documented casts
  (remove when ext-apps#702 lands a v2 peer).
- OAuth provider saveClientInformation reconciled with v2's
  OAuthClientInformationContext; InvalidClientMetadataError re-homed as a
  local class (server-legacy/auth is not browser-safe).
- vitest.shared.mts dedupes @modelcontextprotocol/client+core to one copy so
  vi.mock intercepts the SUT's import.

Accepted v2 behavior changes (per #1624 / spec §9.2): unknown-tool call
rejects with -32602; cancellation is stream-abort (no guaranteed
notifications/cancelled frame); output-schema strictness restored via the
Inspector's own Ajv check on the default path.

npm run ci green: validate (web 3087 / cli 212 / tui 274 / launcher 5),
coverage (>=90 per-file, all clients), smoke (launcher/cli/web), storybook
(410).

Closes #1624

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 10:44:24 -04:00
Bob Dickinson
5de4e911e3
feat(web,core): enterprise-managed authorization (EMA) (#1509) (#1512)
Adds enterprise-managed authorization (EMA): browser-side IdP login + RFC 8693 token-exchange and JWT-bearer resource-token mint, per-server EMA opt-in in Server Settings, IdP config in Client Settings, and IdP clientSecret keychain storage. Includes the v2/main merge and review follow-ups (400 on malformed client config; normalizeIdpIssuer import consolidation).
2026-06-24 22:15:12 -04:00
Cliff Hall
e3b0b6a908
feat(connection-info): wire Connection Info modal to the connection handshake (#1377)
* 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>
2026-05-30 16:42:37 -04:00
Renamed from clients/web/src/components/groups/ServerInfoContent/ServerInfoContent.stories.tsx (Previous history)