Skip to content

Claude.ai ignores authorization_endpoint and token_endpoint from OAuth metadata when MCP server uses external authorization server #82

Description

@johnan319

MCP server URL

{{internal company URL}}

Where does the issue occur?

When pressing the Connect button

Transport used

streamable-http

Client registration type

Dynamic Client Registration (DCR)

SSE only: Does your server return the expected status code for POST requests?

Not applicable (my server does not use SSE)

When did you last reproduce this?

2026-03-04 10:00am AEST

Browser and OS

ARC

Describe the issue

Summary

Claude.ai's MCP OAuth client fetches /.well-known/oauth-authorization-server metadata successfully but ignores the authorization_endpoint and token_endpoint values. Instead, it constructs /authorize and /token URLs from the MCP server's base URL. It also strips the path from registration_endpoint, hitting /register instead of the advertised /mcp/oauth/register.

This means any MCP server using an external authorization server (FusionAuth, Keycloak, Okta, Auth0, Entra ID, etc.) fails to authenticate with Claude.ai, even though the same server works correctly with ChatGPT and Claude Code CLI.

Environment

  • MCP server: Custom Express.js server at {{internal company URL}}
  • Authorization server: FusionAuth at https://{{company auth server}}
  • MCP spec version: Implements RFC 9728 (Protected Resource Metadata) + RFC 8414 (Authorization Server Metadata) + RFC 7591 (Dynamic Client Registration)
  • Claude.ai user-agent: python-httpx/0.28.1 (backend), Claude-User (MCP calls post-auth)
  • Date tested: 2026-03-04

The Three Bugs

Bug 1: registration_endpoint path is stripped

Metadata advertises:

registration_endpoint: "https://<server>/mcp/oauth/register"

Claude.ai hits:

POST /register  → 404

Expected:

POST /mcp/oauth/register  → 201

Bug 2: authorization_endpoint is ignored

Metadata advertises:

authorization_endpoint: "https://{{company auth server}}/oauth2/authorize"

Claude.ai constructs:

GET https://<server>/authorize?response_type=code&client_id=...

Expected:

GET https://{{company auth server}}/oauth2/authorize?response_type=code&client_id=...

Bug 3: token_endpoint is ignored

Metadata advertises:

token_endpoint: "https://{{company auth server}}/oauth2/token"

Claude.ai constructs:

POST https://<server>/token

Expected:

POST https://{{company auth server}}/oauth2/token

Server Logs: Claude.ai vs ChatGPT

Claude.ai flow (fails without workarounds)

# Step 1: Initial MCP request → 401 (correct)
POST /mcp  UA: python-httpx/0.28.1  → 401

# Step 2: Fetches protected resource metadata (correct)
GET /.well-known/oauth-protected-resource/mcp  → 200
  Response: {
    "resource": "https://<server>/mcp",
    "authorization_servers": ["https://<server>"],
    "scopes_supported": ["mcp:read","mcp:write","mcp:admin"],
    "bearer_methods_supported": ["header"]
  }

# Step 3: Fetches authorization server metadata (correct)
GET /.well-known/oauth-authorization-server  → 200
  Response: {
    "issuer": "....com",
    "authorization_endpoint": "https://{{company auth server}}/oauth2/authorize",  ← IGNORED
    "token_endpoint": "https://{{company auth server}}/oauth2/token",              ← IGNORED
    "registration_endpoint": "https://<server>/mcp/oauth/register",          ← PATH STRIPPED
    "response_types_supported": ["code"],
    "grant_types_supported": ["authorization_code","refresh_token"],
    "code_challenge_methods_supported": ["S256"],
    ...
  }

# Step 4: OpenID fallback (expected, fails gracefully)
GET /.well-known/openid-configuration  → 404

# Step 5: BUG — Hits /register instead of /mcp/oauth/register
POST /register  → 404  ← SHOULD BE POST /mcp/oauth/register

# Step 6: BUG — Hits /authorize on MCP server instead of {{company auth server}}
GET /authorize?response_type=code&client_id=...  → 404  ← SHOULD GO TO {{company auth server}}

