Troubleshoot MCP tunnels
Diagnose connectivity, TLS, IP validation, and OAuth routing issues in an MCP tunnel deployment.
MCP tunnels is a research preview feature. Request access to try it.
A tunnel that won't accept traffic can fail at three layers; diagnose them in order: the outbound connection to the tunnel edge, the inner TLS handshake from Anthropic to your proxy, then upstream routing and IP validation.
Quick reference
| Symptom | Cause | Fix |
|---|---|---|
| Tunnel doesn't appear in the agent + MCP Server picker | The picker only lists tunnels in the session's workspace that have at least one active certificate. | Register a CA certificate, or open the session in the workspace the tunnel was created in. |
Caller sees HTTP 500; cloudflared logs No ingress rules were defined | cloudflared has no local target. | Add --url http://localhost:8080 and network_mode: "service:mcp-proxy" to the cloudflared service. |
Proxy logs no route for host | tunnel_domain doesn't match the assigned domain, or config.yaml was edited without restarting (docker compose restart mcp-proxy). | Set tunnel_domain to the exact domain shown on the tunnel detail page. |
Proxy logs IP validation failed: <ip> is not a private address | Upstream resolves outside RFC1918. | See Upstream IP validation. |
Proxy exits with cannot unmarshal !!seq into map[string]string | routes is a YAML list. | Use routes: { name: http://host:port }. |
Proxy exits with open /data/tls.key: permission denied | The key is 0600; the proxy container runs non-root. | chmod 644 data/tls.key. |
curl https://:8080 fails with wrong version number | Expected; the listener is plaintext WebSocket. TLS happens inside the WS stream. | Verify through a Managed Agent or the Messages API instead. |
OAuth fails behind a source-IP allowlist
OAuth flows fail when your authorization server's source-IP allowlist blocks Anthropic's backend from reaching /token, /register, and the discovery endpoints. If you'd rather not allowlist Anthropic's egress ranges, you can route the backend-to-backend OAuth calls through the tunnel while keeping the browser-facing /authorize endpoint on your existing public hostname.
Add a proxy route for the authorization server
routes: mcp: http://your-mcp-server:8080 auth: http://your-auth-server:8080Restart the proxy after editing
routes(docker compose restart mcp-proxy, orhelm upgrade).Serve split-endpoint discovery metadata
Your authorization server's
/.well-known/oauth-authorization-serverresponse should pointauthorization_endpointat your existing allowlisted hostname and everything else at the tunnel:{ "issuer": "https://auth.<tunnel-domain>", "authorization_endpoint": "https://<your-allowlisted-host>/authorize", "token_endpoint": "https://auth.<tunnel-domain>/token", "registration_endpoint": "https://auth.<tunnel-domain>/register", "code_challenge_methods_supported": ["S256"] }Point the MCP server at the tunnel issuer
Your MCP server's
/.well-known/oauth-protected-resourceresponse should reference the tunnel hostname as its authorization server:{ "resource": "https://mcp.<tunnel-domain>", "authorization_servers": ["https://auth.<tunnel-domain>"] }
With this configuration, the user's browser hits /authorize on your existing hostname (which your allowlist already permits), while Anthropic's backend reaches /token, /register, and the discovery documents through the tunnel.
Setup Job authentication failures
The Helm setup Job and the Compose setup service authenticate to the Tunnels API by exchanging an OIDC JWT through your federation rule. When the exchange fails, see Troubleshoot a failed exchange in the Workload Identity Federation reference; the failure modes (subject, audience, issuer, JWKS, lifetime) are the same.
Tunnels-specific causes:
- The chart's default audience is
api.anthropic.com(no scheme). If your rule's audience ishttps://api.anthropic.com, setapi.wif.audienceto match. - A
403from the Tunnels API after a successful exchange means the rule's scope doesn't includeorg:manage_tunnels, or the rule's service account isn't a member of the tunnel's workspace. Set the scope and add the service account to the workspace.
The Helm setup Job runs as a pre-install hook. On failure, the Job is left behind for inspection (kubectl logs job/mcp-tunnel-setup -n mcp-tunnel). Helm doesn't manage hook resources, so delete it before retrying:
helm uninstall mcp-tunnel -n mcp-tunnel
kubectl -n mcp-tunnel delete job mcp-tunnel-setup
Tunnel won't connect
Check the cloudflared logs first. Common causes:
- The
TUNNEL_TOKENis missing, expired, or copied incorrectly. - A firewall is blocking outbound TCP/UDP on port 7844 to the tunnel edge.
cloudflared may also log warnings about UDP receive buffer sizes; this is a QUIC tuning hint, not an error.
Certificate errors
When Anthropic rejects the proxy's certificate during the inner TLS handshake, the proxy logs tls handshake failed. Verify that:
- The server certificate has not expired.
- The certificate's Subject Alternative Name matches
*.<tunnel-domain>. - The signing CA is registered with Anthropic for this tunnel.
See the certificate requirements for the full validation rules.
Upstream IP validation
For SSRF protection, the proxy only dials addresses in the RFC1918 private ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16) by default. Only IPv4 is supported.
If the proxy logs IP validation failed: <ip> is not a private address, the upstream hostname resolved outside that set. On Kubernetes, some managed distributions allocate the Service CIDR outside RFC1918; if kubectl get svc kubernetes -n default -o jsonpath='{.spec.clusterIP}' returns an address outside the private ranges, look up your cluster's Service CIDR and add it.
If the address is legitimate, add the narrowest covering CIDR to upstream.allowed_ips. Setting allowed_ips replaces the RFC1918 default rather than extending it, so include the private ranges your other upstreams use:
upstream:
allowed_ips:
- 10.0.0.0/8
- 172.16.0.0/12
- 192.168.0.0/16
- 127.0.0.0/8 # loopback, for local testing only
Avoid 0.0.0.0/0 outside of local testing; it disables SSRF protection entirely.