npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

cedar-mcp-server

v1.0.0

Published

MCP server for Cedar policy language — validate, authorize, format, and translate Cedar policies directly in your AI assistant

Downloads

162

Readme

cedar-mcp-server

cedar-mcp-server is an MCP server that puts Cedar policy tooling directly inside your AI assistant conversation. It covers the full Cedar policy lifecycle: validate policies, simulate authorization decisions, plan changes against AVP constraints, and diff two policy stores for blue/green deployment. Cedar 4.11.0 runs in-process via WASM, so there's nothing to install beyond npx.

CI npm version License TypeScript Node


What it does

Seventeen tools across six categories, plus three MCP prompts.

Authorization

| Tool | What it does | |------|-------------| | cedar_authorize | Evaluates one authorization request locally; returns the decision and which policies fired | | cedar_authorize_batch | Runs N authorization requests through one policy set and returns the decision matrix; for regression testing after a policy edit |

Validation

| Tool | What it does | |------|-------------| | cedar_validate | Validates Cedar policies against a schema; returns errors with hints and source locations | | cedar_validate_schema | Validates a Cedar schema in isolation (no policies required); returns parse errors and namespace/type counts | | cedar_validate_template | Validates a Cedar template against a schema; detects slot placeholders | | cedar_validate_entities | Validates a Cedar entities JSON array against a schema; classifies errors by kind (unknown_type, missing_required_attribute, type_mismatch, unknown_attribute, disallowed_parent_type) |

Formatting and translation

| Tool | What it does | |------|-------------| | cedar_format | Formats Cedar policy text to canonical style | | cedar_translate | Translates between Cedar text and Cedar JSON formats for policies and schemas |

Planning and analysis

| Tool | What it does | |------|-------------| | cedar_explain | Explains a Cedar policy in plain English with pattern detection | | cedar_check_policy_change | Determines whether a policy modification can be applied in-place in AVP or requires delete-and-recreate | | cedar_generate_sample_request | Generates a complete authorization request payload that produces a target decision | | cedar_advise | Returns a structured context bundle (schema summary, policy inventory with pattern classification, gotchas, AVP rules, sequencing guidance) for any policy-change intent so the calling assistant can plan correctly |

Templates

Instantiate and inspect template-linked policies. Template validation lives in the Validation category above.

| Tool | What it does | |------|-------------| | cedar_link_template | Instantiates a template by binding ?principal and ?resource slots to specific entity references | | cedar_list_templates | Lists all templates in a policy store (reads from templates/ subdirectory) | | cedar_list_template_links | Lists all template-linked policy instances in a store (reads from template-links/ subdirectory) |

Diffing

| Tool | What it does | |------|-------------| | cedar_diff_schema | Structural diff of two schemas with AVP-aware risk classification per change (safe/review/breaking) | | cedar_diff_policy_stores | Structural and optional behavioral diff between two policy stores with AVP immutability classification (embeds structured schema_diff from cedar_diff_schema) |


Why use cedar-mcp-server instead of reading my Cedar files directly?

A fair question, especially in an MCP client (Claude Code, Cursor) where the assistant can already Read the policy files in your workspace. If an LLM can read policies/admin.cedar and schema.cedarschema, why route through tool calls at all?

Because the tools encode things that do not live in the files.

Validation

cedar_validate and cedar_validate_schema run the official Cedar 4.11.0 parser. Reading a policy tells you what the author wrote, not whether the parser accepts it. Syntax errors, schema-type mismatches, missing-attribute references, optional-attribute access without a guard, and appliesTo violations all surface from the parser, not from text inspection. The parser is the only authority on validity.

Evaluation

cedar_authorize and cedar_authorize_batch run the Cedar engine. Reading a policy set tells you the rules; running them tells you the decisions. The default-deny behavior, forbid-overrides-permit precedence, optional-attribute silent-skip, action-group membership resolution, schema-validated entity typing, and condition short-circuiting are engine semantics. You cannot simulate the engine accurately by mental execution over the policy text, especially when the entity graph has parents or shared attributes.

Planning

cedar_advise returns a structured context bundle for any "I want to change my policies to do X" intent: schema summary, policy inventory with AST-classified Cedar pattern per file (Membership / Relationship / Discretionary / hybrid), intent-selected gotcha catalog (10 entries drawn from Cedar/AVP failure modes), AVP UpdatePolicy mutability rules, Cedar patterns reference, sequencing guidance, and explicit follow-up instructions. None of this lives in the policy files. AST-based pattern classification requires parsing each policy and walking the JSON; the AVP API contract requires knowing the UpdatePolicy spec; the gotcha catalog requires Cedar/AVP experience. The bundle is deterministic (no LLM round-trip on the server side); the calling assistant produces the actual plan from it and then verifies snippets via cedar_validate and cedar_check_policy_change.

Change safety

cedar_check_policy_change, cedar_diff_schema, and cedar_diff_policy_stores encode the AVP UpdatePolicy contract and the rules of which schema changes break which policies. Visually diffing two policies in a code review tells you what text changed; only these tools tell you whether AVP will accept the update in place, whether the change will silently drop existing policy matches, or whether decisions will flip for canonical requests. A text diff over schemas does not tell you which attribute removals break which policy reads.

Analysis

cedar_explain returns structure derived from the parsed AST: effect, scope breakdown, conditions, and detected patterns. Reading the policy gives the assistant a paraphrase; the AST gives ground truth (correct slot detection in templates, optional-attribute guard recognition, path-matching pattern detection, name-based identity recognition). For inherited policies that mix RBAC scope with ABAC conditions, the AST-derived breakdown is materially more reliable than text inspection.

Discoverability and sequencing

cedar_list_templates, cedar_list_template_links, the cedar://policies/{store} and cedar://entities/{store} resource URIs, and the StoreManager-backed policy_ref / schema_ref / entities_ref parameters give the assistant a stable, schema-aware view of the project. Doing this through ls and cat works for one-off inspection, but the tools encode the store layout convention (policies/, entities/, templates/, template-links/, schema.cedarschema or schema.json) and surface only what the other tools will actually resolve.

Rule of thumb for assistants: if a question is "what does this policy look like?", Read is fine. If it is "is this valid?", "would it allow X?", "can I update it in place?", "what changes if I deploy this?", "what's the safest way to add Y?", or "what patterns does this store use?", the right answer is a tool call. The tools are load-bearing for correctness, not a convenience layer.


Quick start

How MCP stdio servers work

Your MCP client (Claude Code, Claude Desktop, Cursor) spawns cedar-mcp-server as a child process when it needs Cedar tooling. You do not run the server directly. You configure your client to point at it once, and the client manages the process lifecycle over stdio for each session. If you try node dist/index.js in a terminal it will appear to hang; that is the server waiting for JSON-RPC messages on stdin. Stop it with Ctrl+C.

Run from source (current path)

git clone https://github.com/Pigius/cedar-mcp-server.git
cd cedar-mcp-server
npm install
npm run build      # compiles TypeScript to dist/

