Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions clients/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,12 @@ Options that specify the MCP server (config file, ad-hoc command/URL, env vars,
| `--metadata <key=value>` | General metadata (key=value); applied to all methods. |
| `--tool-metadata <key=value>` | Tool-specific metadata for `tools/call`. |

### Logging (env)

| Env var | Description |
| -------------- | --------------------------------------------------------------- |
| `MCP_LOG_FILE` | If set, CLI and InspectorClient logs are appended to this file. |

## Why use the CLI?

While the Web Client provides a rich visual interface, the CLI is designed for:
Expand Down
24 changes: 22 additions & 2 deletions clients/cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ import {
parseKeyValuePair as parseEnvPair,
parseHeaderPair,
} from "@modelcontextprotocol/inspector-core/mcp/node/index.js";
import {
createFileLogger,
silentLogger,
} from "@modelcontextprotocol/inspector-core/logging/node";
import type { JsonValue } from "@modelcontextprotocol/inspector-core/mcp/index.js";
import {
LoggingLevelSchema,
Expand Down Expand Up @@ -58,9 +62,21 @@ async function callMethod(
const version = packageJson.version;
const clientIdentity = { name, version };

const logger = process.env.MCP_LOG_FILE
? await createFileLogger({
dest: process.env.MCP_LOG_FILE,
append: true,
mkdir: true,
level: "info",
name: "mcp-inspector-cli",
})
: silentLogger;
logger.info("CLI starting");

const inspectorClient = new InspectorClient(serverConfig, {
environment: {
transport: createTransportNode,
logger,
},
clientIdentity,
initialLoggingLevel: "debug",
Expand Down Expand Up @@ -413,8 +429,12 @@ function parseArgs(argv?: string[]): {
}

export async function runCli(argv?: string[]): Promise<void> {
const { serverConfig, methodArgs } = parseArgs(argv ?? process.argv);
await callMethod(serverConfig, methodArgs);
try {
const { serverConfig, methodArgs } = parseArgs(argv ?? process.argv);
await callMethod(serverConfig, methodArgs);
} catch (error) {
handleError(error);
}
}

export async function main(): Promise<void> {
Expand Down
6 changes: 6 additions & 0 deletions clients/tui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ When connecting to SSE or Streamable HTTP servers that use OAuth, you can pass:
| `--client-metadata-url <url>` | OAuth Client ID Metadata Document URL (CIMD). |
| `--callback-url <url>` | OAuth redirect/callback listener URL (default: `http://127.0.0.1:0/oauth/callback`). |

### Logging (env)

| Env var | Description |
| -------------- | --------------------------------------------------------------- |
| `MCP_LOG_FILE` | If set, TUI and InspectorClient logs are appended to this file. |

## Features

The TUI provides terminal-native tabs and panes for interacting with your MCP server:
Expand Down
45 changes: 26 additions & 19 deletions clients/tui/src/logger.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,30 @@
import path from "node:path";
import pino from "pino";
import type { Logger } from "pino";
import {
silentLogger,
createFileLogger,
} from "@modelcontextprotocol/inspector-core/logging/node";

const logDir =
process.env.MCP_INSPECTOR_LOG_DIR ??
path.join(
process.env.HOME || process.env.USERPROFILE || ".",
".mcp-inspector",
);
const logPath = path.join(logDir, "auth.log");
/**
* TUI logger (InspectorClient events, auth, etc.).
* File logger when MCP_LOG_FILE is set, else silentLogger.
*/
export let tuiLogger: Logger = silentLogger;

/**
* TUI file logger for auth and InspectorClient events.
* Writes to ~/.mcp-inspector/auth.log so TUI console output is not corrupted.
* The app controls logger creation and configuration.
* If MCP_LOG_FILE is set, creates a file logger (awaits destination ready);
* otherwise uses silentLogger. Call at the start of runTui() before any work
* that might call process.exit().
*/
export const tuiLogger = pino(
{
name: "mcp-inspector-tui",
level: process.env.LOG_LEVEL ?? "info",
},
pino.destination({ dest: logPath, append: true, mkdir: true }),
);
export async function initTuiLogger(): Promise<void> {
if (process.env.MCP_LOG_FILE) {
tuiLogger = await createFileLogger({
dest: process.env.MCP_LOG_FILE,
append: true,
mkdir: true,
level: "info",
name: "mcp-inspector-tui",
});
} else {
tuiLogger = silentLogger;
}
}
4 changes: 4 additions & 0 deletions clients/tui/tui.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,13 @@ import {
parseKeyValuePair,
parseHeaderPair,
} from "@modelcontextprotocol/inspector-core/mcp/node/index.js";
import { initTuiLogger, tuiLogger } from "./src/logger.js";
import App from "./src/App.js";

export async function runTui(args?: string[]): Promise<void> {
await initTuiLogger();
tuiLogger.info("TUI starting");

const program = new Command();

program
Expand Down
2 changes: 1 addition & 1 deletion clients/web/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ All of these are set via **environment variables**; the web app has no command-l
| **Sandbox port** (MCP Apps) | Env: `MCP_SANDBOX_PORT`; if unset, `SERVER_PORT` (legacy). Use `0` or leave unset for an automatic port. | automatic |
| **Storage directory** (e.g. OAuth) | Env: `MCP_STORAGE_DIR` | (unset) |
| **Allowed origins** (CORS) | Env: `ALLOWED_ORIGINS` (comma-separated) | client origin only |
| **Log file** | Env: `MCP_LOG_FILE` — if set, server logs are appended to this file. | (unset) |
| **Log file** | Env: `MCP_LOG_FILE` — if set, server and InspectorClient logs are appended to this file. | (unset) |
| **Open browser on start** | Env: `MCP_AUTO_OPEN_ENABLED` — set to `false` to disable. | `true` |
| **Development mode** | CLI only: `--dev` (Vite with HMR). No env var. | off |

Expand Down
2 changes: 1 addition & 1 deletion clients/web/src/components/AuthDebugger.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useCallback, useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import { AlertCircle } from "lucide-react";
import type { InspectorClient } from "@modelcontextprotocol/inspector-core/mcp/index.js";
import { silentLogger } from "@modelcontextprotocol/inspector-core/logging";
import { silentLogger } from "@modelcontextprotocol/inspector-core/logging/browser";
import type { AuthGuidedState } from "@modelcontextprotocol/inspector-core/auth/types.js";
import type { WebEnvironmentResult } from "@/lib/adapters/environmentFactory";
import { OAuthFlowProgress } from "./OAuthFlowProgress";
Expand Down
2 changes: 1 addition & 1 deletion clients/web/src/components/OAuthCallback.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useCallback, useEffect, useRef, useState } from "react";
import type { InspectorClient } from "@modelcontextprotocol/inspector-core/mcp/index.js";
import type { WebEnvironmentResult } from "@/lib/adapters/environmentFactory";
import { parseOAuthState } from "@modelcontextprotocol/inspector-core/auth/index.js";
import { silentLogger } from "@modelcontextprotocol/inspector-core/logging";
import { silentLogger } from "@modelcontextprotocol/inspector-core/logging/browser";
import useTheme from "@/lib/hooks/useTheme";
import { useToast } from "@/lib/hooks/useToast";
import {
Expand Down
3 changes: 2 additions & 1 deletion clients/web/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export interface WebServerHandle {
export async function startHonoServer(
config: WebServerConfig,
): Promise<WebServerHandle> {
config.logger.info("Web server starting");
const sandboxController = createSandboxController({
port: config.sandboxPort,
host: config.sandboxHost,
Expand Down Expand Up @@ -131,7 +132,7 @@ 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 config = await buildWebServerConfigFromEnv();
const handle = await startHonoServer(config);
const shutdown = () => {
void handle.close().then(() => process.exit(0));
Expand Down
33 changes: 20 additions & 13 deletions clients/web/src/vite-hono-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,20 @@ import {
} from "./web-server-config.js";

/**
* Plugin factory. Caller must pass a WebServerConfig (runner builds it from argv; vite.config builds it from env via buildWebServerConfigFromEnv).
* Plugin factory. Caller must pass a WebServerConfig or Promise<WebServerConfig>
* (runner builds from argv; vite.config passes buildWebServerConfigFromEnv() which is async).
*/
export function honoMiddlewarePlugin(config: WebServerConfig): Plugin {
export function honoMiddlewarePlugin(
config: WebServerConfig | Promise<WebServerConfig>,
): Plugin {
return {
name: "hono-api-middleware",
async configureServer(server) {
const resolvedConfig = await Promise.resolve(config);
resolvedConfig.logger.info("Web server starting (dev)");
const sandboxController = createSandboxController({
port: config.sandboxPort,
host: config.sandboxHost,
port: resolvedConfig.sandboxPort,
host: resolvedConfig.sandboxHost,
});
await sandboxController.start();

Expand All @@ -40,13 +45,15 @@ export function honoMiddlewarePlugin(config: WebServerConfig): Plugin {
};

const { app: honoApp, authToken: resolvedToken } = createRemoteApp({
authToken: config.dangerouslyOmitAuth ? undefined : config.authToken,
dangerouslyOmitAuth: config.dangerouslyOmitAuth,
storageDir: config.storageDir,
allowedOrigins: config.allowedOrigins,
authToken: resolvedConfig.dangerouslyOmitAuth
? undefined
: resolvedConfig.authToken,
dangerouslyOmitAuth: resolvedConfig.dangerouslyOmitAuth,
storageDir: resolvedConfig.storageDir,
allowedOrigins: resolvedConfig.allowedOrigins,
sandboxUrl: sandboxController.getUrl() ?? undefined,
logger: config.logger,
initialConfig: webServerConfigToInitialPayload(config),
logger: resolvedConfig.logger,
initialConfig: webServerConfigToInitialPayload(resolvedConfig),
});

const sandboxUrl = sandboxController.getUrl();
Expand All @@ -56,16 +63,16 @@ export function honoMiddlewarePlugin(config: WebServerConfig): Plugin {
const actualPort =
typeof address === "object" && address !== null
? address.port
: config.port;
: resolvedConfig.port;

const url = printServerBanner(
config,
resolvedConfig,
actualPort,
resolvedToken,
sandboxUrl ?? undefined,
);

if (config.autoOpen) {
if (resolvedConfig.autoOpen) {
open(url);
}
};
Expand Down
24 changes: 13 additions & 11 deletions clients/web/src/web-server-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@
* Config object for the web server (dev and prod). Passed in-process; no env handoff.
*/

import pino from "pino";
import type { Logger } from "pino";
import type { MCPServerConfig } from "@modelcontextprotocol/inspector-core/mcp/types.js";
import {
createFileLogger,
silentLogger,
} from "@modelcontextprotocol/inspector-core/logging/node";
import {
API_SERVER_ENV_VARS,
LEGACY_AUTH_TOKEN_ENV,
Expand All @@ -24,7 +27,7 @@ export interface WebServerConfig {
/** Sandbox port (0 = dynamic). */
sandboxPort: number;
sandboxHost: string;
logger: Logger | undefined;
logger: Logger;
/** When true, open browser after server starts. */
autoOpen: boolean;
/** Root directory for static files (index.html, assets). When runner starts server in-process, pass path to dist/. */
Expand Down Expand Up @@ -148,8 +151,9 @@ export function printServerBanner(

/**
* Build WebServerConfig from process.env. Used when running server as standalone (e.g. node dist/server.js).
* When MCP_LOG_FILE is set, returns a Promise (file logger destination must be awaited).
*/
export function buildWebServerConfigFromEnv(): WebServerConfig {
export async function buildWebServerConfigFromEnv(): Promise<WebServerConfig> {
const port = parseInt(process.env.CLIENT_PORT ?? "6274", 10);
const hostname = process.env.HOST ?? "localhost";
const baseUrl = `http://${hostname}:${port}`;
Expand All @@ -164,17 +168,15 @@ export function buildWebServerConfigFromEnv(): WebServerConfig {

const sandboxPort = resolveSandboxPort();

let logger: Logger | undefined;
if (process.env.MCP_LOG_FILE) {
logger = pino(
{ level: "info" },
pino.destination({
const logger = process.env.MCP_LOG_FILE
? await createFileLogger({
dest: process.env.MCP_LOG_FILE,
append: true,
mkdir: true,
}),
);
}
level: "info",
name: "mcp-inspector-web",
})
: silentLogger;

return {
port,
Expand Down
19 changes: 18 additions & 1 deletion clients/web/src/web.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { resolve, join, dirname } from "path";
import { fileURLToPath } from "url";
import { randomBytes } from "crypto";
import { Command } from "commander";
import type { Logger } from "pino";
import type {
MCPServerConfig,
StreamableHttpServerConfig,
Expand All @@ -16,6 +17,10 @@ import {
parseHeaderPair,
type ServerConfigOptions,
} from "@modelcontextprotocol/inspector-core/mcp/node/config.js";
import {
createFileLogger,
silentLogger,
} from "@modelcontextprotocol/inspector-core/logging/node";
import { resolveSandboxPort } from "./sandbox-controller.js";
import type { WebServerConfig } from "./web-server-config.js";
import { startViteDevServer } from "./start-vite-dev-server.js";
Expand Down Expand Up @@ -93,6 +98,7 @@ function buildWebServerConfig(
hostname: string,
authToken: string,
dangerouslyOmitAuth: boolean,
logger: Logger,
): WebServerConfig {
const baseUrl = `http://${hostname}:${port}`;
const initialMcpConfig: MCPServerConfig | null =
Expand Down Expand Up @@ -136,7 +142,7 @@ function buildWebServerConfig(
],
sandboxPort: resolveSandboxPort(),
sandboxHost: hostname,
logger: undefined,
logger,
autoOpen: process.env.MCP_AUTO_OPEN_ENABLED !== "false",
};
}
Expand Down Expand Up @@ -252,12 +258,23 @@ export async function runWeb(argv: string[]): Promise<number> {
(process.env[LEGACY_AUTH_TOKEN_ENV] as string | undefined) ??
randomBytes(32).toString("hex"));

const logger = process.env.MCP_LOG_FILE
? await createFileLogger({
dest: process.env.MCP_LOG_FILE,
append: true,
mkdir: true,
level: "info",
name: "mcp-inspector-web",
})
: silentLogger;

const webConfig = buildWebServerConfig(
clientOptions,
port,
hostname,
authToken,
dangerouslyOmitAuth,
logger,
);
if (!clientOptions.isDev) {
webConfig.staticRoot = join(__dirname, "..", "dist");
Expand Down
4 changes: 2 additions & 2 deletions core/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ export type { OAuthStateMode } from "./utils.js";
// Discovery
export { discoverScopes } from "./discovery.js";

// Logging (re-exported from core/logging)
export { silentLogger } from "../logging/index.js";
// Logging (re-exported from core/logging/node)
export { silentLogger } from "../logging/node/index.js";
// State Machine
export type { StateMachineContext, StateTransition } from "./state-machine.js";
export { oauthTransitions, OAuthStateMachine } from "./state-machine.js";
File renamed without changes.
10 changes: 10 additions & 0 deletions core/logging/browser/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* Silent logger for browser contexts. Satisfies pino.Logger; does not output anything.
* Use when no logger is passed (e.g. OAuth callback with no client). Web components
* that need a fallback import from logging/browser so they don't pull in Node logging.
*/
// @ts-expect-error - pino/browser.js exists but TypeScript doesn't have types for the .js extension
import pino from "pino/browser.js";
import type { Logger } from "pino";

export const silentLogger: Logger = pino({ level: "silent" });
Loading