[BUG] Claude Desktop doesn't connect to Custom MCPs altogether (not with OAuth 2.1 nor with SSE)
Hi Anthropics Team,
We have developed and deployed an MCP server with OAuth 2.1 authentication for the purposes of our company.
The deployed MCP works fantastically with Claude Code!
claude mcp add --transport http custom_mcp [redacted-url]
It requires the authentication, goes through the authentication process, lists all the tools and use them appropriately. All communication is logged via server logs.
When configured in Clade Desktop as a Connector with the same url, the service is never contacted. When clicking "Connect":
- Claude in a browser issues an event type: event : "claudeai.mcp.auth.init". Soon later it reloads the page with the error: "There was an error connecting to [Your custom Connection] Please check your server URL and make sure your server handles auth correctly.
- Claude Desktop application opens a url: https://claude.ai/api/organizations/[redacted-id]/mcp/start-auth/[redacted-id]?redirect_url=claude%3A%2F%2Fclaude.ai%2Fnew%3F&open_in_browser=1&fromDesktopApp=true , which in turn opens a New Conversation in the application and nothing happens
This time, no request reaches the deployed server. No single query, successful or not, is logged by the service. The communication seems to be broken when it reaches Claude servers.
Please investigate why Claude Desktop doesn't issue any requests (authentication or functional) to Custom Connectors.
Found 3 possible duplicate issues:
- https://github.com/anthropics/claude-code/issues/3515
- https://github.com/anthropics/claude-code/issues/3140
- https://github.com/anthropics/claude-code/issues/3273
This issue will be automatically closed as a duplicate in 3 days.
- If your issue is a duplicate, please close it and 👍 the existing issue instead
- To prevent auto-closure, add a comment or 👎 this comment
🤖 Generated with Claude Code
Endorse https://github.com/anthropics/claude-code/issues/3515 Make it high priority. Close this ticket as a duplicate of https://github.com/anthropics/claude-code/issues/3515
I am also facing the same issue. Is there any workaround for it?
I also faced exactly the same issue
I also faced exactly the same issue
I am also facing this issue. What's working in a variety of other clients (MCP Inspector, VS Code, Cursor, Cloudflare's AI Playground), Claude Desktop appears to not make the followup request to the resource_metadata URL that is provided in the WWW-Authenticate header when no Bearer token is provided on the Authorization header, so the OAuth dance stops prematurely.
I solved the issue by allowing anthropic's outbound IP addresses to my WAF firewall
- https://docs.anthropic.com/en/api/ip-addresses
Also facing this exact issue - any updates? Works perfectly in Claude Code 👍
Glad that I'm not the only one seeing this.
Debugged it through and through. I'll give a stab at Claude Code. Thanks!
Small update. I managed to get it working, albeit extremely flaky. Hard to tell if the flakiness is due to some state in Claude Desktop, or some bug in the app.
But as mentioned before in this thread by @anyoung-tableau, it seems that Claude Desktop doesn't read or care about the WWW-Authenticate metadata like resource_metadata etc.
Instead it seems to just test some various URLs like:
/.well-known/oauth-authorization-server/mcp
/.well-known/oauth-protected-resource/mcp
/.well-known/oauth-authorization-server/oidc
I tried adding redirects for these to my actual locations, but that didn't seem to work (but again, its hard to tell when it super flaky).
What did finally make it work was just proxying those requests directly (example with Fastify):
const createProxyHandler = (targetPath: string) => {
return async (request: FastifyRequest, reply: FastifyReply) => {
const resp = await server.inject({
headers: request.headers,
method: 'GET',
url: `${issuer}${targetPath}`
});
const data = resp.json();
return reply.type('application/json').send(data);
};
};
server.get(path, createProxyHandler(target));
Despite this though, it seems to fail about 70% of the time with no clear reason. Whereas in Claude Code it works flawlessly.
+1, same issue here. here is a summary of the debugging session I had with Claude Code. There is 0 debugging on Claude Desktop which doesn't help. Tried both Streamable HTTP and SSE.
TL;DR: everything works fine in Claude Code but fails miserably in Claude Desktop. Long version below.
Claude Desktop SSE MCP Connection Failure - Debugging Summary
Problem Description
Custom MCP server with OAuth 2.0 SSE transport works perfectly with MCP Inspector but fails silently with Claude Desktop after OAuth completion.
Environment
- Claude Desktop Version: 0.13.37 (Electron 37.5.1)
- Server: Custom Rust MCP server using
rmcplibrary - Transport: SSE (Server-Sent Events)
- Authentication: OAuth 2.0 with Dynamic Client Registration (RFC 7591)
- URL Pattern:
https://{uuid}.saramcp.com
What Works ✅
-
MCP Inspector Connection
- OAuth discovery: ✅ PASS
- Client registration: ✅ PASS
- Authorization flow: ✅ PASS
- Token exchange: ✅ PASS
- SSE connection: ✅ PASS
initializerequest: ✅ PASStools/listrequest: ✅ PASS (returns 2 tools)
-
OAuth Implementation
- All
.well-knownendpoints return correct metadata - Dynamic Client Registration works (RFC 7591)
- Authorization code flow completes successfully
- Access tokens and refresh tokens issued correctly
- PKCE with S256 working
- All
-
Server Response Times
- All OAuth endpoints: < 200ms
- Metadata endpoints: < 1ms
- Token generation: < 100ms
What Fails ❌
Claude Desktop completes OAuth but never establishes SSE connection.
Server Logs Evidence
# OAuth flow completes successfully:
2025-10-07 23:08:01 [DEBUG] POST /.oauth/token - 200 OK
2025-10-07 23:08:01 [DEBUG] Access token issued: mcp_token_***
# Expected next: SSE connection with Authorization header
# Actual: NOTHING. Zero requests.
# No GET / with Authorization header
# No initialize messages
# No tools/list requests
# Complete silence after token exchange
Network Tab Evidence (Chrome DevTools)
Using developer_settings.json with DevTools enabled in Claude Desktop:
{"allowDevTools": true}
Observed:
- ✅ Metadata discovery requests visible
- ✅ OAuth registration request visible
- ✅ Token exchange request visible (200 OK)
- ❌ NO SSE connection attempt to custom server
- ❌ NO requests with Authorization header
Filter: Searching for domain in Network tab shows ZERO requests after OAuth completion.
Connector Behavior
- Click "Add Custom Connector" with URL
https://{uuid}.saramcp.com - OAuth browser window opens
- User logs in successfully
- Authorization granted
- Browser redirects back to
claude.ai/new - Connector disappears from settings (not shown as connected or disconnected)
- No error messages displayed to user
- No connection attempts logged on server
OAuth Metadata Comparison
Working Server (Doxyde.com)
{
"oauth-authorization-server": "https://doxyde.com/.well-known/oauth-authorization-server",
"protected-resources": [
"https://sse.doxyde.com/",
"https://sse.doxyde.com/message"
]
}
Our Server (Same Structure)
{
"oauth-authorization-server": "https://{uuid}.saramcp.com/.well-known/oauth-authorization-server",
"protected-resources": [
"https://{uuid}.saramcp.com/",
"https://{uuid}.saramcp.com/message"
]
}
Both use identical structure. Doxyde works, ours doesn't.
What We Tried (All Failed)
- ✅ Added
response_modes_supported: ["query"]to auth server metadata - ✅ Added
resourcefield per RFC 9728 - ✅ Removed
resourcefield to match working servers - ✅ Tested different OAuth scopes
- ✅ Verified CORS headers on all endpoints
- ✅ Tested with public/organization/private access levels
- ✅ Checked token expiration times (3600s)
- ✅ Verified redirect URIs match exactly
- ✅ Tested SSE endpoints directly (work perfectly)
- ✅ Validated all JSON-RPC 2.0 responses
None of these changed the behavior.
Authorization Server Metadata (Full)
{
"issuer": "https://{uuid}.saramcp.com",
"authorization_endpoint": "https://{uuid}.saramcp.com/.oauth/authorize",
"token_endpoint": "https://{uuid}.saramcp.com/.oauth/token",
"registration_endpoint": "https://{uuid}.saramcp.com/.oauth/register",
"scopes_supported": ["mcp:read"],
"response_types_supported": ["code"],
"response_modes_supported": ["query"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"token_endpoint_auth_methods_supported": ["none"],
"code_challenge_methods_supported": ["S256", "plain"]
}
SSE Server Implementation
Library: rmcp (Rust MCP SDK) - official implementation
Endpoints:
GET /- SSE connection endpointPOST /message- Message endpoint
Verified working with:
- ✅ MCP Inspector
- ✅ Direct SSE client testing
- ✅ cURL with streaming
Hypothesis
The issue appears to be in Claude Desktop's OAuth-to-SSE bridge:
- OAuth proxy at
claude.aicompletes authentication ✅ - OAuth proxy receives access token ✅
- OAuth proxy should establish SSE connection to custom server ❌
- Connection dies here - SSE connection never attempted
The bug is NOT in:
- Server implementation (works with MCP Inspector)
- OAuth flow (completes successfully)
- SSE implementation (works when tested directly)
The bug IS in:
- Claude Desktop's post-OAuth connection logic
- Whatever happens after Claude receives the access token
- The bridge between OAuth completion and SSE establishment
Debug Logs Access
Server logs: Full request/response logging with timestamps
Claude Desktop logs: ~/Library/Logs/Claude/mcp*.log (no errors shown)
DevTools: Network tab shows OAuth but no SSE connection
Reproducibility
100% reproducible across multiple attempts, different servers, different configurations.
Request for Anthropic
Could you please add more verbose logging to Claude Desktop's MCP connector system? Specifically:
- Log when OAuth completes successfully
- Log when attempting to establish SSE connection
- Log the exact URL being used for SSE
- Log any errors during SSE connection setup
- Show connection status in UI (connecting/failed/connected)
Currently there is ZERO feedback to developers when this fails. The connector just vanishes silently.
Workaround
None known. MCP Inspector works, but Claude Desktop does not. Claude Code (CLI) works fine.
Happy to provide more logs, traces, or test any debugging approaches. Server is running at production URL and can be tested anytime.
I also faced exactly the same issue
Same issue here
Could be same problem as mine - https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1674 But in my case claude receives token as just just calls mcp without it..
@jhiver why 'Browser redirects back to claude.ai/new'? It should redirect to https://claude.ai/api/mcp/auth_callback, no?
Facing same issue
My current theory is that Claude and MCP Inspector both are incorrectly executing the HTTP session handshake on HTTP streamable. When I switched both to sse, I was able to connect properly. Claude Desktop is still doing weird things however. The connection will persist in a chat conversation over many hours, but if I start a new chat, the tool calls fail for both the new chat and the existing chat. My server shows a proper session and refresh token exchange, but also a bunch of new auth requests from Claude. Very strange.
And MCP calls in the client are just rate limited, so even if you get it to work it stinks. I asked Claude to make calls in quick succession and it only made seven relatively slow calls before breaking.
Any workarounds?
Not that I've found yet. Works great for a few minutes, then all the tool calls start failing. I don't have this kind of auth problem with any other AI agent or LLM.
Same issue here. MCP connection works fine with ChatGPT, Mistral, Dust, but not with Claude. Worked fine with Claude until October 26. Auth looks ok but in the browser, at the end of the auth: https://claude.ai/settings/connectors?&server=189fd97b-ed2c-45fd-92ee-27320c6cc04f&step=end_error and then authenticate is called with no token. We desperately tried to get some support from Anthropic for 8 days now, with no answer.
This is crazy.. made whole protocol, made it popular, but dont care about own implementation.. give us at least ability to see some error log..
Got an answer from Anthropic. The problem was that in the authentication workflow, the initialize method (the one returned in WWW-Authenticate, in my case /oauth-protected-resource) returned 200 instead of 401. Now /oauth-protected-resource returns the metadata in a 401 answer and it is fine.
I do not see the link between the fix and what I observed (and it used to work with a 200), but it does fix the issue.
I can connect over SSE. I am returning a 401, so my issue isn't the same. Claude itself is convinced the issues on the platform are severe. The same tool call in the stretch of a couple minutes will fail or succeed intermittently. Failed calls never make it to the server. If you do get it to work, it quickly hits a weird ~6 tool call rate limit within one conversation. It's really quite bad in comparison to Claude Code, which works well with no problems.
Same issue here. MCP connection works fine with ChatGPT, Mistral, Dust, but not with Claude. Worked fine with Claude until October 26. Auth looks ok but in the browser, at the end of the auth: https://claude.ai/settings/connectors?&server=189fd97b-ed2c-45fd-92ee-27320c6cc04f&step=end_error and then authenticate is called with no token. We desperately tried to get some support from Anthropic for 8 days now, with no answer.
I'm facing a similar issue. Everything was working fine until last week, but it suddenly stopped. I'm now getting a start_error in the URL. I tried reaching out through the Fin bot, but it keeps repeating the same resolution and doesn't seem to understand the problem. how did you reach out them ? https://claude.ai/settings/connectors?&server=e231fb70-965e-4fc4-b756-607319035894&step=start_error
Same issue here. MCP connection works fine with ChatGPT, Mistral, Dust, but not with Claude. Worked fine with Claude until October 26. Auth looks ok but in the browser, at the end of the auth: https://claude.ai/settings/connectors?&server=189fd97b-ed2c-45fd-92ee-27320c6cc04f&step=end_error and then authenticate is called with no token. We desperately tried to get some support from Anthropic for 8 days now, with no answer.
I'm facing a similar issue. Everything was working fine until last week, but it suddenly stopped. I'm now getting a start_error in the URL. I tried reaching out through the Fin bot, but it keeps repeating the same resolution and doesn't seem to understand the problem. how did you reach out them ? https://claude.ai/settings/connectors?&server=e231fb70-965e-4fc4-b756-607319035894&step=start_error
We wrote to [email protected]. It took them one week to answer.
So. A couple of additional thoughts. I've had more initial connection stability since making sure that WWW-Authenticate headers were set properly. However, client side tool call failures are still intermittent. On SSE, Claude Desktop makes POST calls to the SSE endpoint that it shouldn't. And on httpstreamable, some tool calls just disappear. No server log for them and they fail client side.
I was able to resolve the issue I was having by doing 2 things:
- Add
client_secret_postas an advertised auth method. Previously I was only advertisingclient_secret_basic(even though it was supported by the token endpoint) but Claude usesclient_secret_postand posts client credentials on the token request body, not in the Authorization header. - I had a bug where the
WWW-Authenticateheader always usedhttpfor theresource_metadataURL. Every other client I've used seems to "upgrade" it to https for me but Claude didn't and I hadn't noticed that until now. :)
If you are providing Dynamic Client Registration and sending empty or null values in the registration response, such as logo_url: null, then it will silently fail. You should be omitting all the fields that are not applicable or not there.
Infact, it does not even send the resource parameter in the authorization request as per their own spec - https://modelcontextprotocol.io/specification/draft/basic/authorization#resource-parameter-implementation
I have fixed the Dynamic Client Registration doing this things: (it now connects correctly).
- WWW-Authenticate Header
- .well-known/oauth-protected-resource returns http status code=401 and header www-authenticate: Bearer realm="mcp", resource_metadata="https://mymcphost.com/.well-known/oauth-protected-resource"
- client_secret_post in
token_endpoint_auth_methods_supported - in
registration_endpointresponse must have this info.
{
"client_id": "...",
"client_secret": "...",
"redirect_uris": ["https://claude.ai/api/mcp/auth_callback"],
"token_endpoint_auth_method": "client_secret_post", // ← same client_secret_post
"grant_types": ["authorization_code", "password"], // ← password grant
"registration_client_uri": "https://...", // ← always a HTTPS
"authorization_endpoint": "https://.../auth", // ← always https
"token_endpoint": "https://.../token", // ← always https
"jwks_uri": "https://.../certs", // ← always https
"issuer": "https://..." // ← always https
}
this is a claude request sent toregistration_endpoint
{
"redirect_uris": [
"https://claude.ai/api/mcp/auth_callback"
],
"token_endpoint_auth_method": "client_secret_post",
"grant_types": [
"authorization_code",
"refresh_token"
],
"response_types": [
"code"
],
"scope": "service_account roles openid email offline_access phone basic acr groups mcp:read microprofile-jwt profile mcp:execute mcp:write address web-origins",
"client_name": "Claude"
}
- in .well-known/oauth-authorization-server i put
{
...
"mcp_extensions":{
"client_id":"client-id",
"discovery_service":{
"keycloak_backend":"<url>",
"note":"Dynamic client registration intermediary on HTTP Streaming service",
"registration_endpoint":"<endpoint>",
"registration_service":"<url_registration>"
},
"http_streaming_auth_required":true,
"http_streaming_endpoint":"<mcp_url>",
"http_streaming_session_timeout":600,
"supported_transports":[
"http_streaming"
],
"version":"2025-06-18"
}
...
}
`
Define your static credentials here (or pull from env vars for security)
You will provide these values to Claude.ai
STATIC_CLIENT_ID = os.getenv("OAUTH_CLIENT_ID", "mcp-obsidian-client") STATIC_CLIENT_SECRET = os.getenv("OAUTH_CLIENT_SECRET", "super-secret-obsidian-key-change-this")
-----------------------------
OAuth storage
We pre-populate the allowed client immediately
oauth_clients: Dict[str, dict] = { STATIC_CLIENT_ID: { "client_id": STATIC_CLIENT_ID, "client_secret": STATIC_CLIENT_SECRET, # You might need to adjust this based on where Claude is redirecting "redirect_uris": ["https://claude.ai/oauth/callback", "http://localhost:8080/callback"], "client_name": "Claude" } } oauth_codes: Dict[str, dict] = {} oauth_tokens: Dict[str, dict] = {}
@app.post("/register") async def register_client(request: Request): """ Dynamic Registration is disabled/modified to return the static credentials. This ensures only your pre-defined ID/Secret works. """ body = await request.json()
# We ignore the request to create new credentials and return our static ones
return JSONResponse({
"client_id": STATIC_CLIENT_ID,
"client_secret": STATIC_CLIENT_SECRET,
"client_id_issued_at": 1234567890,
"redirect_uris": body.get("redirect_uris", [])
})
@app.get("/authorize") async def authorize( request: Request, response_type: str = None, client_id: str = None, redirect_uri: str = None, state: str = None, code_challenge: str = None, code_challenge_method: str = None ): """OAuth Authorization Endpoint""" # Check if the requested Client ID matches our static one if client_id != STATIC_CLIENT_ID: return HTMLResponse(f"
Invalid client: {client_id}
", status_code=400)# Auto-approve (skip user consent for simplicity)
code = secrets.token_urlsafe(32)
oauth_codes[code] = {
"client_id": client_id,
"redirect_uri": redirect_uri,
"code_challenge": code_challenge,
"code_challenge_method": code_challenge_method
}
logger.info(f"Issued authorization code for client {client_id}")
# Redirect back to Claude with the code
separator = "&" if "?" in redirect_uri else "?"
redirect_url = f"{redirect_uri}{separator}code={code}"
if state:
redirect_url += f"&state={state}"
return RedirectResponse(redirect_url)
@app.post("/token") async def token_endpoint( request: Request, grant_type: str = Form(None), code: str = Form(None), redirect_uri: str = Form(None), client_id: str = Form(None), client_secret: str = Form(None), code_verifier: str = Form(None) ): """OAuth Token Endpoint""" if grant_type != "authorization_code": return JSONResponse({"error": "unsupported_grant_type"}, status_code=400)
if code not in oauth_codes:
return JSONResponse({"error": "invalid_grant"}, status_code=400)
code_data = oauth_codes[code]
# 1. Validate Client ID
if code_data["client_id"] != client_id:
return JSONResponse({"error": "invalid_client_id"}, status_code=400)
# 2. Validate Client Secret (ADDED THIS CHECK)
# We check against our stored static client
stored_client = oauth_clients.get(client_id)
if not stored_client or stored_client["client_secret"] != client_secret:
logger.warning(f"Failed secret check for client {client_id}")
return JSONResponse({"error": "invalid_client_secret"}, status_code=401)
# Generate access token
access_token = secrets.token_urlsafe(32)
oauth_tokens[access_token] = {
"client_id": client_id,
"scope": "mcp"
}
# Clean up used code
del oauth_codes[code]
logger.info(f"Issued access token for client {client_id}")
return JSONResponse({
"access_token": access_token,
"token_type": "Bearer",
"expires_in": 3600
})`
This Works