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:
parent
53dc4549be
commit
2488ca5e38
8 changed files with 25594 additions and 236 deletions
19
clients/launcher/README.md
Normal file
19
clients/launcher/README.md
Normal 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.
|
||||
50
clients/launcher/__tests__/parse-launcher-argv.test.ts
Normal file
50
clients/launcher/__tests__/parse-launcher-argv.test.ts
Normal 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
1438
clients/launcher/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
28
clients/launcher/package.json
Normal file
28
clients/launcher/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
26
clients/launcher/scripts/make-executable.js
Normal file
26
clients/launcher/scripts/make-executable.js
Normal 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();
|
||||
64
clients/launcher/src/index.ts
Normal file
64
clients/launcher/src/index.ts
Normal 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);
|
||||
});
|
||||
43
clients/launcher/src/parse-launcher-argv.ts
Normal file
43
clients/launcher/src/parse-launcher-argv.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
13
clients/launcher/tsconfig.json
Normal file
13
clients/launcher/tsconfig.json
Normal 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"]
|
||||
}
|
||||
Loading…
Reference in a new issue