# Configuration Reference `tunnel-client` can be configured via CLI flags, environment variables, a YAML config file, or a named YAML profile. - **Precedence**: flags > environment variables > YAML config > defaults. - **Requirement**: you must provide a control-plane API key, a tunnel ID, and a `main` MCP channel binding (via `--mcp.server-url`, `--mcp.command`, or one of the [embedded demo modes](#embedded-demo-mcp-modes)). ## Agent-first commands Use the CLI itself as the first discovery surface: - `tunnel-client help quickstart` - `tunnel-client help samples` - `tunnel-client help doctor` - `tunnel-client help oauth` - `tunnel-client help plugin` Use the first-run helpers before editing YAML by hand: - `health_url_file="$(mktemp "${TMPDIR:-/tmp}/tunnel-client-health.XXXXXX.url")" && tunnel-client run --embedded-mcp-stub --control-plane.tunnel-id --health.listen-addr 127.0.0.1:0 --health.url-file "$health_url_file"` - `tunnel-client init --sample --profile --tunnel-id --mcp-server-url ` - `tunnel-client doctor --profile ` - `tunnel-client doctor --profile --explain` - `tunnel-client profiles samples list` - `tunnel-client profiles samples show sample_mcp_with_dcr` - `tunnel-client dev mcp-stub` - `tunnel-client codex assistant "Summarize what tunnel-client is for."` - `tunnel-client codex status` - `tunnel-client runtimes list` - `tunnel-client runtimes status ` - `tunnel-client admin-profiles list` - `tunnel-client codex plugin install` - `tunnel-client codex plugin uninstall` Keep the key split straight during first use: - `CONTROL_PLANE_TUNNEL_ID`: create or inspect it in Tunnels management, or via `tunnel-client admin tunnels create|list|get ...` with `OPENAI_ADMIN_KEY`. - `CONTROL_PLANE_API_KEY`: create it in Runtime API keys; this is the key used by `tunnel-client doctor` and `tunnel-client run`. - `OPENAI_ADMIN_KEY`: only for `tunnel-client admin tunnels list|create|update|delete`. Do not use the admin key for the long-lived daemon. Tunnel permission split: - Runtime daemon and ChatGPT connector users need Tunnels **Read** + **Use**. - Tunnel CRUD operators need Tunnels **Read** + **Manage**. - Admin-key creators need Platform admin-key permission separately. See [`permissions.md`](permissions.md) before creating roles, groups, or keys. `run --help` also advertises the config precedence, the sample-discovery path, and the embedded UI convention `http:///ui`. Starter prompts for Codex: - `Figure out what tunnel-client is for from the binary help, then get me to /ui with the shortest local path.` - `Use tunnel-client to create or reuse a profile, run doctor --explain, and then start the daemon.` - `Install the Codex plugin from the tunnel-client binary, connect the provided tunnel id, and tell me whether the runtime is launched, healthy, or ready.` - `Use tunnel-client runtimes to attach a local MCP server to an existing tunnel id and report the ui_url.` ## Embedded demo MCP modes Both optional `run` flags start the same demo MCP tools (`server_info`, `echo`, and `uppercase`) and OAuth metadata endpoints inside the tunnel-client process, then bind its `main` MCP channel to that process's stub: | Flag | MCP compatibility behavior | | --- | --- | | `--embedded-mcp-stub` | Preserves stateful handling for legacy initialization and session requests, including `initialize` and `notifications/initialized`. Self-contained modern discovery and tool requests use stateless handling. | | `--embedded-stateless-mcp-stub` | Uses stateless handling for every MCP request, including `initialize` and `notifications/initialized`, without issuing `Mcp-Session-Id`. | For example, after replacing the runtime key and tunnel ID: ```bash export CONTROL_PLANE_API_KEY="sk-..." export CONTROL_PLANE_TUNNEL_ID="tunnel_0123456789abcdef0123456789abcdef" tunnel-client run \ --embedded-stateless-mcp-stub \ --embedded-mcp-listen-addr 127.0.0.1:0 \ --embedded-mcp-server-name stateless-demo \ --embedded-mcp-server-version 1.0.0 \ --health.listen-addr 127.0.0.1:0 ``` The modes are mutually exclusive. Neither can be combined with an explicit `--mcp.server-url` or `--mcp.command` flag, including the aliases `--mcp-server-url` and `--mcp-command`. Mode selection and embedded options are CLI flags for `run`; they have no environment-variable or YAML equivalents. Without either mode, the configured MCP target and existing defaults apply. `--embedded-stateless-mcp-stub` replaces a `main` target resolved from the environment, YAML config, or profile, and adds one if none is configured. It retains other MCP channels. Its main binding does not inherit configured MCP or global HTTP proxies. Replacing the target does not bypass validation of configured targets or duplicate-channel checks. This behavior applies only to the stateless flag; `--embedded-mcp-stub` keeps its existing configuration behavior. The stateless mode advertises `main` as stateless in v2 server-info metadata. Other configured channels retain their own affinity requirements. The compatible embedded mode retains its existing declaration. Shared options: | Option | Default | Meaning | | --- | --- | --- | | `--embedded-mcp-listen-addr` | `127.0.0.1:0` | Stub listen address; port `0` chooses an available port for each process. | | `--embedded-mcp-unix-socket` | Empty | Bind the stub to a Unix socket instead of TCP; cannot be combined with an explicit `--embedded-mcp-listen-addr`. | | `--embedded-mcp-server-name` | `mcp-stub` | Server name advertised by the demo. | | `--embedded-mcp-server-version` | `0.1.0` | Server version advertised by the demo. | For local socket-only MCP, add `--embedded-mcp-unix-socket /tmp/my-demo-mcp.sock` to either mode. The parent directory must exist and the socket path must be unused. The main channel and its OAuth discovery use the socket; the displayed `http://localhost/mcp` URL is its logical HTTP address. The listener removes its socket on shutdown. Combine this with `--health.unix-socket` to serve health and admin endpoints over a separate socket. Existing TCP defaults remain available. The stateless demo tools need no MCP session affinity: completed requests in one interaction can be followed by requests to another process's embedded stub. Clients do not need a session DELETE on exit; the stateless endpoint returns HTTP 405 for session DELETE and standalone GET streams. This does not provide replay of interrupted requests, shared OAuth state, or shared application state. Authentication and any state used by other applications must be handled separately. ## YAML config file Pass a config file with `--config /path/to/tunnel-client.yaml` or set `TUNNEL_CLIENT_CONFIG=/path/to/tunnel-client.yaml`. Named profiles use the same YAML schema. Run a profile with: ```bash tunnel-client run --profile sample_mcp_with_dcr ``` Or point `run` at one checked-in or ad hoc profile file directly: ```bash tunnel-client run --profile-file ./fixtures/sample_mcp_with_dcr.yaml ``` Profile lookup uses this precedence: 1. `--profile-dir /path/to/profiles` 2. `TUNNEL_CLIENT_PROFILE_DIR=/path/to/profiles` 3. `$XDG_CONFIG_HOME/tunnel-client` 4. `~/.config/tunnel-client` The selected profile directory may itself be a symlink; named profile symlinks must use relative targets within that directory, while `--profile-file` and `--from-file` accept explicit file paths. For example, with the default XDG fallback, the command above loads: ```text ~/.config/tunnel-client/sample_mcp_with_dcr.yaml ``` `TUNNEL_CLIENT_PROFILE=sample_mcp_with_dcr` is equivalent to passing `--profile sample_mcp_with_dcr`. `TUNNEL_CLIENT_PROFILE_FILE` is equivalent to passing `--profile-file /path/to/profile.yaml`. `--config`, `--profile`, and `--profile-file` are mutually exclusive, and `TUNNEL_CLIENT_CONFIG`, `TUNNEL_CLIENT_PROFILE`, and `TUNNEL_CLIENT_PROFILE_FILE` are mutually exclusive. Example: ```yaml config_version: 1 control_plane: base_url: https://api.openai.com # citadel-ignore: public endpoint example for external tunnel-client config # Optional path appended before tunnel-client adds its /v1/... routes. url_path: /chatgpttunnelgateway/dev/us tunnel_id: tunnel_0123456789abcdef0123456789abcdef api_key: env:CONTROL_PLANE_API_KEY # Optional. When configured with the default api.openai.com base URL, # tunnel-client automatically uses https://mtls.api.openai.com. client_cert: file:/run/secrets/control-plane-client.crt client_key: file:/run/secrets/control-plane-client.key max_inflight_requests: 20 poll_timeout: 30000ms poll_deadline_guardrail: 5000ms extra_headers: X-Debug-Mode: "1" X-Internal-Auth: env:CONTROL_PLANE_HEADER_VALUE log: level: info format: json file: /var/log/tunnel-client/tunnel-client.ndjson health: listen_addr: 127.0.0.1:8080 url_file: /run/tunnel-client/health-url admin_ui: open_browser: false log_buffer_events: 2000 process: pid_file: /run/tunnel-client/tunnel-client.pid cloudflared: # Optional. Fetch the managed runtime token from tunnel-service on startup. managed: true # Optional static override. Use an env: or file: reference; literal tokens are rejected. # When present, this takes precedence over managed fetch. token: env:CLOUDFLARED_TOKEN # Optional source-build/test override. Release archives discover the sibling binary. path: /opt/tunnel-client/cloudflared ready_timeout: 30s mcp: server_urls: - channel: main url: https://mcp.example.com/mcp # Optional. Dial the logical HTTP URL over a local Unix socket instead of TCP. unix_socket: env:MCP_UNIX_SOCKET_PATH commands: - channel: tools command: python -m tools_mcp extra_headers: X-Internal-Auth: env:MCP_RUNTIME_HEADER_VALUE discovery_extra_headers: X-Discovery-Auth: file:/run/secrets/mcp-discovery-header # Optional. Explicitly trust separate OAuth metadata/authorization origins. oauth_trusted_origins: - https://auth.example.com # Optional. Wait for a sidecar/local HTTP listener before the first poll. startup_wait_timeout: 60s connection_max_ttl: 10m max_concurrent_requests: 10 harpoon: targets: - label: auth url: https://auth.example.com description: Auth server additional_transports: - http-streamable proxy: check_interval: 60s ``` Secret-bearing fields should use `env:VARNAME` or `file:/path/to/secret` when possible. `control_plane.api_key` accepts either form and resolves it at startup; direct literal values are accepted for compatibility but are not recommended for checked-in configs. For static header values, use `env:VARNAME` or `file:/path/to/secret` on the value side to keep secrets out of argv, profiles, and checked-in YAML. This is supported for control-plane, MCP runtime, and MCP discovery/probe extra headers. The `env:` and `file:` prefixes are reserved for these references; all other values are treated literally. `cloudflared.managed` (or `CLOUDFLARED_MANAGED`) asks `tunnel-client run` to fetch the managed Cloudflare metadata and runtime token from the authenticated control plane before starting the adjacent bundled `cloudflared`. The fetch response is never sent through raw HTTP body logging. `cloudflared.token` is the static override: it accepts only `env:VARNAME` or `file:/path/to/secret`, and `CLOUDFLARED_TUNNEL_TOKEN` is the direct environment equivalent. A static token takes precedence over managed fetch. In either mode, tunnel-client passes the token only through the child environment, waits for its loopback readiness endpoint, and propagates unexpected process exits. `cloudflared.path` / `CLOUDFLARED_PATH` is only an advanced source-build or test override; supported release archives do not require it. The admin UI log export includes `tunnel-client.runtime.yaml`, a redacted snapshot of argv, relevant environment variables, the startup YAML config file under `actual_config.contents` when present, and the effective startup config. API keys, bearer tokens, cookies, shard tokens, URL credentials, and URL query secrets are redacted before export. ## Commands - `init`: create a validated first-use profile and print the exact next commands. - `doctor`: validate the selected config or profile before daemon startup. - `help `: show embedded operator guidance for `quickstart`, `samples`, `doctor`, `oauth`, or `plugin`. - `run`: start the tunnel client poll loop. - `cloudflared version`: print the bundled companion version, pinned Go module, release commit, and security-patch owner. - `cloudflared config --token-file `: print a token-free production `cloudflared` YAML config for operators who run `cloudflared` directly. - `profiles list`: list profile YAML files in the selected profile directory. - `profiles samples list`: enumerate built-in sample profiles. - `profiles samples show `: print the sample plus required inputs and caveats. - `profiles add `: create a profile from `--from-file` or a built-in sample such as `--sample sample_mcp_with_dcr`. - `profiles edit `: open a profile in `$VISUAL` or `$EDITOR`, validate it, and only save it when the edited YAML parses. Only [supported editors and safe options](profile-editor.md) are accepted; shell wrappers and evaluation commands are rejected. - `codex assistant [prompt...]`: run a terminal assistant session through the supervised `codex app-server`; prompt args give one-shot mode and TTY stdin enters REPL mode. The default reasoning effort is `medium`, and the REPL supports `/model` to inspect or change model/reasoning without restarting. - `codex status`: report Codex CLI/app-server discovery, login state, and plugin wiring. - `codex install|upgrade|uninstall`: print the official Codex CLI package manager commands for this host. - `codex plugin install`: install the embedded Tunnel MCP plugin bundle into `CODEX_HOME`. - `codex plugin uninstall`: remove the embedded Tunnel MCP plugin bundle from `CODEX_HOME` and clean up its enablement section from `config.toml`. - `codex plugin export --dir `: export the embedded plugin bundle for inspection or manual installation. - `admin-profiles list|set|delete`: manage saved admin-key profiles used by native runtime workflows. - `runtimes create|connect|list|status|stop|rm`: manage native alias state and local tunnel-client runtime supervision. - `admin tunnels`: manage tunnel metadata via the admin API (`/v1/tunnels*`). - `admin tunnels get `: read-only tunnel metadata lookup; accepts the runtime key or an admin key. - `admin tunnels list|create|update|delete`: admin CRUD; requires an admin key and explicit org/workspace/tenant scope flags. After `create` succeeds, wait 25-30 seconds before expecting the tunnel to be active and ready. - `tunnel-client` with no subcommand prints help and available commands. ## Built-in profile samples Built-in samples are stored as separate embedded files and validated in tests. The starter sample set is: - `sample_mcp_with_dcr`: general-purpose HTTP or stdio MCP target with the full OAuth/DCR-friendly contract and `channel=main` already wired. - `sample_mcp_stdio_local`: shortest path for a local stdio MCP command. - `sample_mcp_remote_no_auth`: remote HTTP MCP server that does not advertise OAuth/PRMD metadata. - `sample_mcp_enterprise_proxy`: HTTP or stdio MCP target for outbound proxies or private PKI, with `http_proxy: env:HTTPS_PROXY`, `ca_bundle: env:ENTERPRISE_CA_BUNDLE`, and sample comments that separate the runtime key from the admin key. Use the sample surfaces instead of guessing sample names: ```bash tunnel-client profiles samples list tunnel-client profiles samples show sample_mcp_with_dcr tunnel-client profiles add my-profile --sample sample_mcp_with_dcr --tunnel-id tunnel_0123456789abcdef0123456789abcdef --mcp-server-url http://127.0.0.1:3001/mcp tunnel-client profiles add corp-proxy --sample sample_mcp_enterprise_proxy --tunnel-id tunnel_0123456789abcdef0123456789abcdef --mcp-server-url https://mcp.internal.example.com/mcp ``` ## Control plane - **Base URL** - Flag: `--control-plane.base-url` - Env: `CONTROL_PLANE_BASE_URL` - Default: `https://api.openai.com` - With control-plane mTLS configured and this value left at the default API host, `tunnel-client` automatically uses `https://mtls.api.openai.com`. Set a non-default base URL explicitly for staging, development, or private control-plane hosts. - **Important**: this value is treated as the **host root**, not a pre-prefixed path. - Correct: `https://api.openai.com` - Incorrect: `https://api.openai.com/v1/tunnel` - **URL path** - Flag: `--control-plane.url-path` - Env: `CONTROL_PLANE_URL_PATH` - YAML: `control_plane.url_path` - Optional. Set this when an enterprise gateway needs a workspace or environment path appended to the base URL before tunnel-client adds `/v1/...` routes. - The same path is honored by `tunnel-client admin tunnels ...` via `--control-plane.url-path`, and by native `runtimes` / tunnel-mcp flows via `--control-plane-url-path` or `control_plane_url_path`. - Example base URL: `https://gateway.example.com` - Example URL path: `/workspace/dev/us` - Effective poll URL: `https://gateway.example.com/workspace/dev/us/v1/tunnels//poll` - **Tunnel ID** - Flag: `--control-plane.tunnel-id` - Env: `CONTROL_PLANE_TUNNEL_ID` - Required: yes - Format: `tunnel_` followed by 32 lowercase hexadecimal characters (for example `tunnel_0123456789abcdef0123456789abcdef`) - **API key** - Flag: `--control-plane.api-key=env:VARNAME` or `--control-plane.api-key=file:/path/to/secret` - Env (preferred): `CONTROL_PLANE_API_KEY` - Env (fallback): `OPENAI_API_KEY` (used only if `CONTROL_PLANE_API_KEY` is unset) - Required: yes - **Client certificate for mTLS (optional)** - Flags: `--control-plane.client-cert=/path/to/client.crt` and `--control-plane.client-key=/path/to/client.key` - Env: `CONTROL_PLANE_CLIENT_CERT` and `CONTROL_PLANE_CLIENT_KEY` - YAML: `control_plane.client_cert` and `control_plane.client_key` - Values may be plain paths, `env:VARNAME` path references, or `file:/path/to/pem` file references. - Configure both fields together. Cert-only, key-only, unreadable files, invalid PEM, and mismatched key/cert pairs fail startup and `doctor`. - The runtime API key is still required; mTLS only adds TLS client-certificate presentation to the control-plane HTTPS connection. - **HTTP proxy (optional)** - Flag: `--control-plane.http-proxy=` - Env: `CONTROL_PLANE_HTTP_PROXY` - **Poll timeout** - Flag: `--control-plane.poll-timeout` - Env: `CONTROL_PLANE_POLL_TIMEOUT` - Default: `30000ms` - Behavior: tunnel-client sends this as the usual `/poll?timeout_ms=...` empty-poll wait budget. Together with `poll_deadline_guardrail`, the client poll HTTP/context deadline must stay at or below `600000ms`. - The first poll attempt with a positive command limit also applies the initial-poll timeout below. Later attempts use the usual wait, including after an initial failure. - On HTTP-proxied routes, if a poll loses its connection before response headers with an EOF-style error while neither deadline has fired, the process automatically learns a shorter wait for future proxied polls. The learned value only decreases, never below `5000ms`; configured `poll_timeout` remains the ceiling. Subsequent direct and Unix-socket polls keep the configured value. - **Initial poll timeout** - Flag: `--control-plane.initial-poll-timeout` - Env: `CONTROL_PLANE_INITIAL_POLL_TIMEOUT` - Default: `30s`, matching the normal poll default; must be positive. - Behavior: the first poll attempt requests the shorter of this value and the normal poll wait. Set a lower value for a shorter first wait. With the default initial setting, a normal wait above `30s` is capped at `30s` on the first attempt. Its full normal client deadline is retained for services that clamp or ignore the requested wait. Local test services can allow a lower minimum to exercise shorter initial waits; the service's own minimum still determines the effective wait. - **Poll deadline guardrail** - Flag: `--control-plane.poll-deadline-guardrail` - Env: `CONTROL_PLANE_POLL_DEADLINE_GUARDRAIL` - Default: `5000ms` - Max: less than `60000ms` - Behavior: tunnel-client adds this to the configured or proxy-learned wait when setting the HTTP/context deadline, including on the first poll attempt, so a normal `204 No Content` empty poll can complete without being classified as a client timeout. Test profiles can override it with a smaller millisecond duration such as `500ms`. - **Poll channels (optional)** - Flag (repeatable): `--control-plane.poll-channel=main` - Env: `CONTROL_PLANE_POLL_CHANNELS=main,harpoon` - YAML: `control_plane.poll_channels: [main, harpoon]` - Precedence is flags, then environment, then YAML. When omitted, the client preserves legacy unscoped polling and sends no `channel` query parameters. - Once configured, this is an allowlist: omitted channels are disabled. The client rejects blanks, duplicates, non-canonical names, and channels without a local handler. Values are sorted before repeated `channel` query parameters are serialized. - A Harpoon-only client may set only `harpoon` and omit the main MCP binding, but it must configure at least one routable Harpoon target. - **Polled-command buffer capacity** - Flag: `--control-plane.max-inflight` - Env: `CONTROL_PLANE_MAX_INFLIGHT_REQUESTS` - Default: `20` (max `10000`) - This is the number of prefetched commands that can wait in the local queue; it does not include requests already dispatched to the MCP server. - Each control-plane poll requests the smaller of the local buffer's free capacity and `25`, matching the tunnel-service API contract. When the buffer is full, tunnel-client skips polling until a queue slot is free. - **Extra headers (optional)** - Flag (repeatable): `--control-plane.extra-headers "Key: Value"` - Env: `CONTROL_PLANE_EXTRA_HEADERS="Key: Value, Key2: Value2"` - YAML: `control_plane.extra_headers` - Header values accept `env:VARNAME` and `file:/path/to/secret`; all other values are treated literally. - Header names must use valid HTTP field-name syntax and are case-insensitive. Identical case variants collapse to one header; conflicting values and invalid wire values are rejected before startup. - Reserved control-plane headers (`Authorization`, `Accept`, `User-Agent`, `X-Tunnel-Client-Name`, `X-Tunnel-Client-Version`, `X-Tunnel-Client-Wire-Protocol-Version`, `X-Tunnel-Client-Instance-Id`, and `X-Tunnel-MCP-Server-Info`) are managed by the client and cannot be overridden by extra headers. ## TLS trust (custom CA bundle) Use a PEM CA bundle to extend (additive to system trust) the trust store for **all** outbound TLS connections (control plane, MCP HTTP, OAuth discovery, and Harpoon). - **CA bundle** - Flag: `--ca-bundle /path/to/ca-bundle.pem` - Env: `CA_BUNDLE` - Bundle format: PEM file containing one or more CA certificates. ## Outbound HTTP proxy Use explicit proxy flags to force tunnel-client traffic through a corporate proxy. Each flag and matching tunnel-client-specific proxy env var accepts a proxy URL or `env:VAR` reference. If you want a ready-made profile instead of wiring the YAML by hand, start from `sample_mcp_enterprise_proxy` and export `HTTPS_PROXY` plus `ENTERPRISE_CA_BUNDLE` before `tunnel-client doctor` or `run`. - **Global proxy (all outbound HTTP)** - Flag: `--http-proxy=` - Env: `TUNNEL_CLIENT_HTTP_PROXY` - Applies to control plane, MCP HTTP, OAuth discovery, and Harpoon unless overridden. - **Control plane proxy** - Flag: `--control-plane.http-proxy=` - Env: `CONTROL_PLANE_HTTP_PROXY` - **MCP proxy default** - Flag: `--mcp.http-proxy=` - Env: `MCP_HTTP_PROXY` - Per-channel override: `--mcp.server-url="channel=...,url=...,http-proxy="` - Note: stdio MCP bindings ignore proxy settings. - **Harpoon proxy** - Flag: `--harpoon.http-proxy=` - Env: `HARPOON_HTTP_PROXY` - **Proxy health checks** - Flag: `--proxy.check-interval=60s` - Env: `PROXY_CHECK_INTERVAL` - Default: `60s` **Precedence (highest to lowest):** 1. Per-target/per-channel proxy flag. 2. MCP default proxy (`--mcp.http-proxy`). 3. Global proxy (`--http-proxy`). 4. Environment (`HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY`). When an explicit proxy flag is set for a target, environment proxy variables (including `NO_PROXY`) are ignored for that target. ## Connector and MCP routing For a contributor-focused walkthrough of connector request flow, channel routing, streaming, OAuth discovery, and common setup pitfalls, see [`connectors.md`](connectors.md). ## MCP server - **Server URL** - Flag (repeatable): `--mcp.server-url` - Env: `MCP_SERVER_URL` - Required: yes for the `main` channel (unless `--mcp.command` supplies `main`) - Legacy form: `--mcp.server-url=https://main.example.com/mcp` (defaults to `main`) - Channel-qualified form: `--mcp.server-url="channel=foo,url=https://foo.example.com/mcp,unix-socket=,http-proxy=,client-cert=,client-key="` - Cross-origin redirects are still followed, but connector-forwarded request headers are removed. - Unix socket dial (optional): set `unix-socket=` on a channel-qualified entry, or `unix_socket:` in YAML, to dial the logical HTTP(S) MCP URL over a local Unix-domain socket instead of TCP. - Note: per-channel `unix-socket` cannot be combined with per-channel `http-proxy`; MCP/global proxy defaults are ignored for that binding. - **Command (stdio transport)** - Flag (repeatable): `--mcp.command` - Env: `MCP_COMMAND` - Required: yes for the `main` channel (unless `--mcp.server-url` supplies `main`) - Legacy form: `--mcp.command="npx -y @org/main-mcp"` (defaults to `main`) - Channel-qualified form: `--mcp.command="channel=bar,command=npx -y @org/bar-mcp"` - Behavior: spawns the command once and uses the child process stdin/stdout for MCP frames - Deployment limit: multiple active `tunnel-client` instances sharing a tunnel ID with stdio bindings are **not supported**. See [stdio deployment limits](#stdio-deployment-limits). - Note: when using `MCP_COMMAND` with multiple entries, separate entries with newlines so semicolons remain part of the command. - **Automatic stdio initialization guard** - Each request selects its protocol lifecycle automatically; no configuration is required. This applies to stdio channels only. - Reject legacy requests such as `tools/call` before the shared child completes a successful `initialize` exchange and receives `notifications/initialized`. This prevents requests from reaching a stateful stdio server before it is ready. - The caller owns initialization and recovery. The guard does not synthesize `initialize`, replay rejected requests, or initialize a replacement child. The notification shim below can supply `notifications/initialized` after the caller's successful `initialize` response. - The client reports rejected calls with `status_code: 409`, JSON-RPC code `-32002`, and `error.data.error_type: "mcp_initialization_required"`; it is not written to the child. Send the handshake before retrying the call. - Self-contained requests with a protocol date of `2026-07-28` or later and a client-capabilities object in `params._meta` bypass this guard. The server validates the requested version and capabilities. These requests do not mark the legacy handshake complete. - Legacy callers must complete the handshake even if their server previously tolerated calls before initialization. Stateless HTTP targets are unaffected. - **Stdio initialized notification shim (optional)** - Flag: `--mcp.stdio-send-initialized-notification` - Env: `MCP_STDIO_SEND_INITIALIZED_NOTIFICATION` - YAML: `mcp.stdio_send_initialized_notification` - Default: `false` (disabled) - When enabled, tunnel-client writes `notifications/initialized` after a successful forwarded stdio `initialize` response and suppresses a later duplicate from the caller. Enable it only for stdio servers that implement the MCP lifecycle notification and callers that can omit it; leaving it disabled forwards the caller's notifications without generating one. - **Multiple entries** - Flags are repeatable; each entry can target a different channel. - Environment variables accept newline-delimited entries. - Configuring both `--mcp.server-url` and `--mcp.command` is allowed as long as they target **different** channels. - If no `main` binding is configured, startup fails with `main channel is required`. - **Connection max TTL** - Flag: `--mcp.connection-max-ttl` - Env: `MCP_CONNECTION_MAX_TTL` - Default: `10m` - **Startup listener wait (optional)** - Flag: `--mcp.startup-wait-timeout` - Env: `MCP_STARTUP_WAIT_TIMEOUT` - YAML: `mcp.startup_wait_timeout` - Default: `0s` (disabled) - When positive for an HTTP-streamable `main` MCP binding, tunnel-client delays its first control-plane poll and OAuth discovery until the MCP listener accepts a connection. It retries only pre-connect `connection refused` and missing Unix-socket errors during this window; any HTTP response, including `401 Unauthorized`, proves the listener is reachable. - If the wait expires, polling resumes for compatibility while `/readyz` remains non-ready for the startup failure. This setting does not retry or replay tunneled MCP commands. - **Max concurrent requests** - Flag: `--mcp.max-concurrent-requests` - Env: `MCP_MAX_CONCURRENT_REQUESTS` - Default: `10` - This caps requests actively dispatched to the MCP server. When all worker slots are occupied, the dispatcher removes one command from the local queue and waits for a worker slot. It does not drain another command until a slot is free. - This limit is independent of `--control-plane.max-inflight`. With the defaults, tunnel-client can hold up to `10` active MCP requests, `20` commands in the local queue, and one dispatcher-held command waiting for a worker slot. - **HTTP proxy default (optional)** - Flag: `--mcp.http-proxy=` - Env: `MCP_HTTP_PROXY` - **mTLS client certificate default (optional)** - Flag: `--mcp.client-cert=` - Env: `MCP_CLIENT_CERT` - **mTLS client private key default (optional)** - Flag: `--mcp.client-key=` - Env: `MCP_CLIENT_KEY` - Behavior: both values are required together. - Scope: applies to all `http-streamable` MCP channels unless a channel-qualified `--mcp.server-url` entry provides its own `client-cert` + `client-key`. - Note: mTLS applies only to `http-streamable`; stdio has no TLS hop and channel-qualified mTLS on a non-HTTP binding is rejected. - **Static MCP headers (optional)** - Flag (repeatable): `--mcp.extra-headers "Key: Value"` - Env: `MCP_EXTRA_HEADERS="Key: Value, Key2: Value2"` - YAML: `mcp.extra_headers` - Header values accept `env:VARNAME` and `file:/path/to/secret`; all other values are treated literally. - Header names must use valid HTTP field-name syntax and are case-insensitive. Identical case variants collapse to one header; conflicting values and invalid wire values are rejected before startup. - Scope: sent only to the configured MCP server origin for outbound MCP HTTP traffic. These headers are not sent to the OpenAI control plane or unrelated authorization-server hosts. - Conflict behavior: connector-forwarded request headers are applied last and override these static values case-insensitively. - **Static discovery/probe headers (optional)** - Flag (repeatable): `--mcp.discovery-extra-headers "Key: Value"` - Env: `MCP_DISCOVERY_EXTRA_HEADERS="Key: Value, Key2: Value2"` - YAML: `mcp.discovery_extra_headers` - Header values accept `env:VARNAME` and `file:/path/to/secret`; all other values are treated literally. - Header names and values follow the same validation and case-insensitive conflict rules as static MCP headers. - Scope: sent only to MCP discovery/probe requests for the configured MCP server origin, including OAuth Protected Resource Metadata discovery, WWW-Authenticate probing, and the startup MCP initialize probe. - Conflict behavior: these discovery/probe headers override `MCP_EXTRA_HEADERS` for discovery/probe requests. If connector-forwarded headers are present on a request, they are still applied last. **OAuth-protected MCP notes:** - OAuth discovery trusts the configured MCP server origin by default. Any additional origin advertised through `WWW-Authenticate resource_metadata`, or protected-resource metadata `authorization_servers` must be explicitly trusted. Redirects remain restricted to the selected metadata origin even when other origins are trusted. Trust is matched by scheme, host, and port; trusting a host does not trust its subdomains or other ports. - Configure additional origins with repeated `--mcp.oauth-trusted-origin https://auth.example.com` flags, the newline-separated `MCP_OAUTH_TRUSTED_ORIGINS` environment variable, or the YAML `mcp.oauth_trusted_origins` list. Flags replace the environment or YAML list; the environment replaces the YAML list. Entries must be absolute `http://` or `https://` origins without credentials, a path (except an optional `/`), a query, or a fragment. Explicitly trusted private origins are supported. This setting authorizes discovery requests only; it does not add forwarding targets or expand the scope of configured credentials. - When upgrading a configuration that discovers metadata or an authorization server on a separate origin, add each expected origin to this list before starting the client. For example, an MCP server at `https://mcp.example.com/mcp` whose metadata advertises `https://auth.example.com/tenant` needs `https://auth.example.com` in `mcp.oauth_trusted_origins`. An untrusted server cannot expand this list by advertising more origins. - For a mixed-version rollout, first set `MCP_OAUTH_TRUSTED_ORIGINS` in the process environment and leave existing YAML and flags unchanged. Older releases ignore this environment variable; updated binaries enforce it. For the example above, use `MCP_OAUTH_TRUSTED_ORIGINS=https://auth.example.com`. Separate multiple origins with newlines. This single configuration supports both binary versions. - Keep the environment setting throughout the mixed-version rollout and rollback window. Canary the new binary, validate discovery and authenticated MCP requests, then upgrade the remaining deployments independently. Only switch to the new YAML key or flag after no older binary needs to read that configuration: older releases reject those new settings. No coordinated tunnel-service upgrade is required. Updated binaries enforce explicit trust immediately; the compatibility window does not enable permissive discovery. - Before promoting the upgrade, validate OAuth discovery and an authenticated MCP request using the deployment's configured transport. Retain the previous binary and compatible configuration until validation completes. Prefer correcting missing trusted origins. If a rollback is approved, restore the previous binary with the unchanged legacy YAML/flags and pre-staged environment variable, or restore its matching configuration if new YAML/flags were adopted. Running the previous binary temporarily removes the new protection. - Forwards inbound `Authorization` headers and protected-resource discovery GETs through the tunnel client. Discovery payload `resource` values and `WWW-Authenticate resource_metadata` values are rewritten to tunnel-service URLs for the same `tunnel_id`. - Uses `authorization_servers[0]` from PRMD as the source of truth and metadata fetch target for auth-server metadata enrichment and Harpoon OAuth target registration. - Accepts auth-server metadata even when metadata `issuer` differs from `authorization_servers[0]` (external IdP issuers are supported). Mismatch details are preserved in diagnostics and logs. - Registered `harpoon://` `registration_endpoint`, `token_endpoint`, and `revocation_endpoint` values are rewritten to Tunnel OAuth-shim routes. Their POST requests and responses traverse Tunnel and Harpoon; public `http(s)` endpoint URLs remain unchanged and are called by the product OAuth caller rather than through Tunnel. - The OAuth shim does not rewrite `authorization_endpoint`; the supported auto-registered path leaves browser authorization direct to the upstream authorization server. Tunnel does not expose arbitrary authorization-server routes. ## Channels `tunnel-client` supports multiple logical channels: - `main`: required; configured from `--mcp.server-url` or `--mcp.command`. - `harpoon`: built-in and enabled only when Harpoon has at least one registered target (see Harpoon config below). - additional channels: configured via channel-qualified `--mcp.server-url` and/or `--mcp.command` entries. All response payloads posted to `/v1/tunnels/{tunnel_id}/response` include the resolved `channel` value. ### Stdio deployment limits Run only **one active `tunnel-client` instance per tunnel ID** when using `--mcp.command` / `MCP_COMMAND`. Multiple active instances sharing that tunnel ID are **not supported**, whether they run on the same host, on different hosts, in containers, or in Kubernetes Pods. This also includes temporary overlap during a rolling restart or upgrade. Each instance starts a separate stdio MCP child with its own initialization and session state. Tunnel requests are not pinned to the instance that handled initialization: `initialize` can reach one child and a later `tools/call` another. Calls can therefore time out even when every instance reports healthy and ready. Setting `--mcp.max-concurrent-requests=1` limits work within each instance; it does not provide routing between instances. Stop the old instance before starting its replacement. For a Kubernetes Deployment using stdio, use `replicas: 1` and `strategy.type: Recreate` to avoid overlap during updates; `replicas: 1` alone can still permit a rolling-update surge. To run independent instances, give each a distinct tunnel ID. Multiple stdio bindings on different channels within one instance remain supported. Stdio uses one shared child connection per channel, without independent MCP session isolation. The `proc_affinity` capability declares a need for process affinity; it does not implement routing affinity. The `--mcp.stdio-send-initialized-notification` option only sends the notification after a successful forwarded `initialize` response. It does not initialize every replica or make multiple stdio instances safe. ## Harpoon MCP (outbound HTTP allowlist) `harpoon` is an embedded MCP server that exposes an allowlisted, buffered HTTP client with labeled targets. Harpoon's channel (`harpoon`) is enabled only when at least one target is registered. If there are no targets, `harpoon` commands return `unsupported_channel`. At startup, after configured targets and startup OAuth-discovered targets have been processed, tunnel-client emits one INFO log named `harpoon startup catalog digest`. The HMAC digest is a privacy-safe comparison aid: it does not expose target labels, URLs, paths, or credentials. Compare digests only between replicas using the same tunnel ID and the same resolved control-plane runtime API key; a different key or key rotation intentionally produces a different digest. The digest is startup-only and is not updated for later OAuth discovery commands or registry mutations. - **Target mappings** - Flag (repeatable): `--harpoon.target="label=auth,url=https://auth.example.com,desc=Auth server"` - Env: `HARPOON_TARGETS` (semicolon- or newline-delimited list of the same `label=...,url=...,desc=...` entries) - **Outbound request headers** - The `call_target` tool drops transport proxy forwarding and client-managed identity headers, plus every caller-supplied field named by a `Connection` header. - **Harpoon target metadata (`list_targets`)** - Each target includes `category`, `source`, and `tags` fields. - Config-provided targets default to `category=source=config`. - OAuth auto-registered targets derive `category`/`source` from discovery tags (currently `oauth`) and derive `tags` from the OAuth role (for example, `auth-server-metadata`, `registration-endpoint`, or `protected-resource-metadata`). - The `list_targets` tool accepts optional filters: - `categories`: OR match within categories. - `sources`: OR match within sources. - `tags`: ALL requested tags must be present on the target. - Filters combine with AND across fields. - `list_targets` continues to omit target URLs. The separate read-only `get_oauth_target_audience` tool is an explicit, narrow exception for auto-registered OAuth `token-endpoint` targets: it returns only the exact upstream token URL needed as a `private_key_jwt` audience. Generic configured targets, non-token OAuth targets, credentialed URLs, and fragment-bearing URLs are rejected. - **Allow plaintext HTTP** - Flag: `--harpoon.allow-plaintext-http` - Env: `HARPOON_ALLOW_PLAINTEXT_HTTP` - Default: `false` - OAuth-discovered protected-resource targets use this same policy. For a trusted HTTP loopback MCP endpoint, prefer HTTPS or explicitly enable this setting so its PRMD source target can be registered. - **Max response bytes** - Flag: `--harpoon.max-response-bytes` - Env: `HARPOON_MAX_RESPONSE_BYTES` - Default: `102400` - Note: this is the upper ceiling for per-call overrides. - **Max redirects** - Flag: `--harpoon.max-redirects` - Env: `HARPOON_MAX_REDIRECTS` - Default: `5` - Note: this is the upper ceiling for per-call overrides. - **HTTP proxy (optional)** - Flag: `--harpoon.http-proxy=` - Env: `HARPOON_HTTP_PROXY` - **Additional transport (optional)** - Flag: `--harpoon.additional-transport=http-streamable` - Env: `HARPOON_ADDITIONAL_TRANSPORTS` (semicolon- or newline-delimited list) - Behavior: exposes the Harpoon MCP server over the admin/health HTTP server at `POST /harpoon/mcp` (loopback-only unless `--allow-remote-ui` is set). MCP 2026-07-28 self-contained POST requests are sessionless. Legacy `initialize` / `notifications/initialized` POST flows remain stateful, including standalone SSE `GET` and session-termination `DELETE` requests carrying their `Mcp-Session-Id`. - **Capture payloads (debug only)** - Flag: `--harpoon.capture-payloads` - Env: `HARPOON_CAPTURE_PAYLOADS` - Default: `false` - Behavior: stores request/response payloads in the Harpoon admin UI call history. - **Private host auto-registration filters** - Flag (repeatable): `--harpoon.hosts-include-suffix` - Env: `HARPOON_HOSTS_INCLUDE_SUFFIX` (semicolon- or newline-delimited list) - Default: empty - Behavior: treat matching host suffixes as private for auto-registration. - **Private host regex filters** - Flag (repeatable): `--harpoon.hosts-include-regex` - Env: `HARPOON_HOSTS_INCLUDE_REGEX` (semicolon- or newline-delimited list) - Default: empty - Behavior: treat matching hostnames as private for auto-registration (case-insensitive). - **Include loopback hosts** - Flag: `--harpoon.hosts-include-loopback` - Env: `HARPOON_HOSTS_INCLUDE_LOOPBACK` - Default: `true` - **Include private IPs** - Flag: `--harpoon.hosts-include-private` - Env: `HARPOON_HOSTS_INCLUDE_PRIVATE` - Default: `true` - Behavior: includes RFC1918 IPv4 plus IPv6 ULA (fc00::/7). ## Logging - **Level** - Flag: `--log.level` (`debug`, `info`, `warn`) - Env: `LOG_LEVEL` - Default: `info` - **Format** - Flag: `--log.format` (`struct-text`, `json`) - Env: `LOG_FORMAT` - Default: unset (uses Go's default logger behavior) - **File (optional)** - Flag: `--log.file` - Env: `LOG_FILE` - Default: stdout (when unset) - **Raw HTTP logging (dangerous)** - Flag: `--log.http-raw-unsafe` - Env: `LOG_HTTP_RAW_UNSAFE` - Default: `false` - Warning: may log sensitive headers/bodies; enable only for controlled debugging. ## Health/admin server See [local health and component details](health.md) for the route contract, HTTP and Unix examples, and component meanings. Existing `/healthz`, `/readyz`, and `/metrics` behavior is unchanged. - **Show component details by default** - Flag: `--health.show-details` - Env: `HEALTH_SHOW_DETAILS` - YAML: `health.show_details` - Default: `false`, in all three binary flavors. - Precedence: explicit flag, environment, YAML/profile, default. Explicit false overrides true from a lower-precedence source. - `/health?details=true` and `/health?details=false` override the default for one request. `/health/mcp` always returns component details. - All new `/health` routes require loopback TCP or the configured Unix socket. This setting controls output only; it does not initiate probes or expand access, including when `--allow-remote-ui` is set. - **Listen address** - Flag: `--health.listen-addr` - Env: `HEALTH_LISTEN_ADDR` - Default: `127.0.0.1:8080` - The default keeps `/healthz`, `/readyz`, `/metrics`, and the embedded UI on loopback. - Set `HEALTH_LISTEN_ADDR=:8080` only when a container orchestrator, sidecar, or trusted operator network must reach the health endpoints remotely. - Set the port to `0` only when you explicitly want the OS to assign an ephemeral port at startup. - **URL file (optional)** - Flag: `--health.url-file` - Env: `HEALTH_URL_FILE` - Recommended with `HEALTH_LISTEN_ADDR=:0` when another process needs the resolved `/healthz`, `/readyz`, or `/ui` base URL. - Use a private per-run path such as `health_url_file="$(mktemp "${TMPDIR:-/tmp}/tunnel-client-health.XXXXXX.url")"` instead of a fixed shared `/tmp` filename. ## Embedded web UI When running `tunnel-client run`, the tunnel client serves a lightweight web UI from the same admin/health server. - **UI entrypoints**: `GET /` or `GET /ui` - **Static assets**: `GET /assets/*` - **Remote access (optional)** - By default, UI + log endpoints only respond to loopback clients (127.0.0.1/::1). - Flag: `--allow-remote-ui` - Env: `ALLOW_REMOTE_UI` - Default: `false` - **Open UI in browser (optional)** - Flag: `--open-web-ui` - Env: `OPEN_WEB_UI` - Default: `false` - **Runtime log level toggle** - The Logs tab can change the live runtime log level between `debug`, `info`, and `warn` through `GET`/`PUT /api/log-level`. - Use this for short troubleshooting windows without restarting the client. ## Process utilities - **PID file (optional)** - Flag: `--pid.file` - Env: `PID_FILE` ## Admin (tunnel management) flags Used with `tunnel-client admin tunnels ...`: - **Admin key** - Flag: `--admin-key` (accepts raw value, `env:VAR`, or `file:/path`) - Env: `OPENAI_ADMIN_KEY` - Required. - **Org/workspace scope** - Flags: `--organization-id`, `--workspace-id` (repeatable). At least one is required for `create`, and duplicates are rejected. - **Base URL** - Flag: `--control-plane.base-url` - Env: `CONTROL_PLANE_BASE_URL` - Default: `https://api.openai.com` - **Output** - Flag: `--json` (structured output) - **Delete safety** - Flag: `--confirm` (required for `tunnels delete`) ## Example configurations ### Minimal env-var run ```bash export CONTROL_PLANE_API_KEY="sk-..." export CONTROL_PLANE_TUNNEL_ID="tunnel_0123456789abcdef0123456789abcdef" export MCP_SERVER_URL="https://mcp.internal.example.com/mcp" ./bin/tunnel-client run --log.level=info --log.format=struct-text ``` ### Stdio MCP command ```bash export CONTROL_PLANE_API_KEY="sk-..." export CONTROL_PLANE_TUNNEL_ID="tunnel_0123456789abcdef0123456789abcdef" ./bin/tunnel-client run \ --mcp.command "python -m my_mcp_server --stdio" \ --log.level=info \ --log.format=struct-text ``` ### API key via file ```bash ./bin/tunnel-client run \ --control-plane.tunnel-id=tunnel_0123456789abcdef0123456789abcdef \ --control-plane.api-key=file:/run/secrets/control-plane-api-key \ --mcp.server-url=https://mcp.internal.example.com/mcp \ --log.level=info \ --log.format=json ``` ### Outbound proxy CA bundle If your outbound proxy presents certificates issued by an internal PKI, add the proxy root CA bundle (additive to system trust) and keep TLS verification enabled: ```bash ./bin/tunnel-client run \ --ca-bundle /etc/ssl/proxy-root.pem \ --control-plane.tunnel-id "tunnel_0123456789abcdef0123456789abcdef" \ --mcp.server-url "https://mcp.internal.example.com/mcp" ``` ### Outbound proxy configuration ```bash ./bin/tunnel-client run \ --http-proxy "http://proxy.internal:8080" \ --control-plane.http-proxy "env:CONTROL_PROXY_URL" \ --mcp.server-url "channel=main,url=https://mcp.internal.example.com/mcp,http-proxy=http://mcp-proxy.internal:8080" \ --harpoon.http-proxy "http://harpoon-proxy.internal:8080" \ --control-plane.tunnel-id "tunnel_0123456789abcdef0123456789abcdef" \ --control-plane.api-key "env:CONTROL_PLANE_API_KEY" ``` ### MCP mTLS configuration ```bash ./bin/tunnel-client run \ --control-plane.tunnel-id "tunnel_0123456789abcdef0123456789abcdef" \ --control-plane.api-key "env:CONTROL_PLANE_API_KEY" \ --mcp.client-cert "/etc/tunnel-client/mtls/default-client.crt" \ --mcp.client-key "/etc/tunnel-client/mtls/default-client.key" \ --mcp.server-url "channel=main,url=https://mcp.internal.example.com/mcp" \ --mcp.server-url "channel=analytics,url=https://analytics.internal.example.com/mcp,client-cert=/etc/tunnel-client/mtls/analytics-client.crt,client-key=/etc/tunnel-client/mtls/analytics-client.key" ``` ### Multi-channel MCP bindings ```bash export CONTROL_PLANE_API_KEY="sk-..." export CONTROL_PLANE_TUNNEL_ID="tunnel_0123456789abcdef0123456789abcdef" ./bin/tunnel-client run \ --mcp.server-url="channel=main,url=https://mcp.internal.example.com/mcp" \ --mcp.server-url="channel=analytics,url=https://analytics.internal.example.com/mcp" \ --mcp.command="channel=tools,command=npx -y @org/tools-mcp" \ --log.level=info \ --log.format=struct-text ```