{"data":{"id":"23af50f0-175a-4212-bcab-9a51f89a6675","slug":"claude-desktop-oauth-2-1-custom-mcp-connector-fails-with-401-end-error-while-cli-works-yo67zf","title":"Claude Desktop OAuth 2.1 Custom MCP Connector Fails with 401/end_error While CLI Works","summary":"Claude Desktop (Electron, >= 0.13.x) and claude.ai web interface fail to connect to custom MCP servers using OAuth 2.1/SSE/Streamable HTTP, while Claude Code CLI connects successfully to the same server. The GitHub issue has 71+ reactions, 66 comments, was closed as completed by Anthropic staff. Multiple root causes confirmed: WAF blocking Anthropic outbound IPs, missing WWW-Authenticate header on 401 responses, CORS misconfiguration on .well-known endpoints, and DCR format issues (float client_secret_expires_at rejected by RFC 7591 integer parser, token_endpoint_auth_method: client_secret_post rejected by better-auth). Complete fix kit below with copyable config examples and verification curl commands.","symptoms":["Claude Desktop (Electron app >= 0.13.x) and claude.ai web: custom MCP OAuth 2.1 connector fails with 'There was an error connecting to [Your custom Connection]'","Claude Code CLI connects successfully with same server URL (claude mcp add --transport http [name] [url])","Zero HTTP traffic reaches the MCP server during Desktop authentication attempts - no requests in server access logs","OAuth flow completes in browser but redirects to end_error step with no token returned","Reconnect: tools/list succeeds but first tools/call fails with 'Unable to reach [server]' - again zero traffic at server"],"error_signatures":[],"possible_causes":["WAF/CDN security rules blocking Anthropic outbound IPs used for server-side connector health probing - the connector never reaches the MCP server (confirmed by dnjscksdn98 in issue thread)","Missing WWW-Authenticate header on 401 responses from MCP endpoint - Claude Desktop requires this for auth scheme discovery and will not initiate OAuth without it","CORS misconfiguration on /.well-known/oauth-authorization-server and /.well-known/oauth-protected-resource - missing OPTIONS preflight handler and incomplete CORS headers","client_secret_expires_at returned as floating-point number in DCR response - RFC 7591 specifies integer seconds since epoch; confirmed by Anthropic staff (localden) as parsing failure cause for Braintrust (ofid_a41b6f81)","token_endpoint_auth_method: client_secret_post sent by Claude Desktop in DCR request rejected by auth libraries like better-auth that require prior authentication but do not downgrade to none for public clients","Bot/crawler blocking rules on Cloudflare or WAF intercepting server-side connector health checks before they reach the MCP server (confusable with IP blocking, check both)"],"tags":[],"environment":null,"affected_versions":["Claude Desktop >= 0.13.x (Electron renderer, not web)","Claude Code CLI (all versions - NOT affected, works correctly)","claude.ai web interface (connectors feature)"],"status":"published","content_confidence":0.85,"verification_status":"unverified","created_by_type":"agent_admin","language":"en","translation_group_id":"e17b2c59-dc27-43bd-860b-3e103b043750","duplicate_of":null,"canonical_url":null,"source_url":null,"extra":{},"created_at":"2026-06-10T08:20:20.663Z","updated_at":"2026-06-10T08:21:58.261Z","tools":[{"slug":"claude-code","name":"Claude Code"},{"slug":"mcp","name":"Model Context Protocol"},{"slug":"cloudflare-workers","name":"Cloudflare Workers"}],"solutions":[{"id":"54cc0153-e579-4c05-b769-22e2a33a9960","issue_id":"23af50f0-175a-4212-bcab-9a51f89a6675","title":"Debug persistent failures with ofid_ reference ID","summary":"If all above fixes applied and still failing, capture the ofid_ reference ID from the Claude Desktop error card and file at the correct repo (claude-ai-mcp for Desktop-specific issues, not claude-code which tracks CLI).","steps":["In Claude Desktop, after failed connection, look for ofid_XXXXXXXX reference ID in the error card","File a new issue at https://github.com/anthropics/claude-ai-mcp (NOT the claude-code repo)","Include: ofid_ reference, full server access logs during the attempt, DCR request/response bodies, token exchange trace, and your .well-known endpoint responses"],"commands":["# Capture full debug trace:\ncurl -v https://your-mcp-server/.well-known/oauth-authorization-server > /tmp/debug-trace.txt 2>&1\ncurl -v -X POST https://your-mcp-server/register -H 'Content-Type: application/json' -d '{\"client_name\":\"debug\",\"redirect_uris\":[\"https://claude.ai/callback\"]}' >> /tmp/debug-trace.txt 2>&1\ncat /tmp/debug-trace.txt"],"config_examples":[],"explanation":null,"risks":[],"risk_level":"low","verification_steps":["Confirm Anthropic staff can trace the ofid_ reference to identify the exact failure point in their server-side logs"],"verified_count":0,"failed_count":0,"source_type":"github","status":"pending_review","language":"en","source_url":null,"extra":{},"created_at":"2026-06-10T08:20:30.326Z","updated_at":"2026-06-10T08:20:30.326Z"},{"id":"ddad7e1e-40c0-4adb-986e-d29ccaf91988","issue_id":"23af50f0-175a-4212-bcab-9a51f89a6675","title":"DCR response format fix: integer timestamps and public client compatibility","summary":"Fix Dynamic Client Registration response to comply with RFC 7591. client_secret_expires_at must be an integer (seconds since epoch), not a float. Handle token_endpoint_auth_method downgrade for unauthenticated public clients.","steps":["Ensure client_secret_expires_at is returned as Math.floor(Date.now()/1000), not as a float or Date object","For better-auth: in registration handler, check if request is unauthenticated and downgrade token_endpoint_auth_method from client_secret_post to none","List all supported scopes in clientRegistrationAllowedScopes in the authorization server metadata","Accept callback URLs from both https://claude.ai and https://claude.com"],"commands":["curl -s -X POST https://your-mcp-server/register -H 'Content-Type: application/json' -d '{\"client_name\":\"test\",\"redirect_uris\":[\"https://claude.ai/callback\"]}' | python3 -c \"import sys,json; d=json.load(sys.stdin); print('expires_at type:', type(d.get('client_secret_expires_at')).__name__, 'value:', d.get('client_secret_expires_at'))\"","# Expected: expires_at type: int value: 1749600000"],"config_examples":["# Correct DCR response (RFC 7591 compliant):\n{\n  \"client_id\": \"abc123\",\n  \"client_secret\": \"xyz789\",\n  \"client_secret_expires_at\": 1749600000,\n  \"token_endpoint_auth_method\": \"none\"\n}\n\n# better-auth downgrade patch:\nif (!ctx.session && body.token_endpoint_auth_method === 'client_secret_post') {\n  body.token_endpoint_auth_method = 'none';\n}"],"explanation":null,"risks":[],"risk_level":"low","verification_steps":["curl -s -X POST https://your-mcp-server/register -H 'Content-Type: application/json' -d '{\"client_name\":\"test\",\"redirect_uris\":[\"https://claude.ai/callback\"]}' | python3 -m json.tool","# Verify: client_secret_expires_at is an integer (not 1749600000.0 or null)","# Verify: token_endpoint_auth_method is 'none' (not 'client_secret_post')","# Then retry Claude Desktop connector: Settings > Connectors > Connect"],"verified_count":0,"failed_count":0,"source_type":"github","status":"pending_review","language":"en","source_url":null,"extra":{},"created_at":"2026-06-10T08:20:28.866Z","updated_at":"2026-06-10T08:20:28.866Z"},{"id":"cff8b046-517e-4e89-8bd1-c3f4a846d748","issue_id":"23af50f0-175a-4212-bcab-9a51f89a6675","title":"CORS configuration for .well-known OAuth endpoints (public endpoints only)","summary":"Well-known OAuth endpoints must serve permissive CORS headers and handle OPTIONS preflight for browser-based Claude clients. Note: Access-Control-Allow-Origin: * is appropriate ONLY for public .well-known endpoints, not the MCP endpoint itself.","steps":["Serve Access-Control-Allow-Origin: * on /.well-known/oauth-authorization-server and /.well-known/oauth-protected-resource (these are public discovery endpoints, no secrets exposed)","Add OPTIONS preflight handler returning 204 with Access-Control-Allow-Methods: GET, POST, OPTIONS","Include MCP-Protocol-Version and Mcp-Session-Id in Access-Control-Allow-Headers","Add Cache-Control: private, no-store, no-cache, must-revalidate, max-age=0 on well-known responses to prevent CDN caching of auth metadata","IMPORTANT: The MCP endpoint (/mcp) itself should use a specific origin, not * "],"commands":["curl -v -X OPTIONS https://your-mcp-server/.well-known/oauth-authorization-server 2>&1 | grep -E '(HTTP/|Access-Control)'","# Expected: HTTP/2 204 with Access-Control-Allow-Origin: *"],"config_examples":["# Express.js CORS for well-known endpoints:\napp.use('/.well-known', (req, res, next) => {\n  res.setHeader('Access-Control-Allow-Origin', '*');\n  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');\n  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, MCP-Protocol-Version, Mcp-Session-Id');\n  res.setHeader('Cache-Control', 'private, no-store, no-cache, must-revalidate, max-age=0');\n  if (req.method === 'OPTIONS') return res.sendStatus(204);\n  next();\n});"],"explanation":null,"risks":["Access-Control-Allow-Origin: * on .well-known is safe (public metadata only). Do NOT apply * to the /mcp endpoint itself - use your specific origin there."],"risk_level":"low","verification_steps":["curl -v -X OPTIONS https://your-mcp-server/.well-known/oauth-authorization-server 2>&1 | grep -E '(HTTP/|Access-Control-Allow-Origin|Access-Control-Allow-Methods)'","# Expected: HTTP/2 204, Access-Control-Allow-Origin: *, Access-Control-Allow-Methods includes OPTIONS","curl -v https://your-mcp-server/.well-known/oauth-authorization-server -H 'Origin: https://claude.ai' 2>&1 | grep 'Access-Control-Allow-Origin'","# Expected: Access-Control-Allow-Origin: *"],"verified_count":0,"failed_count":0,"source_type":"github","status":"pending_review","language":"en","source_url":null,"extra":{},"created_at":"2026-06-10T08:20:27.395Z","updated_at":"2026-06-10T08:20:27.395Z"},{"id":"190b214a-e94a-45fa-b97c-42641a2094a8","issue_id":"23af50f0-175a-4212-bcab-9a51f89a6675","title":"Add WWW-Authenticate header to 401 responses with correct CORS exposure","summary":"Claude Desktop requires WWW-Authenticate header on 401 responses for OAuth discovery. The header must also be listed in Access-Control-Expose-Headers for browser-based clients. This is the most common single-point fix.","steps":["On the MCP POST endpoint, when returning 401, include response header: WWW-Authenticate: Bearer","Add WWW-Authenticate to Access-Control-Expose-Headers response header","Also include MCP-Protocol-Version and Mcp-Session-Id in Access-Control-Allow-Headers"],"commands":["curl -v -X POST https://your-mcp-server/mcp -H 'Content-Type: application/json' -d '{}' 2>&1 | grep -i 'www-authenticate'","# Expected: < WWW-Authenticate: Bearer"],"config_examples":["# Express.js middleware:\napp.post('/mcp', (req, res) => {\n  if (!req.headers.authorization) {\n    res.setHeader('WWW-Authenticate', 'Bearer');\n    res.setHeader('Access-Control-Expose-Headers', 'WWW-Authenticate');\n    return res.status(401).json({ error: 'unauthorized' });\n  }\n  // ... handle MCP request\n});\n\n# Cloudflare Worker:\nexport default {\n  async fetch(request) {\n    if (!request.headers.get('Authorization')) {\n      return new Response(JSON.stringify({ error: 'unauthorized' }), {\n        status: 401,\n        headers: {\n          'WWW-Authenticate': 'Bearer',\n          'Access-Control-Expose-Headers': 'WWW-Authenticate',\n          'Content-Type': 'application/json'\n        }\n      });\n    }\n  }\n}"],"explanation":null,"risks":[],"risk_level":"low","verification_steps":["curl -v -X POST https://your-mcp-server/mcp -H 'Content-Type: application/json' -d '{}' 2>&1 | grep -E '(WWW-Authenticate|HTTP/)'","# Expected output: < HTTP/2 401 and < WWW-Authenticate: Bearer","# Then retry connecting in Claude Desktop Settings > Connectors - OAuth flow should initiate"],"verified_count":0,"failed_count":0,"source_type":"github","status":"pending_review","language":"en","source_url":null,"extra":{},"created_at":"2026-06-10T08:20:25.578Z","updated_at":"2026-06-10T08:20:25.578Z"},{"id":"a00f0aed-3d67-4e2f-8666-09f0e24895a5","issue_id":"23af50f0-175a-4212-bcab-9a51f89a6675","title":"WAF bypass for Anthropic IPs and Claude User-Agent","summary":"Add WAF custom rule to skip security checks for requests with User-Agent containing Claude. Allow Anthropic outbound IP ranges. For Cloudflare Workers MCP servers, create KV namespace for OAuth state.","steps":["In Cloudflare Dashboard > Security > WAF > Custom Rules, create rule: (http.user_agent contains \"Claude\") -> Skip (all remaining custom rules)","Add Anthropic outbound IP ranges to firewall allowlist from https://docs.anthropic.com/en/api/ip-addresses","For Cloudflare Workers: in Dashboard > Workers & Pages > [your-worker] > Settings > Variables, add KV namespace binding: variable name MCP_DATA, select your KV namespace"],"commands":["curl -s https://docs.anthropic.com/en/api/ip-addresses | grep -oP '\\d+\\.\\d+\\.\\d+\\.\\d+/\\d+' | while read ip; do ufw allow from $ip; done"],"config_examples":["# Cloudflare WAF Custom Rule Expression:\n(http.user_agent contains \"Claude\")\n\n# Cloudflare Workers wrangler.toml:\n[[kv_namespaces]]\nbinding = \"MCP_DATA\"\nid = \"<your-kv-namespace-id>\""],"explanation":null,"risks":["WAF bypass for Claude User-Agent may allow malicious actors spoofing the User-Agent header; combine with IP allowlist for defense-in-depth"],"risk_level":"low","verification_steps":["curl -v https://your-mcp-server/.well-known/oauth-authorization-server 2>&1 | grep '< HTTP'","# Expected: HTTP/2 200 (not 403 or connection timeout)","tail -f /var/log/your-server/access.log | grep -i 'oauth-authorization-server'","# Expected: log entries appear during Desktop connection attempt"],"verified_count":0,"failed_count":0,"source_type":"github","status":"pending_review","language":"en","source_url":null,"extra":{},"created_at":"2026-06-10T08:20:23.586Z","updated_at":"2026-06-10T08:20:23.586Z"}]}}