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
4 changed files with 25594 additions and 236 deletions
161
clients/web/server/run-web.ts
Normal file
161
clients/web/server/run-web.ts
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
/**
|
||||
* Programmatic entry for the launcher and published `mcp-inspector --web`.
|
||||
* Day-to-day dev still uses `npm run dev` (Vite CLI + buildWebServerConfigFromEnv).
|
||||
* Prod (`--web`, no `--dev`) starts the Hono server in-process via `startHonoServer`.
|
||||
*/
|
||||
|
||||
import { resolve, join, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { Command } from "commander";
|
||||
import type { MCPServerConfig } from "../../../core/mcp/types.ts";
|
||||
import {
|
||||
resolveServerConfigs,
|
||||
parseKeyValuePair,
|
||||
parseHeaderPair,
|
||||
type ServerConfigOptions,
|
||||
} from "../../../core/mcp/node/config.ts";
|
||||
import { buildWebServerConfig } from "./web-server-config.js";
|
||||
import { startViteDevServer } from "./start-vite-dev-server.js";
|
||||
import { startHonoServer } from "./server.js";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
function ensureStdioCwd(config: MCPServerConfig): MCPServerConfig {
|
||||
if (
|
||||
(config.type === "stdio" || config.type === undefined) &&
|
||||
config.command &&
|
||||
!config.cwd
|
||||
) {
|
||||
return { ...config, cwd: resolve(process.cwd()) };
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
export async function runWeb(argv: string[]): Promise<number> {
|
||||
const program = new Command();
|
||||
|
||||
const argSeparatorIndex = argv.indexOf("--");
|
||||
let preArgs = argv;
|
||||
let postArgs: string[] = [];
|
||||
if (argSeparatorIndex !== -1) {
|
||||
preArgs = argv.slice(0, argSeparatorIndex);
|
||||
postArgs = argv.slice(argSeparatorIndex + 1);
|
||||
}
|
||||
|
||||
program
|
||||
.name("mcp-inspector-web")
|
||||
.description("Web UI for MCP Inspector")
|
||||
.allowExcessArguments()
|
||||
.allowUnknownOption()
|
||||
.option(
|
||||
"-e <env>",
|
||||
"environment variables in KEY=VALUE format",
|
||||
parseKeyValuePair,
|
||||
{},
|
||||
)
|
||||
.option("--config <path>", "config file path")
|
||||
.option("--server <name>", "server name from config file")
|
||||
.option("--transport <type>", "transport type (stdio, sse, http)")
|
||||
.option("--server-url <url>", "server URL for SSE/HTTP transport")
|
||||
.option("--cwd <path>", "working directory for stdio server process")
|
||||
.option(
|
||||
"--header <headers...>",
|
||||
'HTTP headers as "HeaderName: Value" pairs (for HTTP/SSE transports)',
|
||||
parseHeaderPair,
|
||||
{},
|
||||
)
|
||||
.option("--dev", "run in development mode (Vite)")
|
||||
.parse(preArgs);
|
||||
|
||||
const opts = program.opts() as {
|
||||
config?: string;
|
||||
server?: string;
|
||||
e?: Record<string, string>;
|
||||
transport?: string;
|
||||
serverUrl?: string;
|
||||
cwd?: string;
|
||||
header?: Record<string, string>;
|
||||
dev?: boolean;
|
||||
};
|
||||
|
||||
const args = program.args;
|
||||
const target = [...args, ...postArgs];
|
||||
const isDev = !!opts.dev;
|
||||
|
||||
const hasServerInput =
|
||||
opts.config ||
|
||||
target.length > 0 ||
|
||||
opts.serverUrl ||
|
||||
(opts.transport && opts.transport !== "stdio");
|
||||
|
||||
let initialMcpConfig: MCPServerConfig | null = null;
|
||||
|
||||
if (hasServerInput) {
|
||||
const serverOptions: ServerConfigOptions = {
|
||||
configPath: opts.config,
|
||||
serverName: opts.server,
|
||||
target: target.length > 0 ? target : undefined,
|
||||
transport: opts.transport as "stdio" | "sse" | "http" | undefined,
|
||||
serverUrl: opts.serverUrl,
|
||||
cwd: opts.cwd,
|
||||
env: opts.e,
|
||||
};
|
||||
|
||||
if (opts.header && Object.keys(opts.header).length > 0) {
|
||||
console.warn(
|
||||
"Warning: --header is accepted but initial HTTP headers are configured via server settings in the web UI (post-#1358).",
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const configs = resolveServerConfigs(serverOptions, "single");
|
||||
const config = configs[0];
|
||||
if (!config) {
|
||||
console.error(
|
||||
"Error: Could not resolve server config. Use --config and --server, or pass a command/URL.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
initialMcpConfig = ensureStdioCwd(config);
|
||||
} catch (err) {
|
||||
const message =
|
||||
err instanceof Error ? err.message : "Could not resolve server config.";
|
||||
console.error("Error:", message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
const webConfig = buildWebServerConfig({ initialMcpConfig });
|
||||
if (!isDev) {
|
||||
webConfig.staticRoot = join(__dirname, "..", "dist");
|
||||
}
|
||||
|
||||
console.log(
|
||||
isDev
|
||||
? "Starting MCP inspector in development mode..."
|
||||
: "Starting MCP inspector...",
|
||||
);
|
||||
|
||||
let handle: { close(): Promise<void> };
|
||||
|
||||
try {
|
||||
if (isDev) {
|
||||
handle = await startViteDevServer(webConfig);
|
||||
} else {
|
||||
handle = await startHonoServer(webConfig);
|
||||
}
|
||||
} catch (err) {
|
||||
const message =
|
||||
err instanceof Error ? err.message : "Web client failed to start.";
|
||||
console.error("Error:", message);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const shutdown = () => {
|
||||
void handle.close().then(() => process.exit(0));
|
||||
};
|
||||
process.on("SIGINT", shutdown);
|
||||
process.on("SIGTERM", shutdown);
|
||||
|
||||
return new Promise<number>(() => {});
|
||||
}
|
||||
|
|
@ -1,10 +1,12 @@
|
|||
/**
|
||||
* Hono production server. Export startHonoServer(config) for in-process use by the runner.
|
||||
* When run as the main module (e.g. node dist/server.js), build config from env and start.
|
||||
* Hono production server. Export startHonoServer(config) for in-process use by
|
||||
* the runner. The server is started programmatically via `runWeb` (in
|
||||
* `run-web.ts`), which is the bundled `build/index.js` entry the `mcp-inspector`
|
||||
* bin invokes — there is no separate standalone server entry.
|
||||
*/
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join, dirname, resolve } from "node:path";
|
||||
import { join, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { randomBytes } from "node:crypto";
|
||||
import open from "open";
|
||||
|
|
@ -18,7 +20,6 @@ import { injectAuthToken } from "./inject-auth-token.js";
|
|||
import type { WebServerConfig } from "./web-server-config.js";
|
||||
import {
|
||||
webServerConfigToInitialPayload,
|
||||
buildWebServerConfigFromEnv,
|
||||
printServerBanner,
|
||||
} from "./web-server-config.js";
|
||||
import type { WebServerHandle } from "./types.js";
|
||||
|
|
@ -148,21 +149,3 @@ export async function startHonoServer(
|
|||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** Run when this file is executed as the main module (e.g. node dist/server.js). */
|
||||
async function runStandalone(): Promise<void> {
|
||||
const config = buildWebServerConfigFromEnv();
|
||||
const handle = await startHonoServer(config);
|
||||
const shutdown = () => {
|
||||
void handle.close().then(() => process.exit(0));
|
||||
};
|
||||
process.on("SIGINT", shutdown);
|
||||
process.on("SIGTERM", shutdown);
|
||||
}
|
||||
|
||||
const isMain =
|
||||
process.argv[1] !== undefined &&
|
||||
resolve(process.argv[1]) === resolve(__filename);
|
||||
if (isMain) {
|
||||
void runStandalone();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import react from "@vitejs/plugin-react";
|
|||
import type { WebServerConfig } from "./web-server-config.js";
|
||||
import { honoMiddlewarePlugin } from "./vite-hono-plugin.js";
|
||||
import { getViteBaseConfig } from "./vite-base-config.js";
|
||||
import { vitestSharedPaths } from "../../../vitest.shared.mts";
|
||||
import type { WebServerHandle } from "./types.js";
|
||||
|
||||
export type { WebServerHandle };
|
||||
|
|
@ -27,13 +28,37 @@ export async function startViteDevServer(
|
|||
// Canonicalize so Vite's config hash is stable and matches the deps cache.
|
||||
const root = resolve(join(__dirname, ".."));
|
||||
const baseConfig = getViteBaseConfig();
|
||||
// `configFile: false` means this in-process server never loads
|
||||
// `vite.config.ts`, so it must reproduce that file's `resolve` block and
|
||||
// `server.fs.allow` here. Without them, App.tsx's `@inspector/core/*`
|
||||
// imports fail to resolve and the page 500s (#1452 smoke test). The aliases
|
||||
// and dedupe are factored into `vitest.shared.mts` so both paths stay in
|
||||
// sync — pass the client dir (`root`) so bare-module pins resolve against
|
||||
// `clients/web/node_modules`.
|
||||
const { repoRoot, sharedAliases, sharedDedupe, nodeModulesAliases } =
|
||||
vitestSharedPaths(root);
|
||||
const inlineConfig: InlineConfig = {
|
||||
...baseConfig,
|
||||
configFile: false,
|
||||
root,
|
||||
resolve: {
|
||||
alias: [
|
||||
...Object.entries(sharedAliases).map(([find, replacement]) => ({
|
||||
find,
|
||||
replacement,
|
||||
})),
|
||||
...nodeModulesAliases,
|
||||
],
|
||||
dedupe: sharedDedupe,
|
||||
},
|
||||
server: {
|
||||
port: config.port,
|
||||
host: config.hostname,
|
||||
// Allow Vite to serve source files from the repo root (core/ lives
|
||||
// outside clients/web), matching `vite.config.ts`'s `server.fs.allow`.
|
||||
fs: {
|
||||
allow: [repoRoot],
|
||||
},
|
||||
},
|
||||
plugins: [react(), honoMiddlewarePlugin(config)],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -149,10 +149,18 @@ export function printServerBanner(
|
|||
return url;
|
||||
}
|
||||
|
||||
export interface BuildWebServerConfigOptions {
|
||||
initialMcpConfig?: MCPServerConfig | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build WebServerConfig from process.env. Used when running server as standalone (e.g. node dist/server.js).
|
||||
* Build WebServerConfig from process.env and optional initial MCP server config.
|
||||
* Used by the launcher runner, Vite dev (`buildWebServerConfigFromEnv`), and prod standalone.
|
||||
*/
|
||||
export function buildWebServerConfigFromEnv(): WebServerConfig {
|
||||
export function buildWebServerConfig(
|
||||
options: BuildWebServerConfigOptions = {},
|
||||
): WebServerConfig {
|
||||
const { initialMcpConfig = null } = options;
|
||||
const port = parseInt(process.env.CLIENT_PORT ?? "6274", 10);
|
||||
const hostname = process.env.HOST ?? "localhost";
|
||||
const baseUrl = `http://${hostname}:${port}`;
|
||||
|
|
@ -163,8 +171,6 @@ export function buildWebServerConfigFromEnv(): WebServerConfig {
|
|||
(process.env[LEGACY_AUTH_TOKEN_ENV] as string | undefined) ??
|
||||
"");
|
||||
|
||||
const initialMcpConfig: MCPServerConfig | null = null;
|
||||
|
||||
const sandboxPort = resolveSandboxPort();
|
||||
|
||||
let logger: Logger | undefined;
|
||||
|
|
@ -196,6 +202,13 @@ export function buildWebServerConfigFromEnv(): WebServerConfig {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build WebServerConfig from process.env only. Used by `vite dev` (the Hono dev plugin reads env, not argv).
|
||||
*/
|
||||
export function buildWebServerConfigFromEnv(): WebServerConfig {
|
||||
return buildWebServerConfig({ initialMcpConfig: null });
|
||||
}
|
||||
|
||||
/**
|
||||
* Three-way resolution for `autoOpen`:
|
||||
* - explicit `MCP_AUTO_OPEN_ENABLED=true` → always open (overrides VITEST)
|
||||
|
|
|
|||
Loading…
Reference in a new issue