[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.
50 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. 😃
Same here with the latest preview.
Confirming this from a different deployment shape: an MCP gateway/registry that fronts many MCP servers behind a single Entra tenant (agentic-community/mcp-gateway-registry, tracked on our side under #990 and related Entra IDE-login work). We hit the identical
AADSTS9010010at token exchange.A data point that corroborates the WHATWG-normalization root cause and doubles as a workaround for anyone stuck:
https://gw, no trailing slash, verified viacurl), Claude Code sendsresource=https://gw/on/authorize(origin + appended/). Entra can never match this, because App ID URIs must not end in a slash and Entra comparesresourceagainstidentifierUrisby exact string. Bare-origin resource is therefore unresolvable on Entra on both sides at once.https://gw/<server>/mcp), Claude Code sends it verbatim with no appended slash, and login succeeds. This is consistent with the root cause:URL.hrefonly appends/to a path-less URL, so a resource that already carries a path round-trips unchanged.So the path-qualified form is a viable server-side workaround today, but it has a real cost that the verbatim fix would eliminate: because the origin can't be used, each MCP server needs its own path-qualified
identifierUriin Entra (bumping against Entra's 999-URI-per-app cap), instead of registering the origin once for the whole gateway. Fixing the SDK to emit the PRMresourceverbatim would collapse this back to a single origin registration.Strong +1 on the proposed fix: preserve the metadata
resourcestring verbatim and do not round-trip it throughURL. Both emission points (/authorizequery param and/tokenbody param) need it.cc @omrishiv
---
Update: the root cause lives in the MCP TypeScript SDK, not Claude Code itself —
selectResourceURLinpackages/client/src/client/auth.tsreturnsnew URL(resourceMetadata.resource), andURL.hrefis what appends the slash. Tracked upstream as modelcontextprotocol/typescript-sdk#1968, with a fix in modelcontextprotocol/typescript-sdk#1991 that preserves the metadataresourceverbatim (we reviewed it and endorsed it here). Claude Code picks the fix up once it bumps the SDK dependency.@aarora79 Isn't this exactly the workaround I described in https://github.com/anthropics/claude-code/issues/52871#issuecomment-4602137405?
@whiskeysierra, it is, but we need this fixed so that we dont have to put this workaround in our MCP Gateway Registry code and then take care of all the downstream consequences such as adding an
identifierUrifor each of the MCP server registered with our Gateway, and even that approach would break when we reach 999 registered servers. The fix actually is covered by this PR https://github.com/modelcontextprotocol/typescript-sdk/pull/1991 on the MCP Typescript SDK repo.@aarora79 I don't understand. This is a Claude Code issue. The upstream MCP servers of your MCP Gateway are opaque to Claude. This problem only exists where Claude integrates with an MCP which in your case is your MCP Gateway, I'd assume.
At my work, we have a central MCP Gateway and we had exactly this problem here, but we only had to fix it for the MCP Gateway itself. It's own identifier URI had to have a path. But none of the transitive, upstream MCPs were involved, because Claude doesn't see them, doesn't authenticate/authorize against them directly.
I guess I'm misinterpreting something from your response.
@whiskeysierra so we also have a Gateway but I think there is a difference in topology from yours. From what I understand, your gateway is one MCP endpoint from Claude's view, so one path-qualified identifier URI covers it. Our registry exposes each registered MCP server as its own distinct path (
https://gw/<server>/mcp, our Gateway is a reverse proxy), and a coding assistant adds each as a separate MCP connection, each runs its own PRM discovery and sends its own resource. So we have N front-door endpoints, not one, hence N resources. (To your point: this isn't about the transitive upstream MCPs, which Claude never sees or authenticates against; it's the N distinct front-door URLs Claude connects to directly.)RFC 9728 §3.3 lets a PRM advertise the origin as its resource (connection URL _or_ origin), so once the SDK stops appending the slash, every per-server PRM can advertise the shared origin and we register one origin identifier URI for the whole gateway. Today, because bare-origin is unusable on Entra (the slash), we're forced into one path-qualified URI per server.
Bueller?
Stripping the trailing slash is necessary but not sufficient — a second bug hides behind it.
Hit this on 2.1.258 against a server that hosts two RFC 8707 resources on one origin (RFC 9728 explicitly supports this: "Using path components enables supporting multiple resources per host. This is required in some multi-tenant hosting configurations.").
https://mcp.example.com/mcp/.well-known/oauth-protected-resource→"resource": "https://mcp.example.com"(a different API on the same host)/.well-known/oauth-protected-resource/mcp→"resource": "https://mcp.example.com/mcp"GET/POST /mcp→401withWWW-Authenticate: Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource/mcp"Claude Code emits
resource=https%3A%2F%2Fmcp.example.com%2F— the origin, plus this issue's trailing slash. A/B against the authorization endpoint, all else identical (same client, same PKCE, same scopes):|
resourcesent | Result ||---|---|
|
https://mcp.example.com/|invalid_target||
https://mcp.example.com|invalid_target← the de-slashed value still fails ||
https://mcp.example.com/mcp| 303 → consent ✅ |So the fix requested here (de-slash) leaves this class broken. The client also needs to take
resourcefrom the PRM document thatWWW-Authenticate: resource_metadatapoints at, rather than deriving it from the server URL's origin.Spec basis: the MCP authorization spec requires clients to parse
WWW-Authenticateand respond appropriately to 401s, and says clients SHOULD provide "the most specific URI that they can" —https://mcp.example.com/mcpis listed there as a valid canonical resource URI. This matters for any deployment that deliberately separates the MCP transport's audience from a product API's audience on the same origin, which is a security boundary rather than an exotic setup: collapsing them makes a token minted for an MCP session valid against the full REST API.Ruled out a stale client cache: removed and re-added the server, and confirmed genuinely fresh state in the debug log (
No client info found, a new redirect port, and a fresh discovery with noReturning cached discovery statehit). Identical wrong value.Also verified the server side is conformant, so this isn't a server misconfiguration: correct 401 challenge on both GET and POST, both PRM documents returning 200
application/jsonwith distinct correctresourcevalues, the AS advertising both inprotected_resources, and/mcp/307-redirecting to/mcp.Same class of failure we keep hitting across clients: Inspector/green path looks fine, then a real client mutates the PRM
resource(trailing-slash WHATWG normalization) and Entra (or any exact-match AS) rejects with AADSTS9010010 / audience mismatch.We've been treating this as one cell in a broader client×server compatibility matrix — PRM resource string vs endpoint URL, trailing-slash exact match, DCR vs static client_id, needs-auth discovery — because each shipping client breaks in a different doorway.
If useful while this lands: free CompatLab scorecard / early access (permission-only demand test) https://compatlab-waitlist.vercel.app — happy to compare notes on which clients still rewrite vs preserve the raw resource string.
Same error code, different topology — reporting it here since the trailing-slash fix wouldn't resolve it, and it may be worth handling in the same change.
Setup: the MCP server is protected by Auth0 as the authorization server, with Entra as an upstream enterprise connection (strategy: waad, identity_api: microsoft-identity-platform-v2.0). So Entra authenticates the user, but Auth0 issues the token. That differs from the Business Central case above, where Entra is itself the authorization server.
What happens: Claude Code sends resource to Auth0, Auth0 forwards it to Entra during federation, and Entra rejects with AADSTS9010010.
Authorize URL:
Auth0 log for the failed attempt:
Why the trailing-slash fix doesn't cover this: the scopes here are ordinary OIDC scopes (openid profile email offline_access), not Entra resource-prefixed scopes. There is no scope whose prefix could match resource, with or without the trailing slash — so normalising the slash would leave this failing identically.
Ruled out on the server side: the Auth0 connection has no upstream_params (so the forwarding isn't configurable), and identity_api is v2 — where Microsoft rejects resource alongside v2-style scopes by design. There is no setting that stops Auth0 forwarding it.
What would fix it: omitting resource when the authorization server doesn't implement RFC 8707, or a way to opt out per server. Auth0 ignores the parameter itself but passes it upstream, so sending it to a non-supporting AS is strictly harmful here. Related: modelcontextprotocol/modelcontextprotocol#1614 proposes making it OPTIONAL.
Happy to provide trace IDs or test against a build.
@SammedVJS thanks for the Auth0→Entra federation case — that is a different cell than “Entra is the AS,” and you’re right that a trailing-slash-only fix would leave it failing.
Useful split for anyone else hitting AADSTS9010010:
resourcebut still forwards it; Entra then rejects because scopes are ordinary OIDC (openid profile email offline_access) with no resource-prefixed match. Slash normalize cannot fix that.Agree the durable client behavior is: omit
resourcewhen the authorization server does not implement RFC 8707 (or provide a per-server opt-out). modelcontextprotocol/modelcontextprotocol#1614 making it OPTIONAL is the right direction for this topology.If maintainers want a minimal repro matrix cell: Auth0 authorize with
resource=set + Entra enterprise connection + OIDC-only scopes → expectinvalid_target/ AADSTS9010010; same flow withresourceomitted → should pass federation.Another cell: Auth0 as the authorization server, honouring
resourceitself — no Entra anywhere.Adding this because the Auth0 cases above are all Auth0-as-AS with Entra federated upstream, where the conclusion was that Auth0 ignores
resourceand merely forwards it. That holds only under Auth0's default resource-parameter profile. A tenant configured to honour RFC 8707 resolvesresourceitself, by exact string, against its registered resource servers — and then this bug is fatal in the same way it is for Entra-as-AS.Setup:
https://mcp.example.com/mcp/.well-known/oauth-protected-resourceadvertises"resource": "https://mcp.example.com"— no path, the form the MCP spec's own trailing-slash note prescribesClaude Code 2.1.270 sends
resource=https%3A%2F%2Fmcp.example.com%2F.Auth0 finds nothing registered under that string and answers 403 at
/authorize. The diagnostics are worse than Entra's: no error code reaches the client, the browser shows Auth0's generic "Oops!, something went wrong" page, and nothing on it distinguishes this from a tenant misconfiguration. We isolated it only by replaying/authorizewith curl and varying one parameter at a time:|
resourcesent | HTTP ||---|---|
|
https://mcp.example.com/— what Claude Code sends | 403 ||
https://mcp.example.com— what the metadata advertises | passes resource resolution |Two things that may matter for the fix:
resourceby exact string is affected, and exact-string matching is what RFC 8707 and RFC 9728 §3.3 both assume.https://mcp.example.com/mcp— makes the normalization a no-op, and that is what we shipped. It is unavailable to anyone whose identifier is fixed by something outside their control, and it means declining the form the MCP specification explicitly recommends.For whoever picks this up: MCP 2026-07-28 states the underlying principle as a MUST NOT, for
issrather thanresource— "clients MUST NOT apply scheme or host case folding, default-port elision, trailing-slash, or percent-encoding normalization (RFC 3986 Sections 6.2.2-6.2.3) before comparison."resourceis compared the same way, so the same rule wants applying there.@slowe89 Thanks for the response.
Ran the repro you asked for. Confirmed: omitting resource passes federation, everything else identical.
Auth0 as AS, Entra enterprise connection (strategy: waad, identity_api: microsoft-identity-platform-v2.0), OIDC-only scopes.
A — with resource:
→ error=access_denied&error_description=AADSTS9010010: The resource parameter provided in the request doesn't match with the requested scopes
B — byte-identical minus resource: → redirects to the callback with ?code=.... Federation succeeds.
Same client_id, same scope, same audience, same connection, same PKCE challenge. The only difference is the parameter.
One gotcha for anyone reproducing: run B in a private window. Auth0's session cookie replayed the failed transaction on my first attempt and made B look like it failed too.
This supports omitting resource when the authorization server does not implement RFC 8707 (Auth0 ignores it locally but still forwards it upstream), or a per-server opt-out. modelcontextprotocol/modelcontextprotocol#1614.
@johakoch thanks — that third cell is important. Auth0-as-AS honouring RFC 8707 locally (exact-match on
resource, no Entra) is a different failure mode from Auth0+Entra federation where Auth0 ignoresresourcebut forwards it.Updated matrix for anyone hitting this:
resourcevs scope/audience prefix (trailing-slash / WHATWG normalize breaks it; slash hygiene helps).resourcelocally but forwards it; Entra rejects OIDC-only scopes → AADSTS9010010. Slash hygiene does not help; omitresource(or per-server opt-out) does.resource(exact-match, no Entra) — same class as (1): metadata advertises unslashed host-root, client sends slashed → Auth0 403 / opaque Oops page. Slash hygiene matters again.@SammedVJS thanks for running the omit-resource repro — confirmed: with
resource→ AADSTS9010010; without → federation succeeds. For anyone else reproducing: use a private window on the omit-resource pass; Auth0 session cookie can replay the failed transaction and make a clean B look broken.Confirming this still reproduces on Claude Code v2.1.282 against Microsoft's ADO MCP.
The upstream fix
modelcontextprotocol/typescript-sdk#2581shipped in@modelcontextprotocol/client@2.1.0on 2026-09-23.Config:
Result:
invalid_target: AADSTS9010010: The resource parameter provided in the request doesn't match with the requested scopes.
Claude Code is still sending resource=<origin>/ with the trailing slash, per this issue's original root cause. Waiting on an SDK dependency bump in Claude Code to pull in typescript-sdk 2.1.0.
+1 — also reproduces with an AWS Bedrock AgentCore Gateway MCP server, where the downstream authorization server is Auth0 rather than Entra ID directly. Auth0 is configured with an Enterprise (Azure AD) connection, so Entra enters the picture one hop removed: Auth0 federates the login to Entra, and that's where
AADSTS9010010surfaces.One data point that might be useful: in our case the advertised MCP resource URL already includes a path segment (not a bare origin), so the trailing-slash-normalization root cause described above may not be the only trigger — worth checking whether the fix also covers resource values that already have a path but still mismatch once relayed through a federated IdP hop.
Confirmed on the latest Claude Desktop build as of 2026-10-14 — still reproduces.