---
title: "Tool authorization"
url: "/docs/mcp-gateway/tool-authorization/"
kind: "feature"
summary: "Per-tool RBAC for MCP: allow or deny listing, search, calls and code-mode scripts based on validated JWT claims and header fields."
---

> Arion site index: [/llms.txt](/llms.txt)

# Tool authorization

Per-tool RBAC for MCP: allow or deny listing, search, calls and code-mode scripts based on validated JWT claims and header fields.

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](@/docs/security/jwt.md) 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:

```yaml,arion=fragment
# 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
```

> [!IMPORTANT]
> Without `payload_in_metadata`, no claims reach the gateway and every `ALLOW` rule denies — for everyone, including admins.

## Restrict a tool

The admin-only pattern — `action: ALLOW` denies by default and permits only matching callers:

{% raw %}
```yaml,arion=fragment
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}}
```
{% endraw %}

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 blocking `role: 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:

```yaml,arion=fragment
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](@/docs/mcp-gateway/federation.md) or an [OpenAPI source](@/docs/mcp-gateway/openapi-tools.md), the same `rbac` block applies to every tool it contributes.

## In code mode

A [code-mode](@/docs/mcp-gateway/code-mode.md) 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](@/docs/mcp-gateway/tools-and-rest-backends.md#verify)), then call the restricted tool:

```bash
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:

```json
{"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 `rbac` are 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](@/docs/security/cedar-policies.md) evaluates HTTP requests, not individual MCP tool calls; for choosing between the gateway-level authorization filters, see [authorization](@/docs/security/authorization.md).

The `rbac` field table lives in the [filter reference](@/reference/filters/http/mcp-gateway.md).
