Control MCP tool access with per-tool policies

Assign Rego policies to individual tools on an MCP surface so only callers whose credentials satisfy the policy can invoke each tool.

This guide assigns Rego policies to individual tools on an MCP surface using the MCP Tools canvas element.

Without per-tool policies, any caller that passes surface-level authentication can invoke every tool your server exposes. The tool policy mechanism is an allowlist: a tool not in the list is denied regardless of the caller’s credentials.

Use this guide when you have an MCP surface and need to control which tools specific callers can invoke. You create a policy definition in the Policies section, add the MCP Tools element to the canvas, and confirm with three targeted requests.

Prerequisites

  • An MCP surface already created and reachable. If you have not built one yet, complete Restrict surface access with API key authentication first.
  • Dashboard access with permission to manage policies and surfaces.
  • Basic familiarity with Rego, the OPA policy language. See OPA policies for an introduction.
  • If your policies check caller identity (for example, input.jwt.role), the surface must use JWT bearer auth on its Caller Context element. Without JWT bearer auth, input.jwt is absent at evaluation time and any JWT-based rule denies every request.
Surface target caller context MCP

Steps

Create a policy definition

All policy definitions are created in Policies before being attached to a surface.

  1. In the sidebar, select Policies.
  2. Select the Agent Surfaces tab.
  3. Select Define Agent Surface Policy.
  4. Fill in the form.
    • Name: a short identifier such as mcp-admin-only or mcp-safe-tools.
    • Type: select Agent Surfaces.
    • Description (optional): a one-line note about what this policy enforces.
    • Policy Content (Rego): paste your Rego policy. See Example policies below for ready-to-use starting points.
  5. Select Save.

The policy appears in the list with Enabled status.

Repeat this step for each distinct policy you need. A surface can bind different tools to different policies.

Add the MCP Tools element and bind policies to tools

Surface target caller context MCP
  1. Open Surfaces and select the MCP surface to configure.
  2. In the left palette, find the MCP Tools element under the Security & Policy category.
  3. Drag MCP Tools onto the canvas and drop it on the Managed Agent node.
  4. Select the MCP Tools node to open its config panel.
  5. Under Per-Tool Access Control, select Add Tool Policy.
  6. Fill in the tool entry.
    • In the tool name field, enter the exact MCP tool name as your server declares it, for example search or execute_query. Names are case-sensitive.
    • In the policy dropdown, select the policy definition you created in Step 1.
    • In the description field (optional), enter a note explaining why this tool has this policy.
  7. Repeat steps 5–6 for each tool you want to permit.
  8. Select the Save changes button (the blue disk icon in the toolbar) or press Cmd+S (macOS) / Ctrl+S (Windows/Linux).

Any tools/call for a tool not listed here is denied without evaluating any policy.

Confirm

Replace <YOUR_GATEWAY_HOST> and <CHANNEL_ROUTE> with values from the Access Point panel.

These tests use the local echo server (http://localhost:8080) as a stand-in target. HTTP status codes match the expected values. Response bodies reflect the echo server output rather than a real MCP response. Start the echo server with python3 echo-server.py if it is not already running (copy the script from Echo server).

Test 1: calling a listed tool should succeed

Replace search with a tool name you added in Step 2.

The Authorization: Bearer header carries the JWT. The gateway validates it (using the JWT bearer Caller Context on the surface) and extracts the payload claims into input.jwt. A policy that checks input.jwt.role == "admin" therefore requires the JWT payload to contain "role": "admin".

To generate a test JWT with specific claims, use a tool such as jwt.io or your identity provider’s token API. The token must be signed and accepted by your configured JWT strategy.

curl -k -X POST "https://<YOUR_GATEWAY_HOST><CHANNEL_ROUTE>" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <your-jwt-with-required-claims>" \
  -d '{"jsonrpc":"2.0","method":"tools/call","id":1,"params":{"name":"search","arguments":{"query":"test"}}}'

The -k flag disables TLS certificate verification. Use this for local testing only. Remove it in production.

Expected response: 200 OK. The policy passed and the gateway forwarded the request to the target.

Test 2: calling a tool not in the list should be denied

Replace unlisted_tool with any tool name you did not add in Step 2.

curl -k -X POST "https://<YOUR_GATEWAY_HOST><CHANNEL_ROUTE>" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer my-jwt-token" \
  -d '{"jsonrpc":"2.0","method":"tools/call","id":2,"params":{"name":"unlisted_tool","arguments":{}}}'

The -k flag disables TLS certificate verification. Use this for local testing only. Remove it in production.

Expected response: 403 Forbidden. The tool has no policy entry and the allowlist denied it.

Test 3: non-tools/call methods should pass through

initialize is not a tools/call request and is not evaluated by the tool policy gate.

curl -k -X POST "https://<YOUR_GATEWAY_HOST><CHANNEL_ROUTE>" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"initialize","id":3,"params":{"protocolVersion":"2024-11-05","clientInfo":{"name":"test","version":"1.0"},"capabilities":{}}}'

The -k flag disables TLS certificate verification. Use this for local testing only. Remove it in production.

Expected response: 200 OK. The gateway forwarded the request without tool policy evaluation.

Example policies

Each policy uses package surface.policy. The input context for MCP tool policies differs from gateway and surface policies in one important way.

How input.jwt is populated: when a caller sends Authorization: Bearer <token>, the gateway validates the JWT using the JWT bearer Caller Context on the surface, then extracts all payload claims into input.jwt as a flat key-value map. A policy rule such as input.jwt.role == "admin" is satisfied when the token’s payload contains "role": "admin". If the surface has no JWT bearer Caller Context, or the caller sends no token, input.jwt is absent and any rule that references it evaluates to undefined, which counts as false.

// Example JWT payload that satisfies input.jwt.role == "admin"
{
  "sub": "user@example.com",
  "role": "admin",
  "org": "acme"
}

Do not use input.source_auth.* fields in MCP tool policies: those fields are absent from this input context. For the full input schema for each policy scope, see OPA policies.

Allow any authenticated user

Permits any caller that provided a JWT token. The tool name is already controlled by the policy entry. This policy adds an authentication requirement on top of the allowlist.

package surface.policy

default allow = false

allow if {
    input.jwt.sub
}

Admin-only access

Restricts the tool to callers whose JWT contains role = "admin".

package surface.policy

default allow = false

allow if {
    input.jwt.role == "admin"
}

Role-based access with a structured denial reason (audit log only)

Defining deny_reason adds context to the structured audit log event emitted on denial. The caller always receives the same hardcoded JSON-RPC error (code: -32000, message: "Access denied: insufficient permissions for this tool"). deny_reason does not appear in the HTTP response.

package surface.policy

default allow = false

allow if {
    input.jwt.role == "admin"
}

deny_reason = "Only admin users can call this tool." if {
    not input.jwt.role
}

deny_reason = sprintf("Role '%v' is not authorised for this tool.", [input.jwt.role]) if {
    input.jwt.role
    input.jwt.role != "admin"
}

Subscription-based access

Grants premium subscribers access while blocking free-tier callers.

package surface.policy

default allow = false

allow if {
    input.jwt.subscription == "premium"
}

Allow unconditionally (entry list is the sole control)

When access control is determined entirely by which tools are listed, with no condition on caller attributes, a policy that always allows is the correct choice.

package surface.policy

default allow = true

Troubleshooting

SymptomLikely causeFix
403 on every tools/call, including listed tools.The policy is evaluating allow = false for all requests.Open the policy in Policies and test it in the OPA Playground using the MCP tool input schema from OPA policies.
403 with message No MCP tool policy configured for tool.The requested tool name has no entry in the surface’s MCP Tool Policies list.Add an entry for that tool, or check for a case mismatch between the entry and the name your MCP server declares.
Policy updates have no effect.The surface was not saved after editing the tool policy entries.Re-open the surface, confirm the entries are present, and select Save.
403 on tools/call despite a valid JWT.The policy references a claim that is absent from the token.Decode the token at jwt.io to confirm the claim exists and is spelled correctly.
403 on initialize or tools/list.The rejection comes from the Caller Context or a surface-level policy, not the tool policy gate.Tool policies only gate tools/call. Check the surface-level policy for non-tool-call rejections.
Policy definition not available in the surface editor dropdown.The definition was created with type Gateway instead of Agent Surfaces.Create a new definition with type Agent Surfaces and select it in the surface editor.

Next steps