Preflight Checklist
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
-
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.
-
Configure .mcp.json:
{
"mcpServers": {
"bc-mcp": {
"type": "http",
"url": "https://mcp.businesscentral.dynamics.com",
"headers": {
"TenantId": "<tenant-id>",
"EnvironmentName": "Production",
"Company": "<company>",
"ConfigurationName": "MCP Server"
},
"oauth": {
"clientId": "<entra-app-client-id>",
"callbackPort": 8080
}
}
}
}
-
Register the client secret (via claude mcp add … --client-secret or MCP_CLIENT_SECRET env var).
-
Start Claude Code → run /mcp → select bc-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 BROWSER with a script that logs its argv to a file:
printf '#!/bin/bash\nprintf "%%s\\n" "$@" >> /tmp/auth-url.log\nexit 0\n' > /tmp/log-browser.sh
chmod +x /tmp/log-browser.sh
BROWSER=/tmp/log-browser.sh claude
Run /mcp again. URL-decode the captured URL and observe the resource=…com/ trailing slash that is not present in the MCP metadata.
Claude Model
Opus
Is this a regression?
I don't know
Last Working Version
No response
Claude Code Version
2.1.119 (Claude Code)
Platform
Anthropic API
Operating System
macOS
Terminal/Shell
Other
Additional Information
Suggested fix. Preserve the resource string from the MCP metadata verbatim — do not round-trip it through URL. If normalization is desired for comparison, strip a single trailing slash before emitting the OAuth requests. Both emission points need the fix:
- the
resource query param on /authorize
- the
resource body param on /token
Workaround for anyone hitting this today. Bypass Claude Code's OAuth entirely: run a standalone PKCE flow against the tenant-specific Entra endpoint (with the correct un-slashed resource URI), obtain an access token, and inject it as "Authorization": "Bearer …" in .mcp.json's headers. Tokens expire hourly and must be refreshed manually.
Secondary issue (possibly separate). The BC MCP server's metadata advertises authorization_servers: ["https://login.microsoftonline.com/common/v2.0"], but single-tenant Entra apps created after 2018-10-15 cannot use /common/ (Entra returns AADSTS50194). Currently Claude Code offers no way to override the authorization endpoint (e.g. a tenantId / authorizeEndpoint field under oauth), forcing users to either flag their app as multi-tenant or work around the flow manually. A config key would solve this cleanly. Happy to file this as a separate issue if preferred.
Preflight Checklist
What's Wrong?
Claude Code's MCP OAuth client appends a trailing
/to theresourceparameter on both the/authorizeredirect and the/tokenexchange. When the MCP server's.well-known/oauth-protected-resourcemetadata declares the resource URI without a trailing slash, theresourceparameter no longer matches the implicit resource of the requestedscope. Microsoft Entra ID rejects the request withAADSTS9010010: 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 slashEntra string-compares
resourceagainst 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
resourceparameter emitted on/authorizeand/tokenshould match theresourcestring returned by the MCP server's/.well-known/oauth-protected-resourcemetadata 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
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:{ "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:Run
/mcpagain. URL-decode the captured URL and observe theresource=…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
resourcestring from the MCP metadata verbatim — do not round-trip it throughURL. If normalization is desired for comparison, strip a single trailing slash before emitting the OAuth requests. Both emission points need the fix:resourcequery param on/authorizeresourcebody param on/tokenWorkaround 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'sheaders. 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 returnsAADSTS50194). Currently Claude Code offers no way to override the authorization endpoint (e.g. atenantId/authorizeEndpointfield underoauth), 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.