KBCodeKB

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

risk: lowgithubpending_review

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

  1. In Claude Desktop, after failed connection, look for ofid_XXXXXXXX reference ID in the error card
  2. File a new issue at https://github.com/anthropics/claude-ai-mcp (NOT the claude-code repo)
  3. 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.txt

Verification

  • Confirm Anthropic staff can trace the ofid_ reference to identify the exact failure point in their server-side logs
0 verified0 failed

DCR response format fix: integer timestamps and public client compatibility

risk: lowgithubpending_review

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.

  1. Ensure client_secret_expires_at is returned as Math.floor(Date.now()/1000), not as a float or Date object
  2. For better-auth: in registration handler, check if request is unauthenticated and downgrade token_endpoint_auth_method from client_secret_post to none
  3. List all supported scopes in clientRegistrationAllowedScopes in the authorization server metadata
  4. 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
0 verified0 failed

CORS configuration for .well-known OAuth endpoints (public endpoints only)

risk: lowgithubpending_review

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.

  1. 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)
  2. Add OPTIONS preflight handler returning 204 with Access-Control-Allow-Methods: GET, POST, OPTIONS
  3. Include MCP-Protocol-Version and Mcp-Session-Id in Access-Control-Allow-Headers
  4. Add Cache-Control: private, no-store, no-cache, must-revalidate, max-age=0 on well-known responses to prevent CDN caching of auth metadata
  5. 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: *
0 verified0 failed

Add WWW-Authenticate header to 401 responses with correct CORS exposure

risk: lowgithubpending_review

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.

  1. On the MCP POST endpoint, when returning 401, include response header: WWW-Authenticate: Bearer
  2. Add WWW-Authenticate to Access-Control-Expose-Headers response header
  3. 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
0 verified0 failed

WAF bypass for Anthropic IPs and Claude User-Agent

risk: lowgithubpending_review

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.

  1. In Cloudflare Dashboard > Security > WAF > Custom Rules, create rule: (http.user_agent contains "Claude") -> Skip (all remaining custom rules)
  2. Add Anthropic outbound IP ranges to firewall allowlist from https://docs.anthropic.com/en/api/ip-addresses
  3. 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
0 verified0 failed

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"
}
← Back to all issuesPowered by CodeKB