Then point your MCP client at the built entry. Replace command: "npx" and args: ["-y", "cedar-mcp-server"] in the configs below with:

{ "command": "node", "args": ["/absolute/path/to/cedar-mcp-server/dist/index.js"] }

Or run directly via tsx without a build step:

{ "command": "npx", "args": ["tsx", "/absolute/path/to/cedar-mcp-server/src/index.ts"] }

Claude Code (after publish)

Add to .claude/settings.json in your project, or to ~/.claude/settings.json globally:

{
  "mcpServers": {
    "cedar": {
      "command": "npx",
      "args": ["-y", "cedar-mcp-server"]
    }
  }
}

If you register via claude mcp add instead of editing settings.json by hand, run the command from the directory you will actually use the server in. Claude Code stores MCP configurations per-project by default, so a registration done from one project does not surface in another. For a single global registration that works across every project, add --scope user:

claude mcp add --scope user cedar -- npx -y cedar-mcp-server

Claude Desktop (after publish)

Add to claude_desktop_config.json:

{
  "mcpServers": {
    "cedar": {
      "command": "npx",
      "args": ["-y", "cedar-mcp-server"]
    }
  }
}

Cursor (after publish)

Add to .cursor/mcp.json in your project:

{
  "mcpServers": {
    "cedar": {
      "command": "npx",
      "args": ["-y", "cedar-mcp-server"]
    }
  }
}

Once published, first npx run pulls the package; subsequent runs use the npm cache.

Then in your client conversation:

Validate this Cedar policy against this schema: [paste policy and schema]

The workflow it enables

This server is designed around three steps in any Cedar policy lifecycle.

1. PLAN: from intent to a step-by-step Cedar policy change plan

Use cedar_advise to translate a natural-language description of what you want into a concrete, ordered list of Cedar and schema changes, with AVP deployment classification for each step.

You: I need to add a "contractor" role that can read documents
     but only if the document is marked as "external_share".
     Here's my current policy store: [attach via cedar:// ref or paste]

AI:  [calls cedar_advise]

     Step 1: Schema (no changes needed; "classification" attribute already exists).
     Step 2: Policy (new_policy_via_create_policy):
       permit (
         principal in MyApp::Role::"contractor",
         action == MyApp::Action::"read",
         resource
       )
       when { resource.classification == "external_share" };
     Gotcha (high): accessing resource.classification without a guard works here
     because the attribute is required in the schema. If you make it optional
     later, add `resource has classification &&` before the check.

2. DIFF: from "what changed" to "is it safe to deploy"

Configure two policy stores as MCP roots (production and staging), then use cedar_diff_policy_stores to get a structural diff plus AVP immutability classification for each change.

You: Compare staging and production policies before I promote.

AI:  [calls cedar_diff_policy_stores with blue: "production", green: "staging"]

     Added: contractor-read-external.cedar (new_policy_via_create_policy, safe to add)
     Modified: editor-policy.cedar (principal clause changed, requires_delete_recreate)
     Schema: unchanged

     Behavioral diff (optional): pass a list of authorization requests to see
     which decisions would change between the two stores.

3. APPLY: deploy with confidence

The server doesn't call AVP APIs directly. Apply your changes through your own deployment pipeline. The avp_update_mode classification from steps 1 and 2 tells you exactly which AVP operations each change requires:

  • new_policy_via_create_policy: safe to add with CreatePolicy.
  • in_place_via_update_policy: change action or conditions with UpdatePolicy.
  • requires_delete_recreate: principal, resource, or effect changed; use DeletePolicy then CreatePolicy.

See avp-cli for a companion CLI that handles the pull/push side.


Tool details

Every example below assumes a Cedar workspace store called cedar-sandbox: a directory with schema.cedarschema declaring a MyApp namespace (User, Role, Document, Folder entities; read / write / delete actions), per-file policies in policies/ (admin.cedar, editor.cedar, viewer.cedar), and an entities/sample.json containing alice (admin), bob (editor), charlie (viewer) plus a doc-public document. Substitute your own store name in any example prompt that mentions cedar-sandbox. For complete worked fixtures, see examples/.

cedar_validate

Validates Cedar policies with or without a schema. Two modes:

  • Syntax-only (no schema): runs the parser alone. Catches typos, malformed scopes, bad operators. Cheapest sanity check, useful when you have a 5-line snippet and no schema yet.
  • Syntax-and-schema (schema provided): parses then type-checks against the schema. Catches attribute typos, entity type mismatches, action applicability errors, and UnsafeOptionalAttributeAccess warnings.

The response's validation_mode field tells you which mode ran.

Inputs:

| Parameter | Required | Description | |-----------|----------|-------------| | policies | yes (or policy_ref) | Cedar policy text (one or more policies) | | schema | no | Cedar schema (JSON object or .cedarschema text). Omit for syntax-only mode or to auto-discover from the workspace store. | | policy_ref | no | cedar:// URI pointing to a policy in a configured store | | schema_ref | no | cedar:// URI pointing to a schema in a configured store | | store | no | Store name (a configured MCP root). Use to disambiguate auto-discovery when multiple stores are loaded. | | validation_mode | no | One of "auto" (default), "syntax_only", or "syntax_and_schema". See "Forcing a mode" below. |

Workspace auto-discovery. When schema and schema_ref are both omitted and exactly one MCP root is loaded, the tool reads the schema from that store and upgrades the run to syntax_and_schema mode. The response's auto_discovered.schema_from field names the source store. With multiple stores loaded, the response is an actionable error listing the candidate names. Pass store: "<name>" to choose one.

Forcing a mode. Set validation_mode when the default schema-presence heuristic isn't what you want.

  • "auto" (default): schema presence picks the mode, as described above.
  • "syntax_only": parser-only. Skips workspace auto-discovery entirely and ignores any inline schema, schema_ref, or store you pass alongside it (the user said parser-only; the tool honors that literally). Use when the user explicitly says they have no schema, or for a fast parse-only sanity check inside a Cedar-workspace cwd.
  • "syntax_and_schema": require a schema. If neither an inline schema nor a workspace schema is resolvable, the response is an error rather than a silent drop to syntax-only. Use when you want to be sure the type-check ran.

Example prompt (paste into Claude Code from a Cedar workspace):

Use cedar_validate on: permit (prinicpal in MyApp::Role::"admin", action, resource);

Response:

{
  "valid": false,
  "errors": [
    {
      "policy_id": "",
      "message": "failed to parse policies from string: found an invalid variable in the policy scope: prinicpal",
      "hint": "Did you mean 'principal'?",
      "line": 1,
      "column": 9
    }
  ],
  "warnings": [],
  "policy_count": 0,
  "validation_mode": "syntax_and_schema",
  "auto_discovered": { "schema_from": "cedar-sandbox" }
}

