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