Problem
The MCP Python SDK sends an RFC 8707 resource parameter on all token requests — including refresh_token grants. Microsoft Entra ID v2.0 rejects this with AADSTS9010010 (The resource parameter provided in the request doesn't match with the requested scopes).
This causes MCP servers using Entra ID OAuth to lose authentication after ~1 hour when the access token expires and the SDK attempts a silent refresh.
Root Cause
Two compounding issues:
1. Entra v2.0 does not support resource on refresh
Entra's v2.0 token endpoint expects scope, not resource. The resource parameter is a v1.0 concept. Since March 2026, Entra strictly validates and rejects resource on token refresh (previously it was silently ignored).
2. Pydantic v2 AnyHttpUrl trailing-slash normalization
ProtectedResourceMetadata.resource is typed as AnyHttpUrl (shared/auth.py:143). When str() is called on a bare-domain URL, Pydantic v2 adds a trailing slash:
>>> str(AnyHttpUrl("https://mcp-server.example.com"))
'https://mcp-server.example.com/' # trailing slash added
In get_resource_url() (client/auth/oauth2.py:155), this trailing-slash version is used:
prm_resource = str(self.protected_resource_metadata.resource) # adds trailing slash
But the Entra app registration has the audience as https://mcp-server.example.com (no slash), so the resource and scope audience don't match.
Affected Code
src/mcp/client/auth/oauth2.py:
async def _refresh_token(self) -> httpx.Request:
refresh_data = {
"grant_type": "refresh_token",
"refresh_token": self.context.current_tokens.refresh_token,
"client_id": self.context.client_info.client_id,
}
# This sends 'resource' on refresh — Entra v2.0 rejects it
if self.context.should_include_resource_param(self.context.protocol_version):
refresh_data["resource"] = self.context.get_resource_url() # RFC 8707
The same issue exists in the TypeScript SDK (packages/client/src/client/auth.ts), where WHATWG URL also normalizes bare-domain URLs with a trailing slash.
Suggested Fix
Option A: Strip trailing slash in get_resource_url()
def get_resource_url(self) -> str:
resource = resource_url_from_server_url(self.server_url)
if self.protected_resource_metadata and self.protected_resource_metadata.resource:
prm_resource = str(self.protected_resource_metadata.resource).rstrip('/')
if check_resource_allowed(requested_resource=resource, configured_resource=prm_resource):
resource = prm_resource
return resource
Option B: Include scope alongside resource on refresh
Entra v2.0 tolerates resource if scope is also present and consistent:
refresh_data["scope"] = " ".join(self.context.scopes)
Option C: Make resource on refresh configurable
Allow servers to signal whether the resource parameter should be included on refresh grants, since not all authorization servers support RFC 8707.
Related Issues
Environment
- MCP Python SDK: v1.27.0
- Authorization server: Microsoft Entra ID v2.0
- MCP server: Azure Container Apps with custom EntraTokenVerifier
- MCP spec version: 2025-06-18 (mandates RFC 8707
resource)
Current Workaround
Server-side: set resource_server_url=None in AuthSettings and do NOT serve /.well-known/oauth-protected-resource metadata. Without PRM, should_include_resource_param() returns False and resource is omitted from refresh requests. Initial auth still works via the WWW-Authenticate header fallback.
Problem
The MCP Python SDK sends an RFC 8707
resourceparameter on all token requests — includingrefresh_tokengrants. Microsoft Entra ID v2.0 rejects this with AADSTS9010010 (The resource parameter provided in the request doesn't match with the requested scopes).This causes MCP servers using Entra ID OAuth to lose authentication after ~1 hour when the access token expires and the SDK attempts a silent refresh.
Root Cause
Two compounding issues:
1. Entra v2.0 does not support
resourceon refreshEntra's v2.0 token endpoint expects
scope, notresource. Theresourceparameter is a v1.0 concept. Since March 2026, Entra strictly validates and rejectsresourceon token refresh (previously it was silently ignored).2. Pydantic v2
AnyHttpUrltrailing-slash normalizationProtectedResourceMetadata.resourceis typed asAnyHttpUrl(shared/auth.py:143). Whenstr()is called on a bare-domain URL, Pydantic v2 adds a trailing slash:In
get_resource_url()(client/auth/oauth2.py:155), this trailing-slash version is used:But the Entra app registration has the audience as
https://mcp-server.example.com(no slash), so theresourceandscopeaudience don't match.Affected Code
src/mcp/client/auth/oauth2.py:The same issue exists in the TypeScript SDK (
packages/client/src/client/auth.ts), where WHATWGURLalso normalizes bare-domain URLs with a trailing slash.Suggested Fix
Option A: Strip trailing slash in
get_resource_url()Option B: Include
scopealongsideresourceon refreshEntra v2.0 tolerates
resourceifscopeis also present and consistent:Option C: Make
resourceon refresh configurableAllow servers to signal whether the
resourceparameter should be included on refresh grants, since not all authorization servers support RFC 8707.Related Issues
resourceparameter, breaking Entra ID auth (AADSTS9010010) anthropics/claude-code#52871 — same trailing-slash + AADSTS9010010 bugresourceparameter conflicts withscopeon v2.0 endpoint microsoft/powerbi-modeling-mcp#68 — same Entra v2.0 incompatibilityEnvironment
resource)Current Workaround
Server-side: set
resource_server_url=NoneinAuthSettingsand do NOT serve/.well-known/oauth-protected-resourcemetadata. Without PRM,should_include_resource_param()returnsFalseandresourceis omitted from refresh requests. Initial auth still works via theWWW-Authenticateheader fallback.