cedar-mcp-server 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (215) hide show
  1. package/.editorconfig +12 -0
  2. package/.github/workflows/ci.yml +31 -0
  3. package/.github/workflows/release.yml +42 -0
  4. package/.nvmrc +1 -0
  5. package/CHANGELOG.md +241 -0
  6. package/CONTRIBUTING.md +83 -0
  7. package/LICENSE +182 -0
  8. package/README.md +1635 -0
  9. package/SECURITY.md +37 -0
  10. package/dist/http-server.d.ts +61 -0
  11. package/dist/http-server.d.ts.map +1 -0
  12. package/dist/http-server.js +194 -0
  13. package/dist/http-server.js.map +1 -0
  14. package/dist/index.d.ts +32 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +270 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/parser/policy-ast.d.ts +49 -0
  19. package/dist/parser/policy-ast.d.ts.map +1 -0
  20. package/dist/parser/policy-ast.js +311 -0
  21. package/dist/parser/policy-ast.js.map +1 -0
  22. package/dist/prompts/index.d.ts +38 -0
  23. package/dist/prompts/index.d.ts.map +1 -0
  24. package/dist/prompts/index.js +172 -0
  25. package/dist/prompts/index.js.map +1 -0
  26. package/dist/resources/ref-resolver.d.ts +23 -0
  27. package/dist/resources/ref-resolver.d.ts.map +1 -0
  28. package/dist/resources/ref-resolver.js +128 -0
  29. package/dist/resources/ref-resolver.js.map +1 -0
  30. package/dist/resources/store-manager.d.ts +64 -0
  31. package/dist/resources/store-manager.d.ts.map +1 -0
  32. package/dist/resources/store-manager.js +221 -0
  33. package/dist/resources/store-manager.js.map +1 -0
  34. package/dist/server.d.ts +18 -0
  35. package/dist/server.d.ts.map +1 -0
  36. package/dist/server.js +539 -0
  37. package/dist/server.js.map +1 -0
  38. package/dist/tools/advise/avp-rules.d.ts +49 -0
  39. package/dist/tools/advise/avp-rules.d.ts.map +1 -0
  40. package/dist/tools/advise/avp-rules.js +59 -0
  41. package/dist/tools/advise/avp-rules.js.map +1 -0
  42. package/dist/tools/advise/cedar-patterns.d.ts +24 -0
  43. package/dist/tools/advise/cedar-patterns.d.ts.map +1 -0
  44. package/dist/tools/advise/cedar-patterns.js +57 -0
  45. package/dist/tools/advise/cedar-patterns.js.map +1 -0
  46. package/dist/tools/advise/context-builder.d.ts +28 -0
  47. package/dist/tools/advise/context-builder.d.ts.map +1 -0
  48. package/dist/tools/advise/context-builder.js +89 -0
  49. package/dist/tools/advise/context-builder.js.map +1 -0
  50. package/dist/tools/advise/gotchas.d.ts +15 -0
  51. package/dist/tools/advise/gotchas.d.ts.map +1 -0
  52. package/dist/tools/advise/gotchas.js +83 -0
  53. package/dist/tools/advise/gotchas.js.map +1 -0
  54. package/dist/tools/advise.d.ts +96 -0
  55. package/dist/tools/advise.d.ts.map +1 -0
  56. package/dist/tools/advise.js +258 -0
  57. package/dist/tools/advise.js.map +1 -0
  58. package/dist/tools/authorize-batch.d.ts +35 -0
  59. package/dist/tools/authorize-batch.d.ts.map +1 -0
  60. package/dist/tools/authorize-batch.js +262 -0
  61. package/dist/tools/authorize-batch.js.map +1 -0
  62. package/dist/tools/authorize.d.ts +115 -0
  63. package/dist/tools/authorize.d.ts.map +1 -0
  64. package/dist/tools/authorize.js +373 -0
  65. package/dist/tools/authorize.js.map +1 -0
  66. package/dist/tools/check-change.d.ts +19 -0
  67. package/dist/tools/check-change.d.ts.map +1 -0
  68. package/dist/tools/check-change.js +91 -0
  69. package/dist/tools/check-change.js.map +1 -0
  70. package/dist/tools/diff-schema.d.ts +103 -0
  71. package/dist/tools/diff-schema.d.ts.map +1 -0
  72. package/dist/tools/diff-schema.js +379 -0
  73. package/dist/tools/diff-schema.js.map +1 -0
  74. package/dist/tools/diff-stores.d.ts +45 -0
  75. package/dist/tools/diff-stores.d.ts.map +1 -0
  76. package/dist/tools/diff-stores.js +222 -0
  77. package/dist/tools/diff-stores.js.map +1 -0
  78. package/dist/tools/explain.d.ts +80 -0
  79. package/dist/tools/explain.d.ts.map +1 -0
  80. package/dist/tools/explain.js +187 -0
  81. package/dist/tools/explain.js.map +1 -0
  82. package/dist/tools/format.d.ts +11 -0
  83. package/dist/tools/format.d.ts.map +1 -0
  84. package/dist/tools/format.js +20 -0
  85. package/dist/tools/format.js.map +1 -0
  86. package/dist/tools/generate-sample.d.ts +28 -0
  87. package/dist/tools/generate-sample.d.ts.map +1 -0
  88. package/dist/tools/generate-sample.js +568 -0
  89. package/dist/tools/generate-sample.js.map +1 -0
  90. package/dist/tools/link-template.d.ts +17 -0
  91. package/dist/tools/link-template.d.ts.map +1 -0
  92. package/dist/tools/link-template.js +78 -0
  93. package/dist/tools/link-template.js.map +1 -0
  94. package/dist/tools/list-template-links.d.ts +16 -0
  95. package/dist/tools/list-template-links.d.ts.map +1 -0
  96. package/dist/tools/list-template-links.js +22 -0
  97. package/dist/tools/list-template-links.js.map +1 -0
  98. package/dist/tools/list-templates.d.ts +16 -0
  99. package/dist/tools/list-templates.d.ts.map +1 -0
  100. package/dist/tools/list-templates.js +36 -0
  101. package/dist/tools/list-templates.js.map +1 -0
  102. package/dist/tools/translate.d.ts +11 -0
  103. package/dist/tools/translate.d.ts.map +1 -0
  104. package/dist/tools/translate.js +53 -0
  105. package/dist/tools/translate.js.map +1 -0
  106. package/dist/tools/validate-entities.d.ts +19 -0
  107. package/dist/tools/validate-entities.d.ts.map +1 -0
  108. package/dist/tools/validate-entities.js +88 -0
  109. package/dist/tools/validate-entities.js.map +1 -0
  110. package/dist/tools/validate-schema.d.ts +22 -0
  111. package/dist/tools/validate-schema.d.ts.map +1 -0
  112. package/dist/tools/validate-schema.js +89 -0
  113. package/dist/tools/validate-schema.js.map +1 -0
  114. package/dist/tools/validate-template.d.ts +18 -0
  115. package/dist/tools/validate-template.d.ts.map +1 -0
  116. package/dist/tools/validate-template.js +59 -0
  117. package/dist/tools/validate-template.js.map +1 -0
  118. package/dist/tools/validate.d.ts +90 -0
  119. package/dist/tools/validate.d.ts.map +1 -0
  120. package/dist/tools/validate.js +351 -0
  121. package/dist/tools/validate.js.map +1 -0
  122. package/dist/utils/format-detector.d.ts +49 -0
  123. package/dist/utils/format-detector.d.ts.map +1 -0
  124. package/dist/utils/format-detector.js +298 -0
  125. package/dist/utils/format-detector.js.map +1 -0
  126. package/examples/README.md +36 -0
  127. package/examples/abac-multi-tenant/README.md +150 -0
  128. package/examples/abac-multi-tenant/entities/users-and-docs.json +33 -0
  129. package/examples/abac-multi-tenant/policies/member-read-internal.cedar +9 -0
  130. package/examples/abac-multi-tenant/policies/owner-full-access.cedar +9 -0
  131. package/examples/abac-multi-tenant/policies/premium-share-guard.cedar +9 -0
  132. package/examples/abac-multi-tenant/policies/private-doc-guard.cedar +13 -0
  133. package/examples/abac-multi-tenant/run.ts +92 -0
  134. package/examples/abac-multi-tenant/schema.json +60 -0
  135. package/examples/api-gateway-path-routing/README.md +154 -0
  136. package/examples/api-gateway-path-routing/entities/users-and-roles.json +20 -0
  137. package/examples/api-gateway-path-routing/policies/admin-full-access.cedar +6 -0
  138. package/examples/api-gateway-path-routing/policies/developer-projects.cedar +14 -0
  139. package/examples/api-gateway-path-routing/policies/viewer-readonly.cedar +10 -0
  140. package/examples/api-gateway-path-routing/run.ts +108 -0
  141. package/examples/api-gateway-path-routing/schema.json +54 -0
  142. package/examples/rbac-document-management/README.md +167 -0
  143. package/examples/rbac-document-management/entities/users-and-docs.json +43 -0
  144. package/examples/rbac-document-management/policies/admin.cedar +6 -0
  145. package/examples/rbac-document-management/policies/editor.cedar +6 -0
  146. package/examples/rbac-document-management/policies/top-secret-forbid.cedar +13 -0
  147. package/examples/rbac-document-management/policies/viewer.cedar +6 -0
  148. package/examples/rbac-document-management/run.ts +87 -0
  149. package/examples/rbac-document-management/schema.json +57 -0
  150. package/package.json +50 -0
  151. package/src/http-server.ts +239 -0
  152. package/src/index.ts +294 -0
  153. package/src/parser/policy-ast.ts +345 -0
  154. package/src/prompts/README.md +3 -0
  155. package/src/prompts/index.ts +217 -0
  156. package/src/resources/ref-resolver.ts +134 -0
  157. package/src/resources/store-manager.ts +248 -0
  158. package/src/server.ts +711 -0
  159. package/src/tools/advise/avp-rules.ts +70 -0
  160. package/src/tools/advise/cedar-patterns.ts +73 -0
  161. package/src/tools/advise/context-builder.ts +109 -0
  162. package/src/tools/advise/gotchas.ts +92 -0
  163. package/src/tools/advise.ts +366 -0
  164. package/src/tools/authorize-batch.ts +345 -0
  165. package/src/tools/authorize.ts +464 -0
  166. package/src/tools/check-change.ts +119 -0
  167. package/src/tools/diff-schema.ts +510 -0
  168. package/src/tools/diff-stores.ts +298 -0
  169. package/src/tools/explain.ts +278 -0
  170. package/src/tools/format.ts +33 -0
  171. package/src/tools/generate-sample.ts +665 -0
  172. package/src/tools/link-template.ts +109 -0
  173. package/src/tools/list-template-links.ts +41 -0
  174. package/src/tools/list-templates.ts +55 -0
  175. package/src/tools/translate.ts +66 -0
  176. package/src/tools/validate-entities.ts +125 -0
  177. package/src/tools/validate-schema.ts +128 -0
  178. package/src/tools/validate-template.ts +72 -0
  179. package/src/tools/validate.ts +459 -0
  180. package/src/utils/format-detector.ts +356 -0
  181. package/test/fixtures/docmgmt.ts +121 -0
  182. package/test/fixtures/multitenant.ts +163 -0
  183. package/test/index.test.ts +96 -0
  184. package/test/integration/e2e/behavior.test.ts +359 -0
  185. package/test/integration/e2e/edge-cases.test.ts +365 -0
  186. package/test/integration/e2e/failure-modes.test.ts +266 -0
  187. package/test/integration/e2e/protocol.test.ts +252 -0
  188. package/test/integration/http-smoke.test.ts +588 -0
  189. package/test/integration/smoke.test.ts +475 -0
  190. package/test/prompts/prompts.test.ts +173 -0
  191. package/test/property/properties.test.ts +234 -0
  192. package/test/resources/ref-resolver.test.ts +186 -0
  193. package/test/resources/store-manager.test.ts +344 -0
  194. package/test/setup.test.ts +7 -0
  195. package/test/tools/advise/avp-rules.test.ts +76 -0
  196. package/test/tools/advise.test.ts +339 -0
  197. package/test/tools/authorize-batch.test.ts +459 -0
  198. package/test/tools/authorize.test.ts +682 -0
  199. package/test/tools/check-change.test.ts +104 -0
  200. package/test/tools/cross-fixture.test.ts +170 -0
  201. package/test/tools/diff-schema.test.ts +355 -0
  202. package/test/tools/diff-stores.test.ts +291 -0
  203. package/test/tools/explain.test.ts +221 -0
  204. package/test/tools/format.test.ts +33 -0
  205. package/test/tools/generate-sample.test.ts +480 -0
  206. package/test/tools/link-template.test.ts +90 -0
  207. package/test/tools/list-templates.test.ts +151 -0
  208. package/test/tools/translate.test.ts +89 -0
  209. package/test/tools/validate-entities.test.ts +178 -0
  210. package/test/tools/validate-schema.test.ts +86 -0
  211. package/test/tools/validate-template.test.ts +89 -0
  212. package/test/tools/validate.test.ts +331 -0
  213. package/test/utils/format-detector.test.ts +518 -0
  214. package/tsconfig.json +17 -0
  215. package/vitest.config.ts +13 -0
