This guide helps you get from zero to a working tunnel-client process connected to your MCP server.
If you found this by searching "How do I connect local MCP server to
ChatGPT", "How do I connect local MCP server to Codex", "localhost to
ChatGPT", or "Codex local MCP", start with
tunnel-client help quickstart. That is the shortest supported path before you
drop into init, doctor, or Codex plugin flows.
For the customer-shareable network model and request flow, see
architecture.md. For contributor-level connector routing,
streaming, and troubleshooting details, see connectors.md.
- A reachable MCP server endpoint, or a command that starts a stdio MCP server.
- A tunnel control-plane API key.
- A provisioned
tunnel_idfor your environment.
Use these exact setup pages when you need to create or inspect those values:
- Tunnels management and supported tunnel-client download:
https://platform.openai.com/settings/organization/tunnels - Organization roles:
https://platform.openai.com/settings/organization/people/roles - Organization groups:
https://platform.openai.com/settings/organization/people/groups - Runtime API keys:
https://platform.openai.com/settings/organization/api-keys - Admin API keys:
https://platform.openai.com/settings/organization/admin-keys - ChatGPT connector settings:
https://chatgpt.com/#settings/Connectors
Which value comes from where:
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.
Required permissions:
- The runtime key principal needs Tunnels Read + Use for the target tunnel.
- People who create or edit tunnels need Tunnels Read + Manage.
- People who create
OPENAI_ADMIN_KEYneed Platform admin-key permission as well as any tunnel permissions they need for the task.
For the recommended roles/groups and screenshots, see
permissions.md.
If you already have a tunnel-client binary, start there. The CLI now embeds
the shortest first-use path:
tunnel-client help quickstart
tunnel-client help samplesThe recommended binary-first path is:
tunnel-client run --embedded-mcp-stub --control-plane.tunnel-id tunnel_0123456789abcdef0123456789abcdef --health.listen-addr 127.0.0.1:0 --health.url-file /tmp/tunnel-client-health.url
curl -fsS "$(cat /tmp/tunnel-client-health.url)/readyz"
open "$(cat /tmp/tunnel-client-health.url)/ui"If Codex is installed locally and you want the plugin surface instead of the raw
binary flow, install it directly from the binary. The canonical command is
tunnel-client codex plugin ...; tunnel-client plugin codex ... remains a
compatibility alias. Keep lifecycle operations on the native runtimes command
family:
tunnel-client codex plugin install
tunnel-client help plugin
tunnel-client runtimes list
tunnel-client codex plugin uninstallUse tunnel-client run ... when you intentionally want a foreground daemon
attached to the current terminal. For a long-lived local runtime managed by
Codex, prefer tunnel-client runtimes connect ...; do not use nohup or
disown as the tunnel-client supervision path. After runtimes connect, run
tunnel-client runtimes status <alias> before reporting success. Only report
success when status shows the managed runtime running with health reported; use
--json when Codex needs explicit process_running, healthy, and ready
fields.
If you want a named profile instead of the one-command demo path:
tunnel-client init --sample sample_mcp_stdio_local --profile local-stdio --tunnel-id tunnel_0123456789abcdef0123456789abcdef --mcp-command "python /path/to/server.py"
tunnel-client doctor --profile local-stdio --explain
tunnel-client run --profile local-stdioIf you still need the tunnel id or runtime/admin keys, open the matching setup
URL above before running init. If your rollout has self-serve tunnel access,
create the tunnel yourself in Tunnels management or with
tunnel-client admin tunnels create, then export the returned id as
CONTROL_PLANE_TUNNEL_ID and a separate runtime key as
CONTROL_PLANE_API_KEY. Create or verify the connector from the ChatGPT
settings URL above only while tunnel-client run ... is healthy, and keep the
daemon running for connector discovery and every MCP call from ChatGPT.
The Platform Tunnels page download button is sourced from tunnel-service's gated tunnel metadata response. When a new public tunnel-client release becomes the supported download, update tunnel-service's hard-coded public artifact URL alongside the release handoff.
Other fast starts:
- Remote HTTP MCP server with no OAuth/PRMD metadata:
tunnel-client init --sample sample_mcp_remote_no_auth --profile remote-http --tunnel-id tunnel_... --mcp-server-url https://mcp.example.com/mcp - Outbound proxy or private PKI environment:
export HTTPS_PROXY="http://proxy.internal.example.com:8080"export ENTERPRISE_CA_BUNDLE="/etc/ssl/certs/proxy-root.pem"thentunnel-client init --sample sample_mcp_enterprise_proxy --profile corp-proxy --tunnel-id tunnel_... --mcp-server-url https://mcp.internal.example.com/mcp - Embedded demo MCP server for end-to-end validation:
tunnel-client dev mcp-stubthentunnel-client init --sample sample_mcp_with_dcr --profile sample_mcp_with_dcr --tunnel-id tunnel_... --mcp-server-url http://127.0.0.1:NNNN/mcp
The embedded UI is served from the health listener. With the default
127.0.0.1:8080, the UI is at http://127.0.0.1:8080/ui.
If you set HEALTH_LISTEN_ADDR=:0 or --health.listen-addr=:0, the OS picks
an ephemeral port at startup; use HEALTH_URL_FILE or --health.url-file if
another tool needs the resolved UI URL.
If Codex is installed locally and you want the plugin surface instead of the raw binary flow, install it directly from the binary:
tunnel-client codex assistant "Summarize what tunnel-client is for."
tunnel-client codex status
tunnel-client codex plugin install
tunnel-client runtimes list
tunnel-client codex plugin uninstallStarter 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 foreground daemon attached to this terminal.Run tunnel-client codex assistant and summarize what this checkout is for in one sentence.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.For a long-lived local runtime, use tunnel-client runtimes connect to attach the provided tunnel id, then run tunnel-client runtimes status <alias> before reporting whether the runtime is launched, healthy, or ready.
From the tunnel-client module root:
make admin-ui
go build -o bin/tunnel-client ./cmd/clientmake admin-ui refreshes embedded UI assets before the local binary build. You
can skip it for a docs-only checkout when the committed assets under
pkg/adminui/assets are already present and unchanged.
Release source archives include the release semantic version in
pkg/version/VERSION, so a plain go build from a downloaded release .tar.gz
keeps the same client name and version metadata used by released binaries.
Or from a repository checkout with Bazel available:
bazel build //api/tunnel-client/cmd/client:client
./bazel-bin/api/tunnel-client/cmd/client/clientAfter building from source, use ./bin/tunnel-client or the Bazel output path
unless you add that location to your PATH.
At minimum, you must set:
CONTROL_PLANE_API_KEY: control-plane authentication.CONTROL_PLANE_TUNNEL_ID: the tunnel identifier for this deployment.- One
mainMCP binding:MCP_SERVER_URLfor a Streamable HTTP MCP endpoint, or--mcp.commandfor a stdio MCP server.
Auth split to keep straight:
CONTROL_PLANE_API_KEY/OPENAI_API_KEY: runtime key used by the daemon.tunnel-client admin tunnels get <tunnel_id>can use that runtime key for read-only metadata lookup.admin tunnels list/create/update/deleterequireOPENAI_ADMIN_KEYor--admin-key.- The enterprise proxy sample documents both keys in comments so runtime and admin flows stay separate.
Example:
export CONTROL_PLANE_API_KEY="sk-..." # preferred
export CONTROL_PLANE_TUNNEL_ID="tunnel_0123456789abcdef0123456789abcdef"
export MCP_SERVER_URL="https://mcp.internal.example.com/mcp"CONTROL_PLANE_TUNNEL_ID must match the runtime validator: tunnel_ followed by 32 lowercase hexadecimal characters.
For the full surface (flags, defaults, advanced knobs), see configuration.md.
Authorizationheaders are forwarded through the OpenAI tunnel service to your MCP server.- Custom MCP request headers configured on the app are forwarded through the OpenAI tunnel service, except internal auth and IP-forwarding transport headers.
- OAuth discovery GETs are forwarded to the tunnel-client; discovery payloads and
WWW-Authenticate resource_metadataare rewritten to OpenAI tunnel-service URLs for the sametunnel_id. authorization_servers[0]from PRMD is the only source of truth and metadata fetch target for auth-server metadata enrichment and Harpoon OAuth target registration.- Auth-server metadata is accepted even when metadata
issuerdiffers fromauthorization_servers[0](external IdP issuer topologies are supported), and mismatch diagnostics are retained. - The authorization server itself is not tunneled. If it is only reachable on-prem or behind a firewall and not accessible from the internet or the tunnel-client host, the OAuth flow can fail.
./bin/tunnel-client run --log.level=info --log.format=struct-textThis is the foreground/manual operator path. Keep this terminal attached while
you want the daemon running. When Codex needs a long-lived local runtime that
survives the current shell, use tunnel-client runtimes connect ... instead of
improvising nohup or disown.
The process will:
- Start polling the OpenAI tunnel service for work.
- Forward JSON-RPC requests to your MCP server.
- Expose health endpoints on
HEALTH_LISTEN_ADDR(default127.0.0.1:8080). Set it to:8080only when a trusted remote probe needs access, or127.0.0.1:0when you explicitly want an ephemeral loopback port chosen at startup.
In another shell:
curl -fsS "http://127.0.0.1:8080/healthz"
curl -fsS "http://127.0.0.1:8080/readyz"
curl -fsS "http://127.0.0.1:8080/metrics" | head- Permissions, roles, and groups:
permissions.md - Deployments:
deployment/overview.md - Architecture:
architecture.md - Troubleshooting:
troubleshooting.md