claude-code icon indicating copy to clipboard operation
claude-code copied to clipboard

[BUG] Claude Desktop doesn't connect to Custom MCPs altogether (not with OAuth 2.1 nor with SSE)

Open jakub-nezasa opened this issue 4 months ago • 42 comments

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.

jakub-nezasa avatar Aug 15 '25 08:08 jakub-nezasa

Found 3 possible duplicate issues:

  1. https://github.com/anthropics/claude-code/issues/3515
  2. https://github.com/anthropics/claude-code/issues/3140
  3. 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

github-actions[bot] avatar Aug 15 '25 09:08 github-actions[bot]

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

jakub-nezasa avatar Aug 15 '25 14:08 jakub-nezasa

I am also facing the same issue. Is there any workaround for it?

sanidhya-p avatar Aug 21 '25 06:08 sanidhya-p

I also faced exactly the same issue

dnjscksdn98 avatar Sep 08 '25 05:09 dnjscksdn98

I also faced exactly the same issue

AlexMobiCraft avatar Sep 09 '25 14:09 AlexMobiCraft

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.

anyoung-tableau avatar Sep 10 '25 02:09 anyoung-tableau

I solved the issue by allowing anthropic's outbound IP addresses to my WAF firewall

  • https://docs.anthropic.com/en/api/ip-addresses

dnjscksdn98 avatar Sep 12 '25 01:09 dnjscksdn98

Also facing this exact issue - any updates? Works perfectly in Claude Code 👍

EDjur avatar Oct 03 '25 13:10 EDjur

Glad that I'm not the only one seeing this.

Debugged it through and through. I'll give a stab at Claude Code. Thanks!

daukadolt avatar Oct 05 '25 21:10 daukadolt

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.

EDjur avatar Oct 07 '25 07:10 EDjur

+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 rmcp library
  • Transport: SSE (Server-Sent Events)
  • Authentication: OAuth 2.0 with Dynamic Client Registration (RFC 7591)
  • URL Pattern: https://{uuid}.saramcp.com

What Works ✅

  1. MCP Inspector Connection

    • OAuth discovery: ✅ PASS
    • Client registration: ✅ PASS
    • Authorization flow: ✅ PASS
    • Token exchange: ✅ PASS
    • SSE connection: ✅ PASS
    • initialize request: ✅ PASS
    • tools/list request: ✅ PASS (returns 2 tools)
  2. OAuth Implementation

    • All .well-known endpoints 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
  3. 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

  1. Click "Add Custom Connector" with URL https://{uuid}.saramcp.com
  2. OAuth browser window opens
  3. User logs in successfully
  4. Authorization granted
  5. Browser redirects back to claude.ai/new
  6. Connector disappears from settings (not shown as connected or disconnected)
  7. No error messages displayed to user
  8. 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)

  1. ✅ Added response_modes_supported: ["query"] to auth server metadata
  2. ✅ Added resource field per RFC 9728
  3. ✅ Removed resource field to match working servers
  4. ✅ Tested different OAuth scopes
  5. ✅ Verified CORS headers on all endpoints
  6. ✅ Tested with public/organization/private access levels
  7. ✅ Checked token expiration times (3600s)
  8. ✅ Verified redirect URIs match exactly
  9. ✅ Tested SSE endpoints directly (work perfectly)
  10. ✅ 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 endpoint
  • POST /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:

  1. OAuth proxy at claude.ai completes authentication ✅
  2. OAuth proxy receives access token ✅
  3. OAuth proxy should establish SSE connection to custom server
  4. 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:

  1. Log when OAuth completes successfully
  2. Log when attempting to establish SSE connection
  3. Log the exact URL being used for SSE
  4. Log any errors during SSE connection setup
  5. 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.

jhiver avatar Oct 08 '25 00:10 jhiver

I also faced exactly the same issue

byosamah avatar Oct 09 '25 06:10 byosamah

Same issue here

alfranz avatar Oct 19 '25 09:10 alfranz

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?

itspers avatar Oct 21 '25 22:10 itspers

Facing same issue

CodeLuca avatar Oct 22 '25 22:10 CodeLuca

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.

twchad avatar Oct 23 '25 19:10 twchad

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.

twchad avatar Oct 23 '25 21:10 twchad

Any workarounds?

viniFiedler avatar Oct 31 '25 14:10 viniFiedler

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.

twchad avatar Nov 03 '25 00:11 twchad

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.

sebosamet avatar Nov 04 '25 17:11 sebosamet

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..

itspers avatar Nov 05 '25 22:11 itspers

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.

sebosamet avatar Nov 08 '25 18:11 sebosamet

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.

twchad avatar Nov 13 '25 06:11 twchad

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

biswapm avatar Nov 14 '25 12:11 biswapm

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.

sebosamet avatar Nov 14 '25 13:11 sebosamet

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.

twchad avatar Nov 15 '25 03:11 twchad

I was able to resolve the issue I was having by doing 2 things:

  1. Add client_secret_post as an advertised auth method. Previously I was only advertising client_secret_basic (even though it was supported by the token endpoint) but Claude uses client_secret_post and posts client credentials on the token request body, not in the Authorization header.
  2. I had a bug where the WWW-Authenticate header always used http for the resource_metadata URL. 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. :)

anyoung-tableau avatar Nov 19 '25 20:11 anyoung-tableau

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

gplhegde avatar Nov 21 '25 09:11 gplhegde

I have fixed the Dynamic Client Registration doing this things: (it now connects correctly).

  1. WWW-Authenticate Header
  2. .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"
  3. client_secret_post in token_endpoint_auth_methods_supported
  4. in registration_endpoint response 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"
}
  1. 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"
   }
...
}

Jorgevillada avatar Nov 22 '25 04:11 Jorgevillada

`

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

Nihilentropy-117 avatar Nov 24 '25 04:11 Nihilentropy-117