The typo prinicpal lands on the schema-aware path because the cwd-fallback auto-discovered the sandbox schema, so validation_mode is syntax_and_schema. With no workspace store loaded (or validation_mode: "syntax_only" set explicitly), the same prompt produces the same parse error but validation_mode: "syntax_only" and no auto_discovered field.

Additional shapes you'll see:

// Schema-validation error: attribute not declared on the entity type
{
  "valid": false,
  "errors": [
    {
      "policy_id": "policy0",
      "message": "attribute `nonexistent` on entity type `MyApp::Document` not found",
      "hint": "did you mean `classification`?",
      "line": 1,
      "column": 47
    }
  ],
  "policy_count": 1,
  "validation_mode": "syntax_and_schema"
}
// Parse error on a multi-line policy with `int` instead of `in` on line 3
{
  "valid": false,
  "errors": [
    {
      "policy_id": "",
      "message": "failed to parse policies from string: unexpected token `int`",
      "hint": "Did you mean 'in'?",
      "line": 3,
      "column": 10
    }
  ],
  "policy_count": 0,
  "validation_mode": "syntax_and_schema"
}

Each error includes line and column (1-indexed) derived from the WASM parser's source location when available. The hint field is populated either from Cedar's own diagnostic help text or from a small built-in typo table for common misspellings (int for in, permint for permit, prinicpal / prinipal for principal, wen for when, unles for unless, etc.). When neither applies, hint is null.

When to use: every time you write or modify a policy. Syntax-only mode is the fast first pass when iterating on a snippet. Full schema validation catches attribute typos, entity type mismatches, and action applicability errors before they become silent runtime surprises.


cedar_authorize

Evaluates an authorization request locally against your policies and entities. Returns the decision and which policies fired.

Inputs:

| Parameter | Required | Description | |-----------|----------|-------------| | policies | yes (or policy_ref, or auto-discovered) | Cedar policy text | | principal | yes | Entity reference, e.g. MyApp::User::"alice" | | action | yes | Entity reference, e.g. MyApp::Action::"read" | | resource | yes | Entity reference, e.g. MyApp::Document::"doc-public" | | entities | yes (or entities_ref, or auto-discovered) | JSON array of entity objects (uid, attrs, parents) | | schema | no (or schema_ref, or auto-discovered) | Cedar schema; enables request validation | | context | no | JSON object with context attributes | | policy_ref | no | cedar:// URI for policy store reference | | schema_ref | no | cedar:// URI for schema reference | | entities_ref | no | cedar:// URI for an entities file reference | | store | no | Store name (a configured MCP root). Use to disambiguate auto-discovery when multiple stores are loaded. |