# Step 7: BUG — Hits /token on MCP server instead of {{company auth server}}
POST /token  → 404  ← SHOULD GO TO {{company auth server}}

ChatGPT flow (works correctly)

# Step 1: Initial MCP request → 401 (same)
POST /mcp  UA: Python/3.12 aiohttp/3.13.3  → 401

# Step 2: Fetches protected resource metadata (same)
GET /.well-known/oauth-protected-resource/mcp  → 200

# Step 3: Fetches authorization server metadata (same)
GET /.well-known/oauth-authorization-server  → 200

# Step 4: OpenID fallback (same)
GET /.well-known/openid-configuration  → 404

# Step 5: CORRECT — Uses full registration_endpoint from metadata
POST /mcp/oauth/register  → 201  ✅
  Body: {"client_name":"ChatGPT","redirect_uris":["https://chatgpt.com/connector/oauth/..."],...}

# Step 6: CORRECT — Redirects user to authorization_endpoint from metadata
# (Browser goes directly to https://{{company auth server}}/oauth2/authorize)  ✅

# Step 7: CORRECT — Exchanges code at token_endpoint from metadata
# (POST to https://{{company auth server}}/oauth2/token)  ✅

# Step 8: Authenticated MCP calls succeed
POST /mcp  UA: openai-mcp/1.0.0 (ChatGPT)  → 200  ✅

Claude Code CLI flow (also works)

Claude Code CLI (Bun/1.3.11) correctly uses the full registration_endpoint path and follows the metadata endpoints. User-agent switches to claude-code/2.1.66 (cli) for MCP calls after authentication.

Root Cause Analysis

Claude.ai appears to still implement the old MCP authorization spec (2025-03-26) which states:

"The authorization base URL MUST be determined from the MCP server URL by discarding any existing path component."

The current MCP spec (2025-06-18+) introduced RFC 9728 support, which allows the authorization server to be a completely separate entity from the MCP server. Clients should read authorization_endpoint and token_endpoint from the metadata and use them directly.

ChatGPT and Claude Code CLI follow the current spec. Claude.ai (the web client) does not.

Three Anthropic Clients, Three Behaviors

Behavior Claude.ai (web) Claude Code CLI ChatGPT
Fetches /.well-known/oauth-authorization-server Yes Yes Yes
Uses authorization_endpoint from metadata No — appends /authorize to server base URL Yes Yes
Uses token_endpoint from metadata No — appends /token to server base URL Yes Yes
Uses full registration_endpoint path No — strips to /register Yes Yes
OAuth user-agent python-httpx/0.28.1 Bun/1.3.11 Python/3.12 aiohttp/3.13.3
MCP user-agent Claude-User claude-code/2.1.66 (cli) openai-mcp/1.0.0 (ChatGPT)

Impact

Any MCP server that delegates authentication to an external identity provider (FusionAuth, Keycloak, Okta, Auth0, Azure Entra ID, AWS Cognito, etc.) cannot work with Claude.ai without implementing proxy routes on the MCP server itself.

This is the standard architecture for production applications — very few applications embed their own authorization server.

Workaround

The only workaround is to add proxy routes at the MCP server's domain root:

  1. POST /register → mount the registration handler at root (Claude strips /mcp/oauth prefix)
  2. GET /authorize → 302 redirect to authorization_endpoint with all query params forwarded
  3. POST /token → proxy request body to token_endpoint, return the response

This is the same pattern used by Cloudflare's workers-oauth-provider and FastMCP's OAuth proxy.

Related Issues

Expected Fix

Claude.ai's MCP OAuth client should:

  1. Read authorization_endpoint from /.well-known/oauth-authorization-server metadata and use it as-is for the authorization redirect
  2. Read token_endpoint from the same metadata and use it as-is for token exchange
  3. Use the full registration_endpoint URL from metadata without stripping path components

This would bring Claude.ai in line with the current MCP spec, RFC 8414, RFC 9728, and the behavior of ChatGPT and Claude Code CLI.

Issue details

Expected behavior

Not strip the parent path from the auth URL

Logs from your server

Additional context

No response

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions