From 2c1d3fdba80b011c2a34c0f5d91ab141a752cb47 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Sun, 13 Sep 2026 23:35:30 -0400 Subject: [PATCH] docs: add an environment-variable reference (#2365) Every variable that changes runtime behavior now lives on one page, docs/environment-variables.md: which client reads it, its default, and its effect. It covers three variables that were undocumented (MCP_LOG_FILE, MCP_INSPECTOR_LOG_DIR, LOG_LEVEL), the secret-store variables previously only in the Docker guide, and the Node TLS variables that answer #1936 (a self-signed https://localhost server). The README docs table, the migration guide, docker.md and the four client READMEs link to it. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01TRJ64MnDTg1TpzCTEV9Xva Signed-off-by: cliffhall --- README.md | 1 + clients/cli/README.md | 2 +- clients/launcher/README.md | 2 +- clients/tui/README.md | 2 + clients/web/README.md | 2 +- docs/docker.md | 2 +- docs/environment-variables.md | 107 ++++++++++++++++++++++++++++++++++ docs/v1-to-v2-migration.md | 2 + 8 files changed, 116 insertions(+), 4 deletions(-) create mode 100644 docs/environment-variables.md diff --git a/README.md b/README.md index 1b813e38..29b74bd9 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,7 @@ Each client has its own README with client-specific detail: | [Publishing](./docs/publishing.md) | What ships in the tarball, the packaging invariants, and `pack:verify` | | [Docker](./docs/docker.md) | Running the container image — ports, volumes, and where secrets go | | [Migrating from v1 to v2](./docs/v1-to-v2-migration.md) | CLI flag mapping, `--config` vs. `--catalog`, the Node engine bump, env-var renames | +| [Environment variables](./docs/environment-variables.md) | Every variable that changes runtime behavior — auth, ports, storage, the secret store, logging, proxies — plus the Node TLS variables for a self-signed server | | [MCP server configuration](./docs/mcp-server-configuration.md) | Which server(s) the Inspector connects to, and the config file format | | [Reviewing an MCP App](./docs/mcp-app-review.md) | The CLI-first → one-shot-web recipe for automated App-tool review | | [Smoke-testing an MCP server](./docs/cli-smoke-testing.md) | The connect → list → call → assert workflow for a shell or CI job: `--format json` + `jq`, the exit-code map, and keeping OAuth non-interactive | diff --git a/clients/cli/README.md b/clients/cli/README.md index 175c8937..c1fe9467 100644 --- a/clients/cli/README.md +++ b/clients/cli/README.md @@ -84,7 +84,7 @@ When a server is loaded from a `--catalog`/`--config` file, its per-server setti The file is the only durable way to give a run its roots: there is no roots flag, and `--method roots/set` applies only to that one short-lived connection. Roots configured for a server (the same field the web UI's Server Settings writes) are advertised at connect, so a server that asks for `roots/list` — `@modelcontextprotocol/server-filesystem` does, to learn its allowed directories — gets them. -**Environment-variable semantics.** `MCP_CATALOG_PATH` is honored only when no ad-hoc target is given (positional command, `--server-url`, or `--transport`) — so a shell that exports it can still run one-off ad-hoc invocations without hitting the catalog/ad-hoc conflict. `MCP_STORAGE_DIR` sets the storage directory used by the OAuth persist backend (`/oauth.json`); the per-file `MCP_INSPECTOR_OAUTH_STATE_PATH` override still takes precedence over it. +**Environment-variable semantics.** `MCP_CATALOG_PATH` is honored only when no ad-hoc target is given (positional command, `--server-url`, or `--transport`) — so a shell that exports it can still run one-off ad-hoc invocations without hitting the catalog/ad-hoc conflict. `MCP_STORAGE_DIR` sets the storage directory used by the OAuth persist backend (`/oauth.json`); the per-file `MCP_INSPECTOR_OAUTH_STATE_PATH` override still takes precedence over it. Every variable the CLI reads is listed in [Environment variables](../../docs/environment-variables.md). ### HTTP proxy support diff --git a/clients/launcher/README.md b/clients/launcher/README.md index e69cf0cb..207c2a2c 100644 --- a/clients/launcher/README.md +++ b/clients/launcher/README.md @@ -10,7 +10,7 @@ The launcher is the package that provides the global `mcp-inspector` binary (e.g 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. -**Error reporting.** A `--cli` failure is routed through the CLI's own error sink, so `mcp-inspector --cli` preserves the CLI exit-code map (`1` usage, `2` no-app, `3` auth-required, `4` unreachable, `5` tool-error) and its machine-readable `{"error":{…}}` stderr envelope — the same as invoking the CLI bin directly. `--web` / `--tui` failures print a human-readable `Error: ` and exit `1` (append `MCP_DEBUG=1` for the stack). +**Error reporting.** A `--cli` failure is routed through the CLI's own error sink, so `mcp-inspector --cli` preserves the CLI exit-code map (`1` usage, `2` no-app, `3` auth-required, `4` unreachable, `5` tool-error) and its machine-readable `{"error":{…}}` stderr envelope — the same as invoking the CLI bin directly. `--web` / `--tui` failures print a human-readable `Error: ` and exit `1` (append `MCP_DEBUG=1` for the stack). `MCP_DEBUG` (or `DEBUG`) is the only variable the launcher reads itself; the rest are listed in [Environment variables](../../docs/environment-variables.md). ## Web server-list flags (`--web`) diff --git a/clients/tui/README.md b/clients/tui/README.md index 17fdb0c9..54cc8352 100644 --- a/clients/tui/README.md +++ b/clients/tui/README.md @@ -52,6 +52,8 @@ OAuth redirect URIs must match **exactly** what you register on the authorizatio #### Flags +Variables with no flag of their own — the secret store, `MCP_INSPECTOR_LOG_DIR`, `LOG_LEVEL`, proxies — are listed in [Environment variables](../../docs/environment-variables.md). + | Option | Env | Description | | ----------------------------- | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `--client-config ` | `MCP_CLIENT_CONFIG_PATH` | Install-level client config (default: `~/.mcp-inspector/storage/client.json`). | diff --git a/clients/web/README.md b/clients/web/README.md index 02d97a92..b2952821 100644 --- a/clients/web/README.md +++ b/clients/web/README.md @@ -384,7 +384,7 @@ Storybook is first-class here because the components are presentational — each ## Auth token -The dev/prod backend guards every `/api/*` route with `x-mcp-remote-auth: Bearer `. The browser recovers the token, in priority order (see `App.tsx` `getAuthToken()`): the `window.__INSPECTOR_API_TOKEN__` global injected into `index.html` on every page load (`server/inject-auth-token.ts`), then a `?MCP_INSPECTOR_API_TOKEN=…` query param, then `sessionStorage`. Injection is a no-op when auth is disabled (`DANGEROUSLY_OMIT_AUTH`). See the root [AGENTS.md](../../AGENTS.md) for the full rationale. +The dev/prod backend guards every `/api/*` route with `x-mcp-remote-auth: Bearer `. The browser recovers the token, in priority order (see `App.tsx` `getAuthToken()`): the `window.__INSPECTOR_API_TOKEN__` global injected into `index.html` on every page load (`server/inject-auth-token.ts`), then a `?MCP_INSPECTOR_API_TOKEN=…` query param, then `sessionStorage`. Injection is a no-op when auth is disabled (`DANGEROUSLY_OMIT_AUTH`). See the root [AGENTS.md](../../AGENTS.md) for the full rationale, and [Environment variables](../../docs/environment-variables.md) for every variable the backend reads. ## Host binding & the origin allow-list diff --git a/docs/docker.md b/docs/docker.md index ed93353c..46d6278a 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -77,7 +77,7 @@ If another process holds the lock and will not let go, the save **fails** rather It is also what covers the lock being unavailable. This store exists for boxes where the usual mechanism isn't there, so a directory that can't hold a lock file — a read-only `$HOME`, a mount owned by another uid — makes the save proceed unlocked with a warning, rather than turning every `set` into a failure on exactly the deployments the store was written for. -Three env vars affect where the file lands. `MCP_INSPECTOR_SECRET_STORE=keyring|file|memory` picks the store outright, bypassing the probe. `MCP_INSPECTOR_SECRET_FILE` names the file. Failing both, the file follows `MCP_STORAGE_DIR` — the same variable that relocates OAuth tokens and `client.json` — so mounting a volume at your configured storage directory is enough to make secrets durable there. +Three env vars affect where the file lands. `MCP_INSPECTOR_SECRET_STORE=keyring|file|memory` picks the store outright, bypassing the probe. `MCP_INSPECTOR_SECRET_FILE` names the file. Failing both, the file follows `MCP_STORAGE_DIR` — the same variable that relocates OAuth tokens and `client.json` — so mounting a volume at your configured storage directory is enough to make secrets durable there. These variables apply outside a container too; every runtime variable is listed in [Environment variables](./environment-variables.md). **Upgrading from an image before this fix?** Earlier images did not create `/home/node/.mcp-inspector`, so Docker created the volume's mount point as `root` and the non-root `node` user couldn't write to it. An **empty** volume repairs itself on the first run of a current image (Docker applies the image directory's ownership to an empty volume), but one that already has files in it keeps its old `root` ownership and still fails with `EACCES`. Fix it once: diff --git a/docs/environment-variables.md b/docs/environment-variables.md new file mode 100644 index 00000000..56a64f9b --- /dev/null +++ b/docs/environment-variables.md @@ -0,0 +1,107 @@ +# Environment variables + +Every environment variable that changes how the Inspector behaves at runtime, in one place. Set them in the shell that launches `mcp-inspector` (or with `-e` for the [Docker image](./docker.md)). + +The **Read by** column names the client whose process reads the variable: **web** is the Node backend that `--web` starts (the browser itself reads no environment), **CLI** and **TUI** are those clients, and **launcher** is the `mcp-inspector` bin that picks one of them. A variable read in shared `core/` code is marked with every client that reaches it. + +Unless a row says otherwise, an unset variable and an empty one behave the same. + +## Authentication and network exposure + +These guard the web backend, which spawns processes on request. Read [Host binding and the origin allow-list](../clients/web/README.md#host-binding--the-origin-allow-list) before widening any of them. + +| Variable | Read by | Default | Effect | +| --------------------------------- | -------- | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `MCP_INSPECTOR_API_TOKEN` | web, CLI | a random token per launch | Bearer token guarding every `/api/*` route (`x-mcp-remote-auth: Bearer `). Set it to use a known token instead of the generated one printed in the launch banner. The CLI reads it only to fill the `autoConnect` parameter of the deep link it emits. | +| `MCP_PROXY_AUTH_TOKEN` | web | — | **Deprecated** v1 name for `MCP_INSPECTOR_API_TOKEN`, used only when the new name is unset. | +| `DANGEROUSLY_OMIT_AUTH` | web | unset | Disables the API token entirely. ⚠️ **Any non-empty value turns auth off, including `false` and `0`** — unset the variable to keep auth on. | +| `HOST` | web, CLI | `127.0.0.1` | Address the web server binds. An all-interfaces host (`0.0.0.0`, `::`, and equivalent spellings) is **refused** unless `DANGEROUSLY_BIND_ALL_INTERFACES` is enabled. The CLI reads it only to build its deep link. | +| `DANGEROUSLY_BIND_ALL_INTERFACES` | web | off | Opts in to an all-interfaces `HOST`. Only `true` or `1` (case-insensitive) enable it, so `false` reads as off. The Docker image sets it. | +| `ALLOWED_ORIGINS` | web | the loopback origins | Comma-separated origins allowed to call the API. **Replaces** the default list rather than adding to it, so list every form you browse from. Each entry must include the scheme (`http://localhost:6274`). | + +## Ports + +The web client needs **fixed** ports: the origin allow-list and the MCP Apps sandbox CSP are both derived from them. + +| Variable | Read by | Default | Effect | +| --------------------- | -------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `CLIENT_PORT` | web, CLI | `6274` | Web UI port. Must be an integer in 1–65535; `0` (a dynamic port) is rejected at startup. | +| `MCP_SANDBOX_PORT` | web, CLI | `6275` | Port of the MCP Apps sandbox server. An invalid value is ignored with a warning. | +| `SERVER_PORT` | web | — | v1's proxy port, now only a fallback for `MCP_SANDBOX_PORT` when that is unset. | +| `MCP_APP_ORIGIN_PORT` | web, CLI | `6278` | Port of the dedicated app-origin server, used only by an MCP App whose UI resource declares `_meta.ui.domain`. An invalid value is ignored with a warning. | + +The CLI reads the three port variables and `HOST` only to build the deep link and port list it hands to a web session; it binds none of them. + +## Behavior + +| Variable | Read by | Default | Effect | +| ------------------------ | ------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `MCP_AUTO_OPEN_ENABLED` | web, CLI, TUI | open | `false` never opens a browser; `true` always does. In the CLI and TUI it also governs the interactive OAuth flow, and `true` opens the browser even when stderr is not a TTY. Any other value behaves as unset. | +| `MCP_CATALOG_PATH` | web, CLI, TUI | `~/.mcp-inspector/mcp.json` | Default writable catalog, used when no `--catalog` is given. The CLI honors it only when no ad-hoc target (positional command, `--server-url`, or `--transport`) is given. See [MCP server configuration](./mcp-server-configuration.md). | +| `MCP_OAUTH_CALLBACK_URL` | CLI, TUI | `http://127.0.0.1:6276/oauth/callback` | Loopback redirect URL for the CLI/TUI OAuth flow. `--callback-url` takes precedence. | +| `NO_COLOR` | CLI | unset | Any value disables ANSI styling in the CLI's human-readable output. | + +## Storage and state + +| Variable | Read by | Default | Effect | +| -------------------------------- | ------------- | ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `MCP_STORAGE_DIR` | web, CLI, TUI | `~/.mcp-inspector/storage` | Storage directory. Relocates the OAuth state file (`oauth.json`) and the secrets file (`secrets.json`) for every client. For the **web** backend it also relocates `client.json`; the CLI and TUI find `client.json` through `MCP_CLIENT_CONFIG_PATH` instead. | +| `MCP_INSPECTOR_OAUTH_STATE_PATH` | web, CLI, TUI | `/oauth.json` | Names the OAuth state file outright. Lookup order: this variable, then `/oauth.json`, then `~/.mcp-inspector/storage/oauth.json`. ⚠️ Setting `MCP_STORAGE_DIR` alone does not isolate a run if this variable is also exported. | +| `MCP_CLIENT_CONFIG_PATH` | CLI, TUI | `~/.mcp-inspector/storage/client.json` | Install-level client config (CIMD, enterprise IdP). `--client-config` takes precedence. | + +## Secret store + +Where server secrets (headers, client secrets) are kept. The details — the keychain probe, the file format, encryption and locking — are in the [Docker guide](./docker.md); these variables apply to every install, not only containers. + +| Variable | Read by | Default | Effect | +| ---------------------------- | ------------- | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `MCP_INSPECTOR_SECRET_STORE` | web, CLI, TUI | probe the OS keychain | `keyring`, `file`, or `memory` (case-insensitive) picks the store outright and skips the probe. Any other value is ignored with a warning. | +| `MCP_INSPECTOR_SECRET_FILE` | web, CLI, TUI | `/secrets.json` | Path of the file store. Falls back to `secrets.json` in `MCP_STORAGE_DIR`, then to `~/.mcp-inspector/secrets.json`. | +| `MCP_INSPECTOR_SECRET_KEY` | web, CLI, TUI | unset (file is plaintext, `0600`) | Passphrase that encrypts the file store. Use a generated, high-entropy value. ⚠️ Changing or losing it makes the existing file unreadable; see the Docker guide before rotating it. | + +When no store is configured, the choice also depends on whether the Inspector is running in a container, which it detects from `KUBERNETES_SERVICE_HOST` (or Docker's and Podman's marker files). That variable is set by the orchestrator, not by you. + +## Logging and debugging + +| Variable | Read by | Default | Effect | +| ----------------------- | -------- | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | +| `MCP_DEBUG` / `DEBUG` | launcher | off | Prints the full stack trace when the launcher exits on an error. Any value other than `0` or `false` (case-insensitive) turns it on. | +| `MCP_LOG_FILE` | web | unset (no log) | Appends the web backend's structured (pino, JSON lines) log to this file, creating its directory if needed. | +| `MCP_INSPECTOR_LOG_DIR` | TUI | `~/.mcp-inspector` | Directory of the TUI's `auth.log`. The TUI logs to a file so its output does not corrupt the terminal UI. | +| `LOG_LEVEL` | TUI | `info` | Level of the TUI's `auth.log` (`trace`, `debug`, `info`, `warn`, `error`, `fatal`, `silent`). | + +## Outbound proxy + +| Variable | Read by | Default | Effect | +| ------------------------------- | ------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `HTTPS_PROXY` / `HTTP_PROXY` | web, CLI, TUI | unset | Route connections to remote HTTP/SSE servers, including OAuth discovery and token requests, through a proxy. Lowercase forms are honored too. See [HTTP proxy support](../clients/cli/README.md#http-proxy-support). | +| `NO_PROXY` | web, CLI, TUI | unset | Hosts exempted from the proxy. | + +## Node.js variables + +These belong to Node, not the Inspector, but they are the answer to some common connection problems. Because the web client connects to MCP servers **from its Node backend**, not from the browser, they apply to all three clients. + +### Connecting to a server with a self-signed certificate + +A server at `https://localhost`, or anywhere else with a self-signed or privately issued certificate, fails to connect with a generic fetch error, because Node does not trust the certificate. Trusting the browser's exception does not help — the browser is not the one connecting ([#1936](https://github.com/modelcontextprotocol/inspector/issues/1936)). + +**Prefer trusting the certificate's CA.** `NODE_EXTRA_CA_CERTS` adds certificates to the ones Node trusts, and leaves verification on for every other connection: + +```sh +NODE_EXTRA_CA_CERTS=/path/to/your-ca.pem npx @modelcontextprotocol/inspector +``` + +For a self-signed certificate with no separate CA, point it at the certificate itself. Node reads the variable once at startup, so it must be set before launching. + +**Or turn verification off, for local development only:** + +```sh +NODE_TLS_REJECT_UNAUTHORIZED=0 npx @modelcontextprotocol/inspector +``` + +⚠️ This disables certificate verification for **every** TLS connection the Inspector process makes — every MCP server, every OAuth authorization server and token endpoint — not just the one you are testing, and Node prints a warning saying so. A connection that would have been refused for impersonation is then accepted. Never set it in a shell profile, a shared environment, or a deployment. + +| Variable | Default | Effect | +| ------------------------------ | ------- | --------------------------------------------------------------------------------------------------------- | +| `NODE_EXTRA_CA_CERTS` | unset | PEM file of extra CA certificates to trust, in addition to Node's bundled ones. Read once at startup. | +| `NODE_TLS_REJECT_UNAUTHORIZED` | `1` | `0` disables TLS certificate verification for the whole process. Development only. | diff --git a/docs/v1-to-v2-migration.md b/docs/v1-to-v2-migration.md index 752ecc12..62043b82 100644 --- a/docs/v1-to-v2-migration.md +++ b/docs/v1-to-v2-migration.md @@ -268,6 +268,8 @@ Stdout is otherwise compatible: the default `text` format still pretty-prints th ## Environment variables +This table maps v1 names to v2. Defaults, accepted values, and the variables with no v1 counterpart are in [Environment variables](./environment-variables.md). + | v1 | v2 | Notes | | ------------------------ | ----------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `MCP_PROXY_AUTH_TOKEN` | **`MCP_INSPECTOR_API_TOKEN`** | Renamed, but the **old name still works** as a deprecated fallback when the new one is unset — an existing deployment keeps running while you migrate. Guards `/api/*` via `x-mcp-remote-auth: Bearer `; the browser also receives it injected into `index.html`, so a bare reload keeps working. (The `?MCP_PROXY_AUTH_TOKEN=` **query param** has no such fallback — see [Web UI](#web-ui).) |