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
package/src/server.ts
ADDED
|
@@ -0,0 +1,711 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { handleAuthorizeMcp } from "./tools/authorize.js";
|
|
5
|
+
import { handleAuthorizeBatch } from "./tools/authorize-batch.js";
|
|
6
|
+
import { handleValidateMcp } from "./tools/validate.js";
|
|
7
|
+
import { handleValidateSchema } from "./tools/validate-schema.js";
|
|
8
|
+
import { handleDiffSchema } from "./tools/diff-schema.js";
|
|
9
|
+
import { handleValidateEntities } from "./tools/validate-entities.js";
|
|
10
|
+
import { handleFormat } from "./tools/format.js";
|
|
11
|
+
import { handleTranslate } from "./tools/translate.js";
|
|
12
|
+
import { handleExplainMcp } from "./tools/explain.js";
|
|
13
|
+
import { handleCheckChange } from "./tools/check-change.js";
|
|
14
|
+
import { handleGenerateSample } from "./tools/generate-sample.js";
|
|
15
|
+
import { handleDiffStores } from "./tools/diff-stores.js";
|
|
16
|
+
import { handleAdvise } from "./tools/advise.js";
|
|
17
|
+
import { handleValidateTemplate } from "./tools/validate-template.js";
|
|
18
|
+
import { handleLinkTemplate } from "./tools/link-template.js";
|
|
19
|
+
import { handleListTemplates } from "./tools/list-templates.js";
|
|
20
|
+
import { handleListTemplateLinks } from "./tools/list-template-links.js";
|
|
21
|
+
import { storeManager } from "./resources/store-manager.js";
|
|
22
|
+
import { resolveRef } from "./resources/ref-resolver.js";
|
|
23
|
+
import { PROMPT_DEFINITIONS } from "./prompts/index.js";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Helpers that build ListResourcesCallback functions for the cedar:// URI
|
|
27
|
+
* scheme. The MCP `resources/list` method calls each ResourceTemplate's
|
|
28
|
+
* `list` callback and merges the results so clients can discover what is
|
|
29
|
+
* actually available rather than guessing the URI shape.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
type ListResult = { resources: Array<{ uri: string; name: string; mimeType?: string; description?: string }> };
|
|
33
|
+
|
|
34
|
+
/** Build a list of `cedar://<kind>/{store}` index resources, one per loaded store. */
|
|
35
|
+
function listIndexResources(kind: string, mimeType: string, describe: (store: string) => string) {
|
|
36
|
+
return (): ListResult => {
|
|
37
|
+
const resources: ListResult["resources"] = [];
|
|
38
|
+
for (const storeName of storeManager.listStoreNames()) {
|
|
39
|
+
resources.push({
|
|
40
|
+
uri: `cedar://${kind}/${storeName}`,
|
|
41
|
+
name: `${kind}:${storeName}`,
|
|
42
|
+
mimeType,
|
|
43
|
+
description: describe(storeName),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return { resources };
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Build a list of per-item resources `cedar://<kind>/{store}/{id}` across all loaded stores. */
|
|
51
|
+
function listItemResources(
|
|
52
|
+
kind: string,
|
|
53
|
+
mimeType: string,
|
|
54
|
+
enumerate: (storeName: string) => string[],
|
|
55
|
+
describe: (storeName: string, id: string) => string,
|
|
56
|
+
) {
|
|
57
|
+
return (): ListResult => {
|
|
58
|
+
const resources: ListResult["resources"] = [];
|
|
59
|
+
for (const storeName of storeManager.listStoreNames()) {
|
|
60
|
+
let ids: string[];
|
|
61
|
+
try {
|
|
62
|
+
ids = enumerate(storeName);
|
|
63
|
+
} catch {
|
|
64
|
+
continue; // store lacks this subdirectory; silently skip
|
|
65
|
+
}
|
|
66
|
+
for (const id of ids) {
|
|
67
|
+
resources.push({
|
|
68
|
+
uri: `cedar://${kind}/${storeName}/${id}`,
|
|
69
|
+
name: `${id}`,
|
|
70
|
+
mimeType,
|
|
71
|
+
description: describe(storeName, id),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return { resources };
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Build the schema-resource enumeration: one per store that has a schema file. */
|
|
80
|
+
function listSchemaResources() {
|
|
81
|
+
return (): ListResult => {
|
|
82
|
+
const resources: ListResult["resources"] = [];
|
|
83
|
+
for (const storeName of storeManager.listStoreNames()) {
|
|
84
|
+
try {
|
|
85
|
+
storeManager.readSchema(storeName); // confirm the file exists
|
|
86
|
+
} catch {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
resources.push({
|
|
90
|
+
uri: `cedar://schema/${storeName}`,
|
|
91
|
+
name: `schema:${storeName}`,
|
|
92
|
+
mimeType: "text/plain",
|
|
93
|
+
description: `Cedar schema (.cedarschema or schema.json) for store "${storeName}"`,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
return { resources };
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export const SERVER_NAME = "cedar-mcp-server";
|
|
101
|
+
export const SERVER_VERSION = "0.0.1";
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Server-level instructions returned in the MCP `initialize` response. Surfaced
|
|
105
|
+
* by the client (Claude Code, Claude Desktop, Cursor) as a system-prompt hint
|
|
106
|
+
* for when to reach for this server's tools. Truncated at 2KB in Claude Code,
|
|
107
|
+
* so critical routing guidance is front-loaded.
|
|
108
|
+
*
|
|
109
|
+
* Added in response to the 2026-05-21 falsification test result: tool-level
|
|
110
|
+
* descriptions alone (kickoff-08 sub-phase 8b) did not stop Claude from
|
|
111
|
+
* bypassing the cedar_* tools via Read + Bash. See
|
|
112
|
+
* projects/cedar-mcp-server/research-mcp-discoverability-patterns.md
|
|
113
|
+
* "Path 0" for the rationale.
|
|
114
|
+
*/
|
|
115
|
+
export const SERVER_INSTRUCTIONS = `cedar-mcp-server provides Cedar policy language and AWS Verified Permissions tooling.
|
|
116
|
+
|
|
117
|
+
For ANY question about Cedar policies, schemas, entities, or authorization decisions, you MUST call the appropriate cedar_* tool rather than reading files and reasoning natively. The Cedar engine, AST parser, and AVP rules encoded in this server are the authoritative source; reading .cedar files alone is insufficient because pattern classification, policy evaluation, AVP UpdatePolicy mutability, and gotchas catalog cannot be reconstructed from file text.
|
|
118
|
+
|
|
119
|
+
Tool routing:
|
|
120
|
+
- "Plan a Cedar change" / "how do I add X rule" / "help me restrict Y" -> cedar_advise FIRST. Returns structured context bundle (gotchas, AVP rules, Cedar patterns, current policy classification). Reason from the bundle.
|
|
121
|
+
- "What does this policy do?" / "explain this Cedar" -> cedar_explain
|
|
122
|
+
- "Is this policy valid?" / "check my Cedar syntax" -> cedar_validate
|
|
123
|
+
- "Would X be allowed to do Y on Z?" / "test this authorization" -> cedar_authorize
|
|
124
|
+
- "Compare two policy stores" / "is it safe to deploy" -> cedar_diff_policy_stores
|
|
125
|
+
- "Why was X denied?" -> cedar_authorize then cedar_explain (positional policy IDs need explanation lookup)
|
|
126
|
+
- "Generate a test payload" -> cedar_generate_sample_request
|
|
127
|
+
- "Migrating from AVP" / "is my schema AVP-compatible" -> cedar_project_intelligence (when shipped) or cedar_validate_schema
|
|
128
|
+
- "Modify an existing policy" -> cedar_check_policy_change FIRST (returns AVP UpdatePolicy classification)
|
|
129
|
+
|
|
130
|
+
Workspace auto-discovery: if cedar_validate / cedar_authorize / cedar_explain reports missing schema, policies, or entities, retry with the field omitted; the tool auto-discovers from the loaded workspace store (schema.cedarschema, policies/, entities/). Pass store: "<name>" for multi-store deployments. Ask the user for inline input only as a last resort.
|
|
131
|
+
|
|
132
|
+
Do NOT use Read or Bash to inspect Cedar policy semantics. The server tools encode Cedar/AVP knowledge that does not live in the files.`;
|
|
133
|
+
|
|
134
|
+
export function createServer(): McpServer {
|
|
135
|
+
const server = new McpServer({
|
|
136
|
+
name: SERVER_NAME,
|
|
137
|
+
version: SERVER_VERSION,
|
|
138
|
+
}, {
|
|
139
|
+
instructions: SERVER_INSTRUCTIONS,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
server.tool(
|
|
143
|
+
"cedar_authorize",
|
|
144
|
+
"Evaluate a Cedar authorization request against policies and entities. Returns the decision (Allow/Deny), the determining policies, and any evaluation errors. ALWAYS call this for any 'would X be allowed?' or 'why was Y denied?' question. You CANNOT simulate an authorization decision by reading the policy files; only the Cedar engine implements the full evaluation semantics (default-deny, forbid-overrides-permit, attribute guards, action group membership, schema-validated entity types). Accepts inline policy text OR a cedar:// resource reference. When policies / schema / entities are omitted and a single MCP root is loaded, the missing inputs are auto-discovered from the workspace; the response's auto_discovered field reports each source store.",
|
|
145
|
+
{
|
|
146
|
+
policies: z.string().optional().describe("Cedar policy text (one or more policies). Omit if using policy_ref."),
|
|
147
|
+
policy_ref: z.string().optional().describe("cedar:// URI to load policies from a configured store, e.g. cedar://policies/blue"),
|
|
148
|
+
principal: z.string().describe('Principal entity reference, e.g. Namespace::Type::"id"'),
|
|
149
|
+
action: z.string().describe('Action entity reference, e.g. Namespace::Action::"name"'),
|
|
150
|
+
resource: z.string().describe('Resource entity reference, e.g. Namespace::Type::"id"'),
|
|
151
|
+
entities: z.string().optional().describe("JSON array of entity objects with uid, attrs, and parents. Omit if using entities_ref."),
|
|
152
|
+
entities_ref: z.string().optional().describe("cedar:// URI to load entities from a configured store, e.g. cedar://entities/production"),
|
|
153
|
+
schema: z.string().optional().describe("Optional Cedar schema (JSON or .cedarschema format) — enables request validation"),
|
|
154
|
+
schema_ref: z.string().optional().describe("cedar:// URI to load schema from a configured store, e.g. cedar://schema/blue"),
|
|
155
|
+
context: z.string().optional().describe("Optional JSON object with context attributes"),
|
|
156
|
+
store: z.string().optional().describe("Optional store name (a configured MCP root) for workspace auto-discovery. Use when multiple stores are loaded and the tool needs to choose one for policies / schema / entities lookup."),
|
|
157
|
+
},
|
|
158
|
+
async (input) => {
|
|
159
|
+
// 10d workspace auto-discovery lives in handleAuthorizeMcp so tests can
|
|
160
|
+
// exercise the resolution path directly. The wrapper resolves missing
|
|
161
|
+
// policies / schema / entities from a single MCP root, so a call never
|
|
162
|
+
// mixes blue policies with green entities.
|
|
163
|
+
const outcome = await handleAuthorizeMcp(input, resolveRef);
|
|
164
|
+
if ("error" in outcome) {
|
|
165
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: outcome.error }) }] };
|
|
166
|
+
}
|
|
167
|
+
return { content: [{ type: "text", text: JSON.stringify(outcome.result, null, 2) }] };
|
|
168
|
+
}
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
server.tool(
|
|
172
|
+
"cedar_authorize_batch",
|
|
173
|
+
"Run N authorization requests through ONE policy set in a single call and return the full decision matrix (Allow / Deny / Error per request, with determining policies). ALWAYS use this for regression testing after a policy edit, for canonical-request suites, and for behavioral comparisons. You CANNOT predict the matrix by reading policies alone; each request's decision depends on engine evaluation against the resolved entity graph and schema. Accepts inline policies+schema OR cedar:// refs.",
|
|
174
|
+
{
|
|
175
|
+
policies: z.string().optional().describe("Cedar policy text (one or more policies). Omit if using policy_ref."),
|
|
176
|
+
policy_ref: z.string().optional().describe("cedar:// URI to load policies from a configured store, e.g. cedar://policies/blue"),
|
|
177
|
+
schema: z.string().optional().describe("Optional Cedar schema (JSON or .cedarschema). When supplied, schema-violating requests resolve to decision: Error rather than silent evaluation."),
|
|
178
|
+
schema_ref: z.string().optional().describe("cedar:// URI to load schema, e.g. cedar://schema/blue"),
|
|
179
|
+
requests: z.string().describe("JSON array of authorization request objects: {principal, action, resource, entities, context?}"),
|
|
180
|
+
entities: z.string().optional().describe("Shared entities JSON applied when individual requests omit their own entities field. Omit if using entities_ref."),
|
|
181
|
+
entities_ref: z.string().optional().describe("cedar:// URI to load shared entities from a configured store, e.g. cedar://entities/production"),
|
|
182
|
+
},
|
|
183
|
+
async (input) => {
|
|
184
|
+
let entities = input.entities;
|
|
185
|
+
if (!entities && input.entities_ref) {
|
|
186
|
+
const resolved = resolveRef(input.entities_ref);
|
|
187
|
+
if ("error" in resolved) return { content: [{ type: "text", text: JSON.stringify({ error: resolved.error }) }] };
|
|
188
|
+
entities = resolved.content;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// kickoff-14 14a: resolve cedar://policies/{store} into a basename-keyed
|
|
192
|
+
// map so determining_policies returns "admin" / "editor" rather than
|
|
193
|
+
// "policy0" / "policy1". Mirrors the cedar_authorize wrapper pattern in
|
|
194
|
+
// handleAuthorizeMcp. Other ref shapes (single policy, non-store URIs)
|
|
195
|
+
// still fall through to handleAuthorizeBatch's resolveRef call.
|
|
196
|
+
let policiesMap: Record<string, string> | undefined;
|
|
197
|
+
let policies = input.policies;
|
|
198
|
+
let policyRefForward: string | undefined = input.policy_ref;
|
|
199
|
+
if (!policies && input.policy_ref) {
|
|
200
|
+
const storeMatch = input.policy_ref.match(/^cedar:\/\/policies\/([^/]+)$/);
|
|
201
|
+
const singleMatch = input.policy_ref.match(/^cedar:\/\/policies\/([^/]+)\/([^/]+)$/);
|
|
202
|
+
if (storeMatch) {
|
|
203
|
+
const storeName = storeMatch[1]!;
|
|
204
|
+
try {
|
|
205
|
+
const ids = storeManager.listPolicies(storeName);
|
|
206
|
+
policiesMap = {};
|
|
207
|
+
for (const id of ids) policiesMap[id] = storeManager.readPolicy(storeName, id);
|
|
208
|
+
policyRefForward = undefined;
|
|
209
|
+
} catch (e) {
|
|
210
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: e instanceof Error ? e.message : String(e) }) }] };
|
|
211
|
+
}
|
|
212
|
+
} else if (singleMatch) {
|
|
213
|
+
const storeName = singleMatch[1]!;
|
|
214
|
+
const policyId = singleMatch[2]!;
|
|
215
|
+
try {
|
|
216
|
+
policiesMap = { [policyId]: storeManager.readPolicy(storeName, policyId) };
|
|
217
|
+
policyRefForward = undefined;
|
|
218
|
+
} catch (e) {
|
|
219
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: e instanceof Error ? e.message : String(e) }) }] };
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const result = await handleAuthorizeBatch({
|
|
225
|
+
policies,
|
|
226
|
+
policy_ref: policyRefForward,
|
|
227
|
+
policiesMap,
|
|
228
|
+
schema: input.schema,
|
|
229
|
+
schema_ref: input.schema_ref,
|
|
230
|
+
requests: input.requests,
|
|
231
|
+
entities,
|
|
232
|
+
});
|
|
233
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
234
|
+
}
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
server.tool(
|
|
238
|
+
"cedar_validate",
|
|
239
|
+
"Validate Cedar policies, with OR without a schema. Two modes: syntax-only (no schema, parser-only, catches typos and bad scopes) and syntax-and-schema (full parse plus type-check against a Cedar schema). Returns parse errors, schema-type errors, and warnings with source locations; the `validation_mode` field in the response tells you which mode ran. ALWAYS call this before claiming a policy is valid or before recommending it to a user. You CANNOT determine policy validity by reading the file; the Cedar parser is the only authority on syntax, attribute typing, action-applies-to checks, and UnsafeOptionalAttributeAccess warnings. Accepts inline text OR cedar:// resource references. When schema is omitted and a single MCP root is loaded, the schema is auto-discovered from the workspace; the response's auto_discovered field reports the source store. Pass validation_mode to force a specific mode: 'syntax_only' skips workspace auto-discovery and runs parser-only (useful when the user explicitly has no schema); 'syntax_and_schema' requires a schema and returns an error if none is resolvable.",
|
|
240
|
+
{
|
|
241
|
+
policies: z.string().optional().describe("Cedar policy text (one or more policies). Omit if using policy_ref."),
|
|
242
|
+
policy_ref: z.string().optional().describe("cedar:// URI to load policies, e.g. cedar://policies/blue"),
|
|
243
|
+
schema: z.string().optional().describe("Cedar schema — JSON object or .cedarschema text. Omit if using schema_ref or to auto-discover from a loaded workspace store."),
|
|
244
|
+
schema_ref: z.string().optional().describe("cedar:// URI to load schema, e.g. cedar://schema/blue"),
|
|
245
|
+
store: z.string().optional().describe("Optional store name (a configured MCP root) for workspace auto-discovery. Use when multiple stores are loaded and the tool needs to choose one for schema lookup."),
|
|
246
|
+
validation_mode: z.enum(["auto", "syntax_only", "syntax_and_schema"]).optional().describe("Optional explicit mode. 'auto' (default) picks the mode from schema presence. 'syntax_only' forces parser-only and skips workspace auto-discovery (use when the user has explicitly said they have no schema). 'syntax_and_schema' requires a schema (inline or auto-discovered) and errors out if none is available."),
|
|
247
|
+
},
|
|
248
|
+
async (input) => {
|
|
249
|
+
// 10d workspace auto-discovery lives in handleValidateMcp so tests can
|
|
250
|
+
// exercise the resolution path directly.
|
|
251
|
+
const outcome = await handleValidateMcp(input, resolveRef);
|
|
252
|
+
if ("error" in outcome) {
|
|
253
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: outcome.error }) }] };
|
|
254
|
+
}
|
|
255
|
+
return { content: [{ type: "text", text: JSON.stringify(outcome.result, null, 2) }] };
|
|
256
|
+
}
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
server.tool(
|
|
260
|
+
"cedar_validate_schema",
|
|
261
|
+
"Validate a Cedar schema in isolation (no policies required) using the official Cedar schema parser. Accepts JSON object or .cedarschema text. Returns parse errors with source locations plus a structured summary (namespaces, entity types, actions, common types). ALWAYS call this before claiming a schema is well-formed or before treating its declarations as ground truth. You CANNOT determine schema validity or accurate counts by reading the file; the parser is the only authority on common-type resolution, appliesTo cross-references, and reserved-word collisions.",
|
|
262
|
+
{
|
|
263
|
+
schema: z.string().optional().describe("Cedar schema text — JSON object or .cedarschema text. Omit if using schema_ref."),
|
|
264
|
+
schema_ref: z.string().optional().describe("cedar:// URI to load schema, e.g. cedar://schema/blue"),
|
|
265
|
+
},
|
|
266
|
+
async (input) => {
|
|
267
|
+
let schema = input.schema;
|
|
268
|
+
if (!schema && input.schema_ref) {
|
|
269
|
+
const resolved = resolveRef(input.schema_ref);
|
|
270
|
+
if ("error" in resolved) return { content: [{ type: "text", text: JSON.stringify({ error: resolved.error }) }] };
|
|
271
|
+
schema = resolved.content;
|
|
272
|
+
}
|
|
273
|
+
if (!schema) return { content: [{ type: "text", text: JSON.stringify({ error: "Either schema or schema_ref is required" }) }] };
|
|
274
|
+
|
|
275
|
+
const result = await handleValidateSchema({ schema });
|
|
276
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
277
|
+
}
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
server.tool(
|
|
281
|
+
"cedar_format",
|
|
282
|
+
"Format Cedar policies to the canonical Cedar style via the official formatter. ALWAYS use this when emitting Cedar text for storage, diffs, or display; do not hand-format. Hand-formatted output drifts from canonical style and produces noise in `cedar_diff_policy_stores` and code review. The formatter handles operator spacing, line wrapping, comment placement, and nested expression indentation per the Cedar reference implementation.",
|
|
283
|
+
{
|
|
284
|
+
policies: z.string().describe("Cedar policy text to format"),
|
|
285
|
+
line_width: z.number().optional().describe("Maximum line width (default: 80)"),
|
|
286
|
+
indent_width: z.number().optional().describe("Indent width in spaces (default: 2)"),
|
|
287
|
+
},
|
|
288
|
+
async (input) => {
|
|
289
|
+
const result = await handleFormat(input);
|
|
290
|
+
return {
|
|
291
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
server.tool(
|
|
297
|
+
"cedar_translate",
|
|
298
|
+
"Translate between Cedar human-readable format and the official Cedar JSON format for policies or schemas. ALWAYS use this when converting between formats for storage, AVP deployment (which uses JSON), or AST inspection. Do NOT translate by hand: the Cedar JSON shape is non-obvious (templates use slot encoding, schemas resolve common types, conditions are nested op trees), and a hand-written translation will be silently wrong against the parser. Round-trip via this tool is the only safe path.",
|
|
299
|
+
{
|
|
300
|
+
input: z.string().describe("Cedar text or JSON to translate"),
|
|
301
|
+
type: z.enum(["policy", "schema"]).describe("Whether the input is a policy or schema"),
|
|
302
|
+
direction: z.enum(["to_json", "to_cedar"]).describe("Translation direction"),
|
|
303
|
+
},
|
|
304
|
+
async (input) => {
|
|
305
|
+
const result = await handleTranslate(input);
|
|
306
|
+
return {
|
|
307
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
server.tool(
|
|
313
|
+
"cedar_explain",
|
|
314
|
+
"Explain one or more Cedar policies in structured form, derived from the parsed AST (not from text inspection). ALWAYS call this when summarizing what a policy permits, walking a user through inherited policies, or detecting Cedar patterns (RBAC role-membership, ABAC attribute conditions, ReBAC relationship checks, optional-attribute guards, path-matching with depth limiting). Reading the policy text does not reliably yield correct structural breakdown or pattern detection; the AST is the authority. Accepts a single policy, a template, or a full policy set. When schema is omitted and a single MCP root is loaded, the schema is auto-discovered from the workspace; the response's auto_discovered field reports the source store.",
|
|
315
|
+
{
|
|
316
|
+
policy: z.string().describe("Cedar policy text (single policy, template, or policy set with multiple policies)"),
|
|
317
|
+
schema: z.string().optional().describe("Optional Cedar schema for richer context"),
|
|
318
|
+
schema_ref: z.string().optional().describe("cedar:// URI to load schema, e.g. cedar://schema/blue"),
|
|
319
|
+
store: z.string().optional().describe("Optional store name (a configured MCP root) for workspace auto-discovery. Use when multiple stores are loaded and the tool needs to choose one for schema lookup."),
|
|
320
|
+
},
|
|
321
|
+
async (input) => {
|
|
322
|
+
// 10d workspace auto-discovery lives in handleExplainMcp so tests can
|
|
323
|
+
// exercise the resolution path directly.
|
|
324
|
+
const outcome = await handleExplainMcp(input, resolveRef);
|
|
325
|
+
if ("error" in outcome) {
|
|
326
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: outcome.error }) }] };
|
|
327
|
+
}
|
|
328
|
+
return { content: [{ type: "text", text: JSON.stringify(outcome.result, null, 2) }] };
|
|
329
|
+
}
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
server.tool(
|
|
333
|
+
"cedar_check_policy_change",
|
|
334
|
+
"Diff an old vs new Cedar policy and classify the change against Amazon Verified Permissions UpdatePolicy mutability rules: in-place via UpdatePolicy, requires delete+recreate, or new-via-CreatePolicy. ALWAYS call this before recommending any modification to a policy in a deployed AVP store. Visually reading the diff does NOT tell you whether AVP will accept the update; only this tool encodes the actual API contract (effect / principal / resource scope are immutable; action and when/unless are mutable; static ↔ template-linked conversion is not supported).",
|
|
335
|
+
{
|
|
336
|
+
old_policy: z.string().describe("Original Cedar policy text"),
|
|
337
|
+
new_policy: z.string().describe("Modified Cedar policy text"),
|
|
338
|
+
},
|
|
339
|
+
async (input) => {
|
|
340
|
+
const result = await handleCheckChange(input);
|
|
341
|
+
return {
|
|
342
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
server.tool(
|
|
348
|
+
"cedar_generate_sample_request",
|
|
349
|
+
"Generate a complete Cedar authorization request (principal, action, resource, entities JSON) that will be allowed or denied by the given policy under the supplied schema. The generated request is verified by running it through the Cedar engine before return; the response field `ready_to_test: true` confirms the agreement. ALWAYS use this when a user needs a working test payload; constructing entity JSON by hand (correct uid format, required attributes, parent relationships, action-group entities) is error-prone and Cedar will silently reject a malformed entity graph.",
|
|
350
|
+
{
|
|
351
|
+
policy: z.string().describe("Cedar policy text (single policy)"),
|
|
352
|
+
schema: z.string().describe("Cedar schema (JSON or .cedarschema format)"),
|
|
353
|
+
target_decision: z.enum(["allow", "deny"]).describe("Generate a request that will be allowed or denied"),
|
|
354
|
+
},
|
|
355
|
+
async (input) => {
|
|
356
|
+
const result = await handleGenerateSample(input);
|
|
357
|
+
return {
|
|
358
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
server.tool(
|
|
364
|
+
"cedar_validate_entities",
|
|
365
|
+
"Validate a Cedar entities JSON array against a schema using the official Cedar entities parser. Returns per-entity errors classified by kind: unknown_type, missing_required_attribute, type_mismatch, unknown_attribute, disallowed_parent_type, parse_error. ALWAYS call this before treating an entities file as ground truth for a downstream `cedar_authorize` call. You CANNOT predict by reading whether the entity graph will pass schema validation; only the parser checks required attributes, type compatibility, and allowed parent types per the schema's `memberOfTypes`. Schema is optional; without it only JSON shape is checked.",
|
|
366
|
+
{
|
|
367
|
+
entities: z.string().describe("JSON array of entity objects with uid, attrs, and parents"),
|
|
368
|
+
schema: z.string().optional().describe("Cedar schema (JSON or .cedarschema) — enables type validation"),
|
|
369
|
+
schema_ref: z.string().optional().describe("cedar:// URI to load schema, e.g. cedar://schema/blue"),
|
|
370
|
+
},
|
|
371
|
+
async (input) => {
|
|
372
|
+
let schema = input.schema;
|
|
373
|
+
if (!schema && input.schema_ref) {
|
|
374
|
+
const resolved = resolveRef(input.schema_ref);
|
|
375
|
+
if ("error" in resolved) return { content: [{ type: "text", text: JSON.stringify({ error: resolved.error }) }] };
|
|
376
|
+
schema = resolved.content;
|
|
377
|
+
}
|
|
378
|
+
const result = await handleValidateEntities({ entities: input.entities, schema });
|
|
379
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
380
|
+
}
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
server.tool(
|
|
384
|
+
"cedar_diff_schema",
|
|
385
|
+
"Structural diff of two Cedar schemas with AVP-aware risk classification per change (safe / review / breaking) and a reason string per change. ALWAYS call this before recommending a schema migration to a deployed store. A `diff` over the schema text does NOT tell you which changes break policies (e.g. removing a required attribute breaks every policy that reads it; widening principalTypes is safe; narrowing it is breaking). Only this tool normalizes both schemas via the parser, walks entity types / actions / common types / appliesTo / memberOfTypes, and tags each change. Each side accepts inline schema text (JSON or .cedarschema) OR a cedar://schema/{store} URI.",
|
|
386
|
+
{
|
|
387
|
+
blue: z.string().describe("Blue (baseline) schema — inline schema text or cedar://schema/{store} URI"),
|
|
388
|
+
green: z.string().describe("Green (proposed) schema — inline schema text or cedar://schema/{store} URI"),
|
|
389
|
+
},
|
|
390
|
+
async (input) => {
|
|
391
|
+
let blue = input.blue;
|
|
392
|
+
let green = input.green;
|
|
393
|
+
if (blue.startsWith("cedar://")) {
|
|
394
|
+
const resolved = resolveRef(blue);
|
|
395
|
+
if ("error" in resolved) return { content: [{ type: "text", text: JSON.stringify({ error: `blue: ${resolved.error}` }) }] };
|
|
396
|
+
blue = resolved.content;
|
|
397
|
+
}
|
|
398
|
+
if (green.startsWith("cedar://")) {
|
|
399
|
+
const resolved = resolveRef(green);
|
|
400
|
+
if ("error" in resolved) return { content: [{ type: "text", text: JSON.stringify({ error: `green: ${resolved.error}` }) }] };
|
|
401
|
+
green = resolved.content;
|
|
402
|
+
}
|
|
403
|
+
const result = await handleDiffSchema({ blue, green });
|
|
404
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
405
|
+
}
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
server.tool(
|
|
409
|
+
"cedar_diff_policy_stores",
|
|
410
|
+
"Semantic + structural diff of two Cedar policy stores (blue vs green) with AVP immutability classification per change and an optional behavioral diff (run canonical authorization requests through both stores and surface decisions that flip). ALWAYS call this before recommending a deployment from a green/staging store to a blue/production store. A text diff CANNOT tell you which authorization decisions change between the two stores; only running both stores against the same requests does. Returns added / removed / modified policies, structured schema_diff (with safe/review/breaking risk), and (when behavioral_test_requests is supplied) per-request decision drift.",
|
|
411
|
+
{
|
|
412
|
+
blue: z.string().describe("Name of the blue (current/production) store — must be a configured MCP root"),
|
|
413
|
+
green: z.string().describe("Name of the green (proposed/staging) store — must be a configured MCP root"),
|
|
414
|
+
behavioral_test_requests: z.string().optional().describe("JSON array of authorization requests to run through both stores to detect decision drift"),
|
|
415
|
+
},
|
|
416
|
+
async (input) => {
|
|
417
|
+
const result = await handleDiffStores(input, storeManager);
|
|
418
|
+
return {
|
|
419
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
server.tool(
|
|
425
|
+
"cedar_advise",
|
|
426
|
+
"ALWAYS call this before suggesting any Cedar policy modification or addition. Returns the structured project context you need to plan correctly: schema summary, policy inventory with AST-classified Cedar patterns (Membership/Relationship/Discretionary/hybrid) per file, intent-selected gotcha catalog, AVP UpdatePolicy mutability rules (in-place vs delete-recreate vs new), Cedar patterns reference, sequencing guidance, and explicit next steps. Reading policy files alone is INSUFFICIENT, because this tool encodes Cedar/AVP knowledge (AVP API rules, validation error categories, AST-based pattern classification) that does not live in the files. The bundle is deterministic (no LLM sampling); the calling assistant produces the plan from the bundle and verifies snippets with cedar_validate and cedar_check_policy_change.",
|
|
427
|
+
{
|
|
428
|
+
intent: z.string().describe("Natural-language description of what the user wants the authorization model to do. Keep the wording from the user verbatim where possible."),
|
|
429
|
+
store_ref: z.string().optional().describe("cedar:// URI or store name to read current schema and policies from, e.g. cedar://policies/production or just 'production'. Omit when you want auto-resolve: if exactly one store is loaded the bundle resolves to it and the response sets auto_discovered.store_from='single_loaded_store'. With multiple stores loaded and no store_ref, the response sets store_status='ambiguous' and lists available_stores so you can retry with the right one."),
|
|
430
|
+
},
|
|
431
|
+
async (input) => {
|
|
432
|
+
const result = handleAdvise(input);
|
|
433
|
+
return {
|
|
434
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
);
|
|
438
|
+
|
|
439
|
+
server.tool(
|
|
440
|
+
"cedar_validate_template",
|
|
441
|
+
"Validate a Cedar template policy against a schema using the official parser. Templates use slot placeholders (?principal, ?resource) bound at link time. Returns parse errors, schema-type errors, warnings, and detected slots. ALWAYS call this before claiming a template is valid or before linking it; the parser is the only authority on whether a template's slots are typed correctly against the schema's `appliesTo` and on whether the body's attribute access satisfies type rules. Visual inspection of a template does NOT catch slot-type mismatches or missing-attribute references.",
|
|
442
|
+
{
|
|
443
|
+
template: z.string().describe("Cedar template text — may contain ?principal and/or ?resource slot placeholders"),
|
|
444
|
+
schema: z.string().describe("Cedar schema (JSON or .cedarschema format)"),
|
|
445
|
+
},
|
|
446
|
+
async (input) => {
|
|
447
|
+
const result = await handleValidateTemplate(input);
|
|
448
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
449
|
+
}
|
|
450
|
+
);
|
|
451
|
+
|
|
452
|
+
server.tool(
|
|
453
|
+
"cedar_link_template",
|
|
454
|
+
"Instantiate a Cedar template by binding its ?principal and/or ?resource slots to specific entity references. Returns the linked policy as Cedar text and (when a schema is supplied) validates the result as a static policy. ALWAYS use this tool to perform the substitution; do NOT hand-substitute slot text into the template body. The official substitution path uses `templateToJson` → JSON patch → `policyToText`, which preserves operator semantics and slot-position rules that a naïve string replace will silently break (e.g. for templates that reference the slot inside set or relation operators).",
|
|
455
|
+
{
|
|
456
|
+
template: z.string().describe("Cedar template text with ?principal and/or ?resource slots"),
|
|
457
|
+
principal: z.string().optional().describe('Entity reference for the ?principal slot, e.g. App::User::"alice"'),
|
|
458
|
+
resource: z.string().optional().describe('Entity reference for the ?resource slot, e.g. App::Document::"doc-42"'),
|
|
459
|
+
schema: z.string().optional().describe("Cedar schema (JSON or .cedarschema) — if provided, the linked policy is validated against it"),
|
|
460
|
+
},
|
|
461
|
+
async (input) => {
|
|
462
|
+
const result = await handleLinkTemplate(input);
|
|
463
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
464
|
+
}
|
|
465
|
+
);
|
|
466
|
+
|
|
467
|
+
server.tool(
|
|
468
|
+
"cedar_list_templates",
|
|
469
|
+
"List all Cedar template policies in a policy store configured via MCP Roots. Templates live in the templates/ subdirectory of the store root. Returns template IDs, content, and detected slot placeholders. ALWAYS call this when the user asks 'what templates are available?' or before recommending which template to link. The tool walks the configured store's templates/ directory via the same StoreManager other tools use, so the inventory matches what `cedar_link_template` and `cedar_validate_template` will actually resolve; a direct file listing may miss the store's namespacing convention or include unrelated files.",
|
|
470
|
+
{
|
|
471
|
+
store: z.string().describe("Store name (must be a configured MCP root)"),
|
|
472
|
+
},
|
|
473
|
+
async (input) => {
|
|
474
|
+
const result = await handleListTemplates(input, storeManager);
|
|
475
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
476
|
+
}
|
|
477
|
+
);
|
|
478
|
+
|
|
479
|
+
server.tool(
|
|
480
|
+
"cedar_list_template_links",
|
|
481
|
+
"List all template-linked policy instances in a policy store configured via MCP Roots. Links live in the template-links/ subdirectory. Each link records which template it uses and the slot values bound to it. ALWAYS call this when the user asks 'which policies in the store came from a template?' or before recommending a refactor across template links. The tool decodes the link record format the rest of the server uses; a direct file read would still need to parse and join links against templates to produce a usable inventory.",
|
|
482
|
+
{
|
|
483
|
+
store: z.string().describe("Store name (must be a configured MCP root)"),
|
|
484
|
+
},
|
|
485
|
+
async (input) => {
|
|
486
|
+
const result = await handleListTemplateLinks(input, storeManager);
|
|
487
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
488
|
+
}
|
|
489
|
+
);
|
|
490
|
+
|
|
491
|
+
// ─── MCP Resources: cedar:// URI scheme ────────────────────────────────────
|
|
492
|
+
|
|
493
|
+
// List all policy IDs in a store: cedar://policies/{store}
|
|
494
|
+
server.resource(
|
|
495
|
+
"cedar-policies-list",
|
|
496
|
+
new ResourceTemplate("cedar://policies/{store}", {
|
|
497
|
+
list: listIndexResources("policies", "application/json", (s) => `JSON list of policy IDs in store "${s}"`),
|
|
498
|
+
}),
|
|
499
|
+
async (_uri, variables) => {
|
|
500
|
+
const storeName = variables["store"] as string;
|
|
501
|
+
try {
|
|
502
|
+
const policyIds = storeManager.listPolicies(storeName);
|
|
503
|
+
return {
|
|
504
|
+
contents: [{
|
|
505
|
+
uri: `cedar://policies/${storeName}`,
|
|
506
|
+
mimeType: "application/json",
|
|
507
|
+
text: JSON.stringify(policyIds),
|
|
508
|
+
}],
|
|
509
|
+
};
|
|
510
|
+
} catch (e) {
|
|
511
|
+
return {
|
|
512
|
+
contents: [{
|
|
513
|
+
uri: `cedar://policies/${storeName}`,
|
|
514
|
+
mimeType: "application/json",
|
|
515
|
+
text: JSON.stringify({ error: e instanceof Error ? e.message : String(e) }),
|
|
516
|
+
}],
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
);
|
|
521
|
+
|
|
522
|
+
// Read a single policy: cedar://policies/{store}/{policy_id}
|
|
523
|
+
server.resource(
|
|
524
|
+
"cedar-policy",
|
|
525
|
+
new ResourceTemplate("cedar://policies/{store}/{policy_id}", {
|
|
526
|
+
list: listItemResources(
|
|
527
|
+
"policies",
|
|
528
|
+
"text/plain",
|
|
529
|
+
(storeName) => storeManager.listPolicies(storeName),
|
|
530
|
+
(storeName, id) => `Cedar policy "${id}" in store "${storeName}"`,
|
|
531
|
+
),
|
|
532
|
+
}),
|
|
533
|
+
async (uri, variables) => {
|
|
534
|
+
const storeName = variables["store"] as string;
|
|
535
|
+
const policyId = variables["policy_id"] as string;
|
|
536
|
+
try {
|
|
537
|
+
const content = storeManager.readPolicy(storeName, policyId);
|
|
538
|
+
return {
|
|
539
|
+
contents: [{
|
|
540
|
+
uri: uri.toString(),
|
|
541
|
+
mimeType: "text/plain",
|
|
542
|
+
text: content,
|
|
543
|
+
}],
|
|
544
|
+
};
|
|
545
|
+
} catch (e) {
|
|
546
|
+
return {
|
|
547
|
+
contents: [{
|
|
548
|
+
uri: uri.toString(),
|
|
549
|
+
mimeType: "application/json",
|
|
550
|
+
text: JSON.stringify({ error: e instanceof Error ? e.message : String(e) }),
|
|
551
|
+
}],
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
);
|
|
556
|
+
|
|
557
|
+
// Read schema for a store: cedar://schema/{store}
|
|
558
|
+
server.resource(
|
|
559
|
+
"cedar-schema",
|
|
560
|
+
new ResourceTemplate("cedar://schema/{store}", { list: listSchemaResources() }),
|
|
561
|
+
async (uri, variables) => {
|
|
562
|
+
const storeName = variables["store"] as string;
|
|
563
|
+
try {
|
|
564
|
+
const content = storeManager.readSchema(storeName);
|
|
565
|
+
return {
|
|
566
|
+
contents: [{
|
|
567
|
+
uri: uri.toString(),
|
|
568
|
+
mimeType: "text/plain",
|
|
569
|
+
text: content,
|
|
570
|
+
}],
|
|
571
|
+
};
|
|
572
|
+
} catch (e) {
|
|
573
|
+
return {
|
|
574
|
+
contents: [{
|
|
575
|
+
uri: uri.toString(),
|
|
576
|
+
mimeType: "application/json",
|
|
577
|
+
text: JSON.stringify({ error: e instanceof Error ? e.message : String(e) }),
|
|
578
|
+
}],
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
);
|
|
583
|
+
|
|
584
|
+
// List all template IDs in a store: cedar://templates/{store}
|
|
585
|
+
server.resource(
|
|
586
|
+
"cedar-templates-list",
|
|
587
|
+
new ResourceTemplate("cedar://templates/{store}", {
|
|
588
|
+
list: listIndexResources("templates", "application/json", (s) => `JSON list of template IDs in store "${s}"`),
|
|
589
|
+
}),
|
|
590
|
+
async (_uri, variables) => {
|
|
591
|
+
const storeName = variables["store"] as string;
|
|
592
|
+
try {
|
|
593
|
+
const ids = storeManager.listTemplates(storeName);
|
|
594
|
+
return { contents: [{ uri: `cedar://templates/${storeName}`, mimeType: "application/json", text: JSON.stringify(ids) }] };
|
|
595
|
+
} catch (e) {
|
|
596
|
+
return { contents: [{ uri: `cedar://templates/${storeName}`, mimeType: "application/json", text: JSON.stringify({ error: e instanceof Error ? e.message : String(e) }) }] };
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
);
|
|
600
|
+
|
|
601
|
+
// Read a single template: cedar://templates/{store}/{template_id}
|
|
602
|
+
server.resource(
|
|
603
|
+
"cedar-template",
|
|
604
|
+
new ResourceTemplate("cedar://templates/{store}/{template_id}", {
|
|
605
|
+
list: listItemResources(
|
|
606
|
+
"templates",
|
|
607
|
+
"text/plain",
|
|
608
|
+
(storeName) => storeManager.listTemplates(storeName),
|
|
609
|
+
(storeName, id) => `Cedar template "${id}" in store "${storeName}"`,
|
|
610
|
+
),
|
|
611
|
+
}),
|
|
612
|
+
async (uri, variables) => {
|
|
613
|
+
const storeName = variables["store"] as string;
|
|
614
|
+
const templateId = variables["template_id"] as string;
|
|
615
|
+
try {
|
|
616
|
+
const content = storeManager.readTemplate(storeName, templateId);
|
|
617
|
+
return { contents: [{ uri: uri.toString(), mimeType: "text/plain", text: content }] };
|
|
618
|
+
} catch (e) {
|
|
619
|
+
return { contents: [{ uri: uri.toString(), mimeType: "application/json", text: JSON.stringify({ error: e instanceof Error ? e.message : String(e) }) }] };
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
);
|
|
623
|
+
|
|
624
|
+
// List all template link IDs: cedar://template-links/{store}
|
|
625
|
+
server.resource(
|
|
626
|
+
"cedar-template-links-list",
|
|
627
|
+
new ResourceTemplate("cedar://template-links/{store}", {
|
|
628
|
+
list: listIndexResources("template-links", "application/json", (s) => `JSON list of template-link IDs in store "${s}"`),
|
|
629
|
+
}),
|
|
630
|
+
async (_uri, variables) => {
|
|
631
|
+
const storeName = variables["store"] as string;
|
|
632
|
+
try {
|
|
633
|
+
const ids = storeManager.listTemplateLinks(storeName);
|
|
634
|
+
return { contents: [{ uri: `cedar://template-links/${storeName}`, mimeType: "application/json", text: JSON.stringify(ids) }] };
|
|
635
|
+
} catch (e) {
|
|
636
|
+
return { contents: [{ uri: `cedar://template-links/${storeName}`, mimeType: "application/json", text: JSON.stringify({ error: e instanceof Error ? e.message : String(e) }) }] };
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
);
|
|
640
|
+
|
|
641
|
+
// Read a single template link: cedar://template-links/{store}/{link_id}
|
|
642
|
+
server.resource(
|
|
643
|
+
"cedar-template-link",
|
|
644
|
+
new ResourceTemplate("cedar://template-links/{store}/{link_id}", {
|
|
645
|
+
list: listItemResources(
|
|
646
|
+
"template-links",
|
|
647
|
+
"application/json",
|
|
648
|
+
(storeName) => storeManager.listTemplateLinks(storeName),
|
|
649
|
+
(storeName, id) => `Cedar template-link "${id}" in store "${storeName}"`,
|
|
650
|
+
),
|
|
651
|
+
}),
|
|
652
|
+
async (uri, variables) => {
|
|
653
|
+
const storeName = variables["store"] as string;
|
|
654
|
+
const linkId = variables["link_id"] as string;
|
|
655
|
+
try {
|
|
656
|
+
const data = storeManager.readTemplateLink(storeName, linkId);
|
|
657
|
+
return { contents: [{ uri: uri.toString(), mimeType: "application/json", text: JSON.stringify(data, null, 2) }] };
|
|
658
|
+
} catch (e) {
|
|
659
|
+
return { contents: [{ uri: uri.toString(), mimeType: "application/json", text: JSON.stringify({ error: e instanceof Error ? e.message : String(e) }) }] };
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
);
|
|
663
|
+
|
|
664
|
+
// List entity files in a store: cedar://entities/{store}
|
|
665
|
+
server.resource(
|
|
666
|
+
"cedar-entities-list",
|
|
667
|
+
new ResourceTemplate("cedar://entities/{store}", {
|
|
668
|
+
list: listIndexResources("entities", "application/json", (s) => `JSON list of entity-file IDs in store "${s}"`),
|
|
669
|
+
}),
|
|
670
|
+
async (_uri, variables) => {
|
|
671
|
+
const storeName = variables["store"] as string;
|
|
672
|
+
try {
|
|
673
|
+
const ids = storeManager.listEntities(storeName);
|
|
674
|
+
return { contents: [{ uri: `cedar://entities/${storeName}`, mimeType: "application/json", text: JSON.stringify(ids) }] };
|
|
675
|
+
} catch (e) {
|
|
676
|
+
return { contents: [{ uri: `cedar://entities/${storeName}`, mimeType: "application/json", text: JSON.stringify({ error: e instanceof Error ? e.message : String(e) }) }] };
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
);
|
|
680
|
+
|
|
681
|
+
// Read a single entity file: cedar://entities/{store}/{file_id}
|
|
682
|
+
server.resource(
|
|
683
|
+
"cedar-entities",
|
|
684
|
+
new ResourceTemplate("cedar://entities/{store}/{file_id}", {
|
|
685
|
+
list: listItemResources(
|
|
686
|
+
"entities",
|
|
687
|
+
"application/json",
|
|
688
|
+
(storeName) => storeManager.listEntities(storeName),
|
|
689
|
+
(storeName, id) => `Cedar entities file "${id}" in store "${storeName}"`,
|
|
690
|
+
),
|
|
691
|
+
}),
|
|
692
|
+
async (uri, variables) => {
|
|
693
|
+
const storeName = variables["store"] as string;
|
|
694
|
+
const fileId = variables["file_id"] as string;
|
|
695
|
+
try {
|
|
696
|
+
const content = storeManager.readEntities(storeName, fileId);
|
|
697
|
+
return { contents: [{ uri: uri.toString(), mimeType: "application/json", text: content }] };
|
|
698
|
+
} catch (e) {
|
|
699
|
+
return { contents: [{ uri: uri.toString(), mimeType: "application/json", text: JSON.stringify({ error: e instanceof Error ? e.message : String(e) }) }] };
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
);
|
|
703
|
+
|
|
704
|
+
// ─── MCP Prompts: pre-canned templates the client surfaces as slash commands ──
|
|
705
|
+
|
|
706
|
+
for (const p of PROMPT_DEFINITIONS) {
|
|
707
|
+
server.prompt(p.name, p.description, p.argsSchema, p.handler);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
return server;
|
|
711
|
+
}
|