diff --git a/docs/environment-variables.md b/docs/environment-variables.md index a8b3f2a60..392abcbfd 100644 --- a/docs/environment-variables.md +++ b/docs/environment-variables.md @@ -2,7 +2,7 @@ Every environment variable that changes how the Inspector behaves at runtime, in one place: the Inspector's own variables, plus the standard system and Node variables it reads (`HOME`, the proxy variables, the Node TLS variables). 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. +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**, **TUI** and **mcpdo** (the connection CLI, and the daemon it starts) 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. ⚠️ **Unset a variable rather than setting it to an empty string.** The two are not interchangeable: `HOST=""` is read as an all-interfaces bind and refused, and an empty path variable such as `MCP_STORAGE_DIR=` or `MCP_INSPECTOR_LOG_DIR=` can resolve relative to the working directory instead of falling back to the default. A row says so explicitly where an empty value is treated as unset. @@ -12,22 +12,22 @@ A `~` in a default below means the home directory as described under [Home direc 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 when set to `true` or `1` (trimmed, case-insensitive). Any other value — including `false`, `0` and empty — keeps auth on. | -| `HOST` | web, CLI | `127.0.0.1` | Address the web server binds. An all-interfaces host (`0.0.0.0`, `::`, an empty string, 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. | +| 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 when set to `true` or `1` (trimmed, case-insensitive). Any other value — including `false`, `0` and empty — keeps auth on. | +| `HOST` | web, CLI | `127.0.0.1` | Address the web server binds. An all-interfaces host (`0.0.0.0`, `::`, an empty string, 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 | derived from `HOST` | Comma-separated origins allowed to call the API. Unset, the list follows `HOST` at `CLIENT_PORT`: the loopback origins for a loopback host, the loopback origins plus `http://0.0.0.0` and `http://[::]` for an all-interfaces bind, and otherwise only the configured host's own origin (so binding a LAN address does **not** also allow `localhost`). **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`). The same list is the MCP Apps sandbox proxy's embedder allow-list (its `frame-ancestors` header and its referrer check), so a public Inspector origin must be listed here for the Apps tab to render. | ## Ports -| Variable | Read by | Default | Effect | -| --------------------- | -------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Variable | Read by | Default | Effect | +| --------------------- | -------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `CLIENT_PORT` | web, CLI | `6274` | Web UI port. Must be a **fixed** integer in 1–65535: `0` (an OS-assigned port) is rejected at startup, because the origin allow-list and the MCP Apps sandbox CSP are derived from it. | -| `MCP_SANDBOX_PORT` | web, CLI | `6275` | Port of the MCP Apps sandbox server, 0–65535. `0` asks the OS for a free port. An invalid value is ignored with a warning. | -| `SERVER_PORT` | web | — | v1's proxy port, now only a fallback for the sandbox port: used whenever `MCP_SANDBOX_PORT` does not yield a valid port — unset, empty, **or invalid**. | +| `MCP_SANDBOX_PORT` | web, CLI | `6275` | Port of the MCP Apps sandbox server, 0–65535. `0` asks the OS for a free port. An invalid value is ignored with a warning. | +| `SERVER_PORT` | web | — | v1's proxy port, now only a fallback for the sandbox port: used whenever `MCP_SANDBOX_PORT` does not yield a valid port — unset, empty, **or invalid**. | | `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`; 0–65535, where `0` asks the OS for a free port. An invalid value is ignored with a warning. Pin it if your app's backend allowlists that origin. | The sandbox port resolves as: a valid `MCP_SANDBOX_PORT`, else a valid `SERVER_PORT`, else `6275`. The CLI reads `CLIENT_PORT`, `MCP_SANDBOX_PORT`, `MCP_APP_ORIGIN_PORT` and `HOST` only to build the deep link and port list it hands to a web session; it binds none of them. It normalizes `HOST` (an all-interfaces host becomes `localhost`, any other host is canonicalized), but it validates none of the three **port** variables: any non-empty port value is copied into the URLs and port-forwarding command as-is, with no range check, no warning, and no `SERVER_PORT` fallback, so a malformed value produces a broken hand-off rather than an error. @@ -36,38 +36,48 @@ The sandbox port resolves as: a valid `MCP_SANDBOX_PORT`, else a valid `SERVER_P The sandbox and app-origin servers advertise a URL built from their own bind (`http://localhost:6275/sandbox` under a wildcard bind). Behind a reverse proxy or an ingress, where the browser reaches them at a public hostname, set the address the browser should use instead. Neither changes what is bound: the process still listens on the port above, and routing the public address to it is the proxy's job. See [Behind a reverse proxy](../clients/web/README.md#host-binding--the-origin-allow-list). -| Variable | Read by | Default | Effect | -| ----------------------------- | ------- | --------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `MCP_SANDBOX_FULL_ADDRESS` | web | `http://:/sandbox` | Public URL of the MCP Apps sandbox proxy, returned as `sandboxUrl` by `GET /api/config` and printed in the banner (e.g. `https://inspector-sandbox.example.com/sandbox`). A bare origin gets `/sandbox` appended. An empty value counts as unset. | -| `MCP_APP_ORIGIN_FULL_ADDRESS` | web | `http://:` | Public origin that `_meta.ui.domain` app documents are published under (e.g. `https://inspector-apps.example.com`). **Origin only** — a path is refused, since documents are served at `/app-document/`. An empty value counts as unset. | +| Variable | Read by | Default | Effect | +| ----------------------------- | ------- | ------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `MCP_SANDBOX_FULL_ADDRESS` | web | `http://:/sandbox` | Public URL of the MCP Apps sandbox proxy, returned as `sandboxUrl` by `GET /api/config` and printed in the banner (e.g. `https://inspector-sandbox.example.com/sandbox`). A bare origin gets `/sandbox` appended. An empty value counts as unset. | +| `MCP_APP_ORIGIN_FULL_ADDRESS` | web | `http://:` | Public origin that `_meta.ui.domain` app documents are published under (e.g. `https://inspector-apps.example.com`). **Origin only** — a path is refused, since documents are served at `/app-document/`. An empty value counts as unset. | Both are **refused** — ignored with a warning, keeping the bind-derived address — when the value is not an absolute `http(s)` URL, carries credentials, a query string, a fragment or a wildcard, or is a bracketed IPv6 literal. ⚠️ **Each needs its own origin.** The MCP Apps spec requires the sandbox origin to differ from the Inspector's, so a sandbox address sharing an `ALLOWED_ORIGINS` origin is refused (`https://inspector.example.com/sandbox` behind the same hostname as the UI is the common case), as is an app origin equal to the Inspector's or the sandbox's. Neither is used unless its listener is on a fixed port — not when the port is `0`, and not when the pinned port was taken at startup (or collided with another Inspector port) and the server fell back to an OS-assigned one — since the proxy has no stable port to route it to. A plain-`http` address while `ALLOWED_ORIGINS` lists an `https` origin is used, but warned about: the browser blocks it as mixed content. ## Behavior -| Variable | Read by | Default | Effect | -| ------------------------ | ------------- | -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `MCP_AUTO_OPEN_ENABLED` | web, CLI | see effect | Whether a browser is opened for you. `false` never opens one; `true` always does. Unset (or any other value), the web client opens the UI at launch, and the CLI opens the OAuth authorization page only when stderr is a TTY. In the CLI, `true` also lets interactive OAuth **start** when neither stdin nor stderr is a TTY; otherwise that case fails with an auth-required error pointing at `--stored-auth-only`. **The TUI does not read it** and always opens the OAuth page. | -| `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 non-empty value disables ANSI styling in the CLI's human-readable output. An empty `NO_COLOR=` counts as unset. | +| Variable | Read by | Default | Effect | +| ------------------------ | -------------------- | -------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `MCP_AUTO_OPEN_ENABLED` | web, CLI, mcpdo | see effect | Whether a browser is opened for you. `false` never opens one; `true` always does. Unset (or any other value), the web client opens the UI at launch, and the CLI opens the OAuth authorization page only when stderr is a TTY. In the CLI, `true` also lets interactive OAuth **start** when neither stdin nor stderr is a TTY; otherwise that case fails with an auth-required error pointing at `--stored-auth-only`. mcpdo differs there: with no TTY on stdin or stderr and no `--stored-auth-only`, `connect` exits 0 with a pending connection and an `authUrl` to relay, and `true` keeps the blocking interactive flow instead. **The TUI does not read it** and always opens the OAuth page. | +| `MCP_CATALOG_PATH` | web, CLI, TUI, mcpdo | `~/.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, mcpdo | `http://127.0.0.1:6276/oauth/callback` | Loopback redirect URL for the CLI, TUI and mcpdo OAuth flow. In the CLI and TUI, `--callback-url` takes precedence; mcpdo has no such flag and reads only this variable. | +| `NO_COLOR` | CLI, mcpdo | unset | Any non-empty value disables ANSI styling in the CLI's human-readable output. An empty `NO_COLOR=` counts as unset. | ## 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` | CLI, TUI | `~/.mcp-inspector/storage/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 CLI or TUI run if this variable is also exported. **The web backend does not read it** — it always uses `/oauth.json`. Each state file keeps its own secret-store entries — they are scoped by a namespace stamped into the file — so per-profile state paths stay isolated even on a shared keychain (see [Where secrets are stored](./secret-storage.md)). | -| `MCP_CLIENT_CONFIG_PATH` | CLI, TUI | `~/.mcp-inspector/storage/client.json` | Install-level client config (CIMD, enterprise IdP). `--client-config` takes precedence. | +| Variable | Read by | Default | Effect | +| -------------------------------- | -------------------- | -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `MCP_STORAGE_DIR` | web, CLI, TUI, mcpdo | `~/.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` | CLI, TUI, mcpdo | `~/.mcp-inspector/storage/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 CLI or TUI run if this variable is also exported. **The web backend does not read it** — it always uses `/oauth.json`. Each state file keeps its own secret-store entries — they are scoped by a namespace stamped into the file — so per-profile state paths stay isolated even on a shared keychain (see [Where secrets are stored](./secret-storage.md)). | +| `MCP_CLIENT_CONFIG_PATH` | CLI, TUI, mcpdo | `~/.mcp-inspector/storage/client.json` | Install-level client config (CIMD, enterprise IdP). In the CLI and TUI, `--client-config` takes precedence; mcpdo has no such flag and reads only this variable. | ### Home directory Every default above that starts with `~` is built from the home directory the process sees, not from the OS account database: -| Variable | Read by | Effect | -| ------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `HOME` | web, CLI, TUI | Base of `~/.mcp-inspector`: the default catalog, storage directory (`oauth.json`, `client.json`), secrets file, and TUI log directory. | -| `USERPROFILE` | web, CLI, TUI | Used in place of `HOME` when `HOME` is unset or empty — the normal case on Windows. ⚠️ If **neither** is set, as under some service managers, those defaults resolve against the **current working directory** instead. Set `HOME` or the specific path variables above. | +| Variable | Read by | Effect | +| ------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `HOME` | web, CLI, TUI, mcpdo | Base of `~/.mcp-inspector`: the default catalog, storage directory (`oauth.json`, `client.json`), secrets file, and TUI log directory. | +| `USERPROFILE` | web, CLI, TUI, mcpdo | Used in place of `HOME` when `HOME` is unset or empty — the normal case on Windows. ⚠️ If **neither** is set, as under some service managers, those defaults resolve against the **current working directory** instead. Set `HOME` or the specific path variables above. The mcpdo daemon directory is the exception: with neither set, it falls back to the OS account's home directory (`os.homedir()`), not the working directory. | + +## mcpdo connection daemon + +mcpdo runs its connections in a local daemon (`mcpdod`) that it starts on first use. These select which daemon a command talks to, and how a command picks its connection. + +| Variable | Read by | Default | Effect | +| ------------------------------ | ------- | ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `MCP_INSPECTOR_DAEMON_DIR` | mcpdo | `MCP_STORAGE_DIR` if set, else `~/.mcp-inspector` | Directory that holds the daemon's socket (`mcpdod.sock`), lock and published token. Takes precedence over `MCP_STORAGE_DIR`. `eval "$(mcpdo private)"` sets it to a fresh `0700` directory under the system temp directory, which gives that shell a daemon of its own. | +| `MCP_INSPECTOR_DAEMON_TOKEN` | mcpdo | unset | Bearer token every request to the daemon must carry. Unset, the daemon generates one at startup and publishes it to `mcpdod.token` in the daemon directory for same-user clients to read, so the shared daemon is still authenticated. `mcpdo private` sets it alongside `MCP_INSPECTOR_DAEMON_DIR`. Neither is a boundary against other processes running as your user (see the [mcpdo README](../clients/mcpdo/README.md)). | +| `MCP_ALLOW_DEFAULT_CONNECTION` | mcpdo | unset | When stdin is not a terminal, commands that act on a connection require an explicit `@name` or `--connection`. Set it to `1` to let them fall back to the most recently used connection, as they do interactively. Any other value keeps the requirement. | ## Secret store @@ -76,31 +86,31 @@ Where the Inspector's secrets (OAuth client secrets, the enterprise IdP client s > [!WARNING] > On a host with no OS keychain (Linux without libsecret or a Secret Service, headless or SSH sessions, Termux), the Inspector **automatically** stores secrets in a file that is **plaintext** unless `MCP_INSPECTOR_SECRET_KEY_FILE` or `MCP_INSPECTOR_SECRET_KEY` is set. See [the warning in Where secrets are stored](./secret-storage.md#how-the-store-is-chosen). -| 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. An empty or whitespace-only value counts as unset and silently runs automatic selection; any other value is ignored with a warning and also falls back to automatic selection. | -| `MCP_INSPECTOR_SECRET_FILE` | web, CLI, TUI | `~/.mcp-inspector/secrets.json` | Path of the file store. Lookup order: this variable, then `secrets.json` in `MCP_STORAGE_DIR` when that is set, then `~/.mcp-inspector/secrets.json`. ⚠️ The default sits **beside** the storage directory, not inside it. | -| `MCP_INSPECTOR_SECRET_KEY` | web, CLI, TUI | unset (file is plaintext, `0600`) | Passphrase that encrypts the file store; an empty or whitespace-only value counts as unset. Use a generated, high-entropy value. ⚠️ Changing or losing it makes the existing file unreadable; see [Where secrets are stored](./secret-storage.md#encryption) before rotating it. | -| `MCP_INSPECTOR_SECRET_KEY_FILE` | web, CLI, TUI | unset | Path of a file holding the passphrase; trailing line breaks are removed. Use this for Docker or Compose secrets, so the key stays out of the environment. Setting it together with a non-blank `MCP_INSPECTOR_SECRET_KEY` is an error, and so is setting it to an empty value. ⚠️ If the file is missing, unreadable or empty, or is the secrets file itself, the file store refuses to read or write rather than fall back to plaintext. | -| `MCP_INSPECTOR_PERSIST_TOKENS` | web, CLI, TUI | `all` | Which **acquired OAuth tokens** are persisted to the secret store: `all` (access + refresh tokens and IdP session tokens), `access` (access and ID tokens, but no refresh tokens), or `none` (no acquired tokens outlive the process; expect to re-authorize each run). Applies on write only — already-persisted tokens still load, and the next save under a stricter policy removes them from the store. Client secrets are registration credentials, not acquired tokens, and are persisted regardless. An empty value counts as unset; any other value is ignored with a warning and treated as `all`. | +| Variable | Read by | Default | Effect | +| ------------------------------- | -------------------- | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `MCP_INSPECTOR_SECRET_STORE` | web, CLI, TUI, mcpdo | probe the OS keychain | `keyring`, `file`, or `memory` (case-insensitive) picks the store outright and skips the probe. An empty or whitespace-only value counts as unset and silently runs automatic selection; any other value is ignored with a warning and also falls back to automatic selection. | +| `MCP_INSPECTOR_SECRET_FILE` | web, CLI, TUI, mcpdo | `~/.mcp-inspector/secrets.json` | Path of the file store. Lookup order: this variable, then `secrets.json` in `MCP_STORAGE_DIR` when that is set, then `~/.mcp-inspector/secrets.json`. ⚠️ The default sits **beside** the storage directory, not inside it. | +| `MCP_INSPECTOR_SECRET_KEY` | web, CLI, TUI, mcpdo | unset (file is plaintext, `0600`) | Passphrase that encrypts the file store; an empty or whitespace-only value counts as unset. Use a generated, high-entropy value. ⚠️ Changing or losing it makes the existing file unreadable; see [Where secrets are stored](./secret-storage.md#encryption) before rotating it. | +| `MCP_INSPECTOR_SECRET_KEY_FILE` | web, CLI, TUI, mcpdo | unset | Path of a file holding the passphrase; trailing line breaks are removed. Use this for Docker or Compose secrets, so the key stays out of the environment. Setting it together with a non-blank `MCP_INSPECTOR_SECRET_KEY` is an error, and so is setting it to an empty value. ⚠️ If the file is missing, unreadable or empty, or is the secrets file itself, the file store refuses to read or write rather than fall back to plaintext. | +| `MCP_INSPECTOR_PERSIST_TOKENS` | web, CLI, TUI, mcpdo | `all` | Which **acquired OAuth tokens** are persisted to the secret store: `all` (access + refresh tokens and IdP session tokens), `access` (access and ID tokens, but no refresh tokens), or `none` (no acquired tokens outlive the process; expect to re-authorize each run). Applies on write only — already-persisted tokens still load, and the next save under a stricter policy removes them from the store. Client secrets are registration credentials, not acquired tokens, and are persisted regardless. An empty value counts as unset; any other value is ignored with a warning and treated as `all`. | 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 | -| ----------------------- | ------------- | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Variable | Read by | Default | Effect | +| ----------------------- | ------------- | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `MCP_DEBUG` / `DEBUG` | launcher, TUI | off | Prints the full stack trace when the process exits on an error — the launcher for any `--web`/`--tui` failure, and the standalone TUI entry point for a startup failure. Any value other than empty, `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. An empty value counts as unset. | -| `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`: one of `trace`, `debug`, `info`, `warn`, `error`, `fatal`, `silent`. An empty value is not replaced by `info`. | +| `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. An empty value counts as unset. | +| `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`: one of `trace`, `debug`, `info`, `warn`, `error`, `fatal`, `silent`. An empty value is not replaced by `info`. | ## Outbound proxy -| Variable | Read by | Default | Effect | -| ---------------------------- | ------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 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. | +| `NO_PROXY` | web, CLI, TUI | unset | Hosts exempted from the proxy. | ## Node.js variables diff --git a/scripts/dependabot-alerts.mjs b/scripts/dependabot-alerts.mjs index 26f7f5390..3e8d621a0 100644 --- a/scripts/dependabot-alerts.mjs +++ b/scripts/dependabot-alerts.mjs @@ -56,6 +56,7 @@ import { spawnSync } from "node:child_process"; import { readFileSync } from "node:fs"; import semver from "semver"; +import { escapeTableCell as cell } from "./lib/markdown-cell.mjs"; /** Board #28 (v2). The project and field node ids are stable; option ids are not. */ export const PROJECT_ID = "PVT_kwDOCt2Azc4BJVxt"; @@ -431,9 +432,6 @@ export function buildIssueTitle(group) { return `chore(deps): bump \`${group.package}\` to \`${group.fixedIn}\` in \`${group.manifestPath}\` (${n} ${n === 1 ? "advisory" : "advisories"})`; } -/** Escape a value going into a Markdown table cell. */ -const cell = (value) => String(value).replace(/\|/g, "\\|"); - const PLACEMENT_DOC = "https://github.com/modelcontextprotocol/inspector/blob/v2/main/AGENTS.md#dependency-placement"; @@ -675,8 +673,7 @@ export function buildNewAdvisoryComment(group, added) { const rows = group.advisories .filter((a) => added.includes(a.ghsa)) .map( - (a) => - `| [${a.ghsa}](${a.url}) | ${a.severity} | ${a.summary.replace(/\|/g, "\\|")} |`, + (a) => `| [${a.ghsa}](${a.url}) | ${a.severity} | ${cell(a.summary)} |`, ) .join("\n"); return [ diff --git a/scripts/lib/markdown-cell.mjs b/scripts/lib/markdown-cell.mjs new file mode 100644 index 000000000..c97d2d0a2 --- /dev/null +++ b/scripts/lib/markdown-cell.mjs @@ -0,0 +1,19 @@ +/** + * Escape a value for a Markdown table cell (#2546). + * + * The issue-filing sweeps (`dependabot-alerts.mjs`, `sdk-watch.mjs`) build + * table rows from text they do not control: advisory summaries and upstream + * release data. A `|` in that text ends the cell early, so it is escaped as + * `\|`. That alone is incomplete: a value that itself ends in a backslash, + * such as `abc\`, would become `abc\\|`, where the backslash pair cancels and + * the pipe is live again, breaking the row. So backslashes are escaped + * **first**, then pipes (CodeQL js/incomplete-sanitization, alerts #74–#76). + * + * One helper for both scripts, so the order cannot drift between copies. + * + * @param {unknown} value + * @returns {string} + */ +export function escapeTableCell(value) { + return String(value).replace(/\\/g, "\\\\").replace(/\|/g, "\\|"); +} diff --git a/scripts/lib/markdown-cell.test.mjs b/scripts/lib/markdown-cell.test.mjs new file mode 100644 index 000000000..c9b760dc9 --- /dev/null +++ b/scripts/lib/markdown-cell.test.mjs @@ -0,0 +1,31 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { escapeTableCell } from "./markdown-cell.mjs"; + +test("leaves plain text alone", () => { + assert.equal(escapeTableCell("plain text"), "plain text"); +}); + +test("escapes a pipe so it cannot end the cell", () => { + assert.equal(escapeTableCell("a|b"), "a\\|b"); +}); + +test("escapes a trailing backslash before a following pipe can pair with it", () => { + // `abc\` + an escaped pipe must not read as `abc\\|` (a live pipe). + assert.equal(escapeTableCell("abc\\"), "abc\\\\"); + assert.equal(escapeTableCell("abc\\|d"), "abc\\\\\\|d"); +}); + +test("coerces non-strings", () => { + assert.equal(escapeTableCell(42), "42"); + assert.equal(escapeTableCell(null), "null"); +}); + +test("every pipe in the output is escaped by an odd run of backslashes", () => { + for (const input of ["a|b", "a\\|b", "a\\\\|b", "\\", "|", "x\\"]) { + const out = escapeTableCell(input); + for (const m of out.matchAll(/(\\*)\|/g)) { + assert.equal(m[1].length % 2, 1, `${JSON.stringify(input)} → ${out}`); + } + } +}); diff --git a/scripts/sdk-watch.mjs b/scripts/sdk-watch.mjs index 0fffdc75d..ae7d68515 100644 --- a/scripts/sdk-watch.mjs +++ b/scripts/sdk-watch.mjs @@ -67,6 +67,7 @@ import { spawnSync } from "node:child_process"; import { appendFileSync, readFileSync } from "node:fs"; import semver from "semver"; +import { escapeTableCell as cell } from "./lib/markdown-cell.mjs"; /** The branch this repo ships from, and whose manifests are read. */ export const TARGET_BRANCH = "v2/main"; @@ -445,8 +446,6 @@ export function buildIssueTitle(state) { return `chore(deps): upgrade the ${state.group.label} to ${state.target}`; } -const cell = (value) => String(value).replace(/\|/g, "\\|"); - /** * Does adopting `target` require editing the root manifest, or only the lockfile? *