PullMD Part 7: 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.
I tried this a few more times and thought: maybe a CORS issue, maybe a timeout, maybe a brief downtime on my end.
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, I 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 I stopped looking for bugs in my 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.
Someone else had already spent a week fighting the same problem, with significantly more pressure on them.
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 rules out a bug in any single codebase. That’s exactly what I could point to in the thread.
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.
v2.1.0 sits finished on the shelf — I just can’t release 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 three parts, three things ran differently than I could steer.
In Part 5 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 6 it was the multi-user pivot I hadn’t planned. The dependencies between the issues set the order, not me.
And here, in Part 7, 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 7 of 9 in the PullMD series.
← PullMD Part 6: The Multi-User Pivot | PullMD Part 8: An Old Rule →