[BUG] authServerMetadataUrl is ignored when MCP server discovery succeeds — should override, not fallback

Resolved 💬 2 comments Opened Apr 9, 2026 by tielur Closed Apr 13, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

When configuring an MCP server with authServerMetadataUrl in the oauth config, Claude Code never fetches from the configured URL if the MCP server's standard .well-known discovery endpoints succeed.

We're connecting to Slack's MCP server (mcp.slack.com), which advertises 16 OAuth scopes via /.well-known/oauth-protected-resource (RFC 9728), including access to private channels, DMs, and group messages:

  • search:read.private
  • search:read.mpim
  • search:read.im
  • groups:history
  • mpim:history
  • im:history

Our organization cannot approve these scopes. We set up a proxy serving RFC 8414 authorization server metadata with only 9 approved scopes and configured authServerMetadataUrl to point to it.

Result: Claude Code never makes a request to our proxy (confirmed via server logs). The OAuth authorization URL still contains all 16 scopes from Slack's dynamic discovery. The authServerMetadataUrl is completely ignored.

However, when we block mcp.slack.com in /etc/hosts so that discovery fails, Claude Code DOES fall back to authServerMetadataUrl, our proxy receives the request, and the OAuth URL correctly contains only our 9 approved scopes.

This confirms authServerMetadataUrl only functions as a last-resort fallback when discovery fails — not as an override when explicitly configured.

The docs at https://code.claude.com/docs/en/mcp state:

"If your MCP server's standard OAuth metadata endpoints return errors but the server exposes a working OIDC endpoint, you can point Claude Code at a specific metadata URL to bypass the default discovery chain."

This means organizations cannot restrict OAuth scopes for any MCP server that has working discovery endpoints.

What Should Happen?

When authServerMetadataUrl is explicitly configured by the user, it should take priority over dynamic discovery from the MCP server's .well-known endpoints.

Neither RFC 9728 (Protected Resource Metadata) nor RFC 8414 (Authorization Server Metadata) prescribes a priority between pre-configured and discovered metadata — both leave it to the implementation. If a user has explicitly configured a metadata URL, that intent should be respected over automatic discovery.

Alternatively, an explicit scopes field in the oauth config object would solve this use case:

{
  "oauth": {
    "scopes": ["search:read.public", "chat:write", "channels:history"]
  }
}

Error Messages/Logs

No error is shown. The behavior is silent — authServerMetadataUrl is simply never fetched.

Proxy server logs show zero incoming requests from Claude Code when discovery succeeds.

When discovery is blocked (via /etc/hosts), and using a self-signed cert on localhost, the following error appears:
  Error: SDK auth failed: unable to verify the first certificate

This confirms Claude Code attempts to use authServerMetadataUrl only when discovery fails.

Steps to Reproduce

  1. Set up a proxy serving RFC 8414 authorization server metadata with a subset of scopes:
{
  "issuer": "https://slack.com",
  "authorization_endpoint": "https://slack.com/oauth/v2_user/authorize",
  "token_endpoint": "https://slack.com/api/oauth.v2.user.access",
  "response_types_supported": ["code"],
  "grant_types_supported": ["authorization_code", "refresh_token"],
  "token_endpoint_auth_methods_supported": ["client_secret_post"],
  "code_challenge_methods_supported": ["S256"],
  "scopes_supported": [
    "search:read.public",
    "search:read.files",
    "search:read.users",
    "chat:write",
    "channels:history",
    "canvases:read",
    "canvases:write",
    "users:read",
    "users:read.email"
  ]
}
  1. Configure MCP in .mcp.json or a config file:
{
  "mcpServers": {
    "slack": {
      "type": "http",
      "url": "https://mcp.slack.com/mcp",
      "oauth": {
        "authServerMetadataUrl": "https://your-proxy/.well-known/oauth-authorization-server",
        "clientId": "your-slack-client-id",
        "callbackPort": 3118
      }
    }
  }
}
  1. Run: claude --strict-mcp-config --mcp-config mcp.json
  1. Observe the OAuth authorization URL opened in the browser — scope= parameter contains all 16 scopes from Slack's discovery, not the 9 from authServerMetadataUrl.
  1. Check proxy logs — zero requests received from Claude Code.
  1. Now block mcp.slack.com: sudo sh -c 'echo "127.0.0.1 mcp.slack.com" >> /etc/hosts'
  1. Flush DNS: sudo dscacheutil -flushcache && sudo killall -HUP mDNSResponder
  1. Run Claude Code again — now the OAuth URL contains only the 9 scopes from authServerMetadataUrl.
  1. Clean up: sudo sed -i '' '/mcp.slack.com/d' /etc/hosts

Claude Model

Opus

Is this a regression?

I don't know

Last Working Version

_No response_

Claude Code Version

2.1.97

Platform

AWS Bedrock

Operating System

macOS

Terminal/Shell

iTerm2

Additional Information

Related issues:

  • #42419 — Request for explicit scopes config option (Snowflake MCP has the same problem)
  • #39618 — Docs update for RFC 9728 discovery behavior (resolved)

Current discovery priority (confirmed via testing):

  1. RFC 9728: /.well-known/oauth-protected-resource — tried first
  2. RFC 8414: /.well-known/oauth-authorization-server — fallback
  3. authServerMetadataUrl — last resort, only if both above fail

Workaround: Block MCP server domain in /etc/hosts to force discovery failure. Not viable for production/enterprise use.

Enterprise use case: Organizations using Slack's MCP server need to restrict which OAuth scopes are requested to comply with internal security policies. The current behavior provides no way to do this when the MCP server has working discovery endpoints.

A scopes config option in the oauth object would also solve this:

{
  "oauth": {
    "scopes": ["search:read.public", "chat:write", "channels:history"]
  }
}

View original on GitHub ↗

This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