Skip to main content

Cedar policies

Cedar policies control which authenticated clients can access which tools, prompts, and resources on your MCP servers. ToolHive evaluates these policies on every request, denying anything not explicitly permitted.

info

For the conceptual overview of authentication and authorization, see Authentication and authorization framework. For the complete dictionary of entity types, actions, and attributes, see Authorization policy reference.

Cedar policy language

Cedar policies express authorization rules in a clear, declarative syntax:

permit|forbid(principal, action, resource) when { conditions };
  • permit or forbid: Whether to allow or deny the operation
  • principal: The entity making the request (the client)
  • action: The operation being performed
  • resource: The object being accessed
  • conditions: Optional conditions that must be satisfied

MCP-specific entities

In the context of MCP servers, Cedar policies use the following entities:

Principal

The client making the request, identified by the sub claim in the access token:

  • Format: Client::<client_id>
  • Example: Client::user123

Action

The operation being performed on an MCP feature:

  • Format: Action::<operation>
  • Examples:
    • Action::"call_tool": Call a tool
    • Action::"get_prompt": Get a prompt
    • Action::"read_resource": Read a resource

Resource

The object being accessed:

  • Format: <type>::<id>
  • Examples:
    • Tool::"weather": The weather tool
    • Prompt::"greeting": The greeting prompt
    • Resource::"data": The data resource

Configuration formats

You can configure Cedar authorization using either JSON or YAML format:

JSON configuration

{
"version": "1.0",
"type": "cedarv1",
"cedar": {
"policies": [
"permit(principal, action == Action::\"call_tool\", resource == Tool::\"weather\");",
"permit(principal, action == Action::\"get_prompt\", resource == Prompt::\"greeting\");",
"permit(principal, action == Action::\"read_resource\", resource == Resource::\"data\");"
],
"entities_json": "[]"
}
}

YAML configuration

version: '1.0'
type: cedarv1
cedar:
policies:
- 'permit(principal, action == Action::"call_tool", resource ==
Tool::"weather");'
- 'permit(principal, action == Action::"get_prompt", resource ==
Prompt::"greeting");'
- 'permit(principal, action == Action::"read_resource", resource ==
Resource::"data");'
entities_json: '[]'

