---
title: "Couldn't Reach the MCP Server: Works in Claude Code, Not claude.ai"
description: "claude.ai calls your MCP server from Anthropic's network, so DNS, a WAF rule or a redirect can kill it while curl and Claude Code both succeed. Find the hop."
canonicalUrl: "https://zuplo.com/learn/mcp/errors/mcp-server-works-in-claude-code-but-not-claude-ai"
pageType: "mcp-error"
errorString: "Couldn't reach the MCP server"
lastVerified: "2026-07-29"
specVersion: "2026-07-28"
---

# `Couldn't reach the MCP server`

> One toast covers four different failures. Claude Code and `curl` reach your server from your machine; a claude.ai connector reaches it from Anthropic's infrastructure over the public internet, so the request dies at whichever hop only exists on that path — DNS, your edge, a redirect, or OAuth discovery.

_MCP Specification 2026-07-28._

## Is this you?

Four causes, four checks, in the order Anthropic's own diagnostic checklist runs them. Run all of them from a network outside your own — a check that passes on your VPN proves nothing about the hosted path, which is the entire problem.

```bash
# 1. Public DNS. Every address must be globally routable, and there must be
#    an A record: claude.ai connectors are IPv4-only.
dig +short A mcp.example.com

# 2. The registered URL must not redirect to another host — the
#    Authorization header does not survive that hop.
curl -sI https://mcp.example.com/mcp | grep -Ei '^(HTTP/|location:)'

# 3. The endpoint has to answer from a public network. A 401 or 405 is fine.
curl -i -sS -X POST https://mcp.example.com/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

# 4. If the server requires auth, discovery has to answer as well. Append
#    your MCP path to the protected-resource well-known.
curl -i https://mcp.example.com/.well-known/oauth-protected-resource/mcp
```

**This error** — `dig` returns nothing, returns only `AAAA`, or returns an address in `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, `100.64.0.0/10`, loopback or link-local — and a mix of routable and non-routable answers fails the same way. A `301`, `302`, `307` or `308` to a different host. `403` or `429` in your edge or CDN logs during the Connect attempt that your application never generated. `404` on the protected-resource document and on both authorization-server documents, which leaves Claude nothing to read.

**Working** — One or more `A` records, every one of them globally routable. No `3xx` whose `Location` points at another host. Any HTTP response at all from the `POST` — a `401` or a `405` is fine, a timeout or connection refused is not. `200` and valid JSON from `/.well-known/oauth-protected-resource/mcp`, whose `resource` field matches the URL exactly as it is entered in Claude, and `200` from one of `/.well-known/oauth-authorization-server` or `/.well-known/openid-configuration` on the issuer host your `authorization_servers` names first. A `404` on the other one is expected. If this is what you see, this is not your problem.

**Clients also report this as:**

- `Authorization with the MCP server failed`
- `Works in Claude Code or curl but not claude.ai`
- `works in MCP Inspector or Claude Code CLI but not claude.ai`
- `Custom connector "Couldn't reach the MCP server" — zero inbound traffic at edge, server verified reachable`
- `AADSTS9010010 (sometimes surfaced as invalid_target)`

## Fix it

**In short.** Identify the hop before you change anything. There are four, and this is the order Anthropic's own checklist runs them in. Public DNS: Claude resolves your hostname and validates every answer before it sends a request, so it rejects a private (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`), carrier-grade NAT (`100.64.0.0/10`), loopback or link-local address, a mix of routable and non-routable answers, and a host with no `A` record — connectors are IPv4-only, so an `AAAA`-only hostname cannot be reached at all. Your edge: find the `403` or `429` in your CDN or WAF logs that your application never generated, and allowlist Anthropic's outbound range, `160.79.104.0/21`. A redirect: `curl -sI` the URL you registered, because a `3xx` to another host drops the `Authorization` header and the failure then surfaces as "Authorization with the MCP server failed" instead. Discovery: confirm your `/.well-known` documents answer, including on a cross-host authorization server, which Anthropic reaches from that same range. The failure toast and the page URL carry a reference id starting `ofid_`; file it with your server URL and your access logs on `anthropics/claude-ai-mcp`, because it is what lets Anthropic trace the request server-side, and the ids are time-limited.

The symptom is identical across platforms; the cause is not.

### Claude custom connectors

If your server's access log shows nothing at all during the Connect attempt

1. Claude resolves the hostname and validates the result before it makes any request, so a rejected name produces no traffic anywhere you can see. It rejects private, carrier-grade NAT, loopback and link-local addresses, any mix of public and non-public answers, and a hostname with no `A` record from public DNS — connectors are IPv4-only, so an `AAAA`-only host has nothing to dial.
2. Publish the server on a globally routable IPv4 endpoint: a host with a public IP, a public reverse proxy, or a tunnel. An Anthropic MCP tunnel does not close this gap for claude.ai — the docs state that tunnels created through the Console are not available as connectors in claude.ai, and reach Claude Managed Agents and the Messages API only.
3. Then collect the reference id. It starts `ofid_` and appears in the error toast and in the page URL; file it with your server URL and your server-side access logs on the `anthropics/claude-ai-mcp` tracker. It is what lets Anthropic trace the exact failure server-side, and reference ids are time-limited, so report soon after the failure.

```text
.../settings/connectors?step=start_error&flow_id=ofid_d32594c73257a651
```

[Anthropic: troubleshooting connectors](https://claude.com/docs/connectors/building/troubleshooting)

### Kubernetes

If the public record points into your cluster's network, or your ingress has a source-IP allowlist

1. An internal load balancer gets an internal address: `networking.gke.io/load-balancer-type: "Internal"` on a GKE Service creates a load balancer that, per Google's docs, only clients in the same VPC network or a network connected to it can reach. If that address is what your public zone publishes, claude.ai resolves it, sees a non-routable answer, and stops before sending anything.
2. Point the public record at an internet-facing ingress and keep the internal name for in-cluster callers. Verify with `dig +short A` from outside your network, not from a pod and not from a laptop on the VPN.
3. `nginx.ingress.kubernetes.io/whitelist-source-range` sets the allowed client IP source ranges as a comma-separated CIDR list, and ingress-nginx documents that an annotation on an Ingress rule overrides any global restriction — so `160.79.104.0/21` has to be in whichever one applies, or the MCP and OAuth paths never reach your pod. Do not treat that allowlist as authentication: the range is shared by every Anthropic customer.

[ingress-nginx: annotations reference](https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/annotations/)

### Cloudflare Access

If Cloudflare Access or a WAF rule sits in front of the hostname

1. A WAF rule, a bot-management rule and a rate limiter all answer before your application does, and all three look identical from claude.ai. Anthropic's guidance is the only reliable tell: look for `403` or `429` responses in your edge or CDN logs that your application did not generate, especially during a Connect attempt. Cloudflare Access in front of the hostname is a different failure with the same toast — it authenticates browser sessions, and that shape has its own page in the related errors below.
2. Add a Bypass policy for Anthropic's outbound range using an IP-range selector, which takes CIDR notation: `160.79.104.0/21`. Cloudflare documents Bypass as disabling Access enforcement for traffic matching the rule, and that bypassed requests are not logged — so it removes your only gate on that path, and the MCP route needs its own authentication afterwards.
3. Do not plan on a service token instead. That needs Claude to send `CF-Access-Client-Id` and `CF-Access-Client-Secret`, and claude.ai's request-header authentication is in beta and gated on contacting Anthropic, accepts at most four headers, and restricts names to a reviewed allowlist of standard authentication and routing headers such as `authorization`, `x-api-key` and `x-auth-token`.

[Cloudflare: Access policies](https://developers.cloudflare.com/cloudflare-one/policies/access/)

### AWS Load Balancer

If a WAF web ACL, security group or network ACL sits in front of your load balancer

1. A web ACL whose default action is Block, or a managed rule group that reads an unattended client as a bot, answers before your target group does. The allow path is an IP set match rule: create an IP set holding `160.79.104.0/21` and reference it from a rule with an Allow action, ordered ahead of the blocking rules. In the console rule builder the Request option to choose is `Originates from an IP address in`; in the API the statement is `IPSetReferenceStatement`. AWS WAF supports every IPv4 and IPv6 CIDR range except `/0`, and one IP set holds up to 10,000 addresses or ranges.
2. Check the security group and any network ACL as well. A group scoped to your corporate ranges drops the connection with no HTTP response at all, which claude.ai reports with the same string and which leaves nothing in your WAF logs to find.
3. If the listener also carries an `authenticate-oidc` action, that is a separate failure that produces the same toast — a browser redirect an MCP client cannot follow. It has its own page in the related errors below.

[AWS WAF: IP set match rule statement](https://docs.aws.amazon.com/waf/latest/developerguide/waf-rule-statement-type-ipset-match.html)

### Vercel

If the URL you registered redirects — apex to `www`, region routing, or a vanity domain to a CDN

1. Vercel recommends the `www` subdomain as the primary domain with a redirect from the non-`www` domain to it, and its docs say that if someone visits your domain with or without the `www` prefix, Vercel will attempt to redirect automatically. So the apex URL a developer naturally pastes into the connector dialog is a redirect, not an endpoint.
2. The `Authorization` header is dropped on a cross-host redirect per standard HTTP client security behaviour. The redirect target receives an unauthenticated request, returns `401`, and the connection fails with "Authorization with the MCP server failed" — which is why this cause hides behind the other error string.
3. Register the URL the server actually listens on, not one that redirects to it. `curl -sI` the registered URL: if the status is a `3xx` and the `Location` names another host, that target is the URL to register. Region routing and vanity-domain-to-CDN redirects behave the same way.

[Vercel: deploying and redirecting domains](https://vercel.com/docs/domains/working-with-domains/deploying-and-redirecting)

### Microsoft Entra ID

If your authorization server is Microsoft Entra ID and the token request fails with `AADSTS9010010`

1. This one lands on the other toast — Anthropic documents it under "Authorization with the MCP server failed", because the MCP endpoint was reachable and the token request is what failed. Entra is rejecting the `resource` value Claude sends, sometimes surfaced as `invalid_target`: Claude sets `resource` to your MCP server URL including the path, and Entra issues a token only when that value is listed as an Application ID URI on the app registration representing your protected API. The default `api://{client-id}` URI alone is not sufficient.
2. In the Entra admin center open the API's app registration — not the client's, if they are separate — and under Expose an API add your MCP server URL as an additional Application ID URI (`identifierUris` in the manifest). The value must match exactly, including the path, without a trailing slash.
3. If your server validates the audience through Azure App Service Authentication, add the API app's Application (client) ID and its `api://` URI to the allowed token audiences — the `Allowed token audiences` setting, not `Allowed client applications`, which is a different list. And where the client and the API are separate registrations, confirm the client has an admin-consented API permission for the scope your API exposes.

[Microsoft: expose a web API](https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-configure-app-expose-web-apis)

### Zuplo MCP Gateway

If the MCP server genuinely cannot be published to the public internet

1. claude.ai has no private path into your network, and Anthropic's own MCP tunnels do not provide one: they are a research preview, and the docs state that tunnels created through the Console are not available as connectors in claude.ai. For a claude.ai connector the fix has exactly one shape — a public origin in front of the private server.
2. The gateway is that origin: one public HTTPS route per upstream MCP server, with a server that only exists inside your VPC or on bare metal reached over the Zuplo secure tunnel. The tunnel service dials out from inside your network, so nothing is exposed to the internet and no inbound firewall rule is opened.
3. It also answers the discovery half of this error. For its MCP routes the gateway is both the OAuth 2.1 resource server and the authorization server, publishing an RFC 9728 protected resource metadata document per route and RFC 8414 authorization-server metadata both gateway-wide and per route — so there is no cross-host authorization server for a WAF to block.

[Zuplo MCP Gateway: authorization](/docs/mcp-gateway/auth/overview)

### Something else

Whatever sits in front of your server, the branch is the hop, and there are only four. Nothing on your network saw the request: the name you registered resolves to something Anthropic will not dial, so fix the public record or publish a routable IPv4 endpoint. Something answered before your application did: find the `403` or `429` in the edge logs and allowlist `160.79.104.0/21` on that component, or exempt the MCP and OAuth paths from the rule. The request arrived without its credential: your URL redirects to another host, so register the target instead. The MCP endpoint answered but discovery did not: return a `401` carrying `WWW-Authenticate: Bearer resource_metadata="…"`, or serve `/.well-known/oauth-protected-resource/<your-mcp-path>` on the origin, and remember that discovery requests to a cross-host authorization server arrive from the same egress range — so a WAF in front of your identity provider breaks the connection even when your MCP server is perfectly reachable.

## Why it happens

claude.ai connectors run on Anthropic's infrastructure and reach your server over the public internet. Everything that differs between that path and your laptop shows up here and nowhere else: split-horizon DNS answering with an internal address, a hostname that only resolves on the VPN, an edge rule that has never seen Anthropic's egress range, an apex-to-`www` canonicalisation no browser ever complains about. Each cause empties a different log, which is what makes the toast so hard to read: the pre-flight DNS check sends nothing at all, so no log you own records anything; an edge block leaves your application log empty but puts a `403` or `429` in your CDN's; and a discovery failure is documented as surfacing under this same string even when your MCP endpoint is reachable, with the signature that your MCP server sees the first request while your authorization server sees no traffic whatsoever. The redirect case is the one the wording actively misleads on: the request does arrive, without its credential, so the redirect target answers `401` and Claude reports "Authorization with the MCP server failed" instead. Local clients fail fast on a redirect, which is why the same misconfiguration looks instantly broken in MCP Inspector and looks like an auth bug on claude.ai.

### The exchange

claude.ai resolves the connector hostname on Anthropic's infrastructure and checks every address it gets back. One answer is a private address, so the connection is rejected before any HTTP request leaves Anthropic's network. Your edge, your MCP server and your authorization server are never contacted, so nothing appears in any log you own.

**Participants:** claude.ai (Anthropic infrastructure), Your edge (CDN, WAF, ingress), MCP server, Authorization server

1. **claude.ai** (internal step) — Resolve mcp.example.com — `public DNS — A records only, IPv4-only`
2. **claude.ai** (internal step) — Check every address it got back — `203.0.113.9, 10.4.2.11`
3. **claude.ai** (internal step) — Not every address is globally routable — `rejected before any request is sent` _(this step fails — the flow stops here)_

Notes:

- **Steps 1–2** — Split-horizon or internal DNS is the usual cause: the record that answers from the public internet carries an internal address. A mix of public and non-public answers is rejected too — every address must be globally routable.
- **Step 3** — Nothing leaves Anthropic's network, which is why the server looks healthy and the access log is empty. Claude Code and curl connect because they resolve and dial from your machine.

## What lands on you next

Reachability was the gate. Once claude.ai can connect, the route is a public entry point that a whole organization shares, and the questions change shape.

- **We allowlisted Anthropic's range. Isn't that our access control?** — `mcp-oauth-inbound` — No. `160.79.104.0/21` is shared by every Anthropic customer. One MCP OAuth policy per project — the gateway rejects a second — makes identity a token the route validates rather than a CIDR.
- **One connector, added once by an admin. Which tools should it see?** — `mcp-capability-filter-inbound` — A per-route allow-list of tools, prompts and resources; a hidden tool is refused with `MethodNotFound` before forwarding. It is static per route, so a read-only view and a full-power view are two routes.
- **Everyone shares that connector. Who called `refund_order`?** — `capability_invocation` — Every tool call emits an event carrying the caller, the capability, the upstream, the outcome and the latency, so a call resolves to a subject rather than to an egress IP.
- **An agent looped on one tool for an hour. What caps it?** — `rate-limit-inbound` — Limits counted against the authenticated identity with `rateLimitBy: "user"`, so one agent's retry storm does not spend another team's budget.

All of these attach to one MCP route's `policies.inbound` — the same policy engine on the way in and on the way out. See [Zuplo MCP Gateway](/mcp-gateway).

## Keep reading

- **Prevent it** — [How an Anthropic-managed agent authenticates to your MCP server](/learn/mcp/authentication/anthropic-managed-agents) — Reaching your MCP server from an agent that runs on Anthropic's infrastructure
- **Similar error** — [`Your MCP server returns a login page or 403 instead of 401`](/learn/mcp/errors/302-redirect-instead-of-401) — Something in front of your MCP server is enforcing authentication built for humans in browsers. Instead of the 401 an MCP client needs, it returns a login page, a redirect to one, or a bare 403 — so the client never learns how to authenticate.
- **Similar error** — [`Resource server does not implement OAuth 2.0 Protected Resource Metadata.`](/learn/mcp/errors/401-without-www-authenticate) — Your server answers 401 correctly, but the response carries no pointer to protected resource metadata — so the client has nothing to discover the authorization server with, and the OAuth flow never starts.
- **Error reference** — [every documented MCP error](/learn/mcp/errors)
