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,252 @@
1
+ /**
2
+ * E2E layer 1: protocol-level behavior.
3
+ *
4
+ * Tests the MCP protocol surface end-to-end through a real stdio MCP client.
5
+ * Each test exercises behavior the server MUST honor per the MCP spec, with
6
+ * a failure case stated explicitly so the test is not tautological.
7
+ *
8
+ * Transport choice: stdio. The MCP SDK normalizes the JSON-RPC layer, so
9
+ * protocol-level behaviors are transport-agnostic except for session
10
+ * management (HTTP-only, covered in http-smoke.test.ts). Running these
11
+ * tests in stdio is faster and uses the same code path users hit via
12
+ * Claude Code / Claude Desktop / Cursor.
13
+ *
14
+ * Run: npx vitest run test/integration/e2e/protocol
15
+ */
16
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
17
+ import { join } from "node:path";
18
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
19
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
20
+
21
+ const repoRoot = join(import.meta.dirname, "../../..");
22
+
23
+ function makeStdioClient(): { client: Client; transport: StdioClientTransport } {
24
+ const transport = new StdioClientTransport({
25
+ command: "npx",
26
+ args: ["tsx", "src/index.ts"],
27
+ cwd: repoRoot,
28
+ stderr: "pipe",
29
+ });
30
+ const client = new Client(
31
+ { name: "e2e-protocol", version: "1.0.0" },
32
+ { capabilities: {} }
33
+ );
34
+ return { client, transport };
35
+ }
36
+
37
+ function parseToolResult(result: unknown): unknown {
38
+ const r = result as { content?: Array<{ type: string; text?: string }> };
39
+ const textBlock = r.content?.find((b) => b.type === "text");
40
+ if (!textBlock?.text) throw new Error("No text content in tool result");
41
+ return JSON.parse(textBlock.text);
42
+ }
43
+
44
+ const SCHEMA = JSON.stringify({
45
+ DocMgmt: {
46
+ entityTypes: {
47
+ User: { memberOfTypes: [], shape: { type: "Record", attributes: { name: { type: "String", required: true } } } },
48
+ Document: { memberOfTypes: [], shape: { type: "Record", attributes: {} } },
49
+ },
50
+ actions: { read: { appliesTo: { principalTypes: ["User"], resourceTypes: ["Document"], context: { type: "Record", attributes: {} } } } },
51
+ },
52
+ });
53
+ const POLICY = `permit (principal, action == DocMgmt::Action::"read", resource);`;
54
+
55
+ describe("e2e protocol", () => {
56
+ let client: Client | undefined;
57
+ let transport: StdioClientTransport | undefined;
58
+
59
+ beforeEach(async () => {
60
+ const conn = makeStdioClient();
61
+ client = conn.client;
62
+ transport = conn.transport;
63
+ await client.connect(transport);
64
+ });
65
+
66
+ afterEach(async () => {
67
+ try { await client?.close(); } catch { /* ignore */ }
68
+ try { await transport?.close(); } catch { /* ignore */ }
69
+ client = undefined;
70
+ transport = undefined;
71
+ });
72
+
73
+ it("P1 — server advertises tools, resources, and prompts capabilities", async () => {
74
+ // Failure case: if capabilities are wrong, MCP clients fall back to limited mode and
75
+ // won't discover the registered surface. Catches misconfigured McpServer construction.
76
+ const caps = client!.getServerCapabilities();
77
+ expect(caps).toBeDefined();
78
+ expect(caps?.tools).toBeDefined();
79
+ expect(caps?.resources).toBeDefined();
80
+ expect(caps?.prompts).toBeDefined();
81
+ }, 20_000);
82
+
83
+ it("P2 — listTools returns 17 distinct tools, no duplicates", async () => {
84
+ // Failure case: a tool registered twice would show up twice in listTools, breaking
85
+ // client UIs that key by name. Catches accidental double-registration in src/server.ts.
86
+ const { tools } = await client!.listTools();
87
+ const names = tools.map((t) => t.name);
88
+ const unique = new Set(names);
89
+ expect(unique.size).toBe(17);
90
+ expect(names.length).toBe(unique.size);
91
+ }, 20_000);
92
+
93
+ it("P3 — every tool advertises a non-empty description and input schema", async () => {
94
+ // Failure case: tools with empty descriptions render as blank entries in MCP clients.
95
+ // Tools with missing input schemas cause clients to skip parameter validation.
96
+ const { tools } = await client!.listTools();
97
+ for (const tool of tools) {
98
+ expect(tool.description, `${tool.name} description`).toBeTruthy();
99
+ expect(tool.description!.length, `${tool.name} description length`).toBeGreaterThan(10);
100
+ expect(tool.inputSchema, `${tool.name} input schema`).toBeDefined();
101
+ expect((tool.inputSchema as { type?: string }).type).toBe("object");
102
+ }
103
+ }, 20_000);
104
+
105
+ it("P4 — listPrompts returns the 3 registered prompts with required args declared", async () => {
106
+ // Failure case: missing 'required: true' on args means clients let users submit
107
+ // empty values, which then break the handler. Catches arg-schema regressions.
108
+ const { prompts } = await client!.listPrompts();
109
+ const names = prompts.map((p) => p.name);
110
+ expect(names).toContain("cedar-review-policy-diff");
111
+ expect(names).toContain("cedar-explain-denial");
112
+ expect(names).toContain("cedar-avp-migration-checklist");
113
+ expect(prompts).toHaveLength(3);
114
+
115
+ const reviewPrompt = prompts.find((p) => p.name === "cedar-review-policy-diff")!;
116
+ const requiredArgs = (reviewPrompt.arguments ?? []).filter((a) => a.required === true).map((a) => a.name);
117
+ expect(requiredArgs).toContain("blue_store");
118
+ expect(requiredArgs).toContain("green_store");
119
+ }, 20_000);
120
+
121
+ it("P5 — tools/call to an unknown tool returns isError:true, not silent success", async () => {
122
+ // Failure case: silently returning success on unknown tools masks client bugs.
123
+ // Per the MCP SDK contract, tool errors surface as { content, isError: true }
124
+ // rather than JSON-RPC rejections. The envelope MUST be tagged isError so
125
+ // clients can distinguish a tool's deliberate text output from an error.
126
+ const result = await client!.callTool({ name: "cedar_nonexistent_tool", arguments: {} }) as { isError?: boolean; content: unknown[] };
127
+ expect(result.isError).toBe(true);
128
+ expect(result.content.length).toBeGreaterThan(0);
129
+ }, 20_000);
130
+
131
+ it("P6 — tools/call with missing required arg fails with a structured error", async () => {
132
+ // Failure case: missing 'policies' AND 'policy_ref' on cedar_validate. The server
133
+ // returns its own structured error message in the tool result (not a JSON-RPC reject)
134
+ // because the args are optional individually but one of them is required at runtime.
135
+ // We assert the error path produces a JSON body with an 'error' field.
136
+ const result = await client!.callTool({
137
+ name: "cedar_validate",
138
+ arguments: { /* neither policies nor policy_ref */ },
139
+ }) as { content: Array<{ type: string; text?: string }>; isError?: boolean };
140
+ const textBlock = result.content.find((b) => b.type === "text");
141
+ expect(textBlock?.text).toBeTruthy();
142
+ const parsed = JSON.parse(textBlock!.text!);
143
+ // Either isError on the envelope OR an 'error' field in the body — both are valid
144
+ // shapes the server uses in practice. Test the union.
145
+ const hasError = result.isError === true || typeof parsed.error === "string";
146
+ expect(hasError).toBe(true);
147
+ }, 20_000);
148
+
149
+ it("P7 — concurrent tool calls return all results without interleaving", async () => {
150
+ // Failure case: if the server reuses a per-request buffer or has a race in
151
+ // response routing, two concurrent calls could swap their results. This is
152
+ // exactly the bug Streamable HTTP's session ID guards against — but stdio
153
+ // uses correlation IDs, so the SAME bug class is possible if mishandled.
154
+ const calls = [
155
+ client!.callTool({ name: "cedar_validate", arguments: { policies: POLICY, schema: SCHEMA } }),
156
+ client!.callTool({ name: "cedar_format", arguments: { policies: POLICY } }),
157
+ client!.callTool({ name: "cedar_translate", arguments: { input: POLICY, type: "policy", direction: "to_json" } }),
158
+ ];
159
+ const [validateRaw, formatRaw, translateRaw] = await Promise.all(calls);
160
+
161
+ const validate = parseToolResult(validateRaw) as { valid: boolean; policy_count: number };
162
+ const format = parseToolResult(formatRaw) as { formatted: string | null; error: string | null };
163
+ const translate = parseToolResult(translateRaw) as { output: string | null; error: string | null };
164
+
165
+ // Each result must be of its own shape — proves correlation IDs routed correctly.
166
+ // If the server had swapped responses, validate's shape would not have .valid,
167
+ // format would not have .formatted, etc.
168
+ expect(validate.valid).toBe(true);
169
+ expect(validate.policy_count).toBe(1);
170
+ expect(format.formatted).toBeTruthy();
171
+ expect(format.formatted).toContain("permit");
172
+ expect(translate.output).toBeTruthy();
173
+ // Translate to_json output is a JSON string of the AST
174
+ const translatedAst = JSON.parse(translate.output!);
175
+ expect(translatedAst.effect).toBe("permit");
176
+ }, 30_000);
177
+
178
+ it("P8 — sequential calls don't accumulate hidden state between requests", async () => {
179
+ // Failure case: an mcp server that mutates module-level state per request
180
+ // could surface the previous call's data in the current one. Probe by
181
+ // calling validate twice with different inputs and confirming each
182
+ // returns its own result, not the previous.
183
+ const r1 = parseToolResult(
184
+ await client!.callTool({ name: "cedar_validate", arguments: { policies: POLICY, schema: SCHEMA } })
185
+ ) as { valid: boolean; policy_count: number };
186
+ expect(r1.valid).toBe(true);
187
+ expect(r1.policy_count).toBe(1);
188
+
189
+ const r2 = parseToolResult(
190
+ await client!.callTool({
191
+ name: "cedar_validate",
192
+ arguments: {
193
+ policies: POLICY + "\n" + POLICY, // two policies
194
+ schema: SCHEMA,
195
+ },
196
+ })
197
+ ) as { valid: boolean; policy_count: number };
198
+ expect(r2.valid).toBe(true);
199
+ expect(r2.policy_count).toBe(2);
200
+ }, 30_000);
201
+
202
+ it("P9 — prompts/get with required args returns assembled messages", async () => {
203
+ // Failure case: the prompt handler crashes or returns empty messages array,
204
+ // which breaks the client's slash-command UX. We assert the assembled
205
+ // message text mentions the expected tool names (proving the handler ran
206
+ // the substitution, not just returned a template).
207
+ const result = await client!.getPrompt({
208
+ name: "cedar-explain-denial",
209
+ arguments: {
210
+ principal: 'App::User::"alice"',
211
+ action: 'App::Action::"read"',
212
+ resource: 'App::Document::"doc-1"',
213
+ store: "production",
214
+ },
215
+ });
216
+ expect(result.messages.length).toBeGreaterThan(0);
217
+ const allText = result.messages
218
+ .map((m) => (m.content.type === "text" ? m.content.text : ""))
219
+ .join(" ");
220
+ expect(allText).toContain("cedar_authorize");
221
+ expect(allText).toContain("alice");
222
+ expect(allText).toContain("production");
223
+ }, 20_000);
224
+
225
+ it("P10 — prompts/get with missing required arg rejects with a clear error", async () => {
226
+ // Failure case: prompt handler silently substitutes 'undefined' for missing
227
+ // required args, producing assembled text like 'authorize the request for undefined'.
228
+ // The MCP layer (the prompts validator in the SDK) must reject this first.
229
+ await expect(
230
+ client!.getPrompt({
231
+ name: "cedar-explain-denial",
232
+ // missing principal, action, resource, store — all required
233
+ arguments: {},
234
+ })
235
+ ).rejects.toBeDefined();
236
+ }, 20_000);
237
+
238
+ it("P11 — resources/read against an unconfigured store returns a structured error in the resource body", async () => {
239
+ // Failure case: with no roots configured (this server was spawned without
240
+ // any client-side roots support), reading cedar://policies/nonexistent
241
+ // should return a JSON error body, not a transport-level fault. We assert
242
+ // the response is well-formed with an 'error' field.
243
+ const result = await client!.readResource({ uri: "cedar://policies/nonexistent" });
244
+ expect(result.contents.length).toBeGreaterThan(0);
245
+ const first = result.contents[0]!;
246
+ const bodyText = typeof first.text === "string" ? first.text : "";
247
+ // The error path returns JSON like { "error": "..." }
248
+ const parsed = JSON.parse(bodyText);
249
+ expect(parsed.error).toBeDefined();
250
+ expect(typeof parsed.error).toBe("string");
251
+ }, 20_000);
252
+ });