Description
rmcp's OAuth authorization-code client flow (crates/rmcp/src/transport/auth.rs, auth feature) offers only all-or-nothing scope control:
- Leave
AuthorizationRequest::scopes / OAuthClientConfig::scopes empty and the SDK auto-selects and requests every scope discovered from the server (WWW-Authenticate challenge, protected-resource metadata, authorization-server metadata) via AuthorizationManager::select_scopes.
- Supply
with_scopes(...) and the caller must specify the full list themselves, losing the SDK's discovery/offline_access (SEP-2207)/step-up (SEP-2350)/dedup logic.
There is no way to exclude a single problematic scope the server advertises while still keeping the rest of auto-discovery.
This matters because it's not hypothetical: Google's Gmail MCP server advertises gmail.metadata in scopes_supported, but the Gmail API rejects the q search parameter on any token carrying gmail.metadata — even when gmail.readonly is also granted (a long-documented Gmail restriction, see googleapis/google-api-python-client#582). A client that lets rmcp auto-select scopes ends up with a token that silently breaks search, with no client-side remedy short of abandoning auto-discovery and hand-replicating its offline_access/step-up/dedup logic.
The Go SDK just added exactly this capability: modelcontextprotocol/go-sdk#1044 adds an optional ScopeFilter func(discovered []string) []string hook to AuthorizationCodeHandlerConfig, run after scope discovery but before the offline_access/step-up union logic, letting a caller narrow, extend, or reorder the discovered set.
There is also an open (not yet merged) upstream PR, modelcontextprotocol/rust-sdk#1102, that changes rmcp's scope handling to union configured scopes with server-required ones through a stable dedup policy — but it only adds scopes; it does not add any exclusion mechanism, so this gap persists regardless of whether/when modelcontextprotocol#1102 lands.
Reproduction Steps
- Configure an
AuthorizationRequest / OAuthClientConfig against a server whose discovered scopes_supported (or WWW-Authenticate challenge) includes a scope that is mutually exclusive with another required scope (e.g. Gmail's gmail.metadata vs. gmail.readonly).
- Leave
scopes empty (or use with_scopes only to add required scopes) to benefit from auto-discovery.
- Observe: the SDK requests the full discovered set, including the problematic scope, with no way to filter it out short of replacing auto-discovery entirely.
Expected Behavior
A caller can supply an optional callback/hook that receives the SDK's discovered scope set and returns the set to actually request, composing with (not bypassing) the existing offline_access/step-up union logic and whatever scope-merge policy modelcontextprotocol/rust-sdk#1102 eventually lands with. No hook set → unchanged (fully backward compatible) behavior.
Actual Behavior
No such hook exists (grep -n "ScopeFilter\|scope_filter" crates/rmcp/src/transport/auth.rs returns nothing). The only escape hatch, with_scopes(...), fully replaces auto-discovery rather than filtering it.
Environment
- Version: rmcp 3.1.2 (commit 02c62ae)
- Features:
auth
Upstream
modelcontextprotocol/go-sdk#1044 — MERGED, adds the ScopeFilter hook this issue asks to mirror.
modelcontextprotocol/rust-sdk#1102 — OPEN, additive scope-union PR that this capability should compose with once merged; does not itself add an exclusion mechanism, so this finding is not a duplicate of it.
No existing upstream rust-sdk issue/PR found proposing a scope-filter/exclusion hook specifically (checked via gh issue list / gh pr list --search "scope filter" and related terms).
Logs / Evidence
N/A — competitive-parity research finding, not a live failure reproduction.
Spec
.local/specs/001-oauth-scope-filter-hook/spec.md
Description
rmcp's OAuth authorization-code client flow (crates/rmcp/src/transport/auth.rs,authfeature) offers only all-or-nothing scope control:AuthorizationRequest::scopes/OAuthClientConfig::scopesempty and the SDK auto-selects and requests every scope discovered from the server (WWW-Authenticatechallenge, protected-resource metadata, authorization-server metadata) viaAuthorizationManager::select_scopes.with_scopes(...)and the caller must specify the full list themselves, losing the SDK's discovery/offline_access (SEP-2207)/step-up (SEP-2350)/dedup logic.There is no way to exclude a single problematic scope the server advertises while still keeping the rest of auto-discovery.
This matters because it's not hypothetical: Google's Gmail MCP server advertises
gmail.metadatainscopes_supported, but the Gmail API rejects theqsearch parameter on any token carryinggmail.metadata— even whengmail.readonlyis also granted (a long-documented Gmail restriction, seegoogleapis/google-api-python-client#582). A client that letsrmcpauto-select scopes ends up with a token that silently breaks search, with no client-side remedy short of abandoning auto-discovery and hand-replicating its offline_access/step-up/dedup logic.The Go SDK just added exactly this capability:
modelcontextprotocol/go-sdk#1044adds an optionalScopeFilter func(discovered []string) []stringhook toAuthorizationCodeHandlerConfig, run after scope discovery but before the offline_access/step-up union logic, letting a caller narrow, extend, or reorder the discovered set.There is also an open (not yet merged) upstream PR,
modelcontextprotocol/rust-sdk#1102, that changesrmcp's scope handling to union configured scopes with server-required ones through a stable dedup policy — but it only adds scopes; it does not add any exclusion mechanism, so this gap persists regardless of whether/when modelcontextprotocol#1102 lands.Reproduction Steps
AuthorizationRequest/OAuthClientConfigagainst a server whose discoveredscopes_supported(orWWW-Authenticatechallenge) includes a scope that is mutually exclusive with another required scope (e.g. Gmail'sgmail.metadatavs.gmail.readonly).scopesempty (or usewith_scopesonly to add required scopes) to benefit from auto-discovery.Expected Behavior
A caller can supply an optional callback/hook that receives the SDK's discovered scope set and returns the set to actually request, composing with (not bypassing) the existing offline_access/step-up union logic and whatever scope-merge policy
modelcontextprotocol/rust-sdk#1102eventually lands with. No hook set → unchanged (fully backward compatible) behavior.Actual Behavior
No such hook exists (
grep -n "ScopeFilter\|scope_filter" crates/rmcp/src/transport/auth.rsreturns nothing). The only escape hatch,with_scopes(...), fully replaces auto-discovery rather than filtering it.Environment
authUpstream
modelcontextprotocol/go-sdk#1044— MERGED, adds theScopeFilterhook this issue asks to mirror.modelcontextprotocol/rust-sdk#1102— OPEN, additive scope-union PR that this capability should compose with once merged; does not itself add an exclusion mechanism, so this finding is not a duplicate of it.No existing upstream
rust-sdkissue/PR found proposing a scope-filter/exclusion hook specifically (checked viagh issue list/gh pr list --search "scope filter"and related terms).Logs / Evidence
N/A — competitive-parity research finding, not a live failure reproduction.
Spec
.local/specs/001-oauth-scope-filter-hook/spec.md