Skip to main content
Agents on laptops or in your own cluster (Claude Code, Cursor, VS Code, your own agents) connect to the gateway directly. Hosted agents run on their vendor’s servers: claude.ai connectors, ChatGPT and background agents. They can reach an MCP server only at a public HTTPS URL, and your gateway sits in your network. Midplane doesn’t relay this traffic: query results never reach Midplane Cloud. You make the gateway reachable with what you already run, or with the tunnel your agent vendor offers, and you register each URL the gateway answers on.

One gateway, several URLs

A gateway can answer on several URLs at once: its direct URL, plus any tunnel or ingress URL in front of it. List them all in its config:
  • Tokens. Each URL is a token audience. The gateway accepts a token that names any of its URLs, and refuses a token that names none of them. Policy is the same on every URL: it keys on the agent, not on the URL it came in on.
  • Metadata. The gateway’s protected resource metadata names the URL a client came in on, so each client asks for a token for the URL it uses.
  • Host names. The gateway answers only requests for its own host names, which protects against DNS rebinding. A request for any other name gets a 403. A gateway listening on loopback also answers to localhost, 127.0.0.1 and [::1]. If a proxy in front of it rewrites the Host header to another name, add that name to listen.allowed_hosts.
  • Registration. Only registered URLs are audiences, and only a project manager registers one. The URLs in the config when a gateway enrolls are registered then, under the enrollment token. A linked gateway reports its public_urls on every sync; one you add to the config later waits on the project’s Gateways page until a project manager registers it there, since whoever can edit the config or holds the gateway’s identity shouldn’t be able to add audiences on their own. You can also register a URL that isn’t in the config. A URL you remove from the config is dropped within ten minutes, or at once on the Gateways page. A URL another live gateway holds can’t be registered: the Gateways page and the gateway’s log say so (public URL not registered).
  • Revoking. Revoking a gateway frees its URLs for another gateway. Stop the revoked gateway’s process too: it keeps its last bundle, and so keeps accepting tokens that name those URLs.
  • TLS. The gateway requires TLS whenever it listens off loopback. Run a tunnel next to it over loopback (in one Kubernetes pod, or one network namespace in Compose), or give the gateway a certificate (tls).
A proxy that rewrites Host hides which URL a client used. The gateway’s metadata then names its first URL, so list the tunnel’s URL first in public_urls when it is the only way in that rewrites Host.

Which option fits

Tunnels that decrypt can read your data. Cloudflare Tunnel terminates TLS at Cloudflare’s edge, and ngrok does too unless you turn on its end-to-end TLS. Either provider can then read queries, results and tokens. Use them only if you accept that.
In every option, the hosted agent’s vendor sees query results, because it is the client. Midplane never does.

Claude: Anthropic MCP tunnels

Anthropic’s MCP tunnels are in research preview. For claude.ai they need the Enterprise plan and a request to Anthropic; tunnels for the Claude Console and API are separate. Set one up with Anthropic’s guide first.
  • Parts. You run cloudflared and Anthropic’s mcp-proxy. cloudflared holds an outbound connection to Cloudflare. Traffic inside it is encrypted again from Anthropic to mcp-proxy with a certificate from your own CA, so Cloudflare carries ciphertext.
  • URL. A route named midplane gets https://midplane.<tunnel domain>. Register that URL; the connector URL in claude.ai is https://midplane.<tunnel domain>/mcp.
  • Loopback. By default mcp-proxy connects only to RFC 1918 addresses. To reach the gateway over loopback, set upstream.allowed_ips to 127.0.0.1/32. That replaces the default list, so the proxy reaches nothing off this network namespace, though anything else listening on its loopback (such as cloudflared’s metrics) is in reach too. Routes decide which host names go where.
  • Host header. Anthropic doesn’t document which Host mcp-proxy sends upstream. If it sends 127.0.0.1:7433, a loopback gateway answers to it, and its metadata names its first URL: list the tunnel URL first.
  • Names. Anthropic’s proxy runs as mcp-proxy, reads /etc/mcp-gateway/config.yaml, and its Helm values call it gateway. Name Midplane’s service midplane-gateway, never mcp-proxy or mcp-gateway.

Docker Compose

The gateway shares mcp-proxy’s network namespace, as cloudflared does, so the proxy reaches it on 127.0.0.1. Take the image digests and the certificate setup from Anthropic’s Compose guide.

Kubernetes

Anthropic’s Helm chart (mcp-tunnel) runs cloudflared and mcp-proxy in a pod of its own and takes no extra containers, so the gateway can’t join it on loopback. The hop from the proxy to the gateway then crosses the cluster network, where the gateway requires TLS:
  1. Give the gateway a certificate from your internal CA (tls in its config), listen on 0.0.0.0, and put a Service named midplane-gateway in front of it. Add a NetworkPolicy that lets only the tunnel’s pods reach port 7433. The proxy may send the Service’s name as Host, so add it to listen.allowed_hosts (not to public_urls: it isn’t a URL agents use).
  2. Route the chart to that Service over https, and have the proxy trust your CA with upstream.tls.ca_file (see Anthropic’s Helm guide for mounting it):
Pod addresses are usually RFC 1918, which the proxy allows by default.

ChatGPT and Codex: OpenAI Secure MCP Tunnel

OpenAI’s Secure MCP Tunnel serves ChatGPT developer mode, Codex and the Responses API, not publicly listed apps. You run OpenAI’s open-source tunnel-client, which long-polls OpenAI over outbound HTTPS and forwards each request to the gateway. TLS ends at OpenAI, which is the client anyway.
  • Setup. Create a tunnel as OpenAI’s guide describes, then point tunnel-client at the gateway: MCP_SERVER_URL=http://127.0.0.1:7433/mcp. Your authorization server is Midplane Cloud, on another origin than the gateway, so add its origin to MCP_OAUTH_TRUSTED_ORIGINS. Its docs ask for --harpoon.allow-plaintext-http to discover OAuth metadata over plain loopback HTTP.
  • Host header. tunnel-client sends the upstream’s own address as Host, 127.0.0.1:7433 here. A gateway listening on loopback answers to it.
  • Resource URL: not verified yet. tunnel-client’s docs say it rewrites the resource in the gateway’s metadata to an OpenAI URL for your tunnel, so ChatGPT would ask Midplane Cloud for a token for that URL, which must be one of the gateway’s URLs. OpenAI doesn’t document its form, and Midplane registers only URLs whose path ends in /mcp. Until a real run settles this, ChatGPT through OpenAI’s tunnel may not finish signing in.

Docker Compose

tunnel-client shares the gateway’s network namespace:

Kubernetes

The gateway and tunnel-client run in one pod and talk over loopback, the pattern OpenAI documents. Midplane has no Helm chart yet; this is the pod to deploy.
The ConfigMap is read-only and a pod has no identity file, so enroll once with midplane enroll and keep the identity it prints in the secret.

Your own endpoint

When a hosted agent has no vendor tunnel, give the gateway a public URL:
  • Your load balancer or ingress, in front of the gateway. Allow only the vendor’s egress ranges: Anthropic publishes its IP addresses, OpenAI its ChatGPT connector ranges. TLS ends at your load balancer, or passes through to the gateway’s own certificate.
  • A public deployment in your own cloud account, with tls and the gateway listening on 0.0.0.0.
Add the URL to public_urls. If the load balancer rewrites Host, add that name to listen.allowed_hosts.