Workspace auto-discovery. When any of policies / schema / entities (and their _ref siblings) are omitted and exactly one MCP root is loaded, the tool reads each missing input from that store: policies/*.cedar files (per-policy basenames preserved as determining-policy IDs), schema.cedarschema or schema.json, and the entities under entities/*.json merged into one array. The response's auto_discovered field reports which store satisfied each missing input. With multiple stores loaded, the response is an actionable error listing the candidate names. Pass store: "<name>" to choose one.

Example prompt (paste into Claude Code from a Cedar workspace):

Can alice read doc-public?

The assistant translates this to a cedar_authorize call with principal: MyApp::User::"alice", action: MyApp::Action::"read", resource: MyApp::Document::"doc-public". Policies, schema, and entities all auto-discover from the sandbox.

Response:

{
  "decision": "Allow",
  "determining_policies": ["admin"],
  "errors": [],
  "decision_reason": "permit_policy_fired",
  "format_detected": "cedar",
  "format_note": "Input is in Cedar/WASM format.",
  "auto_discovered": {
    "policies_from": "cedar-sandbox",
    "schema_from": "cedar-sandbox",
    "entities_from": "cedar-sandbox"
  }
}

determining_policies: ["admin"] is the file basename (policies/admin.cedaradmin), not a positional placeholder, because cedar_authorize uses the H1 stable-ID resolution ladder described below.

Additional shapes:

// Deny via default-deny (no policy matched)
{
  "decision": "Deny",
  "determining_policies": [],
  "errors": [],
  "decision_reason": "default_deny_no_permit_matched"
}
// Deny via a forbid policy
{
  "decision": "Deny",
  "determining_policies": ["editor_readonly"],
  "errors": [],
  "decision_reason": "forbid_policy_fired"
}

determining_policies lists the stable policy IDs that contributed to the decision. The ID resolution order is: the policy's @id("name") annotation if present, then the source file basename when policies are loaded via policy_ref (e.g. admin.cedar becomes admin), then a positional fallback policy0, policy1, etc. for unannotated inline text. On a deny caused by a forbid policy, that policy's ID appears here. An empty list means default deny: no permit matched.

decision_reason is an explicit machine-readable classification of the outcome. It takes one of four values:

  • permit_policy_fired when decision: "Allow" and at least one permit policy is determining.
  • forbid_policy_fired when decision: "Deny" and at least one forbid policy is determining.
  • default_deny_no_permit_matched when decision: "Deny", no policy fired, and no evaluation errors occurred. This is the Cedar default-deny path.
  • evaluation_error when at least one policy errored during evaluation (for example, a policy reads an attribute the entity lacks). Pair with the errors array for details.

When to use: verifying authorization logic after writing a policy, and checking that your entity payloads produce the expected decisions before deploying.


cedar_authorize_batch

Runs N authorization requests through ONE policy set and returns the decision matrix. Use case: regression testing after a policy edit (run a canonical request suite against the new policy set and confirm decisions haven't drifted).

Inputs:

| Parameter | Required | Description | |-----------|----------|-------------| | policies | one of | Inline Cedar policy text | | policy_ref | one of | cedar:// URI to load policies from a configured store | | schema | no | Cedar schema (JSON or .cedarschema); when supplied, schema-violating requests resolve to decision: "Error" rather than silent Allow/Deny | | schema_ref | no | cedar:// URI to load schema | | requests | yes | JSON array of authorization request objects: {principal, action, resource, entities, context?} | | entities | no | Shared entities JSON applied when individual requests omit their own entities field | | entities_ref | no | cedar:// URI to load shared entities |

Example prompt (paste into Claude Code from a Cedar workspace):

Run a batch of three authorizations against the sandbox: alice/read/doc-public, bob/write/doc-public, charlie/delete/doc-public.

The assistant translates this to a cedar_authorize_batch call referencing cedar://policies/cedar-sandbox, cedar://schema/cedar-sandbox, and cedar://entities/cedar-sandbox, with the three requests inline.

Response:

{
  "total": 3,
  "allowed": 2,
  "denied": 1,
  "errored": 0,
  "decisions": [
    {
      "index": 0,
      "principal": "MyApp::User::\"alice\"",
      "action": "MyApp::Action::\"read\"",
      "resource": "MyApp::Document::\"doc-public\"",
      "decision": "Allow",
      "determining_policies": ["admin"]
    },
    {
      "index": 1,
      "principal": "MyApp::User::\"bob\"",
      "action": "MyApp::Action::\"write\"",
      "resource": "MyApp::Document::\"doc-public\"",
      "decision": "Allow",
      "determining_policies": ["editor"]
    },
    {
      "index": 2,
      "principal": "MyApp::User::\"charlie\"",
      "action": "MyApp::Action::\"delete\"",
      "resource": "MyApp::Document::\"doc-public\"",
      "decision": "Deny",
      "determining_policies": []
    }
  ],
  "summary": "3 requests: 2 Allow, 1 Deny, 0 Error"
}

Per-request errors carry an error field describing what went wrong (malformed entities, schema violation, etc.) without aborting the rest of the batch.

determining_policies returns file basenames (admin, editor) when policies are loaded from a cedar://policies/{store} ref, matching the H1 stable-ID resolution that single-request cedar_authorize uses. Inline policies passed as a flat string still surface as policy0 / policy1 (positional fallback) because the caller did not supply basenames.

When to use: regression testing a canonical request suite after any policy edit. Pair with cedar_diff_policy_stores's behavioral_test_requests when you also want a side-by-side comparison against the previous policy set.


cedar_format

Formats Cedar policy text to canonical style. Useful before committing policy files or pasting into pull requests.

Inputs:

| Parameter | Required | Description | |-----------|----------|-------------| | policies | yes | Cedar policy text to format | | line_width | no | Maximum line width (default: 80) | | indent_width | no | Indent width in spaces (default: 2) |

Example prompt (paste into Claude Code):

Format this Cedar: permit(principal in MyApp::Role::"admin",action,resource);

Response:

{
  "formatted": "permit (\n  principal in MyApp::Role::\"admin\",\n  action,\n  resource\n);\n",
  "error": null
}

The formatted field is the canonical-style Cedar text. The same input applied to the formatter is idempotent (already-canonical text returns unchanged).

When to use: before committing policy files. Canonical formatting makes diffs readable and policy reviews easier.


cedar_translate

Translates between Cedar text and JSON formats for policies and schemas.

Inputs:

| Parameter | Required | Description | |-----------|----------|-------------| | input | yes | Cedar text or JSON string to translate | | type | yes | "policy" or "schema" | | direction | yes | "to_json" or "to_cedar" |

Example prompt (paste into Claude Code):

Translate to JSON: permit (principal, action, resource);

Response:

{
  "output": "{\n  \"effect\": \"permit\",\n  \"principal\": {\n    \"op\": \"All\"\n  },\n  \"action\": {\n    \"op\": \"All\"\n  },\n  \"resource\": {\n    \"op\": \"All\"\n  },\n  \"conditions\": []\n}",
  "error": null
}

The output field is the Cedar JSON-AST representation of the policy. Round-trip back via direction: "to_cedar" to recover the source.

When to use: programmatic policy inspection, generating policies from code, or feeding policy structure into tools that work with JSON.


cedar_explain

Explains a Cedar policy in plain English with pattern detection.

Inputs:

| Parameter | Required | Description | |-----------|----------|-------------| | policy | yes | A single Cedar policy (not a policy set) | | schema | no | Cedar schema; improves entity type descriptions | | schema_ref | no | cedar:// URI for schema reference | | store | no | Store name (a configured MCP root). Use to disambiguate auto-discovery when multiple stores are loaded. |

Workspace auto-discovery. When schema and schema_ref are both omitted and exactly one MCP root is loaded, the tool reads the schema from that store. The response's auto_discovered.schema_from field names the source store. The schema is optional for explain, so a single store with no schema file still produces a result. With multiple stores loaded and no store parameter, the response is an actionable error listing the candidate names.

Example prompt (paste into Claude Code from a Cedar workspace):

Explain this policy: permit (principal in MyApp::Role::"admin", action, resource);

Response:

{
  "effect": "permit",
  "principal": {
    "scope": "in",
    "description": "principal in MyApp::Role::\"admin\""
  },
  "action": {
    "scope": "All",
    "description": "any action"
  },
  "resource": {
    "scope": "All",
    "description": "any resource"
  },
  "conditions": [],
  "summary": "PERMITS principal in MyApp::Role::\"admin\" to perform any action on any resource.",
  "patterns_detected": [
    "role_based_access",
    "unrestricted_action",
    "unrestricted_resource"
  ],
  "auto_discovered": {
    "schema_from": "cedar-sandbox"
  }
}

The patterns_detected array names recognizable shapes the tool spotted in the AST: role_based_access for membership in a Role, unrestricted_action / unrestricted_resource for All-scope clauses, attribute_condition when when/unless reads attributes, forbid_policy for forbid effects, etc.

When to use: onboarding teammates onto an existing policy set, auditing policies you didn't write, or explaining the intent behind a complex when/unless combination.


cedar_check_policy_change

Determines whether a policy modification can be applied in-place in Amazon Verified Permissions or requires delete-and-recreate.

Inputs:

| Parameter | Required | Description | |-----------|----------|-------------| | old_policy | yes | Existing Cedar policy text | | new_policy | yes | Updated Cedar policy text |

Example prompt (paste into Claude Code):

Check this change: editor.cedar narrows from `action in [read, write]` to `action == read`.

The assistant translates this to a cedar_check_policy_change call with old_policy = the read+write permit, new_policy = the read-only permit (both in the MyApp namespace).

Response:

{
  "can_update_in_place": true,
  "changes": [
    {
      "field": "action",
      "old_value": "{\"op\":\"in\",\"entities\":[{\"type\":\"MyApp::Action\",\"id\":\"read\"},{\"type\":\"MyApp::Action\",\"id\":\"write\"}]}",
      "new_value": "{\"op\":\"==\",\"entity\":{\"type\":\"MyApp::Action\",\"id\":\"read\"}}",
      "in_place_allowed": true,
      "reason": "Action clause changes can be applied in-place."
    }
  ],
  "recommendation": "All changes can be applied as an in-place policy update."
}

An example where the diff requires delete-and-recreate instead:

// old_policy changed the principal head clause (e.g. viewer → senior_viewer)
{
  "can_update_in_place": false,
  "changes": [
    {
      "field": "principal",
      "in_place_allowed": false,
      "reason": "Changing the principal clause requires deleting and recreating the policy."
    }
  ],
  "recommendation": "This policy requires delete-and-recreate. Use DeletePolicy then CreatePolicy."
}

AVP immutability rules:

| Field | In-place via UpdatePolicy? | |-------|--------------------------| | effect | No (delete and recreate) | | principal | No (delete and recreate) | | resource | No (delete and recreate) | | action | Yes | | conditions (when/unless) | Yes |

When to use: before deploying any policy change to AVP. Knowing upfront whether a change requires delete-and-recreate prevents surprises in production.


cedar_generate_sample_request

Generates a complete authorization request payload that produces a target decision against a given policy.

Inputs:

| Parameter | Required | Description | |-----------|----------|-------------| | policy | yes | A single Cedar policy | | schema | yes | Cedar schema for the namespace | | target_decision | yes | "allow" or "deny" |

Example prompt (paste into Claude Code):

Generate a sample request that would be allowed by: permit (principal in MyApp::Role::"admin", action, resource);

The assistant translates this to a cedar_generate_sample_request call with the policy text and the cedar-sandbox schema.

Response:

{
  "principal": "MyApp::User::\"sample-principal\"",
  "action": "MyApp::Action::\"delete\"",
  "resource": "MyApp::Document::\"sample-resource\"",
  "entities": [
    {
      "uid": { "type": "MyApp::User", "id": "sample-principal" },
      "attrs": { "email": "", "name": "" },
      "parents": [{ "type": "MyApp::Role", "id": "admin" }]
    },
    {
      "uid": { "type": "MyApp::Document", "id": "sample-resource" },
      "attrs": { "classification": "", "owner": "" },
      "parents": []
    },
    {
      "uid": { "type": "MyApp::Role", "id": "admin" },
      "attrs": {},
      "parents": []
    }
  ],
  "explanation": "This request will be ALLOW as expected.",
  "decision": "Allow",
  "ready_to_test": true
}

The generator pre-fills required attributes from the schema (name, email on User; classification, owner on Document) with neutral defaults so validateRequest: true accepts the payload. The verification step runs the generated request through cedar_authorize with the same schema; ready_to_test: true means a follow-up call with these exact inputs reproduces the documented decision. If the verification fails (most commonly when the policy pins an entity type the schema does not declare), the response carries ready_to_test: false and a non-null error instead.

When to use: generating test payloads without hand-crafting entities, or verifying that a policy produces the decisions you expect before deploying it. Pass the output directly to cedar_authorize to verify.


cedar_advise

Returns a deterministic, structured context bundle for any "I want to change my Cedar policies to do X" intent. The bundle encodes the Cedar / AVP knowledge that does not live in the policy files: AVP UpdatePolicy mutability rules, AVP validation error categories, the 10-entry gotcha catalog (with the subset selected by the user's intent keywords), the Cedar patterns reference, AST-based pattern classification of every policy in the store, and explicit sequencing + follow-up guidance.

The tool itself does not produce the plan. The calling assistant produces the plan from the bundle, then verifies each Cedar snippet with cedar_validate and each modification with cedar_check_policy_change. No MCP sampling, no client LLM round-trip on the server side.

This pivot replaced the original sampling-based cedar_advise after dogfooding revealed two problems: (1) Claude Code does not advertise the MCP sampling capability, so the prior tool returned -32601 Method not found; (2) when the assistant fell back to drafting from file Read alone, it bypassed the server entirely and lost the AVP/gotcha grounding the server is supposed to provide. The bundle design defeats both.

Inputs:

| Parameter | Required | Description | |-----------|----------|-------------| | intent | yes | Natural-language description of the desired authorization behavior, kept verbatim from the user | | store_ref | no | Store name or cedar:// URI (e.g. cedar://policies/production or production); when supplied, the bundle includes schema_summary, policy_inventory with full policy text, and patterns_detected_in_store counts grounded in the actual store. Omit when exactly one store is loaded and the bundle will auto-resolve to it (the response's auto_discovered.store_from field reports "single_loaded_store"). With multiple stores loaded and no store_ref, the response sets store_status: "ambiguous" and lists the candidate names under available_stores. |

Example prompt (paste into Claude Code from a Cedar workspace):

I want to make editors read-only, admins exempt. Plan it.

The assistant translates this to a cedar_advise call with intent: "Make editors read-only, admins exempt." and no store_ref (cedar-sandbox is the only loaded store, so it auto-resolves).

Response (abridged):

{
  "tool": "cedar_advise",
  "bundle_version": "v2",
  "intent": "Make editors read-only, admins exempt.",
  "store_name": "cedar-sandbox",
  "store_status": "loaded",
  "auto_discovered": { "store_from": "single_loaded_store" },
  "schema_summary": {
    "valid": true,
    "format": "cedarschema",
    "namespaces": ["MyApp"],
    "entity_type_count": 4,
    "action_count": 3,
    "raw_text": "namespace MyApp { ... }"
  },
  "policy_inventory": [
    {
      "policy_id": "admin",
      "pattern": "membership",
      "pattern_confidence": "high",
      "summary": "admin (permit, principal scope uses 'in' — group/role membership (RBAC))",
      "policy_text": "permit (principal in MyApp::Role::\"admin\", action, resource);"
    },
    { "policy_id": "editor", "pattern": "membership", "...": "..." },
    { "policy_id": "viewer", "pattern": "membership", "...": "..." }
  ],
  "patterns_detected_in_store": [{ "pattern": "membership", "count": 3 }],
  "applicable_gotchas": [
    {
      "id": "array_containment_syntax",
      "severity": "medium",
      "description": "Cedar array containment is left-first: `[\"a\", \"b\"].contains(attr)` — the ARRAY is on the left. Writing `attr in [\"a\", \"b\"]` is an entity-hierarchy check, not a value containment check."
    }
  ],
  "avp_update_policy_rules": {
    "summary": "...",
    "in_place_via_update_policy": ["Action scope", "When/unless conditions", "Policy name"],
    "requires_delete_recreate": ["Effect", "Principal scope", "Resource scope", "Static ↔ template-linked conversion"],
    "new_via_create_policy": ["Wholly new policy"],
    "notes": ["UpdatePolicy only updates STATIC policies..."]
  },
  "avp_validation_error_catalog": [
    { "id": "UnsafeOptionalAttributeAccess", "description": "..." }
  ],
  "cedar_patterns_reference": {
    "summary": "...",
    "patterns": [
      { "name": "Membership (RBAC)", "description": "...", "example": "..." }
    ]
  },
  "sequencing_guidance": [
    "Schema changes that add new entity types, attributes, or actions MUST be deployed BEFORE policies that reference them..."
  ],
  "next_steps_for_llm": "Use this context to produce a Cedar policy change plan. Do not skip these steps: 1. Identify the entity types... 6. After drafting Cedar snippets, call cedar_validate on each... 7. For each modification to an existing policy, call cedar_check_policy_change..."
}

The main example above shows the auto-resolve path (one store loaded, no store_ref passed). When multiple stores are loaded and no store_ref is passed, the response shape is ambiguous instead:

{
  "tool": "cedar_advise",
  "bundle_version": "v2",
  "intent": "Make editors read-only, admins exempt.",
  "store_status": "ambiguous",
  "available_stores": ["blue", "green"],
  "policy_inventory": [],
  "patterns_detected_in_store": [],
  "...": "universal Cedar / AVP context still present"
}

The calling LLM should ask the user which store and re-invoke with an explicit store_ref. The next_steps_for_llm field in the response includes this guidance.

When to use: at the start of any policy-change conversation, before recommending any Cedar snippet. Call this once per intent; iterate the plan in conversation rather than re-calling for small refinements (the bundle is the same for a given intent + store).

When you have more than one store loaded, name your policy store in the prompt (for example, "plan this change against my cedar-sandbox store" or "modify policies in production"). With a store name, the bundle grounds in your actual schema, full policy inventory, and detected patterns. If exactly one store is loaded the bundle auto-resolves to it (you'll see auto_discovered.store_from: "single_loaded_store"). If multiple stores are loaded and you don't pass store_ref, store_status is "ambiguous" and available_stores lists the candidates so you can retry. If no stores are loaded at all, store_status is "not_provided" and the bundle returns the generic Cedar / AVP context only.


cedar_validate_template

Validates a Cedar template policy against a schema. Templates use slot placeholders (?principal, ?resource) that are bound when the template is instantiated.

Inputs:

| Parameter | Required | Description | |-----------|----------|-------------| | template | yes | Cedar template text, e.g. permit(principal == ?principal, action == ..., resource == ?resource); | | schema | yes | Cedar schema (JSON or .cedarschema format) |

Example prompt (paste into Claude Code from a Cedar workspace):

Validate this Cedar template against the sandbox schema:
permit (principal == ?principal, action == MyApp::Action::"read", resource == ?resource);

The assistant translates this to a cedar_validate_template call with the template text and the inline schema from cedar-sandbox/schema.cedarschema.

Response:

{
  "valid": true,
  "errors": [],
  "warnings": [],
  "slots_detected": ["?principal", "?resource"]
}

When to use: after writing a new template, before adding it to your policy store. Also use to discover which slots a template exposes before calling cedar_link_template.


cedar_link_template

Instantiates a Cedar template by binding its ?principal and/or ?resource slots to specific entity references. Returns the resulting Cedar policy text.

Inputs:

| Parameter | Required | Description | |-----------|----------|-------------| | template | yes | Cedar template text | | principal | no | Entity reference for the ?principal slot, e.g. App::User::"alice" | | resource | no | Entity reference for the ?resource slot, e.g. App::Document::"doc-42" | | schema | no | Cedar schema; if provided, the linked policy is validated against it |

Example prompt (paste into Claude Code from a Cedar workspace):

Link this template with alice + doc-public, validated against the sandbox schema:
permit (principal == ?principal, action == MyApp::Action::"read", resource == ?resource);

Response:

{
  "linked_policy": "permit(principal == MyApp::User::\"alice\", action == MyApp::Action::\"read\", resource == MyApp::Document::\"doc-public\");",
  "slots_bound": {
    "?principal": "MyApp::User::\"alice\"",
    "?resource": "MyApp::Document::\"doc-public\""
  },
  "valid": true,
  "errors": []
}

Entity reference format is Namespace::Type::"id", same as cedar_authorize's principal / resource parameters.

When to use: instantiating a template to inspect the resulting policy or to validate it before deployment. For AVP, you upload the template once via CreatePolicyTemplate and create instances via CreatePolicy (template-linked variant); cedar_link_template helps you reason about what those instances will look like before you make the API call.


cedar_list_templates

Lists all Cedar template policies in a policy store. Templates live in a templates/ subdirectory of the store root, following the same layout convention as policies/.

Store layout with templates:

my-store/
  policies/
    admin.cedar
  templates/
    viewer-access.cedar    <- permit(principal == ?principal, action == ..., resource == ?resource);
    editor-access.cedar
  template-links/
    alice-docs.json        <- { "template_id": "viewer-access", "slot_values": { ... } }
  schema.cedarschema

Inputs:

| Parameter | Required | Description | |-----------|----------|-------------| | store | yes | Store name (must be a configured MCP root) |

Example prompt (paste into Claude Code from a Cedar workspace):

List templates in the cedar-sandbox store.

Response:

{
  "store": "cedar-sandbox",
  "templates": []
}

The sandbox has no templates/ subdirectory, so the array is empty. With templates present in a store, each entry includes id (filename basename without .cedar), content (the template text), and slots (the ?principal / ?resource placeholders detected by parsing the template).

When to use: discovering what templates exist in a store before instantiating or diffing them.


cedar_list_template_links

Lists all template-linked policy instances in a store. Links live in a template-links/ subdirectory. Each link is a JSON file recording which template it uses and the slot values bound to it.

Template link file format (template-links/alice-docs.json):

{
  "template_id": "viewer-access",
  "slot_values": {
    "?principal": "App::User::\"alice\"",
    "?resource": "App::Document::\"doc-42\""
  }
}

Inputs:

| Parameter | Required | Description | |-----------|----------|-------------| | store | yes | Store name (must be a configured MCP root) |

Example prompt (paste into Claude Code from a Cedar workspace):

List template links in the cedar-sandbox store.

Response:

{
  "store": "cedar-sandbox",
  "links": []
}

The sandbox has no template-links/ subdirectory, so the array is empty. With links present, each entry includes id (filename basename without .json), template_id, and slot_values.

When to use: auditing which principal-resource pairs are covered by template-linked policies in a store, or diffing link coverage before a deployment.


cedar_diff_policy_stores

Structural and optional behavioral diff between two policy stores. Requires MCP Roots configured (see Setup with policy stores).

Inputs:

| Parameter | Required | Description | |-----------|----------|-------------| | blue | yes | Store name for the base (e.g. "production") | | green | yes | Store name for the proposed changes (e.g. "staging") | | behavioral_test_requests | no | JSON string of authorization requests to run through both stores; surfaces decision drift |

Output shape:

Example prompt (paste into Claude Code from a Cedar workspace):

Diff the cedar-sandbox store against itself (smoke test the tool shape).

The assistant translates this to a cedar_diff_policy_stores call with blue: "cedar-sandbox", green: "cedar-sandbox". A real promotion run would compare distinct stores (e.g. blue: "production", green: "staging"); see Setup with policy stores for multi-store configuration.

Response:

{
  "blue": "cedar-sandbox",
  "green": "cedar-sandbox",
  "policies_added": [],
  "policies_removed": [],
  "policies_modified": [],
  "schema_diff": {
    "namespaces_added": [],
    "namespaces_removed": [],
    "entity_types": { "added": [], "removed": [], "modified": [] },
    "actions": { "added": [], "removed": [], "modified": [] },
    "common_types": { "added": [], "removed": [], "modified": [] },
    "summary": "No schema changes detected.",
    "risk_level": "safe"
  },
  "summary": "No changes detected between blue and green stores."
}

With real differences between the two stores, policies_added / policies_removed / policies_modified carry per-policy details (the same can_update_in_place + changes shape as cedar_check_policy_change), and schema_diff carries the full structured output of cedar_diff_schema. When behavioral_test_requests is supplied, the response also includes a behavioral_diff array of per-request blue_decision vs green_decision entries with a drifted boolean flagging differences.

When to use: before promoting any policy changes from staging to production. The structural diff tells you what changed and how to deploy it. The behavioral diff tells you which authorization decisions would actually change.


cedar_validate_schema

Validates a Cedar schema in isolation, without requiring any policies. Useful for the schema-first workflow: shape the entity model before writing the first policy.

Inputs:

| Parameter | Required | Description | |-----------|----------|-------------| | schema | one of | Cedar schema text (JSON object or .cedarschema text). Format auto-detected. | | schema_ref | one of | cedar://schema/{store} URI for a configured policy store. |

Exactly one of schema or schema_ref is required.

Example prompt (paste into Claude Code from a Cedar workspace):

Validate the cedar-sandbox schema.

The assistant translates this to a cedar_validate_schema call with schema_ref: "cedar://schema/cedar-sandbox".

Response:

{
  "valid": true,
  "format": "cedarschema",
  "namespaces": ["MyApp"],
  "entity_type_count": 4,
  "action_count": 3,
  "common_type_count": 0,
  "errors": []
}

When invalid, the same shape carries valid: false, zeroed counts, and an errors array with message plus a source_location block:

{
  "valid": false,
  "format": "cedarschema",
  "namespaces": [],
  "entity_type_count": 0,
  "action_count": 0,
  "common_type_count": 0,
  "errors": [
    {
      "message": "failed to parse schema from string: unexpected token `not`",
      "source_location": { "start": 0, "end": 3, "label": "expected `@`, `action`, `entity`, `namespace`, or `type`" }
    }
  ]
}

When to use:

  • before writing the first policy, to confirm the schema you sketched parses correctly
  • before pushing schema changes to AVP via PutSchema, as a syntactic sanity check
  • as a fast check inside an agentic loop that's building a schema iteratively

cedar_diff_schema

Structural diff of two Cedar schemas with AVP-aware risk classification per change. Replaces the hand-wavy "schemas differ, review carefully" pattern with a structured payload that says exactly what changed and how risky each change is for existing policies.

Inputs:

| Parameter | Required | Description | |-----------|----------|-------------| | blue | yes | Baseline schema. Inline schema text (JSON or .cedarschema) OR a cedar://schema/{store} URI. | | green | yes | Proposed schema. Same input forms as blue. |

The tool auto-detects cedar:// URIs and resolves them via configured policy stores.

Example prompt (paste into Claude Code from a Cedar workspace):

Diff the cedar-sandbox schema against a proposed schema that adds a verified_email: Bool attribute to User.

The assistant translates this to a cedar_diff_schema call with blue: "cedar://schema/cedar-sandbox" and green set to the sandbox schema text with verified_email: Bool, inserted into the User entity.

Response:

{
  "namespaces_added": [],
  "namespaces_removed": [],
  "entity_types": {
    "added": [],
    "removed": [],
    "modified": [
      {
        "namespace": "MyApp",
        "name": "User",
        "attribute_changes": [
          {
            "attr": "verified_email",
            "change": "added",
            "new_type": "Bool",
            "risk": "breaking",
            "reason": "Required attribute added: existing entities/requests without this field will fail validation."
          }
        ]
      }
    ]
  },
  "actions": { "added": [], "removed": [], "modified": [] },
  "common_types": { "added": [], "removed": [], "modified": [] },
  "summary": "Schema diff: 1 entity type(s) modified (1 BREAKING).",
  "risk_level": "breaking"
}

The risk: "breaking" classification fires because adding a required attribute to an existing entity type invalidates every entity payload that doesn't already carry the new field. To deploy this safely, make the attribute optional first, backfill values, then tighten to required.

Risk classification rules. Each change carries risk: safe | review | breaking plus a reason string. The rules:

| Change | Risk | Why | |---|---|---| | Entity type added | safe | No existing policy references it | | Entity type removed | breaking | Policies referencing the type fail validation | | Optional attribute added | safe | Existing policies don't reference it | | Required attribute added | breaking | Existing entities lack the field | | Attribute removed | breaking | Policies referencing it fail validation | | Attribute type changed | breaking | Policies expecting the old type fail evaluation | | Optional → required | breaking | Existing entities without the field fail | | Required → optional | safe | Existing entities still satisfy the constraint | | memberOfTypes added | review | Hierarchy widens; in checks may match more entities | | memberOfTypes removed | breaking | Policies using in against removed parents fail | | Action added | safe | No existing policy targets it | | Action removed | breaking | Policies referencing it become invalid | | Action principalTypes widened | review | Policy effect may change | | Action principalTypes narrowed | breaking | Existing policies for the removed type fail | | Action resourceTypes widened / narrowed | review / breaking | Same as principalTypes | | Action context attribute follows entity-attribute rules | (see above) | (see above) | | Common type added | safe | Nothing references it yet | | Common type removed | review | If unreferenced, safe; if referenced, breaking. Audit. | | Common type modified | review | Default to review; precise impact depends on references |

risk_level on the top-level result is the worst risk across all changes.

When to use:

  • before promoting a schema change to production, to see exactly what's at risk
  • as the structured backbone of cedar_diff_policy_stores (embedded automatically there)
  • inside an agentic policy review, so the agent can reason about whether the schema change is deployable as-is

cedar_validate_entities

Validates a Cedar entities JSON array against a schema, returning per-entity errors classified by kind. Useful for catching entity-store drift before it hits authorization at runtime.

Inputs:

| Parameter | Required | Description | |-----------|----------|-------------| | entities | yes | JSON array of entity objects with uid, attrs, parents | | schema | no | Cedar schema (JSON or .cedarschema); when supplied, enables type validation. Without it, only JSON shape is checked. | | schema_ref | no | cedar://schema/{store} URI alternative to inline schema |

Example prompt (paste into Claude Code from a Cedar workspace):

Validate the entities in cedar-sandbox against its schema.

The assistant translates this to a cedar_validate_entities call with the entities payload inline (read from cedar-sandbox/entities/sample.json) and schema_ref: "cedar://schema/cedar-sandbox".

Response:

{
  "valid": true,
  "entity_count": 11,
  "errors": []
}

When some entities fail validation, valid flips to false and errors lists per-entity findings:

{
  "valid": false,
  "entity_count": 1,
  "errors": [
    {
      "entity_uid": "MyApp::User::\"alice\"",
      "error_kind": "type_mismatch",
      "attribute": "name",
      "message": "entity does not conform to the schema: in attribute `name` on `MyApp::User::\"alice\"`, type mismatch: value was expected to have type string, but it actually has type long: `42`"
    }
  ]
}

error_kind is one of: unknown_type, missing_required_attribute, type_mismatch, unknown_attribute, disallowed_parent_type, parse_error, other. The attribute field is present for attribute-related errors. disallowed_parent_type fires when an entity's parents array contains a type the schema doesn't allow as an ancestor for that entity type.

When to use:

  • when working with entity dumps from AVP BatchGet or custom entity stores
  • before running cedar_authorize on a request that includes user-supplied entities, to catch shape issues early
  • inside a CI pipeline that publishes an entity snapshot, to fail fast on drift

MCP Prompts

In addition to tools, the server registers three MCP prompts that clients surface as slash commands or pre-canned message templates. Each prompt takes arguments, then returns an assembled message that drives the assistant through a structured Cedar workflow.

| Prompt | Arguments | What it does | |--------|-----------|--------------| | cedar-review-policy-diff | blue_store (required), green_store (required), focus (optional) | Drives cedar_diff_policy_stores + cedar_diff_schema, summarizes structural changes plus risk-classified schema diff, and recommends whether to promote. | | cedar-explain-denial | principal, action, resource, store (all required) | Runs cedar_authorize against the store via cedar:// refs, calls cedar_explain on the deciding policies, and produces a plain-English explanation of why the request was denied (or allowed) plus what would need to change. | | cedar-avp-migration-checklist | namespace (optional) | Returns a guided checklist for migrating an AVP policy store: schema validation, entity format detection, single-namespace constraint, template-linked policies, schema diff before PutSchema, behavioral diff before traffic shift. Informational only; no tool calls assumed. |

In Claude Code these appear under the / slash menu when the server is configured. Other clients surface them differently per their UI conventions.


Running as a shared HTTP server

The default npx cedar-mcp-server mode is stdio, designed for Claude Code / Claude Desktop / Cursor on a single developer machine. For a shared team deployment (one server, many clients), use Streamable HTTP mode.

Start the server

# Local-only (recommended default; binds to 127.0.0.1 with DNS rebinding protection)
cedar-mcp-server --http 3000 --root production=/etc/cedar/production --root staging=/etc/cedar/staging

# Non-localhost binding (you handle auth via reverse proxy)
cedar-mcp-server --http 3000 --host 0.0.0.0 --root prod=/etc/cedar/prod

CLI flags:

| Flag | Required | Description | |------|----------|-------------| | --http <port> | yes (HTTP mode) | Listen port (1-65535) | | --host <host> | no | Bind host (default 127.0.0.1) | | --root <name>=<path> | repeatable | Deployer-configured policy store; clients see these as MCP Roots | | --help | no | Print usage |

Roots in stdio vs HTTP mode

In stdio mode, your MCP client advertises its workspace folders to the server automatically via the listRoots() protocol. You do not need the --root flag at all; the server queries the client on initialize and loads each advertised root as a named policy store. If you pass --root to the stdio binary, the server exits at startup with an error message saying --root is HTTP-only. This is intentional: in stdio, the client is the authority on what is in scope.

Not every stdio client advertises the workspace as a root. Claude Code currently does not. If the server's working directory itself looks like a Cedar policy store (one of schema.cedarschema, schema.json, or a policies/ directory exists), the server loads the cwd as a store named after the cwd's basename synchronously at startup, before the transport accepts any client requests. By the time the client can send anything (including resources/list), the store is already populated. This is the "user opens an MCP-enabled CLI inside their Cedar repo and expects the tools to work" path.

When the MCP client later advertises roots via listRoots() (during the initialize handshake) or via a notifications/roots/list_changed message, those roots replace the sync-loaded cwd-fallback. A client that advertises roots is stating authoritative intent. When the client advertises zero roots, the sync-loaded cwd-fallback is preserved. The server also emits notifications/resources/list_changed after any reconciliation pass so cache-aware clients can refresh on the rare swap.

In HTTP mode, there is no client workspace to negotiate with; the operator running the long-lived process is the authority. The --root name=path flag is how the deployer tells the server "expose this directory as a named policy store". Pass --root once per store. Every connected HTTP client sees the same set of roots.

Endpoints

  • POST /mcp: the MCP Streamable HTTP endpoint. Each session gets a unique Mcp-Session-Id returned on the initialize response and required on subsequent requests.
  • GET /health: returns { status, transport, mode, active_sessions } JSON. Useful for liveness probes.

Client configuration

Point any Streamable-HTTP-capable MCP client at http://<host>:<port>/mcp. Example (Claude Code or similar) using the SDK's StreamableHTTPClientTransport:

import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";

const client = new Client({ name: "team", version: "1.0.0" }, { capabilities: {} });
const transport = new StreamableHTTPClientTransport(new URL("http://cedar-mcp.internal:3000/mcp"));
await client.connect(transport);

Sharing model and limitations

The HTTP server runs one shared storeManager across all concurrent sessions. The deployment model is "one server per policy-store set; many team clients all see the same roots." Every client connected to the same HTTP server reads the same --root mappings. For per-tenant isolation (different teams seeing different policy stores), deploy multiple processes behind a routing layer.

Each MCP session DOES get its own McpServer instance; protocol state (initialized, message history, sampling) is per-session as the MCP spec requires.

Security

  • Default localhost binding plus the SDK's built-in DNS rebinding protection covers the local team-dev case.
  • Non-localhost binding (--host 0.0.0.0 or a public IP) is on you to secure. Recommended pattern: terminate TLS at a reverse proxy (nginx, Caddy, Cloudflare), add bearer-token or mTLS auth at that layer, forward the POST /mcp and GET /health paths to the server.
  • v1 ships without built-in auth or CORS. Both are deferred until real demand surfaces.
  • See SECURITY.md for the trust boundary and input validation guarantees.

Setup with policy stores (MCP Roots)

If your Cedar policies live on disk, configure MCP roots once and the server reads them directly. No more pasting policy text into every tool call.

Policy store layout

Each root directory must follow this structure:

my-store/
  policies/
    admin.cedar
    editor.cedar
    viewer.cedar
  templates/
    viewer-access.cedar        <- Cedar template with ?principal / ?resource slots (optional)
  template-links/
    alice-docs.json            <- { "template_id": "...", "slot_values": { ... } } (optional)
  schema.cedarschema           <- Cedar schema text (preferred)
  schema.json                  <- Cedar JSON schema (alternative)

Configure roots in Claude Code

Add roots to .claude/settings.json:

{
  "mcpServers": {
    "cedar": {
      "command": "npx",
      "args": ["-y", "cedar-mcp-server"],
      "roots": [
        { "uri": "file:///path/to/production-store", "name": "production" },
        { "uri": "file:///path/to/staging-store", "name": "staging" }
      ]
    }
  }
}

The root name ("production", "staging") becomes the store identifier used in tool calls.

Use cedar:// references instead of inline text

Once roots are configured, use cedar:// URIs instead of pasting policy text:

cedar://policies/production              <- all policies in the production store
cedar://policies/production/admin        <- the admin.cedar policy
cedar://schema/production                <- the production schema
cedar://templates/production             <- all templates in the production store
cedar://templates/production/viewer-access  <- the viewer-access template
cedar://template-links/production        <- all template-link IDs in the store
cedar://template-links/production/alice-docs  <- a specific link's metadata
cedar://entities/production              <- merged entity JSON across all entities/*.json files
cedar://entities/production/users-and-docs  <- a single entity file

Both policy_ref and schema_ref accept these URIs in cedar_validate and cedar_authorize. Inline text still works; pass either form.

Error when no stores are configured

If you call cedar_diff_policy_stores or use a cedar:// reference but haven't configured roots, the error message explains the expected directory layout and how to configure r