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

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions