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

Open 💬 34 comments Opened Apr 24, 2026 by danielvos1998

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?

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.
  1. Configure .mcp.json:

``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
}
}
}
}
``

  1. Register the client secret (via claude mcp add … --client-secret or MCP_CLIENT_SECRET env var).
  1. Start Claude Code → run /mcp → select bc-mcp → authenticate.
  1. Browser redirects to http://localhost:8080/callback?error=invalid_target&error_description=AADSTS9010010… — auth fails.
  1. Verify the bug by capturing the authorize URL Claude opens, e.g. wrap BROWSER with a script that logs its argv to a file:

``bash
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.

View original on GitHub ↗

34 Comments

daankuhlmann-SH · 2 months ago

I also found this bug and it would be very nice if it can be solved asap.

CrazyICT · 2 months ago

I've issues with this aswel

reidwatersbhm · 2 months ago

Confirming this on Claude.ai web (Team workspace) Custom Connector with Microsoft Entra ID.

Setup: MCP server on Azure Functions at https://<app>.azurewebsites.net/mcp behind App Service Authentication (Easy Auth) configured for Microsoft Entra. Single-tenant Entra app with App ID URI api://<client-id>. I can't add the actual MCP URL as a second App ID URI because Entra requires identifier URIs to be on a verified domain of the tenant, and azurewebsites.net is owned by Microsoft, not us. The /.well-known/oauth-protected-resource document declares "resource": "api://<client-id>" and "scopes_supported": ["api://<client-id>/mcp.access"].

When I click Add on the connector, I never even see the Microsoft sign-in page it errors out before that. The redirect URL is:

https://claude.ai/api/mcp/auth_callback?error=invalid_target&error_description=AADSTS9010010%3a+The+resource+parameter+provided+in+the+request+doesn%27t+match+with+the+requested+scopes.+Trace+ID%3a+8f26a744-b393-4350-9974-203f31ad1600

Per AADSTS9010010, Entra compares the resource parameter against the scope's implicit resource (everything before the final slash) and rejects on mismatch. The trailing slash Claude appends turns api://<client-id> into api://<client-id>/, which no longer equals the scope's resource portion api://<client-id>.

Microsoft's documented workaround for this is to put Azure API Management with a verified custom domain in front of the Function App, register that as the Entra App ID URI, and proxy through it (https://developer.microsoft.com/blog/claude-ready-secure-mcp-apim). That's significant infrastructure overhead just to work around the trailing slash.

Could you strip the trailing slash from the resource parameter before sending it to the authorization server? In its current form this breaks every Entra-protected MCP that doesn't have a verified custom domain available.

jnko266 · 2 months ago

Also problematic for me

jpparis02 · 2 months ago

having the same issue

asnape81 · 2 months ago

I'm having the same issue.

chajtus · 1 month ago

This is also issue for me, any fix?

Lukasz-3dhubs · 1 month ago

Ref: ofid_c51480b27686441a
Since #55993 has been marked as a duplicate of this one and closed, I'm adding a related case from Claude Cowork (claude.ai web connector) where resource and scope do not match — not because of a trailing slash, but because the AzDo MCP server's OAuth metadata advertises a GUID-based scope that is unrelated to the MCP server URL:

resource: https://mcp.dev.azure.com/<org> (MCP server URL, passed by Claude)
scope: 2a72489c-aab2-4b65-b93a-a91edccf33b8/.default (AzDo MCP service principal, from resource metadata)

Entra returns AADSTS9010010. Manually removing resource= from the authorization request results in Entra issuing a valid code at https://claude.ai/api/mcp/auth_callback?code=..., confirming resource= is the sole cause.

dagirard · 1 month ago

The MCP specification is clear: 8.1 Canonical URL​

Note: While both https://mcp.example.com/ (with trailing slash) and https://mcp.example.com (without trailing slash) are technically valid absolute URIs according to RFC 3986, implementations SHOULD consistently use the form without the trailing slash for better interoperability unless the trailing slash is semantically significant for the specific resource.

Claude Code SHOULD be following the standard that Anthropic created.

jeffe-mighty · 1 month ago

Adding a +1 to this thread too. We're building out a custom MCP server backed by EntraID and am getting the same error when trying to connect via Claude Web/Desktop. The Azure API Manager is seriously overkill for this.

whiskeysierra · 1 month ago

Worked around this for Microsoft Entra ID after running into AADSTS9010010 ("resource parameter doesn't match the requested scopes"). Posting our notes in case it saves someone the day of debugging.

Root cause — in selectResourceURL:

authorizationUrl.searchParams.set('resource', resource.href);

URL.href runs WHATWG normalization, which appends / to bare-origin URLs (new URL("https://example.com").toString() === "https://example.com/").

