- TypeScript 90.8%
- JavaScript 8.8%
- CSS 0.2%
- HTML 0.2%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
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
* Tasks: modern io.modelcontextprotocol/tasks extension (era fork core) Core + test-server implementation of the SEP-2663 tasks extension for modern (2026-07-28) connections, forked from the legacy 2025-11-25 tasks path. SDK v2 removed all tasks support AND era-gates the tasks/* spec methods out of the modern era on both the client (outbound) and server (inbound) sides, and its codec rejects a resultType:"task" result outright. So the Inspector drives the extension itself: task creation rides tools/call (a modern method) with the resultType:"task" frame rewritten at the transport to a CallToolResult carrying the handle in _meta; tasks/get/update/cancel go over a raw-wire request channel that bypasses the era gate (full modern envelope, string ids, consumed by the transport). input_required tasks reuse the existing pending-request modal via fulfilInputRequests, answered with tasks/update. No tasks/list, no tasks/result. Tab + run-as-task gate on the negotiated extension; the managed task store's refresh re-polls known handles on modern (no server list). Test server serves the extension via an Express interceptor (SDK can't) + a shared poll-driven runtime. Full integration coverage of both eras. Refs #1631 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5 * Tasks modern era: tests + coverage (raw-wire, schemas, era integration) Adds unit coverage for modernTaskSchemas and the raw-wire request channel (transport-null guard, send failure, timeout, error response, teardown), plus end-to-end era integration tests (create/poll/input-required/update/cancel, synchronous passthrough, unknown-id error, modern store re-poll). Fixes the App and constructor-capabilities tests for the always-on tasks extension. Refs #1631 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5 * Tasks modern era: docs + share test servers per era to de-flake coverage Document the tasks era-fork test configs in README and the new core/test-server files in AGENTS.md. Share one modern + one legacy test server across the era integration block (started lazily, stopped in afterAll) — starting a fresh Express server per test made connection negotiation flaky under the heavy concurrent coverage run. Refs #1631 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5 * Restore pr-screenshots deleted inadvertently The logging/subscriptions PR screenshots + README were staged-deleted in the working tree before this branch; restore them from v2/main so this PR doesn't remove other PRs' artifacts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5 * Tasks modern era: offer Run-as-task for any tool + PR screenshots On modern (SEP-2663) task creation is server-directed — the client declares the extension once and any tool may return a task — so the Tools screen now offers 'Run as task' for every tool on a modern connection (threaded via a modernTasks prop through InspectorView → ToolsScreen → ToolDetailPanel), rather than gating on the legacy per-tool taskSupport. Verified end-to-end in the web UI (remote proxy): create → poll tasks/get → input_required elicitation → tasks/update → inline result, plus the unchanged legacy tasks/list/result flow. Adds proof screenshots. Refs #1631 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5 * Tasks modern era: address two follow-ups (task-input note + ordinary-path auto-poll) 1. The pending-request note now distinguishes a modern task round from an MRTR round: a new 'task-input-required' PendingRequestOrigin threads through fulfilInputRequests → the pending-request modal → MrtrOriginNote, which shows 'submitted via a tasks/update request (SEP-2663), not a retry' instead of the MRTR retry copy. 2. The ordinary (non-run-as-task) modern callTool path now auto-polls an unsolicited task handle: attemptToolCall detects the rewritten handle in the result _meta and drives it to termination (get → input_required/update → inline result), dispatching task events so the Tasks tab + toasts track it and the tool result resolves to the task's final result. The poll loop is shared with the streaming path via extracted helpers (submitModernTaskInput / modernTaskTerminalOutcome / modernTaskPollInterval). Refs #1631 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5 * Tasks modern era: address review (negotiated envelope version, input-round cap, clarifying comment) + fresh smoke screenshots Review of #1731: 1. withModernTaskEnvelope now stamps the NEGOTIATED protocol version (getProtocolVersion() ?? MODERN_PROTOCOL_VERSION) so the envelope matches the MCP-Protocol-Version header instead of a hardcoded constant. 2. A never-completing input_required task is now bounded by MRTR_MAX_ROUNDS in the shared submitModernTaskInput (threaded through both poll loops), so a non-conformant server can't re-prompt the user forever. New modern_loop_task test-server tool + integration test cover the cap. 3. ToolDetailPanel: comment clarifying that per-tool taskSupport:"forbidden" is deliberately ignored on modern (task creation is server-directed under SEP-2663). (The abort-during-poll observation is addressed in the PR reply: the cancel path uses tasks/cancel and disconnect rejects the in-flight raw request, so the poll stops; matches legacy.) Fresh web-UI smoke screenshots (modern connected, task completed, input_required with the corrected tasks/update note, input completed). Refs #1631 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5 * Tasks modern era: fix uncancelable input-required loop + sticky cancel Two bug fixes reported during manual testing of the tasks era-fork (#1631): 1. A modern task paused at `input_required` (e.g. `modern_loop_task`, which never advances) could not be cancelled: the poll loop blocked on the pending-request modal, so `cancelRequestorTask` sent `tasks/cancel` but the modal kept re-prompting. Now each modern poll loop registers a per-task AbortController (`registerTaskInputAbort`), `cancelRequestorTask` aborts it to reject the pending elicitation and unblock the poll, and the embedded input requests are tagged with their owning task id (`RELATED_TASK_META_KEY`) so the pending-request UI's Cancel action cancels the TASK rather than sending a "cancel" answer the server would re-prompt on. 2. On the legacy server, cancelling a task briefly showed CANCELLED then flipped to COMPLETED when the in-flight tool call resolved. Cancellation is a deliberate terminal decision, so `ManagedRequestorTasksState` now remembers cancelled task ids and ignores any later non-cancelled status update. Also: move the compact task id onto its own line in the embedded monitoring sidebar so the status badge / Cancel Task control don't wrap; DRY the duplicated task poll-interval logic into a shared `taskPollInterval` helper. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5 * Refresh tasks PR screenshots + capture both bug-fix scenarios Recapture the five tasks flow screenshots against the current build and add two demonstrating the post-review fixes: modern_loop_task cancelled from the pending-request modal (uncancelable-loop fix) and a legacy task staying CANCELLED after the in-flight call resolves (sticky-cancel fix). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5 * Address re-review: sticky cancel on legacy refresh + App cancel-routing test Re-review of the cancelable-loop / sticky-cancel fix raised two non-blocking hardening items: 1. The legacy tasks/list refresh() rebuilt the list from the server, honoring only dismissedTaskIds — so a cooperative-cancel server that re-lists a user-cancelled task as completed could un-stick the cancel on a manual Refresh. The refresh path now pins a cancelledTaskIds task to "cancelled" (keeping it visible-as-cancelled, matching the live-merge handlers) rather than dropping it. Covered by a new state test. 2. Added an App.test.tsx case locking in the user-facing half of the fix: Cancel on a task-input-required elicitation calls cancelRequestorTask (not respond). 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> |
||
| .claude | ||
| .github/workflows | ||
| clients | ||
| core | ||
| docs | ||
| pr-screenshots | ||
| 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.
A streamable-HTTP server can also serve the modern (2026-07-28) protocol era via the SDK's createMcpHandler — set transport.modern in the JSON config (true for dual-era stateless serving, or { "legacy": "reject" } for modern-only strict), or pass modern on the ServerConfig for an in-process createTestServerHttp. This is what lets an Inspector connection negotiating protocolEra: "auto" | "modern" reach the modern leg (populated server/discover, sessionless). See test-servers/configs/modern-http.json. test-servers/configs/modern-mrtr-http.json additionally serves the mrtr_confirm tool (preset mrtr_confirm, createMrtrTool) over the modern leg: its handler returns inputRequired(...) embedding a form elicitation, so invoking it produces a real MRTR round-trip (input_required → the client fulfils the embedded elicitation and retries with a new id → complete). The Inspector drives MRTR manually (inputRequired: { autoFulfill: false }), so the embedded elicitation pauses at the pending-request modal (tagged "input_required") for you to answer, then the retry completes — useful for eyeballing both that pending-request UX and the Protocol view's MRTR conversation grouping. test-servers/configs/mrtr-showcase-http.json bundles every MRTR preset in one modern server for manual testing: mrtr_confirm (single round), mrtr_two_step (two elicitation rounds via requestState), mrtr_sample (embedded sampling → the Sampling panel), mrtr_roots (embedded roots/list, auto-answered silently from configured roots — no modal), mrtr_edge (an inputRequests-only round then a requestState-only round), and mrtr_loop (never completes → the MRTR_MAX_ROUNDS bound trips). (The legacy collect_elicitation preset calls server.elicitInput, which errors on the 2026-07-28 leg — server→client requests aren't allowed there; MRTR is the modern replacement.)
test-servers/configs/modern-network-http.json is the Network-tab showcase for the standardized HTTP headers and new error taxonomy (SEP-2243 / SEP-2575). It serves a get_weather tool whose city argument carries an x-mcp-header: "City" annotation (so a modern client mirrors it to Mcp-Param-City), plus four trigger_* tools that the modern leg's spec-error injector (transport.modern.injectSpecErrors: true) answers with a real HTTP status + JSON-RPC error body: trigger_header_mismatch → 400 / -32020, trigger_missing_capability → 400 / -32021, trigger_unsupported_version → 400 / -32022 (with data.supported), trigger_method_not_found → 404 / -32601. Connect to it with Protocol Era = Modern and open the Network tab to see the mirrored Mcp-* headers highlighted, sentinel values decoded, and each error rendered distinctly. Note: Mcp-Param-* mirroring is skipped by the SDK in the browser (detectProbeEnvironment() !== "browser"), so calling get_weather from the web client omits Mcp-Param-City and the strict server answers -32020 — the same tool is callable from the Node CLI/TUI, where mirroring is active.
test-servers/configs/pagination-http.json is the page-by-page fetch showcase (#1721). It serves 12 tools, 12 resources, and 12 prompts (presets numbered_tools / numbered_resources / numbered_prompts, count: 12) with maxPageSize of 4 for each, so every list paginates into three pages. Turn on "Fetch Lists One Page at a Time" (Server Settings — the paginatedLists setting, or the Paginated switch in a list sidebar) and the Tools/Resources/Prompts lists load page 1 only (4 items) with a Load next page control and an N pages loaded status; each click fetches the next 4 and appends them, and Refresh resets to page 1. With the switch off (the default), the same lists auto-aggregate all three pages on connect.
test-servers/configs/logging-legacy-http.json and test-servers/configs/logging-modern-http.json are the logging era-fork showcase (#1629). Both serve logging: true plus a send_notification tool that emits a notifications/message at a chosen level; the legacy one is a plain streamable-HTTP server (logging/setLevel era) and the modern one sets transport.modern: true. Connect to the legacy server and open the Logs tab to get the session-scoped Set Active Level selector + Set button; calling send_notification streams the log into the panel. Connect to the modern one with Protocol Era = Modern and the same tab instead shows the Log Level per Request control — pick a level to opt in and the client stamps _meta["io.modelcontextprotocol/logLevel"] on every subsequent request (verify in the Network tab's request body); calling send_notification then streams the log into the panel over the request's SSE response. Set the control back to Off and the same call is silently gated — the request omits the logLevel key, so the log never arrives. That gating is faithful to the spec ("a server MUST NOT emit notifications/message for a request that didn't opt in") because send_notification emits through the SDK's request-scoped, threshold-aware extra.log (ctx.mcpReq.log): on the modern leg it reads the per-request logLevel opt-in from the request envelope and drops the message when the client didn't opt in or the level is below the requested severity; on legacy it honors the session level from logging/setLevel. Because it emits through the request's notify, the modern response upgrades to SSE and the log rides the originating request's stream.
test-servers/configs/subscriptions-legacy-http.json and test-servers/configs/subscriptions-modern-http.json are the resource-subscription era-fork showcase (#1630). Both serve three numbered_resources with subscriptions: true; the legacy one also serves an update_resource tool, and the modern one sets transport.modern: true. Connect to the legacy server, open a resource in the Resources tab and click Subscribe — the client sends resources/subscribe (Network/Protocol view) and the Subscriptions section lists the URI with no stream chrome; call update_resource with that URI and the server updates the content and emits notifications/resources/updated, stamping the subscribed tile's last-updated time. Connect to the modern server with Protocol Era = Modern and the same Subscribe instead sends subscriptions/listen (its filter carries resourceSubscriptions + the resourcesListChanged opt-in) and resolves on notifications/subscriptions/acknowledged; the Subscriptions section then shows the stream-status badge (Connecting… → Listening) in its header, and if the long-lived stream drops it reconnects by re-listing. The modern config deliberately omits update_resource: the SDK's modern leg is stateless/per-request (createMcpHandler(() => createMcpServer(config))), so the tool would run against a throwaway server instance — the content change wouldn't persist for the next resources/read, and its resources/updated wouldn't reach the (separate) listen stream — which is more confusing than useful. The live update-notification round-trip is therefore demonstrated on the legacy (stateful-session) server; the modern server is for the subscribe/listen/badge behavior. (The Inspector's receive path is era-transparent, so a real stateful modern server that routes resources/updated onto the listen stream drives the subscribed tile the same way.)
test-servers/configs/tasks-legacy-http.json and test-servers/configs/tasks-modern-http.json are the Tasks era-fork showcase (#1631). The legacy server advertises capabilities.tasks (tasks: { list, cancel }) with the simple_task / progress_task / elicitation_task presets — connect to it, run one of those tools with Run as task on, and the Tasks tab lists it (populated via tasks/list), polls tasks/get, fetches the payload with the blocking tasks/result, and cancels with tasks/cancel. The modern server sets transport.modern: true and tasksExtension: true, advertising the io.modelcontextprotocol/tasks extension (SEP-2663) and serving the modern_task / modern_input_task tools. Connect with Protocol Era = Modern: the Tasks tab is now gated on the negotiated extension (not capabilities.tasks). Run modern_task as a task — the tools/call returns a CreateTaskResult (resultType: "task", visible in the Protocol/Network tabs), the client polls tasks/get (no tasks/list), and the completed task inlines its result (no blocking tasks/result). Run modern_input_task and the task moves to input_required, surfacing an embedded elicitation through the pending-request modal; answering it sends tasks/update with the inputResponses, and the next poll completes. SDK v2 removed all tasks support and era-gates the tasks/* spec methods out of the modern era on both sides — so the Inspector drives the extension itself (the resultType: "task" frame is rewritten at the transport into a CallToolResult carrying the handle; tasks/get/update/cancel ride a raw-wire request channel with the full modern envelope), and the test server serves tasks/* from an Express interceptor ahead of the SDK handler (the SDK's modern leg would answer them -32601). The Tasks tab's Refresh re-polls the handles already known to the client (modern has no server-side task list).
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.
