-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Description
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:
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
- Configure an MCP server with per-operation scopes:
- initialize requires
initscope - tools/list requires
mcp:tools:readscope - tools/call (specific tool) requires
mcp:tools:write
-
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")
-
Connect with the TypeScript SDK using StreamableHTTPClientTransport with an OAuth provider
-
After initialize succeeds (granted init), call tools/list
-
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
-
The next operation requiring init fails with 403, overwriting _scope = "init", losing mcp:tools:read
-
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.