Implement Incremental OAuth Scopes for MCP
Start with only the scopes needed for ordinary MCP operations. When a protected operation returns an insufficient-scope challenge, parse its required scopes and resource metadata, explain the added access to the user, reauthorize only after consent, and retry once. Treat denial as a completed authorization outcome, not an error to bypass.
Prerequisites
Use this flow for a protected MCP server reached over an HTTP-based transport. The server needs an authorization server that can issue a new access token when the requested scope set changes. The client needs to parse HTTP authentication challenges, retain authorization state per protected resource, start authorization again, and resume or finish the operation that caused the challenge.
The MCP authorization specification dated 2025-11-25 defines the server as an OAuth resource server and the MCP client as an OAuth client. Authorization is optional in MCP, but an HTTP implementation that supports it should follow that specification. This challenge flow does not replace token validation or binding access tokens to the intended MCP server.
Implement the scope challenge flow
-
Define the smallest useful initial scope set
Start from operations, not from every permission the server could conceivably grant. For each tool, resource, or prompt that touches protected data, record the scopes required to complete that specific operation. Then identify the minimum set needed for the server’s ordinary entry path.
An illustrative file server might use this contract:
Operation Required scopes Initial or incremental List permitted files files:readInitial Read a permitted file files:readInitial Replace file contents files:read files:writeIncremental Delete a file files:read files:deleteIncremental The names are part of this example, not MCP-defined scope names. The important property is that each operation has a deterministic requirement. A request either satisfies it or receives a challenge naming what is missing.
Do not put
files:writeorfiles:deletein the initial request merely because the server exposes write and delete tools. An MCP server can challenge for additional scopes only when an operation needs them instead of requesting broad access at first authorization. The mechanism limits the first grant to ordinary work and moves exceptional access to the moment when the reason is visible.The cost is another authorization interaction when a protected operation is first used. Incremental authorization is therefore the wrong answer for a scope that almost every successful session immediately needs: placing that scope behind a challenge adds a predictable interruption without reducing ordinary access. It is also a poor fit when the authorization server cannot issue an increased grant. In either case, put the genuinely necessary scope in the initial set and leave unrelated permissions out.
-
Publish protected-resource metadata before relying on challenges
Make the MCP server’s protected-resource metadata available at the applicable well-known location. Include the resource identifier and at least one authorization server. Advertise only the scopes that make sense as the discoverable baseline; do not treat
scopes_supportedas an instruction to request every permission the server may ever issue.RFC 9728, published in 2025, defines OAuth protected-resource metadata, including
resource,authorization_servers, and the recommendedscopes_supportedfield. It also permits a resource server to leave some supported scopes undisclosed. The RFC explicitly says thatscopes_supportedis not a request list: clients should still ask for as little scope as the operation requires.For example:
{ "resource": "https://mcp.example.com/mcp", "authorization_servers": ["https://auth.example.com"], "scopes_supported": ["files:read"] }Keep the resource identifier exact and stable. When a client retrieves metadata from a URL supplied in
WWW-Authenticate, it must reject that metadata if the returnedresourcevalue does not identify the resource it contacted. This check keeps a challenge from silently redirecting authorization toward a different protected resource.Support both discovery paths in the client: use
resource_metadatafrom a parsed authentication challenge when present, and otherwise try the specified well-known locations. The 2025-11-25 MCP changelog records that alignment with RFC 9728 made theWWW-Authenticatemetadata pointer optional and added the well-known fallback. A client that implements only the header path will fail against a conforming server that relies on the fallback; a client that ignores a supplied header can miss resource-specific metadata.Metadata adds an endpoint, validation, and cache behavior to maintain. It is still the wrong place for operation-specific consent prose: metadata describes the protected resource, while the runtime challenge identifies what the current operation needs.
-
Authorize the baseline without preloading later permissions
On the first protected request, the server can return
401 Unauthorizedand identify its protected-resource metadata. If the challenge containsscope, use that scope set for initial authorization. If it does not, usescopes_supportedfrom the validated metadata; if that field is also absent, omit the authorization request’sscopeparameter rather than inventing one.Keep the initial path distinct from step-up authorization:
401 Unauthorizedcovers absent, invalid, or expired authorization.403 Forbiddenwitherror="insufficient_scope"covers a valid token that lacks permission for the current operation.
That distinction prevents an expired token from being presented to the user as a request for wider access. It also prevents a missing scope from entering a generic sign-in loop.
Include the MCP server’s canonical resource identifier in both authorization and token requests, and send the resulting bearer token in the
Authorizationheader on every HTTP request. Do not assume that an existing Streamable HTTP session carries authorization for later requests.The baseline grant should make read-only or otherwise ordinary operations usable. Do not test step-up by asking for all scopes and then pretending the narrower token was issued; exercise the same token and consent path that production clients will use.
-
Return a complete challenge at the protected operation
When a valid token lacks the scopes required by the requested operation, return
403 Forbidden. TheWWW-Authenticateresponse communicates required scope and protected-resource metadata to the client. Use the Bearer scheme and include:error="insufficient_scope"so the client can distinguish the condition from other authorization failures;scope="…"with the scopes needed to satisfy this operation;resource_metadata="…"with the protected-resource metadata URL;- optionally,
error_description="…"with a human-readable description.
For the illustrative write operation:
HTTP/1.1 403 Forbidden WWW-Authenticate: Bearer error="insufficient_scope", scope="files:read files:write", resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource/mcp", error_description="Writing this file requires file write access"Return a sufficient scope set, not merely a delta that may discard a permission the operation still needs. If the write operation requires both read and write access, challenge with both. The MCP specification recommends including existing relevant scopes alongside newly required scopes to avoid losing permissions when the client obtains the replacement token.
Do not add unrelated permissions that are merely adjacent in the product. A challenge for file writing should not silently become a challenge for deletion. Related scopes can reduce future prompts, but they also widen the requested grant before those operations have supplied their own reason. Use them only when they are required together by the current operation.
The server must construct challenges consistently. Otherwise the same operation can alternate between scope sets, forcing repeated authorization. This mapping is also an observability boundary: record the operation, current scope result, challenged scopes, and final outcome without logging access-token contents. That gives operators enough context when tracing what an agent did through an MCP server.
-
Parse and validate the challenge before starting authorization
On
403, parseWWW-Authenticateas an HTTP authentication header rather than splitting it on commas. Confirm the Bearer scheme andinsufficient_scopeerror, extract the challenged scope set, and retrieve the indicated protected-resource metadata. Validate that metadata against the resource that returned the response.Treat the challenge’s
scopevalue as authoritative for the current request. Do not require it to be equal to, a subset of, or a superset ofscopes_supported; the MCP specification allows all of those relationships because runtime scopes may not be advertised in metadata.Compare the requested scopes with the client’s current grant. Preserve any current scopes that remain necessary, remove duplicates, and bind the authorization attempt to both the resource and the operation that caused it. The client should also record that an upgrade has been attempted for that combination. This state prevents two bad loops: repeatedly requesting the same rejected grant, and retrying an operation with a token that still cannot satisfy it.
Stop instead of stepping up when the challenge is malformed, the metadata fails resource validation, no usable authorization server is available, or the same scope upgrade has already failed. Those are permanent outcomes for the current attempt, not reasons to broaden the request or select a different resource silently.
-
Explain the new access and make refusal ordinary
Before redirecting or opening an authorization interaction, show the user three pieces of information:
- the operation waiting to run;
- the additional access being requested, expressed in user-facing language;
- what happens if access is denied.
For example: “Replace
deploy/config.json” needs file write access. Approving continues that operation. Denying leaves the file unchanged and returns control to the client.Scope identifiers can accompany the explanation for inspection, but
files:writealone does not explain which action caused the prompt. Build the message from trusted operation context plus a maintained description of the challenged scope. Do not display arbitrary server text as the sole explanation;error_descriptioncan add detail, but the client still knows which operation it was about to perform.The client should show the user why new access is required and preserve denial as a normal outcome. Cancellation, explicit denial, or an authorization response without the required grant should end the pending operation without changing the user’s previous authorization. Return a structured “additional access not granted” result to the assistant or calling application. Do not translate denial into a generic transport failure, retry it automatically, or ask the model to find a way around the missing permission.
This is where MCP clients may differ most. An IDE can tie the prompt to a visible command or file, while a desktop assistant may need to name the pending tool call and affected resource. Test the actual presentation in the clients you support rather than assuming they expose the same controls; the decision affects how the same MCP server behaves in an IDE and a desktop assistant.
The cost is UI work and a resumable pending operation. It is necessary work: a consent screen that lists scope strings without the triggering action does not let the user judge the request, and a client that cannot represent denial will tend to misclassify an intentional decision as a failure.
-
Reauthorize, replace the token, and retry narrowly
After approval, start authorization again with the challenged scope set and the same MCP resource identifier. Complete the token exchange, store the new token using the client’s normal protected token storage, and retry the original operation with that token.
Retry the operation, not an entire agent plan. Replaying a wider sequence can repeat writes that already succeeded before the challenge. If the operation itself is not safe to repeat, preserve enough state to determine whether the server executed it before returning the authorization response. The normal design is to check authorization before performing the protected effect, so the first insufficient-scope response happens before the mutation.
Set a small retry limit and track attempts by resource and operation. The MCP specification says clients should retry no more than a few times and then treat the result as a permanent authorization failure. A single step-up attempt followed by one retry is the simpler default: if the newly issued token still lacks the challenged scopes, surface the failure instead of reopening consent.
Concurrent calls need the same guard. If several operations receive the same challenge together, let one authorization interaction resolve the grant and have the others wait for that result. If the user denies it, complete every dependent call with the same denial outcome. Do not produce several identical consent windows.
-
Test the decisions, not only the successful redirect
Run the flow with a baseline token against each protected operation. At minimum, verify these cases:
- A baseline operation succeeds without another authorization interaction.
- A higher-scope operation returns
403withinsufficient_scope, the exact required scope set, andresource_metadata. - Approval produces a new token and retries only the pending operation.
- Denial ends the operation without retrying or discarding the previous grant.
- A malformed challenge stops safely.
- Metadata whose
resourcedoes not match the contacted MCP server is rejected. - A replacement token that still lacks scope does not create an authorization loop.
- Two simultaneous identical challenges result in one user decision.
Also test that a later baseline operation still works after both approval and denial. That catches clients which replace authorization state incorrectly. Test with the real authorization server because a local stub can hide how it combines old and new scopes.
Expected result
The initial authorization requests only the scopes needed for ordinary MCP operations. A valid token reaches a more privileged operation and receives a 403 challenge naming the required scopes and protected-resource metadata. The client validates that challenge, explains the pending operation and added access, and reauthorizes only after approval. Approval retries the pending operation with the new token; denial ends it without a loop, a workaround, or loss of the existing grant.
Sources
- MCP authorization specification dated 2025-11-25modelcontextprotocol.io
- RFC 9728, published in 2025, defines OAuth protected-resource metadatarfc-editor.org
- 2025-11-25 MCP changelogmodelcontextprotocol.io
See also
REST is deterministic code you can test and review; an MCP server lets a model decide per item. How cost, failure, auditability split and which to build first.
Configure Origin checks, loopback binding, authentication, and tests for browser-accessible MCP Streamable HTTP endpoints.
Add progress tokens to MCP requests, emit correlated notifications, keep values monotonic, and handle totals, timeouts, and completion correctly.
Define MCP prompt arguments, validate every prompts/get request, and use completion without treating suggestions as enforcement.