From 3075d0aaf34273b2ca1cdaa8507f97eb7085efe8 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Fri, 10 Jul 2026 12:36:26 -0400 Subject: [PATCH 1/6] build(publish): fix v2 single-package bundle and add pack:verify MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prepare the single `@modelcontextprotocol/inspector` tarball for publishing and add a script that verifies the real published package end to end. - Add `scripts/pack-and-verify.mjs` (`npm run pack:verify`): build, `npm pack`, install the tarball into a clean consumer, and drive the installed `mcp-inspector` bin (web/cli/tui) — catching "works in --dev, breaks under npx" packaging/path failures the in-repo smokes miss. Local/release tool (install needs network), not wired into the fast validate/ci loop. - Disable source maps in the cli/tui/web-runner tsup configs (~halves the unpacked payload; runtime doesn't need them). - Add `clients/web/.npmignore` so `clients/web/build` (prod web-server runner) ships — npm's packlist was honoring the nested `.gitignore` `build/` entry over the root "files" allowlist, dropping the runner while `dist/` slipped through. - Ship `clients/cli/package.json` and `clients/tui/package.json`: both bundles read their own package.json at runtime (CLI client identity, TUI header), so the installed CLI crashed on connect and the installed TUI crashed on launch without them. Document the packaging invariants and pack:verify in the launcher README. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw --- clients/cli/tsup.config.ts | 4 +- clients/launcher/README.md | 14 +- clients/tui/tsup.config.ts | 4 +- clients/web/.npmignore | 19 ++ clients/web/tsup.runner.config.ts | 4 +- package.json | 3 + scripts/pack-and-verify.mjs | 327 ++++++++++++++++++++++++++++++ 7 files changed, 371 insertions(+), 4 deletions(-) create mode 100644 clients/web/.npmignore create mode 100644 scripts/pack-and-verify.mjs diff --git a/clients/cli/tsup.config.ts b/clients/cli/tsup.config.ts index 63278f07..752ab878 100644 --- a/clients/cli/tsup.config.ts +++ b/clients/cli/tsup.config.ts @@ -10,7 +10,9 @@ export default defineConfig({ format: ['esm'], outDir: 'build', clean: true, - sourcemap: true, + // No source maps in the published bundle — they roughly double the on-disk + // size and aren't needed at runtime (debug via `npm run dev` on the source). + sourcemap: false, target: 'node22', platform: 'node', // Bundle core source; leave npm deps external. diff --git a/clients/launcher/README.md b/clients/launcher/README.md index 085dc7ee..42b42b11 100644 --- a/clients/launcher/README.md +++ b/clients/launcher/README.md @@ -96,7 +96,19 @@ repo-root `validate:launcher` simply delegates here (`cd clients/launcher && npm ## Publishing -The root `@modelcontextprotocol/inspector` package ships as one fat tarball: `npm run build` at the repo root builds all clients, 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. +The root `@modelcontextprotocol/inspector` package ships as one fat tarball with a **single version number** (no separate `-web` / `-cli` / `-tui` / `-core` packages): `npm run build` at the repo root builds all clients, 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 the packlist — do not remove them without re-running `npm run pack:verify`: + +- **No source maps.** The client bundlers set `sourcemap: false` (`clients/cli/tsup.config.ts`, `clients/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. +- **`clients/web/build` is packed 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`. +- **`clients/cli/package.json` and `clients/tui/package.json` ship** because those bundles read their own `package.json` at runtime for the client identity / TUI header (`name`, `description`, `version`) relative to the bundle location. Without them the installed CLI crashes on connect and the installed TUI crashes on launch — even though both work in-repo, where the files are present. + +### `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 pack`s the publishable tarball (asserting no source maps ship and that `clients/web/{build,dist}` + the two client `package.json`s are present), installs the tarball into a clean throwaway consumer (real `npm install `, runs `postinstall`), 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 @modelcontextprotocol/inspector`" path/packaging failures (exactly how the two `package.json` reads and the `web/build` omission above were found). It requires network access (the install pulls runtime deps), so it is a local / release check, not part of the fast `validate` loop. ## Architecture diff --git a/clients/tui/tsup.config.ts b/clients/tui/tsup.config.ts index 568d97d7..c998221a 100644 --- a/clients/tui/tsup.config.ts +++ b/clients/tui/tsup.config.ts @@ -10,7 +10,9 @@ export default defineConfig({ format: ['esm'], outDir: 'build', clean: true, - sourcemap: true, + // No source maps in the published bundle — they roughly double the on-disk + // size and aren't needed at runtime (debug via `npm run dev` on the source). + sourcemap: false, target: 'node22', platform: 'node', noExternal: [/^@inspector\/core/], diff --git a/clients/web/.npmignore b/clients/web/.npmignore new file mode 100644 index 00000000..133cadc2 --- /dev/null +++ b/clients/web/.npmignore @@ -0,0 +1,19 @@ +# npm's packlist uses THIS file instead of .gitignore when deciding what to +# publish from clients/web. It exists to fix a packaging bug: .gitignore here +# lists `build/` (a git artifact we never commit), and npm honored that +# exclusion even though the root package.json "files" field explicitly lists +# `clients/web/build`. The result was the prod web-server runner silently +# missing from the published @modelcontextprotocol/inspector tarball while +# `clients/web/dist` slipped through — so `mcp-inspector --web` had no server to +# load. (The other clients don't hit this because none of them ship a nested +# .gitignore.) +# +# Crucially this file does NOT list `build` or `dist`, so both the prod runner +# (build/) and the SPA (dist/) are packed. The root "files" allowlist already +# restricts publishing to those two directories, so everything else in +# clients/web (src, configs, node_modules, coverage, storybook-static) stays out +# regardless — the entries below are just belt-and-suspenders. +node_modules +coverage +storybook-static +dist-ssr diff --git a/clients/web/tsup.runner.config.ts b/clients/web/tsup.runner.config.ts index 6841de44..a0fb8a2e 100644 --- a/clients/web/tsup.runner.config.ts +++ b/clients/web/tsup.runner.config.ts @@ -10,7 +10,9 @@ export default defineConfig({ format: ['esm'], outDir: 'build', clean: true, - sourcemap: true, + // No source maps in the published bundle — they roughly double the on-disk + // size and aren't needed at runtime (debug via `npm run dev` on the source). + sourcemap: false, target: 'node22', platform: 'node', noExternal: [/^@inspector\/core/], diff --git a/package.json b/package.json index 3d069649..359605f2 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,9 @@ "clients/web/build", "clients/web/dist", "clients/cli/build", + "clients/cli/package.json", "clients/tui/build", + "clients/tui/package.json", "scripts/install-clients.mjs" ], "scripts": { @@ -54,6 +56,7 @@ "smoke:tui": "node scripts/smoke-tui.mjs", "smoke:web": "node scripts/smoke-web.mjs", "smoke:launcher": "node scripts/smoke-launcher.mjs", + "pack:verify": "node scripts/pack-and-verify.mjs", "prepack": "npm run build", "postinstall": "node scripts/install-clients.mjs" }, diff --git a/scripts/pack-and-verify.mjs b/scripts/pack-and-verify.mjs new file mode 100644 index 00000000..aa44f51f --- /dev/null +++ b/scripts/pack-and-verify.mjs @@ -0,0 +1,327 @@ +#!/usr/bin/env node +/** + * Pack-and-verify: build the exact tarball npm would publish, install it into a + * clean throwaway consumer, and drive the real `mcp-inspector` bin against that + * installed package — web, cli, and tui (#1636). + * + * Why this exists, and why the `smoke:*` scripts aren't enough: the smokes run + * against the in-repo build tree (`clients//build`, `clients/web/dist`) via the + * launcher's relative paths. That tree is NOT the published package. We have + * repeatedly been bitten by things that work in `--dev` / in-repo yet break for + * a real consumer running `npx @modelcontextprotocol/inspector …`, because: + * + * - the `files` allowlist (root + each nested client `package.json`) silently + * omits something the runtime needs (this is exactly how `clients/web/build` + * was missing from the tarball while `clients/web/dist` slipped through); + * - path resolution that is correct relative to the repo differs once the code + * lives under `node_modules/@modelcontextprotocol/inspector/…`; + * - the `postinstall` client-install cascade misbehaves for an end user. + * + * This script closes that gap by exercising the package the way npm publishes + * and a user installs it, end to end. It: + * + * 1. builds every client (`npm run build`); + * 2. packs the publishable tarball (`npm pack`) and inspects its file list — + * asserting NO source maps ship and that `clients/web/{build,dist}` are + * both present (the two packaging fixes this work landed); + * 3. installs that tarball into a fresh temp dir (real `npm install `, + * which runs the package's `postinstall`); + * 4. runs the installed `mcp-inspector` bin: `--help`, `--cli`/`--tui` help + * dispatch, a real `--cli` `tools/list` over stdio, and a prod `--web` boot + * that must serve `/` (HTTP 200) with the injected auth-token global from + * the shipped `dist` — all from the INSTALLED location, not the repo. + * + * Exits non-zero on the first failure. Requires network access (step 3 pulls the + * package's runtime dependencies from the registry) — it is a local / release + * verification tool, not part of the fast `validate` loop. + * + * Usage: `npm run pack:verify` (from the repo root). + */ + +import { spawn, spawnSync } from "node:child_process"; +import { mkdtempSync, rmSync, existsSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; +import { setTimeout as delay } from "node:timers/promises"; + +const repoRoot = resolve(import.meta.dirname, ".."); +const testServer = join(repoRoot, "test-servers", "build", "test-server-stdio.js"); + +// Mirrors INSPECTOR_API_TOKEN_GLOBAL in core/mcp/remote/constants.ts; kept as a +// literal because this plain .mjs script can't import the TS source. +const TOKEN_GLOBAL = "__INSPECTOR_API_TOKEN__"; + +function fail(message) { + console.error(`\npack:verify FAILED — ${message}`); + process.exit(1); +} + +function step(message) { + console.log(`\npack:verify — ${message}`); +} + +/** Run a command to completion, inheriting stdio. Returns the exit status. */ +function runInherit(command, args, cwd = repoRoot) { + const r = spawnSync(command, args, { + cwd, + stdio: "inherit", + shell: process.platform === "win32", + }); + return r.status; +} + +/** Build the bundled stdio test server if it isn't present yet. */ +function ensureTestServer() { + if (existsSync(testServer)) return; + step("building test-servers (missing build output)..."); + const status = runInherit("npx", ["tsc", "-p", "test-servers", "--noCheck"]); + if (status !== 0 || !existsSync(testServer)) { + fail("could not build the stdio test server (test-servers/build/test-server-stdio.js)"); + } +} + +// --------------------------------------------------------------------------- +// 1. Build every client exactly as the publish would. +// --------------------------------------------------------------------------- +step("building all clients (npm run build)..."); +if (runInherit("npm", ["run", "build"]) !== 0) { + fail("`npm run build` failed"); +} + +// --------------------------------------------------------------------------- +// 2. Pack the publishable tarball and inspect its file list. +// --ignore-scripts: we just built, so skip the prepack rebuild AND keep +// stdout clean JSON (prepack build output would otherwise pollute it). +// --------------------------------------------------------------------------- +step("packing the publishable tarball (npm pack)..."); +const pack = spawnSync( + "npm", + ["pack", "--json", "--ignore-scripts", "--pack-destination", tmpdir()], + { cwd: repoRoot, encoding: "utf8" }, +); +if (pack.status !== 0) { + fail(`\`npm pack\` failed:\n${pack.stderr || pack.stdout}`); +} +let packInfo; +try { + packInfo = JSON.parse(pack.stdout)[0]; +} catch { + fail(`could not parse \`npm pack --json\` output:\n${pack.stdout}`); +} +const tarball = join(tmpdir(), packInfo.filename.replace(/^.*[/\\]/, "")); +if (!existsSync(tarball)) { + fail(`packed tarball not found at ${tarball}`); +} +const tarredPaths = packInfo.files.map((f) => f.path); + +// 2a. No source maps in the published bundle. +const maps = tarredPaths.filter((p) => p.endsWith(".map")); +if (maps.length > 0) { + fail( + `${maps.length} source map(s) leaked into the tarball — they should be disabled ` + + `in the client bundlers:\n ${maps.slice(0, 10).join("\n ")}`, + ); +} + +// 2b. Runtime files that are easy to omit from the packlist and only fail once +// installed: both web artifacts — the prod server runner (build) AND the SPA +// (dist) — plus the TUI's package.json, which the TUI bundle reads at +// runtime for its header (name/description/version). `clients/web/build` was +// previously dropped by the nested .gitignore; `clients/tui/package.json` +// was never listed, so the installed TUI crashed on launch. +for (const required of [ + "clients/web/build/index.js", + "clients/web/dist/index.html", + "clients/cli/package.json", + "clients/tui/package.json", +]) { + if (!tarredPaths.includes(required)) { + fail( + `expected \`${required}\` in the published tarball but it is missing — ` + + `check the "files" field in clients/web/package.json`, + ); + } +} +console.log( + `pack:verify — tarball OK: ${tarredPaths.length} files, no source maps, ` + + `clients/web/{build,dist} present (${(packInfo.unpackedSize / 1048576).toFixed(2)} MB unpacked)`, +); + +// --------------------------------------------------------------------------- +// 3. Install the tarball into a clean throwaway consumer (real npm install, +// runs the package's postinstall). +// --------------------------------------------------------------------------- +const work = mkdtempSync(join(tmpdir(), "pack-verify-")); +let cleanupTarball = true; +try { + ensureTestServer(); + step(`installing the tarball into a clean consumer at ${work} (pulls runtime deps)...`); + writeFileSync( + join(work, "package.json"), + JSON.stringify({ name: "pack-verify-consumer", private: true, version: "0.0.0" }), + ); + const install = runInherit( + "npm", + ["install", tarball, "--no-audit", "--no-fund", "--loglevel", "error"], + work, + ); + if (install !== 0) { + fail("`npm install ` into the throwaway consumer failed"); + } + + const installedPkg = join(work, "node_modules", "@modelcontextprotocol", "inspector"); + const bin = join(work, "node_modules", ".bin", process.platform === "win32" ? "mcp-inspector.cmd" : "mcp-inspector"); + if (!existsSync(bin)) { + fail(`installed \`mcp-inspector\` bin not found at ${bin}`); + } + // Confirm the two packaging fixes survived install onto disk. + for (const required of [ + join(installedPkg, "clients", "web", "build", "index.js"), + join(installedPkg, "clients", "web", "dist", "index.html"), + join(installedPkg, "clients", "launcher", "build", "index.js"), + join(installedPkg, "clients", "cli", "package.json"), + join(installedPkg, "clients", "tui", "package.json"), + ]) { + if (!existsSync(required)) { + fail(`expected installed file missing: ${required}`); + } + } + + /** Run the installed bin. Returns { status, output }. */ + const runBin = (args, extraEnv = {}) => { + const r = spawnSync(bin, args, { + cwd: work, + encoding: "utf8", + env: { ...process.env, ...extraEnv }, + shell: process.platform === "win32", + }); + return { status: r.status, output: `${r.stdout ?? ""}${r.stderr ?? ""}`, stdout: r.stdout ?? "" }; + }; + + // --------------------------------------------------------------------------- + // 4. Drive the installed bin. + // --------------------------------------------------------------------------- + // 4a. Help dispatch for each mode — proves the launcher resolved every client + // build from its installed location. + step("verifying help dispatch (--help, --cli --help, --tui --help)..."); + const helpChecks = [ + { args: ["--help"], marker: "Mode flags (--web, --cli, --tui)", label: "launcher --help" }, + { args: ["--cli", "--help"], marker: "Usage: inspector-cli", label: "--cli dispatch" }, + { args: ["--tui", "--help"], marker: "Usage: mcp-inspector-tui", label: "--tui dispatch" }, + ]; + for (const { args, marker, label } of helpChecks) { + const r = runBin(args); + if (r.status !== 0) { + fail(`\`${args.join(" ")}\` exited ${r.status}\n${r.output.slice(0, 800)}`); + } + if (!r.output.includes(marker)) { + fail(`\`${args.join(" ")}\` (${label}) did not print "${marker}"\n${r.output.slice(0, 800)}`); + } + } + + // 4b. Real CLI connect over stdio from the installed package: tools/list must + // return the bundled test server's tools. Exercises launcher → cli → core + // → stdio transport path from node_modules. + step("verifying `--cli` tools/list over stdio from the installed package..."); + const catalogPath = join(work, "catalog.json"); + writeFileSync( + catalogPath, + JSON.stringify({ + mcpServers: { + test: { type: "stdio", command: process.execPath, args: [testServer] }, + }, + }), + ); + const list = runBin(["--cli", "--catalog", catalogPath, "--server", "test", "--method", "tools/list"]); + if (list.status !== 0) { + fail(`\`--cli … tools/list\` exited ${list.status}\n${list.output}`); + } + let parsed; + try { + parsed = JSON.parse(list.stdout); + } catch { + fail(`\`--cli … tools/list\` did not return JSON on stdout:\n${list.stdout}`); + } + if (!(parsed.tools ?? []).some((t) => t.name === "echo")) { + fail(`\`--cli … tools/list\` missing expected "echo" tool`); + } + + // 4c. Prod `--web` boot from the installed package — THE critical packaging + // path: the runner must locate and serve the shipped `dist` (not rebuild + // it) and inject the auth token. Run non-blocking and poll `/`. + await verifyWeb(bin, work); + + console.log( + "\npack:verify OK — published tarball installs clean and the real bin drives " + + "web (prod / served dist), cli (stdio tools/list), and tui (help) end to end.", + ); +} catch (err) { + cleanupTarball = true; + fail(err instanceof Error ? err.message : String(err)); +} finally { + rmSync(work, { recursive: true, force: true }); + if (cleanupTarball) rmSync(tarball, { force: true }); +} + +/** + * Boot the installed `mcp-inspector --web` (prod), wait for it to listen, and + * assert `GET /` returns 200 with the injected token global served from the + * shipped `dist`. Kills the server before returning. + */ +async function verifyWeb(bin, cwd) { + step("verifying prod `--web` serves the shipped dist from the installed package..."); + const host = "127.0.0.1"; + const port = process.env.PACK_VERIFY_WEB_PORT ?? "6399"; + const token = "pack-verify-token"; + const child = spawn(bin, ["--web"], { + cwd, + env: { + ...process.env, + CLIENT_PORT: port, + HOST: host, + MCP_INSPECTOR_API_TOKEN: token, + MCP_AUTO_OPEN_ENABLED: "false", + }, + stdio: ["ignore", "inherit", "inherit"], + shell: process.platform === "win32", + }); + + let exited = false; + let exitCode = null; + child.on("exit", (code) => { + exited = true; + exitCode = code; + }); + const stop = () => { + if (!exited) child.kill("SIGTERM"); + }; + + try { + let res = null; + for (let attempt = 0; attempt < 120 && !res; attempt++) { + if (exited) { + fail(`\`--web\` exited (code ${exitCode}) before serving — see output above`); + } + try { + res = await fetch(`http://${host}:${port}/`); + } catch { + await delay(500); + } + } + if (!res) { + fail("`--web` server did not start within 60s"); + } + if (res.status !== 200) { + fail(`\`--web\` GET / returned HTTP ${res.status}, expected 200`); + } + const body = await res.text(); + if (!body.includes(TOKEN_GLOBAL)) { + fail(`\`--web\` served HTML is missing the ${TOKEN_GLOBAL} global (token not injected)`); + } + if (!body.includes(token)) { + fail("`--web` served HTML is missing the injected auth-token value"); + } + } finally { + stop(); + } +} From 79b80b459d6d695dc36152d62677a46cdf5e43fb Mon Sep 17 00:00:00 2001 From: cliffhall Date: Fri, 10 Jul 2026 15:46:51 -0400 Subject: [PATCH 2/6] docs: add root README; move publishing docs out of launcher README CLAUDE.md included @./README.md, which did not exist. Add a root README covering project layout, setup, the @inspector/core shared package, the web "dumb components" + Storybook approach, test-servers, build, the testing/quality gate, publishing, and how AGENTS.md/CLAUDE.md govern contributions for humans and agents alike. Move the publishing/packaging documentation (single-package tarball, "files" invariants, pack:verify) from the launcher README to the root README, where whole-repo packaging belongs; the launcher README now points there. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw --- README.md | 159 +++++++++++++++++++++++++++++++++++++ clients/launcher/README.md | 14 +--- 2 files changed, 160 insertions(+), 13 deletions(-) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 00000000..f422f2c0 --- /dev/null +++ b/README.md @@ -0,0 +1,159 @@ +# MCP Inspector + +A developer tool for inspecting [Model Context Protocol](https://modelcontextprotocol.io) (MCP) servers. It ships as a single package, `@modelcontextprotocol/inspector`, that provides three ways to inspect a server: + +- **Web** — a Vite + React + [Mantine](https://mantine.dev) 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](https://github.com/vadimdemedes/ink). + +All three run through one global `mcp-inspector` binary: + +```bash +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`). The `main` branch is the legacy v1 implementation (bug fixes only). v2 will eventually replace `main`. See [`AGENTS.md`](./AGENTS.md) for 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](#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](./clients/web/README.md) · [cli](./clients/cli/README.md) · [tui](./clients/tui/README.md) · [launcher](./clients/launcher/README.md). + +## Setup + +Requires Node `>=22.19.0`. + +```bash +npm install # root install; postinstall cascades into every client +``` + +- **Fresh clone:** run `npm install` at the repo root. +- **After a pull that changes a client's dependencies:** re-run `npm install` at 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): + +```bash +cd clients/web && npm run dev +``` + +The launcher-driven scripts below run the **built** launcher, so build first (`npm run build`): + +```bash +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.alias` in their `tsup.config.ts` maps `@inspector/core` → the repo `core/` directory, and `noExternal: [/^@inspector\/core/]` inlines it into the bundle. +- **Web:** the same alias in `clients/web/vite.config.ts` for 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](https://github.com/modelcontextprotocol/inspector/issues/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`](./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.js` is 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: + +```bash +npm run test-servers:build # (from clients/web) → tsc -p test-servers, emits test-servers/build/ +``` + +The Vite alias `@modelcontextprotocol/inspector-test-server` (in `clients/web/vite.config.ts`) points at `test-servers/build/index.js` so `getTestMcpServerPath()` resolves to a real `.js` path. + +## Building + +```bash +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](#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`](./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'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. +- **`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`. +- **`clients/cli/package.json` and `clients/tui/package.json` ship** because those bundles read their own `package.json` at runtime (CLI client identity; TUI header `name`/`description`/`version`) relative to the bundle. Without them the installed CLI crashes on connect and the installed TUI crashes on launch — even though both work in-repo, where the files are present. + +### `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 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 ` (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. + +## Contributing — `AGENTS.md` and `CLAUDE.md` + +**[`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 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](https://claude.com/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 #`. The exact recipes (labels, board IDs, statuses) are in `AGENTS.md`. + +## License + +MIT. diff --git a/clients/launcher/README.md b/clients/launcher/README.md index 42b42b11..73e268c6 100644 --- a/clients/launcher/README.md +++ b/clients/launcher/README.md @@ -96,19 +96,7 @@ repo-root `validate:launcher` simply delegates here (`cd clients/launcher && npm ## Publishing -The root `@modelcontextprotocol/inspector` package ships as one fat tarball with a **single version number** (no separate `-web` / `-cli` / `-tui` / `-core` packages): `npm run build` at the repo root builds all clients, 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 the packlist — do not remove them without re-running `npm run pack:verify`: - -- **No source maps.** The client bundlers set `sourcemap: false` (`clients/cli/tsup.config.ts`, `clients/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. -- **`clients/web/build` is packed 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`. -- **`clients/cli/package.json` and `clients/tui/package.json` ship** because those bundles read their own `package.json` at runtime for the client identity / TUI header (`name`, `description`, `version`) relative to the bundle location. Without them the installed CLI crashes on connect and the installed TUI crashes on launch — even though both work in-repo, where the files are present. - -### `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 pack`s the publishable tarball (asserting no source maps ship and that `clients/web/{build,dist}` + the two client `package.json`s are present), installs the tarball into a clean throwaway consumer (real `npm install `, runs `postinstall`), 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 @modelcontextprotocol/inspector`" path/packaging failures (exactly how the two `package.json` reads and the `web/build` omission above were found). It requires network access (the install pulls runtime deps), so it is a local / release check, not part of the fast `validate` loop. +The launcher provides the `mcp-inspector` bin for the single `@modelcontextprotocol/inspector` tarball. Packaging is a whole-repo concern — how the one-package/single-version tarball is assembled, the `"files"` allowlist invariants (no source maps, why `clients/web/build` needs `.npmignore`, why the cli/tui `package.json`s ship), and the `npm run pack:verify` publish smoke — is documented in the [root README](../../README.md#publishing). ## Architecture From 7815548300c5a39d98e4c5d82be62d95204ce49d Mon Sep 17 00:00:00 2001 From: cliffhall Date: Fri, 10 Jul 2026 17:31:21 -0400 Subject: [PATCH 3/6] refactor(version): single-source the version from the root package.json MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Inspector ships as one package with one version, but four client package.jsons each carried their own `version` and were read at runtime (cli.ts, tui App.tsx), and core hardcoded a third, stale value (0.20.0) that the TUI and web actually reported to servers. Consolidate onto the root package.json as the single source of truth. - Add core/node/version.ts: shared, Node-only readInspectorVersion() that walks up to the root package.json (always shipped in the tarball). 100% covered by a new integration test. - CLI and TUI read the version via the shared reader instead of their own manifest; TUI now also passes a real clientIdentity (was falling through to the stale core default). - Drop the `version` field from all four client package.jsons; the root keeps the only version. TUI header name/description become local display constants. - core default client identity: replace the stale 0.20.0 hardcode with a neutral 0.0.0 placeholder (real clients pass clientIdentity; web will source its version from GET /api/config, see #1639). - Stop shipping clients/cli/package.json and clients/tui/package.json (no longer read at runtime) — supersedes the earlier "ship them" fix; update pack:verify and the root README packaging invariants accordingly. - Add core/node/** to the web coverage include so the new module is gated. - Add the shared-code architecture diagram to the root README. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw --- README.md | 4 +- clients/cli/package.json | 1 - clients/cli/src/cli.ts | 23 +++--- clients/launcher/package.json | 1 - clients/tui/package.json | 1 - clients/tui/src/App.tsx | 47 ++++-------- clients/web/package.json | 1 - .../src/test/integration/node/version.test.ts | 69 ++++++++++++++++++ clients/web/vite.config.ts | 1 + core/mcp/inspectorClient.ts | 13 +++- core/node/version.ts | 66 +++++++++++++++++ package.json | 2 - scripts/pack-and-verify.mjs | 13 ++-- .../diagrams/shared-code-architecture.png | Bin 0 -> 55485 bytes 14 files changed, 177 insertions(+), 65 deletions(-) create mode 100644 clients/web/src/test/integration/node/version.test.ts create mode 100644 core/node/version.ts create mode 100644 specification/diagrams/shared-code-architecture.png diff --git a/README.md b/README.md index f422f2c0..faaf675d 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,8 @@ npm run web:dev # web launcher in --dev mode (Vite) ## The `@inspector/core` shared package +![Shared code architecture: the four clients over the @inspector/core shared package](specification/diagrams/shared-code-architecture.png) + `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: @@ -140,7 +142,7 @@ The root `package.json` `"files"` allowlist is the source of truth for the tarba - **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. - **`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`. -- **`clients/cli/package.json` and `clients/tui/package.json` ship** because those bundles read their own `package.json` at runtime (CLI client identity; TUI header `name`/`description`/`version`) relative to the bundle. Without them the installed CLI crashes on connect and the installed TUI crashes on launch — even though both work in-repo, where the files are present. +- **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)). ### `npm run pack:verify` — publish smoke against the real tarball diff --git a/clients/cli/package.json b/clients/cli/package.json index 0a1232d9..89671c50 100644 --- a/clients/cli/package.json +++ b/clients/cli/package.json @@ -1,6 +1,5 @@ { "name": "@modelcontextprotocol/inspector-cli", - "version": "2.0.0", "private": true, "description": "CLI for the Model Context Protocol inspector", "license": "MIT", diff --git a/clients/cli/src/cli.ts b/clients/cli/src/cli.ts index b5d8e42d..3d858815 100644 --- a/clients/cli/src/cli.ts +++ b/clients/cli/src/cli.ts @@ -1,5 +1,3 @@ -import { dirname, join } from "path"; -import { fileURLToPath, pathToFileURL } from "url"; import { Command } from "commander"; type McpResponse = Record; import { awaitableLog } from "./utils/awaitable-log.js"; @@ -49,11 +47,15 @@ import { LoggingLevelSchema, type LoggingLevel, } from "@modelcontextprotocol/sdk/types.js"; +import { readInspectorVersion } from "@inspector/core/node/version.js"; export const validLogLevels: LoggingLevel[] = Object.values( LoggingLevelSchema.enum, ); +/** Client identity name the CLI reports to servers. */ +const CLI_CLIENT_NAME = "inspector-cli"; + type MethodArgs = { method?: string; promptName?: string; @@ -74,20 +76,13 @@ async function callMethod( cliAuthOverrides: RunnerClientConfigOverrides, callbackUrlConfig: RunnerOAuthCallbackConfig, ): Promise { - const __dirname = dirname(fileURLToPath(import.meta.url)); - const packageJsonPath = join(__dirname, "../package.json"); - const packageJsonData = await import(pathToFileURL(packageJsonPath).href, { - with: { type: "json" }, - }); - const packageJson = packageJsonData.default as { - name: string; - version: string; + // Version comes from the single source of truth — the root package.json — + // via the shared core reader, not the CLI's own manifest. + const clientIdentity = { + name: CLI_CLIENT_NAME, + version: readInspectorVersion(import.meta.url), }; - const [, name = packageJson.name] = packageJson.name.split("/"); - const version = packageJson.version; - const clientIdentity = { name, version }; - const environment: InspectorClientEnvironment = { transport: createTransportNode, }; diff --git a/clients/launcher/package.json b/clients/launcher/package.json index dbfbaecc..6a5f5ff7 100644 --- a/clients/launcher/package.json +++ b/clients/launcher/package.json @@ -1,6 +1,5 @@ { "name": "@modelcontextprotocol/inspector-launcher", - "version": "2.0.0", "private": true, "description": "Launcher for MCP Inspector (web, CLI, TUI)", "license": "MIT", diff --git a/clients/tui/package.json b/clients/tui/package.json index 73070e2f..e4b644d5 100644 --- a/clients/tui/package.json +++ b/clients/tui/package.json @@ -1,6 +1,5 @@ { "name": "@modelcontextprotocol/inspector-tui", - "version": "2.0.0", "private": true, "description": "Terminal User Interface (TUI) for the Model Context Protocol inspector", "license": "MIT", diff --git a/clients/tui/src/App.tsx b/clients/tui/src/App.tsx index 0d126414..66a2584b 100644 --- a/clients/tui/src/App.tsx +++ b/clients/tui/src/App.tsx @@ -6,9 +6,6 @@ import React, { useRef, } from "react"; import { Box, Text, useInput, useApp, type Key } from "ink"; -import { readFileSync } from "fs"; -import { fileURLToPath } from "url"; -import { dirname, join } from "path"; import type { MessageEntry, FetchRequestEntry, @@ -56,6 +53,7 @@ import { isOAuthCapableServerConfig, } from "@inspector/core/client/runner.js"; import { formatRunnerOAuthRedirectUrl } from "@inspector/core/auth/node/runner-oauth-callback.js"; +import { readInspectorVersion } from "@inspector/core/node/version.js"; import { createOAuthCallbackServer, type OAuthCallbackServer, @@ -85,33 +83,17 @@ import { PromptTestModal } from "./components/PromptTestModal.js"; import { DetailsModal } from "./components/DetailsModal.js"; import type { TuiServer } from "./tui-servers.js"; -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); +// Header branding. The version is the single source of truth — the root +// package.json — read via the shared core reader; the name/description are the +// TUI's own display strings (they live in code, not the npm manifest, which no +// longer carries them per-client). +const APP_NAME = "MCP Inspector TUI"; +const APP_DESCRIPTION = + "Terminal User Interface for the Model Context Protocol Inspector"; +const APP_VERSION = readInspectorVersion(import.meta.url); -// Read package.json to get project info -// Strategy: Try multiple paths to handle both local dev and global install -// - Local dev (tsx): __dirname = src/, package.json is one level up -// - Global install: __dirname = dist/src/, package.json is two levels up -let packagePath: string; -let packageJson: { name: string; description: string; version: string }; - -try { - // Try two levels up first (global install case) - packagePath = join(__dirname, "..", "..", "package.json"); - packageJson = JSON.parse(readFileSync(packagePath, "utf-8")) as { - name: string; - description: string; - version: string; - }; -} catch { - // Fall back to one level up (local dev case) - packagePath = join(__dirname, "..", "package.json"); - packageJson = JSON.parse(readFileSync(packagePath, "utf-8")) as { - name: string; - description: string; - version: string; - }; -} +/** Client identity name the TUI reports to servers. */ +const TUI_CLIENT_NAME = "inspector-tui"; // Focus management types type FocusArea = @@ -310,6 +292,7 @@ function App({ ); const opts: InspectorClientOptions = { environment, + clientIdentity: { name: TUI_CLIENT_NAME, version: APP_VERSION }, pipeStderr: true, ...(savedSettings && savedSettings.requestTimeout > 0 && { @@ -1498,11 +1481,11 @@ function App({ > - {packageJson.name} + {APP_NAME} - - {packageJson.description} + - {APP_DESCRIPTION} - v{packageJson.version} + v{APP_VERSION} {/* Main content area */} diff --git a/clients/web/package.json b/clients/web/package.json index 3c666856..6b9900d9 100644 --- a/clients/web/package.json +++ b/clients/web/package.json @@ -1,7 +1,6 @@ { "name": "@modelcontextprotocol/inspector-web", "private": true, - "version": "2.0.0", "type": "module", "main": "build/index.js", "bin": { diff --git a/clients/web/src/test/integration/node/version.test.ts b/clients/web/src/test/integration/node/version.test.ts new file mode 100644 index 00000000..6bb8d0bb --- /dev/null +++ b/clients/web/src/test/integration/node/version.test.ts @@ -0,0 +1,69 @@ +import { describe, it, expect } from "vitest"; +import { + mkdtempSync, + mkdirSync, + rmSync, + writeFileSync, + readFileSync, +} from "fs"; +import { tmpdir } from "os"; +import { join, dirname } from "path"; +import { pathToFileURL, fileURLToPath } from "url"; +import { + readInspectorVersion, + ROOT_PACKAGE_NAME, +} from "@inspector/core/node/version.js"; + +/** Absolute path to the repo root package.json (three levels above core/node). */ +const rootPackageJsonPath = join( + dirname(fileURLToPath(import.meta.url)), + "..", + "..", + "..", + "..", + "..", + "..", + "package.json", +); + +function readRootVersion(): string { + return ( + JSON.parse(readFileSync(rootPackageJsonPath, "utf-8")) as { + version: string; + } + ).version; +} + +describe("readInspectorVersion", () => { + it("resolves the version from the root package.json for a caller inside the repo", () => { + // This test file lives below the root manifest; walking up from it passes + // clients/web/package.json (wrong name → skipped) and several levels with no + // package.json (the catch branch) before reaching the root. + expect(readInspectorVersion(import.meta.url)).toBe(readRootVersion()); + // Sanity: the root manifest really is the one we match on. + expect(ROOT_PACKAGE_NAME).toBe("@modelcontextprotocol/inspector"); + }); + + it("skips a matching-name manifest that has no version, then throws when no versioned root exists above the caller", () => { + // A fake root manifest with the right name but NO version must not satisfy + // the search — the walk keeps going up. Rooted in the OS temp dir (outside + // the repo), so the walk reaches the filesystem root without finding a + // versioned inspector manifest and throws. + const work = mkdtempSync(join(tmpdir(), "version-test-")); + try { + writeFileSync( + join(work, "package.json"), + JSON.stringify({ name: ROOT_PACKAGE_NAME }), + ); + const sub = join(work, "sub"); + mkdirSync(sub); + const callerUrl = pathToFileURL(join(sub, "caller.js")).href; + + expect(() => readInspectorVersion(callerUrl)).toThrow( + /Could not locate the @modelcontextprotocol\/inspector root package\.json/, + ); + } finally { + rmSync(work, { recursive: true, force: true }); + } + }); +}); diff --git a/clients/web/vite.config.ts b/clients/web/vite.config.ts index cc2ed813..d1acd996 100644 --- a/clients/web/vite.config.ts +++ b/clients/web/vite.config.ts @@ -109,6 +109,7 @@ export default defineConfig(({ command }) => { path.join(repoRoot, 'core/auth/**/*.{ts,tsx}'), path.join(repoRoot, 'core/storage/**/*.{ts,tsx}'), path.join(repoRoot, 'core/logging/**/*.{ts,tsx}'), + path.join(repoRoot, 'core/node/**/*.{ts,tsx}'), ], exclude: [ '**/*.stories.{ts,tsx}', diff --git a/core/mcp/inspectorClient.ts b/core/mcp/inspectorClient.ts index e51d9887..8ae4fccb 100644 --- a/core/mcp/inspectorClient.ts +++ b/core/mcp/inspectorClient.ts @@ -26,11 +26,16 @@ export type { AppRendererClient, } from "./types.js"; import { getServerType as getServerTypeFromConfig } from "./config.js"; -// v2 doesn't have a core/package.json (the package isn't published independently), -// so we hardcode the client identity that v1.5 read from corePackageJson. +// Fallback client identity, used ONLY when a caller doesn't pass +// `clientIdentity`. Real clients supply their own: the Node clients (CLI, TUI) +// read the single-source version from the root package.json via +// `readInspectorVersion()`, and the web browser — which can't read the +// filesystem — will pass a version sourced from `GET /api/config` (see #1639). +// This stays a neutral placeholder rather than a hardcoded release number that +// would silently drift out of sync with the root package.json version. const corePackageJson = { - name: "@modelcontextprotocol/inspector-core", - version: "0.20.0", + name: "mcp-inspector", + version: "0.0.0", } as const; import type { CreateTransport, diff --git a/core/node/version.ts b/core/node/version.ts new file mode 100644 index 00000000..063612cc --- /dev/null +++ b/core/node/version.ts @@ -0,0 +1,66 @@ +/** + * Single source of truth for the Inspector's version, read from the **root** + * `package.json` of the published `@modelcontextprotocol/inspector` package. + * + * The Inspector ships as one package with one version number; the per-client + * `package.json`s deliberately carry no `version` field, so every Node client + * (CLI, TUI, and the web backend) resolves the version here instead of reading + * its own manifest. + * + * **Node-only** — this uses `node:fs`; never import it from browser code. The + * web *browser* cannot read the filesystem and gets its version from the + * backend via `GET /api/config` instead. + */ + +import { readFileSync } from "node:fs"; +import { dirname, join, parse } from "node:path"; +import { fileURLToPath } from "node:url"; + +/** The published root package's name — how we recognize the root manifest. */ +export const ROOT_PACKAGE_NAME = "@modelcontextprotocol/inspector"; + +interface RootManifest { + name?: string; + version?: string; +} + +/** + * Resolve the Inspector version by walking up from the calling module to the + * root `package.json` (the one named {@link ROOT_PACKAGE_NAME}, which alone + * carries a `version`). Pass the caller's `import.meta.url`. + * + * This works identically in the dev tree and the published tarball: in both, + * a client entry (`clients//{src,build}/…`) sits below the root manifest, + * and npm always ships the root `package.json`. Client `package.json`s are + * skipped because they have no matching `name`/`version`, so a stray manifest + * on the way up never shadows the real root. + * + * @param callerUrl the calling module's `import.meta.url` + * @returns the version string from the root `package.json` + * @throws if no root manifest is found above the caller + */ +export function readInspectorVersion(callerUrl: string): string { + const startDir = dirname(fileURLToPath(callerUrl)); + const { root } = parse(startDir); + + let dir = startDir; + for (;;) { + let manifest: RootManifest | undefined; + try { + manifest = JSON.parse( + readFileSync(join(dir, "package.json"), "utf-8"), + ) as RootManifest; + } catch { + // No (readable) package.json at this level — keep walking up. + } + if (manifest?.name === ROOT_PACKAGE_NAME && manifest.version) { + return manifest.version; + } + if (dir === root) break; + dir = dirname(dir); + } + + throw new Error( + `Could not locate the ${ROOT_PACKAGE_NAME} root package.json above ${startDir}`, + ); +} diff --git a/package.json b/package.json index 359605f2..278bc437 100644 --- a/package.json +++ b/package.json @@ -25,9 +25,7 @@ "clients/web/build", "clients/web/dist", "clients/cli/build", - "clients/cli/package.json", "clients/tui/build", - "clients/tui/package.json", "scripts/install-clients.mjs" ], "scripts": { diff --git a/scripts/pack-and-verify.mjs b/scripts/pack-and-verify.mjs index aa44f51f..758d2525 100644 --- a/scripts/pack-and-verify.mjs +++ b/scripts/pack-and-verify.mjs @@ -125,15 +125,14 @@ if (maps.length > 0) { // 2b. Runtime files that are easy to omit from the packlist and only fail once // installed: both web artifacts — the prod server runner (build) AND the SPA -// (dist) — plus the TUI's package.json, which the TUI bundle reads at -// runtime for its header (name/description/version). `clients/web/build` was -// previously dropped by the nested .gitignore; `clients/tui/package.json` -// was never listed, so the installed TUI crashed on launch. +// (dist). `clients/web/build` was previously dropped by the nested +// .gitignore. (The version the CLI/TUI report is read from the root +// package.json — always shipped — via readInspectorVersion(), so no client +// package.json needs to ship; that read is exercised by driving the bin in +// step 4.) for (const required of [ "clients/web/build/index.js", "clients/web/dist/index.html", - "clients/cli/package.json", - "clients/tui/package.json", ]) { if (!tarredPaths.includes(required)) { fail( @@ -179,8 +178,6 @@ try { join(installedPkg, "clients", "web", "build", "index.js"), join(installedPkg, "clients", "web", "dist", "index.html"), join(installedPkg, "clients", "launcher", "build", "index.js"), - join(installedPkg, "clients", "cli", "package.json"), - join(installedPkg, "clients", "tui", "package.json"), ]) { if (!existsSync(required)) { fail(`expected installed file missing: ${required}`); diff --git a/specification/diagrams/shared-code-architecture.png b/specification/diagrams/shared-code-architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..b05d8c4ea18eb68ef2de6d198188098d7617cfb8 GIT binary patch literal 55485 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 QKk0rcak)p?51zmNZ)tqT0ssI2 literal 0 HcmV?d00001 From c1b168e522a0cb337fdc7addcb117b30d0cd5730 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Fri, 10 Jul 2026 18:25:10 -0400 Subject: [PATCH 4/6] chore(pack-verify): retain tarball on failure (fix dead cleanup flag) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review follow-up: `cleanupTarball` was always true (set true at init and again in the catch), so the finally guard was dead logic. Make it meaningful — retain the packed tarball on failure so a packaging problem can be inspected, and log where it is; the happy path still removes it. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw --- scripts/pack-and-verify.mjs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scripts/pack-and-verify.mjs b/scripts/pack-and-verify.mjs index 758d2525..a49e5e7a 100644 --- a/scripts/pack-and-verify.mjs +++ b/scripts/pack-and-verify.mjs @@ -151,6 +151,8 @@ console.log( // runs the package's postinstall). // --------------------------------------------------------------------------- const work = mkdtempSync(join(tmpdir(), "pack-verify-")); +// Remove the packed tarball on success; retain it on failure so a packaging +// problem can be inspected (unpack it, diff the file list). let cleanupTarball = true; try { ensureTestServer(); @@ -253,7 +255,9 @@ try { "web (prod / served dist), cli (stdio tools/list), and tui (help) end to end.", ); } catch (err) { - cleanupTarball = true; + // Keep the tarball around for post-mortem inspection of the packaging failure. + cleanupTarball = false; + console.error(`pack:verify — tarball retained for inspection at ${tarball}`); fail(err instanceof Error ? err.message : String(err)); } finally { rmSync(work, { recursive: true, force: true }); From 3dead1f83d5e5b1bbd4c9e5894f744ca0f882e9a Mon Sep 17 00:00:00 2001 From: cliffhall Date: Fri, 10 Jul 2026 18:32:03 -0400 Subject: [PATCH 5/6] fix(pack-verify): actually clean up on failure (fail() bypasses finally) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Re-review caught that the previous fix was inert: fail() calls process.exit(1), which skips finally blocks — so `cleanupTarball = false` never took effect and, worse, the temp work dir (a full node_modules from the real install) leaked on every failed run. Move cleanup into fail() itself — the single failure-exit point, called from everywhere including deep inside the run. On failure it removes the heavy work dir and leaves the packed tarball in place for post-mortem inspection; the success path removes both inline before the OK log. Drop the dead `cleanupTarball` flag entirely. Verified both paths: success removes work dir + tarball (exit 0); a forced failure removes the work dir, retains the tarball, and exits 1. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw --- scripts/pack-and-verify.mjs | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/scripts/pack-and-verify.mjs b/scripts/pack-and-verify.mjs index a49e5e7a..a460daa5 100644 --- a/scripts/pack-and-verify.mjs +++ b/scripts/pack-and-verify.mjs @@ -51,8 +51,21 @@ const testServer = join(repoRoot, "test-servers", "build", "test-server-stdio.js // literal because this plain .mjs script can't import the TS source. const TOKEN_GLOBAL = "__INSPECTOR_API_TOKEN__"; +// Set once the throwaway consumer dir exists. `fail()` is the single +// failure-exit point (called from everywhere, including deep inside the run), +// and it calls process.exit(), which skips any `finally` — so cleanup has to +// happen here, not in a trailing `finally`. On failure we remove the heavy work +// dir (a full node_modules from the real install) but leave the packed tarball +// in place for post-mortem inspection. +let workDir = null; + function fail(message) { console.error(`\npack:verify FAILED — ${message}`); + if (workDir) { + rmSync(workDir, { recursive: true, force: true }); + // `tarball` is initialized before `workDir` is ever set, so this is safe. + console.error(`pack:verify — tarball retained for inspection at ${tarball}`); + } process.exit(1); } @@ -151,9 +164,7 @@ console.log( // runs the package's postinstall). // --------------------------------------------------------------------------- const work = mkdtempSync(join(tmpdir(), "pack-verify-")); -// Remove the packed tarball on success; retain it on failure so a packaging -// problem can be inspected (unpack it, diff the file list). -let cleanupTarball = true; +workDir = work; // from now on, fail() cleans this up try { ensureTestServer(); step(`installing the tarball into a clean consumer at ${work} (pulls runtime deps)...`); @@ -250,18 +261,20 @@ try { // it) and inject the auth token. Run non-blocking and poll `/`. await verifyWeb(bin, work); + // Success: clean up both the work dir and the tarball. (This is reached only + // on success — every failure path goes through fail() → process.exit(), which + // does its own cleanup above and never returns here.) + rmSync(work, { recursive: true, force: true }); + rmSync(tarball, { force: true }); + console.log( "\npack:verify OK — published tarball installs clean and the real bin drives " + "web (prod / served dist), cli (stdio tools/list), and tui (help) end to end.", ); } catch (err) { - // Keep the tarball around for post-mortem inspection of the packaging failure. - cleanupTarball = false; - console.error(`pack:verify — tarball retained for inspection at ${tarball}`); + // Unexpected throw (not via fail()) — route through fail() for consistent + // cleanup + exit. fail(err instanceof Error ? err.message : String(err)); -} finally { - rmSync(work, { recursive: true, force: true }); - if (cleanupTarball) rmSync(tarball, { force: true }); } /** From 9324f9f21c3240ce54abb201256d4963265bb270 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Fri, 10 Jul 2026 18:35:46 -0400 Subject: [PATCH 6/6] fix(pack-verify): kill the --web child on failure too (same exit-skips-finally) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Re-review found the same process.exit()-skips-finally pattern in verifyWeb: a failure during the --web phase goes through fail() → process.exit(1), skipping its `finally { stop() }`, so the spawned web server was orphaned — left on the port, where a later run could fetch a stale false-200 and mask a regression. Track the live child in a module `webChild` and have fail() SIGTERM it before exiting, alongside the workDir cleanup; stop() clears it on the success path. Verified: a forced web-phase failure exits 1, retains the tarball, and leaves no listener on the port (child killed, not orphaned). Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw --- scripts/pack-and-verify.mjs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/scripts/pack-and-verify.mjs b/scripts/pack-and-verify.mjs index a460daa5..79bfc33b 100644 --- a/scripts/pack-and-verify.mjs +++ b/scripts/pack-and-verify.mjs @@ -58,9 +58,17 @@ const TOKEN_GLOBAL = "__INSPECTOR_API_TOKEN__"; // dir (a full node_modules from the real install) but leave the packed tarball // in place for post-mortem inspection. let workDir = null; +// The live `--web` child during verifyWeb(), if any. fail() kills it so a +// failure in the web phase doesn't orphan the server — which would linger on +// the port and could even serve a stale false-200 to a later run (verifyWeb's +// own `finally { stop() }` is skipped when fail() calls process.exit()). +let webChild = null; function fail(message) { console.error(`\npack:verify FAILED — ${message}`); + if (webChild && webChild.exitCode === null) { + webChild.kill("SIGTERM"); + } if (workDir) { rmSync(workDir, { recursive: true, force: true }); // `tarball` is initialized before `workDir` is ever set, so this is safe. @@ -299,6 +307,9 @@ async function verifyWeb(bin, cwd) { stdio: ["ignore", "inherit", "inherit"], shell: process.platform === "win32", }); + // Expose the child so fail() can kill it if a check below exits the process + // (process.exit skips the `finally { stop() }`). + webChild = child; let exited = false; let exitCode = null; @@ -308,6 +319,7 @@ async function verifyWeb(bin, cwd) { }); const stop = () => { if (!exited) child.kill("SIGTERM"); + webChild = null; }; try {