tunnel-client connects OpenAI-hosted products to a private MCP server without
requiring the customer to expose that MCP server to the public internet. The
customer runs a small agent inside their network. The agent keeps an outbound
HTTPS connection to the OpenAI tunnel service, receives MCP work, forwards it to
the configured MCP server, and returns the response through the same tunnel.
- No inbound firewall rule is required for the MCP server. The tunnel client initiates all tunnel traffic as outbound HTTPS to OpenAI.
- The MCP server remains private. OpenAI products call an OpenAI-hosted MCP tunnel URL; the customer's internal MCP URL is only used by the tunnel client.
- Traffic is request/response with backpressure. The client long-polls for queued work, forwards only the work it can process, and posts the result back to OpenAI.
- Operations stay local. Health, readiness, metrics, logs, and the optional admin UI are exposed by the tunnel client for the customer's operators.
flowchart LR
subgraph openai["OpenAI"]
product["ChatGPT, Codex, Responses API, or AgentKit"]
tunnel["OpenAI tunnel service"]
end
subgraph customer["Customer network"]
client["tunnel-client"]
mcp["Private MCP server"]
ops["Local health, metrics, and admin UI"]
end
product -->|"MCP JSON-RPC request"| tunnel
client ==>|"Outbound HTTPS long-poll<br/>GET /v1/tunnels/{tunnel_id}/poll"| tunnel
client ==>|"Outbound HTTPS response<br/>POST /v1/tunnels/{tunnel_id}/response"| tunnel
client -->|"Streamable HTTP, stdio, or in-memory MCP"| mcp
client -.->|"Loopback or operator network"| ops
classDef openaiNode fill:#eef5ff,stroke:#4a6fa5,color:#172033
classDef customerNode fill:#eefaf4,stroke:#3f7f5f,color:#172033
classDef opsNode fill:#fff7e6,stroke:#9a6b00,color:#172033
class product,tunnel openaiNode
class client,mcp customerNode
class ops opsNode
In the current ChatGPT connector UI, operators attach a tunnel by selecting an
available tunnel or pasting a tunnel_id. Under the hood, the product still
targets the OpenAI tunnel service endpoint
<OPENAI_MCP_TUNNEL_BASE_URL>/v1/mcp/<tunnel_id>. The tunnel client is
configured separately with the same tunnel_id, an API key, and the private
MCP server address that is reachable from inside the customer network. See
connectors.md for connector-specific setup, channel routing,
and troubleshooting notes.
sequenceDiagram
autonumber
participant Product as OpenAI product
participant Tunnel as OpenAI tunnel service
participant Client as tunnel-client
participant MCP as Customer MCP server
Client->>Tunnel: Long-poll for work
Product->>Tunnel: POST /v1/mcp/{tunnel_id}
Tunnel-->>Client: Return queued JSON-RPC command
Client->>MCP: Forward MCP JSON-RPC request
MCP-->>Client: Return MCP response or notifications
Client->>Tunnel: POST /v1/tunnels/{tunnel_id}/response
Tunnel-->>Product: Return final response or SSE stream
For streaming requests, JSON-RPC notifications are posted back with
resp_type=jsonrpc_notify and forwarded to the connector stream when the
connector requested text/event-stream. A final JSON-RPC response closes the
stream.
flowchart TB
subgraph internet["OpenAI-managed public edge"]
edge["OpenAI MCP tunnel URL<br/>/v1/mcp/{tunnel_id}"]
control["Tunnel control plane<br/>/v1/tunnels/{tunnel_id}/*"]
end
subgraph private["Customer-controlled environment"]
client["tunnel-client process"]
mcp["Private MCP server"]
proxy["Optional outbound proxy"]
ca["Optional custom CA bundle"]
mtls["Optional MCP mTLS client cert"]
end
edge --> control
client -->|"Outbound HTTPS, API-key authenticated"| control
client -->|"Private network request"| mcp
client -.->|"If configured"| proxy
ca -.-> client
mtls -.-> client
classDef public fill:#eef5ff,stroke:#4a6fa5,color:#172033
classDef privateNode fill:#eefaf4,stroke:#3f7f5f,color:#172033
classDef option fill:#f8f8f8,stroke:#7a7a7a,color:#172033
class edge,control public
class client,mcp privateNode
class proxy,ca,mtls option
Security-relevant defaults:
- The tunnel path requires the tunnel client's control-plane API key.
- The MCP server does not need a public listener.
- The admin UI and log endpoints are loopback-only by default unless
--allow-remote-uiis enabled. - A custom CA bundle can extend trust for outbound TLS connections.
- MCP mTLS can be configured when the private MCP server requires client certificate authentication.
- Raw HTTP logging is disabled by default and should only be enabled for tightly controlled debugging sessions.
flowchart LR
subgraph sidecar["Kubernetes sidecar: one Pod"]
podclient["tunnel-client"]
podmcp["MCP container"]
podclient -->|"localhost"| podmcp
end
subgraph dedicated["Kubernetes dedicated Deployment"]
deployclient["tunnel-client Deployment"]
svcmcp["MCP Service"]
deployclient -->|"Cluster DNS"| svcmcp
end
subgraph vm["VM or host service"]
systemd["systemd service"]
hostmcp["MCP endpoint"]
systemd -->|"Host or private network"| hostmcp
end
classDef pattern fill:#f8f8f8,stroke:#7a7a7a,color:#172033
class podclient,podmcp,deployclient,svcmcp,systemd,hostmcp pattern
Choose the pattern that matches the MCP server's deployment model:
- Use a sidecar when the MCP server is in the same Pod and can be reached on
localhost. - Use a dedicated Deployment when the MCP server is already exposed through a Kubernetes Service and the tunnel client should be upgraded independently.
- Use VM / systemd when the MCP server runs on a host or is reachable through private networking outside Kubernetes.
- CLI / process entry:
cmd/clientloads config, wires dependencies, and starts the app. - Configuration:
pkg/confighandles flags, environment variables, validation, and defaults. - Control plane:
pkg/controlplanebuilds the HTTP client and runs the poll/response loop. - Dispatcher:
pkg/dispatcheruses a bounded in-memory queue sized bycontrol-plane.max-inflight. Workers are sized bymcp.max-concurrent-requests. - MCP client:
pkg/mcpclienthandles Streamable HTTP MCP, stdio MCP, header forwarding, and startup probing. - Channel state and admin UI:
pkg/adminuiexposes channel status, OAuth state, Harpoon state, log export, and the embedded web UI. - Operations surface:
pkg/health,pkg/metrics,pkg/log, andpkg/processprovide health checks, readiness, Prometheus metrics, structured logging, and optional PID-file lifecycle.
- Outbound-only tunnel: tunnel traffic is initiated by the client. The only inbound listener in the client process is the optional local admin/health server.
- Queueing and backpressure: the poller requests only the number of commands that can fit in the bounded queue, which avoids unbounded buffering in the client.
- Channel routing:
mainroutes to the configured MCP transport.harpoonroutes to the embedded Harpoon server and is enabled only when at least one Harpoon target is registered. Additional channels can be configured with channel-qualified MCP bindings. - Streaming semantics: requests can stream intermediate JSON-RPC
notifications over SSE when the connector asks for
text/event-stream; a final JSON-RPC response closes the stream. - Connector GET not supported:
/v1/mcpaccepts POST requests for MCP JSON-RPC traffic. GET requests do not provide an SSE stream.
For OAuth-protected MCP servers, the tunnel client and tunnel service preserve the standard MCP OAuth flow while keeping the MCP server private:
- Inbound
Authorizationheaders are forwarded to the MCP server through the tunnel client. - OAuth discovery GETs are queued as tunnel commands and executed from the customer's network by the tunnel client.
WWW-Authenticateresource_metadatavalues and discovery payloadresourceURLs are rewritten to OpenAI tunnel-service endpoints for the sametunnel_id.authorization_servers[0]from Protected Resource Metadata is treated as the source of truth for auth-server metadata enrichment and Harpoon OAuth target registration.- Metadata is accepted when the returned
issuerdiffers fromauthorization_servers[0], which supports external enterprise identity provider issuer URLs while preserving mismatch diagnostics in logs and state. - The authorization server itself is not tunneled. If the authorization server is unreachable from the public internet and from the tunnel-client host, the OAuth flow can fail.