Repository navigation
OAuth scope selection falls back to the authorization server's scopes_supported, which the spec does not allow #3782
Description
Initial Checks
- I confirm that I'm using the latest version of MCP Python SDK
- I confirm that I searched for my issue in https://github.com/modelcontextprotocol/python-sdk/issues before opening this issue
Description
When the 401 carries no scope and the Protected Resource Metadata (PRM) has no scopes_supported, get_client_metadata_scopes (src/mcp/client/auth/utils.py) requests every scope the authorization server lists:
# MCP spec scope selection priority:
# 1. WWW-Authenticate header scope
# 2. PRM scopes_supported
# 3. AS scopes_supported (SDK fallback)
# 4. Omit scope parameter
...
elif authorization_server_metadata is not None and authorization_server_metadata.scopes_supported is not None:
selected_scope = " ".join(authorization_server_metadata.scopes_supported)The spec has two steps, and the second one ends in omitting scope (2026-07-28, Authorization → Scope Selection Strategy):
- Use
scopeparameter from the initialWWW-Authenticateheader in the 401 response, if provided- If
scopeis not available, use all scopes defined inscopes_supportedfrom the Protected Resource Metadata document, omitting thescopeparameter ifscopes_supportedis undefined.
The same section says clients SHOULD request "only the scopes necessary for their intended operations."
An authorization server's scopes_supported describes the whole authorization server, not this resource. Often that's a shared IdP serving many APIs (openid, profile, email, org-admin and write scopes for unrelated services). Tier 3 therefore turns "the resource didn't say" into "ask for everything the IdP has". The results:
- an over-privileged token for an MCP server that needed none of it;
- a broad consent screen for the user;
- a login failure when the authorization server refuses to issue some of the listed scopes to a public or dynamically registered client (
invalid_scope), where omittingscopewould have succeeded with the authorization server's default.
Example Code
The selection function on its own (works on 1.26.0, 1.30.0 and 2.3.0):
from mcp.client.auth.utils import get_client_metadata_scopes
from mcp.shared.auth import OAuthMetadata, ProtectedResourceMetadata
prm = ProtectedResourceMetadata(
resource="https://mcp.example.com/mcp",
authorization_servers=["https://auth.example.com"],
) # no scopes_supported
asm = OAuthMetadata(
issuer="https://auth.example.com",
authorization_endpoint="https://auth.example.com/authorize",
token_endpoint="https://auth.example.com/token",
scopes_supported=["openid", "profile", "email", "admin:org", "repo:write"],
)
print(get_client_metadata_scopes(None, prm, asm))
# 'openid profile email admin:org repo:write' — expected: None (omit scope)In the real flow, OAuthClientProvider.async_auth_flow assigns this result to client_metadata.scope on every 401. So a client can't avoid it by configuring a narrower scope: that is #2317, which has several production reports.
Expected behaviour
Follow the spec: (1) the challenge scope, (2) PRM scopes_supported, otherwise omit scope. If a fallback beyond the spec is wanted, make it something a client turns on, not the default.
Relation to existing issues
- Don't override client_metadata.scopes if they are already set #2317: a configured
client_metadata.scopeis overwritten. This issue is separate: even with no configured scope, the SDK should omitscoperather than take the authorization server's list. Fixing Don't override client_metadata.scopes if they are already set #2317 alone would leave clients that don't configure a scope on tier 3. - Fall back to the caller-configured scope when the server advertises none #3172 (closed): put the configured scope after tier 3. That doesn't help here, because the AS-wide list still wins whenever the authorization server advertises any scopes.
Python & MCP Python SDK
- MCP Python SDK: reproduced on 1.26.0, 1.30.0, 2.3.0. The same code is on
mainatd7845c0. - Python 3.12
Activity
jin-bo commentedon Oct 28, 2026
Duplicate of #3781, posted twice by mistake.