From 397d66dbbfc4be6f02c64bf9d11b67891d5e9111 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Wed, 7 Oct 2026 23:28:57 -0400 Subject: [PATCH 1/3] fix(package): restore `npx @modelcontextprotocol/inspector` with an `inspector` bin alias (#2651) 2.10.0 added `mcpdo` as a second bin. With more than one distinct bin, npm's npx/exec runs only the bin named after the unscoped package name (`inspector`), which didn't exist, so the documented entry point failed with "could not determine executable to run". Add `inspector` -> the launcher, and guard it twice: an offline test:scripts check that the root manifest's npx default bin is the launcher (mirrors libnpmexec's getBinFromManifest), and a pack:verify step that runs the installed tarball via `npm exec` instead of by bin name, which is why every existing check stayed green. Co-Authored-By: Claude Opus 5.5 Signed-off-by: cliffhall --- clients/launcher/README.md | 2 + package.json | 1 + scripts/lib/npx-default-bin.mjs | 37 ++++++++++++++ scripts/lib/npx-default-bin.test.mjs | 72 ++++++++++++++++++++++++++++ scripts/pack-and-verify.mjs | 35 +++++++++++++- 5 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 scripts/lib/npx-default-bin.mjs create mode 100644 scripts/lib/npx-default-bin.test.mjs diff --git a/clients/launcher/README.md b/clients/launcher/README.md index dade4b0d18..af91f9a6ea 100644 --- a/clients/launcher/README.md +++ b/clients/launcher/README.md @@ -2,6 +2,8 @@ The launcher is the package that provides the global `mcp-inspector` binary (e.g. when users run `npx @modelcontextprotocol/inspector`). It is not a separate user-facing app—it is the single entrypoint that selects and runs one of the clients (web, CLI, or TUI). +The root `package.json` also publishes it as an `inspector` bin. That alias is what makes a bare `npx @modelcontextprotocol/inspector` work: the package ships a second bin (`mcpdo`), and with more than one distinct bin npm only runs the one named after the unscoped package name. Without that alias npx fails with "could not determine executable to run", which is how 2.10.0 shipped (#2651). `scripts/lib/npx-default-bin.test.mjs` and `pack:verify` both check this. + ## Responsibility - Parse mode from a leading prefix of `--web` (default), `--cli`, or `--tui` immediately after the script name. diff --git a/package.json b/package.json index 51931a9565..258b08e2b9 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "author": "The MCP Maintainers and Community", "type": "module", "bin": { + "inspector": "./clients/launcher/build/index.js", "mcp-inspector": "./clients/launcher/build/index.js", "mcpdo": "./clients/mcpdo/build/mcp-bin.js" }, diff --git a/scripts/lib/npx-default-bin.mjs b/scripts/lib/npx-default-bin.mjs new file mode 100644 index 0000000000..848e83f8a8 --- /dev/null +++ b/scripts/lib/npx-default-bin.mjs @@ -0,0 +1,37 @@ +/** + * Which bin `npx ` / `npm exec ` runs, by npm's own rule + * (#2651). + * + * A mirror of `getBinFromManifest` in npm's bundled `libnpmexec` + * (`lib/get-bin-from-manifest.js`): run the sole bin when every entry points at + * the same file; otherwise run the bin named after the package's **unscoped** + * name; otherwise npm fails with "could not determine executable to run". + * + * 2.10.0 shipped exactly that failure. Adding `mcpdo` gave the package a second + * distinct bin, and nothing named `inspector` existed, so the documented + * `npx @modelcontextprotocol/inspector` stopped working for everyone — while + * `pack:verify` and every smoke stayed green, because they all invoke the + * installed bin by name and never ask npm to choose one. This is the offline + * half of the guard (its test asserts the root manifest); `pack:verify` runs + * the installed tarball through `npm exec` for the online half. + * + * Mirrored rather than imported: `libnpmexec` is npm's internal dependency, not + * one of ours, and its location depends on how Node was installed. + */ + +/** + * @param {{ name: string, bin?: string | Record }} manifest + * @returns {string | null} the bin name npm would run, or null where npm fails + */ +export function npxDefaultBin(manifest) { + // npm normalizes a string `bin` to `{ : }` on publish. + const name = manifest.name.replace(/^@[^/]+\//, ""); + const bin = + typeof manifest.bin === "string" + ? { [name]: manifest.bin } + : (manifest.bin ?? {}); + const entries = Object.keys(bin); + if (new Set(Object.values(bin)).size === 1) return entries[0]; + if (bin[name]) return name; + return null; +} diff --git a/scripts/lib/npx-default-bin.test.mjs b/scripts/lib/npx-default-bin.test.mjs new file mode 100644 index 0000000000..10984d6496 --- /dev/null +++ b/scripts/lib/npx-default-bin.test.mjs @@ -0,0 +1,72 @@ +// Tests for `npx-default-bin.mjs` (#2651): npm's default-bin rule, and the +// assertion that the root manifest — the one that publishes — resolves +// `npx @modelcontextprotocol/inspector` to the launcher. + +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { npxDefaultBin } from "./npx-default-bin.mjs"; + +const repoRoot = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + "..", + "..", +); +const LAUNCHER = "./clients/launcher/build/index.js"; + +test("the root manifest's npx default bin is the launcher", () => { + const manifest = JSON.parse( + readFileSync(path.join(repoRoot, "package.json"), "utf8"), + ); + const bin = npxDefaultBin(manifest); + assert.notEqual( + bin, + null, + "npx @modelcontextprotocol/inspector would fail: no single bin and none named after the package", + ); + assert.equal(manifest.bin[bin], LAUNCHER); +}); + +test("a sole bin is run whatever its name", () => { + assert.equal( + npxDefaultBin({ name: "@s/pkg", bin: { "mcp-inspector": LAUNCHER } }), + "mcp-inspector", + ); +}); + +test("aliases of one file count as a sole bin", () => { + assert.equal( + npxDefaultBin({ name: "@s/pkg", bin: { a: LAUNCHER, b: LAUNCHER } }), + "a", + ); +}); + +test("several distinct bins resolve to the one named after the unscoped package", () => { + assert.equal( + npxDefaultBin({ + name: "@s/inspector", + bin: { inspector: LAUNCHER, mcpdo: "./mcpdo.js" }, + }), + "inspector", + ); +}); + +test("several distinct bins with none named after the package fail (the 2.10.0 shape)", () => { + assert.equal( + npxDefaultBin({ + name: "@modelcontextprotocol/inspector", + bin: { "mcp-inspector": LAUNCHER, mcpdo: "./mcpdo.js" }, + }), + null, + ); +}); + +test("a string bin is named after the unscoped package", () => { + assert.equal(npxDefaultBin({ name: "@s/pkg", bin: LAUNCHER }), "pkg"); +}); + +test("no bin at all fails", () => { + assert.equal(npxDefaultBin({ name: "pkg" }), null); +}); diff --git a/scripts/pack-and-verify.mjs b/scripts/pack-and-verify.mjs index 7e88ef79b8..81b826576c 100644 --- a/scripts/pack-and-verify.mjs +++ b/scripts/pack-and-verify.mjs @@ -29,7 +29,9 @@ * 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; + * the shipped `dist` — all from the INSTALLED location, not the repo — + * plus `npm exec @modelcontextprotocol/inspector --help`, so npm's own + * default-bin choice is exercised and not just the bin by name (#2651); * 5. drives the **MCP Apps** path in headless Chromium against that same * installed `--web` server — connect → open app → `data-app-status="ready"` * (#2003). Asserting the sandbox proxy page merely *exists* (step 2/3) is @@ -354,6 +356,37 @@ try { } } + // 4a'. The same help, reached the way a user reaches it: through npm's own + // default-bin choice rather than the bin's name (#2651). Every check + // above names `mcp-inspector` directly, which is how 2.10.0 shipped a + // second bin, broke `npx @modelcontextprotocol/inspector` for everyone, + // and stayed green here. `--no` keeps npm on the installed tarball. + step( + "verifying `npm exec @modelcontextprotocol/inspector` picks the launcher...", + ); + const npmExec = spawnSync( + "npm", + shellArgs([ + "exec", + "--no", + "--", + "@modelcontextprotocol/inspector", + "--help", + ]), + { cwd: work, encoding: "utf8", shell: WIN_SHELL }, + ); + const npmExecOutput = `${npmExec.stdout ?? ""}${npmExec.stderr ?? ""}`; + if ( + npmExec.status !== 0 || + !npmExecOutput.includes("Mode flags (--web, --cli, --tui)") + ) { + fail( + `\`npm exec @modelcontextprotocol/inspector --help\` exited ${npmExec.status} ` + + `without the launcher's help — check the root package.json "bin" ` + + `(scripts/lib/npx-default-bin.mjs)\n${npmExecOutput.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. From 7759adb371399645d8ce071f8e8dabfd3f27ab06 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Wed, 7 Oct 2026 23:34:33 -0400 Subject: [PATCH 2/3] chore(package): record the inspector bin alias in package-lock.json (#2651) Co-Authored-By: Claude Opus 5.5 Signed-off-by: cliffhall --- package-lock.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package-lock.json b/package-lock.json index 7b5c7f073d..b8ffcc4c42 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,6 +34,7 @@ "zod": "^4.4.3" }, "bin": { + "inspector": "clients/launcher/build/index.js", "mcp-inspector": "clients/launcher/build/index.js", "mcpdo": "clients/mcpdo/build/mcp-bin.js" }, From c92978ee93ec356676c3bdfc09367d31dbf34033 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Wed, 7 Oct 2026 23:37:36 -0400 Subject: [PATCH 3/3] docs(readme): list mcpdo among the clients, with its npx command (#2651) Co-Authored-By: Claude Opus 5.5 Signed-off-by: cliffhall --- README.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 53e031ab64..7a7e7f3888 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,19 @@ # 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: +A developer tool for inspecting [Model Context Protocol](https://modelcontextprotocol.io) (MCP) servers. It ships as a single package, `@modelcontextprotocol/inspector`, that provides four 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). +- **mcpdo** _(experimental)_ — a connection CLI: connect once, then run many commands against the named connection through an implicit local daemon. See [`clients/mcpdo`](./clients/mcpdo/README.md). -All three run through one global `mcp-inspector` binary: +The first three run through one global `mcp-inspector` binary; `mcpdo` is a second binary in the same package: ```bash -npx @modelcontextprotocol/inspector # web UI (default) -npx @modelcontextprotocol/inspector --cli # CLI -npx @modelcontextprotocol/inspector --tui # TUI +npx @modelcontextprotocol/inspector # web UI (default) +npx @modelcontextprotocol/inspector --cli # CLI +npx @modelcontextprotocol/inspector --tui # TUI +npx -p @modelcontextprotocol/inspector mcpdo --help # mcpdo ``` > [!WARNING]