Skip to content

Scopes and step-up

Scopes and permissions are two axes on purpose. A scope gap is a 403 the client can fix by re-authorising. A permission gap is an administrator’s job. See Deployment for how the two fit in the stack.

toolScopes: {
search_docs: 'mcp',
update_doc: ['mcp', 'write'],
},

Use capabilityScopes when prompts or resources need step-up too. Keys are a bare tool name (or tool:name), prompt:name, and resource:<uri> — the URI as you registered it, so a templated resource keeps its placeholders: resource:cases://case/{id}. A read of cases://case/C1 is matched back to that template before its scope is looked up. In proxy mode the key is the label instead, because that is what the permission map is keyed by there.

Permissions determine capability visibility. Scopes determine whether this token can invoke a visible capability. The request order follows: verify the baseline token, evaluate the capability’s permission, challenge for a missing capability scope, then dispatch.

cases:write permission ✓ + write scope ✗
tools/list → update_case is visible
tools/call → 403 insufficient_scope, scope="mcp write"
client re-authorises → retry

Insufficient scope becomes HTTP 403 with WWW-Authenticate: … error="insufficient_scope", which a conforming client can act on. A tool isError cannot.

A token carries the scopes the person ticked, which can be fewer than the client requested. Cloudflare’s consent dialog for Wrangler and its API MCP server lets somebody drop any optional scope before approving, leaving the required ones selected.

Mint a scope per tool and you hand that person forty checkboxes. They clear the ones they do not recognise, and the first call behind one of those fails. Name a step-up scope after the risk it carries, write or delete, and let the permission sort out which tools sit behind it. Twenty tools share two scopes, and the dialog asks two questions somebody can answer.

Declining a scope runs the same trace as the write gap under What each axis controls. The capability stays in the listing because the permission still grants it, and the challenge names the one scope to grant, so the client re-authorises for that scope alone.

Every scope in toolScopes, capabilityScopes and supportedScopes is advertised in protected-resource metadata, so a client can see the full set before it asks for anything.

Scope selection happens only after the 2026 request classifier and standard-header validation have proved Mcp-Method and the decoded Mcp-Name match the JSON-RPC body. SEP-2243 Base64 sentinel names are decoded before lookup, so encoding a name cannot downgrade its scope.

The official Python SDK exposes only global HTTP scope requirements. AuthSettings.required_scopes enforces the baseline scope, and per-capability permissions control discovery and invocation.

The shared routing and scope helpers (classify_scoped_request, scopes_for_capability, decode_mcp_name_header) pin the intended step-up behaviour. The package stops short of double-verifying tokens or patching private SDK routes to manufacture transport-level per-capability challenges.

Policy decisions stay identical across the two languages, which is what the conformance fixtures pin.