Cookbook: Authorize MCP tool calls with OpenFGA
- Applies to
- Gatewayruntime for API, AI, and MCP gatewaysEnterprise add-on
- Hosting
- Managed EdgeManaged DedicatedSelf-Hosted
Authenticating the MCP route tells you who the caller is. It doesn't tell you
whether that caller may run a particular tool with particular arguments, such as
transferring $5,000 out of account acc_123. This cookbook adds that decision
with the OpenFGA Authorization policy:
a small custom policy reads the caller and the tool arguments, and OpenFGA
decides whether the call goes ahead.
Where the decision runs
When a client calls a tool, the MCP Server handler invokes the tool's API route inside the gateway. That invocation runs the route's full inbound policy pipeline, then its handler and outbound policies, the same as a direct HTTP request. Put the authorization policies on the tool's route, not only on the MCP route.
On the tool's route, a policy sees:
| What | Where to read it |
|---|---|
| The caller | request.user, which the authentication policy on the tool's route sets |
| Path arguments | request.params |
| Query arguments | request.query |
| Body arguments | await request.clone().json() |
| The tool name | new ZuploMcpSdk(context).getRawCallToolRequest()?.params.name |
| The raw tool input | new ZuploMcpSdk(context).getRawCallToolRequest()?.params.arguments, as the client sent it |
The MCP Server handler maps tool arguments onto the route's path, query, header, and body parameters before it invokes the route, for both flat and nested tool input styles. Read the arguments from the request, and your policy works the same for every input style.
The handler passes the MCP request's headers, including Authorization, to the
tool's route. Add the same authentication policy you use on the MCP route to the
tool's route, and request.user holds the same caller.
Example: limit transfers by account and amount
The example API has a transferFunds operation,
POST /accounts/{accountId}/transfers, with a body of
{ "amount": 250, "toAccountId": "acc_456" }. The operation declares the
accountId path parameter and the request body schema, so the tool takes them
as arguments. The tool's name is the operationId, transferFunds, unless you
set mcp.name on the route. The rules are:
- Only callers you grant the
transferFundstool can call it. - A caller can transfer from an account only up to the limit you grant them on that account.
1. Model the rules in OpenFGA
Write the authorization model in your OpenFGA store. The within_transfer_limit
condition compares the requested amount, which the gateway sends with each
check, to the maximum stored on the relationship:
Code
Then write tuples for each user. For example, to let alice call the tool and
transfer up to 500 from acc_123:
| User | Relation | Object | Condition |
|---|---|---|---|
user:alice | can_call | mcp_tool:transferFunds | None |
user:alice | can_transfer | account:acc_123 | within_transfer_limit, { "max_amount": 500 } |
2. Write the checks policy
Create modules/transfer-authz.ts. The policy builds two OpenFGA checks from
the caller and the arguments, and stores them on the context with
OpenFGAAuthZInboundPolicy.setContextChecks. The OpenFGA policy that runs next
sends them to your store.
modules/transfer-authz.ts
The request goes ahead only when OpenFGA allows every check. If any check fails,
the OpenFGA policy returns 403 Forbidden, and the MCP client receives a tool
result with isError set.
3. Configure the policies
Add the checks policy and the OpenFGA policy to config/policies.json. Store
the OpenFGA token in an
environment variable:
config/policies.json
4. Add the policies to the tool's route
On the transferFunds operation, list the authentication policy first, then the
checks policy, then the OpenFGA policy. This example uses mcp-oauth-inbound,
the OAuth policy on the MCP route from the
MCP Server handler setup:
config/routes.oas.json
Because the checks run on the route, they also apply when a client calls
POST /accounts/{accountId}/transfers directly over HTTP. In that case there's
no tool call, so only the account check runs. To accept the operation only
through the MCP server, also add the
Internal Only policy, as described in
Keep tool routes private.
Use AuthZEN instead
If your policy decision point speaks AuthZEN, use the
AuthZEN Authorization policy in place of
OpenFGA. Build the subject, resource, and action from request.user and the
arguments in the same kind of custom policy, and store them with
AuthZenInboundPolicy.setAuthorizationPayload. This example uses the userId
and toolCall variables from the checks policy above:
Code
An AuthZEN request carries one subject, resource, and action. To decide on an argument value such as the amount, use OpenFGA conditions as this cookbook does.
Next steps
- Choose which operations become tools in Tools.
- Read every ZuploMcpSdk method available to policies and handlers.
- Test the denied and allowed paths with the tools in Test your MCP server.