Skip to content
7 min read

How to Assign OAuth Scopes for MCP Tools?

OAuth scopes for MCP help separate reading, changing, and administration, configure scope challenges, and verify permissions on the server.

How to Assign OAuth Scopes for MCP Tools?

MCP tools are dangerous not because a model occasionally chooses the wrong function. The danger appears when a server gives one token permission to read data, change it, run exports, and perform administration. Then one extra integration, one incorrect call, or one stolen token gets the entire set of capabilities at once.

A good scope design does not mirror the tool list. It describes permitted operations on business objects, separates reversible actions from irreversible ones, and leaves the server responsible for checking the request context. This is access design work, not an OAuth naming exercise.

A scope should describe an action, not a server

A broad scope such as mcp:access almost always means the team has postponed the permissions decision. That decision usually comes after the first deletion, export, or approval tool is introduced, when old tokens have already been issued and several clients depend on them.

Start with this question: what exactly can the calling party do if the model selects this tool without additional confirmation? The answer “call our MCP server” says nothing about the risk. Answers such as “view inventory,” “create a request,” “publish a pricing plan,” and “change routing rules” say enough.

A practical scope name usually consists of a domain and a verb:

inventory:read
inventory:write
orders:read
orders:write
orders:approve
billing:export
admin:manage

This is not a universal vocabulary. For one team, orders:approve means approving a limit; for another, it means issuing a refund. The important thing is that the meaning stays consistent: a person, policy engine, and tool author should all understand the same action that the string grants.

Do not add a scope for every technical method just because there are many of them. For example, inventory.get_item, inventory.search_items, and inventory.list_low_stock may all require inventory:read if they only return data with the same sensitivity level. But inventory.export_all deserves its own inventory:export: its output can easily end up in a file, email, or external agent.

There is an opposite mistake too: verbs that are too broad. orders:write should not automatically cover canceling a paid order, issuing a refund, or changing prices in bulk. If an action changes an obligation to a customer, affects money, or is difficult to undo, separate it from ordinary editing.

A tool, operation, and resource are not the same thing

Teams often try to solve all access control with one list of scopes. That does not work because a scope answers only one question: what class of actions is the token allowed to perform in principle?

Every MCP tool call has at least four distinct dimensions:

  1. Tool identifies the entry point, such as create_refund.
  2. Operation describes the meaning of the action: read, create, change, approve, export, or administer.
  3. Resource answers what the action applies to: an order, contract, specific customer, project, or branch.
  4. Subject and context define who is making the call, which organization they are acting from, which environment is involved, and what additional conditions apply.

A scope usually covers the operation and domain. Resource and organization-boundary checks should live separately. A token with orders:read should not let an employee of one bank read another bank's orders just because both use the same MCP server.

Here is a typical mistake:

orders:read:tenant-482
orders:read:tenant-721
orders:read:tenant-913

This design looks precise while there are only a few clients. Then branches, projects, temporary delegations, and external contractors appear. The scope set expands, user consent becomes unreadable, and an audit cannot explain why that particular ID ended up in the token.

It is better to keep a regular orders:read in the token and obtain access to a specific organization from verifiable claims, a server-side policy, or a structured authorization. RFC 9396, OAuth 2.0 Rich Authorization Requests, introduces the authorization_details parameter for this purpose: the client sends a machine-readable description of the requested access, and the authorization server can issue a more precise grant. This is useful when access depends on an account, document set, amount, or time period rather than a verb alone.

The scope and resource restriction should work together. The first prevents calls to a class of dangerous operations. The second prevents an allowed operation from being applied to the wrong or unsuitable object.

Reading, changing, and administering need different boundaries

For an MCP server, it is useful to first draw a consequences matrix rather than a tool list. It quickly shows where “write” hides actions with very different risks.

ActionExample toolBasic permissionWhat to check beyond the scope
Read one recordget_customercustomer:readorganization membership, field masking
Search and listsearch_ordersorders:readfilters, result limit, field access
Create a draftcreate_quotequotes:writeorganization, template, limits
Change a working objectupdate_tickettickets:writeauthor or operator role
Approveapprove_discountdiscounts:approveamount limit, separation of duties
Exportexport_customerscustomer:exportformat, volume, justification, audit log
Administrationrotate_api_keyadmin:manageMFA, administrator role, separate channel

Reading is not always safe. A customer search may return personal data, and exporting ten thousand rows is often more dangerous than making one change. So read does not mean “without consent and without auditing.” It means the operation does not change the source data. That is not enough to assess the potential harm.

