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,217 @@
1
+ /**
2
+ * MCP Prompt definitions for cedar-mcp-server.
3
+ *
4
+ * Registration in src/server.ts:
5
+ * import { PROMPT_DEFINITIONS } from "./prompts/index.js";
6
+ * for (const p of PROMPT_DEFINITIONS) {
7
+ * server.prompt(p.name, p.description, p.argsSchema, p.handler);
8
+ * }
9
+ *
10
+ * Each PromptDefinition is built via definePrompt(), which captures the Zod
11
+ * raw shape generically so TypeScript preserves the concrete arg types through
12
+ * the handler. The array is typed as PromptDefinition<ZodRawShape> for the
13
+ * export; the SDK's server.prompt() overload accepts the same shape.
14
+ */
15
+
16
+ import { z } from "zod";
17
+ import type { GetPromptResult } from "@modelcontextprotocol/sdk/types.js";
18
+ import type { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js";
19
+ import type { ServerRequest, ServerNotification } from "@modelcontextprotocol/sdk/types.js";
20
+
21
+ type ZodRawShape = Record<string, z.ZodTypeAny>;
22
+ type ShapeOutput<S extends ZodRawShape> = { [K in keyof S]: z.infer<S[K]> };
23
+ type PromptExtra = RequestHandlerExtra<ServerRequest, ServerNotification>;
24
+
25
+ /**
26
+ * PromptDefinition is intentionally generic so handler arg types are preserved
27
+ * internally. At the export boundary the array is cast to the widened union
28
+ * via definePrompt() which uses `as unknown as` once, in one place.
29
+ */
30
+ export interface PromptDefinition<Args extends ZodRawShape = ZodRawShape> {
31
+ name: string;
32
+ description: string;
33
+ argsSchema: Args;
34
+ handler: (args: ShapeOutput<Args>, extra: PromptExtra) => GetPromptResult | Promise<GetPromptResult>;
35
+ }
36
+
37
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
38
+ type AnyPromptDefinition = PromptDefinition<any>;
39
+
40
+ function definePrompt<Args extends ZodRawShape>(
41
+ def: PromptDefinition<Args>
42
+ ): AnyPromptDefinition {
43
+ return def as AnyPromptDefinition;
44
+ }
45
+
46
+ // ---------------------------------------------------------------------------
47
+ // 1. cedar-review-policy-diff
48
+ //
49
+ // UX assumption: the user has two named policy stores ("blue" = production,
50
+ // "green" = candidate) and wants a structured review before promoting. They
51
+ // expect the prompt to drive all Cedar tool calls and produce a clear verdict.
52
+ //
53
+ // Falsifiability check: would this be useless? Yes, if it skipped the schema
54
+ // diff step. A promoter who misses schema diff will hit breaking changes only
55
+ // at query time. Fixed: schema diff is an explicit step with BREAKING/
56
+ // NON-BREAKING classification.
57
+ // ---------------------------------------------------------------------------
58
+ const reviewPolicyDiff = definePrompt({
59
+ name: "cedar-review-policy-diff",
60
+ description:
61
+ "Drive a structured Cedar policy store promotion review: diff policies, diff schema, summarize breaking risk, and produce a go/no-go recommendation.",
62
+ argsSchema: {
63
+ blue_store: z.string().describe("Name of the baseline (production) policy store"),
64
+ green_store: z.string().describe("Name of the candidate policy store to review"),
65
+ focus: z
66
+ .string()
67
+ .optional()
68
+ .describe(
69
+ "Optional: narrows review attention, e.g. 'AVP immutability' or 'forbid rules only'"
70
+ ),
71
+ },
72
+ handler: (args) => {
73
+ const focusNote = args.focus
74
+ ? `\n\nFocus area for this review: ${args.focus}. Apply extra scrutiny to anything touching that area.`
75
+ : "";
76
+
77
+ const text =
78
+ `You are reviewing a Cedar policy store promotion from "${args.blue_store}" (baseline) to "${args.green_store}" (candidate).` +
79
+ `${focusNote}` +
80
+ `\n\nWork through the following steps in order.\n\n` +
81
+ `Step 1: Structural diff.\n` +
82
+ `Call cedar_diff_policy_stores with blue="${args.blue_store}" and green="${args.green_store}". ` +
83
+ `List every added, removed, and modified policy. Note the policy IDs and whether each change is additive or restrictive.\n\n` +
84
+ `Step 2: Schema diff.\n` +
85
+ `Call cedar_diff_schema with schemas from both stores (cedar://schema/${args.blue_store} and cedar://schema/${args.green_store}). ` +
86
+ `If the diff is non-trivial (any entity type added, removed, or attribute changed), classify each change as BREAKING or NON-BREAKING. ` +
87
+ `A schema change is BREAKING if existing valid Cedar requests could become invalid or if attribute types narrow.\n\n` +
88
+ `Step 3: Behavioral drift summary.\n` +
89
+ `If behavioral_test_requests are available in either store, note any decisions that differ between blue and green. ` +
90
+ `Flag any new Deny decisions that did not exist in blue as HIGH risk.\n\n` +
91
+ `Step 4: Recommendation.\n` +
92
+ `Produce a plain-English summary covering: (a) structural changes, (b) breaking schema risk items, ` +
93
+ `(c) behavioral drift if present, and (d) a clear PROMOTE / DO NOT PROMOTE / PROMOTE WITH CAUTION verdict with reasoning. ` +
94
+ `No marketing language. Use semicolons instead of dashes for lists. Be specific about which policy IDs drive the verdict.`;
95
+
96
+ return {
97
+ messages: [{ role: "user" as const, content: { type: "text" as const, text } }],
98
+ };
99
+ },
100
+ });
101
+
102
+ // ---------------------------------------------------------------------------
103
+ // 2. cedar-explain-denial
104
+ //
105
+ // UX assumption: a developer got an unexpected Deny (or Allow) and wants a
106
+ // plain-English explanation without needing to know which tools to call or
107
+ // in what order.
108
+ // ---------------------------------------------------------------------------
109
+ const explainDenial = definePrompt({
110
+ name: "cedar-explain-denial",
111
+ description:
112
+ "Explain why a Cedar authorization request was allowed or denied, in plain English, by evaluating the request and examining the deciding policies.",
113
+ argsSchema: {
114
+ principal: z.string().describe('Principal entity reference, e.g. MyApp::User::"alice"'),
115
+ action: z.string().describe('Action entity reference, e.g. MyApp::Action::"read"'),
116
+ resource: z.string().describe('Resource entity reference, e.g. MyApp::Document::"doc-1"'),
117
+ store: z.string().describe("Name of the policy store to evaluate against"),
118
+ },
119
+ handler: (args) => {
120
+ const text =
121
+ `Explain why the following Cedar authorization request was decided the way it was.\n\n` +
122
+ `Principal: ${args.principal}\n` +
123
+ `Action: ${args.action}\n` +
124
+ `Resource: ${args.resource}\n` +
125
+ `Store: ${args.store}\n\n` +
126
+ `Work through the following steps.\n\n` +
127
+ `Step 1: Evaluate.\n` +
128
+ `Call cedar_authorize with:\n` +
129
+ ` policy_ref = "cedar://policies/${args.store}"\n` +
130
+ ` schema_ref = "cedar://schema/${args.store}"\n` +
131
+ ` principal = "${args.principal}"\n` +
132
+ ` action = "${args.action}"\n` +
133
+ ` resource = "${args.resource}"\n` +
134
+ ` entities = load from "cedar://entities/${args.store}" if available, otherwise use []\n\n` +
135
+ `Step 2: Explain deciding policies.\n` +
136
+ `Take the policy IDs returned in determining_policies from Step 1. ` +
137
+ `Call cedar_explain on those policy IDs so you have the human-readable logic for each one.\n\n` +
138
+ `Step 3: Plain-English explanation.\n` +
139
+ `Write a clear explanation covering:\n` +
140
+ ` (a) The decision (Allow or Deny) and which policy or policies drove it.\n` +
141
+ ` (b) Why this principal, action, and resource matched or did not match each determining policy.\n` +
142
+ ` (c) What would need to change for the opposite decision: either a policy edit, an entity attribute change, or a context value.\n\n` +
143
+ `Keep the explanation factual and specific. Avoid jargon beyond standard Cedar terms (permit, forbid, principal, action, resource, context).`;
144
+
145
+ return {
146
+ messages: [{ role: "user" as const, content: { type: "text" as const, text } }],
147
+ };
148
+ },
149
+ });
150
+
151
+ // ---------------------------------------------------------------------------
152
+ // 3. cedar-avp-migration-checklist
153
+ //
154
+ // UX assumption: the user is preparing to move a local Cedar policy set into
155
+ // Amazon Verified Permissions and wants a guided checklist to avoid easy-to-miss
156
+ // steps (schema format, single-namespace constraint, entity validation).
157
+ // This is purely informational; no tool calls are issued inside the prompt body.
158
+ //
159
+ // Design for "no required args": namespace is optional. When supplied it is
160
+ // substituted into the checklist where AVP requires a single namespace. When
161
+ // omitted a placeholder is used so the checklist is still complete and actionable.
162
+ // ---------------------------------------------------------------------------
163
+ const avpMigrationChecklist = definePrompt({
164
+ name: "cedar-avp-migration-checklist",
165
+ description:
166
+ "Provide a guided checklist for migrating a local Cedar policy set into Amazon Verified Permissions, covering schema format, namespace constraints, entity validation, and behavioral diff.",
167
+ argsSchema: {
168
+ namespace: z
169
+ .string()
170
+ .optional()
171
+ .describe(
172
+ "Optional: the single Cedar namespace your AVP policy store will use, e.g. MyApp"
173
+ ),
174
+ },
175
+ handler: (args) => {
176
+ const ns = args.namespace ?? "<YourNamespace>";
177
+
178
+ const text =
179
+ `AVP migration checklist for namespace: ${ns}\n\n` +
180
+ `Work through each item before moving policies or schema into Amazon Verified Permissions.\n\n` +
181
+ `1. Schema format detection.\n` +
182
+ `Call cedar_validate_schema on your existing schema file. Note whether it is in Cedar JSON format or .cedarschema (human-readable) format. ` +
183
+ `AVP accepts both, but they have different upload paths. Confirm which format you have before proceeding.\n\n` +
184
+ `2. Single-namespace constraint.\n` +
185
+ `AVP enforces a single namespace per policy store. All entity types, actions, and attributes must live under "${ns}". ` +
186
+ `If your local Cedar schema uses multiple namespaces, flatten them now. cedar_validate_schema will surface any namespace collisions.\n\n` +
187
+ `3. Entity format auto-detection.\n` +
188
+ `Cedar WASM accepts a relaxed entity format; AVP is stricter. Run cedar_validate_entities against your entity set with the schema attached. ` +
189
+ `Fix any entities missing required attributes or using incorrect UID formats before upload.\n\n` +
190
+ `4. Template-linked policies.\n` +
191
+ `If you use policy templates, call cedar_link_template for each template-principal-resource combination to confirm links are valid. ` +
192
+ `AVP supports template-linked policies but each link must reference a template that already exists in the store.\n\n` +
193
+ `5. Schema diff before PutSchema.\n` +
194
+ `If you are updating an existing AVP store schema (not a fresh store), call cedar_diff_schema between your local schema and the current AVP schema. ` +
195
+ `Classify every change as BREAKING or NON-BREAKING before calling PutSchema. ` +
196
+ `AVP does not roll back schema changes automatically; a BREAKING change can silently invalidate existing policies.\n\n` +
197
+ `6. Behavioral diff before traffic shift.\n` +
198
+ `After loading policies into the AVP store, use cedar_diff_policy_stores to compare local (blue) with AVP (green). ` +
199
+ `Run any behavioral test cases you have. Confirm no new Deny decisions appear for requests that should be allowed. ` +
200
+ `Only shift production traffic after this step passes.\n\n` +
201
+ `All steps must pass before tagging the migration as complete.`;
202
+
203
+ return {
204
+ messages: [{ role: "user" as const, content: { type: "text" as const, text } }],
205
+ };
206
+ },
207
+ });
208
+
209
+ // ---------------------------------------------------------------------------
210
+ // Export
211
+ // ---------------------------------------------------------------------------
212
+
213
+ export const PROMPT_DEFINITIONS: AnyPromptDefinition[] = [
214
+ reviewPolicyDiff,
215
+ explainDenial,
216
+ avpMigrationChecklist,
217
+ ];
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Resolves cedar:// resource references to their content.
3
+ * Allows tools to accept policy_ref / schema_ref as alternatives to inline text.
4
+ *
5
+ * URI patterns:
6
+ * cedar://policies/{store} → all policies in store concatenated
7
+ * cedar://policies/{store}/{id} → single policy content
8
+ * cedar://schema/{store} → schema content
9
+ * cedar://templates/{store} → template ID list as JSON array
10
+ * cedar://templates/{store}/{template_id} → single template content
11
+ * cedar://template-links/{store} → link ID list as JSON array
12
+ * cedar://template-links/{store}/{link_id}→ single template-link JSON content
13
+ * cedar://entities/{store} → merged entity arrays as JSON
14
+ * cedar://entities/{store}/{file_id} → single entity file content
15
+ */
16
+
17
+ import { storeManager } from "./store-manager.js";
18
+
19
+ export type RefResolution =
20
+ | { content: string; resolved_from: string }
21
+ | { error: string };
22
+
23
+ export function resolveRef(ref: string): RefResolution {
24
+ const match = ref.match(/^cedar:\/\/(.+)$/);
25
+ if (!match) return { error: `Invalid cedar:// reference: "${ref}"` };
26
+
27
+ const path = match[1]!;
28
+
29
+ // cedar://schema/{store}
30
+ const schemaMatch = path.match(/^schema\/([^/]+)$/);
31
+ if (schemaMatch) {
32
+ const storeName = schemaMatch[1]!;
33
+ try {
34
+ return { content: storeManager.readSchema(storeName), resolved_from: ref };
35
+ } catch (e) {
36
+ return { error: e instanceof Error ? e.message : String(e) };
37
+ }
38
+ }
39
+
40
+ // cedar://policies/{store}/{policy_id}
41
+ const singlePolicyMatch = path.match(/^policies\/([^/]+)\/([^/]+)$/);
42
+ if (singlePolicyMatch) {
43
+ const storeName = singlePolicyMatch[1]!;
44
+ const policyId = singlePolicyMatch[2]!;
45
+ try {
46
+ return { content: storeManager.readPolicy(storeName, policyId), resolved_from: ref };
47
+ } catch (e) {
48
+ return { error: e instanceof Error ? e.message : String(e) };
49
+ }
50
+ }
51
+
52
+ // cedar://policies/{store} — all policies concatenated
53
+ const allPoliciesMatch = path.match(/^policies\/([^/]+)$/);
54
+ if (allPoliciesMatch) {
55
+ const storeName = allPoliciesMatch[1]!;
56
+ try {
57
+ return { content: storeManager.readAllPolicies(storeName), resolved_from: ref };
58
+ } catch (e) {
59
+ return { error: e instanceof Error ? e.message : String(e) };
60
+ }
61
+ }
62
+
63
+ // cedar://templates/{store}/{template_id}
64
+ const singleTemplateMatch = path.match(/^templates\/([^/]+)\/([^/]+)$/);
65
+ if (singleTemplateMatch) {
66
+ const storeName = singleTemplateMatch[1]!;
67
+ const templateId = singleTemplateMatch[2]!;
68
+ try {
69
+ return { content: storeManager.readTemplate(storeName, templateId), resolved_from: ref };
70
+ } catch (e) {
71
+ return { error: e instanceof Error ? e.message : String(e) };
72
+ }
73
+ }
74
+
75
+ // cedar://templates/{store} — template ID list as JSON
76
+ const allTemplatesMatch = path.match(/^templates\/([^/]+)$/);
77
+ if (allTemplatesMatch) {
78
+ const storeName = allTemplatesMatch[1]!;
79
+ try {
80
+ return { content: JSON.stringify(storeManager.listTemplates(storeName)), resolved_from: ref };
81
+ } catch (e) {
82
+ return { error: e instanceof Error ? e.message : String(e) };
83
+ }
84
+ }
85
+
86
+ // cedar://template-links/{store}/{link_id}
87
+ const singleLinkMatch = path.match(/^template-links\/([^/]+)\/([^/]+)$/);
88
+ if (singleLinkMatch) {
89
+ const storeName = singleLinkMatch[1]!;
90
+ const linkId = singleLinkMatch[2]!;
91
+ try {
92
+ const link = storeManager.readTemplateLink(storeName, linkId);
93
+ return { content: JSON.stringify(link), resolved_from: ref };
94
+ } catch (e) {
95
+ return { error: e instanceof Error ? e.message : String(e) };
96
+ }
97
+ }
98
+
99
+ // cedar://template-links/{store} — link ID list as JSON
100
+ const allLinksMatch = path.match(/^template-links\/([^/]+)$/);
101
+ if (allLinksMatch) {
102
+ const storeName = allLinksMatch[1]!;
103
+ try {
104
+ return { content: JSON.stringify(storeManager.listTemplateLinks(storeName)), resolved_from: ref };
105
+ } catch (e) {
106
+ return { error: e instanceof Error ? e.message : String(e) };
107
+ }
108
+ }
109
+
110
+ // cedar://entities/{store}/{file_id}
111
+ const singleEntityMatch = path.match(/^entities\/([^/]+)\/([^/]+)$/);
112
+ if (singleEntityMatch) {
113
+ const storeName = singleEntityMatch[1]!;
114
+ const fileId = singleEntityMatch[2]!;
115
+ try {
116
+ return { content: storeManager.readEntities(storeName, fileId), resolved_from: ref };
117
+ } catch (e) {
118
+ return { error: e instanceof Error ? e.message : String(e) };
119
+ }
120
+ }
121
+
122
+ // cedar://entities/{store} — merged entity arrays as JSON
123
+ const allEntitiesMatch = path.match(/^entities\/([^/]+)$/);
124
+ if (allEntitiesMatch) {
125
+ const storeName = allEntitiesMatch[1]!;
126
+ try {
127
+ return { content: storeManager.readAllEntities(storeName), resolved_from: ref };
128
+ } catch (e) {
129
+ return { error: e instanceof Error ? e.message : String(e) };
130
+ }
131
+ }
132
+
133
+ return { error: `Unrecognized cedar:// URI pattern: "${ref}". Supported: cedar://policies/{store}[/{id}], cedar://schema/{store}, cedar://templates/{store}[/{id}], cedar://template-links/{store}[/{id}], cedar://entities/{store}[/{id}]` };
134
+ }
@@ -0,0 +1,248 @@
1
+ /**
2
+ * StoreManager maps MCP root URIs to named Cedar policy stores.
3
+ *
4
+ * Convention for a policy store directory:
5
+ * <root>/
6
+ * policies/ ← .cedar files, one per policy
7
+ * schema.cedarschema ← Cedar schema text (preferred)
8
+ * schema.json ← Cedar JSON schema (fallback)
9
+ *
10
+ * Security: isPathAllowed() checks that any file access stays within a loaded root.
11
+ * The SDK does not enforce roots automatically — every file operation calls this check.
12
+ */
13
+
14
+ import { readdirSync, readFileSync, existsSync } from "node:fs";
15
+ import { join, basename } from "node:path";
16
+
17
+ export interface PolicyStore {
18
+ name: string;
19
+ uri: string;
20
+ path: string;
21
+ }
22
+
23
+ export class StoreManager {
24
+ private stores = new Map<string, PolicyStore>();
25
+
26
+ // ─── Store lifecycle ────────────────────────────────────────────────────────
27
+
28
+ loadFromRoots(roots: Array<{ uri: string; name?: string }>): void {
29
+ this.stores.clear();
30
+ const usedNames = new Map<string, number>(); // tracks how many times each base name is used
31
+
32
+ for (const root of roots) {
33
+ if (!root.uri.startsWith("file://")) {
34
+ console.error(`[cedar-mcp-server] Skipping unsupported root URI scheme: ${root.uri} (only file:// is supported)`);
35
+ continue;
36
+ }
37
+ const rawPath = root.uri.replace(/^file:\/\//, "").replace(/\/$/, "");
38
+ // Security: refuse to load a root that resolves to an empty filesystem
39
+ // path (uri === "file:///" or similar). isPathAllowed below uses
40
+ // `startsWith(store.path)`, and `<anything>.startsWith("")` is true,
41
+ // so an empty-path store would silently let every Cedar file operation
42
+ // touch any path on the filesystem. The fix is defense-in-depth: this
43
+ // skips at the StoreManager boundary regardless of which caller
44
+ // (cwd-fallback, --root flag, or client listRoots) produced the URI.
45
+ if (rawPath.length === 0) {
46
+ console.error(`[cedar-mcp-server] Refusing to load root with empty path after URI normalization (uri: "${root.uri}"). Filesystem-root URIs (file:///) are unsafe — they would bypass the per-store path sandbox.`);
47
+ continue;
48
+ }
49
+ const baseName = root.name ?? basename(rawPath) ?? "default";
50
+
51
+ // Disambiguate collisions with a numeric suffix
52
+ const count = usedNames.get(baseName) ?? 0;
53
+ usedNames.set(baseName, count + 1);
54
+ const name = count === 0 ? baseName : `${baseName}-${count + 1}`;
55
+
56
+ if (count > 0) {
57
+ console.error(`[cedar-mcp-server] Store name collision: "${baseName}" is used by multiple roots. Renamed to "${name}". Consider giving roots explicit names.`);
58
+ }
59
+
60
+ this.stores.set(name, { name, uri: root.uri, path: rawPath });
61
+ }
62
+ }
63
+
64
+ listStoreNames(): string[] {
65
+ return [...this.stores.keys()];
66
+ }
67
+
68
+ getStore(name: string): PolicyStore | undefined {
69
+ return this.stores.get(name);
70
+ }
71
+
72
+ /**
73
+ * Workspace auto-discovery (10d) helper. Returns:
74
+ * - { kind: "none" } when no stores are loaded.
75
+ * - { kind: "single", ... } when exactly one store is loaded.
76
+ * - { kind: "ambiguous", names } when multiple stores are loaded and the
77
+ * caller did not pass an explicit `store` name to disambiguate.
78
+ *
79
+ * Tools call this when a required input ref is missing. Single-store
80
+ * deployments resolve cleanly; multi-store deployments surface an
81
+ * actionable error listing the candidates rather than guessing.
82
+ */
83
+ getDefaultStore(): { kind: "none" } | { kind: "single"; store: PolicyStore } | { kind: "ambiguous"; names: string[] } {
84
+ const names = this.listStoreNames();
85
+ if (names.length === 0) return { kind: "none" };
86
+ if (names.length === 1) return { kind: "single", store: this.stores.get(names[0]!)! };
87
+ return { kind: "ambiguous", names };
88
+ }
89
+
90
+ // ─── Policy access ──────────────────────────────────────────────────────────
91
+
92
+ listPolicies(storeName: string): string[] {
93
+ const store = this.requireStore(storeName);
94
+ const policiesDir = join(store.path, "policies");
95
+ if (!existsSync(policiesDir)) return [];
96
+ return readdirSync(policiesDir)
97
+ .filter((f) => f.endsWith(".cedar"))
98
+ .map((f) => f.replace(/\.cedar$/, ""))
99
+ .sort();
100
+ }
101
+
102
+ readPolicy(storeName: string, policyId: string): string {
103
+ const store = this.requireStore(storeName);
104
+ // Prevent path traversal — policy IDs must be simple filenames with no slashes or dots
105
+ if (!/^[a-zA-Z0-9_-]+$/.test(policyId)) {
106
+ throw new Error(`Invalid policy ID: "${policyId}". Policy IDs must contain only letters, digits, hyphens, and underscores.`);
107
+ }
108
+ const filePath = join(store.path, "policies", `${policyId}.cedar`);
109
+ if (!existsSync(filePath)) {
110
+ throw new Error(`Policy not found: "${policyId}" in store "${storeName}"`);
111
+ }
112
+ return readFileSync(filePath, "utf8");
113
+ }
114
+
115
+ readAllPolicies(storeName: string): string {
116
+ const ids = this.listPolicies(storeName);
117
+ return ids.map((id) => this.readPolicy(storeName, id)).join("\n\n");
118
+ }
119
+
120
+ // ─── Template access ────────────────────────────────────────────────────────
121
+
122
+ listTemplates(storeName: string): string[] {
123
+ const store = this.requireStore(storeName);
124
+ const templatesDir = join(store.path, "templates");
125
+ if (!existsSync(templatesDir)) return [];
126
+ return readdirSync(templatesDir)
127
+ .filter((f) => f.endsWith(".cedar"))
128
+ .map((f) => f.replace(/\.cedar$/, ""))
129
+ .sort();
130
+ }
131
+
132
+ readTemplate(storeName: string, templateId: string): string {
133
+ const store = this.requireStore(storeName);
134
+ if (!/^[a-zA-Z0-9_-]+$/.test(templateId)) {
135
+ throw new Error(`Invalid template ID: "${templateId}". Template IDs must contain only letters, digits, hyphens, and underscores.`);
136
+ }
137
+ const filePath = join(store.path, "templates", `${templateId}.cedar`);
138
+ if (!existsSync(filePath)) {
139
+ throw new Error(`Template not found: "${templateId}" in store "${storeName}"`);
140
+ }
141
+ return readFileSync(filePath, "utf8");
142
+ }
143
+
144
+ // ─── Template link access ────────────────────────────────────────────────────
145
+
146
+ listTemplateLinks(storeName: string): string[] {
147
+ const store = this.requireStore(storeName);
148
+ const linksDir = join(store.path, "template-links");
149
+ if (!existsSync(linksDir)) return [];
150
+ return readdirSync(linksDir)
151
+ .filter((f) => f.endsWith(".json"))
152
+ .map((f) => f.replace(/\.json$/, ""))
153
+ .sort();
154
+ }
155
+
156
+ readTemplateLink(storeName: string, linkId: string): { template_id: string; slot_values: Record<string, string> } {
157
+ const store = this.requireStore(storeName);
158
+ if (!/^[a-zA-Z0-9_-]+$/.test(linkId)) {
159
+ throw new Error(`Invalid link ID: "${linkId}". Link IDs must contain only letters, digits, hyphens, and underscores.`);
160
+ }
161
+ const filePath = join(store.path, "template-links", `${linkId}.json`);
162
+ if (!existsSync(filePath)) {
163
+ throw new Error(`Template link not found: "${linkId}" in store "${storeName}"`);
164
+ }
165
+ const raw = readFileSync(filePath, "utf8");
166
+ return JSON.parse(raw) as { template_id: string; slot_values: Record<string, string> };
167
+ }
168
+
169
+ // ─── Entities access ────────────────────────────────────────────────────────
170
+
171
+ listEntities(storeName: string): string[] {
172
+ const store = this.requireStore(storeName);
173
+ const entitiesDir = join(store.path, "entities");
174
+ if (!existsSync(entitiesDir)) return [];
175
+ return readdirSync(entitiesDir)
176
+ .filter((f) => f.endsWith(".json"))
177
+ .map((f) => f.replace(/\.json$/, ""))
178
+ .sort();
179
+ }
180
+
181
+ readEntities(storeName: string, entityFileId: string): string {
182
+ const store = this.requireStore(storeName);
183
+ if (!/^[a-zA-Z0-9_-]+$/.test(entityFileId)) {
184
+ throw new Error(`Invalid entity file ID: "${entityFileId}". Entity file IDs must contain only letters, digits, hyphens, and underscores.`);
185
+ }
186
+ const filePath = join(store.path, "entities", `${entityFileId}.json`);
187
+ if (!existsSync(filePath)) {
188
+ throw new Error(`Entity file not found: "${entityFileId}" in store "${storeName}"`);
189
+ }
190
+ return readFileSync(filePath, "utf8");
191
+ }
192
+
193
+ readAllEntities(storeName: string): string {
194
+ const ids = this.listEntities(storeName);
195
+ const merged: unknown[] = [];
196
+ for (const id of ids) {
197
+ const raw = this.readEntities(storeName, id);
198
+ let parsed: unknown;
199
+ try {
200
+ parsed = JSON.parse(raw);
201
+ } catch {
202
+ throw new Error(`Entity file "${id}" in store "${storeName}" contains invalid JSON.`);
203
+ }
204
+ if (!Array.isArray(parsed)) {
205
+ throw new Error(`Entity file "${id}" in store "${storeName}" must contain a JSON array at the top level, got ${typeof parsed}.`);
206
+ }
207
+ merged.push(...parsed);
208
+ }
209
+ return JSON.stringify(merged);
210
+ }
211
+
212
+ // ─── Schema access ──────────────────────────────────────────────────────────
213
+
214
+ readSchema(storeName: string): string {
215
+ const store = this.requireStore(storeName);
216
+ const cedarSchema = join(store.path, "schema.cedarschema");
217
+ if (existsSync(cedarSchema)) return readFileSync(cedarSchema, "utf8");
218
+ const jsonSchema = join(store.path, "schema.json");
219
+ if (existsSync(jsonSchema)) return readFileSync(jsonSchema, "utf8");
220
+ throw new Error(`Schema not found in store "${storeName}". Expected schema.cedarschema or schema.json at ${store.path}`);
221
+ }
222
+
223
+ // ─── Security ───────────────────────────────────────────────────────────────
224
+
225
+ isPathAllowed(filePath: string): boolean {
226
+ const normalizedRequest = filePath.replace(/\/$/, "");
227
+ for (const store of this.stores.values()) {
228
+ if (normalizedRequest.startsWith(store.path)) return true;
229
+ }
230
+ return false;
231
+ }
232
+
233
+ // ─── Private ────────────────────────────────────────────────────────────────
234
+
235
+ requireStore(name: string): PolicyStore {
236
+ const store = this.stores.get(name);
237
+ if (!store) {
238
+ const available = [...this.stores.keys()].join(", ") || "none";
239
+ const hint = this.stores.size === 0
240
+ ? " No roots are configured. Add MCP roots in your client settings, each pointing at a directory with a policies/ subdirectory and a schema.cedarschema or schema.json file."
241
+ : ` Available stores: ${available}.`;
242
+ throw new Error(`Store not found: "${name}".${hint}`);
243
+ }
244
+ return store;
245
+ }
246
+ }
247
+
248
+ export const storeManager = new StoreManager();