Every MCP tool can carry its own rbac block that allows or denies access based on the caller's validated JWT. Use it to serve one gateway to many audiences: unauthorized tools are hidden from tools/list and search and rejected on tools/call, so each agent only ever sees its own catalog.
Before you begin
Tool RBAC reads claims that the JWT filter has already validated, so envoy.filters.http.jwt_authn must run before the MCP gateway in the same filter chain — and its provider must publish the token into request metadata:
# inside the jwt_authn provider
payload_in_metadata: jwt_payload # required for jwt_claim rules
header_in_metadata: jwt_header # only needed for jwt_header rules
Without
payload_in_metadata, no claims reach the gateway and everyALLOWrule denies — for everyone, including admins.
Restrict a tool
The admin-only pattern — action: ALLOW denies by default and permits only matching callers:
tools:
- namespace: admin
name: delete_user
description: Delete a user account.
input_schema:
inline_string: '{"type":"object","properties":{"id":{"type":"string"}},"required":["id"]}'
rbac:
action: ALLOW
permissions:
- jwt_claim:
field: role
value: admin
rest_backend:
cluster: admin_api
method: DELETE
path: /users/{{id}}
Permissions use OR logic — the rule is satisfied if any entry matches:
action: ALLOW— deny unless some permission matches.action: DENY— allow unless some permission matches (an exclusion list, for example blockingrole: guest).
A request with no JWT matches nothing: ALLOW tools deny it, DENY tools admit it. At least one permission is required.
What you can match
jwt_claim compares one payload claim to an exact value. Standard claims work by short or long name (iss/issuer, sub/subject, aud/audience — matched against any audience entry — exp, iat, nbf, jti), and any other field name matches a custom claim; numbers and booleans are compared by their string form.
jwt_header compares a token header field, typically to pin service accounts to signing keys:
rbac:
action: ALLOW
permissions:
- jwt_header:
field: kid
value: service-key-1
- jwt_header:
field: kid
value: service-key-2
Supported header fields are alg, typ, cty, kid, jku and x5u (long aliases such as algorithm and key_id also work).
On a dynamic MCP server or an OpenAPI source, the same rbac block applies to every tool it contributes.
In code mode
A code-mode script runs with the identity of the execute call that started it, and every tool call it makes goes through the same check. A denied call resolves to { ok: false, error: { code: "access_denied" } }, which the script can branch on. The catalog counts, lists, searches and describes only permitted tools. A toolkit appears only when the caller may use every tool in it. A script cannot reach anything its caller could not reach with a direct tools/call.
Verify
Initialize a session without an admin token and export the returned mcp-session-id (the flow is shown in tools and REST backends), then call the restricted tool:
curl -s http://127.0.0.1:8080/mcp \
-H 'content-type: application/json' \
-H 'accept: application/json, text/event-stream' \
-H "mcp-session-id: $SESSION_ID" \
-d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"admin.delete_user","arguments":{"id":"42"}}}'
The HTTP status is 200 but the JSON-RPC payload is an error:
{"jsonrpc":"2.0","id":3,"error":{"code":-32600,"message":"Access denied by RBAC policy for tool 'admin.delete_user'"}}
With an admin token the call dispatches normally, and the tool appears in tools/list (if it is eager) and in search results.
Limits
- Tools without
rbacare visible and callable by every request that passed the filter chain. RBAC narrows access; the JWT filter grants it. - Matching is exact string equality; there are no wildcards, regexes or list-valued custom claims.
- Denials are JSON-RPC errors over HTTP 200, so watch MCP results, not HTTP status codes.
- Per-tool policy is JWT RBAC only. The Cedar policy filter evaluates HTTP requests, not individual MCP tool calls; for choosing between the gateway-level authorization filters, see authorization.
The rbac field table lives in the filter reference.