PullMD Part 12: Spec-Compliance Isn't Enough
v2.0 shipped on May 2. It finally gave me user accounts. And those are the prerequisite for OAuth. That’s exactly why the multi-user rebuild had to come first. Now I could take on Issue #6: OAuth for the claude.ai web connector.
Claude Code and I built an OAuth 2.1 implementation against the MCP spec on feat/oauth, tested it locally, and deployed. Then I tried to add the server in claude.ai web.
What Was Built on feat/oauth
The implementation follows the current MCP spec in full:
- RFC 9728 — Protected Resource Metadata (
/.well-known/oauth-protected-resource) - RFC 8414 — Authorization Server Metadata (
/.well-known/oauth-authorization-server) - RFC 7591 — Dynamic Client Registration (
POST /oauth/register) - RFC 7636 — PKCE-S256 (code challenge on the authorization endpoint)
- RFC 6750 — Bearer auth +
WWW-Authenticatechallenge on/mcpwithout token - RFC 7009 — Token revocation (
POST /oauth/revoke)
Plus the complete authorization code flow: GET /oauth/authorize → consent page → POST /oauth/consent → code exchange → JWT issue → refresh token rotation with reuse detection.
Tests passed, the endpoints responded correctly. The implementation was done.
The last commit before deployment: fix(oauth): CORS on token/register/revoke/discovery + /mcp for browser MCP clients. Then docker compose pull && docker compose up -d on my Ubuntu development server.
Add Custom Connector → “Couldn’t reach the MCP server”
claude.ai web, settings, “Add custom connector”. Hostname entered. Expected: OAuth flow opens, browser navigates to the consent page, I authorize, done.
What actually came back: “Couldn’t reach the MCP server”. No further information, no stack trace, no HTTP status code.
Server Logs Empty
The most striking finding wasn’t the error message. It was what didn’t happen. Traefik access log: empty. Not a single request during the attempts. Cloudflare analytics: zero traffic on the relevant paths.
No TCP handshake, no TLS handshake, no HTTP request. The connection attempts never reached my server.
Direct curl requests from the same network, from mobile, from a VPS: all reached the server without issue and got the expected responses back.
The problem wasn’t on my side of the cable.
8 Spec Checks Live-Verified
Before drawing any conclusions, we manually checked all endpoints from a third network:
| Check | Result |
|---|---|
GET /.well-known/oauth-protected-resource (RFC 9728) |
200 ✅ |
GET /.well-known/oauth-protected-resource/mcp (sub-path) |
200 ✅ |
GET /.well-known/oauth-authorization-server (RFC 8414) |
200 ✅ |
POST /mcp without token → 401 + RFC 6750 WWW-Authenticate bearer challenge |
✅ |
HEAD /mcp → 405 with Allow: POST, OPTIONS |
✅ |
POST /oauth/register (RFC 7591 DCR) — accepts claude.ai redirect URI |
201 ✅ |
OPTIONS /mcp from Origin: https://claude.ai (CORS preflight) |
204 ✅ |
GET /authorize → 302 → same-origin /oauth/authorize |
✅ |
All eight checks green. From that point we stopped looking for bugs in our own code and started looking elsewhere.
anthropics/claude-ai-mcp#237
One GitHub search later: Issue #237 in the anthropics/claude-ai-mcp repo. Opened on April 27 – one day before my Reddit post – by a user named Philippstf.
The issue describes a company called Helferlain: a SaaS performance-marketing platform whose entire differentiator is built on letting users connect their own Claude Pro account via a custom MCP connector. YC-track startup, launch deadline within 30 days, several paying beta customers waiting on the integration.
The symptom: exactly the same as mine. “Couldn’t reach the MCP server”, server logs empty, spec compliance verified.
Philippstf had even run wrangler tail live during the connection attempts: clientsRegistered=0, codesIssued=0, tokensIssued=0. Not a single request.
The diagnosis was precise: Anthropic’s claudeai-proxy never initiates the OAuth flow. The rejection happens internally, before an HTTP request reaches the origin server.
May 3 — Adding a Data Point
I left a comment on #237, brief and factual:
“Same symptom on a different server — adding a data point. Setup: Custom MCP server, OAuth 2.1 spec-compliant. […] Server-side evidence: Traefik access log shows ZERO requests from claudeai-proxy during repeated ‘Add custom connector’ attempts.” —
syswave-dev(issue comment)
My reference ID: ofid_24917fdc13bc12fd. That was what claude.ai had returned to me: an identifier on Anthropic’s side that corresponds to a failed connection attempt. I included it in case anyone wants to search Anthropic’s logs for it.
May 4 — The Second Server
The next morning I built a second, independent OAuth MCP server with Claude Code. Different stack: TypeScript instead of JavaScript, PostgreSQL instead of SQLite, but the same RFC coverage.
I’d been meaning to build this second server anyway. And it doubled as the strongest argument for the issue: two independent implementations on different stacks failing identically all but rule out a bug in any single codebase.
Second comment on #237:
“Built a second independent OAuth-MCP server (different codebase, different stack — TypeScript/PostgreSQL instead of JavaScript/SQLite). Same exhaustive spec compliance, same symptom on ‘Add custom connector’ in claude.ai web UI. […] Two independent codebases failing identically rules out implementation-specific bugs on the user side.” —
syswave-dev(issue comment)
New reference ID: ofid_a20d3248e15a0e7b. Same finding. That answered the question “what did I do wrong?”: nothing.
May 6, Early Morning — Confirmation
On the morning I had actually intended to start writing this article, #237 had a new comment from localden (Anthropic Collaborator). The issue was closed:
“#256 captured an
x-deny-reason: host_not_allowedheader from Anthropic’s egress proxy, which is a strong signal that the underlying cause is a host allowlist on our side rather than anything in your server.” — @localden (Anthropic, issue close)
Issue #256 was a separate report: someone had reproduced the same error with a Meta Ads MCP server on Cloudflare Workers and had captured the egress header x-deny-reason: host_not_allowed. Directly from Anthropic’s proxy response, not from their own server. That was the missing evidence: not just “no request comes through”, but specifically “because your host isn’t on the allowlist”.
Consolidated in Issue #214, which remains open and continues collecting affected hosts and reference IDs.
The OAuth release sits finished on the shelf. I just can’t ship it yet. The feat/oauth branch is complete; when the allowlist opens is not in my hands.
In the Reddit thread from April 28 I captured the status briefly as an EDIT2, for anyone heading down the same path.
What I Couldn’t Control
Across the last few parts, three things ran differently than I could steer.
In Part 9 it was the response on Reddit, which I hadn’t counted on: 58 comments, 320 unique cloners in two weeks, plus feature requests I hadn’t planned for.
In Part 11 it was the multi-user pivot I hadn’t planned. The dependencies between the issues set the order, not me.
And here, in Part 12, it’s something I can’t influence at all: a host allowlist on Anthropic’s side.
With the first two I could have planned or prioritized differently. Not here: the code is finished and spec-compliant, the rest is up to someone else.
Building on another company’s infrastructure means living with waits like this. It’s not a complaint, just how it is.
So I documented instead of getting annoyed: what got built, which checks were green, which reference IDs I got, what the issue thread turned up.
Anyone with the same problem will find it when they search.
Part 12 of 15 in the PullMD series.
← PullMD Part 11: The Multi-User Pivot | PullMD Part 13: An Old Rule →