Configuration fields

  • version: The version of the configuration format
  • type: The type of authorization configuration (currently only cedarv1 is supported)
  • cedar: The Cedar-specific configuration
    • policies: An array of Cedar policy strings
    • entities_json: A JSON string representing Cedar entities
    • group_claim_name: Optional custom JWT claim name for group membership (for example, https://example.com/groups)

Writing effective policies

This section covers common policy patterns, from simple tool-level permits to role-based and attribute-based access control.

Basic policy patterns

Start with simple policies and build complexity as needed:

Allow specific tool access

permit(principal, action == Action::"call_tool", resource == Tool::"weather");

This policy allows any authenticated client to call the weather tool. It's useful when you want to provide broad access to specific functionality.

Allow specific user access

permit(principal == Client::"user123", action == Action::"call_tool", resource);

This policy allows a specific user to call any tool. Use this pattern when you need to grant broad permissions to trusted users.

Role-based access control (RBAC)

RBAC policies use roles from JWT claims to determine access:

permit(principal, action == Action::"call_tool", resource) when {
principal.claim_roles.contains("admin")
};

This policy allows clients with the "admin" role to call any tool. RBAC is effective when you have well-defined roles in your organization.

Group-based access control

If your identity provider includes group claims in JWT tokens (for example, groups, roles, or cognito:groups), ToolHive automatically creates THVGroup entities that you can use with Cedar's in operator:

permit(
principal in THVGroup::"engineering",
action == Action::"call_tool",
resource
);

This policy allows any member of the "engineering" group to call any tool. Group-based policies are useful when your identity provider manages group memberships centrally.

You can combine group membership with other conditions:

permit(
principal in THVGroup::"data-science",
action == Action::"call_tool",
resource == Tool::"query_database"
);

For details on how groups are resolved from JWT claims, see Group membership in the policy reference.

Attribute-based access control (ABAC)

ABAC policies use multiple attributes to make fine-grained decisions:

permit(principal, action == Action::"call_tool", resource == Tool::"sensitive_data") when {
principal.claim_roles.contains("data_analyst") &&
resource.arg_data_level <= principal.claim_clearance_level
};

This policy allows data analysts to access sensitive data, but only if their clearance level is sufficient. ABAC provides the most flexibility for complex security requirements.

Tool annotation policies

MCP servers can declare behavioral hints on their tools using annotations. ToolHive makes these annotations available as resource attributes during tools/call authorization, letting you write policies based on what a tool does rather than what it's named.

Not all MCP servers set all annotation fields. Always use Cedar's has operator to check for an annotation before accessing it, otherwise a missing attribute causes a Cedar evaluation error that ToolHive treats as a deny. For the full list of annotation attributes and detailed has operator guidance, see Tool annotation attributes.

Annotation policy examples

Allow non-destructive, closed-world tools

This pattern is useful when you want to allow tools that are both safe to run and operate within a controlled environment:

permit(
principal,
action == Action::"call_tool",
resource
) when {
resource has destructiveHint && resource.destructiveHint == false &&
resource has openWorldHint && resource.openWorldHint == false
};

Block destructive tools for non-admin users

forbid(
principal,
action == Action::"call_tool",
resource
) when {
resource has destructiveHint && resource.destructiveHint == true &&
!(principal.claim_roles.contains("admin"))
};

Real-world policy profiles

These profiles represent common authorization patterns. They progress from most restrictive to least restrictive.

Observe profile (read-only)

Allow reading prompts and resources, but block all tool calls:

authz-observe.yaml
version: '1.0'
type: cedarv1
cedar:
policies:
- 'permit(principal, action == Action::"get_prompt", resource);'
- 'permit(principal, action == Action::"read_resource", resource);'
entities_json: '[]'

This profile is useful for monitoring or auditing scenarios where clients need access to prompts and data resources without executing any tools.

note

Because tools/list responses are filtered based on call_tool policies, tools won't appear in list responses under this profile. Prompts and resources appear normally because get_prompt and read_resource policies are present.

Safe tools profile

Extend the observe profile to also allow tool calls for tools that MCP servers have annotated as safe. This allows read-only tools and non-destructive closed-world tools, while blocking everything else:

authz-safe-tools.yaml
version: '1.0'
type: cedarv1
cedar:
policies:
# Prompt and resource access
- 'permit(principal, action == Action::"get_prompt", resource);'
- 'permit(principal, action == Action::"read_resource", resource);'
# Read-only tools
- >-
permit(principal, action == Action::"call_tool", resource) when { resource
has readOnlyHint && resource.readOnlyHint == true };
# Non-destructive AND closed-world tools
- >-
permit(principal, action == Action::"call_tool", resource) when { resource
has destructiveHint && resource.destructiveHint == false && resource has
openWorldHint && resource.openWorldHint == false };
entities_json: '[]'
tip

Tools that omit all annotation attributes are denied under this profile, preserving a conservative default-deny posture. Only tools that explicitly declare safe annotations are allowed.

Tool allowlist profile

Allow only specific, named tools. This is the most explicit approach and doesn't depend on MCP servers setting annotations correctly:

authz-allowlist.yaml
version: '1.0'
type: cedarv1
cedar:
policies:
- 'permit(principal, action == Action::"get_prompt", resource);'
- 'permit(principal, action == Action::"read_resource", resource);'
- 'permit(principal, action == Action::"call_tool", resource ==
Tool::"search_code");'
- 'permit(principal, action == Action::"call_tool", resource ==
Tool::"read_file");'
- 'permit(principal, action == Action::"call_tool", resource ==
Tool::"list_repos");'
entities_json: '[]'

RBAC with annotation guardrails

Combine role-based access with annotation checks. Admins get full access, while regular users are restricted to safe tools:

authz-rbac-annotations.yaml
version: '1.0'
type: cedarv1
cedar:
policies:
# Everyone can read prompts and resources
- 'permit(principal, action == Action::"get_prompt", resource);'
- 'permit(principal, action == Action::"read_resource", resource);'
# Admins can call any tool
- >-
permit(principal, action == Action::"call_tool", resource) when {
principal.claim_roles.contains("admin") };
# Non-admins can only call read-only tools
- >-
permit(principal, action == Action::"call_tool", resource) when { resource
has readOnlyHint && resource.readOnlyHint == true };
entities_json: '[]'

Working with JWT claims

JWT claims from your identity provider become available in policies with a claim_ prefix. You can use these claims in two ways:

On the principal entity:

permit(principal, action == Action::"call_tool", resource == Tool::"weather") when {
principal.claim_name == "John Doe"
};

In the context:

permit(principal, action == Action::"call_tool", resource == Tool::"weather") when {
context.claim_name == "John Doe"
};

Both approaches work identically. Choose the one that makes your policies more readable.

Working with tool arguments

Tool arguments become available in policies with an arg_ prefix. This lets you create policies based on the specific parameters of requests:

On the resource entity:

permit(principal, action == Action::"call_tool", resource == Tool::"weather") when {
resource.arg_location == "New York" || resource.arg_location == "London"
};

In the context:

permit(principal, action == Action::"call_tool", resource == Tool::"weather") when {
context.arg_location == "New York" || context.arg_location == "London"
};

This policy allows weather tool calls only for specific locations, demonstrating how you can control access based on request parameters.

List operations and filtering

List operations (tools/list, prompts/list, resources/list) bypass request-level authorization entirely. ToolHive forwards the list request to the MCP server, then automatically filters the response based on what the caller is authorized to access:

  • tools/list shows only tools the user can call (based on call_tool policies)
  • prompts/list shows only prompts the user can get (based on get_prompt policies)
  • resources/list shows only resources the user can read (based on read_resource policies)

You don't need to write explicit policies for list operations. Instead, focus on the underlying access policies, and the lists will be filtered automatically.

For example, if you have this policy:

permit(principal, action == Action::"call_tool", resource == Tool::"weather");

Then tools/list will only show the "weather" tool for that user.

Optimizer meta-tool enforcement

When the optimizer is enabled alongside Cedar authorization, Cedar policies cover the optimizer's find_tool and call_tool meta-tools:

  • tools/list: The meta-tools (find_tool, call_tool) pass through Cedar filtering. Real backend tools are filtered as before.
  • tools/call with call_tool: Cedar extracts the inner tool_name argument and authorizes the actual backend tool before execution. Your existing per-tool policies apply transparently.
  • tools/call with find_tool: The response is filtered through Cedar so clients cannot discover unauthorized tools via search.

You don't need to write separate policies for the meta-tools themselves. Your existing call_tool policies on backend tools are enforced automatically when the optimizer routes calls.

Review policies when enabling Cedar with the optimizer

If you enable Cedar on a deployment that already uses the optimizer, ensure your backend tool policies are comprehensive. Previously unchecked operations are now subject to default-deny authorization. Tools that were accessible without policies before may now be denied.

Upstream identity provider claims

When using the embedded authorization server, Cedar policies can reference claims from the upstream identity provider token (for example, GitHub login or Okta groups). This enables group-based authorization using your organization's existing identity provider groups.

Group-based authorization

The Cedar authorizer extracts group membership from upstream tokens using configurable claim names. By default, it looks for groups, roles, and cognito:groups claims. Groups are mapped to THVGroup parent entities, so you can write policies like:

permit(
principal in THVGroup::"engineering",
action == Action::"call_tool",
resource
);

This permits any user in the "engineering" group to call any tool.

Custom group claim names

