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
    • Action::"list_tools": List available tools

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.

The four annotation attributes are:

AttributeWhen trueWhen false
readOnlyHintTool only reads dataTool may modify data
destructiveHintTool may perform irreversible operationsTool is non-destructive
idempotentHintRepeated calls produce the same resultRepeated calls may differ
openWorldHintTool interacts with external systemsTool operates in a closed environment

Using the has operator

Not all MCP servers set all annotation fields. If an annotation is absent, the attribute does not exist on the resource entity. Accessing a missing attribute causes a Cedar evaluation error, which ToolHive treats as a deny.

Always use Cedar's has operator to check for an annotation before accessing it:

// Safe: guards against missing attributes
permit(
principal,
action == Action::"call_tool",
resource
) when {
resource has readOnlyHint && resource.readOnlyHint == true
};

Without the has guard, a tool that sets readOnlyHint: true but omits destructiveHint would be incorrectly denied by a policy that checks resource.destructiveHint == false without guarding.

Annotation policy examples

Allow only read-only tools

permit(
principal,
action == Action::"call_tool",
resource
) when {
resource has readOnlyHint && resource.readOnlyHint == true
};

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 listing and reading MCP capabilities, but block all tool calls:

authz-observe.yaml
version: '1.0'
type: cedarv1
cedar:
policies:
- 'permit(principal, action == Action::"list_tools", resource);'
- 'permit(principal, action == Action::"list_prompts", resource);'
- 'permit(principal, action == Action::"list_resources", resource);'
- '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 you want clients to see what's available without executing any tools.

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:
# List and read operations
- 'permit(principal, action == Action::"list_tools", resource);'
- 'permit(principal, action == Action::"list_prompts", resource);'
- 'permit(principal, action == Action::"list_resources", resource);'
- '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::"list_tools", resource);'
- 'permit(principal, action == Action::"list_prompts", resource);'
- 'permit(principal, action == Action::"list_resources", resource);'
- '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 list and read
- 'permit(principal, action == Action::"list_tools", resource);'
- 'permit(principal, action == Action::"list_prompts", resource);'
- 'permit(principal, action == Action::"list_resources", resource);'
- '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) work differently from other operations. They're always allowed, but the response is automatically filtered based on what the user can actually 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.

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.