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

Status Open
Reported on v2.1.119
Maintainer reply None cached
Activity 40 comments · opened Apr 24, 2026

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 ↗

40 Comments

daankuhlmann-SH · 4 months ago

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

CrazyICT · 4 months ago

I've issues with this aswel

reidwatersbhm · 4 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 · 3 months ago

Also problematic for me

jpparis02 · 3 months ago

having the same issue

asnape81 · 3 months ago

I'm having the same issue.

chajtus · 3 months ago

This is also issue for me, any fix?

Lukasz-3dhubs · 3 months 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 · 3 months 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 · 3 months 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 · 2 months 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 · 2 months ago

+1 same issue for me

juanfran-n8n · 2 months ago

+1, same issue here

ygemara · 2 months ago

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

glenn-zarb · 2 months ago

+1, same issue here

tannercorbin · 2 months ago

+1, same issue here

sonmaximum · 2 months 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 · 2 months 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 · 2 months ago

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

feifanl · 2 months ago

+1, same issue here

zhouxingyu-kinto · 2 months ago

+1, same issue here

cailyoung · 2 months ago

+1 trailing slash is blocking us

gavearlima · 2 months ago

+1 same issue

matzegebbe · 2 months 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 · 2 months ago

+1 same issue

santanaguy · 2 months 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 · 2 months 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 · 2 months 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 · 2 months ago

⏫ this needs to be fixed!

Marshall-Milbourn · 2 months ago

we're blocked by this in production

Coppo459 · 1 month 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 · 1 month 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 · 1 month 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 · 1 month 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. 😃

stephaneey · 29 days ago

Same here with the latest preview.

aarora79 · 21 days ago

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 AADSTS9010010 at token exchange.

A data point that corroborates the WHATWG-normalization root cause and doubles as a workaround for anyone stuck:

  • When our PRM advertises a bare-origin resource (https://gw, no trailing slash, verified via curl), Claude Code sends resource=https://gw/ on /authorize (origin + appended /). Entra can never match this, because App ID URIs must not end in a slash and Entra compares resource against identifierUris by exact string. Bare-origin resource is therefore unresolvable on Entra on both sides at once.
  • When we instead advertise a path-qualified resource (https://gw/<server>/mcp), Claude Code sends it verbatim with no appended slash, and login succeeds. This is consistent with the root cause: URL.href only 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 identifierUri in 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 PRM resource verbatim would collapse this back to a single origin registration.

Strong +1 on the proposed fix: preserve the metadata resource string verbatim and do not round-trip it through URL. Both emission points (/authorize query param and /token body param) need it.

cc @omrishiv

---

Update: the root cause lives in the MCP TypeScript SDK, not Claude Code itself — selectResourceURL in packages/client/src/client/auth.ts returns new URL(resourceMetadata.resource), and URL.href is what appends the slash. Tracked upstream as modelcontextprotocol/typescript-sdk#1968, with a fix in modelcontextprotocol/typescript-sdk#1991 that preserves the metadata resource verbatim (we reviewed it and endorsed it here). Claude Code picks the fix up once it bumps the SDK dependency.

whiskeysierra · 21 days ago

@aarora79 Isn't this exactly the workaround I described in https://github.com/anthropics/claude-code/issues/52871#issuecomment-4602137405?

aarora79 · 21 days ago

@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 identifierUri for 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.

whiskeysierra · 21 days ago

@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.

aarora79 · 20 days ago

@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.