If your identity provider uses a non-standard claim name for groups (for example, Auth0 and Okta often use URI-style claims like https://example.com/groups), set the group_claim_name option in your Cedar configuration:

{
"version": "1.0",
"type": "cedarv1",
"cedar": {
"policies": [
"permit(principal in THVGroup::\"engineering\", action, resource);"
],
"entities_json": "[]",
"group_claim_name": "https://example.com/groups"
}
}

When group_claim_name is set, it takes priority over the well-known defaults. When it is empty (the default), ToolHive checks groups, roles, and cognito:groups in order.

How it works

  1. The embedded authorization server authenticates the user with your upstream identity provider and issues a ToolHive JWT.
  2. The Cedar authorizer reads claims from the upstream token (not just the ToolHive-issued JWT).
  3. Group claims are extracted and used to build THVGroup parent entities for the principal.
  4. Policies using principal in THVGroup::"<group>" evaluate correctly.
note

If the upstream token is opaque (not a JWT), the authorizer denies the request. There is no silent fallback to ToolHive-issued claims only.

Policy evaluation and secure defaults

Understanding how Cedar evaluates policies helps you write more effective and secure authorization rules.

Evaluation order

ToolHive's policy evaluation follows a secure-by-default, least-privilege model:

  1. Deny precedence: If any forbid policy matches, the request is denied
  2. Permit evaluation: If any permit policy matches, the request is authorized
  3. Default deny: If no policy matches, the request is denied

This means that forbid policies always override permit policies, and any request not explicitly permitted is denied. This approach minimizes risk and ensures that only authorized actions are allowed.

Designing secure policies

When writing policies, follow these principles:

Start with least privilege: Begin by denying everything, then add specific permissions as needed. This approach is more secure than starting with broad permissions and then trying to restrict them.

Use explicit deny sparingly: While forbid policies can be useful, they can also make your policy set harder to understand. In most cases, the default deny behavior is sufficient.

Guard annotation access with has: Always use resource has <attr> before accessing annotation attributes. Many MCP servers only set some annotations, and unguarded access causes evaluation errors that result in a deny.

Test your policies: Always test policies with real requests to ensure they work as expected. Pay special attention to edge cases and error conditions.

Advanced policy examples

Multi-tenant environments

In multi-tenant environments, you can use custom entity attributes in entities_json to isolate tenants:

permit(principal, action == Action::"call_tool", resource) when {
resource.tenant_id == principal.claim_tenant_id
};

This ensures that clients can only access tools belonging to their tenant. You must define the tenant_id attribute on each tool entity in entities_json for this pattern to work.

Data sensitivity levels

For data with different sensitivity levels:

permit(principal, action == Action::"call_tool", resource == Tool::"data_access") when {
principal.claim_clearance_level >= resource.arg_data_sensitivity
};

This ensures that clients can only access data within their clearance level.

Argument-scoped access

Restrict a tool to specific argument values:

permit(principal, action == Action::"call_tool", resource == Tool::"calculator") when {
resource.arg_operation == "add" || resource.arg_operation == "subtract"
};

This permits calling the calculator tool, but only for the "add" and "subtract" operations.

Entity attributes

Cedar entities can have attributes that can be used in policy conditions. The authorization middleware automatically adds JWT claims and tool arguments as attributes to the principal entity.

You can also define custom entities with attributes in the entities_json field of the configuration file:

{
"version": "1.0",
"type": "cedarv1",
"cedar": {
"policies": [
"permit(principal, action == Action::\"call_tool\", resource) when { resource.owner == principal.claim_sub };"
],
"entities_json": "[
{
\"uid\": \"Tool::weather\",
\"attrs\": {
\"owner\": \"user123\"
}
}
]"
}
}

This configuration defines a custom entity for the weather tool with an owner attribute set to user123. The policy allows clients to call tools only if they own them.

For the complete list of built-in attributes available on each entity type, see the Authorization policy reference.

Next steps

Troubleshooting policies

When policies don't work as expected, follow this systematic approach:

Request is denied unexpectedly

  1. Check policy syntax: Ensure your policies are correctly formatted and use valid Cedar syntax.
  2. Verify entity matching: Confirm that the principal, action, and resource in your policies match the actual values in the request.
  3. Check has guards: If your policy references annotation attributes (readOnlyHint, destructiveHint, idempotentHint, openWorldHint), ensure you're using resource has <attr> before accessing them. A missing attribute causes an evaluation error, which ToolHive treats as a deny.
  4. Test conditions: Check that any conditions in your policies are satisfied by the request context.
  5. Remember default deny: If no policy explicitly permits the request, it will be denied.

JWT claims are not available

  1. Verify JWT middleware: Ensure that JWT authentication is configured correctly and running before authorization.
  2. Check token claims: Verify that the JWT token contains the expected claims.
  3. Use correct prefix: Remember that JWT claims are available with a claim_ prefix.

Tool arguments are not available

  1. Check request format: Ensure that tool arguments are correctly specified in the request.
  2. Use correct prefix: Remember that tool arguments are available with an arg_ prefix.
  3. Verify argument names: Confirm that the argument names in your policies match those in the actual requests.
  4. Check argument types: Complex arguments (objects, arrays) are not available directly. Instead, check for arg_<key>_present == true.

Tool annotations are not available

  1. Check MCP server support: Not all MCP servers set annotation hints on their tools. Check the server's tools/list response to see which annotations are present.
  2. Use has guards: Always check resource has readOnlyHint before accessing resource.readOnlyHint. A missing annotation attribute is not the same as false -- it simply doesn't exist.
  3. Verify annotation source: Annotations come from the MCP server's tools/list response, not from the client's tools/call request. If you don't see annotations, the MCP server may not be setting them.

Groups are not working

  1. Check JWT claims: Verify that your JWT token contains a group claim (groups, roles, or cognito:groups).
  2. Configure custom claim name: If your identity provider uses a non-standard claim name, set group_claim_name in the Cedar configuration.
  3. Use correct syntax: Use principal in THVGroup::"group-name" rather than principal.claim_groups.contains("group-name"). Both evaluate correctly, but the in syntax is the idiomatic Cedar approach for group membership.