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.
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 };
permitorforbid: Whether to allow or deny the operationprincipal: The entity making the request (the client)action: The operation being performedresource: The object being accessedconditions: 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 toolAction::"get_prompt": Get a promptAction::"read_resource": Read a resourceAction::"list_tools": List available tools
Resource
The object being accessed:
- Format:
<type>::<id> - Examples:
Tool::"weather": The weather toolPrompt::"greeting": The greeting promptResource::"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 formattype: The type of authorization configuration (currently onlycedarv1is supported)cedar: The Cedar-specific configurationpolicies: An array of Cedar policy stringsentities_json: A JSON string representing Cedar entitiesgroup_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:
| Attribute | When true | When false |
|---|---|---|
readOnlyHint | Tool only reads data | Tool may modify data |
destructiveHint | Tool may perform irreversible operations | Tool is non-destructive |
idempotentHint | Repeated calls produce the same result | Repeated calls may differ |
openWorldHint | Tool interacts with external systems | Tool 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:
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:
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: '[]'
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:
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:
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/listshows only tools the user can call (based oncall_toolpolicies)prompts/listshows only prompts the user can get (based onget_promptpolicies)resources/listshows only resources the user can read (based onread_resourcepolicies)
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:
- Deny precedence: If any
forbidpolicy matches, the request is denied - Permit evaluation: If any
permitpolicy matches, the request is authorized - 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
- Look up every available entity type, action, and attribute in the Authorization policy reference
- Set up authentication and authorization for CLI-managed MCP servers or Kubernetes-deployed MCP servers
- Follow the end-to-end Role-based authorization with Okta tutorial
Related information
- Authentication and authorization framework -- Conceptual overview of ToolHive's auth architecture
- Cedar documentation -- Official Cedar policy language reference
Troubleshooting policies
When policies don't work as expected, follow this systematic approach:
Request is denied unexpectedly
- Check policy syntax: Ensure your policies are correctly formatted and use valid Cedar syntax.
- Verify entity matching: Confirm that the principal, action, and resource in your policies match the actual values in the request.
- Check
hasguards: If your policy references annotation attributes (readOnlyHint,destructiveHint,idempotentHint,openWorldHint), ensure you're usingresource has <attr>before accessing them. A missing attribute causes an evaluation error, which ToolHive treats as a deny. - Test conditions: Check that any conditions in your policies are satisfied by the request context.
- Remember default deny: If no policy explicitly permits the request, it will be denied.
JWT claims are not available
- Verify JWT middleware: Ensure that JWT authentication is configured correctly and running before authorization.
- Check token claims: Verify that the JWT token contains the expected claims.
- Use correct prefix: Remember that JWT claims are available with a
claim_prefix.
Tool arguments are not available
- Check request format: Ensure that tool arguments are correctly specified in the request.
- Use correct prefix: Remember that tool arguments are available with an
arg_prefix. - Verify argument names: Confirm that the argument names in your policies match those in the actual requests.
- Check argument types: Complex arguments (objects, arrays) are not
available directly. Instead, check for
arg_<key>_present == true.
Tool annotations are not available
- Check MCP server support: Not all MCP servers set annotation hints on
their tools. Check the server's
tools/listresponse to see which annotations are present. - Use
hasguards: Always checkresource has readOnlyHintbefore accessingresource.readOnlyHint. A missing annotation attribute is not the same asfalse-- it simply doesn't exist. - Verify annotation source: Annotations come from the MCP server's
tools/listresponse, not from the client'stools/callrequest. If you don't see annotations, the MCP server may not be setting them.
Groups are not working
- Check JWT claims: Verify that your JWT token contains a group claim
(
groups,roles, orcognito:groups). - Configure custom claim name: If your identity provider uses a
non-standard claim name, set
group_claim_namein the Cedar configuration. - Use correct syntax: Use
principal in THVGroup::"group-name"rather thanprincipal.claim_groups.contains("group-name"). Both evaluate correctly, but theinsyntax is the idiomatic Cedar approach for group membership.