Scope Enforcement - WunderGraph
Enforcement Levels
HTTP Request
-> Initialize scopes (all requests)
-> Method scopes (tools/list, tools/call)
-> Built-in tool scopes (execute_graphql, get_schema, get_operation_info)
-> Per-tool scopes (registered operations, from @requiresScopes)
-> Runtime scopes (execute_graphql inline queries, per-query)
Initialize Scopes
The scopes.initialize list defines scopes required for every HTTP request to the MCP server. These are checked before the JSON-RPC payload is parsed, serving as the baseline authorization to establish an MCP connection.
oauth:
scopes:
initialize:
- 'mcp:connect'
Method-Level Scopes
Additional scopes can be required for specific MCP methods:
oauth:
scopes:
tools_list:
- 'mcp:tools:read' # Required to discover available tools
tools_call:
- 'mcp:tools:execute' # Required to execute any tool
When both initialize and method-level scopes are configured, the token must contain all of them. For example, calling tools/call requires both the initialize scopes and the tools_call scopes.
Scopes in the JWT must be provided as a space-separated string in the scope claim (per OAuth 2.0 convention). Array-format scope claims are not supported.
Built-in Tool Scopes
The MCP server provides three built-in tools: execute_graphql, get_operation_info, and get_schema. Each can have its own scope requirements:
oauth:
scopes:
tools_call:
- 'mcp:tools:execute' # Base gate for any tool
execute_graphql:
- 'mcp:graphql:execute' # Additional scope for execute_graphql
get_operation_info:
- 'mcp:ops:read' # Additional scope for get_operation_info
get_schema:
- 'mcp:schema:read' # Additional scope for get_schema
Built-in tool scopes are additive to tools_call - the token must satisfy both. If tools_call is empty, only the built-in tool scope is checked. Scopes for execute_graphql are only relevant when enable_arbitrary_operations: true, and scopes for get_schema are only relevant when expose_schema: true. When the corresponding feature is disabled, the tool is not registered and its scopes are excluded from the scopes_supported metadata.
Per-Tool Scopes (registered operations)
When your GraphQL schema uses the @requiresScopes directive on fields, the MCP server automatically extracts scope requirements for each registered operation at startup. If a tool’s underlying GraphQL operation touches fields that require specific scopes, those scopes are enforced when the tool is called. This level only applies to registered operations exposed as tools. The execute_graphql built-in tool is checked at the next level instead. For example, if your schema defines:
type Query {
topSecretFacts: [Fact!]! @requiresScopes(scopes: [["read:fact"], ["read:all"]])
employee(id: ID!): Employee @requiresScopes(scopes: [["read:employee", "read:private"], ["read:all"]])
}
And you have an operation getTopSecretFacts.graphql that queries topSecretFacts, calling that tool will require either read:fact OR read:all in addition to any initialize and tools_call scopes. The @requiresScopes directive uses OR-of-AND semantics.
Runtime Scopes (execute_graphql inline queries)
When enable_arbitrary_operations is enabled, the execute_graphql tool allows AI models to craft custom GraphQL queries. Since the server cannot know which fields will be queried ahead of time, scope checking happens at request time by parsing the GraphQL query and extracting @requiresScopes requirements for the fields it references. This runtime check uses the same OR-of-AND semantics and smart challenge selection as per-tool scopes. If the token lacks required scopes, the server returns a 403 Forbidden with an appropriate scope challenge before the query is executed.
Scope Discovery with get_operation_info
The get_operation_info tool includes scope requirements in its response, allowing AI models to discover what scopes a tool needs before calling it:
Required Scopes (OR-of-AND):
- read:employee AND read:private
OR
- read:all
Summary
| Level | When Checked | Configured Via | Failure Response |
|---|---|---|---|
| Initialize | Every HTTP request | oauth.scopes.initialize |
403 with required scopes |
| Method | tools/list, tools/call |
oauth.scopes.tools_list, oauth.scopes.tools_call |
403 with required scopes |
| Built-in tool | tools/call for built-in tools |
oauth.scopes.execute_graphql, oauth.scopes.get_schema, oauth.scopes.get_operation_info |
403 with required scopes |
| Per-tool | tools/call for a registered operation |
@requiresScopes in GraphQL schema |
403 with best scope challenge |
| Runtime | execute_graphql inline queries |
@requiresScopes in GraphQL schema |
403 with best scope challenge |
Token Upgrade Flow
Tokens can be upgraded on the same MCP session without reconnecting:
- Initial connection
- Client connects with a token that has
mcp:connectscope.
- Client connects with a token that has
- Tool call rejected
- Client calls
tools/calland receives403 Forbiddenwithinsufficient_scope.
- Client calls
- Step-up authorization
- Client obtains a new token with additional scopes from the authorization server. Requires client support for step-up authorization.
- Retry on same session
- Client retries with the new token on the same session (same
Mcp-Session-Id).
- Client retries with the new token on the same session (same
MCP client limitation (as of April 2026): Most MCP clients - including Claude Code and the MCP TypeScript SDK - do not yet support step-up re-authorization when they receive a 403 insufficient_scope response. The server implements the flow correctly per the MCP spec, but clients may fail to obtain a new token with broader scopes.
Scope Challenge Behavior
When the server returns a 403 Forbidden response, the WWW-Authenticate header includes a scope parameter telling the client which scopes to request. For per-tool and runtime challenges, the server selects the best group - the one requiring the fewest additional scopes based on what the token already has:
| AND-group | Missing scopes | Count |
|---|---|---|
read:employee, read:private, read:fact |
read:fact |
1 |
read:all |
read:all |
1 |
For example, suppose a tool requires (read:employee AND read:private AND read:fact) OR (read:all) and a client presents a token with scopes read:employee read:private:
WWW-Authenticate: Bearer error="insufficient_scope",
scope="read:employee read:private read:fact"
The client can then request the additional read:fact scope from the authorization server and retry.
scope_challenge_include_token_scopes
Some MCP client SDKs do not correctly accumulate scopes when performing step-up authorization. To work around this, set scope_challenge_include_token_scopes: true to include the token’s existing scopes alongside the required scopes in the challenge.
| Value | Behavior | Trade-off |
|---|---|---|
false (default) |
Returns only the scopes required (strict RFC 6750). | Spec-compliant and secure, but requires the client to correctly accumulate scopes. |
true |
Returns the token’s existing scopes and required scopes in the challenge. | More compatible but reveals the token’s existing scopes in the response header. |