Bug
When a CIMD client (e.g., Claude Code with client_id=https://claude.ai/oauth/claude-code-client-metadata) connects to an OAuthProxy/GoogleProvider server, it gets registered with only required_scopes instead of valid_scopes. This causes the MCP SDK's validate_scope to reject authorization requests with:
invalid_scope: Client was not registered with scope https://www.googleapis.com/auth/calendar
The error never reaches the upstream IdP — it's rejected at the proxy's own scope validation before building the upstream authorization URL.
Root Cause
In OAuthProxy.__init__ (proxy.py:363):
self._default_scope_str: str = " ".join(self.required_scopes or [])
This is used in two places as the fallback when a client doesn't specify scopes:
CIMDClientManager.__init__ — receives default_scope=self._default_scope_str, and get_client() uses it: scope=cimd_doc.scope or self.default_scope
register_client — uses it directly: scope=client_info.scope or self._default_scope_str
For case 2, the MCP SDK's RegistrationHandler applies default_scopes from ClientRegistrationOptions before calling register_client, so client_info.scope is already populated and the fallback is never hit.
For case 1 (CIMD clients), the CIMDClientManager creates clients directly — it never goes through the RegistrationHandler, so ClientRegistrationOptions.default_scopes is never applied. The only fallback is _default_scope_str, which contains required_scopes (e.g., ["openid"]) instead of valid_scopes (e.g., all Google Workspace scopes).
Reproduction
- Create a
GoogleProvider with required_scopes=["openid"] and valid_scopes=["openid", "https://www.googleapis.com/auth/calendar", ...]
- Connect with Claude Code (or any CIMD client whose metadata document has no
scope field)
- Authorize with any scope beyond
openid
- Get
invalid_scope error — the client was registered with only ["openid"]
Suggested Fix
In OAuthProxy.__init__, prefer valid_scopes over required_scopes for the default scope string:
# Before (line 363):
self._default_scope_str: str = " ".join(self.required_scopes or [])
# After:
self._default_scope_str: str = " ".join(
valid_scopes or self.required_scopes or []
)
This is consistent with how ClientRegistrationOptions already handles it — valid_scopes represents what clients can request, while required_scopes represents what tokens must have.
Workaround
Override _default_scope_str and the CIMD manager's default_scope after provider creation:
valid_scope_str = " ".join(all_valid_scopes)
provider._default_scope_str = valid_scope_str
if provider._cimd_manager is not None:
provider._cimd_manager.default_scope = valid_scope_str
Environment
- fastmcp 3.2.0
- mcp 1.26.0
- Client: Claude Code (CIMD client_id:
https://claude.ai/oauth/claude-code-client-metadata)
Bug
When a CIMD client (e.g., Claude Code with
client_id=https://claude.ai/oauth/claude-code-client-metadata) connects to anOAuthProxy/GoogleProviderserver, it gets registered with onlyrequired_scopesinstead ofvalid_scopes. This causes the MCP SDK'svalidate_scopeto reject authorization requests with:The error never reaches the upstream IdP — it's rejected at the proxy's own scope validation before building the upstream authorization URL.
Root Cause
In
OAuthProxy.__init__(proxy.py:363):This is used in two places as the fallback when a client doesn't specify scopes:
CIMDClientManager.__init__— receivesdefault_scope=self._default_scope_str, andget_client()uses it:scope=cimd_doc.scope or self.default_scoperegister_client— uses it directly:scope=client_info.scope or self._default_scope_strFor case 2, the MCP SDK's
RegistrationHandlerappliesdefault_scopesfromClientRegistrationOptionsbefore callingregister_client, soclient_info.scopeis already populated and the fallback is never hit.For case 1 (CIMD clients), the
CIMDClientManagercreates clients directly — it never goes through theRegistrationHandler, soClientRegistrationOptions.default_scopesis never applied. The only fallback is_default_scope_str, which containsrequired_scopes(e.g.,["openid"]) instead ofvalid_scopes(e.g., all Google Workspace scopes).Reproduction
GoogleProviderwithrequired_scopes=["openid"]andvalid_scopes=["openid", "https://www.googleapis.com/auth/calendar", ...]scopefield)openidinvalid_scopeerror — the client was registered with only["openid"]Suggested Fix
In
OAuthProxy.__init__, prefervalid_scopesoverrequired_scopesfor the default scope string:This is consistent with how
ClientRegistrationOptionsalready handles it —valid_scopesrepresents what clients can request, whilerequired_scopesrepresents what tokens must have.Workaround
Override
_default_scope_strand the CIMD manager'sdefault_scopeafter provider creation:Environment
https://claude.ai/oauth/claude-code-client-metadata)