- TypeScript 90.8%
- JavaScript 8.8%
- CSS 0.2%
- HTML 0.2%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
* web: accessible light/dark color palette + dark-mode legibility fixes (#1678) Ports the design system's accessible palette. Colors are defined once as Mantine color tuples in theme.ts (shade 0 lightest → 9 darkest); every `--inspector-*` token in App.css references `--mantine-color-*`, so improving the tuples cascades through the whole UI. - theme.ts: full accessible tuple set (inspector/gray/dark/green/red/yellow/blue/ teal/violet), WCAG-AA at the primary shades. Tuples are the one place literal hex belongs — the token source. Primary stays inspector-7 (light) / -8 (dark). - theme/ActionIcon.ts: default `color: "gray"` so chrome icons (settings, theme toggle, sidebar, pin, sort, replay) render neutral grey instead of inheriting the blue primary. Icons that set their own color/variant still override. - App.css: fix light surfaces (`surface-body` gray-0, `surface-card` white — the body token pointed at near-black dark-9); add direction/method badge tokens; lift the semantic status/log/task/history colors 1–2 steps in the dark scheme (the shared `*-6` shade sat too dark on charcoal). Light `:root` keeps `*-6`. - Badges: MessageDirectionBadge + new MethodBadge element driven by `--inspector-badge-*` tokens (deep fill + light text in dark, tinted in light) via Mantine `bg`/`c` style props with `autoContrast={false}`; swaps `<Badge color="dark">{method}</Badge>` in ProtocolEntry/NetworkEntry (their Badge import stays — both still use it for status/category chips). Stories + test added for MethodBadge (≥90 coverage gate). Domain badge semantics live in the element components, not theme files. No raw hex outside the tuples; no inline styles. Supersedes the reverted #1677. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw * web: WCAG AA accessibility pass — contrast, labels, names, roles (#1678) Drives the axe-core ruleset (the engine behind Storybook's a11y addon) to zero violations across all stories in light and dark mode. Contrast (805 → 0 hits): - Darken Mantine `*-light-color` + `dimmed` in light mode (and lift dark `dimmed`) via `!important` App.css overrides — Mantine injects these at runtime at matching specificity, so specificity alone loses the tie. - `filledBadgeColor` pins amber (orange/yellow) filled badges to shade 5 so `autoContrast` (JS-computed from the theme shade, not CSS vars) picks black. - `accessibleTextColor` routes colored log/filter text through the scheme-aware `-light-color` token so it clears AA on light and dark surfaces. - `status` Badge variant: amber statuses use black-on-amber (orange text can't reach AA on light at any Mantine shade). - Resource URIs use `-light-color` (blue-4 was 4.38:1 in dark). Names / roles: - `aria-dialog-name`: custom-header modals use `Modal.Title` (wires the dialog's `aria-labelledby`) instead of a bare `Title` + a root `aria-label` (which also tripped `aria-prohibited-attr`). - `button-name`: a `Modal` theme default labels every built-in close button. - `label`: `aria-label` on the previously-unlabelled Textarea/Select inputs. - `scrollable-region-focusable`: keyboard-focusable content Code/ScrollArea viewports; long OAuth values wrap instead of scrolling. - `heading-order`: correct heading levels (keeping visual size via `size`) so outlines don't skip a level under the h2 modal/section titles. Scrollbars: `type="scroll"` ScrollArea theme default — scrollbars auto-hide and show only while scrolling (a deliberate product choice over the always-visible a11y default). `region` is disabled in the Storybook a11y config: isolated component/screen stories render outside the app's `AppShell` landmarks, so it's a page-level concern the components can't own. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * web: let the resource template form fill the main-area width (#1678) Drop the maw=40% cap on the template-form pane so the URI preview and variable inputs use the full width beside the sidebar, matching the resource preview pane. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * web: left-align + reverse the input-panel action rows (#1678) Move the footer action buttons to the left (justify flex-start) and put the primary action first on the Tool / Prompt / Resource-preview / Resource-template panels — Subscribe, Refresh, View Source on the preview panel; Execute before Cancel on the tool panel. Shortens the pointer travel from the sidebar controls / the form fields above to the actions, which matters most when the monitoring sidebar is hidden and the form is wide. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * web: match the dark-mode body background to the header tab bar (#1678) Set the dark `--inspector-surface-body` to `dark-8` (the Mantine SegmentedControl fill used by the header tab bar) instead of `dark-9`, mirroring light mode where the body and the tab bar share `gray-1`. Cards (`gray-9`) still sit a step above the body, and the contrast audit stays at zero AA violations on the slightly lighter background. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * web: enforce the a11y gate + fix modal-overlay heading-order (review #1679) Address the code review: - Flip the Storybook a11y addon from test: 'todo' to 'error' so axe violations fail the play-function tests in CI — the zero-violation state is now gated, not point-in-time. - Enforcing it surfaced a real heading-order skip: the Tools screen's h4 'Tools'/'Results' sit under an h2 Modal.Title when a sampling/ elicitation request modal opens over them. Lift both to h3 (size h4 preserves the look) so h2→h3 is valid in any DOM order. - Comment the intentional color+c pairing on the Remove/Disconnect danger buttons. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * web: brighter warning alerts + pin the elicitation form's actions (#1678) - Add a 'warning' Alert variant: a bright light-yellow fill (yellow-1) with crisp near-black text and a bright yellow-5 left accent, instead of the muddy brown a tinted yellow alert produced once yellow-light-color was darkened to yellow-9 for contrast. Dark mode keeps the light-on-dark-tint treatment. Applied to all five yellow warning/notice alerts. - Elicitation form: scroll only the form fields (capped ScrollArea), keeping the quoted message, warning, and Cancel/Decline/Submit buttons in view. The modal has no other close affordance, so the actions must never scroll out of reach. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * web: robustly pin the elicitation form's actions (#1678) The previous approach capped only the fields' ScrollArea with a fixed rem reserve for the pinned chrome, which underestimated it on some viewports — the whole modal then overflowed and the buttons clipped half-off-screen. Instead cap the whole panel below the modal height and lay it out as a flex column: the message and the warning + buttons are pinned (flex: 0 0 auto) and only the fields (flex: 1) shrink and scroll. Verified the buttons stay within the panel and the modal fits the viewport down to very short heights. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * web: fix elicitation form scrolling — fields scroll, actions pinned (#1678) The prior fix capped the panel with a bare max-height, but a flex:1 ScrollArea's viewport won't bound against a max-height (not fixed-height) parent, so a tall form's fields overflowed unscrollably (clipped) rather than scrolling. Adopt ToolDetailPanel's pattern instead: the modal gives its body a bounded flex column, the panel fills it, and the fields use a flex: "0 1 auto" ScrollArea — which sizes to content when the form is short (modal stays compact, nothing scrolls) and shrinks + scrolls once the panel is capped at the modal height, keeping the message, warning, and Cancel/Decline/Submit pinned in view. Add a ManyFields story (with a bounded-height decorator mirroring the modal) to cover the scroll case. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * web: fix elicitation scrolling in the real modal (cap fields, not flex) The flex-column approach (panel flex:1 in a flex-column modal body, fields flex:"0 1 auto") worked in a fixed-height Storybook decorator but NOT in the real modal: the modal body is bounded by max-height, not an explicit height, so the fields' ScrollArea viewport still couldn't bound — a tall form's fields overflowed and pushed the warning + Cancel/Decline/Submit off-screen. Drop the flex plumbing (and the modal-body styles) and cap the fields directly with ScrollArea.Autosize mah=calc(90dvh - 22rem): it sizes to content when the form is short (modal compact, nothing scrolls) and scrolls once the fields hit the cap. The 22rem reserve = the measured non-field chrome (modal header/padding + message + warning + buttons + gaps), so the whole modal stays within 90dvh and the actions are always in view. Verified in the real PendingClientRequestModal (new ElicitationFormTall story) for both short and tall forms. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * web: pin the sampling modal's actions too (#1678) Apply the elicitation-form treatment to SamplingRequestPanel: wrap the request/response content in a ScrollArea.Autosize capped at calc(90dvh - 10rem) so it scrolls while the Reject / Send Response buttons stay pinned in view (the modal has no other close affordance). The 10rem reserve is the pinned chrome (modal header/padding + the actions + gaps) — smaller than the elicitation form's 22rem since there's no warning banner. Add a SamplingTall story (8 messages + preferences) to cover the scroll case in the real PendingClientRequestModal. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
| .claude | ||
| .github/workflows | ||
| clients | ||
| core | ||
| docs | ||
| scripts | ||
| specification | ||
| test-servers | ||
| .dockerignore | ||
| .gitignore | ||
| .npmignore | ||
| AGENTS.md | ||
| CLAUDE.md | ||
| Dockerfile | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| tsconfig.base.json | ||
| vitest.shared.mts | ||
MCP Inspector
A developer tool for inspecting Model Context Protocol (MCP) servers. It ships as a single package, @modelcontextprotocol/inspector, that provides three ways to inspect a server:
- Web — a Vite + React + Mantine single-page app with a Node backend.
- CLI — a scriptable command-line client for automation, CI, and fast agent feedback loops.
- TUI — an interactive terminal UI built with Ink.
All three run through one global mcp-inspector binary:
npx @modelcontextprotocol/inspector # web UI (default)
npx @modelcontextprotocol/inspector --cli # CLI
npx @modelcontextprotocol/inspector --tui # TUI
Repo status. This is the v2 line of the Inspector (branch
v2/main). Themainbranch is the legacy v1 implementation (bug fixes only). v2 will eventually replacemain. SeeAGENTS.mdfor branch/board conventions.
Project layout
v2 is not an npm workspace. Each client under clients/* keeps its own package.json and node_modules; shared code lives in core/ and is consumed via a @inspector/core build-time alias (no package.json of its own). A single npm install at the root cascades installs into every client (see Setup).
inspector/
├── clients/
│ ├── web/ # Web client (Vite + React + Mantine). src/ = browser app; server/ = Node dev/prod backend
│ ├── cli/ # CLI client (tsup bundle, @inspector/core alias)
│ ├── tui/ # TUI client (Ink + React, tsup bundle)
│ └── launcher/ # Shared launcher — provides the `mcp-inspector` bin, dispatches to web/cli/tui
├── core/ # Shared code consumed via the `@inspector/core` alias (no package.json)
│ ├── auth/ # OAuth: providers, discovery, storage, mid-session recovery (browser/node/remote backends)
│ ├── json/ # JSON + parameter/argument conversion utilities
│ ├── logging/ # Silent pino logger singleton
│ ├── mcp/ # InspectorClient runtime, state stores, transports, config import
│ ├── react/ # React hooks over the state stores
│ └── storage/ # File I/O helpers for the OAuth persist backends
├── test-servers/ # Composable MCP test servers + fixtures used by integration tests
├── scripts/ # Root build/verify tooling (install cascade, smokes, pack:verify)
├── specification/ # Design/build specifications
├── AGENTS.md # Contribution rules for agents AND humans (see below)
└── README.md # You are here
Each client has its own README with client-specific detail: web · cli · tui · launcher.
Task-oriented guides live under docs/ — see Reviewing an MCP App, the CLI-first → one-shot-web recipe for automated App-tool review: --app-info probe → deep-link navigate → rendered widget, plus OAuth handoff and proxy support.
Setup
Requires Node >=22.19.0.
npm install # root install; postinstall cascades into every client
- Fresh clone: run
npm installat the repo root. - After a pull that changes a client's dependencies: re-run
npm installat the root to re-sync every client.
The cascade (scripts/install-clients.mjs) is dev-only — it exits early when the package is installed as a dependency, and the published tarball ships only each client's build/, so end users are unaffected. Set INSPECTOR_SKIP_CLIENT_INSTALL=1 to skip it.
Running during development
For day-to-day web iteration, run Vite directly from the web client (fast HMR, no launcher build needed):
cd clients/web && npm run dev
The launcher-driven scripts below run the built launcher, so build first (npm run build):
npm run web # prod web launcher against clients/web/dist
npm run web:dev # web launcher in --dev mode (Vite)
The @inspector/core shared package
core/ holds the logic shared by all three clients so that web, CLI, and TUI behave identically. Its entry point is the InspectorClient class (core/mcp/), which owns the connection to an MCP server, the request/response lifecycle, and a set of state stores; core/react/ exposes React hooks over those stores that both the web and TUI (Ink) React trees consume. OAuth (core/auth/) is factored into isomorphic logic plus browser/node/remote backends so the same flows work in the browser, in Node, and against a remote backend.
core/ intentionally has no package.json — it is not published on its own. Each client bundles it in via a @inspector/core alias:
- CLI / TUI:
esbuildOptions.aliasin theirtsup.config.tsmaps@inspector/core→ the repocore/directory, andnoExternal: [/^@inspector\/core/]inlines it into the bundle. - Web: the same alias in
clients/web/vite.config.tsfor the browser app and the Node backend runner.
Publishing core/ as its own package (e.g. for third parties to build on) is deliberately deferred — see issue #1636.
Web client: "dumb components" + Storybook
The v2 web client is built from presentational ("dumb") components — they accept data and callbacks as props and contain only display logic, with no direct data fetching or client state. State comes from the @inspector/core hooks, wired in near the top of the tree. This keeps components isolated, testable, and documentable.
That approach is what makes Storybook first-class here: every screen and element component has a *.stories.tsx file (96+ stories) that renders it against fixture props. Storybook play functions double as interaction tests, run headless in CI (npm run ci:storybook, Chromium via Playwright).
Styling follows a strict Mantine-first convention (theme variants and component props over CSS classes, --inspector-* CSS custom properties over raw color literals). The full rules live in AGENTS.md under React instructions — read them before touching web UI. Element components live in clients/web/src/components/elements/; theme variants in clients/web/src/theme/.
Test servers
test-servers/ provides composable MCP servers used by the integration and smoke suites, so tests exercise a real server over a real transport instead of mocks. A server is assembled from presets (fixture factories in test-servers/src/preset-registry.ts — tools, resources, prompts, tasks, elicitation, sampling, OAuth, …) and can be driven two ways:
- In-process — import the factories (
createTestServerHttp,createEchoTool, …) and run the server inside the test's event loop (used by the HTTP integration paths). - As a subprocess —
test-servers/build/test-server-stdio.jsis spawned as a real stdio child (used by the CLI smoke and stdio integration tests).
Configure a server declaratively with a JSON config (see test-servers/configs/*.json) selecting presets, then load it via --config. Because the servers are spawned as real subprocesses, the build output must exist first:
npm run test-servers:build # (from clients/web) → tsc -p test-servers, emits test-servers/build/
The Vite alias @modelcontextprotocol/inspector-test-server (in clients/web/vite.config.ts) points at test-servers/build/index.js so getTestMcpServerPath() resolves to a real .js path.
Building
npm run build # builds all clients: web → cli → tui → launcher
Individual clients: build:web, build:cli, build:tui, build:launcher. The web build produces both the browser SPA (clients/web/dist, Vite) and the Node prod-server runner (clients/web/build, tsup).
Testing & the quality gate
Each client self-validates from its own folder; the root scripts chain them. There is no aggregate root test script — use validate (fast) or coverage (the gate).
| Script | What it does |
|---|---|
npm run validate |
format:check + lint + build + fast unit tests, per client. The quick inner-loop check. |
npm run coverage |
The per-file ≥90% gate (lines/statements/functions/branches) under v8 instrumentation, per client. CI-enforced. For web this also runs the integration project. |
npm run smoke |
End-to-end smokes through the built launcher (--help dispatch + prod cli/tui/web). |
npm run ci |
Mandatory pre-push command. validate → coverage → smoke → Storybook. A true superset of GitHub CI. |
npm run pack:verify |
Publish smoke — see Publishing. |
Per-client scripts exist too (validate:web, coverage:cli, smoke:tui, …). Run npm run format (per client) before committing — validate runs the non-fixing format:check and fails CI on any unformatted file.
For the full testing rules — the ≥90% per-file gate, where test files live, the unit vs. integration vs. storybook projects, and the v8 ignore policy — see AGENTS.md.
Publishing
The root @modelcontextprotocol/inspector package ships as one tarball with a single version number — no separate -web / -cli / -tui / -core packages. npm run build builds every client, then prepack runs before npm publish. Runtime dependencies are declared on the root package.json; client builds bundle @inspector/core and externalize npm packages resolved from the root install.
What ships, and the packaging invariants
The root package.json "files" allowlist is the source of truth for the tarball. A few non-obvious entries exist because they are read at runtime or were silently dropped by npm's packlist — do not remove them without re-running npm run pack:verify:
- No source maps. The client bundlers set
sourcemap: false(clients/{cli,tui}/tsup.config.ts,clients/web/tsup.runner.config.ts); Vite and the launcher'stscalready emit none. Maps are ~half the unpacked size and aren't needed at runtime — debug vianpm run devon the source. clients/web/buildships viaclients/web/.npmignore.clients/web/.gitignorelistsbuild/, and npm's packlist honors that nested.gitignoreover the root"files"allowlist — so the prod web-server runner was silently missing from the tarball whileclients/web/distslipped through (its.gitignoreonly listsdist-ssr).clients/web/.npmignoreoverrides the.gitignorefor publishing so bothbuild/(runner) anddist/(SPA) ship. The other clients don't need this — none ship a nested.gitignore.- A single version number, read from the root
package.json. The Inspector ships as one package with one version, so only the rootpackage.jsoncarries aversion— the fourclients/*/package.jsons deliberately have none. Every Node client (CLI, TUI, and the web backend) resolves the version through the sharedreadInspectorVersion()reader incore/node/version.ts, which walks up to the root manifest (always present in the tarball). No clientpackage.jsonis read at runtime, so none needs to ship. The web browser can't read the filesystem; it gets its version from the backend viaGET /api/config(see #1639).
npm run pack:verify — publish smoke against the real tarball
The smoke:* scripts run against the in-repo build tree, which is not the published package. npm run pack:verify (scripts/pack-and-verify.mjs) closes that gap: it builds, npm packs the publishable tarball (asserting no source maps ship and that the runtime-required files are present), installs the tarball into a clean throwaway consumer — a fresh temp directory where it runs a real npm install <tgz> (pulls runtime deps, runs postinstall), exactly as npx @modelcontextprotocol/inspector would — and drives the installed mcp-inspector bin end to end: --help dispatch, a real --cli tools/list over stdio, and a prod --web boot that must serve / from the shipped dist. It catches "works in --dev, breaks under npx …" path/packaging failures. It requires network access (the install pulls deps), so it is a local / release check, not part of the fast validate/ci loop.
Cutting a release
Publishing is automated by two release-gated jobs in .github/workflows/main.yml (github.event_name == 'release', both needs: build):
publish— the npm package. Runsnpm run pack:verifyas the pre-publish gate, asserts the release tag matches the rootpackage.jsonversion, thennpm publish --access public --provenance— a singlenpm publish(v2 is not an npm workspace, so there is no v1-stylepublish-all/--workspaces), with a signed provenance attestation via GitHub OIDC (id-token: write,environment: release,NPM_TOKEN).publish-github-container-registry— the container image (see Docker).
Because there is one version number (only the root package.json has one — the clients carry none, so there is nothing to keep in sync and no check-version step), the release flow is just:
npm version <major|minor|patch> # bumps the root package.json + tags
git push --follow-tags
# then draft & publish a GitHub Release for that tag → triggers `publish`
The release's target commit selects which workflow runs, so this only publishes when a release is cut from a commit carrying this (v2) workflow.
Docker
A container image is published to GHCR (ghcr.io/modelcontextprotocol/inspector, linux/amd64 + linux/arm64) by the release workflow. The Dockerfile is a two-stage build: the first stage installs and npm packs the publishable tarball; the second stage npm install -gs that tarball, so the image ships the exact same artifact as npm, with a clean mcp-inspector bin.
# run the web UI (reads the auth token from the container logs)
docker run --rm -p 6274:6274 ghcr.io/modelcontextprotocol/inspector
# or build the image locally
docker build -t mcp-inspector .
docker run --rm -p 6274:6274 mcp-inspector
The image defaults to --web bound to 0.0.0.0:6274 with browser auto-open disabled; override the args to run another mode (docker run --rm ghcr.io/modelcontextprotocol/inspector --cli …). Pass -e MCP_INSPECTOR_API_TOKEN=… to set a known token (otherwise one is generated and printed in the logs), or -e DANGEROUSLY_OMIT_AUTH=true to disable auth. The image runs as the non-root node user and has a HEALTHCHECK that probes the web UI — it assumes the default --web mode, so add --no-healthcheck when running --cli/--tui (which have no web server).
Contributing — AGENTS.md and CLAUDE.md
AGENTS.md is the contract for changing this codebase, and it applies to humans and AI agents alike. It is not agent-only boilerplate — it holds the project's real conventions: the issue-and-board workflow, branch/label rules, the TypeScript and Mantine/React standards, the testing and coverage requirements, and the mandatory pre-push gate. Read it before making changes, and keep it up to date when you change structure, tooling, or rules.
CLAUDE.md is the entry point the Claude Code agent loads automatically; it simply includes AGENTS.md and this README, so both agents and humans work from the same source of truth. If you use a different agent that reads AGENTS.md, you get the same rules.
A key rule worth surfacing here: all work is issue-driven. Before starting, find or create a tracking issue on the v2 project board; open PRs against v2/main with Closes #<issue>. The exact recipes (labels, board IDs, statuses) are in AGENTS.md.
License
MIT.
