feat: port CLI, TUI, and launcher from v1.5 into v2 (#1452)

Ports the CLI, TUI, and launcher from v1.5 into v2 behind a single
`mcp-inspector` entrypoint (launcher → web/cli/tui). Includes the
`mcp-inspector --web --dev` launcher fix (Vite alias resolution + prod-server
double-start), the root `postinstall` install cascade with simplified CI, and
spec/AGENTS docs (no-workspaces rationale, Development setup).

Closes #1246.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bob Dickinson 2026-06-14 17:27:49 -07:00 • committed by GitHub
commit 2488ca5e38
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 25594 additions and 236 deletions

View file

@ -0,0 +1,19 @@
# MCP Inspector Launcher
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).
## Responsibility
- Parse mode from a leading prefix of `--web` (default), `--cli`, or `--tui` immediately after the script name.
- Forward all following arguments unchanged (including tokens that look like mode flags).
- Dynamically import that app’s runner from `clients/{web,cli,tui}/build/index.js` (relative to the launcher build output) and call it **in-process** (no `spawn()`).
All configuration parsing, config-file loading, and server setup are handled by the app runners and by **core**; the launcher does not interpret config or env vars.
## 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.
## Architecture
For how the launcher fits with the shared config processor and app runners, see the [Launcher and config consolidation](../../docs/launcher-config-consolidation-plan.md) document in the repo root `docs/` folder.

View file

@ -0,0 +1,50 @@
import { describe, expect, it } from "vitest";
import { parseLauncherArgv } from "../src/parse-launcher-argv.js";
const EXEC = ["node", "/path/to/launcher/build/index.js"];
describe("parseLauncherArgv", () => {
it("defaults to web when no mode flag is in the prefix", () => {
expect(parseLauncherArgv([...EXEC, "--method", "tools/list"])).toEqual({
mode: "web",
forwardedArgv: [...EXEC, "--method", "tools/list"],
hasPrefixModeFlag: false,
});
});
it("selects mode from a leading mode flag and strips only the prefix", () => {
expect(
parseLauncherArgv([...EXEC, "--cli", "node", "./server.js", "--cli"]),
).toEqual({
mode: "cli",
forwardedArgv: [...EXEC, "node", "./server.js", "--cli"],
hasPrefixModeFlag: true,
});
});
it("does not treat a trailing mode-like token as launcher mode", () => {
expect(parseLauncherArgv([...EXEC, "node", "./server.js", "--cli"])).toEqual(
{
mode: "web",
forwardedArgv: [...EXEC, "node", "./server.js", "--cli"],
hasPrefixModeFlag: false,
},
);
});
it("rejects multiple mode flags in the launcher prefix", () => {
expect(() => parseLauncherArgv([...EXEC, "--cli", "--tui"])).toThrow(
/at most one of --web, --cli, or --tui/,
);
});
it("forwards a later mode-like token after non-mode app args", () => {
expect(
parseLauncherArgv([...EXEC, "--tui", "--config", "x", "--cli"]),
).toEqual({
mode: "tui",
forwardedArgv: [...EXEC, "--config", "x", "--cli"],
hasPrefixModeFlag: true,
});
});
});

1438
clients/launcher/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,28 @@
{
"name": "@modelcontextprotocol/inspector-launcher",
"version": "2.0.0",
"private": true,
"description": "Launcher for MCP Inspector (web, CLI, TUI)",
"license": "MIT",
"type": "module",
"main": "build/index.js",
"bin": {
"mcp-inspector": "./build/index.js"
},
"files": [
"build"
],
"scripts": {
"build": "tsc",
"postbuild": "node scripts/make-executable.js",
"test": "vitest run"
},
"dependencies": {
"commander": "^13.1.0"
},
"devDependencies": {
"@types/node": "^24.12.4",
"typescript": "~5.9.3",
"vitest": "^4.1.0"
}
}

View file

