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
mainMCP channel binding (via--mcp.server-urlor--mcp.command).
Use the CLI itself as the first discovery surface:
tunnel-client help quickstarttunnel-client help samplestunnel-client help doctortunnel-client help oauthtunnel-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 <tunnel_id> --health.listen-addr 127.0.0.1:0 --health.url-file "$health_url_file"tunnel-client init --sample <sample> --profile <name> --tunnel-id <tunnel_id> --mcp-server-url <url>tunnel-client doctor --profile <name>tunnel-client doctor --profile <name> --explaintunnel-client profiles samples listtunnel-client profiles samples show sample_mcp_with_dcrtunnel-client dev mcp-stubtunnel-client codex assistant "Summarize what tunnel-client is for."tunnel-client codex statustunnel-client runtimes listtunnel-client runtimes status <alias>tunnel-client admin-profiles listtunnel-client codex plugin installtunnel-client codex plugin uninstall
Keep the key split straight during first use:
CONTROL_PLANE_TUNNEL_ID: create or inspect it in Tunnels management, or viatunnel-client admin tunnels create|list|get ...withOPENAI_ADMIN_KEY.CONTROL_PLANE_API_KEY: create it in Runtime API keys; this is the key used bytunnel-client doctorandtunnel-client run.OPENAI_ADMIN_KEY: only fortunnel-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 before creating roles, groups, or keys.
run --help also advertises the config precedence, the sample-discovery path,
and the embedded UI convention http://<health.listen-addr>/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.
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:
tunnel-client run --profile sample_mcp_with_dcrOr point run at one checked-in or ad hoc profile file directly:
tunnel-client run --profile-file ./fixtures/sample_mcp_with_dcr.yamlProfile lookup uses this precedence:
--profile-dir /path/to/profilesTUNNEL_CLIENT_PROFILE_DIR=/path/to/profiles$XDG_CONFIG_HOME/tunnel-client~/.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:
~/.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:
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. 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: 60sSecret-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.
init: create a validated first-use profile and print the exact next commands.doctor: validate the selected config or profile before daemon startup.help <topic>: show embedded operator guidance forquickstart,samples,doctor,oauth, orplugin.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 <path>: print a token-free productioncloudflaredYAML config for operators who runcloudflareddirectly.profiles list: list profile YAML files in the selected profile directory.profiles samples list: enumerate built-in sample profiles.profiles samples show <name>: print the sample plus required inputs and caveats.profiles add <name>: create a profile from--from-fileor a built-in sample such as--sample sample_mcp_with_dcr.profiles edit <name>: open a profile in$VISUALor$EDITOR, validate it, and only save it when the edited YAML parses.codex assistant [prompt...]: run a terminal assistant session through the supervisedcodex app-server; prompt args give one-shot mode and TTY stdin enters REPL mode. The default reasoning effort ismedium, and the REPL supports/modelto 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 intoCODEX_HOME.codex plugin uninstall: remove the embedded Tunnel MCP plugin bundle fromCODEX_HOMEand clean up its enablement section fromconfig.toml.codex plugin export --dir <path>: 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 <id>: 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. Aftercreatesucceeds, wait 25-30 seconds before expecting the tunnel to be active and ready.tunnel-clientwith no subcommand prints help and available commands.
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 andchannel=mainalready 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, withhttp_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:
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- 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-clientautomatically useshttps://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
- Correct:
- Flag:
- 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 nativeruntimes/ tunnel-mcp flows via--control-plane-url-pathorcontrol_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/<tunnel_id>/poll
- Example base URL:
- Flag:
- Tunnel ID
- Flag:
--control-plane.tunnel-id - Env:
CONTROL_PLANE_TUNNEL_ID - Required: yes
- Format:
tunnel_followed by 32 lowercase hexadecimal characters (for exampletunnel_0123456789abcdef0123456789abcdef)
- Flag:
- API key
- Flag:
--control-plane.api-key=env:VARNAMEor--control-plane.api-key=file:/path/to/secret - Env (preferred):
CONTROL_PLANE_API_KEY - Env (fallback):
OPENAI_API_KEY(used only ifCONTROL_PLANE_API_KEYis unset) - Required: yes
- Flag:
- Client certificate for mTLS (optional)
- Flags:
--control-plane.client-cert=/path/to/client.crtand--control-plane.client-key=/path/to/client.key - Env:
CONTROL_PLANE_CLIENT_CERTandCONTROL_PLANE_CLIENT_KEY - YAML:
control_plane.client_certandcontrol_plane.client_key - Values may be plain paths,
env:VARNAMEpath references, orfile:/path/to/pemfile 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.
- Flags:
- HTTP proxy (optional)
- Flag:
--control-plane.http-proxy=<url|env:VAR> - Env:
CONTROL_PLANE_HTTP_PROXY
- Flag:
- 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 withpoll_deadline_guardrail, the client poll HTTP/context deadline must stay at or below600000ms. - 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; configuredpoll_timeoutremains the ceiling. Subsequent direct and Unix-socket polls keep the configured value.
- Flag:
- 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
30sis capped at30son 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.
- Flag:
- 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 Contentempty poll can complete without being classified as a client timeout. Test profiles can override it with a smaller millisecond duration such as500ms.
- Flag:
- 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
channelquery 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
channelquery parameters are serialized. - A Harpoon-only client may set only
harpoonand omit the main MCP binding, but it must configure at least one routable Harpoon target.
- Flag (repeatable):
- Polled-command buffer capacity
- Flag:
--control-plane.max-inflight - Env:
CONTROL_PLANE_MAX_INFLIGHT_REQUESTS - Default:
20(max10000) - 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.
- Flag:
- 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:VARNAMEandfile:/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, andX-Tunnel-MCP-Server-Info) are managed by the client and cannot be overridden by extra headers.
- Flag (repeatable):
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.
- Flag:
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=<url|env:VAR> - Env:
TUNNEL_CLIENT_HTTP_PROXY - Applies to control plane, MCP HTTP, OAuth discovery, and Harpoon unless overridden.
- Flag:
-
Control plane proxy
- Flag:
--control-plane.http-proxy=<url|env:VAR> - Env:
CONTROL_PLANE_HTTP_PROXY
- Flag:
-
MCP proxy default
- Flag:
--mcp.http-proxy=<url|env:VAR> - Env:
MCP_HTTP_PROXY - Per-channel override:
--mcp.server-url="channel=...,url=...,http-proxy=<url|env:VAR>" - Note: stdio MCP bindings ignore proxy settings.
- Flag:
-
Harpoon proxy
- Flag:
--harpoon.http-proxy=<url|env:VAR> - Env:
HARPOON_HTTP_PROXY
- Flag:
-
Proxy health checks
- Flag:
--proxy.check-interval=60s - Env:
PROXY_CHECK_INTERVAL - Default:
60s
- Flag:
Precedence (highest to lowest):
- Per-target/per-channel proxy flag.
- MCP default proxy (
--mcp.http-proxy). - Global proxy (
--http-proxy). - 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.
For a contributor-focused walkthrough of connector request flow, channel
routing, streaming, OAuth discovery, and common setup pitfalls, see
connectors.md.
- Server URL
- Flag (repeatable):
--mcp.server-url - Env:
MCP_SERVER_URL - Required: yes for the
mainchannel (unless--mcp.commandsuppliesmain) - Legacy form:
--mcp.server-url=https://main.example.com/mcp(defaults tomain) - Channel-qualified form:
--mcp.server-url="channel=foo,url=https://foo.example.com/mcp,unix-socket=<path|env:VAR>,http-proxy=<url|env:VAR>,client-cert=<path|env:VAR>,client-key=<path|env:VAR>" - Cross-origin redirects are still followed, but connector-forwarded request headers are removed.
- Unix socket dial (optional): set
unix-socket=<path|env:VAR>on a channel-qualified entry, orunix_socket:in YAML, to dial the logical HTTP(S) MCP URL over a local Unix-domain socket instead of TCP. - Note: per-channel
unix-socketcannot be combined with per-channelhttp-proxy; MCP/global proxy defaults are ignored for that binding.
- Flag (repeatable):
- Command (stdio transport)
- Flag (repeatable):
--mcp.command - Env:
MCP_COMMAND - Required: yes for the
mainchannel (unless--mcp.server-urlsuppliesmain) - Legacy form:
--mcp.command="npx -y @org/main-mcp"(defaults tomain) - 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-clientinstances sharing a tunnel ID with stdio bindings are not supported. See stdio deployment limits. - Note: when using
MCP_COMMANDwith multiple entries, separate entries with newlines so semicolons remain part of the command.
- Flag (repeatable):
- 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/initializedafter a successful forwarded stdioinitializeresponse 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 preserves legacy verbatim forwarding.
- Flag:
- Multiple entries
- Flags are repeatable; each entry can target a different channel.
- Environment variables accept newline-delimited entries.
- Configuring both
--mcp.server-urland--mcp.commandis allowed as long as they target different channels. - If no
mainbinding is configured, startup fails withmain channel is required.
- Connection max TTL
- Flag:
--mcp.connection-max-ttl - Env:
MCP_CONNECTION_MAX_TTL - Default:
10m
- Flag:
- 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
mainMCP binding, tunnel-client delays its first control-plane poll and OAuth discovery until the MCP listener accepts a connection. It retries only pre-connectconnection refusedand missing Unix-socket errors during this window; any HTTP response, including401 Unauthorized, proves the listener is reachable. - If the wait expires, polling resumes for compatibility while
/readyzremains non-ready for the startup failure. This setting does not retry or replay tunneled MCP commands.
- Flag:
- 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 to10active MCP requests,20commands in the local queue, and one dispatcher-held command waiting for a worker slot.
- Flag:
- HTTP proxy default (optional)
- Flag:
--mcp.http-proxy=<url|env:VAR> - Env:
MCP_HTTP_PROXY
- Flag:
- mTLS client certificate default (optional)
- Flag:
--mcp.client-cert=<path|env:VAR> - Env:
MCP_CLIENT_CERT
- Flag:
- mTLS client private key default (optional)
- Flag:
--mcp.client-key=<path|env:VAR> - Env:
MCP_CLIENT_KEY - Behavior: both values are required together.
- Scope: applies to all
http-streamableMCP channels unless a channel-qualified--mcp.server-urlentry provides its ownclient-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.
- Flag:
- 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:VARNAMEandfile:/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.
- Flag (repeatable):
- 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:VARNAMEandfile:/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_HEADERSfor discovery/probe requests. If connector-forwarded headers are present on a request, they are still applied last.
- Flag (repeatable):
OAuth-protected MCP notes:
- Forwards inbound
Authorizationheaders and protected-resource discovery GETs through the tunnel client. Discovery payloadresourcevalues andWWW-Authenticate resource_metadatavalues are rewritten to tunnel-service URLs for the sametunnel_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
issuerdiffers fromauthorization_servers[0](external IdP issuers are supported). Mismatch details are preserved in diagnostics and logs. - Registered
harpoon://registration_endpoint,token_endpoint, andrevocation_endpointvalues are rewritten to Tunnel OAuth-shim routes. Their POST requests and responses traverse Tunnel and Harpoon; publichttp(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.
tunnel-client supports multiple logical channels:
main: required; configured from--mcp.server-urlor--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-urland/or--mcp.commandentries.
All response payloads posted to /v1/tunnels/{tunnel_id}/response include the
resolved channel value.
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 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 samelabel=...,url=...,desc=...entries)
- Flag (repeatable):
- Outbound request headers
- The
call_targettool drops transport proxy forwarding and client-managed identity headers, plus every caller-supplied field named by aConnectionheader.
- The
- Harpoon target metadata (
list_targets)- Each target includes
category,source, andtagsfields. - Config-provided targets default to
category=source=config. - OAuth auto-registered targets derive
category/sourcefrom discovery tags (currentlyoauth) and derivetagsfrom the OAuth role (for example,auth-server-metadata,registration-endpoint, orprotected-resource-metadata). - The
list_targetstool 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_targetscontinues to omit target URLs. The separate read-onlyget_oauth_target_audiencetool is an explicit, narrow exception for auto-registered OAuthtoken-endpointtargets: it returns only the exact upstream token URL needed as aprivate_key_jwtaudience. Generic configured targets, non-token OAuth targets, credentialed URLs, and fragment-bearing URLs are rejected.
- Each target includes
- 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.
- Flag:
- 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.
- Flag:
- Max redirects
- Flag:
--harpoon.max-redirects - Env:
HARPOON_MAX_REDIRECTS - Default:
5 - Note: this is the upper ceiling for per-call overrides.
- Flag:
- HTTP proxy (optional)
- Flag:
--harpoon.http-proxy=<url|env:VAR> - Env:
HARPOON_HTTP_PROXY
- Flag:
- 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-uiis set). MCP 2026-07-28 self-contained POST requests are sessionless. Legacyinitialize/notifications/initializedPOST flows remain stateful, including standalone SSEGETand session-terminationDELETErequests carrying theirMcp-Session-Id.
- Flag:
- 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.
- Flag:
- 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.
- Flag (repeatable):
- 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).
- Flag (repeatable):
- Include loopback hosts
- Flag:
--harpoon.hosts-include-loopback - Env:
HARPOON_HOSTS_INCLUDE_LOOPBACK - Default:
true
- Flag:
- Include private IPs
- Flag:
--harpoon.hosts-include-private - Env:
HARPOON_HOSTS_INCLUDE_PRIVATE - Default:
true - Behavior: includes RFC1918 IPv4 plus IPv6 ULA (fc00::/7).
- Flag:
- Level
- Flag:
--log.level(debug,info,warn) - Env:
LOG_LEVEL - Default:
info
- Flag:
- Format
- Flag:
--log.format(struct-text,json) - Env:
LOG_FORMAT - Default: unset (uses Go's default logger behavior)
- Flag:
- File (optional)
- Flag:
--log.file - Env:
LOG_FILE - Default: stdout (when unset)
- Flag:
- 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.
- Flag:
See local health and component details 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=trueand/health?details=falseoverride the default for one request./health/mcpalways returns component details.- All new
/healthroutes 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-uiis set.
- Flag:
-
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=:8080only when a container orchestrator, sidecar, or trusted operator network must reach the health endpoints remotely. - Set the port to
0only when you explicitly want the OS to assign an ephemeral port at startup.
- Flag:
-
URL file (optional)
- Flag:
--health.url-file - Env:
HEALTH_URL_FILE - Recommended with
HEALTH_LISTEN_ADDR=:0when another process needs the resolved/healthz,/readyz, or/uibase 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/tmpfilename.
- Flag:
When running tunnel-client run, the tunnel client serves a lightweight web UI from the same admin/health server.
- UI entrypoints:
GET /orGET /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
- Flag:
- Runtime log level toggle
- The Logs tab can change the live runtime log level between
debug,info, andwarnthroughGET/PUT /api/log-level. - Use this for short troubleshooting windows without restarting the client.
- The Logs tab can change the live runtime log level between
- PID file (optional)
- Flag:
--pid.file - Env:
PID_FILE
- Flag:
Used with tunnel-client admin tunnels ...:
- Admin key
- Flag:
--admin-key(accepts raw value,env:VAR, orfile:/path) - Env:
OPENAI_ADMIN_KEY - Required.
- Flag:
- Org/workspace scope
- Flags:
--organization-id,--workspace-id(repeatable). At least one is required forcreate, and duplicates are rejected.
- Flags:
- Base URL
- Flag:
--control-plane.base-url - Env:
CONTROL_PLANE_BASE_URL - Default:
https://api.openai.com
- Flag:
- Output
- Flag:
--json(structured output)
- Flag:
- Delete safety
- Flag:
--confirm(required fortunnels delete)
- Flag:
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-textexport 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./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=jsonIf 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:
./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"./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"./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"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