@modelcontextprotocol/inspector 2.3.0 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -419
- package/clients/cli/build/index.js +12710 -10802
- package/clients/launcher/build/index.js +6 -4
- package/clients/tui/build/index.js +4436 -1102
- package/clients/web/build/index.js +2143 -230
- package/clients/web/dist/assets/index-ZYqA4f7P.js +1486 -0
- package/clients/web/dist/assets/{index-CiUpWldH.css → index-dqp8Sd8W.css} +1 -1
- package/clients/web/dist/assets/{prism-light-DBxbqjnM.js → prism-light-_bvDBTQE.js} +1 -1
- package/clients/web/dist/assets/worker-json-8CDXFfik.js +1631 -0
- package/clients/web/dist/index.html +2 -2
- package/clients/web/static/sandbox_proxy.html +122 -10
- package/package.json +29 -13
- package/clients/tui/build/chunk-MCKGQKYU.js +0 -15
- package/clients/tui/build/undici-KNXAVD2F.js +0 -26644
- package/clients/web/build/chunk-MCKGQKYU.js +0 -15
- package/clients/web/build/undici-KNXAVD2F.js +0 -26644
- package/clients/web/dist/assets/index-CwhDusR3.js +0 -237
- package/clients/web/dist/assets/json-BJ8dCynk.js +0 -1
package/README.md
CHANGED
|
@@ -18,460 +18,88 @@ npx @modelcontextprotocol/inspector --tui # TUI
|
|
|
18
18
|
|
|
19
19
|
> **Repo status.** This is the **v2** line of the Inspector. Active development happens on **`v2/main`** (the develop branch — all v2 PRs target it), which is merged into **`main`** at milestone releases; `main` is the default branch and holds the latest released v2, published to the npm `latest` tag. The legacy **v1** line lives on **`v1/main`** — security fixes only, published straight from that branch to the npm `v1-latest` tag (`npx @modelcontextprotocol/inspector@v1-latest`). See [`AGENTS.md`](./AGENTS.md) for branch/board conventions.
|
|
20
20
|
|
|
21
|
-
##
|
|
22
|
-
|
|
23
|
-
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](#setup)).
|
|
24
|
-
|
|
25
|
-
```
|
|
26
|
-
inspector/
|
|
27
|
-
├── clients/
|
|
28
|
-
│ ├── web/ # Web client (Vite + React + Mantine). src/ = browser app; server/ = Node dev/prod backend
|
|
29
|
-
│ ├── cli/ # CLI client (tsup bundle, @inspector/core alias)
|
|
30
|
-
│ ├── tui/ # TUI client (Ink + React, tsup bundle)
|
|
31
|
-
│ └── launcher/ # Shared launcher — provides the `mcp-inspector` bin, dispatches to web/cli/tui
|
|
32
|
-
├── core/ # Shared code consumed via the `@inspector/core` alias (no package.json)
|
|
33
|
-
│ ├── auth/ # OAuth: providers, discovery, storage, endpoint overrides, mid-session recovery (browser/node/remote backends)
|
|
34
|
-
│ ├── client/ # Install-level client config (`client.json`): browser-safe parse/validate + Node load/save, remote backend, secrets
|
|
35
|
-
│ ├── json/ # JSON + parameter/argument conversion utilities, and the nullable-union
|
|
36
|
-
│ │ # schema collapse shared by the web and TUI form builders
|
|
37
|
-
│ ├── logging/ # Silent pino logger singleton
|
|
38
|
-
│ ├── mcp/ # InspectorClient runtime, state stores, transports, config import,
|
|
39
|
-
│ │ # and the RFC 6570 URI-template helpers the web form and TUI expand through
|
|
40
|
-
│ ├── node/ # Node-only shared helpers: version reader, hostUrl (host normalize/canonicalize + all-interfaces/loopback detection)
|
|
41
|
-
│ ├── react/ # React hooks over the state stores
|
|
42
|
-
│ └── storage/ # File I/O helpers for the OAuth persist backends
|
|
43
|
-
├── test-servers/ # Composable MCP test servers + fixtures used by integration tests
|
|
44
|
-
├── scripts/ # Root build/verify tooling (install cascade, smokes, verify-build-gate, verify-format-coverage, verify-dep-lockstep, pack:verify)
|
|
45
|
-
├── docs/ # Task-oriented guides (v1→v2 migration, server configuration, MCP App review, launcher/config plan)
|
|
46
|
-
├── specification/ # Design/build specifications
|
|
47
|
-
├── AGENTS.md # Contribution rules for agents AND humans (see below)
|
|
48
|
-
└── README.md # You are here
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
Each client has its own README with client-specific detail:
|
|
52
|
-
[web](./clients/web/README.md) · [cli](./clients/cli/README.md) · [tui](./clients/tui/README.md) · [launcher](./clients/launcher/README.md).
|
|
53
|
-
|
|
54
|
-
Task-oriented guides live under [`docs/`](./docs):
|
|
55
|
-
|
|
56
|
-
- [Migrating from v1 to v2](./docs/v1-to-v2-migration.md) — the v1 → v2 map: CLI flag mapping, `--config` vs. `--catalog` semantics with before/after examples, the Node engine bump (`>=22.7.5` → `>=22.19.0`), env-var renames, and the sub-packages that no longer ship.
|
|
57
|
-
- [MCP server configuration](./docs/mcp-server-configuration.md) — which server(s) the Inspector connects to: `--catalog` vs. `--config`, ad-hoc targets, the `--` separator, the file format and its Inspector-specific per-server fields. Shared by all three clients; the cli and tui READMEs delegate their server-options sections to it.
|
|
58
|
-
- [Reviewing an MCP App](./docs/mcp-app-review.md) — 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.
|
|
59
|
-
- [Launcher and config consolidation](./docs/launcher-config-consolidation-plan.md) — why the launcher runs a client in-process rather than spawning it, and how the shared config processor fits in.
|
|
60
|
-
|
|
61
|
-
## Setup
|
|
21
|
+
## Quick start (development)
|
|
62
22
|
|
|
63
23
|
Requires Node `>=22.19.0`.
|
|
64
24
|
|
|
65
25
|
```bash
|
|
66
|
-
npm install
|
|
26
|
+
npm install # at the repo root; postinstall cascades into every client
|
|
27
|
+
npm run build # web → cli → tui → launcher
|
|
67
28
|
```
|
|
68
29
|
|
|
69
|
-
- **
|
|
70
|
-
- **After a pull that changes a client's dependencies:** re-run `npm install` at the root to re-sync every client.
|
|
71
|
-
|
|
72
|
-
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.
|
|
73
|
-
|
|
74
|
-
**Where a dependency is declared.** The MCP SDK packages (`@modelcontextprotocol/client`, `core`, `server`, `server-legacy`, `ext-apps`) live in the **root** `package.json` only — never in a client's. Node resolution walks up, so the root install is on every client's chain, and the root manifest is already what the published tarball resolves against. Declaring them per client installs a second copy that can drift from the root's, which is how two versions of `ext-apps` (and of the transitive v1 `@modelcontextprotocol/sdk`) ended up in the tree before [#1970](https://github.com/modelcontextprotocol/inspector/issues/1970) — and a second copy of `client`/`core` is the failure `vitest.shared.mts` carries a `dedupe` workaround for. The same root-only placement holds for anything reached solely through root-owned code with no manifest of its own (`test-servers/src`, `core/`), and `vitest.shared.mts` aliases those to the repo root — `express` and `yaml`, both reached through `test-servers/src`, are the two today. **Whether such a package is a `dependency` or a `devDependency` follows from who consumes it at runtime, not from where it is declared:** anything `core/` imports at runtime must be a root **`dependency`**, because the client builds externalize npm packages and a published install resolves them from the root manifest, where devDependencies are absent. `express` is test-only and is a devDependency; `yaml` currently sits in `dependencies`. **`vite` and `@vitejs/plugin-react` are root `dependencies` for the same reason, not by mistake** — they look like build tooling, but `clients/web/server/start-vite-dev-server.ts` imports them at runtime for `mcp-inspector --web --dev`, and `clients/web/tsup.runner.config.ts` lists both as `external`, so a published install resolves them from the root manifest. Moving them to `devDependencies` would break `--web --dev` for consumers (and the on-demand `vite build` in `ensure-web-build.ts`) while passing every local check. It does mean they show up under `npm audit --omit=dev`, which is a feature: they really are in the production tree.
|
|
75
|
-
|
|
76
|
-
## Running during development
|
|
77
|
-
|
|
78
|
-
For day-to-day web iteration, run Vite directly from the web client (fast HMR, no launcher build needed):
|
|
30
|
+
For day-to-day **web** iteration, run Vite directly — fast HMR, no launcher build needed:
|
|
79
31
|
|
|
80
32
|
```bash
|
|
81
33
|
cd clients/web && npm run dev
|
|
82
34
|
```
|
|
83
35
|
|
|
84
|
-
The launcher-driven scripts
|
|
36
|
+
The launcher-driven scripts run the **built** launcher, so build first:
|
|
85
37
|
|
|
86
38
|
```bash
|
|
87
39
|
npm run web # prod web launcher against clients/web/dist
|
|
88
40
|
npm run web:dev # web launcher in --dev mode (Vite)
|
|
89
41
|
```
|
|
90
42
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-

|
|
94
|
-
|
|
95
|
-
`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.
|
|
96
|
-
|
|
97
|
-
`core/` intentionally has **no `package.json`** — it is not published on its own. Each client bundles it in via a `@inspector/core` alias:
|
|
98
|
-
|
|
99
|
-
- **CLI / TUI:** `esbuildOptions.alias` in their `tsup.config.ts` maps `@inspector/core` → the repo `core/` directory, and `noExternal: [/^@inspector\/core/]` inlines it into the bundle.
|
|
100
|
-
- **Web:** the same alias in `clients/web/vite.config.ts` for the browser app and the Node backend runner.
|
|
101
|
-
|
|
102
|
-
Publishing `core/` as its own package (e.g. for third parties to build on) is deliberately deferred — see issue [#1636](https://github.com/modelcontextprotocol/inspector/issues/1636).
|
|
103
|
-
|
|
104
|
-
## Web client: "dumb components" + Storybook
|
|
105
|
-
|
|
106
|
-
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.
|
|
107
|
-
|
|
108
|
-
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).
|
|
109
|
-
|
|
110
|
-
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`](./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/`.
|
|
111
|
-
|
|
112
|
-
## Test servers
|
|
113
|
-
|
|
114
|
-
`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:
|
|
115
|
-
|
|
116
|
-
- **In-process** — import the factories (`createTestServerHttp`, `createEchoTool`, …) and run the server inside the test's event loop (used by the HTTP integration paths).
|
|
117
|
-
- **As a subprocess** — `test-servers/build/test-server-stdio.js` is spawned as a real stdio child (used by the CLI smoke and stdio integration tests).
|
|
118
|
-
|
|
119
|
-
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:
|
|
120
|
-
|
|
121
|
-
```bash
|
|
122
|
-
npm run test-servers:build # (from clients/web) → tsc -p test-servers, emits test-servers/build/
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
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.
|
|
126
|
-
|
|
127
|
-
### Serving the modern protocol era
|
|
128
|
-
|
|
129
|
-
A streamable-HTTP server can also serve the **modern (2026-07-28) protocol era** via the SDK's `createMcpHandler`:
|
|
130
|
-
|
|
131
|
-
- Set `transport.modern` in the JSON config — `true` for dual-era stateless serving, or `{ "legacy": "reject" }` for modern-only strict.
|
|
132
|
-
- Or pass `modern` on the `ServerConfig` for an in-process `createTestServerHttp`.
|
|
133
|
-
|
|
134
|
-
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`.
|
|
135
|
-
|
|
136
|
-
### Showcase configs
|
|
137
|
-
|
|
138
|
-
Each config below is a ready-made server for exercising one feature by hand. Load one with `--config`, and unless noted, connect with **Protocol Era = Modern**.
|
|
139
|
-
|
|
140
|
-
| Config | Demonstrates | Issue |
|
|
141
|
-
| ----------------------------------------- | -------------------------------------------------- | ---------------------------------------------------------------------- |
|
|
142
|
-
| `mcp-app-http.json` **(legacy era)** | An MCP App (UI resource + app tool) in the Apps tab | [#1859](https://github.com/modelcontextprotocol/inspector/issues/1859) |
|
|
143
|
-
| `modern-mrtr-http.json` | A single MRTR round-trip | — |
|
|
144
|
-
| `mrtr-showcase-http.json` | Every MRTR preset in one server | [#1860](https://github.com/modelcontextprotocol/inspector/issues/1860) |
|
|
145
|
-
| `modern-network-http.json` | Network tab: `Mcp-*` headers + error taxonomy | [#1628](https://github.com/modelcontextprotocol/inspector/issues/1628) |
|
|
146
|
-
| `xmcpheader-modern-http.json` | Tools tab: `x-mcp-header` mirroring and exclusions | [#1632](https://github.com/modelcontextprotocol/inspector/issues/1632) |
|
|
147
|
-
| `pagination-http.json` | Page-by-page list fetching | [#1721](https://github.com/modelcontextprotocol/inspector/issues/1721) |
|
|
148
|
-
| `structured-output-http.json` | Tools tab: a result's `structuredContent` section | [#1908](https://github.com/modelcontextprotocol/inspector/issues/1908) |
|
|
149
|
-
| `duplicate-tool-names-http.json` | A `tools/list` that repeats a tool name | [#1957](https://github.com/modelcontextprotocol/inspector/issues/1957) |
|
|
150
|
-
| `nullable-fields-http.json` | Tools tab: nullable (`anyOf` + `null`) arguments | [#1928](https://github.com/modelcontextprotocol/inspector/issues/1928) |
|
|
151
|
-
| `rfc6570-templates-http.json` | Resources tab: RFC 6570 resource-template expansion | [#1919](https://github.com/modelcontextprotocol/inspector/issues/1919) |
|
|
152
|
-
| `advertised-extensions-http.json` | Tool registration gated on advertised extensions | [#1739](https://github.com/modelcontextprotocol/inspector/issues/1739) |
|
|
153
|
-
| `logging-{legacy,modern}-http.json` | Logging, both eras | [#1629](https://github.com/modelcontextprotocol/inspector/issues/1629) |
|
|
154
|
-
| `subscriptions-{legacy,modern}-http.json` | Resource subscriptions, both eras | [#1630](https://github.com/modelcontextprotocol/inspector/issues/1630) |
|
|
155
|
-
| `tasks-{legacy,modern}-http.json` | Tasks, both eras | [#1631](https://github.com/modelcontextprotocol/inspector/issues/1631) |
|
|
156
|
-
|
|
157
|
-
#### MCP Apps
|
|
158
|
-
|
|
159
|
-
`mcp-app-http.json` serves the `mcp_app_demo` tool (`_meta.ui.resourceUri`) alongside its `mcp_app_demo_widget` UI resource, so the **Apps** tab has a real App to render. It is a plain streamable-HTTP server — connect with the **default (legacy)** protocol era, not Modern.
|
|
160
|
-
|
|
161
|
-
Open the Apps tab, select `mcp_app_demo`, give it a title and click **Open App**: the widget renders inside the sandbox iframe and exercises the host-side UI protocol surface — host-context render, `size-changed`, `ui/message`, and a log line into the **App logs** panel. Because the widget is served through the sandbox proxy page, this config is also what reproduces [#1859](https://github.com/modelcontextprotocol/inspector/issues/1859) (a missing `clients/web/static/sandbox_proxy.html` surfaces here as a "Sandbox not loaded" message in place of the widget) — a failure that only ever appeared in an installed package, never in the repo.
|
|
162
|
-
|
|
163
|
-
For the scripted version of the same flow (`--app-info` probe → deep link → rendered widget), see [Reviewing an MCP App](./docs/mcp-app-review.md).
|
|
164
|
-
|
|
165
|
-
#### MRTR
|
|
166
|
-
|
|
167
|
-
`modern-mrtr-http.json` 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 round-trip: `input_required` → the client fulfils the embedded elicitation and retries with a new id → `complete`.
|
|
168
|
-
|
|
169
|
-
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.
|
|
170
|
-
|
|
171
|
-
`mrtr-showcase-http.json` bundles every MRTR preset in one server:
|
|
172
|
-
|
|
173
|
-
| Preset | Behavior |
|
|
174
|
-
| --------------- | ------------------------------------------------------------------------------ |
|
|
175
|
-
| `mrtr_confirm` | Single round |
|
|
176
|
-
| `mrtr_two_step` | Two elicitation rounds via `requestState` |
|
|
177
|
-
| `mrtr_sample` | Embedded sampling → the Sampling panel |
|
|
178
|
-
| `mrtr_roots` | Embedded `roots/list`, auto-answered silently from configured roots (no modal) |
|
|
179
|
-
| `mrtr_edge` | An `inputRequests`-only round, then a `requestState`-only round |
|
|
180
|
-
| `mrtr_empty` | Completes with an empty result — no `content`, no `structuredContent` |
|
|
181
|
-
| `mrtr_loop` | Never completes → trips the `MRTR_MAX_ROUNDS` bound |
|
|
182
|
-
|
|
183
|
-
Run `mrtr_empty` and answer its single elicitation: the Protocol tab groups the
|
|
184
|
-
exchange as an MRTR conversation ending **COMPLETE**, and the Results panel says
|
|
185
|
-
**"Empty result — The tool call completed successfully and returned no content."**
|
|
186
|
-
On the broken build that same result rendered as **"No results yet"**, the panel's
|
|
187
|
-
pre-run placeholder ([#1860](https://github.com/modelcontextprotocol/inspector/issues/1860)) —
|
|
188
|
-
so a call the user had just watched succeed read as a call that never ran. An
|
|
189
|
-
empty `content` array with no `structuredContent` is a legal `CallToolResult`,
|
|
190
|
-
and the panel only ever mounts once a result exists, so the placeholder wording
|
|
191
|
-
could not be true there. (The neighbouring half of the same gap — a result whose
|
|
192
|
-
payload lives only in `structuredContent` — was closed by
|
|
193
|
-
[#1908](https://github.com/modelcontextprotocol/inspector/issues/1908).)
|
|
194
|
-
|
|
195
|
-
> 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.
|
|
196
|
-
|
|
197
|
-
#### Network tab — standardized headers and error taxonomy
|
|
198
|
-
|
|
199
|
-
`modern-network-http.json` covers 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`.
|
|
200
|
-
|
|
201
|
-
It also serves four `trigger_*` tools that the modern leg's spec-error injector (`transport.modern.injectSpecErrors: true`) answers with a real HTTP status plus JSON-RPC error body:
|
|
202
|
-
|
|
203
|
-
| Tool | Response |
|
|
204
|
-
| ----------------------------- | ---------------------------------------- |
|
|
205
|
-
| `trigger_header_mismatch` | `400` / `-32020` |
|
|
206
|
-
| `trigger_missing_capability` | `400` / `-32021` |
|
|
207
|
-
| `trigger_unsupported_version` | `400` / `-32022` (with `data.supported`) |
|
|
208
|
-
| `trigger_method_not_found` | `404` / `-32601` |
|
|
209
|
-
|
|
210
|
-
Open the Network tab to see the mirrored `Mcp-*` headers highlighted, sentinel values decoded, and each error rendered distinctly.
|
|
211
|
-
|
|
212
|
-
> **`Mcp-Param-*` mirroring is built by the Inspector, not the SDK.** The SDK only mirrors inside `client.callTool()`, and skips it in the browser (`detectProbeEnvironment() !== "browser"`). The Inspector routes `tools/call` through `client.request()` to drive MRTR manually, so it builds the mirrored headers itself ([#1846](https://github.com/modelcontextprotocol/inspector/issues/1846)) — on **every** client, web included, since the web client's upstream request is issued by the Node backend rather than the browser. So `get_weather` is callable from web, CLI, and TUI alike, in both the plain and "Run as task" forms.
|
|
213
|
-
|
|
214
|
-
#### `x-mcp-header` in the Tools tab
|
|
215
|
-
|
|
216
|
-
`xmcpheader-modern-http.json` serves:
|
|
217
|
-
|
|
218
|
-
- `echo` — plain tool.
|
|
219
|
-
- `get_weather` — a **valid** `x-mcp-header: "City"` annotation on its `city` argument.
|
|
220
|
-
- `invalid_header_tool` — an annotation using the header name `"Bad Header"`. The space makes it an invalid RFC 9110 token, so the whole tool definition is invalid.
|
|
221
|
-
- `trigger_invalid_params` — answered with a real `-32602 Invalid params` error whose message is _not_ about a missing tool.
|
|
222
|
-
|
|
223
|
-
Open the Tools tab: `get_weather`'s detail panel shows a **"Mirrored request headers (SEP-2243)"** section (`city → Mcp-Param-City`), and `invalid_header_tool` appears struck-through under an **"Excluded (SEP-2243)"** divider with the reason on hover. A conforming Streamable HTTP client MUST drop it from `tools/list`; the Inspector surfaces _why_.
|
|
224
|
-
|
|
225
|
-
Under SDK v2 a `tools/call` rejecting with `-32602` renders as a distinct error panel rather than an `isError` result — headed **"Unknown Tool"** when the message names a missing tool, or **"Invalid Parameters"** otherwise (run `trigger_invalid_params`).
|
|
226
|
-
|
|
227
|
-
#### Page-by-page fetching
|
|
228
|
-
|
|
229
|
-
`pagination-http.json` serves 12 tools, 12 resources, and 12 prompts (presets `numbered_tools` / `numbered_resources` / `numbered_prompts`, `count: 12`) with a `maxPageSize` of 4 each, so every list paginates into three pages.
|
|
230
|
-
|
|
231
|
-
Turn on **"Fetch Lists One Page at a Time"** (Server Settings — the `paginatedLists` setting, or the **Paginated** switch in a list sidebar) and the 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; Refresh resets to page 1. With the switch off (the default), the same lists auto-aggregate all three pages on connect.
|
|
232
|
-
|
|
233
|
-
#### Structured output
|
|
234
|
-
|
|
235
|
-
`structured-output-http.json` serves `list_items` (nested `structuredContent` — objects inside arrays inside an object, the shape from [#1908](https://github.com/modelcontextprotocol/inspector/issues/1908)), `get_temp` (a flat three-key payload), and `echo` (no `outputSchema` at all). It is a plain streamable-HTTP server — connect with the **default (legacy)** protocol era.
|
|
236
|
-
|
|
237
|
-
Run `list_items` from the Tools tab: the result panel shows the `content[]` text summary ("Found 2 items.") **and** a collapsible **Structured Output** section rendering the schema-validated payload as pretty-printed, copyable JSON. That section is what v2 was dropping — a tool declaring an `outputSchema` returns its real data there, and the text block usually only summarizes it. Run `echo` to confirm the section is absent when a result carries no `structuredContent`.
|
|
238
|
-
|
|
239
|
-
#### Duplicate tool names
|
|
240
|
-
|
|
241
|
-
`duplicate-tool-names-http.json` serves `get_weather`, `get_temp`, `echo`, and `add`, then repeats `get_weather` and `echo` at the end of `tools/list` with the same `name` and a `(duplicate)` title (`duplicateToolNames`). No preset can produce this shape — the SDK's `registerTool` rejects a repeated name — but a real server can and does, and the Inspector has to render it faithfully.
|
|
242
|
-
|
|
243
|
-
Connect (default legacy era), open the Tools tab, and type `get` into **Search tools**: the list must narrow to exactly the three `get_*` rows. On the broken build it kept a stale `echo` row, because the sidebar keyed rows by `tool.name` alone and the colliding keys orphaned a child during reconciliation ([#1957](https://github.com/modelcontextprotocol/inspector/issues/1957)).
|
|
244
|
-
|
|
245
|
-
The duplicated copies are appended rather than placed beside their twin on purpose. React matches a leading run of same-key children first, so a head-adjacent duplicate happens to line up and the defect hides; separating the pair is what makes it observable — and it is also the realistic shape, two tool sources concatenated.
|
|
246
|
-
|
|
247
|
-
#### Nullable arguments
|
|
248
|
-
|
|
249
|
-
`nullable-fields-http.json` serves `record_shipment`, whose four arguments are each declared with Zod's `.nullish()` — "optional **and** explicitly nullable". That compiles to `anyOf: [<branch>, { "type": "null" }]`, so the real type (and, for the enum, its `enum` list) sits on a branch rather than at the top level. `get_temp` sits alongside it with a plain, non-nullable `units` enum for comparison. Plain streamable-HTTP — connect with the **default (legacy)** protocol era.
|
|
250
|
-
|
|
251
|
-
Open the Tools tab and select `record_shipment`: `direction` must render as a **Select** (`envio` / `recebimento`) with a clear button that sets it back to `null`, `reference` as a text input, `quantity` as a number input, and `express` as a checkbox. On the broken build every one of them fell through to the raw-JSON textarea, which re-escaped its own contents on each keystroke until the value was unusable ([#1928](https://github.com/modelcontextprotocol/inspector/issues/1928)). The tool echoes the arguments it received, so the result panel shows exactly what was sent.
|
|
252
|
-
|
|
253
|
-
The **TUI** had the same gap and is worth checking against the same server (`--tui`, then test `record_shipment`): `direction` is a select, `quantity` an integer field, `express` a boolean. Both clients now share one collapse step — `normalizeNullableUnion` in [`core/json/nullableUnion.ts`](./core/json/nullableUnion.ts) — precisely so they cannot drift on which schemas they can render.
|
|
254
|
-
|
|
255
|
-
#### RFC 6570 resource templates
|
|
256
|
-
|
|
257
|
-
`rfc6570-templates-http.json` serves two resource templates straight out of [#1919](https://github.com/modelcontextprotocol/inspector/issues/1919) — `events_by_topic` (`foobar://events/{topic}`) and `events_by_query` (`foobar://events{?topic}`) — each echoing the URI it was matched against, plus a plain `foobar://events` resource (see below). Plain streamable-HTTP; connect with the **default (legacy)** protocol era.
|
|
258
|
-
|
|
259
|
-
Open the Resources tab and pick **events_by_topic**, then enter `foo/bar`. The request must go out as `foobar://events/foo%2Fbar`, and the result echoes back the URI the server matched. On the broken build the value was spliced in raw, so the slash created a second path segment and the SDK's matcher answered `-32602 Resource not found: foobar://events/foo/bar` — the exact failure in the issue. The same holds for `?`, `#`, `%`, spaces, and non-ASCII text.
|
|
260
|
-
|
|
261
|
-
**events_by_query** is the half that was invisible: the old `/\{(\w+)\}/g` scan could not see an expression carrying an operator, so no `topic` input was rendered at all. It now appears, marked **Optional** — RFC 6570 drops the whole expression when the variable is undefined, so reading with the field blank requests `foobar://events`, and filling it in requests `foobar://events?topic=foo%2Fbar`. The URI preview beside the title shows the partially-expanded form as you type, leaving unfilled expressions standing as written.
|
|
262
|
-
|
|
263
|
-
> The plain `foobar://events` resource is registered deliberately, not as filler. The SDK's `UriTemplate.match()` compiles `{?topic}` to a **required** `\?topic=([^&]+)`, so a template alone cannot serve the blank read — `match("foobar://events")` returns `null`. A real server exposes the unfiltered collection as its own resource; the showcase does the same so that step actually resolves.
|
|
264
|
-
|
|
265
|
-
The web client and the TUI expand through one shared helper, [`core/mcp/uriTemplate.ts`](./core/mcp/uriTemplate.ts) — the web Resources form directly, the TUI via `InspectorClient.readResourceFromTemplate` — and both derive their **form fields** from its parser too, which is the half that makes the sharing real: a form submits values under the names it rendered, so a parser that mangles a name silently drops the value at expansion time. (The CLI is not a consumer: it has no template form, and its `resources/read` passes the already-expanded `--uri` straight through.)
|
|
266
|
-
|
|
267
|
-
The SDK's `UriTemplate` is still used, but only to *validate* a template (constructing it is what rejects an unclosed expression). Its expander is not, because it is incomplete in five ways — each measured against the pinned SDK, not inferred:
|
|
268
|
-
|
|
269
|
-
| Shape | SDK behavior |
|
|
270
|
-
| --- | --- |
|
|
271
|
-
| `{a,b}` | raw-joins the values — no encoding, operator prefix dropped |
|
|
272
|
-
| `{;id}` | `;` is missing from its operator list, so the variable parses as `;id` |
|
|
273
|
-
| `{id:3}` | the prefix modifier is folded into the name, giving `id:3` |
|
|
274
|
-
| `{+v}` / `{#v}` | `encodeURI` mangles reserved `[`/`]` (`[::1]` → `%5B::1%5D`) and double-encodes pct-triplets (`%2F` → `%252F`) |
|
|
275
|
-
| `{v}` | `encodeURIComponent` leaves the sub-delims `!'()*` bare, which RFC 6570 requires encoded |
|
|
276
|
-
|
|
277
|
-
The `;` and `:3` rows are the ones a user sees directly: on the SDK's parse the form renders fields literally labelled `;id` and `id:3`. The `+`/`#` row is silent corruption rather than over-escaping — an IPv6 literal or an already-encoded path arrives at the server altered.
|
|
278
|
-
|
|
279
|
-
A template that cannot be expanded at all — an out-of-grammar modifier (`{id:abc}`), or an expression declaring no variable (`{}`, `{a,}`, `{?}`) — **withholds the read** rather than sending something. Pick **events_malformed** (`foobar://events/{topic:abc}`) to see it: Read Resource is disabled, the reason is printed under the form, and the preview shows the template as the server declared it. The alternative is worse than it looks: `x://{}` would otherwise expand to `x://` with no inputs rendered, so the form's "everything required is filled" check passes vacuously and it reads a URI that is not the template the server published.
|
|
280
|
-
|
|
281
|
-
Literals are pct-encoded on expansion too (RFC 6570 §3.1): `café/{var}` sends `caf%C3%A9/value`, not raw UTF-8 in the path — something the SDK's expander does not do either. And the *names* a template may use are RFC 6570's `varchar` plus a labelled tolerance for `-` and `~`: the conformance suite rejects `{default-graph-uri}`, but real servers publish such names and the SDK's matcher round-trips them, so the Inspector expands them and marks the variable `conforming: false` rather than refusing a resource that demonstrably works.
|
|
282
|
-
|
|
283
|
-
An **undefined** variable is what omits its expression — a variable defined as the empty string expands (`x{?q}` gives `x?q=`, `x{;q}` gives `x;q`, per RFC 6570 §3.2.7). The expander honors that distinction, so a caller such as `readResourceFromTemplate` can request either URI. Collapsing the two is a *form* concern, not a template one: both clients seed every declared variable with `""` and a text input cannot express "defined but empty", so each form drops its blanks (`definedValues`) on the way in.
|
|
284
|
-
|
|
285
|
-
Requiredness is a property of the **expression**, not the variable: RFC 6570 drops undefined names from a multi-name expression, so `{a,b}` with only `a` filled is expandable and a form must not block it. `requiredGroups` returns one entry per non-omittable expression and `hasRequiredValues` asks that each be satisfied by any one of its names — which no per-variable flag can express once a name recurs across expressions (`{a,b}{a,c}` is satisfied by filling `b` and `c`).
|
|
286
|
-
|
|
287
|
-
#### Advertised extensions
|
|
288
|
-
|
|
289
|
-
`advertised-extensions-http.json` serves `echo` (always) and a `get_weather` tool **gated on the `io.modelcontextprotocol/tasks` extension** (`extensionGatedTools`): the tool is registered but starts disabled, and the server enables it on `notifications/initialized` only when the client declared that extension in its `capabilities.extensions`.
|
|
290
|
-
|
|
291
|
-
1. Connect — the Inspector advertises the Tasks extension by default, so the Tools list shows both `echo` and `get_weather`.
|
|
292
|
-
2. Open **Server Settings → Advertised Extensions**, uncheck **Tasks (io.modelcontextprotocol/tasks)**, and reconnect.
|
|
293
|
-
3. The client now advertises no extensions, the server never enables `get_weather`, and the Tools list shows only `echo`.
|
|
294
|
-
|
|
295
|
-
This is the debugging knob for a server legitimately changing tool registration based on what the client advertises. Legacy stateful leg only — the modern per-request leg has no persistent `oninitialized`.
|
|
296
|
-
|
|
297
|
-
#### Logging, both eras
|
|
298
|
-
|
|
299
|
-
`logging-legacy-http.json` and `logging-modern-http.json` 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; the modern one sets `transport.modern: true`.
|
|
300
|
-
|
|
301
|
-
- **Legacy** — the **Logs** tab gives a session-scoped **Set Active Level** selector + **Set** button. Calling `send_notification` streams the log into the panel.
|
|
302
|
-
- **Modern** — the same tab instead shows **Log Level per Request**. 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` streams the log over the request's SSE response. Set it back to **Off** and the same call is silently gated — the request omits the `logLevel` key, so the log never arrives.
|
|
303
|
-
|
|
304
|
-
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.
|
|
305
|
-
|
|
306
|
-
#### Resource subscriptions, both eras
|
|
307
|
-
|
|
308
|
-
`subscriptions-legacy-http.json` and `subscriptions-modern-http.json` both serve three `numbered_resources` with `subscriptions: true`. The legacy one also serves an `update_resource` tool; the modern one sets `transport.modern: true`.
|
|
309
|
-
|
|
310
|
-
- **Legacy** — open a resource in the **Resources** tab and click **Subscribe**. The client sends `resources/subscribe` 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.
|
|
311
|
-
- **Modern** — the same Subscribe instead sends **`subscriptions/listen`** (its filter carries `resourceSubscriptions` plus the `resourcesListChanged` opt-in) and resolves on `notifications/subscriptions/acknowledged`. The Subscriptions section then shows a stream-status badge (`Connecting…` → `Listening`) in its header, and reconnects by re-listing if the long-lived stream drops.
|
|
312
|
-
|
|
313
|
-
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. More confusing than useful.
|
|
314
|
-
|
|
315
|
-
So the live update-notification round-trip is demonstrated on the legacy (stateful-session) server, and 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.
|
|
316
|
-
|
|
317
|
-
#### Tasks, both eras
|
|
318
|
-
|
|
319
|
-
**Legacy** (`tasks-legacy-http.json`) advertises `capabilities.tasks` (`tasks: { list, cancel }`) with the `simple_task` / `progress_task` / `elicitation_task` presets. 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`.
|
|
320
|
-
|
|
321
|
-
**Modern** (`tasks-modern-http.json`) sets `transport.modern: true` and `tasksExtension: true`, advertising the `io.modelcontextprotocol/tasks` extension (SEP-2663) and serving `modern_task` / `modern_input_task`. The **Tasks** tab is gated on the negotiated extension, not `capabilities.tasks`.
|
|
322
|
-
|
|
323
|
-
- 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`).
|
|
324
|
-
- Run `modern_input_task` — 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.
|
|
325
|
-
|
|
326
|
-
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, and `tasks/get` / `update` / `cancel` ride a raw-wire request channel with the full modern envelope. The test server serves `tasks/*` from an Express interceptor ahead of the SDK handler, since the SDK's modern leg would answer them `-32601`.
|
|
327
|
-
|
|
328
|
-
The Tasks tab's **Refresh** re-polls the handles already known to the client — modern has no server-side task list.
|
|
329
|
-
|
|
330
|
-
## Building
|
|
331
|
-
|
|
332
|
-
```bash
|
|
333
|
-
npm run build # builds all clients: web → cli → tui → launcher
|
|
334
|
-
```
|
|
335
|
-
|
|
336
|
-
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).
|
|
337
|
-
|
|
338
|
-
## Testing & the quality gate
|
|
339
|
-
|
|
340
|
-
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).
|
|
341
|
-
|
|
342
|
-
| Script | What it does |
|
|
343
|
-
| ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
344
|
-
| `npm run validate` | Runs the three durable guards first — `verify:format-coverage` (every tracked source file is format-gated), `verify:typecheck-coverage` (every one lands in a tsconfig project), `verify:dep-lockstep` (no dependency reaching one `tsc` program from two installs skews across them) — then `test:scripts` (the guards' own parser unit tests), then `validate:core` (the shared `core/` `format:check` + `lint` gate), then per client: `format:check` + `lint` + **`typecheck`** (cli/tui/launcher; web typechecks via `tsc -b` inside its `build`) + `build` + fast unit tests. The quick inner-loop check. |
|
|
345
|
-
| `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 and covers the shared `core/` runtime (including `core/json` and `core/client`). |
|
|
346
|
-
| `npm run smoke` | End-to-end smokes through the built launcher (`--help` dispatch + prod cli/tui/web), plus two headless-Chromium smokes: a boot smoke that runs the prod web bundle and asserts a clean first render (no uncaught error — sync exception or unhandled rejection, how a Node built-in reaching the browser bundle manifests), and an **MCP Apps** smoke (`smoke:web:app`) that drives connect → open app → `data-app-status="ready"` against a composable App server, covering the sandbox proxy and UI-protocol bridge. |
|
|
347
|
-
| `npm run verify:build-gate` | Runs a real `vite build` with a Node built-in forced into the browser graph and asserts the build **fails** via the #1769 gate (which turns Vite's browser-externalization warning into a hard error). Guards against the warning phrasing drifting in a Vite bump and silently disabling the gate. Part of `npm run ci`. |
|
|
348
|
-
| `npm run verify:format-coverage` | Parses the `format:check` globs out of every `package.json` (only those reachable from `validate`), enumerates all tracked source files, and **fails** listing any not covered by a glob — the durable guard for the "every first-party source file is format-gated" invariant (#1792). Runs first in `validate`. |
|
|
349
|
-
| `npm run test:scripts` | Table-driven unit tests (`node --test`) for the guard's own pure parsers (`scripts/lib/npm-scripts.mjs`, `scripts/lib/tsc-program.mjs` + the exported helpers of `verify-typecheck-coverage.mjs` and `verify-dep-lockstep.mjs`), one case per rule they encode, plus `scripts/lib/resolve-node-bin.test.mjs` — the cross-platform bin resolver (#1939), pinned against the real `bin`/`exports` shapes of the packages the scripts actually spawn. Runs in `validate` — and `verify:typecheck-coverage` guards *this* gate in turn (reachable from `validate`, non-empty test set, every test file matched by the `test:scripts` glob), since `node --test` silently skips a file its glob misses and still exits 0. |
|
|
350
|
-
| `npm run verify:typecheck-coverage` | The typecheck-coverage analog of the above (#1791): for each Node client (auto-discovered from disk — enrolled via its `typecheck` script's projects, or for a `tsc -b` client like `clients/web` via its `tsconfig.json` `references`) it runs those projects with `tsc --listFilesOnly`, unions them, and **fails** listing any tracked `.ts`/`.tsx`/`.mts`/`.cts` under the client that lands in no project (so a new top-level config/helper can't silently go untypechecked). It also requires, deny-by-default, the first-party TS no client owns (`test-servers/src`, the root `vitest.shared.mts`, all of `core/`, and any new top-level location) to land in some client project's tsc pass — so a `core` `*.tsx` web's projects don't reach is caught too. Also asserts the gate is wired (each client's typecheck pass — its `typecheck` script, or web's `tsc -b` — is reachable from its `validate`, and the root chain runs each client's `validate`). Runs in `validate`. |
|
|
351
|
-
| `npm run verify:dep-lockstep` | Guards the "one version per install-crossing dependency" invariant (#1896). v2 is not a workspace, so a client's test project compiles the shared first-party TypeScript — `core/`, `test-servers/src`, and the root-owned `vitest.shared.mts`, all of which resolve their dependencies from the **root** install — alongside the client's own sources, putting the same package in one `tsc` program twice. At the same version that's harmless; skewed, TypeScript must relate two structurally-distinct copies of every type, which for a recursive-generic surface is exponential (zod `4.3.6` vs `4.4.3` exhausted the 4GB tsc heap in `clients/web`). Derives its candidate set from **what actually enters each program** (#1965) — every client tsconfig project listed with `tsc --listFilesOnly` via the shared `scripts/lib/tsc-program.mjs`, each resolved `node_modules` file mapped to its owning install, keeping the packages that reach one program from two installs (a package whose declarations arrive only through another package's `.d.ts`, as `@modelcontextprotocol/sdk`'s do, is invisible to a scan of first-party imports). Prices each copy from the lockfile entry for the exact install path the program resolved, compares only the installs that met in one program, and **fails deny-by-default** on any disagreement not in the annotated `TOLERATED_SKEW` allowlist — empty today — with an allowlisted package tolerated only *within a major version*. Runs in `validate`.
|
|
352
|
-
| `npm run ci` | **Mandatory pre-push command.** `validate` → `coverage` → `verify:build-gate` → `smoke` → Storybook. A true superset of GitHub CI. |
|
|
353
|
-
| `npm run pack:verify` | Publish smoke — see [Publishing](#publishing). |
|
|
354
|
-
|
|
355
|
-
Per-client scripts exist too (`validate:web`, `coverage:cli`, `smoke:tui`, …), plus root `validate:core` / `format:core` for the shared `core/` package, `format:scripts` for the root `scripts/` tooling, and `format:shared` / `lint:shared` for the root "shared" surface (`test-servers/src/**`, `vitest.shared.mts`, the root `eslint.config.js`). Run `npm run format` before committing — the root `format` fixes `core/`, the root `scripts/`, the shared surface, and every client; `validate` runs the non-fixing `format:check` and fails CI on any unformatted file.
|
|
356
|
-
|
|
357
|
-
**Linting is type-aware.** All five ESLint scopes (`clients/{web,cli,tui,launcher}` plus the root `core/` + shared gate) enable `@typescript-eslint/no-floating-promises` at `error`, so a promise that is neither awaited, returned, `.catch(…)`-terminated, nor explicitly discarded with `void` fails `lint` — and therefore `validate` ([#1959](https://github.com/modelcontextprotocol/inspector/issues/1959)). The rule needs type information, so each scope's config names a parser project; the root scope's is **`tsconfig.lint.json`**, a lint-only project covering `core/**`, `test-servers/src/**`, and `vitest.shared.mts`, which have no tsconfig of their own. It emits nothing and changes no typecheck — but a new first-party TS location added to the root lint scope must be added to its `include`. See **TypeScript instructions** in [`AGENTS.md`](./AGENTS.md) for when `void` is acceptable.
|
|
358
|
-
|
|
359
|
-
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`](./AGENTS.md).
|
|
360
|
-
|
|
361
|
-
## Publishing
|
|
362
|
-
|
|
363
|
-
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.
|
|
364
|
-
|
|
365
|
-
### What ships, and the packaging invariants
|
|
366
|
-
|
|
367
|
-
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`:
|
|
368
|
-
|
|
369
|
-
- **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's `tsc` already emit none. Maps are ~half the unpacked size and aren't needed at runtime — debug via `npm run dev` on the source.
|
|
370
|
-
- **`clients/web/build` ships via `clients/web/.npmignore`.** `clients/web/.gitignore` lists `build/`, and npm's packlist honors that nested `.gitignore` over the root `"files"` allowlist — so the prod web-server runner was silently missing from the tarball while `clients/web/dist` slipped through (its `.gitignore` only lists `dist-ssr`). `clients/web/.npmignore` overrides the `.gitignore` for publishing so both `build/` (runner) and `dist/` (SPA) ship. The other clients don't need this — none ship a nested `.gitignore`.
|
|
371
|
-
- **`clients/web/static` ships the MCP Apps sandbox proxy.** `clients/web/static/sandbox_proxy.html` is a committed source file (not a build artifact), read from disk at runtime by `clients/web/server/sandbox-controller.ts` as `<runner dir>/../static/sandbox_proxy.html`. It was missing from the root `"files"` allowlist entirely, so every published build failed the Apps tab with **"Sandbox not loaded"** ([#1859](https://github.com/modelcontextprotocol/inspector/issues/1859)) while working fine in the repo. Because the path is resolved _relative to_ `clients/web/build`, the directory must ship at that exact location — `pack:verify` asserts both the tarball entry and the installed-on-disk path.
|
|
372
|
-
- **A dependency that renders React is bundled, not externalized.** An externalized package resolves its own `react` from wherever npm placed **it** in the consumer's tree, which is not necessarily where the bundle resolves ours — npm places a package beside a React satisfying *its* peer range, and those ranges are looser than ours. `ink-form` and `ink-scroll-view` declare `">=18"`, so a project holding React 18 satisfies them and gets them hoisted while the Inspector's React 19 nests underneath: two React copies, and the TUI dies with `TypeError: Cannot read properties of null (reading 'useState')` the moment a tool test form or a scroll view mounts ([#1952](https://github.com/modelcontextprotocol/inspector/issues/1952)). Both are therefore inlined by `clients/tui/tsup.config.ts` and are **not** root dependencies: the tarball ships their code inside `clients/tui/build/index.js` rather than having consumers install them. Bundling also pins their transitive deps to what this repo's install resolved (notably `ink-select-input@6` via `overrides`, which npm ignores for a package installed as a dependency). **`ink` is the one exception, on cost:** bundling it works but adds ~1.4 MB (`react-reconciler` and `yoga-layout` come along, plus a `createRequire` banner for the inlined CJS), so it stays external — *not* because its `">=19"` peer makes it safe, which it does not. What keeps that tolerable is the root `react` range: `"^19.0.0"` is deliberately open to the whole major so npm can dedupe our React with whatever React 19 a consumer pins, leaving an external `ink` on the same copy the bundle uses. **Narrowing that range reopens the bug for the renderer itself** — `clients/tui/__tests__/tsupConfig.test.ts` pins it to `ink`'s peer floor, and guards the rest of the split; see the [TUI README](./clients/tui/README.md#bundling-react-rendering-dependencies-must-be-inlined-1952).
|
|
373
|
-
- **A single version number, read from the root `package.json`.** The Inspector ships as one package with one version, so only the **root** `package.json` carries a `version` — the four `clients/*/package.json`s deliberately have none. Every Node client (CLI, TUI, and the web backend) resolves the version through the shared `readInspectorVersion()` reader in `core/node/version.ts`, which walks up to the root manifest (always present in the tarball). No client `package.json` is read at runtime, so none needs to ship. The web **browser** can't read the filesystem; it gets its version from the backend via `GET /api/config` (see [#1639](https://github.com/modelcontextprotocol/inspector/issues/1639)).
|
|
374
|
-
|
|
375
|
-
### `npm run pack:verify` — publish smoke against the real tarball
|
|
376
|
-
|
|
377
|
-
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 pack`s 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.
|
|
378
|
-
|
|
379
|
-
### Cutting a release
|
|
380
|
-
|
|
381
|
-
Publishing is automated by two release-gated jobs in [`.github/workflows/main.yml`](.github/workflows/main.yml) (`github.event_name == 'release'`, both `needs: build`):
|
|
382
|
-
|
|
383
|
-
- **`publish`** — the npm package. Runs `npm run pack:verify` as the pre-publish gate, asserts the release tag matches the root `package.json` version, then `npm publish --access public --provenance` — a single `npm publish` (v2 is not an npm workspace, so there is no v1-style `publish-all`/`--workspaces`), with a signed provenance attestation via GitHub OIDC (`id-token: write`, `environment: release`, `NPM_TOKEN`).
|
|
384
|
-
- **`publish-github-container-registry`** — the container image (see [Docker](#docker)).
|
|
385
|
-
|
|
386
|
-
A v2 release is cut from **`main`**, after the milestone's work has been merged there from `v2/main` — not from `v2/main` itself. (The v1 line releases independently from `v1/main` to the `v1-latest` tag and never touches `main`; see [Repo status](#mcp-inspector).)
|
|
387
|
-
|
|
388
|
-
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 three steps.
|
|
389
|
-
|
|
390
|
-
**1. Bump on `v2/main`, before the milestone merge.** The bump is part of the milestone's work, so it belongs on the develop branch and flows into `main` with everything else:
|
|
391
|
-
|
|
392
|
-
Substitute the real issue number and release below — the commands are written to be copy-pasteable as-is (a `2.2.0` → `2.3.0` minor bump):
|
|
393
|
-
|
|
394
|
-
```bash
|
|
395
|
-
git checkout -b v2/chore/2010-bump-2-3-0 v2/main
|
|
396
|
-
npm version minor --no-git-tag-version # or major / patch; bump only, no tag
|
|
397
|
-
# PR → v2/main
|
|
398
|
-
```
|
|
399
|
-
|
|
400
|
-
⚠️ **`--no-git-tag-version` is load-bearing.** A bare `npm version` also tags, and the tag would land on a `v2/main` commit — but the release must be cut from `main`, so the tag has to point at the merge commit there (step 3). Tagging here creates a tag on a commit that is never released.
|
|
43
|
+
v2 is **not** an npm workspace — each client under `clients/*` keeps its own `package.json` and `node_modules`, and shared code lives in `core/`, consumed via a `@inspector/core` build-time alias. **Every runtime dependency `core/` imports is declared once, in the repo-root `package.json`**, and each client declares only what that client alone consumes — its UI stack, its bundler-inlined packages, its dev tooling — which leaves `clients/cli` and `clients/launcher` with no runtime dependencies of their own. What that means for adding a dependency (root vs. client, `dependencies` vs. `devDependencies`, and the bundler `external` lists) is in the [`local-dev` skill](./.claude/skills/local-dev/SKILL.md).
|
|
401
44
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
Between steps 1 and 2 the two branches **do** differ, and that is expected, not drift: `v2/main` reads the version being built while `main` still reads the one currently released. What this ordering removes is *post-release* drift — once the milestone merge lands they agree again, and `v2/main` is never left **behind** `main`. If you see `v2/main` ahead of `main`, a release is in flight; if you see it behind, something went wrong.
|
|
405
|
-
|
|
406
|
-
**3. Tag the `main` commit and draft the Release:**
|
|
45
|
+
## Project layout
|
|
407
46
|
|
|
408
|
-
```bash
|
|
409
|
-
git fetch origin main
|
|
410
|
-
git tag 2.3.0 origin/main && git push origin 2.3.0
|
|
411
|
-
# then draft & publish a GitHub Release for that tag → triggers `publish`
|
|
412
47
|
```
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
```bash
|
|
429
|
-
# run the web UI (reads the auth token from the container logs)
|
|
430
|
-
docker run --rm -p 127.0.0.1:6274:6274 ghcr.io/modelcontextprotocol/inspector
|
|
431
|
-
|
|
432
|
-
# or build the image locally
|
|
433
|
-
docker build -t mcp-inspector .
|
|
434
|
-
docker run --rm -p 127.0.0.1:6274:6274 mcp-inspector
|
|
48
|
+
inspector/
|
|
49
|
+
├── clients/
|
|
50
|
+
│ ├── web/ Web client (Vite + React + Mantine). src/ = browser app; server/ = Node backend
|
|
51
|
+
│ ├── cli/ CLI client (tsup bundle, @inspector/core alias)
|
|
52
|
+
│ ├── tui/ TUI client (Ink + React, tsup bundle)
|
|
53
|
+
│ └── launcher/ Shared launcher — provides the `mcp-inspector` bin, dispatches to web/cli/tui
|
|
54
|
+
├── core/ Shared code consumed via the `@inspector/core` alias (no package.json)
|
|
55
|
+
├── test-servers/ Composable MCP test servers + fixtures used by integration and smoke tests
|
|
56
|
+
├── scripts/ Root build/verify tooling (install cascade, smokes, the verify:* guards)
|
|
57
|
+
├── docs/ Task-oriented guides — see below
|
|
58
|
+
├── specification/ Design/build specifications
|
|
59
|
+
├── .claude/skills/ Agent skills: the repo's procedures, invokable by name
|
|
60
|
+
├── AGENTS.md Contribution rules for agents AND humans
|
|
61
|
+
└── README.md You are here
|
|
435
62
|
```
|
|
436
63
|
|
|
437
|
-
|
|
64
|
+
Each client has its own README with client-specific detail:
|
|
65
|
+
[web](./clients/web/README.md) · [cli](./clients/cli/README.md) · [tui](./clients/tui/README.md) · [launcher](./clients/launcher/README.md).
|
|
438
66
|
|
|
439
|
-
|
|
440
|
-
docker run --rm -p 127.0.0.1:6274:6274 -p 127.0.0.1:6275:6275 \
|
|
441
|
-
ghcr.io/modelcontextprotocol/inspector
|
|
442
|
-
```
|
|
67
|
+
## Documentation
|
|
443
68
|
|
|
444
|
-
|
|
69
|
+
| Guide | Covers |
|
|
70
|
+
| --- | --- |
|
|
71
|
+
| [Architecture](./docs/architecture.md) | The `@inspector/core` shared package, and the web client's "dumb components" + Storybook approach |
|
|
72
|
+
| [Testing and the quality gate](./docs/quality-gate.md) | What each `validate` / `coverage` / `smoke` / `verify:*` script covers, the GitHub-CI-vs-local-gate split, and the supported browsers |
|
|
73
|
+
| [Writing a skill](./docs/skill-authoring.md) | How to write a skill description that actually fires, and eval cases that measure it — the case shapes that work, and the tuning loop |
|
|
74
|
+
| [Test servers](./docs/test-servers.md) | The composable test servers and the showcase config for every feature — what to run, what to click, and what the broken build did |
|
|
75
|
+
| [Publishing](./docs/publishing.md) | What ships in the tarball, the packaging invariants, and `pack:verify` |
|
|
76
|
+
| [Docker](./docs/docker.md) | Running the container image — ports, volumes, and where secrets go |
|
|
77
|
+
| [Migrating from v1 to v2](./docs/v1-to-v2-migration.md) | CLI flag mapping, `--config` vs. `--catalog`, the Node engine bump, env-var renames |
|
|
78
|
+
| [MCP server configuration](./docs/mcp-server-configuration.md) | Which server(s) the Inspector connects to, and the config file format |
|
|
79
|
+
| [Reviewing an MCP App](./docs/mcp-app-review.md) | The CLI-first → one-shot-web recipe for automated App-tool review |
|
|
80
|
+
| [Launcher and config consolidation](./docs/launcher-config-consolidation-plan.md) | Why the launcher runs a client in-process rather than spawning it |
|
|
445
81
|
|
|
446
|
-
|
|
82
|
+
## Testing and the quality gate
|
|
447
83
|
|
|
448
|
-
|
|
84
|
+
Each client self-validates from its own folder; the root scripts chain them. There is **no** aggregate root `test` script.
|
|
449
85
|
|
|
450
86
|
```bash
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
87
|
+
npm run validate # fast inner loop: format:check + lint + typecheck + build + unit tests
|
|
88
|
+
npm run coverage # the per-file ≥90% gate (lines/statements/functions/branches)
|
|
89
|
+
npm run local:gate # MANDATORY before pushing — a strict superset of GitHub CI
|
|
454
90
|
```
|
|
455
91
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
**Upgrading from an image before this fix?** Earlier images did not create `/home/node/.mcp-inspector`, so Docker created the volume's mount point as `root` and the non-root `node` user couldn't write to it. An **empty** volume repairs itself on the first run of a current image (Docker applies the image directory's ownership to an empty volume), but one that already has files in it keeps its old `root` ownership and still fails with `EACCES`. Fix it once:
|
|
459
|
-
|
|
460
|
-
```bash
|
|
461
|
-
docker run --rm -u 0 --entrypoint chown \
|
|
462
|
-
-v mcp-inspector-data:/data ghcr.io/modelcontextprotocol/inspector \
|
|
463
|
-
-R node:node /data
|
|
464
|
-
```
|
|
92
|
+
`npm run local:gate` chains every check below, plus the smokes and the Storybook tests. [Testing and the quality gate](./docs/quality-gate.md) owns the stage list and says what each one covers and why two are local-only; [`AGENTS.md`](./AGENTS.md) holds the testing rules themselves.
|
|
465
93
|
|
|
466
|
-
|
|
94
|
+
## Contributing — `AGENTS.md`, `CLAUDE.md`, and the skills
|
|
467
95
|
|
|
468
|
-
|
|
96
|
+
**[`AGENTS.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 **rules**: the version/label conventions, 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.
|
|
469
97
|
|
|
470
|
-
|
|
98
|
+
The repo's **procedures** — multi-step recipes with commands and live IDs — live in [`.claude/skills/`](./.claude/skills) instead, one directory per procedure, so they are loaded only when the task calls for them. They are ordinary committed Markdown: an agent that doesn't understand skills can read them, and `AGENTS.md` carries an index of what exists. Claude Code users invoke one by name (`/release`, `/issue-triage`, …).
|
|
471
99
|
|
|
472
|
-
`CLAUDE.md` is the entry point
|
|
100
|
+
`CLAUDE.md` is the entry point [Claude Code](https://claude.com/claude-code) loads automatically; it includes `AGENTS.md`, so 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.
|
|
473
101
|
|
|
474
|
-
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>`.
|
|
102
|
+
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>`. External contributions are accepted as **issues, not pull requests** — see [`CONTRIBUTING.md`](./CONTRIBUTING.md).
|
|
475
103
|
|
|
476
104
|
## License
|
|
477
105
|
|