inspector/clients/web
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Cliff Hall 402d7b204d
Server Settings: disambiguate EMA OAuth credential fields (#1692) (#1749)
* Server Settings: disambiguate EMA OAuth credential fields (#1692)

Under enterprise-managed authorization the OAuth Settings "Client ID" /
"Client Secret" fields hold the *resource authorization server* credentials
(EMA leg 3 — the Test Client from the xaa.dev resource registration), not the
app/IdP pair configured in Client Settings. The unqualified labels sat under a
toggle naming the enterprise IdP, pulling readers toward the wrong pair.

- When enterpriseManaged is on, relabel to "Resource AS Client ID" /
  "Resource AS Client Secret" and add a description naming the resource
  authorization server and pointing the app pair back to Client Settings.
- Document the Scopes delimiter: space-separated (RFC 6749), with a warning
  that commas produce one invalid token, plus a placeholder example.

Adds tests for both label states and the scopes hint, plus an
EnterpriseManagedOAuth story.

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

* pr-screenshots: OAuth EMA field disambiguation proof shots (#1692)

Add before/after screenshots of the Server Settings → OAuth Settings fields
(EMA off = plain Client ID/Secret; EMA on = Resource AS Client ID/Secret) plus
the new space-separated Scopes hint, with a README section documenting them.

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

* Address review: genericize EMA copy, reuse enterpriseManaged const (#1692)

- Drop the hardcoded xaa.dev staging host from the resource-AS field
  description (it's a general EMA concept, not env-specific); phrase it as
  "the resource authorization server's registered client credential (EMA leg
  3)". Also trims the copy's overlap with the checkbox description.
- Reuse the existing `enterpriseManaged` const for the EMA Checkbox's `checked`
  prop instead of recomputing `settings.enterpriseManaged ?? false`.
- Update the test assertion and pr-screenshots README/EMA-on screenshot to
  match the new copy.

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

* Client Settings: mirror EMA disambiguation (IdP-qualified labels) (#1692)

The Server Settings OAuth fields now label the leg-3 resource-AS pair
explicitly; make Client Settings parallel so each modal names its pair and
points at where the other lives.

- Relabel the EMA credential fields to "IdP Client ID" / "IdP Client Secret"
  (parallel to Server Settings' "Resource AS Client ID/Secret"), so the label
  alone disambiguates without relying on the description.
- Add the reciprocal cross-reference to each description: these are the
  enterprise IdP pair (EMA legs 1–2), not the per-server resource authorization
  server credentials, which go in Server Settings → OAuth Settings.
- Update ClientSettingsForm/ClientSettingsModal tests for the new labels; add a
  pr-screenshots section + Client Settings screenshot.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 16:51:24 -04:00
..
.storybook web: Accessibility — accessible palette + WCAG AA contrast, labels, names, roles (#1679) 2026-07-13 18:20:55 -04:00
public feat(connection-info): wire Connection Info modal to the connection handshake (#1377) 2026-05-30 16:42:37 -04:00
server History: MRTR conversation grouping + modern message vocabulary (#1705) 2026-07-17 20:02:11 -04:00
src Server Settings: disambiguate EMA OAuth credential fields (#1692) (#1749) 2026-07-23 16:51:24 -04:00
static web: Wave 2 — spec-conformant MCP Apps host (rollup of #1565–#1572) (#1660) 2026-07-11 22:14:20 -04:00
.gitignore feat: port CLI, TUI, and launcher from v1.5 into v2 (#1452) 2026-06-14 20:27:49 -04:00
.npmignore build(publish): fix v2 single-package bundle and add pack:verify 2026-07-10 12:36:26 -04:00
chromatic.config.json add chromatic.config.json 2026-03-22 15:42:09 -04:00
eslint.config.js test(coverage): bootstrap vitest unit infra and add tests at 90%+ per file (#1245) (#1269) 2026-05-01 15:15:32 -04:00
index.html feat(connection-info): wire Connection Info modal to the connection handshake (#1377) 2026-05-30 16:42:37 -04:00
package-lock.json Tools: x-mcp-header annotation tooling + -32602 unknown-tool rendering (#1632) (#1735) 2026-07-22 11:42:54 -04:00
package.json Tools: x-mcp-header annotation tooling + -32602 unknown-tool rendering (#1632) (#1735) 2026-07-22 11:42:54 -04:00
README.md web: deep-link openApp/appArgs/autoOpen — pre-select & pre-fill an app from the URL (#1577) 2026-07-11 22:58:34 -04:00
tsconfig.app.json Auth hardening CI gaps + SDK consolidation: EMA leg 2 / scopes (#1527) (#1725) 2026-07-19 22:36:21 -04:00
tsconfig.json test(coverage): bootstrap vitest unit infra and add tests at 90%+ per file (#1245) (#1269) 2026-05-01 15:15:32 -04:00
tsconfig.node.json Auth hardening CI gaps + SDK consolidation: EMA leg 2 / scopes (#1527) (#1725) 2026-07-19 22:36:21 -04:00
tsconfig.storybook.json fix dark mode, storybook config, and minor UI issues 2026-03-23 13:12:37 -04:00
tsconfig.test.json Removed Zustand storage, cleanup up auth storage IO interfaces, implementations, and usage. 2026-07-04 21:56:45 -07:00
tsup.runner.config.ts SDK v2 migration (behavior-neutral, versionNegotiation: 'legacy') (#1624) 2026-07-15 10:44:24 -04:00
vite.config.ts Tools: x-mcp-header annotation tooling + -32602 unknown-tool rendering (#1632) (#1735) 2026-07-22 11:42:54 -04:00
vitest.shims.d.ts * Added base project to clients/web 2026-03-16 22:47:03 -04:00

MCP Inspector Web Client

The browser incarnation of the Inspector: a Vite + React + Mantine single-page app backed by a small Node (Hono) server. The SPA is presentational — it renders data and fires callbacks; all MCP state comes from the shared @inspector/core hooks. The backend proxies MCP connections, serves the built SPA, and exposes /api/*.

This README covers what's specific to the web client. For the repo-wide picture (the @inspector/core shared package, the "dumb components" philosophy, the top-level validate/coverage/ci scripts, and publishing), see the root README.

Two halves: src/ (browser) and server/ (Node)

Path Runs in Purpose
src/ browser The React SPA — components, hooks, theme, entry (main.tsx).
server/ Node The dev/prod backend wiring (never imported by the browser).

The server/ directory holds the Node-only backend:

  • vite-hono-plugin.ts — mounts the Hono /api/* middleware onto the Vite dev server (so npm run dev has a live backend).
  • server.ts — the standalone Hono production server (serves dist/ + /api/*).
  • run-web.ts / start-vite-dev-server.ts — entry points the launcher calls for prod --web and --web --dev.
  • web-server-config.ts — env parsing, the GET /api/config payload, the startup banner.
  • inject-auth-token.ts — embeds the API token into the served index.html (see Auth token).
  • sandbox-controller.ts — the MCP Apps sandbox HTTP server; ensure-web-build.ts — builds dist/ on demand for prod --web; vite-base-config.ts — shared optimizeDeps exclusions.

Development

npm run dev        # Vite dev server + Hono /api middleware, HMR

For the launcher-driven prod/dev flows (npm run web / web:dev), see the root README — those run the built launcher.

Build

npm run build      # tsc -b  →  vite build  →  build:runner

Two artifacts come out, both of which ship in the published package:

  • dist/ — the browser SPA (vite build). Served statically by the prod backend.
  • build/ — the Node prod-server runner (build:runner = tsup --config tsup.runner.config.ts), which bundles server/ + @inspector/core into one ESM file and externalizes npm deps.

build:client runs only the vite build half when you just need fresh dist/.

Component layers

Components live under src/components/ in four layers, smallest to largest:

Layer Count What it is
elements/ ~31 Leaf presentational pieces (badges, buttons, toggles) over Mantine primitives.
groups/ ~63 Composite pieces (cards, panels, modals, control bars).
screens/ ~11 Full tab screens (Tools, Resources, Servers, monitoring screens…).
views/ 1 InspectorView — the top-level layout that composes the screens.

Every screen and element has a *.stories.tsx (see Storybook). Styling follows the Mantine-first rules in AGENTS.md — theme variants and component props over CSS, --inspector-* tokens over raw colors.

MCP Apps screen automation contract

The Apps screen exposes a small, stable set of data-testid / data-* attributes so an automated driver (deep-link auto-open, CI review harness) can waitForSelector on a deterministic signal instead of sleeping. Treat these as a public contract — drivers depend on them staying stable:

Attribute Where Meaning
data-testid="apps-form" Apps content card The container that carries the status/error attributes below.
data-app-status on apps-form Renderer lifecycle: idle (nothing running) → loading (bridge building / ui/initialize in flight) → ready (view fired notifications/initialized) → error (bridge factory threw/rejected). Poll for ready.
data-app-error on apps-form The failure reason string when data-app-status="error" (e.g. no connected client); absent otherwise.
data-testid="apps-error" error panel Rendered below the frame when the app fails to load (factory throw/reject); shows the reason so the failure isn't a silent blank frame.
data-testid="open-app" Open App button Launches the selected app.
data-testid="apps-stage" Stage-partial button Snapshots the current form values for progressive-render testing.
data-testid="apps-messages" messages panel ui/message submissions from the running view.
data-testid="apps-logs" app-logs panel notifications/message log entries (default-expanded).

The renderer lifecycle itself is AppRendererStatus (loading | ready | error) reported via AppRenderer's onAppStatusChange; the screen maps it to data-app-status. Resource-read failures (malformed/404 UI resource) are surfaced as a toast via the bridge factory's onResourceError; because the app never reaches ready in that case, a driver times out on data-app-status and reads the toast.

A driver (launcher, CLI --print-handoff, CI review harness) can reach a connected inspector with a single navigate by encoding the target in the URL query string. Parsing + security gating live in src/utils/deepLink.ts (parseDeepLink), and a returned DeepLink is proof the link passed validation.

http://127.0.0.1:6274/?serverUrl=<url>&transport=http|sse&autoConnect=<token>
Param Meaning
serverUrl The MCP server URL. Restricted to http: / https: (a crafted javascript: / data: / file: value is rejected). Canonicalized via URL.href so it matches the OAuth store's key form.
transport http (streamable-HTTP, the default) or sse. Unknown values fall back to http.
autoConnect CSRF gate. Must equal the per-launch MCP_INSPECTOR_API_TOKEN. The token is random per launch and only known to whatever started the server, so a third-party-minted link cannot satisfy it — this is the same exposure surface as the existing ?MCP_INSPECTOR_API_TOKEN= param. Without a match the link is ignored.

The deep link upserts a stable deep-link catalog row (so a reload reconnects to the same row instead of accumulating duplicates) and connects. Connection-level outcomes are surfaced on the AppShell.Header as a machine-readable contract, so a driver can waitForSelector and read the failure reason without scraping a transient toast:

Attribute Where Meaning
data-testid="connection-status" header The element carrying the attributes below.
data-status on connection-status The live ConnectionStatus (disconnected → connecting → connected / error). Poll for connected.
data-error-message on connection-status Why the last connect failed (handshake error, OAuth-start failure, deep-link automation failure); absent when there is no error.
data-deeplink on connection-status parsed (a valid deep link drove this load), rejected (deep-link params present but the token/serverUrl gate failed), or none. Lets a driver distinguish "no deep link" from "rejected" — both otherwise leave data-status idle.

Landing on a rendered app

Three further params extend the deep link to pre-select — and optionally auto-open — an MCP App, so a driver reaches a rendered widget with zero clicks:

…&openApp=<toolName>&appArgs=<base64url(JSON)>&autoOpen=<token>
Param Meaning
openApp The app-tool name. Once the connection is up and the tool appears in the app list, the inspector switches to the Apps tab and pre-selects it.
appArgs base64url(JSON) object of form values. Merged over the tool's schema defaults (collectSchemaDefaults) so a required-with-default field isn't left blank — which would otherwise disable "Open App". Malformed / non-object values fall back to {}.
autoOpen Same CSRF gate as autoConnect — must equal the session token. When set, "Open App" fires automatically (a tool call from a URL), so the token gate is mandatory. Without a match the app is pre-selected but not opened.

The app-side render lifecycle is observable through the MCP Apps screen automation contract above (data-app-status="ready"), so a driver can waitForSelector the whole connect → open → ready chain deterministically.

Theme (src/theme/)

Each customized Mantine component has a Theme<Name>.ts file (Button.ts, Text.ts, …, ~21 total) exporting a Theme<Name> constant; the barrel index.ts re-exports them and theme.ts assembles the MantineProvider theme. Theme files hold app-wide defaults and variants (flat CSS-in-JS); only pseudo-selectors, nested child selectors, keyframes, and native-HTML styling belong in App.css. Element components import from @mantine/core (never from theme/) — the theme layer is applied transparently by the provider.

Testing

Tests run under three Vitest projects (configured in vite.config.ts), each in the right environment:

Project Env Scope Script
unit happy-dom Components, hooks, utils (*.test.tsx beside the source) npm test
integration node @inspector/core + transports + auth, spawning the real stdio test server (src/test/integration/**) npm run test:integration
storybook real Chromium Story play functions as interaction tests npm run test:storybook
  • npm test runs the fast unit project (happy-dom). test:watch for the loop.
  • Integration tests run in a real Node env (no happy-dom, 30s timeouts) and spawn test-servers/build/test-server-stdio.js as a subprocess, so pretest/the coverage script build the test servers first (test-servers:build).
  • npm run test:coverage runs unit and integration under v8 instrumentation and enforces the per-file ≥90% gate (lines/statements/functions/branches) — the same gate CI runs. Genuinely-unreachable branches are annotated with a justified /* v8 ignore … */, not waved through.

Integration tests live under src/test/integration/ mirroring the core/ layout; anything placed there is picked up by the integration project automatically. Render components with renderWithMantine (src/test/renderWithMantine.tsx) so they get the project theme.

Storybook

npm run storybook        # dev server on :6006
npm run build:storybook  # static build
npm run test:storybook   # run every story's play function in headless Chromium

Storybook is first-class here because the components are presentational — each renders against fixture props. Play functions double as interaction tests and run headless in real Chromium via @vitest/browser-playwright + @storybook/addon-vitest (the storybook Vitest project). They're part of npm run ci (which installs the Chromium binary first) but kept out of the fast validate loop since they need the browser.

Auth token

The dev/prod backend guards every /api/* route with x-mcp-remote-auth: Bearer <MCP_INSPECTOR_API_TOKEN>. The browser recovers the token, in priority order (see App.tsx getAuthToken()): the window.__INSPECTOR_API_TOKEN__ global injected into index.html on every page load (server/inject-auth-token.ts), then a ?MCP_INSPECTOR_API_TOKEN=… query param, then sessionStorage. Injection is a no-op when auth is disabled (DANGEROUSLY_OMIT_AUTH). See the root AGENTS.md for the full rationale.

HTTP proxy support

The web backend connects to remote MCP servers through the shared Node transport (core/mcp/node/transport.ts), which honors the conventional proxy environment variables: HTTPS_PROXY / HTTP_PROXY (and their lowercase forms) select the proxy, and NO_PROXY exempts hosts. Routing is powered by undici's EnvHttpProxyAgent, imported lazily only when a proxy variable is set, so runs without a proxy configured pay no cost. See the CLI README for more detail.