You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Advertise scopes_supported separately from required_scopes #3778
AuthSettings.required_scopes is used both to refuse tokens (RequireAuthMiddleware) and as scopes_supported in the protected resource metadata. So a server can't advertise a scope without requiring it on every request.
Our server's read tools need returns:read. One write tool also needs returns:write, which its handler checks with get_access_token(). We want read-only tokens to keep working, so we set required_scopes=["returns:read"]. But then the metadata lists only returns:read, and a client that takes its scopes from scopes_supported never asks for returns:write.
auth=AuthSettings(
issuer_url=AnyHttpUrl("https://auth.example.com"),
resource_server_url=AnyHttpUrl("http://127.0.0.1:8000/mcp"),
required_scopes=["returns:read"],
)
# GET /.well-known/oauth-protected-resource/mcp# -> "scopes_supported": ["returns:read"], with no way to also advertise "returns:write"
Proposal: an optional AuthSettings.scopes_supported, used for the metadata and defaulting to required_scopes. FastMCP's RemoteAuthProvider already separates these, and the TypeScript SDK's scoped-tools example advertises more scopes than it challenges for.
Our workaround is to insert our own route ahead of the SDK's at the same path. #3277 couples the challenge's scope to required_scopes in the same way.
Seen on mcp 2.3.0. I'm happy to open a PR if a maintainer assigns this.
This issue was written primarily by Claude Code. I hit this problem in our MCP server, and I've reviewed the issue.
The proposal is right, and the split is more consequential than a metadata field, because required_scopes is currently carrying two different meanings that only look alike.
Read what consumes it: the refusal check in RequireAuthMiddleware needs the set that must be present on every request, and the protected resource metadata needs the set the server will accept. Those are different questions. Enforcement answers "what do I demand of this token", advertisement answers "what should a client ask for". Coupling them means the server can only advertise what it enforces, which is precisely the case you describe: a read-only token must keep working, so you enforce returns:read, and the write scope disappears from the only document a client can discover it from.
Two design notes worth adding to the proposal, because they decide what the correct fix looks like rather than just how to unblock you:
Once the two sets are separate, someone has to decide which authorization model the server implements. Up-front union consent says advertise everything the server can ever use and have the client request it at first authorization; the write scope then arrives only if the client reads scopes_supported and requests the union. Step-up says advertise what you require, and challenge for the extra scope at the individual call that needs it, which is what a scope parameter on an insufficient_scope challenge is for. Most servers want step-up, because forcing a write scope at consent time makes read-only clients refuse the connection entirely. But step-up only works if the challenge path can name a scope the enforcement path does not require, which is exactly the coupling you are asking to break. So the proposal is a prerequisite for step-up, not an alternative to it.
Defaulting scopes_supported to required_scopes keeps today's behaviour, which is the right call for a patch release, but it also means the incorrect configuration stays the default and only the people who already understood the problem will fix it. Worth saying explicitly in the issue so the maintainer can decide deliberately rather than by inertia.
On your workaround: inserting a route ahead of the SDK's at the same path will work, but it is the kind of thing that breaks quietly on upgrade when the SDK changes its route registration order. Overriding the metadata handler, if the SDK exposes one, fails loudly instead. If neither is stable, that is itself an argument for the change landing upstream rather than staying local.
You might also note in the issue that FastMCP already separates the two, which shows the split is not a protocol question but a modelling one. Anyone reading this who is on FastMCP will otherwise assume the SDK behaviour is the intended semantics.
Activity
Ben-Home commentedon Oct 29, 2026
The proposal is right, and the split is more consequential than a metadata field, because
required_scopesis currently carrying two different meanings that only look alike.Read what consumes it: the refusal check in
RequireAuthMiddlewareneeds the set that must be present on every request, and the protected resource metadata needs the set the server will accept. Those are different questions. Enforcement answers "what do I demand of this token", advertisement answers "what should a client ask for". Coupling them means the server can only advertise what it enforces, which is precisely the case you describe: a read-only token must keep working, so you enforcereturns:read, and the write scope disappears from the only document a client can discover it from.Two design notes worth adding to the proposal, because they decide what the correct fix looks like rather than just how to unblock you:
Once the two sets are separate, someone has to decide which authorization model the server implements. Up-front union consent says advertise everything the server can ever use and have the client request it at first authorization; the write scope then arrives only if the client reads
scopes_supportedand requests the union. Step-up says advertise what you require, and challenge for the extra scope at the individual call that needs it, which is what ascopeparameter on aninsufficient_scopechallenge is for. Most servers want step-up, because forcing a write scope at consent time makes read-only clients refuse the connection entirely. But step-up only works if the challenge path can name a scope the enforcement path does not require, which is exactly the coupling you are asking to break. So the proposal is a prerequisite for step-up, not an alternative to it.Defaulting
scopes_supportedtorequired_scopeskeeps today's behaviour, which is the right call for a patch release, but it also means the incorrect configuration stays the default and only the people who already understood the problem will fix it. Worth saying explicitly in the issue so the maintainer can decide deliberately rather than by inertia.On your workaround: inserting a route ahead of the SDK's at the same path will work, but it is the kind of thing that breaks quietly on upgrade when the SDK changes its route registration order. Overriding the metadata handler, if the SDK exposes one, fails loudly instead. If neither is stable, that is itself an argument for the change landing upstream rather than staying local.
You might also note in the issue that FastMCP already separates the two, which shows the split is not a protocol question but a modelling one. Anyone reading this who is on FastMCP will otherwise assume the SDK behaviour is the intended semantics.