Skip to content

SEP-2350: accumulate scopes on step-up auth#2676

Open
dogacancolak wants to merge 3 commits into
modelcontextprotocol:mainfrom
dogacancolak:dc/mcp-python-sdk/scope-accumulation-step-up
Open

SEP-2350: accumulate scopes on step-up auth#2676
dogacancolak wants to merge 3 commits into
modelcontextprotocol:mainfrom
dogacancolak:dc/mcp-python-sdk/scope-accumulation-step-up

Conversation

@dogacancolak
Copy link
Copy Markdown
Contributor

Implements MCP spec PR #2350 (closes #2349): on a 403 insufficient_scope step-up challenge, the client now unions the challenge scopes with its previously-requested scope set instead of replacing them. Stateless servers can now emit per-operation WWW-Authenticate challenges (the spec-recommended posture) without forcing the client to drop prior grants on every step-up.

Motivation and Context

The previous spec told servers to include "previously granted scopes" in insufficient_scope challenges, conflicting with RFC 6750 §3.1. The amended spec moves accumulation to the client.

The SDK was replacing client_metadata.scope with the challenge scopes, so a client doing readwriteadmin operations would lose prior scopes on each step-up and trigger re-auth loops.

What changed

  • 403 step-up branch in src/mcp/client/auth/oauth2.py now calls union_scopes(client_metadata.scope, challenge_scopes) instead of overwriting. The 401 branch is unchanged (replace semantics) — full re-login remains the natural down-scoping opportunity.
  • Union source is client_metadata.scope (what we requested last time), not current_tokens.scope (what the AS granted). If the AS narrowed the grant at consent time, we still re-request the broader set on the next step-up rather than silently dropping scopes the user previously declined.

Behavior

  • read requested → 403 scope="write" → re-auth with read write.
  • read write requested, granted read only → 403 scope="delete" → re-auth with read write delete (write survives the narrow grant).
  • 401 (expired token) → scope is replaced by initial set, accumulated set is dropped. This is the down-scoping path.
  • Happy path with a valid token → scope untouched.

Tests

Unit tests for union_scopes.

End-to-end tests driving async_auth_flow cover the entire step-up branch, not just the union behavior:

  • Step-up retry uses the new access token. After step-up succeeds, the retried request carries the stepped-up token, not the rejected one.
  • Multi-step accumulation. read → step-up adds write → step-up adds admin ends with read write admin, not just admin.
  • Union source is requested, not granted. If the user requested read write admin but the AS only granted read write at consent, a later step-up for delete re-authorizes with read write admin delete — the declined admin is re-requested rather than silently dropped.
  • No scope in challenge falls back to PRM. A step-up challenge with no scope attribute uses the priority ladder (PRM, then AS metadata).
  • Non-step-up 403 is a no-op. A 403 without insufficient-scope does not trigger re-auth.
  • 401 is the down-scoping opportunity. A fresh 401 replaces an accumulated read write admin with the initial auth handshake's narrower read. Pins the invariant so a future "let's union on 401 too" change fails loudly.
  • Happy path does not mutate scope. A successful request with a valid token leaves the requested scope set untouched.
  • Step-up errors are logged and re-raised. Failures during step-up are surfaced rather than silently swallowed.

Limitations / follow-ups

  • Refresh-token path does not re-evaluate scope. As long as the refresh token is valid, the over-permissive accumulated set stays — the 401 down-scoping path is never hit. This is a limitation of the refresh_token flow itself.

@dogacancolak dogacancolak changed the title SEP-2350: union scopes on 403 step-up auth SEP-2350: accumulate scopes on step-up auth May 24, 2026
@dogacancolak dogacancolak marked this pull request as ready for review May 25, 2026 01:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Clarify client-side scope accumulation behaviour during step-up authorization

1 participant