Most MCP servers ship with one scope. It is called something like mcp or full-access, the authorization server hands it out at first consent, and every tool checks nothing beyond the presence of a valid token. That design passes a functional test and fails a security review, because the reviewer's question is not "is the caller authenticated" but "what is the smallest thing this token can do, and how would we take it away".
The MCP security best practices document added a section on this: scope minimization. It names the failure directly - broad up-front scopes expand the blast radius of a stolen token, make revocation all-or-nothing, and blur the audit trail, because one omnibus scope hides which operation the user actually intended. This is how to design the scope model instead.
A scope is a ceiling, not a grant
Start with the distinction that most implementations blur. The scopes in an access token describe what the client application was allowed to ask for on this user's behalf. They do not describe what the user is allowed to do. Those are two separate checks and the server owes both.
A token carrying invoices:write means the agent may attempt invoice writes. Whether this particular user may write this particular invoice in this particular tenant is a question only your application can answer, from the subject and tenant claims in the token, against your own permission model. The specification's own list of common mistakes includes treating claimed scopes in the token as sufficient without server-side authorization logic. Scope narrows the ceiling. Your entitlement check sets the floor. Remove either and the other is not enough.
Derive the catalog from tools, not from tables
The scope catalog should be readable by the person clicking the consent screen. That rules out both extremes: one scope for everything, and one scope per tool in a server with sixty tools.
What works is a catalog derived from the resource-and-verb pairs your tools already imply, at the granularity a user would recognise:
tickets:read,tickets:writeinvoices:read,invoices:writeadmin:usersfor the small set of tools that change access
Then two rules. Reads and writes never share a scope, because the read set is what an agent needs ninety percent of the time and the write set is what a reviewer will read line by line. And anything destructive or access-changing gets its own scope rather than riding along inside a broader one, so that granting an agent the ability to read your billing data does not also grant the ability to cancel the account.
Avoid wildcards. files:* looks tidy in code and reads as "everything, forever" to a security team, including the operations you add next quarter. If you need a grouping construct, express it in the consent UI, not in the token.
Publish only what you actually offer
Two metadata documents advertise scopes. Your protected resource metadata (RFC 9728) may list scopes_supported for the resource; your authorization server publishes its own list under RFC 8414. Publishing the entire catalog in both places invites the predictable client behaviour: request all of them at first authorization, because it avoids a second prompt later.
Publish the baseline set - discovery and low-risk reads - and let the privileged scopes be discovered when they are needed. That is not obscurity. The scopes are documented for developers. It is a default that makes the cheapest client behaviour also the safest one.
Elevate incrementally, with a precise challenge
The mechanism for asking later is already standard. When a tool call requires a scope the presented token does not carry, the server responds 403 with a WWW-Authenticate header carrying error="insufficient_scope" and a scope parameter naming what is missing, as defined in RFC 6750. A conformant client re-runs the authorization flow requesting that scope, the user sees a prompt that mentions one concrete capability, and the call is retried.
Two details decide whether this works in practice.
Name only the missing scope. Returning the full catalog in the challenge converts every elevation into the same all-or-nothing consent screen you were trying to avoid.
Accept down-scoped tokens. An authorization server is permitted to issue a subset of the requested scopes, and users may deselect an item on the consent screen. A server that treats "I asked for three scopes and got two" as an error will break on every identity provider that supports granular consent. Take what you were given, enforce per call, and challenge again if a later call needs more.
The same pattern covers step-up authentication where you need it. RFC 9470 defines the challenge for demanding a stronger or more recent authentication event - useful for the handful of tools where re-authentication, not just consent, is the right gate.
Make elevation visible in the audit log
Scope granularity pays for itself in the audit trail, but only if you record the transitions. For each elevation, log the scopes requested, the subset granted, the tool call that triggered the challenge, the client, the subject, and a correlation ID that ties the challenge to the retried call. For each denial, log the scope that was missing.
This gives the customer's security team something they cannot get from HTTP logs: a record of when an agent's authority grew, on whose consent, and for what. It also gives you a metric worth watching. If ninety percent of users end up holding the full catalog within a week, your tool boundaries are wrong, not your scope model.
Version the meaning, not just the name
Scope names are a contract. Quietly widening what tickets:write permits - because a new tool fit naturally under it - re-uses a consent the user gave for something narrower. That is the one change in this area that will be treated as a finding rather than a suggestion.
If a scope's meaning grows, introduce a new scope. Keep a short written table mapping every tool to its required scope, dated, and put it in the repository next to the server. It is the artifact a reviewer asks for, and it is the artifact that keeps the model honest as tools are added.
What a reviewer will ask
Prepare four answers.
- Show the scope catalog and the tool-to-scope table.
- Show what a token issued to a first-time user contains, and what it does not.
- Show a
403withinsufficient_scopeand the challenge that follows it. - Show the audit event for an elevation, and explain how revoking one scope affects the rest.
When you do not need this
A single-purpose read-only server with three tools over one dataset does not need a scope hierarchy. One scope, honestly named for what it grants, plus real per-user enforcement inside the handlers, is a defensible design and easier to explain than a catalog nobody uses. The threshold is roughly this: the moment your server has tools a user could reasonably want to withhold from an agent, you need more than one scope.
Scope design is one of the areas we assess in an MCP security audit, and one of the first decisions we make on a server build, because it is cheap to get right in the design week and expensive to change after clients have consented.