The broken round-trip, concretely. Server publishes:

// GET https://example.com/.well-known/oauth-protected-resource
{
  "resource": "https://example.com",
  "authorization_servers": ["https://login.microsoftonline.com/<tenant>/v2.0"],
  "scopes_supported": ["https://example.com/access_as_user"]
}

Server's 401 challenge points the client at that document:

WWW-Authenticate: Bearer error="invalid_token",
  resource_metadata="https://example.com/.well-known/oauth-protected-resource"

SDK fetches the document, sees resource = "https://example.com", runs it through URL.href"https://example.com/", and forwards:

.../authorize?...
  &scope=https%3A%2F%2Fexample.com%2Faccess_as_user
  &resource=https%3A%2F%2Fexample.com%2F

Entra now sees resource=https://example.com/ (with slash), but the prefix of the scope (https://example.com/access_as_userhttps://example.com) has no slash. Verbatim prefix check fails → AADSTS9010010.

What didn't work for us:

  1. Add a trailing slash to the identifier URI on the Entra app (https://example.com/). Entra refuses identifier_uris with trailing slashes outright — not configurable.
  1. Switch the resource namespace to api://<guid>. Non-special URL schemes survive URL.href untouched (no trailing-slash mutation), and Entra is happy with api://<guid> natively since every app reg gets one as the default App ID URI. But:
  • The SDK's checkResourceAllowed (src/shared/auth-utils.ts) compares requested.origin !== configured.origin, and non-special-scheme URLs have a null origin in WHATWG — so the SDK throws Protected resource api://<guid> does not match expected https://example.com/path (or origin) and never sends the request.
  • Independently, RFC 9728 §3.3 requires the OPRM document's resource to be verifiable against the URL the client was attempting to access. Swapping schemes mid-flight isn't spec-compliant.
  1. What actually works: register a secondary https:// identifier URI on the Entra app with a non-trivial path (e.g. https://example.com/mcp). Advertise that URL as resource in the OPRM document and as the prefix in scopes_supported. Non-empty paths survive URL.href unchanged, so both Entra's prefix check and the SDK's origin check pass. Same aud=<client-id> in issued tokens, so resource-server JWT validation is unaffected. resource_metadata in the WWW-Authenticate header can (and should) still point at the host-root well-known location — that's an independent URL.

Working configuration (sanitized to example.com, but this is the actual shape running in production for us):

// GET https://example.com/.well-known/oauth-protected-resource
{
  "resource": "https://example.com/mcp",
  "authorization_servers": ["https://login.microsoftonline.com/<tenant>/v2.0"],
  "scopes_supported": ["https://example.com/mcp/access_as_user"],
  "bearer_methods_supported": ["header"]
}
HTTP/2 401
WWW-Authenticate: Bearer error="invalid_token",
  error_description="An error occurred while attempting to decode the Jwt: Malformed token",
  error_uri="https://tools.ietf.org/html/rfc6750#section-3.1",
  resource_metadata="https://example.com/.well-known/oauth-protected-resource"

Note the /mcp path on resource and inside scopes_supported, contrasted with the host-root resource_metadata URL — those two URLs don't need to be the same, and in fact must differ if resource carries a path.

Server-side workaround, not a Claude Code / SDK fix — but it unblocks Entra-backed MCP servers today. The underlying bug is the SDK treating the OPRM document's resource as round-trippable text while quietly putting it through URL.href; either stop normalizing, or stop sending resource whose normalized form differs from scopes_supported's prefix.

madebymatthew · 1 month ago

+1 same issue for me

juanfran-n8n · 1 month ago

+1, same issue here

ygemara · 1 month ago

+1. Seems like it's an easy fix. Not sure why it's taking a month to move on it.

glenn-zarb · 1 month ago

+1, same issue here

tannercorbin · 1 month ago

+1, same issue here

sonmaximum · 1 month ago

Confirming this same bug on Claude Code v2.1.x against the Azure DevOps remote MCP server
(https://mcp.dev.azure.com/<org>) — different MCP server than the original report, identical failure mode:

AADSTS9010010: The resource parameter provided in the request doesn't match with the requested scopes.

The server's oauth-protected-resource metadata declares:
{
"resource": https://mcp.dev.azure.com/<org>,
"scopes_supported": ["<entra-app-guid>/.default"]
}

Wireshark/redirect inspection confirms Claude Code emits resource=https://mcp.dev.azure.com/<org>/ with a trailing
slash, matching the reporter's diagnosis.

A useful additional data point for whoever picks this up: the bug is silent when the advertised scope is URL-prefixed
({resource}/.default) but visible when the scope is GUID-prefixed ({guid}/.default). With a URL-prefixed scope (e.g.
Microsoft 365 Copilot agent MCPs), Entra's resource-vs-scope comparator passes because the scope literally starts with
the mangled resource string. With a GUID-prefixed scope, Entra has to resolve the GUID to the App ID URI and
string-compare against the slashed resource — and rejects. This means a fix would unblock GUID-scope servers like ADO
without changing behavior for the URL-scope ones that happen to work today.

Reproduction is not platform-specific — hitting this on Linux (WSL2) (just noting since this has platform:macos label)

Workaround in use: headersHelper invoking az account get-access-token --resource <guid> and emitting {"Authorization":
"Bearer ..."}. Stable but requires az and a separate az login session.

Tried the oauth.scopes override (replacing the discovered GUID scope with the URL form {resource}/.default) as a
config-only sidestep. That avoids 9010010 but Entra then fails with AADSTS500011: The resource principal was not found
in the tenant because URL-form scopes require a service principal in the consuming tenant, which the ADO MCP backing
app doesn't have. So scopes override is not a viable workaround for this server class.

Claude code used in drafting this comment

mertcanvural · 1 month ago

I hit a similar Entra OAuth mismatch once, the authorize step looked fine until one normalized resource string drifted from the metadata. Since your repro already proves the protected-resource metadata omits the trailing slash while both /authorize and /token add it back, I'd diff those exact resource values before touching scopes again. My usual checks are a raw manual diff, authproxy.dev if I need to inspect the handoff, or OAuth Redirect Doctor when I want resource URI, callback path, and provider expectations lined up. ngl one auto-added slash is enough to make Entra treat it as a different resource.

jabbera · 1 month ago

@whiskeysierra thank you so much. This worked for us.

feifanl · 1 month ago

+1, same issue here

zhouxingyu-kinto · 1 month ago

+1, same issue here

cailyoung · 1 month ago

+1 trailing slash is blocking us

gavearlima · 1 month ago

+1 same issue

matzegebbe · 1 month ago

+1, same impact on our side.

We worked around it by putting a small OAuth compatibility proxy in front of Entra. It publishes its own discovery metadata, forwards /authorize and /token to the tenant-specific Entra v2 endpoint, and removes only the resource parameter before forwarding.

Important detail from our setup: stripping resource only during /authorize was not enough - it also had to be removed during the /token exchange.

I published a sanitized example here:
https://gist.github.com/matzegebbe/fba6417a81cc692eca64d0244512b066

ArthurMynl · 29 days ago

+1 same issue

santanaguy · 28 days ago

+1 this is blocking us and preventing a full migration to Claude.

We can't have all the domains verified on our tenant, because some of those domains are also local domains. Claude Code should not send the resource parameter, or at least should do what Copilot already does, which works out of the box.

jbasilan · 21 days ago

Reproducing on Microsoft Fabric MCP Server (Windows, Claude Code v2.1.191 + claude.ai web)

Affected endpoints:

  • Fabric Core MCP: https://api.fabric.microsoft.com/v1/mcp
  • Fabric Data Agent MCP: https://api.fabric.microsoft.com/v1/mcp/workspaces/{workspace-id}/dataagents/{agent-id}/agent

Environment:

  • Platform: Windows 11
  • Claude Code version: v2.1.191 (latest as of 2026-06-25)
  • Also reproduced on claude.ai web connector (same error)
  • Entra tenant: single-tenant, Microsoft Entra ID

Error observed:

Authorization with the MCP server failed. You can check your credentials and permissions.
Entra Trace IDs: b9729652-bd65-49d9-b32c-9d5f5f0d4500, d7cf68c1-8184-4b64-b612-0f8c12481900
ofid references: ofid_43c91433f910864b, ofid_c1114c29671b0a1f, ofid_16bbfa64b85c7ca1, ofid_c69ca4f1994091b0

Confirmed via Entra non-interactive sign-in logs:

Sign-in error code: 9010010
Failure reason: The resource parameter provided in the request doesn't match with the requested scopes.
User agent: python-httpx/0.28.1
Client credential type: Client secret

Key observations:

  • Interactive sign-in logs show Success — user authenticates fine against Power BI Service
  • Non-interactive sign-in logs show Failure with AADSTS9010010 — the OBO token exchange from Anthropic's backend (python-httpx/0.28.1) fails
  • Conditional Access is not the issue — CA shows "Not applied" on the failed entry
  • App registration has correct delegated permissions: Item.Read.All, Workspace.Read.All both granted with admin consent
  • Both redirect URIs registered: https://claude.ai/oauth/callback and https://claude.ai/api/mcp/auth_callback
  • Issue persists on latest Claude Code version (v2.1.191) — not fixed by updating

This is consistent with the trailing slash / v1 resource parameter bug described in this issue and #55993. The fix needs to be on Anthropic's backend — the resource parameter being sent to Entra's v2 endpoint is either malformed or mismatched against the scope prefix.

Please prioritize — this blocks all Microsoft Fabric MCP connections for Entra-protected tenants.

thehappycheese · 18 days ago

just lost most of my day to trying and failing to fix this exact problem. Wish i had found this first. Please fix :(

christiandietrich0 · 16 days ago

⏫ this needs to be fixed!

Marshall-Milbourn · 15 days ago

we're blocked by this in production

Coppo459 · 10 days ago

We hit this exact failure mode (AADSTS9010010: The resource parameter provided in the request doesn't match with the requested scopes) building a production MCP integration for a financial services firm, fronting a custom MCP server with Azure API Management for Entra ID authentication. Landed here via #55993 → this issue.
Setup: APIM-fronted MCP server, validate-jwt policy, PRM correctly published at /.well-known/oauth-protected-resource, Entra v2 app registration with a custom API scope (api://{app-id}/access_as_user).
Confirmed via mcp-remote (v0.1.38) with --debug:

No --resource flag → auto-derives resource from the MCP server URL, mismatches the API's audience → AADSTS9010010
--resource explicitly set to match the audience → authorize step succeeds, but token exchange still fails with the identical error
No flag combination in 0.1.38 allows omitting the resource parameter entirely

Cross-client confirmation: the official @modelcontextprotocol/inspector (a separate, independent client) hits the identical failure against the same endpoint — rejected at the authorize step rather than token exchange.
Relevant to this specific issue's root cause: I want to flag that in our case, the resource value used matched our PRM's declared resource exactly, character-for-character (no trailing-slash discrepancy on either side) — so if the trailing-slash normalization bug described here is the sole cause, that wouldn't explain our failure. Wondering if this represents a second, distinct trigger for the same AADSTS9010010 error rather than the same root cause — happy to share sanitized --debug logs and the exact authorize/token URLs we captured if that'd help distinguish the two.

steve-orcina · 9 days ago

Adding an independent production repro with one point I don't think has been made explicitly in this thread: for the common case, no server-side configuration can work around this, by Microsoft's own rules.

Setup: custom MCP resource server (FastMCP over Streamable HTTP on Azure Container Apps), single-tenant Entra, pre-registered public client (authorization-code + PKCE, no DCR), RFC 9728 PRM served correctly, resource advertised without a trailing slash. Both Claude Code (v2.1.179) and the claude.ai / Claude Desktop custom connector sign in successfully, then fail at the authorization request with AADSTS9010010 (confirmed in the Entra sign-in log, errorCode 9010010).

Why there is no workaround for host-root resources:

  1. Entra rejects an authorization request carrying both an RFC 8707 resource and a v2 scope unless the resource exactly matches the App ID URI the scope belongs to (that is what AADSTS9010010 is).
  2. Microsoft's identifier URI restrictions state: "The application ID URI value must not end with a slash." (https://learn.microsoft.com/en-us/entra/identity-platform/identifier-uri-restrictions)
  3. Claude's OAuth client appends a trailing slash to host-root resource URIs (the selectResourceURL URL-normalisation noted above).

So the slashed value Claude sends can never equal any App ID URI Entra will accept. We verified the "verified custom domain" route specifically: tenant-verified domain, https:// App ID URI permitted and ready to set, and it still cannot help, because the URI cannot carry the trailing slash the client insists on. The spec note quoted earlier in the thread already says implementations SHOULD use the un-slashed form; using the PRM resource value verbatim would fix this.

Impact for us: our public MCP ships unauthenticated, and an Entra-authenticated MCP for future workloads is procedurally ready and blocked solely on this.

whiskeysierra · 9 days ago

There is a workaround. Did you read my comment earlier in this thread? If
you register your identifier URI with a path, e.g. /mcp then it works.
You are correct in "no workaround for host-root resources" exist.

steve-orcina · 7 days ago
There is a workaround. Did you read my comment earlier in this thread? If you register your identifier URI with a path, e.g. /mcp then it works. You are correct in "no workaround for host-root resources" exist.

@whiskeysierra apologies, I'd missed the pathed-URI detail. Thanks for the correction and for the full working configuration; it is the probably most useful thing in this thread. The subtleties of resource_metadata staying at the host-root well-known while resource carries the path is what got us unblocked.

The SDK fix is still needed for host-root servers (including Microsoft's own, which can't change their canonical URIs), but agreed: custom Entra-backed servers are unblockable today with your configuration. 😃