Your MCP server works from Claude Desktop and fails as a connector on claude.ai. Almost every guide explains that with "Desktop and web have different auth models", and that explanation is wrong. This is the actual difference, the four ways connections fail, and which of five approaches fits your situation.

The difference is not Desktop versus web

It is worth killing this idea first, because it sends people debugging the wrong layer. Anthropic's documentation is explicit that a remote connector behaves the same everywhere: This is true across every Claude client, including claude.ai, Claude Desktop, Cowork, and the mobile apps. Remote connectors run through Anthropic's own infrastructure regardless of which client you opened. There is no laxer rulebook for the desktop app.

What differs is not the client, it is the mechanism. Two of them are easy to confuse:

  • A local server, configured in claude_desktop_config.json, which Claude Desktop starts as a process on your machine and talks to over stdio. Anthropic calls this a separate mechanism. Nothing is authenticated here, because nothing crosses a network: the process runs as you, on your machine.
  • A remote connector, added by URL, which Anthropic's infrastructure connects to on behalf of a signed-in user. This is where OAuth lives.
A LOCAL SERVERYOUR MACHINEClaude Desktopstdioyour server processNothing crosses a network, so nothing is authenticated.A REMOTE CONNECTORANY CLIENT · DESKTOP, WEB, MOBILEClaudeAnthropicinfrastructureOAuthyoursSame rules in every client, desktop included.What mcp-remote doesIt runs as a local server and forwards to your remote URL with a static token attached.So it turns the right-hand case into the left-hand one, and the requirements never apply.
The trick that makes "it works in Desktop" true is usually the bridge, not the client. mcp-remote is a community tool and appears nowhere in Anthropic's documentation.

So the honest framing is this: your server never satisfied a connector's requirements, and the local bridge meant it never had to. The moment you register the same URL as a remote connector, the requirements apply, and they apply in Desktop just as much as on the web.

What a connector actually has to satisfy

Four groups of requirements, and the network ones bite before the OAuth ones ever get a chance.

Authorization

Anthropic's connector documentation names two supported OAuth types, oauth_dcr and oauth_cimd, and describes both as OAuth 2.0, not 2.1. What is non-negotiable is PKCE: Your authorization server must support S256 PKCE. If you have read "OAuth 2.1 required" elsewhere, that comes from the MCP specification, which requires authorization servers to implement OAuth 2.1 and clients to implement PKCE. Practically the two agree, but if you are reading Anthropic's error messages, read them against OAuth 2.0 plus S256 PKCE.

Discovery

This is where most write-ups, including an earlier version of this one, are too absolute. The documented primary path is not a well-known URL at all: your server answers an unauthenticated request with 401 and a WWW-Authenticate header carrying a resource_metadata pointer to your Protected Resource Metadata (RFC 9728). That pointer is free to live elsewhere: the resource_metadata URL doesn't have to be on the MCP server's origin. Probing /.well-known/oauth-protected-resource on your origin exists, but the docs tell you to Treat this as a fallback.

For client registration, the MCP specification deprecated Dynamic Client Registration in its 2026-07-28 revision, with Client ID Metadata Documents as the successor. Two caveats worth having before you rewrite anything: DCR still works and remains a valid MAY under a deprecation policy that grants at least twelve months, and CIMD is a SHOULD resting on an IETF draft, not a finished RFC. The specification's actual order of preference starts earlier than either: pre-registration first, then CIMD, then DCR, then asking the user.

The network

Anthropic's outbound traffic originates from 160.79.104.0/21. Note the prefix length: the same reference page lists a /23 for inbound traffic, and confusing the two is the single most common error in third-party posts about this. Connectors are also IPv4 only, so a hostname publishing only AAAA records cannot be reached at all.

Latency

Your endpoints get up to 10 seconds for a response from your OAuth discovery, registration, and token endpoints, and up to 30 seconds for refresh. A cold Lambda in front of a slow identity provider can fail on time alone, with everything configured correctly.

Failure one: there is no authorization server to discover

The most common case, and the one that fills forum threads. Your server has no OAuth endpoints at all, so discovery ends before it starts. No amount of header configuration fixes this: either something in front of the server provides the endpoints, or the connector will not connect.

n8n is the example most people reach for, and it is the one to be careful with, because the popular answer is out of date. Since version 2.27.0, released 16 June 2026, the MCP Server Trigger offers n8n User Auth (OAuth2) and n8n runs a real authorization server behind it: authorization server metadata, S256 PKCE, a registration endpoint and Protected Resource Metadata, plus the WWW-Authenticate pointer since a fix on 20 July 2026. That is the whole chain a connector looks for.

Three things keep the old answer circulating. n8n's documentation still lists only Bearer auth, Header auth. The option is gated on trigger node version 2, so an older workflow does not show it even on a current n8n. And the community feature request from 8 March 2026 is still open with a reply recommending a proxy, a reply written a week before the release. If you are about to build a proxy because a forum thread told you to, check your n8n version first. The remaining reason to put one in front is not the protocol but who the callers are: n8nOAuth2 signs people in as n8n users, and by default they need execute permission on the workflow.