Keep administrative actions separate even in a small product. Adding a user, changing a retention policy, issuing a key, changing payment routing, and disabling auditing can expand access beyond one business domain. The admin:manage scope itself may also be too broad. Sometimes you need admin:identity, admin:keys, and admin:policy if these responsibilities are genuinely split between different people.

Do not try to compensate for a poor permissions design with text in the tool description: “use only when requested by an administrator.” The model may read that description, but the server must make the decision itself. The description affects the likelihood of a call. Scopes and server-side policy determine whether the call is executed.

The operation map should come before the OAuth code

Before configuring the authorization server, create a table of all MCP tools. Do not assign it entirely to the server developer: the data owner and process owner understand the consequences of a call better. This table becomes a contract between the tool team, identity team, and auditors.

Fill in five fields for each operation:

FieldQuestion it answers
ToolWhich tools/call reaches the server?
Minimum scopeWhat permission is required for this class of action?
Resource checkWhich tenant, project, owner, or role must match?
Step-up conditionWhen are reauthorization, MFA, or separate confirmation required?
AuditWhat should be recorded so the server's decision can be reconstructed?

Suppose a team is building an MCP server for procurement. The first version of the map might look like this:

operations:
  purchase_order.get:
    required_scopes: [procurement:read]
    resource_check: same_organization

  purchase_order.create_draft:
    required_scopes: [procurement:write]
    resource_check: requester_can_create_for_cost_center

  purchase_order.submit:
    required_scopes: [procurement:submit]
    resource_check: requester_is_draft_owner

  purchase_order.approve:
    required_scopes: [procurement:approve]
    resource_check: approver_limit_covers_total
    step_up_if: total_exceeds_approval_limit

  supplier.export:
    required_scopes: [supplier:export]
    resource_check: export_allowed_for_organization

Pay attention to submit and approve. Both operations change the document status, but their consequences differ. If both are assigned procurement:write, an employee who is allowed to edit a draft can approve the purchase. This is not a subtle architectural complaint. It is a straightforward path to violating separation of duties.

The map also protects against another problem: a tool with a neutral name. update_purchase_order might change the description, amount, recipient, and status. If its arguments allow actions with different risks, it is better to split the tool itself: update_purchase_order_draft, submit_purchase_order, approve_purchase_order. Separate entry points are easier to authorize, test, and explain to users.

The server should reject the call before executing the tool

Mask PII in requests
PII masking in AI Router reduces the amount of sensitive data sent in LLM requests.

An MCP client may show the user a consent screen. A model may select only available tools. A gateway may filter some requests. None of these layers removes the need for a check on the MCP server.

Check access after the server has identified the tool name and parsed the arguments, but before any database, queue, external API, or side effect is called. At that point, the server knows which action the client is requesting and which object it applies to.

A simplified check looks like this:

async function authorizeToolCall(ctx, call) {
  const rule = policy.forTool(call.name);
  const claims = await verifyAccessToken(ctx.authorization);

  requireAudience(claims, \"https://mcp.example.kz\");
  requireScope(claims.scope, rule.requiredScopes);

  const resource = await resolveResource(call.name, call.arguments);
  await rule.checkResourceAccess({ claims, resource, args: call.arguments });

  if (rule.requiresStepUp({ claims, resource, args: call.arguments })) {
    throw insufficientScope(rule.stepUpScopes);
  }
}

verifyAccessToken should not mean only checking a JWT signature. The server needs to verify the issuer (iss), audience (aud), lifetime, permitted signature algorithm, and scopes. If the access token is opaque, the server will usually introspect it or use a reliable cache of the result with a short lifetime.

Never check a permission by substring. Code such as scope.includes("write") would let profile:write through where orders:write is required. Parse the scope string as a set of values, compare complete elements, and define the semantics of multiple required permissions explicitly.

function requireScope(scopeString, expected) {
  const granted = new Set((scopeString ?? \"\").split(/\\s+/).filter(Boolean));
  const missing = expected.filter(scope => !granted.has(scope));

  if (missing.length) {
    throw new AuthorizationError(\"insufficient_scope\", { missing });
  }
}

If a tool requires two independent permissions, such as exporting customers for a particular department, do not hide the rule in the scope name. Check customer:export as the action permission and department membership as the resource restriction.

A scope challenge is better than access granted in advance

The MCP specification dated November 25, 2025 recommends that a server include the required scopes in the WWW-Authenticate header when returning 401. The client should treat the scopes in the challenge as authoritative for the current request. This provides a normal privilege-escalation mechanism: the client starts with a minimal set and asks for more only when it attempts a specific protected operation.

For a person, this is clearer than a consent screen showing twenty permissions on the first connection. For a security team, it is better because broad tokens do not circulate among agents simply to support the possibility of calling a rare administrative tool someday.

The server response might look like this:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata=\"https://mcp.example.kz/.well-known/oauth-protected-resource\", scope=\"procurement:approve\"
Content-Type: application/json

{
  \"jsonrpc\": \"2.0\",
  \"id\": 42,
  \"error\": {
    \"code\": -32001,
    \"message\": \"Approval permission is required\"
  }
}

Do not confuse this challenge with a simple “access denied” message. The client needs machine-readable data so it can request exactly the missing permission from the authorization server. At the same time, do not assume that every MCP client supports scope escalation perfectly. Test the behavior of specific clients in integration tests and provide a clear denial for clients that cannot repeat authorization.

In the current work of the MCP community, this remains an area where you cannot rely on SDK magic. At the Tool Scopes working group meeting in February 2026, participants explicitly noted that the specification already supports OAuth and scope challenges, but common guidance for defining scopes and mapping them to tools is still insufficient, while tool arguments can sometimes change the required permission. This supports a practical rule: keep the operation map in your own policy layer instead of expecting a library to derive it from JSON Schema.

Do not issue one token and call it convenience

Route across 500 models
AI Router routes requests to 500+ frontier models from 68+ providers.

The most popular early-stage recommendation sounds like this: “let's give the agent full access for now and narrow it later.” It is popular because the first demo goes faster. It is wrong because narrowing access after launch breaks workflows, refresh tokens, and user expectations, while permissions have time to spread through configurations.

Four patterns especially often lead to excessive access:

  • One mcp:full_access scope for every tool.
  • write for approval, refund, deletion, and bulk-import operations.
  • Including administrative permissions in a machine-to-machine token.
  • Granting all scopes_supported to a client that initially knows only one read operation.

MCP uses an OAuth approach, but OAuth does not force a server to follow least privilege. The team chooses the policy. The MCP specification includes a scope-selection strategy in which the server challenge takes priority and the basic scope set should be minimal for normal operation. This is a useful goal, but not a reason to grant the maximum when there is no challenge.

For machine-to-machine scenarios, boundaries matter even more. OAuth client credentials are appropriate when the client acts as an application rather than on behalf of an employee. The official MCP client credentials extension describes exactly this case: an automated system receives an application-level credential instead of interactive user consent.

Such a client does not need admin:manage if it synchronizes a catalog every night. Give it catalog:read and, if needed, catalog:write for a clearly defined synchronization direction. Restrict the token audience to the specific MCP resource, shorten its lifetime, and use a separate client identity for each service. One technical client for every background task saves a few configuration entries and destroys incident investigation.

RFC 9700 establishes modern OAuth 2.0 security recommendations: exact redirect URI matching, protection for redirect-based flows, and rejection of modes recognized as unsafe. For MCP with interactive authorization, this means that carefully designed scopes cannot rescue a poorly protected authorization code flow. Use Authorization Code with PKCE for public clients, and never pass access tokens through a URL.

The token should be bound to the MCP resource

Even a perfect scope list is not useful if a token for one API is accepted by another. The MCP specification requires the use of OAuth Resource Indicators: the client includes the resource parameter in the authorization request and token request, identifying the canonical URI of the MCP server. The server checks that the token is intended for it.

This matters for organizations with separate MCP servers for HR data, procurement, analytics, and administration. A token with employee:read issued for the HR server should not become a universal pass to any endpoint that happens to accept a signature from the same issuer.

The check usually comes down to two conditions:

iss = expected authorization server
and
resource/aud = canonical URI of this MCP server

If the authorization server issues a JWT with aud, compare aud. If it uses another way to represent the target resource, document it in the contract and test rejection when it does not match. Do not accept a token simply because its signature is valid. A valid signature says who issued it. It does not say that this server should accept it.

For remote MCP servers, discovery is also part of the access model. Protected Resource Metadata tells the client which authorization server to use, and WWW-Authenticate can point it to the metadata URL. Do not replace this process with an issuer hardcoded into every client if the server should work with several approved authorization methods. But do not let the client choose an arbitrary issuer without a server-side trust policy either.

Tests should prove rejection, not only successful calls

One endpoint for your MCP client
A single OpenAI-compatible AI Router endpoint helps you keep your existing SDK when changing models for an MCP client.

Teams often write an integration test saying “a token with orders:read reads an order” and consider authorization complete. That test is necessary, but it does not find dangerous permissions. You need pairs: an allowed call and an almost identical denied call.

The minimum test set for each group of operations looks like this:

cases:
  - name: reader_can_get_own_order
    token_scopes: [orders:read]
    tenant: alpha
    tool: get_order
    args: { order_id: \"alpha-104\" }
    expected: success

  - name: reader_cannot_update_order
    token_scopes: [orders:read]
    tenant: alpha
    tool: update_order
    args: { order_id: \"alpha-104\", status: \"cancelled\" }
    expected: insufficient_scope

  - name: reader_cannot_read_other_tenant
    token_scopes: [orders:read]
    tenant: alpha
    tool: get_order
    args: { order_id: \"beta-104\" }
    expected: forbidden

  - name: writer_cannot_approve_order
    token_scopes: [orders:write]
    tenant: alpha
    tool: approve_order
    args: { order_id: \"alpha-104\" }
    expected: insufficient_scope

Distinguish insufficient_scope from forbidden. The first means the token lacks permission for that type of operation. The second means the operation type is allowed, but the specific object or condition prevents execution. This distinction is necessary for correct client behavior and for your team's investigations. If every denial returns “403 access denied,” you lose the meaningful part of the failure.

Check argument boundaries too. One transfer_funds tool may require ordinary payments:write up to a limit, and payments:approve plus additional authentication above that limit. If the policy looks only at the tool name, the model may pass a large amount through a method that is formally allowed.

Record the subject, client ID, issuer, audience, tool name, required scopes, actually granted scopes, decision type, and resource identifier in a safe form. Do not record the access token. Do not copy complete arguments containing personal data into logs merely for debugging convenience.

The design should survive new models and new routes

Routing between models does not change access rules. If one agent calls MCP tools through several providers, permission is determined by the token and server policy, not by which model generated the JSON-RPC request. This is especially important when the team changes models because of cost, latency, or tool-calling quality.

AI Router may accept an OpenAI-compatible request and send it to different models, but the MCP server must still check scopes at its execution point. Do not move authorization into the prompt, model selection, or a gateway that cannot see the business meaning of the arguments.

Schema stability matters more than elegant names. Do not rename orders:read to orders:view without a real need: old tokens, consent history, and policies will start to diverge. If the meaning of a permission has genuinely changed, add a new scope, support a migration period, and revoke the old one according to a plan. Never silently change the semantics of an existing name.

Before launching a new tool, require a short review covering the operation, minimum scope, resource check, step-up conditions, and rejection tests. If the tool author cannot fill in these five points, the tool is not ready for production. OAuth does not create this discipline by itself, but it makes the discipline testable.

Frequently asked questions

Does every MCP tool need its own scope?

No. A single scope per server is appropriate only for a very small server where every operation has the same risk and is available to the same role. As soon as data changes, exports, or administration appear alongside search, a shared token turns any tool-selection mistake into an access-control problem.

How do you know when two tools can use the same scope?

Usually not. Divide permissions by operation and object domain, not by the internal structure of the code. Several low-risk read tools can share one scope if they access the same class of data and an error would have the same consequences.

Should a read tool require a write scope?

A read tool should require read permission only, even when a tool for changing the same object is available nearby. Do not grant write access “just in case”: the model may call the available tool by mistake, and the server will no longer be able to take that extra permission back.

Should exports and deletions have separate scopes?

Separate permissions are usually appropriate. An export creates a copy of data outside the normal working context, while deletion is often irreversible or subject to separate retention and auditing requirements. Names such as customer:export and customer:delete make the distinction visible in both policy and event logs.

Where should an MCP server check scopes?

Check the scope on the server immediately before executing the operation, after parsing the arguments and before calling a business API. Checking the tool description, client, or system prompt does not provide protection: all three layers can be bypassed or misconfigured.

Can a client or document ID be included in a scope name?

A scope should not encode the identifier of every customer, account, or document. Use subject attributes, organization-membership checks, resource restrictions, or authorization_details from Rich Authorization Requests instead. Otherwise the permission set grows to thousands of entries and becomes impossible to manage.

Can new scopes be obtained through a refresh token?

Refreshing a token is acceptable if the refresh token does not expand the original consent by itself. When a client needs a new high-risk scope, start a separate authorization request or use a scope challenge so the user and the policy can see the privilege increase.

How should scopes be assigned to an MCP service without a user?

For a service MCP client, use client credentials only for actions that belong to the application itself, not to a person. The token should have a short lifetime, a specific audience, and scopes that describe the service's work, such as reading a catalog rather than approving payments.

Does an MCP server need to contact the authorization server for every call?

Yes, if the token contains enough information for local verification or the server introspects the token. Local JWT signature verification does not replace checking aud, iss, expiration, and the actual scopes. Otherwise the server may accept a token issued for someone else or an expired token as valid.

What logs are needed for scope denials?

Record denial events with the required scope, operation name, subject type, and reason for the denial, but never log the access token or complete sensitive arguments. If the team repeatedly adds broad permissions after denials, that is a sign to fix the operation map rather than weaken the check.