@@ -0,0 +1,167 @@
1
+ # RBAC Document Management
2
+
3
+ Role-based access control for a document system — the simplest Cedar pattern and the right place to start.
4
+
5
+ ## What this example covers
6
+
7
+ Four roles (admin, editor, viewer, no-role), three actions (READ, WRITE, DELETE), and a `forbid` policy that blocks access to top-secret documents regardless of role. Demonstrates `cedar_authorize`, `cedar_validate`, `cedar_explain`, and `cedar_generate_sample_request`.
8
+
9
+ ## Quick start
10
+
11
+ Configure the MCP server in Claude Code (`.claude/settings.json`):
12
+
13
+ ```json
14
+ {
15
+ "mcpServers": {
16
+ "cedar": {
17
+ "command": "npx",
18
+ "args": ["-y", "cedar-mcp-server"]
19
+ }
20
+ }
21
+ }
22
+ ```
23
+
24
+ Or run offline:
25
+
26
+ ```bash
27
+ npx tsx examples/rbac-document-management/run.ts
28
+ ```
29
+
30
+ ## Files
31
+
32
+ ```
33
+ schema.json Cedar schema — DocMgmt namespace
34
+ policies/
35
+ admin.cedar Admins can do anything
36
+ editor.cedar Editors can read and write
37
+ viewer.cedar Viewers can only read
38
+ top-secret-forbid.cedar Forbid top_secret access (except admins)
39
+ entities/
40
+ users-and-docs.json Alice (admin), Bob (editor), Charlie (viewer), Dave (no role)
41
+ ```
42
+
43
+ ---
44
+
45
+ ## Tool examples — copy and paste to Claude Code
46
+
47
+ ### cedar_validate
48
+
49
+ ```
50
+ Validate these Cedar policies against the schema.
51
+
52
+ Schema:
53
+ [paste contents of schema.json]
54
+
55
+ Policies:
56
+ [paste all .cedar files]
57
+ ```
58
+
59
+ Expected: valid, 4 policies, no errors.
60
+
61
+ ### cedar_authorize
62
+
63
+ ```
64
+ Would Bob be allowed to read the document "acquisition-details"?
65
+
66
+ Policies: [paste all .cedar files]
67
+ Principal: DocMgmt::User::"bob"
68
+ Action: DocMgmt::Action::"READ"
69
+ Resource: DocMgmt::Document::"acquisition-details"
70
+ Entities: [paste entities/users-and-docs.json]
71
+ Schema: [paste schema.json]
72
+ ```
73
+
74
+ Expected: **Deny** — the `top-secret-forbid` policy fires. Bob is an editor but that forbid overrides his permit.
75
+
76
+ ```
77
+ Would Alice be allowed to read acquisition-details?
78
+ ```
79
+
80
+ Expected: **Allow** — Alice is an admin. The `unless` clause in the forbid exempts admins.
81
+
82
+ ### cedar_explain
83
+
84
+ ```
85
+ Explain this Cedar policy in plain English:
86
+
87
+ forbid (
88
+ principal,
89
+ action,
90
+ resource
91
+ )
92
+ when {
93
+ resource.classification == "top_secret"
94
+ }
95
+ unless {
96
+ principal in DocMgmt::Role::"admin"
97
+ };
98
+ ```
99
+
100
+ Expected: a breakdown showing `forbid_policy`, `role_exemption`, the when/unless conditions in plain English.
101
+
102
+ ### cedar_generate_sample_request
103
+
104
+ ```
105
+ Generate a sample request that would be DENIED by this policy:
106
+
107
+ permit (
108
+ principal in DocMgmt::Role::"viewer",
109
+ action == DocMgmt::Action::"READ",
110
+ resource
111
+ );
112
+
113
+ Schema: [paste schema.json]
114
+ ```
115
+
116
+ Expected: a complete entity payload with principal outside the viewer role.
117
+
118
+ ### cedar_check_policy_change
119
+
120
+ ```
121
+ Can this policy change be applied in-place in Amazon Verified Permissions?
122
+
123
+ Old policy:
124
+ permit (
125
+ principal in DocMgmt::Role::"viewer",
126
+ action == DocMgmt::Action::"READ",
127
+ resource
128
+ );
129
+
130
+ New policy:
131
+ permit (
132
+ principal in DocMgmt::Role::"senior_viewer",
133
+ action == DocMgmt::Action::"READ",
134
+ resource
135
+ );
136
+ ```
137
+
138
+ Expected: **cannot update in-place** — the principal clause changed. AVP requires delete and recreate.
139
+
140
+ ---
141
+
142
+ ## Test cases
143
+
144
+ | Principal | Action | Resource | Expected | Reason |
145
+ |-----------|--------|----------|----------|--------|
146
+ | alice (admin) | READ | acquisition-details | **Allow** | Admin policy permits |
147
+ | alice (admin) | DELETE | acquisition-details | **Allow** | Admin exempt from top_secret forbid |
148
+ | bob (editor) | WRITE | roadmap-2026 | **Allow** | Editor can write |
149
+ | bob (editor) | DELETE | roadmap-2026 | **Deny** | Editor cannot delete |
150
+ | bob (editor) | READ | acquisition-details | **Deny** | top_secret forbid overrides editor permit |
151
+ | charlie (viewer) | READ | public-announcement | **Allow** | Viewer can read |
152
+ | charlie (viewer) | WRITE | public-announcement | **Deny** | Viewer cannot write |
153
+ | charlie (viewer) | READ | acquisition-details | **Deny** | top_secret forbid overrides viewer permit |
154
+ | dave (no role) | READ | public-announcement | **Deny** | Default deny — no matching permit |
155
+ | dave (no role) | READ | acquisition-details | **Deny** | Default deny + top_secret forbid |
156
+
157
+ ---
158
+
159
+ ## Common pitfalls in this pattern
160
+
161
+ **`forbid` overrides `permit` — always.** A single matching `forbid` blocks the request regardless of how many `permit` policies also match. If you add an admin `permit` and a top-secret `forbid`, the `unless { principal in Role::"admin" }` clause is what lets admins through — not some priority system.
162
+
163
+ **Default deny is not a policy.** Cedar denies by default when no `permit` matches. Dave gets denied not because of a `forbid` but because no policy grants him anything. These are different: a `forbid` appears in `diagnostics.reason`; a default deny leaves `determining_policies` empty.
164
+
165
+ **Role membership is transitive via `parents`.** `principal in DocMgmt::Role::"admin"` is true when the entity has `"admin"` anywhere in its parent chain — direct or inherited. If roles inherit from other roles, the `in` check follows the chain.
166
+
167
+ **Schema validation catches attribute typos silently at runtime.** If you access `resource.clasification` (one `s`) without schema validation, Cedar silently makes the policy inapplicable rather than erroring. Always validate against the schema during development.
@@ -0,0 +1,43 @@
1
+ [
2
+ {
3
+ "uid": { "type": "DocMgmt::User", "id": "alice" },
4
+ "attrs": { "name": "Alice", "email": "alice@example.com" },
5
+ "parents": [{ "type": "DocMgmt::Role", "id": "admin" }]
6
+ },
7
+ {
8
+ "uid": { "type": "DocMgmt::User", "id": "bob" },
9
+ "attrs": { "name": "Bob", "email": "bob@example.com" },
10
+ "parents": [{ "type": "DocMgmt::Role", "id": "editor" }]
11
+ },
12
+ {
13
+ "uid": { "type": "DocMgmt::User", "id": "charlie" },
14
+ "attrs": { "name": "Charlie", "email": "charlie@example.com" },
15
+ "parents": [{ "type": "DocMgmt::Role", "id": "viewer" }]
16
+ },
17
+ {
18
+ "uid": { "type": "DocMgmt::User", "id": "dave" },
19
+ "attrs": { "name": "Dave", "email": "dave@example.com" },
20
+ "parents": []
21
+ },
22
+ { "uid": { "type": "DocMgmt::Role", "id": "admin" }, "attrs": {}, "parents": [] },
23
+ { "uid": { "type": "DocMgmt::Role", "id": "editor" }, "attrs": {}, "parents": [] },
24
+ { "uid": { "type": "DocMgmt::Role", "id": "viewer" }, "attrs": {}, "parents": [] },
25
+ {
26
+ "uid": { "type": "DocMgmt::Document", "id": "roadmap-2026" },
27
+ "attrs": { "owner": "alice", "classification": "internal" },
28
+ "parents": [{ "type": "DocMgmt::Folder", "id": "strategy" }]
29
+ },
30
+ {
31
+ "uid": { "type": "DocMgmt::Document", "id": "public-announcement" },
32
+ "attrs": { "owner": "alice", "classification": "public" },
33
+ "parents": [{ "type": "DocMgmt::Folder", "id": "comms" }]
34
+ },
35
+ {
36
+ "uid": { "type": "DocMgmt::Document", "id": "acquisition-details" },
37
+ "attrs": { "owner": "alice", "classification": "top_secret" },
38
+ "parents": [{ "type": "DocMgmt::Folder", "id": "confidential" }]
39
+ },
40
+ { "uid": { "type": "DocMgmt::Folder", "id": "strategy" }, "attrs": {}, "parents": [] },
41
+ { "uid": { "type": "DocMgmt::Folder", "id": "comms" }, "attrs": {}, "parents": [] },
42
+ { "uid": { "type": "DocMgmt::Folder", "id": "confidential" }, "attrs": {}, "parents": [] }
43
+ ]
@@ -0,0 +1,6 @@
1
+ // Admins can do anything on any document
2
+ permit (
3
+ principal in DocMgmt::Role::"admin",
4
+ action,
5
+ resource
6
+ );
@@ -0,0 +1,6 @@
1
+ // Editors can read and write, but not delete
2
+ permit (
3
+ principal in DocMgmt::Role::"editor",
4
+ action in [DocMgmt::Action::"READ", DocMgmt::Action::"WRITE"],
5
+ resource
6
+ );
@@ -0,0 +1,13 @@
1
+ // Nobody can access top-secret documents except admins
2
+ // This forbid overrides all permit policies
3
+ forbid (
4
+ principal,
5
+ action,
6
+ resource
7
+ )
8
+ when {
9
+ resource.classification == "top_secret"
10
+ }
11
+ unless {
12
+ principal in DocMgmt::Role::"admin"
13
+ };
@@ -0,0 +1,6 @@
1
+ // Viewers can only read
2
+ permit (
3
+ principal in DocMgmt::Role::"viewer",
4
+ action == DocMgmt::Action::"READ",
5
+ resource
6
+ );
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Offline runner for the rbac-document-management example.
3
+ * Runs each cedar-mcp-server tool against the example files and prints results.
4
+ *
5
+ * Usage: npx tsx examples/rbac-document-management/run.ts
6
+ */
7
+
8
+ import { readFileSync } from "node:fs";
9
+ import { join } from "node:path";
10
+ import { handleAuthorize } from "../../src/tools/authorize.js";
11
+ import { handleValidate } from "../../src/tools/validate.js";
12
+ import { handleExplain } from "../../src/tools/explain.js";
13
+ import { handleGenerateSample } from "../../src/tools/generate-sample.js";
14
+
15
+ const dir = new URL(".", import.meta.url).pathname;
16
+ const read = (p: string) => readFileSync(join(dir, p), "utf8");
17
+
18
+ const schema = read("schema.json");
19
+ const entities = read("entities/users-and-docs.json");
20
+
21
+ const policies = [
22
+ read("policies/admin.cedar"),
23
+ read("policies/editor.cedar"),
24
+ read("policies/viewer.cedar"),
25
+ read("policies/top-secret-forbid.cedar"),
26
+ ].join("\n\n");
27
+
28
+ function section(title: string) {
29
+ console.log(`\n${"─".repeat(60)}`);
30
+ console.log(` ${title}`);
31
+ console.log("─".repeat(60));
32
+ }
33
+
34
+ // ─── cedar_validate ───────────────────────────────────────────────────────────
35
+
36
+ section("cedar_validate — all policies against schema");
37
+ const validateResult = await handleValidate({ policies, schema });
38
+ console.log(` valid: ${validateResult.valid}`);
39
+ console.log(` policies: ${validateResult.policy_count}`);
40
+ if (validateResult.errors.length > 0) {
41
+ console.log(" errors:", validateResult.errors);
42
+ }
43
+
44
+ // ─── cedar_authorize ──────────────────────────────────────────────────────────
45
+
46
+ section("cedar_authorize — 4 representative decisions");
47
+ const authCases = [
48
+ { label: "alice (admin) reads acquisition-details", principal: 'DocMgmt::User::"alice"', action: 'DocMgmt::Action::"READ"', resource: 'DocMgmt::Document::"acquisition-details"', expected: "Allow" },
49
+ { label: "bob (editor) writes roadmap-2026", principal: 'DocMgmt::User::"bob"', action: 'DocMgmt::Action::"WRITE"', resource: 'DocMgmt::Document::"roadmap-2026"', expected: "Allow" },
50
+ { label: "bob (editor) reads acquisition-details (top_secret forbid)", principal: 'DocMgmt::User::"bob"', action: 'DocMgmt::Action::"READ"', resource: 'DocMgmt::Document::"acquisition-details"', expected: "Deny" },
51
+ { label: "dave (no role) reads public-announcement (default deny)", principal: 'DocMgmt::User::"dave"', action: 'DocMgmt::Action::"READ"', resource: 'DocMgmt::Document::"public-announcement"', expected: "Deny" },
52
+ ];
53
+
54
+ for (const c of authCases) {
55
+ const r = await handleAuthorize({ policies, principal: c.principal, action: c.action, resource: c.resource, entities, schema });
56
+ const pass = r.decision === c.expected ? "✓" : "✗";
57
+ console.log(` ${pass} ${c.label}: ${r.decision}`);
58
+ if (r.determining_policies.length > 0) console.log(` determined by: ${r.determining_policies.join(", ")}`);
59
+ }
60
+
61
+ // ─── cedar_explain ────────────────────────────────────────────────────────────
62
+
63
+ section("cedar_explain — editor policy");
64
+ const explainResult = await handleExplain({ policy: read("policies/top-secret-forbid.cedar") });
65
+ console.log(` effect: ${explainResult.effect}`);
66
+ console.log(` summary: ${explainResult.summary}`);
67
+ console.log(` patterns: ${explainResult.patterns_detected.join(", ")}`);
68
+ console.log(` conditions:`);
69
+ for (const c of explainResult.conditions) {
70
+ console.log(` [${c.kind}] ${c.text}`);
71
+ }
72
+
73
+ // ─── cedar_generate_sample_request ───────────────────────────────────────────
74
+
75
+ section("cedar_generate_sample_request — allow request for editor policy");
76
+ const sampleResult = await handleGenerateSample({
77
+ policy: read("policies/editor.cedar"),
78
+ schema,
79
+ target_decision: "allow",
80
+ });
81
+ console.log(` decision: ${sampleResult.decision}`);
82
+ console.log(` principal: ${sampleResult.principal}`);
83
+ console.log(` action: ${sampleResult.action}`);
84
+ console.log(` resource: ${sampleResult.resource}`);
85
+ console.log(` explanation: ${sampleResult.explanation}`);
86
+
87
+ console.log("\n✓ All done.");
@@ -0,0 +1,57 @@
1
+ {
2
+ "DocMgmt": {
3
+ "entityTypes": {
4
+ "User": {
5
+ "memberOfTypes": ["Role"],
6
+ "shape": {
7
+ "type": "Record",
8
+ "attributes": {
9
+ "name": { "type": "String", "required": true },
10
+ "email": { "type": "String", "required": true }
11
+ }
12
+ }
13
+ },
14
+ "Role": {
15
+ "memberOfTypes": [],
16
+ "shape": { "type": "Record", "attributes": {} }
17
+ },
18
+ "Document": {
19
+ "memberOfTypes": ["Folder"],
20
+ "shape": {
21
+ "type": "Record",
22
+ "attributes": {
23
+ "owner": { "type": "String", "required": true },
24
+ "classification": { "type": "String", "required": true }
25
+ }
26
+ }
27
+ },
28
+ "Folder": {
29
+ "memberOfTypes": [],
30
+ "shape": { "type": "Record", "attributes": {} }
31
+ }
32
+ },
33
+ "actions": {
34
+ "READ": {
35
+ "appliesTo": {
36
+ "principalTypes": ["User"],
37
+ "resourceTypes": ["Document"],
38
+ "context": { "type": "Record", "attributes": {} }
39
+ }
40
+ },
41
+ "WRITE": {
42
+ "appliesTo": {
43
+ "principalTypes": ["User"],
44
+ "resourceTypes": ["Document"],
45
+ "context": { "type": "Record", "attributes": {} }
46
+ }
47
+ },
48
+ "DELETE": {
49
+ "appliesTo": {
50
+ "principalTypes": ["User"],
51
+ "resourceTypes": ["Document"],
52
+ "context": { "type": "Record", "attributes": {} }
53
+ }
54
+ }
55
+ }
56
+ }
57
+ }
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "cedar-mcp-server",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for Cedar policy language — validate, authorize, format, and translate Cedar policies directly in your AI assistant",
5
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "bin": {
8
+ "cedar-mcp-server": "./dist/index.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "dev": "tsx src/index.ts",
13
+ "test": "vitest run --exclude 'test/integration/**'",
14
+ "test:integration": "vitest run test/integration",
15
+ "test:watch": "vitest --exclude 'test/integration/**'"
16
+ },
17
+ "dependencies": {
18
+ "@cedar-policy/cedar-wasm": "4.11.0",
19
+ "@modelcontextprotocol/sdk": "^1.0.0",
20
+ "@types/express": "^5.0.6",
21
+ "express": "^5.2.1"
22
+ },
23
+ "devDependencies": {
24
+ "@types/node": "^22.0.0",
25
+ "fast-check": "^4.8.0",
26
+ "tsx": "^4.0.0",
27
+ "typescript": "^5.0.0",
28
+ "vitest": "^4.0.0"
29
+ },
30
+ "engines": {
31
+ "node": ">=20"
32
+ },
33
+ "keywords": [
34
+ "cedar",
35
+ "mcp",
36
+ "model-context-protocol",
37
+ "authorization",
38
+ "policy",
39
+ "aws",
40
+ "verified-permissions"
41
+ ],
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "git+https://github.com/Pigius/cedar-mcp-server.git"
45
+ },
46
+ "bugs": {
47
+ "url": "https://github.com/Pigius/cedar-mcp-server/issues"
48
+ },
49
+ "homepage": "https://github.com/Pigius/cedar-mcp-server#readme"
50
+ }
@@ -0,0 +1,239 @@
1
+ import { createServer as createHttpServer, type Server as HttpServer } from "node:http";
2
+ import { randomUUID } from "node:crypto";
3
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
4
+ import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
5
+ import express from "express";
6
+ import { createServer } from "./server.js";
7
+ import { storeManager } from "./resources/store-manager.js";
8
+
9
+ export interface HttpServerOptions {
10
+ port: number;
11
+ host?: string;
12
+ roots?: Array<{ name: string; path: string }>;
13
+ /** Max concurrent HTTP sessions. New sessions over the cap receive 503.
14
+ * Default: 100. Override via env CEDAR_MAX_HTTP_SESSIONS or this option. */
15
+ maxSessions?: number;
16
+ /** Idle session TTL in milliseconds. A session unused for longer is evicted
17
+ * by the reaper. Default: 30 minutes. Override via env
18
+ * CEDAR_HTTP_SESSION_IDLE_TTL_MS or this option. */
19
+ sessionIdleTtlMs?: number;
20
+ /** How often the reaper scans for stale sessions. Default: 60 seconds.
21
+ * Mostly relevant for tests; production deploys can leave the default. */
22
+ reaperIntervalMs?: number;
23
+ }
24
+
25
+ export interface RunningHttpServer {
26
+ httpServer: HttpServer;
27
+ port: number;
28
+ host: string;
29
+ close(): Promise<void>;
30
+ }
31
+
32
+ interface Session {
33
+ transport: StreamableHTTPServerTransport;
34
+ server: Awaited<ReturnType<typeof createServer>>;
35
+ /** Wall-clock timestamp of the last request observed on this session.
36
+ * Used by the reaper to evict idle sessions. */
37
+ lastActiveAt: number;
38
+ }
39
+
40
+ const DEFAULT_MAX_SESSIONS = 100;
41
+ const DEFAULT_SESSION_IDLE_TTL_MS = 30 * 60 * 1000; // 30 min
42
+ const DEFAULT_REAPER_INTERVAL_MS = 60 * 1000; // 1 min
43
+
44
+ /**
45
+ * Boot cedar-mcp-server in Streamable HTTP mode.
46
+ *
47
+ * Per-session model: each MCP session gets its own McpServer + transport pair.
48
+ * The Streamable HTTP spec mandates this because each session has independent
49
+ * protocol state (initialized handshake, message history, capabilities).
50
+ *
51
+ * Shared across all sessions: the storeManager singleton. The deployment model
52
+ * is "one server per policy-store set, many team clients all seeing the same
53
+ * roots." Roots are deployer-configured via CLI flags at startup; client
54
+ * listRoots() is NOT called in HTTP mode. For per-tenant isolation, deploy
55
+ * multiple processes.
56
+ *
57
+ * Resource management:
58
+ * - Max-sessions cap: new sessions over the limit receive HTTP 503. This is
59
+ * backpressure, not eviction — existing sessions are not interrupted to
60
+ * make room. Default 100; override via maxSessions option or
61
+ * CEDAR_MAX_HTTP_SESSIONS env var.
62
+ * - Idle TTL: a reaper scans periodically and evicts sessions whose last
63
+ * observed request exceeds the TTL. Default 30 min idle / 60s scan;
64
+ * override via sessionIdleTtlMs / reaperIntervalMs options or env vars.
65
+ * This catches the case where transport.onclose doesn't fire (e.g.,
66
+ * network partition, TCP RST) and prevents the sessions map from leaking.
67
+ *
68
+ * Session lifecycle:
69
+ * 1. Client POSTs to /mcp without Mcp-Session-Id → server creates a new
70
+ * session (transport + server pair), runs initialize, returns the
71
+ * session ID in the response header. New sessions over maxSessions are
72
+ * rejected with HTTP 503.
73
+ * 2. Subsequent requests with that Mcp-Session-Id route to the same pair
74
+ * and refresh its lastActiveAt timestamp.
75
+ * 3. transport.onclose fires on graceful disconnect → session removed.
76
+ * 4. Reaper sweeps idle sessions out periodically as a backstop.
77
+ */
78
+ export async function startHttpServer(options: HttpServerOptions): Promise<RunningHttpServer> {
79
+ const host = options.host ?? "127.0.0.1";
80
+ const maxSessions = options.maxSessions
81
+ ?? (Number(process.env.CEDAR_MAX_HTTP_SESSIONS) || DEFAULT_MAX_SESSIONS);
82
+ const sessionIdleTtlMs = options.sessionIdleTtlMs
83
+ ?? (Number(process.env.CEDAR_HTTP_SESSION_IDLE_TTL_MS) || DEFAULT_SESSION_IDLE_TTL_MS);
84
+ const reaperIntervalMs = options.reaperIntervalMs ?? DEFAULT_REAPER_INTERVAL_MS;
85
+
86
+ // Load deployer-configured roots before the server starts accepting traffic.
87
+ // Shared by all sessions via the storeManager singleton.
88
+ if (options.roots && options.roots.length > 0) {
89
+ storeManager.loadFromRoots(
90
+ options.roots.map((r) => ({ uri: `file://${r.path}`, name: r.name }))
91
+ );
92
+ }
93
+
94
+ const sessions = new Map<string, Session>();
95
+
96
+ async function createSession(): Promise<Session> {
97
+ const transport = new StreamableHTTPServerTransport({
98
+ sessionIdGenerator: () => randomUUID(),
99
+ });
100
+ const server = createServer();
101
+ await server.connect(transport);
102
+
103
+ transport.onclose = () => {
104
+ // Only drop the session-map entry here. Do NOT call server.close():
105
+ // server.close() triggers transport close, which fires this handler
106
+ // again. The McpServer becomes unreferenced and gets GC'd. The shared
107
+ // WASM module lives at process scope, not per-session.
108
+ const sid = transport.sessionId;
109
+ if (sid && sessions.has(sid)) {
110
+ sessions.delete(sid);
111
+ }
112
+ };
113
+
114
+ return { transport, server, lastActiveAt: Date.now() };
115
+ }
116
+
117
+ // Reaper: evict sessions whose lastActiveAt is older than the idle TTL.
118
+ // Collect-then-delete pattern avoids mid-iteration map mutation when the
119
+ // transport.close() callback re-enters the sessions map.
120
+ const reaper = setInterval(() => {
121
+ const cutoff = Date.now() - sessionIdleTtlMs;
122
+ const toEvict: string[] = [];
123
+ for (const [sid, sess] of sessions.entries()) {
124
+ if (sess.lastActiveAt < cutoff) toEvict.push(sid);
125
+ }
126
+ for (const sid of toEvict) {
127
+ const sess = sessions.get(sid);
128
+ if (sess) {
129
+ sessions.delete(sid);
130
+ void sess.transport.close().catch(() => { /* ignore */ });
131
+ }
132
+ }
133
+ }, reaperIntervalMs);
134
+ // Don't keep the Node event loop alive just for the reaper — let the
135
+ // process exit cleanly when the HTTP server closes.
136
+ reaper.unref();
137
+
138
+ const app = createMcpExpressApp({ host });
139
+ app.use(express.json({ limit: "10mb" }));
140
+
141
+ app.post("/mcp", async (req, res) => {
142
+ try {
143
+ const sessionIdHeader = req.headers["mcp-session-id"];
144
+ const sessionId = Array.isArray(sessionIdHeader) ? sessionIdHeader[0] : sessionIdHeader;
145
+
146
+ let session: Session | undefined;
147
+ if (sessionId && sessions.has(sessionId)) {
148
+ session = sessions.get(sessionId);
149
+ if (session) session.lastActiveAt = Date.now();
150
+ } else {
151
+ // New session — apply backpressure if at cap. We do this BEFORE
152
+ // creating the transport/server pair to avoid leaking resources
153
+ // on rejection.
154
+ if (sessions.size >= maxSessions) {
155
+ res.status(503).json({
156
+ error: "Too many concurrent MCP sessions",
157
+ message: `Server is at the max-session limit of ${maxSessions}. Try again later, or run multiple processes for higher capacity.`,
158
+ active_sessions: sessions.size,
159
+ max_sessions: maxSessions,
160
+ });
161
+ return;
162
+ }
163
+ session = await createSession();
164
+ }
165
+
166
+ if (!session) {
167
+ res.status(400).json({ error: "Could not establish MCP session" });
168
+ return;
169
+ }
170
+
171
+ await session.transport.handleRequest(req, res, req.body);
172
+
173
+ // After the initialize request completes the transport will have set
174
+ // its sessionId. Register the session under that ID so subsequent
175
+ // requests find it.
176
+ const sidAfter = session.transport.sessionId;
177
+ if (sidAfter && !sessions.has(sidAfter)) {
178
+ sessions.set(sidAfter, session);
179
+ }
180
+ } catch (e) {
181
+ if (!res.headersSent) {
182
+ res.status(500).json({
183
+ error: "Internal MCP transport error",
184
+ message: e instanceof Error ? e.message : String(e),
185
+ });
186
+ }
187
+ }
188
+ });
189
+
190
+ app.get("/health", (_req, res) => {
191
+ res.json({
192
+ status: "ok",
193
+ transport: "streamable-http",
194
+ mode: "stateful",
195
+ active_sessions: sessions.size,
196
+ max_sessions: maxSessions,
197
+ session_idle_ttl_ms: sessionIdleTtlMs,
198
+ });
199
+ });
200
+
201
+ const httpServer = createHttpServer(app);
202
+
203
+ await new Promise<void>((resolve, reject) => {
204
+ httpServer.once("error", reject);
205
+ httpServer.listen(options.port, host, () => {
206
+ httpServer.off("error", reject);
207
+ resolve();
208
+ });
209
+ });
210
+
211
+ // eslint-disable-next-line no-console
212
+ console.error(`[cedar-mcp-server] Streamable HTTP listening on http://${host}:${options.port}/mcp (max_sessions=${maxSessions}, idle_ttl=${Math.round(sessionIdleTtlMs / 1000)}s)`);
213
+ if (options.roots && options.roots.length > 0) {
214
+ // eslint-disable-next-line no-console
215
+ console.error(`[cedar-mcp-server] Loaded ${options.roots.length} root(s): ${options.roots.map((r) => r.name).join(", ")}`);
216
+ } else {
217
+ // eslint-disable-next-line no-console
218
+ console.error("[cedar-mcp-server] WARNING: no --root flags supplied; tools that depend on a configured store will error.");
219
+ }
220
+
221
+ return {
222
+ httpServer,
223
+ port: options.port,
224
+ host,
225
+ async close() {
226
+ clearInterval(reaper);
227
+ // Close all sessions first so each McpServer cleans up its WASM state
228
+ for (const session of sessions.values()) {
229
+ try {
230
+ await session.server.close();
231
+ } catch { /* ignore */ }
232
+ }
233
+ sessions.clear();
234
+ await new Promise<void>((resolve, reject) => {
235
+ httpServer.close((err) => (err ? reject(err) : resolve()));
236
+ });
237
+ },
238
+ };
239
+ }