Skip to content

[BUG] MCP OAuth appends trailing slash to resource parameter, breaking Entra ID auth (AADSTS9010010) #52871

Description

@danielvos1998

Preflight Checklist

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

What's Wrong?

Claude Code's MCP OAuth client appends a trailing / to the resource parameter on both the /authorize redirect and the /token exchange. When the MCP server's .well-known/oauth-protected-resource metadata declares the resource URI without a trailing slash, the resource parameter no longer matches the implicit resource of the requested scope. Microsoft Entra ID rejects the request with AADSTS9010010: The resource parameter provided in the request doesn't match with the requested scopes.

This makes Claude Code unable to connect to any Entra-ID-protected MCP server whose metadata uses an un-slashed resource URI — including Microsoft's official Business Central MCP server (https://mcp.businesscentral.dynamics.com).

Concrete example. The server advertises:

{
  "resource": "https://mcp.businesscentral.dynamics.com",
  "scopes_supported": ["https://mcp.businesscentral.dynamics.com/user_impersonation"]
}

Claude Code constructs the authorize URL with:

  • scope=https%3A%2F%2Fmcp.businesscentral.dynamics.com%2Fuser_impersonation+offline_access
  • resource=https%3A%2F%2Fmcp.businesscentral.dynamics.com%2F ← extra trailing slash

Entra string-compares resource against the scope-prefix. …com/…com → mismatch → AADSTS9010010.

Likely root cause: a new URL(resource).toString() (or equivalent) somewhere between parsing the MCP metadata and emitting the OAuth requests. The WHATWG URL spec normalizes host-only URLs to include a trailing slash, which silently corrupts the resource identifier.

What Should Happen?

The resource parameter emitted on /authorize and /token should match the resource string returned by the MCP server's /.well-known/oauth-protected-resource metadata verbatim — including the absence of a trailing slash. Entra should accept the auth request and return an authorization code, and the subsequent token exchange should succeed.

Error Messages/Logs

Returned to the OAuth callback by Entra during `/authorize`:


invalid_target
AADSTS9010010: The resource parameter provided in the request doesn't match with the requested scopes.
Trace ID: 2de814e5-5da2-4367-b65f-826dbb7a7200
Correlation ID: ab1daa86-30d0-4c41-a52f-af76ed61cc13


The same `AADSTS9010010` is returned by the token endpoint when the authorize step is manually rewritten to strip the slash — confirming the same bug exists in the token-exchange code path.

Authorize URL Claude Code actually emits (captured via a `BROWSER` wrapper logging the URL passed to `open(1)`):


https://login.microsoftonline.com/common/oauth2/v2.0/authorize
  ?response_type=code
  &client_id=<redacted>
  &code_challenge=<pkce>
  &code_challenge_method=S256
  &redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fcallback
  &state=<state>
  &scope=https%3A%2F%2Fmcp.businesscentral.dynamics.com%2Fuser_impersonation+offline_access
  &resource=https%3A%2F%2Fmcp.businesscentral.dynamics.com%2F

Steps to Reproduce

  1. Register a single-tenant Web app in Microsoft Entra ID with redirect URI http://localhost:8080/callback and a delegated user_impersonation permission for the Business Central MCP Server resource (https://mcp.businesscentral.dynamics.com). Create a client secret.

  2. Configure .mcp.json:

    {
      "mcpServers": {
        "bc-mcp": {
          "type": "http",
          "url": "https://mcp.businesscentral.dynamics.com",
          "headers": {
            "TenantId": "<tenant-id>",
            "EnvironmentName": "Production",
            "Company": "<company>",
            "ConfigurationName": "MCP Server"
          },
          "oauth": {
            "clientId": "<entra-app-client-id>",
            "callbackPort": 8080
          }
        }
      }
    }
  3. Register the client secret (via claude mcp add … --client-secret or MCP_CLIENT_SECRET env var).

  4. Start Claude Code → run /mcp → select bc-mcp → authenticate.

  5. Browser redirects to http://localhost:8080/callback?error=invalid_target&error_description=AADSTS9010010… — auth fails.

  6. Verify the bug by capturing the authorize URL Claude opens, e.g. wrap BROWSER with a script that logs its argv to a file:

    printf '#!/bin/bash\nprintf "%%s\\n" "$@" >> /tmp/auth-url.log\nexit 0\n' > /tmp/log-browser.sh
    chmod +x /tmp/log-browser.sh
    BROWSER=/tmp/log-browser.sh claude

    Run /mcp again. URL-decode the captured URL and observe the resource=…com/ trailing slash that is not present in the MCP metadata.

Claude Model

Opus

Is this a regression?

I don't know

Last Working Version

No response

Claude Code Version

2.1.119 (Claude Code)

Platform

Anthropic API

Operating System

macOS

Terminal/Shell

Other

Additional Information

Suggested fix. Preserve the resource string from the MCP metadata verbatim — do not round-trip it through URL. If normalization is desired for comparison, strip a single trailing slash before emitting the OAuth requests. Both emission points need the fix:

  • the resource query param on /authorize
  • the resource body param on /token

Workaround for anyone hitting this today. Bypass Claude Code's OAuth entirely: run a standalone PKCE flow against the tenant-specific Entra endpoint (with the correct un-slashed resource URI), obtain an access token, and inject it as "Authorization": "Bearer …" in .mcp.json's headers. Tokens expire hourly and must be refreshed manually.

Secondary issue (possibly separate). The BC MCP server's metadata advertises authorization_servers: ["https://login.microsoftonline.com/common/v2.0"], but single-tenant Entra apps created after 2018-10-15 cannot use /common/ (Entra returns AADSTS50194). Currently Claude Code offers no way to override the authorization endpoint (e.g. a tenantId / authorizeEndpoint field under oauth), forcing users to either flag their app as multi-tenant or work around the flow manually. A config key would solve this cleanly. Happy to file this as a separate issue if preferred.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:autharea:mcpbugSomething isn't workinghas reproHas detailed reproduction stepsplatform:macosIssue specifically occurs on macOS

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions