Troubleshoot MCP tunnels ​
Diagnose connectivity, TLS, IP validation, and OAuth routing issues in an MCP tunnel deployment.
INFO
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 ​
```yaml
routes:
mcp: http://your-mcp-server:8080
auth: http://your-auth-server:8080
```
Restart the proxy after editing `routes` (`docker compose restart mcp-proxy`, or `helm upgrade`).
Serve split-endpoint discovery metadata ​
Your authorization server's `/.well-known/oauth-authorization-server` response should point `authorization_endpoint` at your existing allowlisted hostname and everything else at the tunnel:
```json
{
"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-resource` response should reference the tunnel hostname as its authorization server:
```json
{
"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:
bash
helm uninstall mcp-tunnel -n mcp-tunnel
kubectl -n mcp-tunnel delete job mcp-tunnel-setupTunnel 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:
yaml
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 onlyWARNING
Avoid 0.0.0.0/0 outside of local testing; it disables SSRF protection entirely.