@ -0,0 +1,26 @@
/**
* Cross-platform script to make the launcher entrypoint executable
*/
import { promises as fs } from "fs";
import { platform } from "os";
import { execSync } from "child_process";
import path from "path";
const TARGET_FILE = path.resolve("build/index.js");
async function makeExecutable() {
try {
if (platform() !== "win32") {
execSync(`chmod +x "${TARGET_FILE}"`);
console.log("Made file executable with chmod");
} else {
await fs.access(TARGET_FILE);
console.log("File exists and is accessible on Windows");
}
} catch (error) {
console.error("Error making file executable:", error);
process.exit(1);
}
}
makeExecutable();

View file

@ -0,0 +1,64 @@
#!/usr/bin/env node
import { dirname, join } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { Command } from "commander";
import { parseLauncherArgv } from "./parse-launcher-argv.js";
const launcherDir = dirname(fileURLToPath(import.meta.url));
function clientEntry(client: "web" | "cli" | "tui"): string {
return pathToFileURL(
join(launcherDir, "..", "..", client, "build", "index.js"),
).href;
}
const program = new Command();
program
.name("mcp-inspector")
.description("MCP Inspector – run web UI, CLI, or TUI")
.option("--web", "Run web UI (default)")
.option("--cli", "Run CLI")
.option("--tui", "Run TUI");
let parsedArgv;
try {
parsedArgv = parseLauncherArgv(process.argv);
} catch (err) {
const message =
err instanceof Error ? err.message : "Invalid launcher arguments.";
console.error(`Error: ${message}`);
process.exit(1);
}
const { mode, forwardedArgv, hasPrefixModeFlag } = parsedArgv;
const helpOnly =
process.argv.includes("-h") || process.argv.includes("--help");
if (helpOnly && !hasPrefixModeFlag) {
program.outputHelp();
console.log(
"\nMode flags (--web, --cli, --tui) must appear before app options. All following arguments are forwarded unchanged.",
);
process.exit(0);
}
async function run(): Promise<void> {
if (mode === "web") {
const { runWeb } = await import(clientEntry("web"));
await runWeb(forwardedArgv);
} else if (mode === "cli") {
const { runCli } = await import(clientEntry("cli"));
await runCli(forwardedArgv);
} else {
const { runTui } = await import(clientEntry("tui"));
await runTui(forwardedArgv);
}
}
run().catch((err: unknown) => {
console.error("Error running MCP Inspector:", err);
process.exit(1);
});

View file

@ -0,0 +1,43 @@
export const LAUNCHER_MODE_FLAGS = new Set(["--web", "--cli", "--tui"]);
export type LauncherMode = "web" | "cli" | "tui";
export type ParseLauncherArgvResult = {
mode: LauncherMode;
forwardedArgv: string[];
hasPrefixModeFlag: boolean;
};
/**
* Launcher mode flags are read only from a contiguous prefix of user args
* (argv[2…]). Everything after the first non-mode token is forwarded
* unchanged so values like `--cli` can appear in app/server config.
*/
export function parseLauncherArgv(argv: string[]): ParseLauncherArgvResult {
const executable = argv.slice(0, 2);
const userArgs = argv.slice(2);
const prefixFlags: string[] = [];
let index = 0;
while (index < userArgs.length && LAUNCHER_MODE_FLAGS.has(userArgs[index]!)) {
prefixFlags.push(userArgs[index]!);
index++;
}
if (prefixFlags.length > 1) {
throw new Error("Specify at most one of --web, --cli, or --tui.");
}
const mode: LauncherMode =
prefixFlags[0] === "--tui"
? "tui"
: prefixFlags[0] === "--cli"
? "cli"
: "web";
return {
mode,
forwardedArgv: [...executable, ...userArgs.slice(index)],
hasPrefixModeFlag: prefixFlags.length > 0,
};
}

View file

@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "./build",
"rootDir": "./src",
"strict": true,
"skipLibCheck": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "build"]
}