Skip to content

Don't override client_metadata.scopes if they are already set #2317

Description

@artdent

Initial Checks

Description

The scope selection strategy inside async_auth_flow unconditionally requests all available scopes. This overwrites the scope list that may have been explicitly set by the client. Being able to explicitly set the requested scopes is an important use case, either to reduce the permissions granted or because the server only permits certain scopes (despite advertising others).

From https://github.com/modelcontextprotocol/python-sdk/blob/v1.26.0/src/mcp/client/auth/oauth2.py#L553-L558:

                    # Step 3: Apply scope selection strategy
                    self.context.client_metadata.scope = get_client_metadata_scopes(
                        extract_scope_from_www_auth(response),
                        self.context.protected_resource_metadata,
                        self.context.oauth_metadata,
                    )

This could be conditional on if self.context.client_metadata.scope is None.

I see that this behavior was previously suggested in #1324 (comment) and rejected, on the basis that "Requesting all available scopes allows the authorization server and end-user to determine appropriate permissions during the consent process". However, I think this is worth revisiting. The specific motivating example here is the official SalesForce MCP server: if the client requests scopes that are not authorized for the given client application, the server rejects the request entirely.

Example Code

Python & MCP Python SDK

python 3.12.12
sdk 1.26.0

Activity

  1. Ram9199 commented on Jun 13, 2026

    @Ram9199

    I am collecting real-world incidents around MCP authorization and least-privilege behavior.

    Did this scope-overwrite behavior cause a real failed deployment, over-broad consent request, rejected enterprise connector, or changed auth practice? What changed afterward - explicit scope pinning, separate OAuth apps, manual review, custom auth code, audit logging, or no change?

    Not asking about any product. I am trying to separate current operational pain from pre-incident hardening.

  2. added
    enhancementRequest for a new feature that's not currently supported
    P2Moderate issues affecting some users, edge cases, potentially valuable feature
    authIssues and PRs related to Authentication / OAuth
    needs decisionIssue is actionable, needs maintainer decision on whether to implement
    on Aug 14, 2026
  3. haizaar commented on Sep 2, 2026

    @haizaar

    Hit this in production with a security consequence, which I think speaks directly
    to the rationale used to reject it in #1324.

    Setup: Hermes Agent (which sets client_metadata.scope from its own
    oauth.scope config key) against Bonusly's MCP server. The operator explicitly
    configured five scopes:

    user:read company:read recognition:read rewards:read recognition:write
    

    Bonusly's protected-resource metadata advertises 37 scopes in
    scopes_supported, 16 of them :administer — including billing:administer,
    finance:administer, user:administer and recognition:administer. Step 3
    overwrote the configured five, the client registered for all 37, and the
    resulting token carries all 37.

    Verified the server itself is not the cause. Registering directly against their
    /oauth/register:

    DCR request scope returned
    "scope": "user:read recognition:read recognition:write" exactly those 3
    no scope field empty
    what the SDK sent all 37

    So the AS honours a narrow request precisely. It granted everything because the
    SDK asked for everything.

    Why this rebuts the original objection

    The reasoning for rejecting the conditional was that "requesting all available
    scopes allows the authorization server and end-user to determine appropriate
    permissions during the consent process."

    In practice the consent screen can only offer what the client requested, so
    requesting everything removes the choice rather than enabling it. Ours rendered
    as:

    Grants Hermes Agent permission to:
      Read:   User information, Company information, Recognitions, Rewards, …
      Write:  User information, Recognitions, Awards, Incentives, …
      Manage: User information, Company information, Billing, Reports,
              Analytics, Finance, Recognitions, Rewards, …
    

    The end-user's options are "grant administrative access to billing and finance"
    or "deny". There is no per-scope toggle — few authorization servers offer one —
    so the client's request is the ceiling, and the consent screen is where the
    user discovers that their configuration was discarded.

    Two further points:

    1. It is silent. The application set scope and got no error, no warning,
      and no indication the value was dropped. From the operator's side the
      configuration simply had no effect, which took us a while to attribute.
    2. It defaults to maximum privilege. Any client on this SDK requests every
      scope every server advertises. That is fine where a server advertises only
      what it needs, and becomes a real exposure the moment a vendor lists admin
      scopes in scopes_supported — which is exactly what a "supported scopes"
      field is for.

    The spec's selection strategy seems right as a default for a client with no
    preference. The problem is applying it to a client that expressed one. The
    proposed if self.context.client_metadata.scope is None: preserves the strategy
    for everyone who has not set a scope, and stops silently discarding an explicit
    security decision for everyone who has.

    Happy to send that PR if it would help.

    Environment: mcp==2.0.0, Python 3.12, remote HTTP transport with DCR.

  4. haizaar commented on Sep 2, 2026

    @haizaar

    should the granted scope set returned by the authorization server be surfaced back to the client so a caller can detect when it received less authority than it asked for, before it calls a protected resource

    I say YES.

  5. adtyavrdhn commented on Sep 4, 2026

    @adtyavrdhn

    Additional reproducible security impact from pydantic/pydantic-ai-harness#791:

    Using FastMCP 3.4.5 and MCP Python SDK 1.26.0 against Google's official Gmail MCP endpoint, a read-only search_threads connection received a 401 without a scope parameter. Its protected-resource metadata advertised https://mail.google.com/, gmail.modify, and gmail.readonly. The SDK replaced the configured scope ceiling with all three values. The generated Google authorization URL then requested exactly those three scopes.

    This reproduces without credentials by intercepting the redirect URL. Supplying FastMCP OAuth(scopes=...) does not help because the SDK overwrites that value during protected-resource discovery.

    We have withheld automatic local OAuth from the Harness integration and require caller-managed bearer tokens or clients until an explicit scope list remains a ceiling through discovery and step-up.

  6. robertreppel commented on Sep 13, 2026

    @robertreppel

    Third independent reproduction, with a variant not described above: the overwrite can also silently narrow the requested scope below both the configured value and the server's own advertised scopes_supported — the opposite failure direction from the Bonusly case, same root cause.

    Setup: taylorwilsdon/google_workspace_mcp (self-hosted, FastMCP GoogleProvider) as the MCP server, Hermes Agent v0.21.0 as the client, with mcp_servers.<name>.oauth.scope configured to include documents, spreadsheets, drive.readonly, drive.file (plus baseline userinfo.email openid).

    The server's .well-known/oauth-authorization-server correctly lists all of those in scopes_supported — not a server misconfiguration. But it also (correctly, per spec) sends a WWW-Authenticate challenge with only a minimal required scope (userinfo.email openid) on the first unauthenticated request, since that's all it needs to accept a bearer token at the protocol layer — actual Drive/Docs/Sheets access is meant to be requested by the client on top of that.

    Per get_client_metadata_scopes's rule 1, that challenge value wins outright over both the configured scope and the broader scopes_supported advertisement. So in this deployment shape, the client silently ends up with less privilege than either the operator configured or the server made available — confirmed via a debug patch on _build_client_metadata showing the correctly-configured scope reaches OAuthClientMetadata before registration, while the actual DCR response and subsequent /authorize request both carry only the WWW-Authenticate-derived minimum.

    Adding this as a third independent corroboration (alongside the Bonusly and Gmail MCP reports above) that this is a real, cross-vendor problem, and that the "let the AS/consent screen decide" rationale from #1324's original rejection doesn't hold here either — no consent screen was ever involved for the narrowed-out scopes, since the request never reached the AS with more than the challenge-declared minimum.

  7. SomehowLiving commented on Oct 11, 2026

    @SomehowLiving

    Another reproduction, from a read-only client: SniffMCP (https://github.com/SomehowLiving/sniffmcp)
    only ever calls tools/list. Against a production MCP server whose PRM advertises
    profile.read profile.write applications.read applications.write projects.read projects.write …,
    the SDK (2.3) replaced our configured scope with all of scopes_supported, so users were asked to
    grant write access to a scanner that never writes.

    Our workaround shows why a supported hook would help: we subclass OAuthClientMetadata and override
    __setattr__ so scope is filtered every time the SDK assigns it (initial selection and step-up).
    It works, but it depends on SDK internals.

    +1 to respecting an explicitly configured scope. A scope_filter / allowlist on
    OAuthClientProvider would cover the "narrow what the server advertises" case too.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Moderate issues affecting some users, edge cases, potentially valuable featureauthIssues and PRs related to Authentication / OAuthenhancementRequest for a new feature that's not currently supportedneeds decisionIssue is actionable, needs maintainer decision on whether to implement

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions