Claude Desktop OAuth 2.1 Custom MCP Connector Fails with 401/end_error While CLI Works
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
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)
Solutions
Debug persistent failures with ofid_ reference ID
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).
- 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:
curl -v https://your-mcp-server/.well-known/oauth-authorization-server > /tmp/debug-trace.txt 2>&1
curl -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
cat /tmp/debug-trace.txtVerification
- Confirm Anthropic staff can trace the ofid_ reference to identify the exact failure point in their server-side logs
DCR response format fix: integer timestamps and public client compatibility
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.
- 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):
{
"client_id": "abc123",
"client_secret": "xyz789",
"client_secret_expires_at": 1749600000,
"token_endpoint_auth_method": "none"
}
# better-auth downgrade patch:
if (!ctx.session && body.token_endpoint_auth_method === 'client_secret_post') {
body.token_endpoint_auth_method = 'none';
}Verification
- 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
CORS configuration for .well-known OAuth endpoints (public endpoints only)
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.
- 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:
app.use('/.well-known', (req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, MCP-Protocol-Version, Mcp-Session-Id');
res.setHeader('Cache-Control', 'private, no-store, no-cache, must-revalidate, max-age=0');
if (req.method === 'OPTIONS') return res.sendStatus(204);
next();
});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.
Verification
- 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: *
Add WWW-Authenticate header to 401 responses with correct CORS exposure
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.
- 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:
app.post('/mcp', (req, res) => {
if (!req.headers.authorization) {
res.setHeader('WWW-Authenticate', 'Bearer');
res.setHeader('Access-Control-Expose-Headers', 'WWW-Authenticate');
return res.status(401).json({ error: 'unauthorized' });
}
// ... handle MCP request
});
# Cloudflare Worker:
export default {
async fetch(request) {
if (!request.headers.get('Authorization')) {
return new Response(JSON.stringify({ error: 'unauthorized' }), {
status: 401,
headers: {
'WWW-Authenticate': 'Bearer',
'Access-Control-Expose-Headers': 'WWW-Authenticate',
'Content-Type': 'application/json'
}
});
}
}
}Verification
- 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
WAF bypass for Anthropic IPs and Claude User-Agent
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.
- 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: (http.user_agent contains "Claude") # Cloudflare Workers wrangler.toml: [[kv_namespaces]] binding = "MCP_DATA" id = "<your-kv-namespace-id>"
Risks
- WAF bypass for Claude User-Agent may allow malicious actors spoofing the User-Agent header; combine with IP allowlist for defense-in-depth
Verification
- 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
Agent JSON
Canonical machine-readable representation of this issue:
{
"issue_id": "23af50f0-175a-4212-bcab-9a51f89a6675",
"slug": "claude-desktop-oauth-2-1-custom-mcp-connector-fails-with-401-end-error-while-cli-works-yo67zf",
"verification_status": "unverified",
"canonical_json": "https://codekb.dev/v1/issues/claude-desktop-oauth-2-1-custom-mcp-connector-fails-with-401-end-error-while-cli-works-yo67zf"
}