Why MCP permission scopes drift, and why that matters to you

If you ship or operate software that talks to customers through an MCP (Model Context Protocol) server, the permission scopes you granted six months ago are probably not the scopes you would grant now. Tools get added, OAuth providers get swapped, an integration vendor expands what its server exposes, and the token sitting in your agent client quietly inherits every change. By the time a customer asks why your agent can rename a file in their drive, you have a scope-drift problem you cannot answer.

This checklist is for founders and small engineering teams who already run at least one MCP server in production or have customers connecting through one. It is a process, not a product: how to take inventory, narrow what is too broad, revoke without breaking workflows, and detect drift after a server update.

What “scoped permissions” actually means on an MCP server

An MCP server advertises tools. Each tool needs some permission to run. The permission can be expressed two ways, and the difference is the whole game:

  • OAuth scopes on the access token. These are the broad capability labels an authorization server attaches to a token at login — things like notes:read, notes:write, or ats.Candidate:read. The MCP server reads them off the token and decides whether the call may proceed.
  • Server-side checks against roles, ACLs, or tool allowlists. The token says who is calling; the server’s own policy decides whether that caller may run this tool against this resource now. Scopes alone cannot do this because tokens are static snapshots of what was allowed when they were issued. Roles change. Tools change. Resources change.

Practical pattern most teams land on: use OAuth scopes to define the shape of capability a client gets (read-only vs. read-write, which integration categories), and use server-side checks for who, when, and against which resource. Trying to express every fine-grained rule as a scope leads to scope sprawl — too many scopes, oversized tokens, and a catalog that breaks every time you ship a new tool.

The audit checklist

Walk through these steps once a quarter, and again any time a server vendor ships a major update or you add a new integration.

Step 1: Inventory every MCP server in use

Start by listing every MCP server your product, your customers’ agents, or your own internal agents connect to. For each, record:

  • Server name and vendor (in-house, third-party, open-source)
  • Where the OAuth client credentials live (env vars, secrets manager, your deployment config)
  • The scopes requested at consent
  • The scopes the token actually came back with (often a subset of what was requested, sometimes the full set)
  • Who in your team has admin access to the authorization server

For third-party servers, look at the server’s published scope list or its source config. Servers like the Merge MCP server take scopes as command-line arguments — ats.Job:read, ats.Application:write — and the list is right there in the README or your deployment config. Other vendors (Kinde’s MCP server, PropelAuth’s MCP support) publish a scope-to-operation table in their docs. If a vendor will not show you the scope catalog, that itself is a finding.

Step 2: Map scopes to actual tools and mutations

A scope is only as safe as what it unlocks. For each scope in your inventory, write down:

  • Which tools require it
  • Whether each tool reads or writes
  • Which resource (files, tickets, candidate records, billing config) it touches
  • Whether the resource is per-user or shared across the org

If your server uses fine-grained permission (FGP) requirements — a catalog mapping each tool to the permission it needs — verify the catalog is in version control and matches the running build. The GitHub MCP server discussion around a “declarative FGP requirement subsystem” is a useful reference: the team realized they had a catalog of what each tool needs but no way to declare what each token actually has. That gap is the audit target.

Step 3: Flag overly broad read-write access

Three patterns almost always warrant a closer look:

  1. A single :write scope that covers every operation in a category. If notes:write lets the agent create, delete, and rename every note for every user, the scope is too coarse for any non-trusted caller.
  2. Default-on scopes. Some MCP servers enable every available scope when none are specified. The Merge server’s README is explicit: “If no scopes are specified, all available scopes will be enabled.” That is convenient in development and a liability in production. Treat default-on as a bug.
  3. Classic OAuth read/write combined into one label. “Read and write contacts” cannot be granted without granting both. Split it.

Write down which of these you found. Each becomes a row in the remediation list.

Step 4: Narrow scopes without breaking workflows

The goal is to keep workflows working while removing capability you do not use. Practical moves:

  • Split read from write. If a workflow only ever reads notes, do not grant notes:write. If a workflow needs to write one specific record, prefer a narrower scope like notes:append over a blanket write scope, if your server supports it.
  • Pre-filter tools the token cannot use. Some servers let you declare the token’s granted scopes up front (a flag like --granted-scopes or an env var like GITHUB_GRANTED_PERMISSIONS) so the tools list is computed against what the token actually has, not what it requested. Hiding a tool the model can’t run reduces hallucinated 403s and tightens the surface area at the same time.
  • Use server-side checks for context. If a tool’s behavior depends on user role, resource owner, or workflow state, do not encode that in the scope string. Evaluate it in the tool handler against your own RBAC layer. Scopes are a coarse first gate; they are not a substitute for policy.
  • Make the consent screen per-scope, not all-or-nothing. Some auth providers now let users deselect individual scopes at the consent screen. If yours does, enable it. Founders in B2B contexts especially want to stop a junior employee from granting broad org data access to their personal Claude account by accident.

When you revoke or narrow a scope, do it on a staging token first. Watch logs for one to two weeks for tools that quietly relied on the broader scope, then promote the change.

Step 5: Detect drift after server updates

Scope drift is what happens when a vendor ships a new tool or a new scope in a minor release and your config still asks for the old set, so the token silently grows new grants. Two habits help:

  • Diff the scope catalog on every server update. Subscribe to vendor changelogs or pin to a version and review the diff. A new tool that needs a new scope is a decision, not an automatic upgrade.
  • Re-run steps 1–4 after each update, even if “nothing changed.” The audit is cheap; the incident is not. A quarterly cadence plus an on-update trigger is usually enough for a small team.

Step 6: Record findings and decisions

Every audit produces a short document: which servers were reviewed, which scopes were flagged, what was narrowed or revoked, and what was intentionally left in place. “We left notes:write broad because only the founder’s agent uses it” is a legitimate decision if it is written down and reviewed.

FAQ

How often should I run this audit? Once per quarter is a reasonable baseline for a small team. Add an extra pass every time a vendor ships a server update that mentions new tools or scopes, and every time you add a new integration yourself.

What if my MCP server does not expose scopes at all? That is a finding in itself. A server that lets any caller with a valid bearer token invoke any tool is the worst case for least privilege. Either push the vendor for a scope catalog or replace it with one that has one.

Are OAuth scopes enough on their own? No. Scopes are a coarse first gate. They define broad capability classes. Anything that depends on role, resource, or context — which, in practice, is most things — needs a server-side check. Treat scopes as the entry ticket and your own policy layer as the door.

What is the single highest-value change I can make this week? Turn off default-on scopes everywhere they exist, and split read from write for any scope that currently bundles them. That alone removes most of the accidental-write risk in an MCP setup.

Sources