PullMD Teil 7: Spec-Compliant ist nicht genug
v2.0 war am 2. Mai draußen. Damit gab es endlich Nutzer-Konten — und die sind die Voraussetzung für OAuth. Genau darum musste der Multi-User-Umbau vorher passieren. Jetzt konnte ich Issue #6 angehen: OAuth für den claude.ai-Web-Connector.
Claude Code und ich haben auf feat/oauth eine OAuth-2.1-Implementierung gegen den MCP-Spec gebaut, lokal getestet und deployed. Dann wollte ich den Server in claude.ai-Web hinzufügen.
Was auf feat/oauth gebaut wurde
Die Implementierung folgt dem aktuellen MCP-Spec vollständig:
- 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 auf dem Authorization-Endpoint)
- RFC 6750 — Bearer-Auth +
WWW-Authenticate-Challenge auf/mcpohne Token - RFC 7009 — Token-Revocation (
POST /oauth/revoke)
Dazu der vollständige Authorization-Code-Flow: GET /oauth/authorize → Consent-Page → POST /oauth/consent → Code-Exchange → JWT-Issue → Refresh-Token-Rotation mit Reuse-Detection. Die Tests liefen durch, die Endpoints antworteten korrekt. Die Implementierung war fertig.
Der letzte Commit vor dem Deployment: fix(oauth): CORS on token/register/revoke/discovery + /mcp for browser MCP clients. Dann docker compose pull && docker compose up -d auf meinem Ubuntu-Entwicklungsserver.
Add custom connector → „Couldn’t reach the MCP server“
claude.ai-Web, Einstellungen, „Add custom connector“. Hostname eingegeben. Erwartung: OAuth-Flow öffnet sich, Browser navigiert zur Consent-Page, ich authorize, fertig.
Was tatsächlich kam: „Couldn’t reach the MCP server“ — ohne weitere Information, ohne Stack-Trace, ohne HTTP-Statuscode.
Ich habe das ein paarmal wiederholt und gedacht: vielleicht ein CORS-Problem, vielleicht ein Timeout, vielleicht eine kurze Downtime auf meiner Seite.
Server-Logs leer
Der auffälligste Befund war nicht die Fehlermeldung — es war, was nicht passierte.
Traefik-Access-Log: leer. Kein einziger Request während der Versuche. Cloudflare-Analytics: null Traffic auf den relevanten Pfaden. Kein TCP-Handshake, kein TLS-Handshake, keine HTTP-Anfrage — die Verbindungsversuche erreichten meinen Server nie.
Direkte Curl-Anfragen vom selben Netzwerk, vom Mobilnetz, von einem VPS: alle erreichten den Server problemlos und bekamen die erwarteten Antworten zurück. Das Problem lag nicht auf meiner Seite des Kabels.
8 Spec-Checks live verifiziert
Bevor ich irgendwelche Schlüsse zog, habe ich alle Endpoints von einem dritten Netzwerk aus manuell durchgecheckt:
| Check | Ergebnis |
|---|---|
GET /.well-known/oauth-protected-resource (RFC 9728) |
200 ✅ |
GET /.well-known/oauth-protected-resource/mcp (Sub-Pfad) |
200 ✅ |
GET /.well-known/oauth-authorization-server (RFC 8414) |
200 ✅ |
POST /mcp ohne Token → 401 + RFC 6750 WWW-Authenticate-Bearer-Challenge |
✅ |
HEAD /mcp → 405 mit Allow: POST, OPTIONS |
✅ |
POST /oauth/register (RFC 7591 DCR) — akzeptiert claude.ai Redirect-URI |
201 ✅ |
OPTIONS /mcp von Origin: https://claude.ai (CORS-Preflight) |
204 ✅ |
GET /authorize → 302 → same-origin /oauth/authorize |
✅ |
Alle acht Checks grün. Ab da habe ich aufgehört, in meinem Code nach Fehlern zu suchen, und anderswo weitergesucht.
anthropics/claude-ai-mcp#237
Eine GitHub-Suche später: Issue #237 im anthropics/claude-ai-mcp-Repo. Eröffnet am 27. April — einen Tag vor meinem Reddit-Post — von einem User namens Philippstf.
Das Issue beschreibt ein Unternehmen namens Helferlain: eine SaaS-Performance-Marketing-Plattform, deren gesamtes Differenzierungsmerkmal darauf aufbaut, dass Nutzer ihren eigenen Claude-Pro-Account via custom MCP-Connector anbinden können. YC-Track-Startup, Launch-Deadline innerhalb von 30 Tagen, mehrere bezahlende Beta-Kunden, die auf die Integration warten. Das Symptom: exakt dasselbe wie meines — „Couldn’t reach the MCP server“, Server-Logs leer, Spec-Compliance verifiziert.
Philippstf hatte sogar wrangler tail live laufen während der Verbindungsversuche: clientsRegistered=0, codesIssued=0, tokensIssued=0. Kein einziger Request. Die Diagnose war präzise: Anthropic’s claudeai-proxy initiiert den OAuth-Flow nie — die Ablehnung passiert intern, bevor ein HTTP-Request den Origin-Server erreicht.
Jemand anderes hatte schon eine Woche mit dem gleichen Problem gekämpft, mit deutlich höherem Druck.
03.05. — Adding a data point
Ich habe einen Kommentar auf #237 hinterlassen, kurz und sachlich:
“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-Kommentar)„Selbes Symptom auf einem anderen Server — füge einen Datenpunkt hinzu. Setup: Custom MCP-Server, OAuth-2.1-Spec-konform. […] Server-seitige Belege: Traefik-Access-Log zeigt NULL Requests vom claudeai-proxy während wiederholter ‚Add custom connector’-Versuche.“ —
syswave-dev(Issue-Kommentar)
Meine Referenz-ID: ofid_24917fdc13bc12fd. Die hatte mir claude.ai zurückgegeben — ein Identifier, der auf Anthropic-Seite zu einem fehlgeschlagenen Verbindungsversuch gehört. Den habe ich mit dazugeschrieben, falls jemand Anthropics Logs danach durchsuchen will.
04.05. — Der zweite Server
Am nächsten Morgen habe ich mit Claude Code einen zweiten, unabhängigen OAuth-MCP-Server gebaut.
Anderer Stack: TypeScript statt JavaScript, PostgreSQL statt SQLite, aber dieselbe RFC-Coverage. Diesen zweiten Server wollte ich ohnehin bauen — und er war zugleich das beste Argument für den Issue: Zwei unabhängige Implementierungen auf verschiedenen Stacks, die identisch scheitern, schließen einen Fehler in einer einzelnen Codebase praktisch aus. Genau damit ließ sich im Thread argumentieren.
Zweiter Kommentar auf #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-Kommentar)„Einen zweiten unabhängigen OAuth-MCP-Server gebaut (andere Codebase, anderer Stack — TypeScript/PostgreSQL statt JavaScript/SQLite). Gleiche erschöpfende Spec-Compliance, gleiches Symptom bei ‚Add custom connector’ in der claude.ai-Web-UI. […] Zwei unabhängige Codebasen, die identisch fehlschlagen, schließen implementierungsspezifische Bugs auf User-Seite aus.“ —
syswave-dev(Issue-Kommentar)
Neue Referenz-ID: ofid_a20d3248e15a0e7b. Selber Befund. Damit war die Frage „was habe ich falsch gemacht?“ beantwortet: nichts.
06.05. früh morgens — Bestätigung
An dem Morgen, an dem ich diesen Artikel eigentlich zu schreiben beginnen wollte, hatte #237 einen neuen Kommentar von localden (Anthropic Collaborator). Das Issue wurde geschlossen:
“#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)„#256 hat einen
x-deny-reason: host_not_allowed-Header vom Anthropic-Egress-Proxy abgefangen — ein starker Hinweis, dass die zugrundeliegende Ursache eine Host-Allowlist auf unserer Seite ist, nicht etwas in deinem Server.“ — @localden (Anthropic, Issue-Close)
Issue #256 war ein separater Bericht: jemand hatte mit einem Meta-Ads-MCP-Server auf Cloudflare Workers denselben Fehler reproduziert und dabei den Egress-Header x-deny-reason: host_not_allowed abgefangen — direkt aus Anthropics Proxy-Response, nicht aus dem eigenen Server. Das war der fehlende Beweis: nicht nur „kein Request kommt an“, sondern konkret „weil dein Host nicht auf der Allowlist steht“.
Konsolidiert in Issue #214, das weiterhin offen ist und weitere betroffene Hosts und Referenz-IDs sammelt.
v2.1.0 liegt fertig im Regal — freigeben kann ich sie nur noch nicht. Der Branch feat/oauth ist komplett; wann die Allowlist aufgeht, liegt nicht in meiner Hand. Im Reddit-Thread vom 28. April habe ich den Stand kurz als EDIT2 festgehalten, für alle, die denselben Weg gehen wollten.
Was ich nicht in der Hand hatte
Über die drei Teile hinweg ging es dreimal darum, dass etwas anders lief, als ich es steuern konnte.
In Teil 5 war es die Resonanz auf Reddit, mit der ich nicht gerechnet hatte: 58 Kommentare, 320 Unique Cloner in zwei Wochen, dazu Feature-Wünsche, die so nicht auf meinem Plan standen.
In Teil 6 war es der Multi-User-Pivot, den ich nicht eingeplant hatte. Die Abhängigkeiten zwischen den Issues gaben die Reihenfolge vor, nicht ich.
Und hier, in Teil 7, ist es etwas, das ich gar nicht beeinflussen kann: eine Host-Allowlist auf Anthropics Seite. Bei den ersten beiden hätte ich anders planen oder priorisieren können. Hier nicht — der Code ist fertig und spec-konform, der Rest liegt bei jemand anderem.
Wer auf der Infrastruktur einer anderen Firma aufbaut, muss mit solchen Wartezeiten rechnen. Das ist keine Beschwerde, sondern einfach, wie es ist. Ich habe deshalb dokumentiert statt mich zu ärgern: was gebaut wurde, welche Checks grün waren, welche Referenz-IDs ich bekommen habe, was im Issue-Thread stand. Wer dasselbe Problem hat, findet das beim Suchen.
Teil 7 von 9 der PullMD Serie.
← PullMD Teil 6: Multi-User-Pivot | PullMD Teil 8: Eine alte Regel →