Skip to content

fix: accumulate OAuth scopes across 401/403 responses for progressive authorization#1601

Closed
giulio-leone wants to merge 3 commits intomodelcontextprotocol:v1.xfrom
giulio-leone:fix/oauth-scope-accumulation
Closed

fix: accumulate OAuth scopes across 401/403 responses for progressive authorization#1601
giulio-leone wants to merge 3 commits intomodelcontextprotocol:v1.xfrom
giulio-leone:fix/oauth-scope-accumulation

Conversation

@giulio-leone
Copy link
Contributor

Summary

The StreamableHTTPClientTransport 401/403 handlers overwrite this._scope with the scope from the WWW-Authenticate header, discarding previously granted scopes. This causes infinite re-authorization loops when an MCP server requires different scopes for different operations (progressive/step-up authorization).

Root Cause

// 401 handler (line 497)
this._scope = scope;  // overwrites — previous scopes are lost

// 403 handler (line 527)
this._scope = scope;  // same problem

When a server requires init scope for initialize and mcp:tools:read for tools/list:

  1. initialize → 403 → _scope = "init" → re-auth → token gets init
  2. tools/list → 403 → _scope = "mcp:tools:read" → re-auth → token gets mcp:tools:read but loses init
  3. Next operation needing init → 403 → infinite loop

Fix

Added a mergeScopes() helper that unions space-delimited OAuth scope strings:

function mergeScopes(existing: string | undefined, incoming: string | undefined): string | undefined {
    const merged = new Set(existing.split(' '));
    for (const scope of incoming.split(' ')) merged.add(scope);
    return [...merged].join(' ');
}

Applied at both the 401 and 403 handler sites. Per RFC 6750 §3.1, servers report only the scopes needed for the specific operation, so scope accumulation is the client's responsibility.

The existing _lastUpscopingHeader infinite loop protection still works: if the same header appears twice, it throws. Different operations produce different headers, so progressive auth flows through correctly.

Tests

Added 1 new test: accumulates scopes across multiple 403 responses for progressive authorization — verifies that after two 403 responses with different scopes (read, then write), the second auth call includes the accumulated scope "read write".

Verification

2 consecutive clean test runs: 1548/1548 tests passed, 0 failures.

Fixes #1582

@changeset-bot
Copy link

changeset-bot bot commented Feb 27, 2026

🦋 Changeset detected

Latest commit: 4972dd2

The changes in this PR will be included in the next version bump.

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new
Copy link

pkg-pr-new bot commented Feb 27, 2026

Open in StackBlitz

npm i https://pkg.pr.new/@modelcontextprotocol/sdk@1601

commit: 38ae91b

g97iulio1609 added 3 commits February 28, 2026 15:50
… authorization

The 401 and 403 handlers in StreamableHTTPClientTransport overwrite
`this._scope` with the scope from WWW-Authenticate, discarding
previously granted scopes. This causes infinite re-authorization loops
when an MCP server requires different scopes for different operations
(progressive/step-up authorization).

Per RFC 6750 §3.1, servers report only the scopes needed for the
specific operation being accessed. The client must accumulate scopes
across responses.

This adds a mergeScopes() helper that unions space-delimited scope
strings, and applies it at both the 401 and 403 handler sites.

Fixes modelcontextprotocol#1582
@giulio-leone
Copy link
Contributor Author

All CI checks pass. Ready for review.

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.

1 participant