[BUG] MCP OAuth appends trailing slash to `resource` parameter, breaking Entra ID auth (AADSTS9010010)
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_accessresource=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
- Register a single-tenant Web app in Microsoft Entra ID with redirect URI
http://localhost:8080/callbackand a delegateduser_impersonationpermission for the Business Central MCP Server resource (https://mcp.businesscentral.dynamics.com). Create a client secret.
- 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
}
}
}
}
- Register the client secret (via
claude mcp add … --client-secretorMCP_CLIENT_SECRETenv var).
- Start Claude Code → run
/mcp→ selectbc-mcp→ authenticate.
- Browser redirects to
http://localhost:8080/callback?error=invalid_target&error_description=AADSTS9010010…— auth fails.
- Verify the bug by capturing the authorize URL Claude opens, e.g. wrap
BROWSERwith 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
/mcp
Run 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
resourcequery param on/authorize - the
resourcebody 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.
34 Comments
I also found this bug and it would be very nice if it can be solved asap.
I've issues with this aswel
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.
Also problematic for me
having the same issue
I'm having the same issue.
This is also issue for me, any fix?
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.
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.
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.
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:URL.hrefruns WHATWG normalization, which appends/to bare-origin URLs (new URL("https://example.com").toString() === "https://example.com/").The broken round-trip, concretely. Server publishes:
Server's 401 challenge points the client at that document:
SDK fetches the document, sees
resource = "https://example.com", runs it throughURL.href→"https://example.com/", and forwards:Entra now sees
resource=https://example.com/(with slash), but the prefix of the scope (https://example.com/access_as_user→https://example.com) has no slash. Verbatim prefix check fails →AADSTS9010010.What didn't work for us:
https://example.com/). Entra refuses identifier_uris with trailing slashes outright — not configurable.api://<guid>. Non-special URL schemes surviveURL.hrefuntouched (no trailing-slash mutation), and Entra is happy withapi://<guid>natively since every app reg gets one as the default App ID URI. But:checkResourceAllowed(src/shared/auth-utils.ts) comparesrequested.origin !== configured.origin, and non-special-scheme URLs have anullorigin in WHATWG — so the SDK throwsProtected resource api://<guid> does not match expected https://example.com/path (or origin)and never sends the request.resourceto be verifiable against the URL the client was attempting to access. Swapping schemes mid-flight isn't spec-compliant.https://identifier URI on the Entra app with a non-trivial path (e.g.https://example.com/mcp). Advertise that URL asresourcein the OPRM document and as the prefix inscopes_supported. Non-empty paths surviveURL.hrefunchanged, so both Entra's prefix check and the SDK's origin check pass. Sameaud=<client-id>in issued tokens, so resource-server JWT validation is unaffected.resource_metadatain 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):Note the
/mcppath onresourceand insidescopes_supported, contrasted with the host-rootresource_metadataURL — those two URLs don't need to be the same, and in fact must differ ifresourcecarries 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
resourceas round-trippable text while quietly putting it throughURL.href; either stop normalizing, or stop sendingresourcewhose normalized form differs fromscopes_supported's prefix.+1 same issue for me
+1, same issue here
+1. Seems like it's an easy fix. Not sure why it's taking a month to move on it.
+1, same issue here
+1, same issue here
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 trailingslash, 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
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
/authorizeand/tokenadd 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.@whiskeysierra thank you so much. This worked for us.
+1, same issue here
+1, same issue here
+1 trailing slash is blocking us
+1 same issue
+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
/authorizeand/tokento the tenant-specific Entra v2 endpoint, and removes only theresourceparameter before forwarding.Important detail from our setup: stripping
resourceonly during/authorizewas not enough - it also had to be removed during the/tokenexchange.I published a sanitized example here:
https://gist.github.com/matzegebbe/fba6417a81cc692eca64d0244512b066
+1 same issue
+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.
Reproducing on Microsoft Fabric MCP Server (Windows, Claude Code v2.1.191 + claude.ai web)
Affected endpoints:
https://api.fabric.microsoft.com/v1/mcphttps://api.fabric.microsoft.com/v1/mcp/workspaces/{workspace-id}/dataagents/{agent-id}/agentEnvironment:
Error observed:
Confirmed via Entra non-interactive sign-in logs:
Key observations:
Item.Read.All,Workspace.Read.Allboth granted with admin consenthttps://claude.ai/oauth/callbackandhttps://claude.ai/api/mcp/auth_callbackThis 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
resourceparameter 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.
just lost most of my day to trying and failing to fix this exact problem. Wish i had found this first. Please fix :(
⏫ this needs to be fixed!
we're blocked by this in production
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.
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,
resourceadvertised 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 withAADSTS9010010(confirmed in the Entra sign-in log, errorCode 9010010).Why there is no workaround for host-root resources:
resourceand a v2 scope unless theresourceexactly matches the App ID URI the scope belongs to (that is what AADSTS9010010 is).selectResourceURLURL-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 PRMresourcevalue 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.
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_metadatastaying at the host-root well-known whileresourcecarries 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. 😃