Customer-run client for Secure MCP Tunnel: connect private or localhost MCP servers to ChatGPT, Codex, the Responses API, and AgentKit without exposing them to the public internet.
Secure MCP Tunnel client
tunnel-client is the customer-run agent behind Secure MCP Tunnel. It connects a private or localhost MCP (Model Context Protocol) server to ChatGPT, Codex, the Responses API, and AgentKit through an OpenAI-hosted MCP tunnel endpoint, while keeping the MCP server off the public internet.
Use it when:
- You have an MCP server on a laptop, VM, Kubernetes cluster, or private
- Security will not approve a new inbound firewall rule or public endpoint for
- You want an operator-visible daemon with
/healthz,/readyz,/metrics,
/ui before a connector or API call depends on it.
If you searched for "secure MCP tunnel", "MCP tunnel ChatGPT", "connect local MCP server to ChatGPT", "connect local MCP server to Codex", "localhost to ChatGPT", or "Codex local MCP", start with tunnel-client help quickstart, then read the onboarding guide below.
Start Here
- **Need the shortest working path from localhost or a private MCP server to
docs/onboarding.md.
- Need the customer-shareable network and trust-boundary story? Read
docs/architecture.md.
- Need roles, groups, tunnel IDs, or API keys? Read
docs/permissions.md.
- Need Docker, Kubernetes, or VM deployment guidance? Read
docs/deployment/overview.md.
- Need to debug readiness, connector discovery, or OAuth? Read
docs/troubleshooting.md.
- Building a compatible client in another language? Read
docs/protocol.md and use
docs/openapi.json.
- Embedding an MCP server directly in a Go process? Use the Go SDK with
examples/go-sdk-inmemory.
Embed as a Go SDK
The module can run in the same process as a Go MCP server. The MCP server does not need to bind a port or use stdio: give the server side of an in-memory MCP transport pair to your server and the client side to tunnelclient.New.
~~~bash go get github.com/openai/tunnel-client ~~~
~~~go import ( "context"
"github.com/modelcontextprotocol/go-sdk/mcp" tunnelclient "github.com/openai/tunnel-client" )
ctx := context.Background() server := mcp.NewServer(&mcp.Implementation{Name: "my-server", Version: "1.0.0"}, nil) serverTransport, tunnelTransport := mcp.NewInMemoryTransports() go server.Run(ctx, serverTransport)
client, err := tunnelclient.New(tunnelclient.Config{ TunnelID: "tunnel_0123456789abcdef0123456789abcdef", APIKey: apiKey, }, tunnelTransport) if err != nil { return err } return client.Run(ctx) ~~~
The runnable Go SDK example registers an echo tool and connects it to the OpenAI Tunnel control plane.
Documentation Map
- Public Secure MCP Tunnel guide:
developers.openai.com/api/docs/guides/secure-mcp-tunnels
- Shareable end-user guide:
docs/end-user-guide.md - Start here:
docs/onboarding.md - Permissions, roles, and groups:
docs/permissions.md - Architecture diagrams:
docs/architecture.md - Connector behavior:
docs/connectors.md - Wire protocol for client implementers:
docs/protocol.md - OpenAPI contract:
docs/openapi.json - Enterprise customer handoff:
docs/enterprise-customer-onboarding.md
- Configuration reference:
docs/configuration.md - Deployment guides:
docs/deployment/overview.md - Bundled Cloudflare companion:
docs/deployment/cloudflared.md
- Troubleshooting:
docs/troubleshooting.md - Development & testing:
docs/development.md - In-memory Go SDK example:
examples/go-sdk-inmemory
- Roadmap / design notes:
docs/roadmap.md
Homebrew publication tooling
Homebrew installation is not generally available until a compatible stable Formula is present in the tap. For approved private-CI release automation, scripts/publishlatesthomebrew_formula.sh reads the latest published stable release, renders the Formula from its public checksum manifest, and opens a Formula-only pull request in openai/homebrew-tools; it never merges that PR.
The wrapper keeps release reads separate from tap writes:
TUNNELCLIENTRELEASEREADTOKENis an optional read-only GitHub token for
OPENAIGITHUBINSTALLATIONTOKENFILEis the preferred private-CI input:
openai/homebrew-tools with only contents:write and
pullrequests:write. OPENAIGITHUBINSTALLATIONTOKEN_JSON remains
available for local testing.
Run it only from the approved private publisher path:
TUNNELCLIENTRELEASEREADTOKEN=<read-only-github-token> \
OPENAIGITHUBINSTALLATIONTOKENFILE=/etc/secrets/github-installation-token/token-json \
bash ./scripts/publishlatesthomebrew_formula.sh
See docs/development.md for the local test command and the availability boundary.
To generate the shareable guide output locally:
make end-user-guide-screenshots
make end-user-guide-html
make end-user-guide-slides
For Codex / Claude / Copilot
If you want the shortest supported path from a local or localhost MCP server to ChatGPT or Codex, start with tunnel-client help quickstart. For Codex plugin lifecycle work, use the native tunnel-client runtimes ... and tunnel-client admin-profiles ... command trees surfaced by tunnel-client help plugin.
Supervision choice:
- Use
tunnel-client run ...when you intentionally want a foreground daemon
- 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, checktunnel-client runtimes status <alias>
--json when Codex needs the
explicit process_running, healthy, and ready fields.
Use these exact setup pages during first use:
- 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
CONTROLPLANETUNNEL_ID: create or inspect it in Tunnels management, or via
tunnel-client admin tunnels create|list|get ... with OPENAIADMINKEY.
CONTROLPLANEAPI_KEY: create it in Runtime API keys; this is the key used
tunnel-client doctor and tunnel-client run.
OPENAIADMINKEY: only fortunnel-client admin tunnels
Required tunnel permissions:
- Runtime users and the principal that creates
CONTROLPLANEAPI_KEYneed
- Tunnel managers need Tunnels Read + Manage, plus Use if they also
- Admin-key creators need the Platform admin-key permission in addition to any
See docs/permissions.md for the group/role workflow and screenshots.
Binary-first flow:
tunnel-client help quickstart
tunnel-client profiles samples list
tunnel-client profiles samples show samplemcpenterprise_proxy
tunnel-client init --sample samplemcpstdiolocal --profile local-stdio --tunnel-id tunnel0123456789abcdef0123456789abcdef --mcp-command "python /path/to/server.py"
tunnel-client doctor --profile local-stdio --explain
tunnel-client run --profile local-stdio
tunnel-client run --profile-file ./profiles/local-stdio.yaml
If you need the tunnel id or runtime/admin keys first, open the matching 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 CONTROLPLANETUNNEL_ID and a separate runtime key as CONTROLPLANEAPI_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.
Validate a source checkout with native Go tooling:
go build ./...
go test ./...
Build the CLI binary:
make admin-ui
go build -o bin/tunnel-client ./cmd/client
./bin/tunnel-client help quickstart
Public releases use plain semantic-version tags such as v0.0.10. Source archives from release tags carry the release version in pkg/version/VERSION. A plain go build from a downloaded release .tar.gz therefore reports the tag semantic version through tunnel-client --version, User-Agent, and the explicit control-plane version headers.
Supported release archives also bundle pinned cloudflared 2026.7.2 beside the CLI for Linux amd64/arm64, macOS amd64/arm64, and Windows amd64/arm64. Official release images are published at ghcr.io/openai/tunnel-client for Linux amd64 and arm64; they bundle the matching companion. Pin an exact vX.Y.Z tag or digest for production. Stable releases also update the X.Y and latest aliases; prereleases do not. For a logical tunnel created with managed Cloudflare provisioning, let the authenticated client fetch the runtime token and start the companion without distributing a static token:
tunnel-client run \
--cloudflared.managed \
--control-plane.tunnel-id tunnel_0123456789abcdef0123456789abcdef \
--mcp.server-url https://mcp.example.com/mcp
For a pre-provisioned Cloudflare tunnel, a static token remains available as an explicit override and is never put in argv:
export CLOUDFLARED_TOKEN='...'
tunnel-client run \
--cloudflared.token env:CLOUDFLARED_TOKEN \
--control-plane.tunnel-id tunnel_0123456789abcdef0123456789abcdef \
--mcp.server-url https://mcp.example.com/mcp
See docs/deployment/cloudflared.md for platform coverage, readiness/failure behavior, Go module provenance, and security-update ownership.
If an operator intentionally runs cloudflared without tunnel-client, print a token-free production config and keep the token in a separate secret file:
tunnel-client cloudflared config \
--token-file /run/secrets/cloudflared/token \
> /etc/cloudflared/config.yml
TUNNELMANAGEMENTDIAGNOSTICS=false \
cloudflared tunnel --config /etc/cloudflared/config.yml run
Fastest Codex terminal path:
tunnel-client codex assistant "Summarize what tunnel-client is doing in this checkout."
tunnel-client codex status
tunnel-client codex plugin install
tunnel-client runtimes list
tunnel-client help plugin
tunnel-client codex plugin uninstall
Choose the raw binary when you want the smallest possible setup surface. Choose tunnel-client codex assistant when you want the fastest Codex-native terminal path. Choose the plugin when you want a Codex-local entrypoint over the native runtimes / admin-profiles command trees.
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.I only have the source checkout. Figure out how to build tunnel-client, 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.
What it does
- The client long-polls the OpenAI tunnel control plane over HTTPS:
GET /v1/tunnels/{tunnel_id}/poll
- POST /v1/tunnels/{tunnel_id}/response
- Older tunnel-client releases may still use the singular
/v1/tunnel/...
- Control-plane requests include
User-Agent: oai-tunnel-client/<version>for
X-Tunnel-Client-Name and
X-Tunnel-Client-Version headers for service-side logs and metrics. Each
process also generates a new opaque clientinstanceid, sends it as
X-Tunnel-Client-Instance-Id, and includes it in structured logs and the
local admin UI for request correlation.
- Control-plane HTTPS requests can present a separate client certificate/key
--control-plane.client-cert and --control-plane.client-key
(or CONTROLPLANECLIENTCERT / CONTROLPLANECLIENTKEY). When those are
configured with the default https://api.openai.com host, the client
automatically uses https://mtls.api.openai.com for control-plane calls.
- On startup, it fetches tunnel metadata for operator visibility:
GET /v1/tunnels/{tunnel_id}
- It forwards received JSON-RPC requests to your configured MCP server over
DELETE /v1/mcp/{tunnel_id}.
- It routes commands by channel:
maintargets the configured MCP binding,
harpoon is routable only when Harpoon has registered targets.
- On startup, it fetches OAuth Protected Resource Metadata from the MCP server
- For sidecar deployments whose local MCP listener may bind after
tunnel-client starts, the optional MCPSTARTUPWAIT_TIMEOUT gate delays
the first control-plane poll and OAuth discovery until the main MCP listener
is reachable.
- For OAuth auth-server handling,
authorization_servers[0]from PRMD is the
- Metadata is accepted even when
issuerdiffers from
authorization_servers[0] (external IdP issuer URLs are supported), with
mismatch diagnostics preserved in logs/state.
- It exposes an admin/health server (
/healthz,/readyz,/metrics) and
/ui) for operational status.
- The admin UI Overview reports the process-scoped
clientinstanceid,
- The admin UI Logs tab can switch the live runtime log level between
debug,
info, and warn without restarting the process.
- The admin UI log export returns a redacted support bundle with recent logs
/metrics and a redacted
runtime YAML snapshot containing argv, relevant environment, actual YAML
config, and effective config.
- It embeds the Harpoon MCP server to provide a labeled, allowlisted
Admin UI build notes
The admin UI assets under pkg/adminui/assets are generated from the TypeScript/Svelte source in adminui/. To rebuild them locally:
./scripts/buildadminui.sh ./adminui ./pkg/adminui/assets
or
make admin-ui
CLI
tunnel-clientshows help and available subcommands.tunnel-client help <topic>shows embedded task-oriented help for
quickstart, samples, doctor, oauth, and plugin.
tunnel-client codex assistant [prompt...]starts a terminal assistant
codex app-server, using prompt args for
one-shot mode and TTY stdin for REPL mode. It defaults to medium
reasoning effort, and the REPL supports /model to inspect or change model
and reasoning without restarting.
tunnel-client codex status|install|upgrade|uninstallinspects local Codex
tunnel-client codex plugin install|uninstall|exportinstalls, removes, or
tunnel-client dev mcp-stubruns an embedded demo MCP + OAuth metadata server
tunnel-client dev proxyruns a local control plane plus tunnel-client for
mcp_url; pass
--listen-unix-socket PATH for external MCP ingress over a Unix socket. Its
--backend auto|go|rust flag defaults to auto, and
--engine-queue-backend inmem|redis defaults to inmem. Ordinary public
builds use the Go in-memory backend; rust and Redis require a binary with
the optional linked Rust adapter. Redis also needs --engine-redis-url or
TUNNELENGINEREDIS_URL.
tunnel-client initwrites a validated first-use profile.tunnel-client doctorvalidates config and explains what is missing before
tunnel-client profiles samples list|showexposes built-in sample profiles.samplemcpenterprise_proxyis the built-in starter for outbound proxies
tunnel-client admin-profiles list|set|deletemanages saved admin-key
tunnel-client runtimes create|connect|list|status|stop|rmmanages native
tunnel-client runstarts the foreground/manual client poller attached to
tunnel-client cloudflared versionprints the bundled companion pin,
tunnel-client cloudflared config --token-file <path>prints a token-free
cloudflared config for operators who run cloudflared directly.
tunnel-client admin tunnels get <id>is the read-only metadata lookup used
admin tunnels CRUD still requires an
admin key. When you need admin CRUD scope, inspect the returned
organizationids / workspaceids from tunnel-client admin --json tunnels get <id>
and reuse those live values instead of guessing ids.