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 resource
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.
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:
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.
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:
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: '[]'
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::"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 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/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.
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/callwithcall_tool: Cedar extracts the innertool_nameargument and authorizes the actual backend tool before execution. Your existing per-tool policies apply transparently.tools/callwithfind_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.
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
- The embedded authorization server authenticates the user with your upstream identity provider and issues a ToolHive JWT.
- The Cedar authorizer reads claims from the upstream token (not just the ToolHive-issued JWT).
- Group claims are extracted and used to build
THVGroupparent entities for the principal. - Policies using
principal in THVGroup::"<group>"evaluate correctly.
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:
- 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.