CedarDraft MCP
README.md
# CedarDraft MCP
An MCP server that turns plain-language access rules for agent tool calls (English or Spanish)
into Cedar policies, refuses any draft it cannot prove, and then gates each proposed tool call
with ALLOW or DENY plus the ids of the policies that decided it.
[](https://github.com/SkilledDragonCorp/cedardraft-mcp/actions/workflows/ci.yml)
[](./LICENSE)
## The problem
Teams know an agent's tool calls should sit behind a policy: name the tools and arguments a task
may use, and deny everything else. Writing Cedar by hand is slow. A policy nobody has checked can
be worse than no policy, because it looks like protection. CedarDraft writes the draft, proves it
against its own stated intent with the official Cedar tools, and enforces only proven drafts.
## Three tools
| Tool | What it does |
| --- | --- |
| `draft_policy` | Rules, one sentence per line, EN or ES -> one Cedar policy per rule (`@id("rule-N")`) plus a Cedar schema fragment generated from the tool catalog. Keyless: a deterministic template drafter for the [documented patterns](#rule-patterns). With `GEMINI_API_KEY`: Gemini structured output returns the rule intent and its own Cedar text. |
| `prove_policy` | Accepts a draft only if all 8 stages pass (below). Writes `proof.json` and `tests.json` into the bundle. |
| `gate_call` | ALLOW/DENY for one call `{agent, roles, server, tool, arguments}` with the cedar-policy 4.14.0 crate, against a bundle whose proof is accepted and whose files still hash to the proof. Returns the determining policy ids. |
### How a call maps to Cedar
| MCP | Cedar |
| --- | --- |
| agent id, roles | `CedarDraft::Agent::"<id>"`, member of `CedarDraft::Role::"<role>"` |
| tool `refund` on server `billing` | `CedarDraft::Action::"billing___refund"`, member of group action `CedarDraft::Action::"billing"` |
| server | `CedarDraft::Server::"billing"` (resource) |
| tool arguments | `context.input`, typed from the tool's JSON Schema (`integer` -> `Long`, `string` -> `String`, `boolean` -> `Bool`, arrays -> `Set`, objects -> records) |
The `<server>___<tool>` action name copies the `<TargetName>___<ToolName>` shape that the
AgentCore Gateway docs give for MCP targets. That shape is the only AgentCore-related piece here.
### Shared base schema with Cedar 4.14.0 `external`
`schemas/base.cedarschema` defines `Role`, `Agent` and `Server` once. Each draft's
`schema.fragment.cedarschema` declares `external entity Agent;` and `external entity Server;`
and only defines the actions for its catalog. The engine links them with
`Schema::schema_link_complete` (cedar-policy 4.14.0). `cedar validate` refuses the fragment
alone because it is a partial schema. Many drafts can share one base.
### prove_policy stages
1. **tools**: `cedar --version` is exactly `cedar-policy-cli 4.14.0`, and the engine was built on `cedar-policy 4.14.0`.
2. **intent**: every rule names catalog servers, tools and arguments of the right type.
3. **link**: the fragment uses `external`, and linking it with the base leaves nothing external.
4. **validate**: strict validation in the crate against the linked schema, with zero errors and zero warnings.
5. **cedar-cli**: `cedar validate --validation-mode strict --deny-warnings` with the official CLI. The CLI has no schema-link command, so it gets a JSON merge of `cedar translate-schema` output.
6. **ids**: policy `@id`s equal the rule ids. Unexplained extra policies are rejected.
7. **table**: an authorization table built from the rule intent and the catalog, never from the policy text. Every catalog tool is called by an agent of each named role and of an outsider role, with neutral arguments and boundary values for each condition (N-1, N, N+1; each listed value plus one unlisted value). There must be at least one default-deny case, and every case must pass `cedar run-tests`.
8. **crate-table**: the same table through the crate, which is the gate_call path. Decisions and determining-policy sets must match exactly.
## Quickstart
Requires Bun 1.4.3, Node 24 (`.nvmrc`) and Rust 1.99.0 (`rust-toolchain.toml`). No API keys.
```bash
bun install --frozen-lockfile
bun run setup # cargo install cedar-policy-cli 4.14.0 --locked into .tools/, build engine/
bun run check # full local gate
bun run cedardraft -- draft --file examples/rules.en-es.txt --out .cedardraft/example
bun run cedardraft -- prove .cedardraft/example # exit 0 accepted, 1 rejected
bun run cedardraft -- gate .cedardraft/example --agent a1 --role soporte \
--server billing --tool refund --args '{"invoice_id":"i1","amount":50001,"currency":"USD"}'
# DENY - default deny: no permit policy matched (exit 2)
```
MCP client config (stdio):
```json
{ "mcpServers": { "cedardraft": { "command": "bun", "args": ["run", "/path/to/cedardraft-mcp/src/mcp.ts"] } } }
```
The skill at `.claude/skills/cedardraft-mcp/SKILL.md` tells an agent how to use the three tools.
## Rule patterns
The template drafter understands only these patterns. Any other sentence is an error that names
the sentence. The drafter does not guess. Tools are written `server.tool`, or as
`any tool on server S` / `cualquier herramienta del servidor S`. Roles are written
`agents with role R (or R2)` / `los agentes con rol R (o R2)`, or `any agent` / `cualquier agente`.
| Pattern | English | Spanish |
| --- | --- | --- |
| `allow` | Allow agents with role support to call billing.get_invoice and billing.list_invoices | Permitir que los agentes con rol soporte llamen a billing.get_invoice y billing.list_invoices |
| `allow-cap` | Allow agents with role support to call billing.refund only when amount is at most 50000 | Permitir que los agentes con rol soporte llamen a billing.refund solo cuando amount sea como máximo 50000 |
| `allow-values` | Allow agents with role dev or ops to call github.create_issue only when repo is one of "acme/web", "acme/api" | Permitir que los agentes con rol dev o ops llamen a github.create_issue solo cuando repo sea uno de "acme/web", "acme/api" |
| `forbid` | Never allow any agent to call billing.delete_customer | Nunca permitir que ningún agente llame a billing.delete_customer |
| `forbid-when` | Never allow any agent to call crm.export_contacts when include_pii is true | Nunca permitir que ningún agente llame a crm.export_contacts cuando include_pii sea verdadero |
| `scope` | In this task only crm.search_contacts and crm.get_contact are in scope | En esta tarea solo crm.search_contacts y crm.get_contact están en alcance |
Numeric conditions: `at most / no more than / up to` (<=), `under / less than / below` (<),
`at least` (>=), `over / more than / greater than / above` (>). Spanish: `como máximo / a lo sumo
/ hasta`, `menor que / menos de`, `al menos / como mínimo`, `mayor que / más de`. An optional
argument is guarded with `has`, so a missing argument never satisfies a condition.
## Measured
Produced by `bun run report` (`reports/cedardraft.md`, `reports/cedardraft.json`) on the
committed fixtures on this machine. The CLI was cedar-policy-cli 4.14.0 and the engine was built
on cedar-policy 4.14.0.
| Measure | Result |
| --- | --- |
| Documented patterns drafted keyless and accepted by prove_policy | 12/12 (6 patterns x EN, ES) |
| Example rule set (`examples/rules.en-es.txt`, 6 rules, mixed EN/ES) | accepted |
| Its generated table | 85 cases: 19 allow, 66 deny (10 forbid, 56 default deny) |
| `cedar run-tests` / crate agreement on that table | 85/85 passed / 85/85 matched |
| Deliberately wrong drafts rejected | 8/8 (widened limit, off-by-one: table; type error, unknown argument, missing @id: validate; extra policy: ids; permit-all: table; fragment without `external`: link) |
| gate_call vs expected decision + determining policies | 85/85 agree |
| gate_call latency (one engine process per call) | median 14.2 ms, p95 18.2 ms |
| Live Gemini drafting | not run: `gemini-3.8-flash` returned HTTP 402 for this key |
## Verified facts (and where)
| Claim | Status | Source |
| --- | --- | --- |
| `cedar-policy` and `cedar-policy-cli` 4.14.0 are current | Verified: crates.io API, both `max_stable_version` 4.14.0, published 2026-10-17. Tags `v4.14.0` and `cedar-policy-cli-v4.14.0` exist on GitHub. | crates.io, github.com/cedar-policy/cedar releases |
| 4.14.0 adds the `external` keyword for partial/shared schemas | Verified in the `cedar-policy` changelog `[4.14.0] - 2026-10-17` (`external` entity type and action declarations, `Schema::schema_link`, `schema_link_complete`, `is_complete`) and in PR #2615. Checked in code: the fragment links, and `cedar validate` refuses the fragment alone because "schema is incomplete". | cedar-policy/CHANGELOG.md on `main`, PR cedar-policy/cedar#2615 |
| CLI flags used | Read from `cedar --help` at 4.14.0: `validate --schema --schema-format --policies --validation-mode strict --deny-warnings --error-format`, `run-tests --policies --tests --schema --schema-format`, `translate-schema --direction cedar-to-json`. The CLI uses each policy's `@id` annotation as its id. | `.tools/bin/cedar` 4.14.0, its source |
| Amazon Bedrock AgentCore Policy uses Cedar | Verified in the AgentCore developer guide: policies are "constructed using Cedar language". The same page says Dogwood is also supported. The Cedar page documents `AgentCore::Action::"<TargetName>___<ToolName>"` for MCP targets. No AWS call was made. | docs.aws.amazon.com/bedrock-agentcore/latest/devguide/policy.html and policy-understanding-cedar.html |
## Limits
- **Templates are narrow on purpose.** There are six patterns, one condition per sentence, and one server per sentence. Anything else must be rephrased or drafted with Gemini.
- **Gemini drafting is untested live.** The key on this machine returns HTTP 402 (no credits), so no Gemini-written draft has been produced or measured. The Gemini path is covered by unit tests with synthetic, mocked responses. `tests/live` skips on a missing key, 402 or 429.
- **The proof checks the policy text against the rule intent, not against what the user meant.** If the intent itself is wrong (a wrong role or a too-high limit), the draft can prove. Review `draft.json` rules and their `source` sentences.
- **The table is generated, not exhaustive.** It probes boundaries and every catalog tool × role. It is not a symbolic proof. `cedar symcc` exists in the experimental CLI build and is not used here.
- **The CLI does not link schemas.** The `cedar-cli` stage validates a JSON merge that this repo computes. The link itself is checked by the crate (`schema_link_complete`). Both must pass.
- **Cedar types:** JSON `number` arguments (non-integer) cannot be typed in Cedar. They are left out of the context and listed as warnings, and no rule can reference them. Undeclared arguments make gate_call DENY.
- **No AgentCore export.** The idea's optional "AgentCore Policy export" is not built. The policies use the `CedarDraft` namespace, not `AgentCore`.
- **Unverified idea context:** the CERT-In guidance the idea quotes (define in-scope systems and actions per task) was not checked here. The `scope` pattern is only modeled on it. The Cedar docs site's schema grammar page does not yet mention `external`; the changelog and PR are the sources.
- **Speed:** gate_call starts one engine process per call (median 14.2 ms here). A long-lived engine would be faster but is not built.
## Not affiliated
Not affiliated with, endorsed by, or sponsored by Amazon Web Services, the Cedar project, Google
(Gemini), Anthropic or the Model Context Protocol project. Cedar and AgentCore are referenced
only to describe compatibility facts checked above.
---
Built by [SkilledDragon LLC](https://skilleddragon.com). Questions: hello@skilleddragon.com.
Educational minilab, not regularly maintained. See [SECURITY.md](./SECURITY.md).
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues