modelcontextprotocol / modelcontextprotocol/typescript-sdk
Scope overwrite in 403 upscoping prevents progressive authorization for servers with per-operation scopes
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 13.4k
- Forks
- 2.2k
- Avg merge
- 3d 15h
- Merged PRs (30d)
- 4
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.
Related: #1039, #1115, #1151, #941, #1317
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in packages/client/src/client/streamableHttp.ts at the 401 and 403 handlers around lines 507 and 540, and trace how _scope changes during OAuth reauthorization. Use the listed sequential init, tools/list, and tools/call scenario to validate the behavior; done means scopes learned from earlier challenges remain available and the authorization loop ends.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- api, authentication
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 65/100