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.
- package/.editorconfig +12 -0
- package/.github/workflows/ci.yml +31 -0
- package/.github/workflows/release.yml +42 -0
- package/.nvmrc +1 -0
- package/CHANGELOG.md +241 -0
- package/CONTRIBUTING.md +83 -0
- package/LICENSE +182 -0
- package/README.md +1635 -0
- package/SECURITY.md +37 -0
- package/dist/http-server.d.ts +61 -0
- package/dist/http-server.d.ts.map +1 -0
- package/dist/http-server.js +194 -0
- package/dist/http-server.js.map +1 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +270 -0
- package/dist/index.js.map +1 -0
- package/dist/parser/policy-ast.d.ts +49 -0
- package/dist/parser/policy-ast.d.ts.map +1 -0
- package/dist/parser/policy-ast.js +311 -0
- package/dist/parser/policy-ast.js.map +1 -0
- package/dist/prompts/index.d.ts +38 -0
- package/dist/prompts/index.d.ts.map +1 -0
- package/dist/prompts/index.js +172 -0
- package/dist/prompts/index.js.map +1 -0
- package/dist/resources/ref-resolver.d.ts +23 -0
- package/dist/resources/ref-resolver.d.ts.map +1 -0
- package/dist/resources/ref-resolver.js +128 -0
- package/dist/resources/ref-resolver.js.map +1 -0
- package/dist/resources/store-manager.d.ts +64 -0
- package/dist/resources/store-manager.d.ts.map +1 -0
- package/dist/resources/store-manager.js +221 -0
- package/dist/resources/store-manager.js.map +1 -0
- package/dist/server.d.ts +18 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +539 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/advise/avp-rules.d.ts +49 -0
- package/dist/tools/advise/avp-rules.d.ts.map +1 -0
- package/dist/tools/advise/avp-rules.js +59 -0
- package/dist/tools/advise/avp-rules.js.map +1 -0
- package/dist/tools/advise/cedar-patterns.d.ts +24 -0
- package/dist/tools/advise/cedar-patterns.d.ts.map +1 -0
- package/dist/tools/advise/cedar-patterns.js +57 -0
- package/dist/tools/advise/cedar-patterns.js.map +1 -0
- package/dist/tools/advise/context-builder.d.ts +28 -0
- package/dist/tools/advise/context-builder.d.ts.map +1 -0
- package/dist/tools/advise/context-builder.js +89 -0
- package/dist/tools/advise/context-builder.js.map +1 -0
- package/dist/tools/advise/gotchas.d.ts +15 -0
- package/dist/tools/advise/gotchas.d.ts.map +1 -0
- package/dist/tools/advise/gotchas.js +83 -0
- package/dist/tools/advise/gotchas.js.map +1 -0
- package/dist/tools/advise.d.ts +96 -0
- package/dist/tools/advise.d.ts.map +1 -0
- package/dist/tools/advise.js +258 -0
- package/dist/tools/advise.js.map +1 -0
- package/dist/tools/authorize-batch.d.ts +35 -0
- package/dist/tools/authorize-batch.d.ts.map +1 -0
- package/dist/tools/authorize-batch.js +262 -0
- package/dist/tools/authorize-batch.js.map +1 -0
- package/dist/tools/authorize.d.ts +115 -0
- package/dist/tools/authorize.d.ts.map +1 -0
- package/dist/tools/authorize.js +373 -0
- package/dist/tools/authorize.js.map +1 -0
- package/dist/tools/check-change.d.ts +19 -0
- package/dist/tools/check-change.d.ts.map +1 -0
- package/dist/tools/check-change.js +91 -0
- package/dist/tools/check-change.js.map +1 -0
- package/dist/tools/diff-schema.d.ts +103 -0
- package/dist/tools/diff-schema.d.ts.map +1 -0
- package/dist/tools/diff-schema.js +379 -0
- package/dist/tools/diff-schema.js.map +1 -0
- package/dist/tools/diff-stores.d.ts +45 -0
- package/dist/tools/diff-stores.d.ts.map +1 -0
- package/dist/tools/diff-stores.js +222 -0
- package/dist/tools/diff-stores.js.map +1 -0
- package/dist/tools/explain.d.ts +80 -0
- package/dist/tools/explain.d.ts.map +1 -0
- package/dist/tools/explain.js +187 -0
- package/dist/tools/explain.js.map +1 -0
- package/dist/tools/format.d.ts +11 -0
- package/dist/tools/format.d.ts.map +1 -0
- package/dist/tools/format.js +20 -0
- package/dist/tools/format.js.map +1 -0
- package/dist/tools/generate-sample.d.ts +28 -0
- package/dist/tools/generate-sample.d.ts.map +1 -0
- package/dist/tools/generate-sample.js +568 -0
- package/dist/tools/generate-sample.js.map +1 -0
- package/dist/tools/link-template.d.ts +17 -0
- package/dist/tools/link-template.d.ts.map +1 -0
- package/dist/tools/link-template.js +78 -0
- package/dist/tools/link-template.js.map +1 -0
- package/dist/tools/list-template-links.d.ts +16 -0
- package/dist/tools/list-template-links.d.ts.map +1 -0
- package/dist/tools/list-template-links.js +22 -0
- package/dist/tools/list-template-links.js.map +1 -0
- package/dist/tools/list-templates.d.ts +16 -0
- package/dist/tools/list-templates.d.ts.map +1 -0
- package/dist/tools/list-templates.js +36 -0
- package/dist/tools/list-templates.js.map +1 -0
- package/dist/tools/translate.d.ts +11 -0
- package/dist/tools/translate.d.ts.map +1 -0
- package/dist/tools/translate.js +53 -0
- package/dist/tools/translate.js.map +1 -0
- package/dist/tools/validate-entities.d.ts +19 -0
- package/dist/tools/validate-entities.d.ts.map +1 -0
- package/dist/tools/validate-entities.js +88 -0
- package/dist/tools/validate-entities.js.map +1 -0
- package/dist/tools/validate-schema.d.ts +22 -0
- package/dist/tools/validate-schema.d.ts.map +1 -0
- package/dist/tools/validate-schema.js +89 -0
- package/dist/tools/validate-schema.js.map +1 -0
- package/dist/tools/validate-template.d.ts +18 -0
- package/dist/tools/validate-template.d.ts.map +1 -0
- package/dist/tools/validate-template.js +59 -0
- package/dist/tools/validate-template.js.map +1 -0
- package/dist/tools/validate.d.ts +90 -0
- package/dist/tools/validate.d.ts.map +1 -0
- package/dist/tools/validate.js +351 -0
- package/dist/tools/validate.js.map +1 -0
- package/dist/utils/format-detector.d.ts +49 -0
- package/dist/utils/format-detector.d.ts.map +1 -0
- package/dist/utils/format-detector.js +298 -0
- package/dist/utils/format-detector.js.map +1 -0
- package/examples/README.md +36 -0
- package/examples/abac-multi-tenant/README.md +150 -0
- package/examples/abac-multi-tenant/entities/users-and-docs.json +33 -0
- package/examples/abac-multi-tenant/policies/member-read-internal.cedar +9 -0
- package/examples/abac-multi-tenant/policies/owner-full-access.cedar +9 -0
- package/examples/abac-multi-tenant/policies/premium-share-guard.cedar +9 -0
- package/examples/abac-multi-tenant/policies/private-doc-guard.cedar +13 -0
- package/examples/abac-multi-tenant/run.ts +92 -0
- package/examples/abac-multi-tenant/schema.json +60 -0
- package/examples/api-gateway-path-routing/README.md +154 -0
- package/examples/api-gateway-path-routing/entities/users-and-roles.json +20 -0
- package/examples/api-gateway-path-routing/policies/admin-full-access.cedar +6 -0
- package/examples/api-gateway-path-routing/policies/developer-projects.cedar +14 -0
- package/examples/api-gateway-path-routing/policies/viewer-readonly.cedar +10 -0
- package/examples/api-gateway-path-routing/run.ts +108 -0
- package/examples/api-gateway-path-routing/schema.json +54 -0
- package/examples/rbac-document-management/README.md +167 -0
- package/examples/rbac-document-management/entities/users-and-docs.json +43 -0
- package/examples/rbac-document-management/policies/admin.cedar +6 -0
- package/examples/rbac-document-management/policies/editor.cedar +6 -0
- package/examples/rbac-document-management/policies/top-secret-forbid.cedar +13 -0
- package/examples/rbac-document-management/policies/viewer.cedar +6 -0
- package/examples/rbac-document-management/run.ts +87 -0
- package/examples/rbac-document-management/schema.json +57 -0
- package/package.json +50 -0
- package/src/http-server.ts +239 -0
- package/src/index.ts +294 -0
- package/src/parser/policy-ast.ts +345 -0
- package/src/prompts/README.md +3 -0
- package/src/prompts/index.ts +217 -0
- package/src/resources/ref-resolver.ts +134 -0
- package/src/resources/store-manager.ts +248 -0
- package/src/server.ts +711 -0
- package/src/tools/advise/avp-rules.ts +70 -0
- package/src/tools/advise/cedar-patterns.ts +73 -0
- package/src/tools/advise/context-builder.ts +109 -0
- package/src/tools/advise/gotchas.ts +92 -0
- package/src/tools/advise.ts +366 -0
- package/src/tools/authorize-batch.ts +345 -0
- package/src/tools/authorize.ts +464 -0
- package/src/tools/check-change.ts +119 -0
- package/src/tools/diff-schema.ts +510 -0
- package/src/tools/diff-stores.ts +298 -0
- package/src/tools/explain.ts +278 -0
- package/src/tools/format.ts +33 -0
- package/src/tools/generate-sample.ts +665 -0
- package/src/tools/link-template.ts +109 -0
- package/src/tools/list-template-links.ts +41 -0
- package/src/tools/list-templates.ts +55 -0
- package/src/tools/translate.ts +66 -0
- package/src/tools/validate-entities.ts +125 -0
- package/src/tools/validate-schema.ts +128 -0
- package/src/tools/validate-template.ts +72 -0
- package/src/tools/validate.ts +459 -0
- package/src/utils/format-detector.ts +356 -0
- package/test/fixtures/docmgmt.ts +121 -0
- package/test/fixtures/multitenant.ts +163 -0
- package/test/index.test.ts +96 -0
- package/test/integration/e2e/behavior.test.ts +359 -0
- package/test/integration/e2e/edge-cases.test.ts +365 -0
- package/test/integration/e2e/failure-modes.test.ts +266 -0
- package/test/integration/e2e/protocol.test.ts +252 -0
- package/test/integration/http-smoke.test.ts +588 -0
- package/test/integration/smoke.test.ts +475 -0
- package/test/prompts/prompts.test.ts +173 -0
- package/test/property/properties.test.ts +234 -0
- package/test/resources/ref-resolver.test.ts +186 -0
- package/test/resources/store-manager.test.ts +344 -0
- package/test/setup.test.ts +7 -0
- package/test/tools/advise/avp-rules.test.ts +76 -0
- package/test/tools/advise.test.ts +339 -0
- package/test/tools/authorize-batch.test.ts +459 -0
- package/test/tools/authorize.test.ts +682 -0
- package/test/tools/check-change.test.ts +104 -0
- package/test/tools/cross-fixture.test.ts +170 -0
- package/test/tools/diff-schema.test.ts +355 -0
- package/test/tools/diff-stores.test.ts +291 -0
- package/test/tools/explain.test.ts +221 -0
- package/test/tools/format.test.ts +33 -0
- package/test/tools/generate-sample.test.ts +480 -0
- package/test/tools/link-template.test.ts +90 -0
- package/test/tools/list-templates.test.ts +151 -0
- package/test/tools/translate.test.ts +89 -0
- package/test/tools/validate-entities.test.ts +178 -0
- package/test/tools/validate-schema.test.ts +86 -0
- package/test/tools/validate-template.test.ts +89 -0
- package/test/tools/validate.test.ts +331 -0
- package/test/utils/format-detector.test.ts +518 -0
- package/tsconfig.json +17 -0
- 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
|
+
});
|