Skip to content

Commit b442200

Browse files
committed
docs(mcp): clarify that both safelisted and arbitrary operations are supported
1 parent ff35373 commit b442200

2 files changed

Lines changed: 30 additions & 109 deletions

File tree

docs/router/mcp.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ icon: "robot"
66

77
For a high-level introduction, see the [MCP Gateway overview](https://wundergraph.com/mcp-gateway).
88

9-
WunderGraph's MCP Gateway is a feature of the Cosmo Router that enables AI models to interact with your GraphQL APIs using a structured protocol. Instead of allowing arbitrary queries, it exposes a predefined set of validated GraphQL operations as tools that AI models can discover and use.
9+
WunderGraph's MCP Gateway is a feature of the Cosmo Router that enables AI models to interact with your GraphQL APIs using a structured protocol. We can expose a predefined set of safelisted operations, or capabilities as MCP tools, or allow agents to execute GraphQL directly.
1010

1111
<Frame caption="Cosmo Router exposes your GraphQL Supergraph over MCP, enabling AI platforms like Cursor, Windsurf, and Claude to discover and execute operations.">
1212
<img

docs/router/mcp/oauth.mdx

Lines changed: 29 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,7 @@ The MCP server supports OAuth 2.1 authorization with JWKS-based JWT validation,
1212

1313
## Overview
1414

15-
When OAuth is enabled, every HTTP request to the MCP server must include a valid JWT bearer token in the `Authorization` header. The server validates the token using configured JWKS providers and enforces scope requirements at multiple levels:
16-
17-
1. **HTTP level**`initialize` scopes are checked on every request before any JSON-RPC processing
18-
2. **Method level** — Additional scopes can be required for specific MCP methods (`tools/list`, `tools/call`)
19-
3. **Per-tool level** — Individual tools can require additional scopes derived from `@requiresScopes` directives in your GraphQL schema
20-
4. **Runtime level** — The `execute_graphql` tool (when enabled) checks `@requiresScopes` for the specific fields in each query at request time
21-
22-
All scope enforcement happens at the HTTP transport level per the MCP specification, returning standard `403 Forbidden` responses with `WWW-Authenticate` headers that enable MCP clients to perform step-up authorization.
15+
When OAuth is enabled, every HTTP request to the MCP server must include a valid JWT bearer token in the `Authorization` header. The server validates the token using configured JWKS providers and enforces scope requirements at multiple levels. All scope enforcement happens at the HTTP transport level per the MCP specification, returning `403 Forbidden` responses with `WWW-Authenticate` headers that enable [step-up authorization](#token-upgrade-flow).
2316

2417
## Quick Start
2518

@@ -177,99 +170,15 @@ type Query {
177170

178171
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.
179172

180-
The `@requiresScopes` directive uses **OR-of-AND** semantics:
181-
- `[["read:fact"], ["read:all"]]` means the token needs `read:fact` **OR** `read:all`
182-
- `[["read:employee", "read:private"], ["read:all"]]` means the token needs (`read:employee` **AND** `read:private`) **OR** `read:all`
183-
184-
When a token lacks the required scopes, the server returns a `403 Forbidden` with a `WWW-Authenticate` header containing the **best** scope challenge — the scope group that requires the fewest additional scopes based on what the token already has.
185-
186-
#### Multi-Field Scope Combination
187-
188-
When an operation touches **multiple** fields that have `@requiresScopes`, the MCP server computes the **Cartesian product** of their OR-groups to produce the combined scope requirement for the tool. This ensures the token satisfies the requirements of every field in the operation simultaneously.
173+
The `@requiresScopes` directive uses OR-of-AND semantics. When an operation touches multiple fields with `@requiresScopes`, the MCP server computes the combined scope requirement using the [Cartesian product rules](/federation/directives/requiresscopes#combining-scopes-in-the-same-subgraph). For a full explanation of how scopes combine, see the [`@requiresScopes` directive documentation](/federation/directives/requiresscopes).
189174

190-
For example, given a schema:
191-
192-
```graphql
193-
type Query {
194-
employee(id: ID!): Employee @requiresScopes(scopes: [["read:employee", "read:private"], ["read:all"]])
195-
topSecretFacts: [Fact!]! @requiresScopes(scopes: [["read:fact"], ["read:all"]])
196-
}
197-
```
198-
199-
And an operation that queries **both** fields:
200-
201-
```graphql
202-
"""
203-
Get an employee's profile and top secret facts.
204-
"""
205-
query GetEmployeeWithFacts($id: ID!) {
206-
employee(id: $id) { id name }
207-
topSecretFacts { id description }
208-
}
209-
```
210-
211-
The per-field requirements are:
212-
- `employee`: `(read:employee AND read:private) OR (read:all)`
213-
- `topSecretFacts`: `(read:fact) OR (read:all)`
214-
215-
The Cartesian product combines every group from the first field with every group from the second field, deduplicating within each combination:
216-
217-
| employee group | topSecretFacts group | Combined AND-group |
218-
| --- | --- | --- |
219-
| `read:employee`, `read:private` | `read:fact` | `read:employee`, `read:private`, `read:fact` |
220-
| `read:employee`, `read:private` | `read:all` | `read:employee`, `read:private`, `read:all` |
221-
| `read:all` | `read:fact` | `read:all`, `read:fact` |
222-
| `read:all` | `read:all` | `read:all` |
223-
224-
The resulting combined requirement for the tool is:
225-
226-
```
227-
(read:employee AND read:private AND read:fact)
228-
OR (read:employee AND read:private AND read:all)
229-
OR (read:all AND read:fact)
230-
OR (read:all)
231-
```
232-
233-
A token satisfying **any one** of these four groups can call the tool.
234-
235-
#### Smart Scope Challenge Selection
236-
237-
When a token lacks the required scopes, the server doesn't just return an arbitrary scope group — it picks the **best** one based on what the token already has. The algorithm:
238-
239-
1. For each AND-group, count how many scopes the token is **missing**
240-
2. If any group has 0 missing, the token is sufficient (no challenge needed)
241-
3. Pick the group with the **fewest missing** scopes (ties go to the first group)
242-
243-
Using the combined requirement above, if a client presents a token with scopes `read:employee read:private`:
244-
245-
| AND-group | Missing scopes | Count |
246-
| --- | --- | --- |
247-
| `read:employee`, `read:private`, `read:fact` | `read:fact` | 1 |
248-
| `read:employee`, `read:private`, `read:all` | `read:all` | 1 |
249-
| `read:all`, `read:fact` | `read:all`, `read:fact` | 2 |
250-
| `read:all` | `read:all` | 1 |
251-
252-
Three groups tie with 1 missing scope. The server picks the first: `read:employee`, `read:private`, `read:fact`. The 403 response includes:
253-
254-
```
255-
WWW-Authenticate: Bearer error="insufficient_scope", scope="read:employee read:private read:fact"
256-
```
257-
258-
The client can then request the additional `read:fact` scope from the authorization server and retry.
259-
260-
<Info>
261-
The `scope` parameter in the `WWW-Authenticate` header contains the **complete AND-group** (not just the missing scopes), per RFC 6750. This tells the client exactly which scopes are needed for the operation. If `scope_challenge_include_token_scopes` is enabled, the token's existing scopes are also included — see [Scope Challenge Behavior](#scope-challenge-behavior).
262-
</Info>
263-
264-
<Info>
265-
Per-tool scopes are computed at startup (and on config reload) by analyzing which fields each operation touches. This means scope requirements are enforced at the HTTP level before the GraphQL query is executed, with zero runtime overhead per request.
266-
</Info>
175+
Per-tool scopes are computed at startup (and on config reload), so they are enforced at the HTTP level with zero runtime overhead per request.
267176

268177
### Runtime Scopes (`execute_graphql`)
269178

270179
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.
271180

272-
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.
181+
This runtime check uses the same [OR-of-AND semantics](/federation/directives/requiresscopes) 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.
273182

274183
<Info>
275184
If the GraphQL query cannot be parsed, the request is passed through to the GraphQL engine, which handles the error. Scope checking is best-effort and does not block malformed queries.
@@ -287,7 +196,7 @@ This runtime check uses the same OR-of-AND semantics and smart challenge selecti
287196

288197
## `get_operation_info` and Scope Discovery
289198

290-
The built-in `get_operation_info` tool includes scope requirements in its response when a tool's underlying operation has `@requiresScopes` directives. This allows AI models to understand what scopes are needed before attempting to call a tool:
199+
The `get_operation_info` tool includes scope requirements in its response, allowing AI models to discover what scopes a tool needs before calling it:
291200

292201
```
293202
Required Scopes (OR-of-AND):
@@ -296,26 +205,38 @@ Required Scopes (OR-of-AND):
296205
- read:all
297206
```
298207

299-
This enables AI assistants to inform users about required permissions before attempting operations that may fail due to insufficient scopes.
300-
301208
## Token Upgrade Flow
302209

303-
Because authentication happens at the HTTP level, tokens can be upgraded **on the same MCP session** without reconnecting:
304-
305-
1. A client connects with a token that has `mcp:connect` scope
306-
2. The client calls `tools/call` and receives a `403 Forbidden` with `insufficient_scope`
307-
3. The client obtains a new token with additional scopes from the authorization server
308-
4. The client retries with the new token on the same session (same `Mcp-Session-Id`)
210+
Tokens can be upgraded **on the same MCP session** without reconnecting:
309211

310-
This step-up authorization pattern is fully supported and does not require re-establishing the MCP session. The flow is the same regardless of which scope level triggered the 403 (method, per-tool, or runtime).
212+
1. Client connects with a token that has `mcp:connect` scope
213+
2. Client calls `tools/call` and receives `403 Forbidden` with `insufficient_scope`
214+
3. Client obtains a new token with additional scopes from the authorization server
215+
4. Client retries with the new token on the same session (same `Mcp-Session-Id`)
311216

312217
## Scope Challenge Behavior
313218

314-
When the server returns a `403 Forbidden` response, the `WWW-Authenticate` header includes a `scope` parameter telling the client which scopes to request. The `scope_challenge_include_token_scopes` option controls how this parameter is built.
219+
When the server returns a `403 Forbidden` response, the `WWW-Authenticate` header includes a `scope` parameter per [RFC 6750](https://datatracker.ietf.org/doc/rfc6750/) telling the client which scopes to request.
220+
221+
For per-tool and runtime challenges where multiple scope groups can satisfy the requirement (OR-of-AND), the server selects the **best** group — the one requiring the fewest additional scopes based on what the token already has:
222+
223+
1. For each AND-group, count how many scopes the token is **missing**
224+
2. Pick the group with the **fewest missing** scopes (ties go to the first group)
225+
226+
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`:
227+
228+
| AND-group | Missing scopes | Count |
229+
| --- | --- | --- |
230+
| `read:employee`, `read:private`, `read:fact` | `read:fact` | 1 |
231+
| `read:all` | `read:all` | 1 |
232+
233+
Both groups have 1 missing scope. The server picks the first, returning the **complete AND-group** (not just the missing scopes):
315234

316-
Per [RFC 6750](https://datatracker.ietf.org/doc/rfc6750/), the `scope` parameter should contain the scopes *"required for accessing the requested resource"*. The server returns only the scopes needed for the specific operation that failed — not all scopes the server supports.
235+
```
236+
WWW-Authenticate: Bearer error="insufficient_scope", scope="read:employee read:private read:fact"
237+
```
317238

318-
For per-tool and runtime scope challenges where multiple scope groups can satisfy the requirement (OR-of-AND), the server selects the **best** group: the one requiring the fewest additional scopes based on what the token already has. This minimizes the authorization step-up needed.
239+
The client can then request the additional `read:fact` scope from the authorization server and retry.
319240

320241
### `scope_challenge_include_token_scopes`
321242

0 commit comments

Comments
 (0)