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

@ -1 +1 @@
export { silentLogger } from "./logger.js";
export { silentLogger, type InspectorLogger } from "./logger.js";

View file

@ -1,7 +1,39 @@
import pino from "pino";
import type { Bindings, LevelWithSilentOrString, LogFn } from "pino";
/**
* Silent logger for use when no logger is injected. Satisfies pino.Logger,
* does not output anything. InspectorClient uses this as the default.
* Logging surface InspectorClient uses. Real pino loggers satisfy this;
* the default silent logger implements it without opening a stream.
*/
export const silentLogger = pino({ level: "silent" });
export interface InspectorLogger {
level: LevelWithSilentOrString;
fatal: LogFn;
error: LogFn;
warn: LogFn;
info: LogFn;
debug: LogFn;
trace: LogFn;
silent: LogFn;
child(bindings?: Bindings): InspectorLogger;
}
const noop: LogFn = () => {};
function createSilentLogger(): InspectorLogger {
const logger: InspectorLogger = {
level: "silent",
fatal: noop,
error: noop,
warn: noop,
info: noop,
debug: noop,
trace: noop,
silent: noop,
child: () => logger,
};
return logger;
}
/**
* Default logger when none is injected. No-op at all levels; no SonicBoom stream.
*/
export const silentLogger: InspectorLogger = createSilentLogger();

View file

@ -120,8 +120,7 @@ import {
import { ToolCallCancelledError } from "./toolCallCancelledError.js";
import type { AuthGuidedState, OAuthStep } from "../auth/types.js";
import type { OAuthTokens } from "@modelcontextprotocol/sdk/shared/auth.js";
import type pino from "pino";
import { silentLogger } from "../logging/logger.js";
import { silentLogger, type InspectorLogger } from "../logging/logger.js";
import { createFetchTracker } from "./fetchTracking.js";
import { OAuthManager, type OAuthManagerConfig } from "./oauthManager.js";
@ -236,7 +235,7 @@ export class InspectorClient extends InspectorClientEventTarget {
private receiverTaskRecords: Map<string, ReceiverTaskRecord> = new Map();
// OAuth support (config owned by oauthManager; client delegates and uses !!oauthManager for "is OAuth configured")
private oauthManager: OAuthManager | null = null;
private logger: pino.Logger;
private logger: InspectorLogger;
private transportClientFactory: CreateTransport;
private fetchFn?: typeof fetch;
private effectiveAuthFetch: typeof fetch;

View file

@ -1,5 +1,6 @@
import { existsSync, readFileSync } from "fs";
import { resolve } from "path";
import { getDefaultMcpConfigPath } from "../../storage/store-io.js";
import type {
MCPConfig,
MCPServerConfig,
@ -211,8 +212,26 @@ function applyOverrides(
export type ResolveServerConfigsMode = "single" | "multi";
export function hasAdHocServerOptions(options: ServerConfigOptions): boolean {
return (
(options.target != null && options.target.length > 0) ||
Boolean(options.transport) ||
Boolean(options.serverUrl?.trim())
);
}
/** When no --config and no ad-hoc target, use ~/.mcp-inspector/mcp.json (same as web). */
export function withDefaultConfigPath(
options: ServerConfigOptions,
): ServerConfigOptions {
if (options.configPath?.trim() || hasAdHocServerOptions(options)) {
return options;
}
return { ...options, configPath: getDefaultMcpConfigPath() };
}
/**
* Resolves server config(s) from options and mode. Used by all runners.
* Resolves server config(s) from explicit options and mode.
* Single mode: one config (from file + overrides, or from args).
* Multi mode: all servers from file (with optional env/cwd/headers overrides), or one from args; errors if config path + transport/serverUrl/positional.
*/
@ -221,10 +240,7 @@ export function resolveServerConfigs(
mode: ResolveServerConfigsMode,
): MCPServerConfig[] {
const hasConfigPath = Boolean(options.configPath?.trim());
const hasAdHoc =
(options.target && options.target.length > 0) ||
Boolean(options.transport) ||
Boolean(options.serverUrl);
const hasAdHoc = hasAdHocServerOptions(options);
if (mode === "single") {
if (hasConfigPath && options.serverName) {
@ -286,6 +302,17 @@ export function resolveServerConfigs(
return [];
}
/**
* Launch-time resolver for CLI/TUI: applies the default catalog path when no
* `--config` or ad-hoc target is given, then delegates to `resolveServerConfigs`.
*/
export function resolveLaunchServerConfigs(
options: ServerConfigOptions,
mode: ResolveServerConfigsMode,
): MCPServerConfig[] {
return resolveServerConfigs(withDefaultConfigPath(options), mode);
}
/**
* Returns named server configs from a config file (multi-server). Use when the caller
* needs server names (e.g. TUI). Errors if config path is missing or if ad-hoc options

View file

@ -1,7 +1,9 @@
export {
parseKeyValuePair,
parseHeaderPair,
withDefaultConfigPath,
resolveServerConfigs,
resolveLaunchServerConfigs,
getNamedServerConfigs,
type ServerConfigOptions,
type ResolveServerConfigsMode,

View file

@ -13,8 +13,7 @@
* concrete `InspectorClient` since the runtime class is not yet ported.
*/
import type pino from "pino";
import { silentLogger } from "../../logging/logger.js";
import { silentLogger, type InspectorLogger } from "../../logging/logger.js";
import type { InspectorClientProtocol } from "../inspectorClientProtocol.js";
import type { FetchRequestEntry } from "../types.js";
import type {
@ -63,7 +62,7 @@ export interface FetchRequestLogStateOptions {
* Logger for diagnostic traces (e.g. a deferred body update arriving after
* its entry rotated out of the log). Defaults to the silent logger.
*/
logger?: pino.Logger;
logger?: InspectorLogger;
}
export class FetchRequestLogState extends TypedEventTarget<FetchRequestLogStateEventMap> {
@ -71,7 +70,7 @@ export class FetchRequestLogState extends TypedEventTarget<FetchRequestLogStateE
private client: InspectorClientProtocol | null = null;
private unsubscribe: (() => void) | null = null;
private maxFetchRequests: number;
private readonly logger: pino.Logger;
private readonly logger: InspectorLogger;
constructor(
client: InspectorClientProtocol,

View file

@ -21,7 +21,7 @@ import type {
import type { Client } from "@modelcontextprotocol/sdk/client/index.js";
import type { OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js";
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
import type pino from "pino";
import type { InspectorLogger } from "../logging/logger.js";
import type { JsonValue } from "../json/jsonUtils.js";
import type {
OAuthNavigation,
@ -548,7 +548,7 @@ export interface InspectorClientEnvironment {
* - Node: pino file logger
* - Browser: createRemoteLogger
*/
logger?: pino.Logger;
logger?: InspectorLogger;
/**
* OAuth environment components

16
core/tsconfig.json Normal file
View file

@ -0,0 +1,16 @@
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
"noEmit": true,
"types": ["node"],
"typeRoots": ["../clients/web/node_modules/@types"]
},
"include": [
"mcp/**/*.ts",
"storage/**/*.ts",
"auth/**/*.ts",
"json/**/*.ts",
"logging/**/*.ts"
],
"exclude": ["node_modules", "**/__tests__/**", "react/**"]
}