Skip to content

Scope overwrite in 403 upscoping prevents progressive authorization for servers with per-operation scopes #1582

@asoorm

Description

@asoorm

Describe the bug

The StreamableHTTPClientTransport 403 insufficient_scope handler overwrites this._scope with the scope from the WWW-Authenticate header instead of accumulating scopes across responses:

https://github.com/modelcontextprotocol/typescript-sdk/blob/main/packages/client/src/client/streamableHttp.ts#L540

if (scope) {
  this._scope = scope; // overwrites - previous scopes are lost
}

This causes an infinite re-authorization loop when an MCP server requires different scopes for different operations (progressive/step-up authorization).

The same overwrite exists on https://github.com/modelcontextprotocol/typescript-sdk/blob/main/packages/client/src/client/streamableHttp.ts#L507 in the 401 handler.

The Python SDK has the same behavior in https://github.com/modelcontextprotocol/python-sdk/blob/main/src/mcp/client/auth/oauth2.py.

To Reproduce

  1. Configure an MCP server with per-operation scopes:
  • initialize requires init scope
  • tools/list requires mcp:tools:read scope
  • tools/call (specific tool) requires mcp:tools:write
  1. When a request lacks the required scope, the server returns 403 with only the scopes that specific operation needs per https://datatracker.ietf.org/doc/html/rfc6750#section-3.1 (e.g. WWW-Authenticate: Bearer error="insufficient_scope", scope="mcp:tools:read")

  2. Connect with the TypeScript SDK using StreamableHTTPClientTransport with an OAuth provider

  3. After initialize succeeds (granted init), call tools/list

  4. The 403 handler sets _scope = "mcp:tools:read", re-authorizes, and the auth server grants mcp:tools:read - but init was not requested, so it is not included in the new token

  5. The next operation requiring init fails with 403, overwriting _scope = "init", losing mcp:tools:read

  6. Infinite loop between steps 5 and 6

Expected behavior

The transport should accumulate (union) scopes across 401/403 responses. After steps 1–5 above, _scope should be "init mcp:tools:read", not just "mcp:tools:read".

It is incorrect to expect the MCP server to solve this by returning all accumulated scopes in the WWW-Authenticate header because:

  • Per RFC 6750 §3.1, the scope attribute describes "the scope necessary to access the protected resource" - the specific resource being accessed, not every resource the client might access in the future.
  • The server has no knowledge of client-side token state. It validates the presented token and reports what's missing for this specific operation.
  • Different operations have different scope requirements. Including scopes beyond what the operation needs (e.g. returning init in a tools/list 403) would misrepresent the requirements of that operation.

The responsibility for accumulating scopes across operations belongs to the client, not the server.

Related: #1039, #1115, #1151, #941, #1317

Metadata

Metadata

Assignees

No one assigned

    Labels

    authIssues and PRs related to Authentication / OAuthbugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions