| Filename | Latest commit message | Latest commit date |
|---|---|---|
* chore(web): format-gate the ungated first-party TS/JS (#1778) Prettier only covered core/ and each client's src/, leaving the web Node backend (clients/web/server/**), web top-level configs (vite.config.ts, tsup.runner.config.ts, eslint.config.js, ...), and the root build/verify tooling (scripts/**/*.mjs) ungated — so their formatting was never checked by validate/CI. eslint already reached these via `eslint .`, so this is a prettier-only gap. - clients/web format/format:check now cover src, server, and top-level *.{ts,js}, not just src. - New root format:scripts / format:check:scripts for scripts/**/*.mjs, folded into the root format and into validate:core. - Normalized the newly-covered files with prettier --write (pure formatting; no runtime change). Closes #1778 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5 * chore: future-proof the scripts format glob (#1778 review) Widen format:scripts / format:check:scripts from "scripts/**/*.mjs" to "scripts/**/*.{mjs,js,cjs,ts}" so a future non-.mjs file under scripts/ is auto-gated, matching the future-proof glob style clients/web now uses. A brace glob is a single pattern, so prettier does not error on the unmatched branches. Addresses observation #2 from the PR #1788 review. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5 * docs: sync AGENTS.md scripts glob after the widening (#1788 review r2) d86d8f7 widened the scripts glob to {mjs,js,cjs,ts} and updated the validate:core bullet, but the format bullet at AGENTS.md:217 still said scripts/**/*.mjs. Bring it in line so the two mentions agree. 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> |
||
| .. | ||
| .storybook | ||
| public | ||
| server | ||
| src | ||
| static | ||
| .gitignore | ||
| .npmignore | ||
| chromatic.config.json | ||
| eslint.config.js | ||
| index.html | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| tsconfig.app.json | ||
| tsconfig.json | ||
| tsconfig.node.json | ||
| tsconfig.storybook.json | ||
| tsconfig.test.json | ||
| tsup.runner.config.ts | ||
| vite.config.ts | ||
| vitest.shims.d.ts | ||
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 (sonpm run devhas a live backend).server.ts— the standalone Hono production server (servesdist/+/api/*).run-web.ts/start-vite-dev-server.ts— entry points the launcher calls for prod--weband--web --dev.web-server-config.ts— env parsing, theGET /api/configpayload, the startup banner.inject-auth-token.ts— embeds the API token into the servedindex.html(see Auth token).sandbox-controller.ts— the MCP Apps sandbox HTTP server;ensure-web-build.ts— buildsdist/on demand for prod--web;vite-base-config.ts— sharedoptimizeDepsexclusions.browser-externalized-builtin-gate.ts— Vite-agnostic build-gate logic that failsvite buildwhen a Node built-in reaches the browser bundle (#1769); the thin Vite plugin wiring lives invite.config.ts. It sits underserver/(rather thansrc/) as the home for Node-only, build-time tooling — it's imported by the Vite config, never by the browser — alongside the othervite-*config helpers here.
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 bundlesserver/+@inspector/coreinto 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.
Non-component code: src/lib vs src/utils
Two grab-bag directories, split by one rule: utils = functions that compute; lib = things that instantiate, adapt, or touch the environment. If it does I/O or wraps a subsystem, it's lib; if it's a pure transform, it's utils.
src/utils/— pure, side-effect-free functions (no DOM/window/sessionStorageI/O, no subsystem ownership), trivially unit-testable with no mocks. Examples:jsonUtils,schemaUtils,toolUtils,maskSecrets,inspectorTabs,deepLink,mcpNetworkHeaders. Carve-outs that stayutils:- Diagnostic logging (
console.warn/console.error) doesn't count as a side effect. - Importing from
@inspector/core— neither a type-only import nor re-exporting core's pure functions/constants is a subsystem dependency (what makes a modulelibis wrapping core's stateful runtime). - Pure domain types + their constructors (
customHeaders) — there is notypes/sub-bucket insidelib/utils.
- Diagnostic logging (
src/lib/— infrastructure / stateful adapters: modules that compose subsystems, wrap the@inspector/coreruntime, or produce side effects. Examples:environmentFactory,remoteOAuthStorage,oauthResume(sessionStorage),browserTabVisibility(DOM listeners),clearServerOAuthState,downloadFile.
The top-level src/types/ is a separate sibling — ambient .d.ts module stubs, not the place for new domain types (the one that lingers there, dead navigation.ts, is tracked for removal in #1785).
Nothing enforces the boundary — no path alias keys off it, and the coverage include in vite.config.ts lists both directories, so a move between them is coverage-neutral. It's a human-legible import-time signal. See AGENTS.md for the full rule (including the whitelist caveat — a module placed outside components/lib/utils/server falls out of the ≥90 gate).
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.
Deep-link auto-connect
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 testruns the fast unit project (happy-dom).test:watchfor the loop.- Integration tests run in a real Node env (no happy-dom, 30s timeouts) and spawn
test-servers/build/test-server-stdio.jsas a subprocess, sopretest/the coverage script build the test servers first (test-servers:build). npm run test:coverageruns 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.