Title: SEP-2350: accumulate scopes on step-up auth by dogacancolak · Pull Request #2676 · modelcontextprotocol/python-sdk · GitHub
Open Graph Title: SEP-2350: accumulate scopes on step-up auth by dogacancolak · Pull Request #2676 · modelcontextprotocol/python-sdk
X Title: SEP-2350: accumulate scopes on step-up auth by dogacancolak · Pull Request #2676 · modelcontextprotocol/python-sdk
Description: 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 read → write → admin 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.
Open Graph Description: 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 t...
X Description: 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 t...
Opengraph URL: https://github.com/modelcontextprotocol/python-sdk/pull/2676
X: @github
Domain: github.com
| route-pattern | /:user_id/:repository/pull/:id/files(.:format) |
| route-controller | pull_requests |
| route-action | files |
| fetch-nonce | v2:055d86a6-7941-afaf-66b7-ebd99d0f8942 |
| current-catalog-service-hash | ae870bc5e265a340912cde392f23dad3671a0a881730ffdadd82f2f57d81641b |
| request-id | 945A:3D725F:1D4441C:2A18D6C:6A58D4AA |
| html-safe-nonce | cc0c094f14d3abfb7e4157e5fa178895417f4e6d12c52b9ff1785105fefc3e71 |
| visitor-payload | eyJyZWZlcnJlciI6IiIsInJlcXVlc3RfaWQiOiI5NDVBOjNENzI1RjoxRDQ0NDFDOjJBMThENkM6NkE1OEQ0QUEiLCJ2aXNpdG9yX2lkIjoiODE2NzAxNTg3NzE4MDk3MDE1NCIsInJlZ2lvbl9lZGdlIjoiaWFkIiwicmVnaW9uX3JlbmRlciI6ImlhZCJ9 |
| visitor-hmac | 810c1be9fcc8ecd070b884d48edc2a4f6937d25f4f56caf03588a1d81bd86435 |
| hovercard-subject-tag | pull_request:3738959452 |
| github-keyboard-shortcuts | repository,pull-request-list,pull-request-conversation,pull-request-files-changed,copilot |
| google-site-verification | Apib7-x98H0j5cPqHWwSMm6dNU4GmODRoqxLiDzdx9I |
| octolytics-url | https://collector.github.com/github/collect |
| analytics-location | / |
| fb:app_id | 1401488693436528 |
| apple-itunes-app | app-id=1477376905, app-argument=https://github.com/modelcontextprotocol/python-sdk/pull/2676/files |
| twitter:image | https://avatars.githubusercontent.com/u/54953926?s=400&v=4 |
| twitter:card | summary_large_image |
| og:image | https://avatars.githubusercontent.com/u/54953926?s=400&v=4 |
| og:image:alt | 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 t... |
| og:site_name | GitHub |
| og:type | object |
| hostname | github.com |
| expected-hostname | github.com |
| None | 5f2a0c7865178af3d91dd9f13b0cdfc3c73a2529c873d2780bb4c01271a57ec6 |
| turbo-cache-control | no-preview |
| diff-view | unified |
| go-import | github.com/modelcontextprotocol/python-sdk git https://github.com/modelcontextprotocol/python-sdk.git |
| octolytics-dimension-user_id | 182288589 |
| octolytics-dimension-user_login | modelcontextprotocol |
| octolytics-dimension-repository_id | 862584018 |
| octolytics-dimension-repository_nwo | modelcontextprotocol/python-sdk |
| octolytics-dimension-repository_public | true |
| octolytics-dimension-repository_is_fork | false |
| octolytics-dimension-repository_network_root_id | 862584018 |
| octolytics-dimension-repository_network_root_nwo | modelcontextprotocol/python-sdk |
| turbo-body-classes | logged-out env-production page-responsive full-width |
| disable-turbo | true |
| browser-stats-url | https://api.github.com/_private/browser/stats |
| browser-errors-url | https://api.github.com/_private/browser/errors |
| release | 8aae7b8d6caacacf5c66eaeb2702d8dc88d85b4a |
| ui-target | full |
| theme-color | #1e2327 |
| color-scheme | light dark |
Links:
Viewport: width=device-width