Skip to main content

Scope design for MCP tools

This is the hardest part of putting OAuth on an MCP server, and the part nobody writes down. Scope IDs end up in user-facing consent screens, customer OAuth client configs, audit logs, and every token issued for the lifetime of your server. Getting them roughly right at the start saves you a migration later.

The three granularity choices

There are basically three places to land on:

GranularityExampleProsCons
Catch-allmcp:fullTrivial to use. One scope grants everything.No way to restrict clients. A leaked token is total access.
Read/write splitmcp:read, mcp:writeTwo scopes cover 80% of access decisions. Easy for users to consent to.Doesn't differentiate "read repos" from "read users."
Per-resource read/writemcp:repos:read, mcp:repos:write, mcp:users:read, ...Maps naturally to the tool surface. Users grant only what's needed.More scopes for users to consent to. Schema grows with the API.

A fourth option exists conceptually — per-tool scopes (mcp:get_user, mcp:create_pr) — but it's almost always wrong. Tools change names and proliferate; scope IDs need to be stable. Per-resource granularity gives you ~80% of per-tool's safety without the maintenance burden.

The matrix

Here's the table to copy. Tool taxonomy on the left, recommended scope strategy on the right.

Your MCP server's surfaceRecommended scopesWhy
Single-purpose, all tools read-only<rs>:readOne scope. Done.
Single-purpose, read + write<rs>:read, <rs>:writeTwo-way split is the cheapest meaningful boundary.
Multiple resources, all read-only<rs>:<resource>:read per resourceLets clients ask for narrow grants ("just users, not repos").
Multiple resources, mixed CRUD<rs>:<resource>:read, <rs>:<resource>:write per resourceThe standard pattern. AuthSec's default scope template.
One tool is uniquely destructiveAdd <rs>:admin for itA separate scope for "do irreversible things" lets you grant write without granting destructive.
Background-job tools (delete, mass-update)<rs>:adminSame as above. Don't bundle these into normal write.

The seeded set on registration — <rs>:tools:read, <rs>:tools:write, <rs>:admin — is the safe starting point. Add per-resource scopes if your tool count is >20 or your tools span genuinely different domains.

The naming convention that wears well

Use <rs-prefix>:<resource>:<verb>:

  • rs-prefix is namespaced to your specific resource server (e.g. rs-525da3b4). Tokens with this scope are bound to your server; other servers reject them. AuthSec auto-prefixes this.
  • resource is the kind of thing being accessed (repos, issues, users, actions). Plural nouns. Avoid data or things.
  • verb is the action level. read and write cover most things; admin for destructive; avoid manage (it conflates with admin).

Reserved patterns to know:

  • <rs>:admin — administrative actions. Grant carefully.
  • <rs>:offline_access — refresh-token issuance. Standard OIDC; let your auth server handle it.
  • openid, profile, email — OIDC standard scopes. Don't repurpose them.

The decision tree

Start at the top, follow yes/no:

  1. Do you have more than five tools? No → catch-all (<rs>:full). Done.
  2. Do all your tools touch the same kind of thing? Yes → read/write split (<rs>:read, <rs>:write).
  3. Do some tools touch sensitive data others don't? Yes → per-resource (<rs>:repos:read etc.).
  4. Are there one or two tools that are uniquely destructive? Yes → add <rs>:admin for them.
  5. Are you sure? Pause for a week. Scope renames are expensive; the default seed (<rs>:tools:read/write/admin) is fine for the first month while you find the patterns.

What public tools look like

Some tools genuinely don't need scopes: health, ping, capability discovery. Mark these Public on the Tools tab. The SDK allows them for any caller (authenticated or not). Be deliberate — public is the only state without an audit trail at the token level, so a leak is invisible.

Cases where public is wrong:

  • Anything that returns user-specific data.
  • Anything that modifies state.
  • Anything that wraps a paid upstream API where the caller's identity matters for cost attribution.

Scope step-up

Most clients don't request all scopes up front; they ask for the minimum. If a user tries a tool requiring <rs>:tools:write with a token that only has <rs>:tools:read, the SDK returns 403 insufficient_scope. The MCP client should catch this, redirect the user back through the OAuth flow with the new scope requested, and retry.

Today this works fine for one-shot HTTP. For SSE (streaming) responses, the in-stream step-up flow is less well-defined — see the wedge content on this for the state of the spec.

Common mistakes

Naming scopes after tool names. "I'll call it mcp:get_pr." Now you rename the tool to mcp:fetch_pr and your scope is misleading. Use resource nouns, not tool verbs.

Putting environment in the scope. <rs>:repos:read:prod is fragile — promotion to prod becomes a scope change. Use the same scope across environments; let the resource server's aud claim distinguish.

Granting write without read. Some MCP servers have asymmetric tools where write doesn't imply read. If your write tool needs to read first to do its job, document this — and consider mapping the tool to require both scopes (the SDK supports this).

Bundling everything into one scope. "We just have one mcp:all scope." This is functionally identical to no auth — a leaked token does everything. Even two scopes is meaningfully better than one.