claude-code - 💡(How to fix) Fix [BUG] MCP OAuth appends trailing slash to `resource` parameter, breaking Entra ID auth (AADSTS9010010) [1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
anthropics/claude-code#52871Fetched 2026-04-25 06:18:39
View on GitHub
Comments
0
Participants
1
Timeline
5
Reactions
0
Participants
Timeline (top)
labeled ×5

Error Message

Error Messages/Logs

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

Root Cause

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.

Fix Action

Fix / Workaround

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.

Code Example

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

---

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

---

{
     "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
         }
       }
     }
   }

---

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
RAW_BUFFERClick to expand / collapse

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.

extent analysis

TL;DR

The issue can be fixed by preserving the resource string from the MCP metadata verbatim and avoiding normalization through URL, or by stripping a single trailing slash before emitting the OAuth requests.

Guidance

  • Identify the code paths where the resource parameter is constructed for the /authorize and /token requests and modify them to preserve the original resource string from the MCP metadata.
  • Consider adding a configuration option to override the authorization endpoint for single-tenant Entra apps to address the secondary issue.
  • Verify the fix by capturing the authorize URL and checking that the resource parameter no longer has a trailing slash.
  • Test the OAuth flow with the modified code to ensure that the AADSTS9010010 error is resolved.

Example

No code snippet is provided as the issue does not contain sufficient information about the codebase. However, the suggested fix involves modifying the code to preserve the original resource string, for example:

// Before
const resource = new URL(mcpMetadata.resource).toString();

// After
const resource = mcpMetadata.resource;

Notes

The issue may be related to the WHATWG URL spec normalizing host-only URLs to include a trailing slash. The suggested fix aims to preserve the original resource string to avoid this normalization.

Recommendation

Apply the workaround by preserving the resource string from the MCP metadata verbatim or stripping a single trailing slash before emitting the OAuth requests, as this is a more targeted solution to the specific issue at hand.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING