---
title: Fine-Grained Authorization | Keycard
description: Write Policies that permit or deny individual MCP tool calls through a Unified Access Gateway
---

Fine-grained authorization lets you govern a [Unified Access Gateway](/admin/unified-access-gateway/index.md) at the level of individual MCP tool calls. On every tool call the gateway forwards, Keycard evaluates your Zone’s active [Access Policies](/admin/access-policies/index.md) to verify that the specific action is allowed, and you can configure different permissions for different sets of users. For example, permit read-only tools to everyone while restricting write or delete tools to a smaller cohort.

Tools only

The initial release of Unified Access Gateway only exposes MCP **tools**. Requests for other items such as `prompts/*` or `resources/*` are answered with a JSON-RPC “method not found” error (`-32601`) rather than forwarded to an upstream. Fine-grained authorization therefore covers tool calls and tool discovery. See [Prompts and resources](#prompts-and-resources) for the planned shape.

## How it works

The gateway checks the MCP method and tool name against your Policies before forwarding the request to the upstream MCP server. A denial blocks the call before it ever reaches the upstream, and every decision is recorded in the audit log.

Gateway activity maps onto Cedar actions as follows:

| MCP activity                  | Cedar action                                                                       |
| ----------------------------- | ---------------------------------------------------------------------------------- |
| Tool call (`tools/call`)      | `Keycard::Action::"mcp::tools/call::<tool-name>"`                                  |
| Tool discovery (`tools/list`) | `Keycard::Action::"mcp::tools/list"`, a member of `Keycard::Action::"MCPProtocol"` |

These are the only two actions a gateway evaluates. The `resource` in each evaluation is the upstream MCP server the gateway is talking to, so `resource.identifier` is that upstream’s MCP URL — not the gateway’s own URL.

## Prerequisites

- A Unified Access Gateway with upstream MCP servers attached (see [Unified Access Gateway](/admin/unified-access-gateway/index.md))
- **Manager** access to the Zone (see [Roles & Permissions](/admin/roles-and-permissions/index.md))
- Familiarity with authoring and activating Policies (see [Access Policies](/admin/access-policies/index.md))

## Create fine-grained Policies

1. **Identify the tools to gate**

   Reference each upstream MCP server’s documentation for the tool names it exposes. To construct the Cedar action for a tool, prepend `mcp::tools/call::` to the tool name exactly as the upstream publishes it. For example, a tool named `create_event` becomes:

   ```
   Keycard::Action::"mcp::tools/call::create_event"
   ```

   Use the bare tool name from the upstream, not the `<upstream>__` prefixed name shown in the gateway’s aggregated tool list. The prefix is presentation only: the gateway strips it before evaluating Policy, so the action always carries the name the upstream will actually execute.

   Note

   Tool discovery and management in the Console is coming soon: you will be able to browse the tools each upstream exposes and review tool call activity directly in Keycard.

2. **Permit sign-in and discovery**

   Users need to sign in to the gateway and list its tools before any tool is called. Permit the gateway endpoint itself, and tool discovery on each upstream.

   To find the identifiers, open the gateway’s Application details page in Keycard Console (**Applications → your gateway**): the gateway’s own identifier is the **MCP Access URL** on the **Application settings** page, and the upstream identifiers are the MCP URLs of the Resources listed under **Dependencies**.

   ```
   @id("gateway-signin")
   @description("Allow users to sign in to and authorize the gateway")
   permit (
     principal is Keycard::User,
     action,
     resource
   ) when {
     resource.identifier == "https://<gateway-name>.<zone-id>.mcp.gateway.context/mcp"
   };


   @id("gateway-mcp-protocol")
   @description("Tool discovery on the upstreams")
   permit (
     principal is Keycard::User,
     action in Keycard::Action::"MCPProtocol",
     resource
   ) when {
     ["https://<first-upstream-mcp-url>/mcp",
      "https://<second-upstream-mcp-url>/mcp"].contains(resource.identifier)
   };
   ```

   The `MCPProtocol` group covers the MCP protocol actions that target no specific item, including the discovery actions `mcp::tools/list`, `mcp::prompts/list`, `mcp::resources/list`, and `mcp::resources/templates/list`.

   Caution

   Without the sign-in permission users cannot authorize the gateway at all. Without the discovery permission an upstream’s tools cannot be listed, so that upstream is excluded from the user’s aggregated tool list. Discovery never invokes a tool, so permitting `MCPProtocol` does not grant tool access by itself.

   Note

   The ability to reduce the number of tools presented to the client is coming soon: the aggregated tool list will only include the tools each user is permitted to call. Today, discovery is permitted or denied per upstream — a user who can list an upstream sees all of its tools, whether or not they can call them.

3. **Permit the baseline tools for all users**

   Enumerate the read-only tools every authenticated user may call. For example, on a calendar upstream:

   ```
   @id("calendar-events-read")
   @description("Read-only event tools for all users")
   permit (
     principal is Keycard::User,
     action in
       [Keycard::Action::"mcp::tools/call::list_events",
        Keycard::Action::"mcp::tools/call::get_event",
        Keycard::Action::"mcp::tools/call::search_events"],
     resource
   ) when {
     resource.identifier == "https://<upstream-mcp-url>/mcp"
   };
   ```

   Tool calls that no Policy permits are denied: fine-grained authorization is default-deny, like every other Keycard enforcement point.

4. **Restrict sensitive tools to identity provider groups**

   Gate mutating and destructive tools on the `groups` claim your identity provider issues, using `context.subject_claims`:

   ```
   @id("calendar-events-write")
   @description("Event mutations granted to Calendar Writers")
   permit (
     principal is Keycard::User,
     action in
       [Keycard::Action::"mcp::tools/call::create_event",
        Keycard::Action::"mcp::tools/call::update_event"],
     resource
   ) when {
     resource.identifier == "https://<upstream-mcp-url>/mcp" &&
     context has subject_claims &&
     context.subject_claims has groups &&
     context.subject_claims.groups.contains("Calendar Writers")
   };


   @id("calendar-events-delete")
   @description("Event deletion granted to Calendar Admins")
   permit (
     principal is Keycard::User,
     action == Keycard::Action::"mcp::tools/call::delete_event",
     resource
   ) when {
     resource.identifier == "https://<upstream-mcp-url>/mcp" &&
     context has subject_claims &&
     context.subject_claims has groups &&
     context.subject_claims.groups.contains("Calendar Admins")
   };
   ```

5. **Activate the Policies**

   Activate the Policies in your Zone on the **Console → Policies** page. Changes take effect on the next tool call. There is nothing to deploy or restart on the gateway.

## Verify

Sign in to the gateway as a user in one of the permitted groups and call a permitted tool: it succeeds. Then call a tool that your Policies do not permit. The call is denied before reaching the upstream, and the client receives a JSON-RPC error (`-32603`, `access denied by policy for <upstream-mcp-url>`) over HTTP 403 rather than a tool result. Because it is a protocol-level error and not a failed tool result, MCP clients typically surface it as a connection or request error rather than as tool output.

If discovery is denied on every upstream at once, the gateway answers the client’s `tools/list` with HTTP 403 and `no unified upstreams are accessible with the presented grant`. When only some upstreams are denied, the response is a successful, partial tool list.

Check **Console → Audit Log** for both decisions. Every evaluation is logged with the user, the upstream, the tool name, and the Policy outcome, so denied calls carry the context of what was denied and why.

## Prompts and resources

MCP prompts and resources are not yet available through a Unified Access Gateway. When these surfaces arrive, they are expected to follow the same action-naming pattern as tools, with the item identifier appended to the method:

| Planned MCP activity             | Planned Cedar action                                     |
| -------------------------------- | -------------------------------------------------------- |
| Prompt (`prompts/get`)           | `Keycard::Action::"mcp::prompts/get::<prompt-name>"`     |
| Resource read (`resources/read`) | `Keycard::Action::"mcp::resources/read::<resource-uri>"` |

A Policy written against those actions would look like any other:

```
@id("prompts-code-review")
@description("The code_review prompt template for Engineering")
permit (
  principal is Keycard::User,
  action == Keycard::Action::"mcp::prompts/get::code_review",
  resource
) when {
  resource.identifier == "https://<upstream-mcp-url>/mcp" &&
  context has subject_claims &&
  context.subject_claims has groups &&
  context.subject_claims.groups.contains("Engineering")
};
```

Caution

This is a preview of the intended shape, not a supported configuration. A Policy like the one above is accepted and activated, but no gateway traffic will ever match it, so it neither grants nor blocks anything today. The action names and the identifier used for MCP resources may change before the surfaces ship.

## Troubleshooting

- **A tool call is denied that a Policy should allow.** Confirm the action uses the bare tool name (`mcp::tools/call::create_event`), not the prefixed name from the aggregated tool list (`calendar__create_event`), and that `resource.identifier` matches the upstream MCP server URL exactly. The gateway evaluates against the upstream’s identifier, never the gateway’s own.
- **A group-gated Policy never matches.** Verify your identity provider issues the `groups` claim and that the user is a member. The Policy’s `context has subject_claims` guards only prevent evaluation errors; a missing claim means the Policy silently does not match.
- **A user sees no tools from an upstream at all.** Tool listing is gated by `mcp::tools/list` on that upstream. Check the user is permitted it (directly or via the `MCPProtocol` group), then have them reconnect.
- **A client reports “method not found” for a prompt or resource.** Expected: a gateway serves the tools surface only. Reach that upstream directly instead of through the gateway if you need its prompts or resources.