Failure two: the request never leaves Anthropic

The signature here is unmistakable once you know it: an error in Claude, and nothing whatsoever in your access log. People spend hours on TLS and firewalls for a request that was never sent. Anthropic's troubleshooting page says it plainly: Claude rejects the connection before any HTTP request leaves Anthropic's network.

Three ways to land there. Your hostname resolves to a private range, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 or CGNAT 100.64.0.0/10. It publishes only AAAA records, and connectors are IPv4 only. Or it resolves differently inside and outside your network, and split-horizon DNS hands the outside world an internal address. That third one is worth understanding precisely: split-horizon is not detected as such. Anthropic resolves your hostname from public DNS and validates what comes back, so a private answer is rejected on its own merits. Every returned address has to be globally routable, which also rules out mixing public and private records on one name.

Failure three: it connects, then dies

The frustrating class, because OAuth succeeded. The connector appears, tools may list once, and then the connection drops on first use. The cause is almost always something between Anthropic and your server buffering or rewriting a stream that was never meant to be buffered.

Four settings, and the order matters more than the list, because the first one changes what all the others see:

  1. Tell your app how many proxies sit in front of it. In n8n that is N8N_PROXY_HOPS, a number, default 0: Number of reverse-proxies n8n is running behind. You will find N8N_TRUST_PROXY=true in blog posts and forum answers; it is not in n8n's environment variable reference, and there are threads from people watching it do nothing. Until the hop count is right, your app builds absolute URLs from its internal hostname, and every URL it hands out in discovery and redirects is wrong.
  2. Set the public hostname on the request, so it arrives under the name the OAuth flow ran against. In a Cloudflare Tunnel that is httpHostHeader, which Sets the HTTP Host header on requests sent to the local service.
  3. Stop the hop from re-chunking the response. In a Cloudflare Tunnel that is disableChunkedEncoding. Be honest with yourself about this one: Cloudflare documents it for WSGI servers, not for SSE, and there is no vendor statement that it is required for streaming MCP. It is a field remedy that helps often enough to try early, not a documented requirement, and it applies to HTTP/1.1 only.
  4. Allow WebSocket upgrades through the tunnel, so an upgrade request reaches your server instead of being answered by the hop.

One more thing that turns a fixed setup into a still-broken one: cross-host redirects drop the Authorization header. If your connector URL redirects to a different hostname, what arrives at the target is anonymous. Point the connector at the final URL rather than at something that redirects to it.

Failure four: everything works, and you have no idea who did what

Not an error message, which is exactly why it survives into production. Every approach below that puts a shared credential in front of your server authenticates the connection. The upstream sees one identity for everyone behind it, and the log says the same thing on every call. For one person that is irrelevant. From the second person on, it is the difference between an audit trail and a shrug.

Why static_headers is not the shortcut

Anthropic does support static request headers, so the temptation is to skip OAuth entirely. Four facts from the documentation, each of which limits it:

  • Request header authentication is in beta, and it is being slowly rolled out to customers. You cannot plan on having it.
  • You can add up to four headers.
  • Header names are restricted: Header names are restricted to this allowlist for security reasons.
  • And the one that decides it: The credential is shared by the organization rather than pasted per user.

That last point makes it a different product decision, not a shortcut to the same place. Everyone in the organization shares one credential, so you cannot revoke one person, and you cannot tell two people apart in a log.

Five approaches, and when each is right

ApproachRight whenThe limit
A local server, or mcp-remote as a local bridgeOne person, one machine, no audit requirementNot a connector at all: no web, no mobile, no colleagues, no identity
Self-hosted OAuth proxy (sigbit/mcp-auth-proxy)You run infrastructure and want to keep it in-houseAuthenticates the connection, not the person
Build the OAuth endpoints into your own serverMaximum control, and someone who wants to own itYou now maintain auth-critical code, discovery included
An identity-aware proxy such as Cloudflare AccessYou are already on that platformGates the door, not the tool call; no per-tool audit
A hosted serviceMore than one user, or you owe somebody an audit trailCosts money, and your traffic runs through someone else

The question that separates them

Connection or person. Everything above gets a connector to say "connected". Only some of them can answer the question that arrives six months later, when somebody asks who pulled the customer list. If nobody in your organization will ever ask it, take the free proxy and enjoy your afternoon. If somebody will, that is the row to decide on, and it is worth deciding before the second user arrives rather than after.

Narrowed it down to "something in front of my server"? Then the follow-up is which something: Do you need an MCP OAuth proxy? Five options compared.

Sources

Every quotation above comes from one of these. All of them were checked on 27 August 2026; connector behaviour changes, so check the date on anything you rely on.