Custom MCP OAuth connector: token exchange succeeds server-side, connector never becomes 'Connected'
Subject: Custom MCP connector — OAuth token exchange succeeds server-side, but claude.ai connector never becomes "Connected"
Summary
I run a custom MCP server (DevOps Agent) at https://mcp.devopsagent.io/mcp, added as a
custom connector in claude.ai (Settings → Connectors). The connector has never
successfully shown as "Connected" — it stays in the "Not connected" list even immediately
after completing sign-in.
I've spent significant time (~4 months, across many iterations) verifying and fixing my
own server's OAuth implementation against RFC 8414, RFC 9728, RFC 7591, RFC 8707, and the
MCP Authorization spec (2025-06-18). As of today my server is fully spec-compliant and
I've reproduced the entire flow manually with curl with correct results at every step.
Despite that, the connector still fails — and my server-side logs show why: your
connector backend completes the OAuth token exchange and receives a valid access token,
but never follows up with a request to the MCP resource endpoint itself. I'd like your
team to investigate what happens on the claude.ai/connector-backend side immediately
after a successful token response.
Connector configuration
- MCP server URL used:
https://mcp.devopsagent.io/mcp - Transport: Streamable HTTP (
POST /mcp/) - Auth: OAuth 2.1 (public client, PKCE, dynamic client registration)
- Redirect URI observed in the flow:
https://claude.ai/api/mcp/auth_callback
(i.e. this is the web/account-level claude.ai connector flow, not a local
Claude Code/Desktop loopback flow)
What I verified is correct on my server (reproduced live just before filing this)
| Step | Result |
|---|---|
| TLS certificate for mcp.devopsagent.io | Valid, *.devopsagent.io, matches SNI |
| GET /.well-known/oauth-protected-resource/mcp | 200, resource field exactly matches the MCP server URL (https://mcp.devopsagent.io/mcp) |
| GET /.well-known/oauth-authorization-server | 200, correct issuer/authorization_endpoint/token_endpoint/registration_endpoint, host-relative (matches whichever domain — apex or mcp. subdomain — the client used) |
| 401 on unauthenticated POST /mcp | Correct WWW-Authenticate: Bearer realm="DevOps Agent", resource_metadata="https://mcp.devopsagent.io/.well-known/oauth-protected-resource/mcp" |
| POST /mcp/oauth/register (RFC 7591 DCR) | 201, echoes client_name/grant_types/response_types/redirect_uris |
| GET /mcp/oauth/authorize (with PKCE code_challenge, state, resource) | 200, renders sign-in form with all params round-tripped |
| POST /mcp/oauth/authorize (sign-in) | 302 redirect to redirect_uri with code, state, and RFC 9207 iss param |
| POST /mcp/oauth/token (authorization_code grant, PKCE verified) | 200, body: {"access_token","token_type":"Bearer","expires_in","refresh_token","scope":"mcp"}, headers: Cache-Control: no-store, correct CORS, valid JSON with no encoding issues |
| POST /mcp with a valid Bearer token | Correctly authenticated and processed |
| Cloudflare (in front of origin) | Passes all of the above through untouched — not blocking, challenging, or rate-limiting any of these requests |
The actual failure — evidence from production logs
Two real connection attempts from claude.ai's backend, captured in my application log and
nginx access log (both origin-side, i.e. these requests did reach my server and were
handled):
Attempt 1 (2026-07-26, UTC):
01:31:46.660Z MCP: registered OAuth client '7mbCGThoFPKpAkrifISg8g' (Claude)
01:31:47Z GET /mcp/oauth/authorize?...&resource=https%3A%2F%2Fmcp.devopsagent.io%2Fmcp -> 200 (sign-in page)
01:31:52Z POST /mcp/oauth/authorize (same params) -> 302 (redirect with code+state+iss)
01:31:53.351Z MCP OAuth: token issued for user_id=1 token_prefix=pmSMdKze...
01:31:53Z POST /mcp/oauth/token -> 200
Attempt 2 (~4 minutes later, same session):
01:35:28.681Z MCP: registered OAuth client '_GfkVeuc8IqT3LjWUi0nKQ' (Claude)
01:35:29Z GET /mcp/oauth/authorize?...&resource=https%3A%2F%2Fmcp.devopsagent.io%2Fmcp -> 200
01:35:33Z POST /mcp/oauth/authorize (same params) -> 302
01:35:34.195Z MCP OAuth: token issued for user_id=1 token_prefix=jsw7oLI8...
01:35:34Z POST /mcp/oauth/token -> 200
In neither case is there any subsequent request to /mcp (the actual resource
endpoint) using either issued token — checked directly against both the application
log (which logs every hit to the auth check, including rejected ones) and the nginx
access log (origin-level, so this isn't a client-side logging gap — no request of any
kind reached the origin for /mcp after either token was issued). I independently
confirmed a request to /mcp with a Bearer token does reach and get processed
correctly by my origin (tested manually with curl, immediately visible in both logs) —
so this isn't a network/Cloudflare/WAF issue either.
Per the MCP Authorization spec (2025-06-18), the client should go directly from
"receive access token" to "MCP request with access token" — no additional step in
between. That's the step that appears to be silently failing or not being attempted on
the claude.ai connector side.
After both attempts, claude.ai's own Settings → Connectors page (not just the Claude
Code /mcp command) shows the connector as "Not connected", despite the token
having been issued successfully to the account seconds earlier.
Update (2026-08-22): tested and ruled out a plausible client_id-churn theory
Between filing the original report above and now, I found one concrete, fixable
difference on my side worth testing: POST /mcp/oauth/register was minting a
brand-new random client_id on every single call, even though claude.ai calls/register again on every reconnect attempt with an otherwise-identical payload
(same client_name, same redirect_uris). My hypothesis was that if claude.ai's
connector bookkeeping expects a stable client identity across reconnects, that churn
could be why the connection never settles into "Connected" even though each
individual token issued is valid.
I shipped a fix making registration idempotent by redirect_uris (reusing the
existing client_id on a repeat registration instead of minting a new one), deployed
it to production, and verified via server logs that it's working correctly — e.g. on
2026-08-20:
MCP: reusing existing OAuth client '21xX0nLMVHOdrfP2TKZaGw' (Claude Code
(io-github-shittuay-devops-agent)) for repeat registration
This did not resolve the issue. A fresh attempt on 2026-08-22, a full month after
the original report, shows the exact same failure signature, now with a stableclient_id throughout:
14:32:48Z POST /mcp (unauthenticated probe) -> 401 [python-httpx]
14:32:49Z POST /mcp/oauth/register -> 201 [python-httpx]
14:32:49Z GET /mcp/oauth/authorize?...&client_id=2San2yM1JF4cYLp9H7yhVA... -> 200 [Safari, browser]
14:32:55Z POST /mcp/oauth/authorize (sign-in submitted) -> 302 [Safari]
14:32:56Z POST /mcp/oauth/token -> 200 [python-httpx]
MCP OAuth: token issued for user_id=1 token_prefix=81ShvUFf...
14:33:00Z POST /mcp/oauth/authorize (same code_challenge, resubmitted) -> 302 [Safari]
14:33:07Z GET /mcp/sse (unauthenticated probe) -> 401 [node]
14:33:13Z POST /mcp/oauth/register -> reuses client_id '2San2yM1JF4cYLp9H7yhVA' [python-httpx]
14:33:17Z GET/POST /mcp/oauth/authorize (fresh code_challenge) -> 302 [Safari]
14:33:18Z POST /mcp/oauth/token -> 200 [python-httpx]
MCP OAuth: token issued for user_id=1 token_prefix=K_Zgm13N...
14:33:35Z POST /mcp/oauth/register -> reuses same client_id again [python-httpx]
14:33:40Z POST /mcp/oauth/token -> 200
MCP OAuth: token issued for user_id=1 token_prefix=_qXAS8fU...
14:33:45Z GET /mcp/sse -> 401 [node]
14:34:21Z POST /mcp/oauth/register -> reuses same client_id again [python-httpx]
14:34:27Z POST /mcp/oauth/token -> 200
14:34:44Z GET /mcp/sse -> 401 [node]
14:35:34Z GET /mcp/sse -> 401 [node]
Same client_id reused correctly across 4 full register→authorize→token cycles in
under 3 minutes, 4 valid access tokens issued (confirmed in application logs, not just
HTTP status), and still zero requests anywhere in this window — or in the following
hour of continued polling — carry an Authorization header. I checked exhaustively:
no such request in the application log (which logs every hit to the bearer-token check,
including rejections) or the nginx access log (origin-level).
One more detail that may be useful: the recurring GET /mcp/sse 401s (User-Agentnode) are on a completely separate ~40-60s timer, decorrelated from the browser/python-httpx OAuth traffic — these look like Claude Code's local process polling the
legacy SSE transport directly from the user's machine, independent of whatever account-
level token state the web OAuth dance produces. Whatever holds the freshly-issued
token (the python-httpx client doing DCR/authorize/token — presumably server-side
infrastructure on your end) never appears to hand it off to the client that's actually
polling the resource. Both the web claude.ai Settings → Connectors page and Claude
Code's own /mcp status command report "not connected"/"unauthorized" throughout.
As a practical workaround (not a fix — just confirming the server itself works fine
outside this specific OAuth connector path), the user switched to a static API key viaAuthorization: Bearer <key> in a manually-configured .mcp.json entry, which
connects immediately with no issues. So this is specific to the OAuth connector flow,
not a general reachability/compatibility problem with the MCP server.
Request
At this point I'm fairly confident the remaining gap is in your connector-backend
infrastructure, not in my server's OAuth implementation — I've now tested this over
roughly 5 months, verified full RFC 8414/9728/7591/8707 and MCP-authorization-spec
compliance by hand, and shipped and verified a real fix for the one protocol-level
difference I could find (client_id stability), which did not change the outcome.
Could someone on the connectors/MCP team check what happens on claude.ai's backend
immediately after it receives a 200 from a custom connector's token endpoint — in
particular:
- Does it attempt an
initialize/tools/listcall against the resource server right
after obtaining the token? If so, is that call failing silently (timeout, unhandled
exception, response parsing issue)?
- Is there a handoff step between whatever issues/holds the token (server-side,
python-httpx) and whatever needs to present it (the local Claude Code client, or
the browser session) that could be failing to propagate the token?
Happy to provide full raw log excerpts, add more logging on my side and try again live
with someone from your team, or hop on a call if useful.
Thanks for looking into this.