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/SECURITY.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Security
|
|
2
|
+
|
|
3
|
+
## Trust boundary
|
|
4
|
+
|
|
5
|
+
`cedar-mcp-server` is a local MCP server. It runs as a child process on the same machine as the MCP client (Claude Code, Claude Desktop, Cursor, etc.). The trust model is: the client and its workspace are trusted. The server does not authenticate callers, enforce rate limits, or apply any network-level access controls.
|
|
6
|
+
|
|
7
|
+
Do not expose this server over a network without an authentication layer in front of it. The stdio transport (`npx cedar-mcp-server`) is local-only by design.
|
|
8
|
+
|
|
9
|
+
## What the server does
|
|
10
|
+
|
|
11
|
+
- Evaluates Cedar policies and authorization requests in-process using `@cedar-policy/cedar-wasm`. No network calls.
|
|
12
|
+
- Reads `.cedar` and `.cedarschema` / `.json` files from directories configured as MCP Roots. Reads only; never writes to the filesystem.
|
|
13
|
+
- Calls the MCP client's `sampling/createMessage` capability for `cedar_advise`. The client decides whether to fulfill the sampling request and which model to use.
|
|
14
|
+
|
|
15
|
+
## What the server does NOT do
|
|
16
|
+
|
|
17
|
+
- Makes no calls to AWS APIs or Amazon Verified Permissions.
|
|
18
|
+
- Makes no outbound network requests of any kind.
|
|
19
|
+
- Does not write, modify, or delete files on disk.
|
|
20
|
+
- Does not execute arbitrary code from policy inputs.
|
|
21
|
+
- Does not store policy text, entity data, or authorization results anywhere outside the MCP conversation.
|
|
22
|
+
|
|
23
|
+
## Input validation
|
|
24
|
+
|
|
25
|
+
**Policy IDs.** Policy files are loaded from the `policies/` subdirectory of each configured root. Policy IDs are derived from filenames and validated against `^[a-zA-Z0-9_-]+$` before use. IDs containing path separators or `..` sequences are rejected. This prevents directory traversal attacks on the policy store.
|
|
26
|
+
|
|
27
|
+
**Root URIs.** Only `file://` URIs are accepted as MCP Roots. Non-`file://` URIs (e.g. `http://`) are rejected at load time.
|
|
28
|
+
|
|
29
|
+
**Policy and schema text.** Cedar policies and schemas passed as inline text or read from disk are evaluated inside the `@cedar-policy/cedar-wasm` WASM sandbox. They cannot execute arbitrary code. Malformed input returns a structured error, not a process crash.
|
|
30
|
+
|
|
31
|
+
**Entity JSON.** Entity data is parsed and passed to the Cedar evaluator. It is not executed. Excessively large entity payloads may cause slow evaluation but do not create a code execution path.
|
|
32
|
+
|
|
33
|
+
## Reporting a vulnerability
|
|
34
|
+
|
|
35
|
+
If you find a security issue in this project, open a [GitHub Issue](https://github.com/Pigius/cedar-mcp-server/issues) with the label `security`. For issues you prefer not to disclose publicly, email the maintainer directly (contact via the GitHub profile).
|
|
36
|
+
|
|
37
|
+
Please include: a description of the issue, steps to reproduce, and the potential impact.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { type Server as HttpServer } from "node:http";
|
|
2
|
+
export interface HttpServerOptions {
|
|
3
|
+
port: number;
|
|
4
|
+
host?: string;
|
|
5
|
+
roots?: Array<{
|
|
6
|
+
name: string;
|
|
7
|
+
path: string;
|
|
8
|
+
}>;
|
|
9
|
+
/** Max concurrent HTTP sessions. New sessions over the cap receive 503.
|
|
10
|
+
* Default: 100. Override via env CEDAR_MAX_HTTP_SESSIONS or this option. */
|
|
11
|
+
maxSessions?: number;
|
|
12
|
+
/** Idle session TTL in milliseconds. A session unused for longer is evicted
|
|
13
|
+
* by the reaper. Default: 30 minutes. Override via env
|
|
14
|
+
* CEDAR_HTTP_SESSION_IDLE_TTL_MS or this option. */
|
|
15
|
+
sessionIdleTtlMs?: number;
|
|
16
|
+
/** How often the reaper scans for stale sessions. Default: 60 seconds.
|
|
17
|
+
* Mostly relevant for tests; production deploys can leave the default. */
|
|
18
|
+
reaperIntervalMs?: number;
|
|
19
|
+
}
|
|
20
|
+
export interface RunningHttpServer {
|
|
21
|
+
httpServer: HttpServer;
|
|
22
|
+
port: number;
|
|
23
|
+
host: string;
|
|
24
|
+
close(): Promise<void>;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Boot cedar-mcp-server in Streamable HTTP mode.
|
|
28
|
+
*
|
|
29
|
+
* Per-session model: each MCP session gets its own McpServer + transport pair.
|
|
30
|
+
* The Streamable HTTP spec mandates this because each session has independent
|
|
31
|
+
* protocol state (initialized handshake, message history, capabilities).
|
|
32
|
+
*
|
|
33
|
+
* Shared across all sessions: the storeManager singleton. The deployment model
|
|
34
|
+
* is "one server per policy-store set, many team clients all seeing the same
|
|
35
|
+
* roots." Roots are deployer-configured via CLI flags at startup; client
|
|
36
|
+
* listRoots() is NOT called in HTTP mode. For per-tenant isolation, deploy
|
|
37
|
+
* multiple processes.
|
|
38
|
+
*
|
|
39
|
+
* Resource management:
|
|
40
|
+
* - Max-sessions cap: new sessions over the limit receive HTTP 503. This is
|
|
41
|
+
* backpressure, not eviction — existing sessions are not interrupted to
|
|
42
|
+
* make room. Default 100; override via maxSessions option or
|
|
43
|
+
* CEDAR_MAX_HTTP_SESSIONS env var.
|
|
44
|
+
* - Idle TTL: a reaper scans periodically and evicts sessions whose last
|
|
45
|
+
* observed request exceeds the TTL. Default 30 min idle / 60s scan;
|
|
46
|
+
* override via sessionIdleTtlMs / reaperIntervalMs options or env vars.
|
|
47
|
+
* This catches the case where transport.onclose doesn't fire (e.g.,
|
|
48
|
+
* network partition, TCP RST) and prevents the sessions map from leaking.
|
|
49
|
+
*
|
|
50
|
+
* Session lifecycle:
|
|
51
|
+
* 1. Client POSTs to /mcp without Mcp-Session-Id → server creates a new
|
|
52
|
+
* session (transport + server pair), runs initialize, returns the
|
|
53
|
+
* session ID in the response header. New sessions over maxSessions are
|
|
54
|
+
* rejected with HTTP 503.
|
|
55
|
+
* 2. Subsequent requests with that Mcp-Session-Id route to the same pair
|
|
56
|
+
* and refresh its lastActiveAt timestamp.
|
|
57
|
+
* 3. transport.onclose fires on graceful disconnect → session removed.
|
|
58
|
+
* 4. Reaper sweeps idle sessions out periodically as a backstop.
|
|
59
|
+
*/
|
|
60
|
+
export declare function startHttpServer(options: HttpServerOptions): Promise<RunningHttpServer>;
|
|
61
|
+
//# sourceMappingURL=http-server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http-server.d.ts","sourceRoot":"","sources":["../src/http-server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoC,KAAK,MAAM,IAAI,UAAU,EAAE,MAAM,WAAW,CAAC;AAQxF,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC9C;iFAC6E;IAC7E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;yDAEqD;IACrD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;+EAC2E;IAC3E,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,UAAU,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAcD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,wBAAsB,eAAe,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAiK5F"}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { createServer as createHttpServer } 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
|
+
const DEFAULT_MAX_SESSIONS = 100;
|
|
9
|
+
const DEFAULT_SESSION_IDLE_TTL_MS = 30 * 60 * 1000; // 30 min
|
|
10
|
+
const DEFAULT_REAPER_INTERVAL_MS = 60 * 1000; // 1 min
|
|
11
|
+
/**
|
|
12
|
+
* Boot cedar-mcp-server in Streamable HTTP mode.
|
|
13
|
+
*
|
|
14
|
+
* Per-session model: each MCP session gets its own McpServer + transport pair.
|
|
15
|
+
* The Streamable HTTP spec mandates this because each session has independent
|
|
16
|
+
* protocol state (initialized handshake, message history, capabilities).
|
|
17
|
+
*
|
|
18
|
+
* Shared across all sessions: the storeManager singleton. The deployment model
|
|
19
|
+
* is "one server per policy-store set, many team clients all seeing the same
|
|
20
|
+
* roots." Roots are deployer-configured via CLI flags at startup; client
|
|
21
|
+
* listRoots() is NOT called in HTTP mode. For per-tenant isolation, deploy
|
|
22
|
+
* multiple processes.
|
|
23
|
+
*
|
|
24
|
+
* Resource management:
|
|
25
|
+
* - Max-sessions cap: new sessions over the limit receive HTTP 503. This is
|
|
26
|
+
* backpressure, not eviction — existing sessions are not interrupted to
|
|
27
|
+
* make room. Default 100; override via maxSessions option or
|
|
28
|
+
* CEDAR_MAX_HTTP_SESSIONS env var.
|
|
29
|
+
* - Idle TTL: a reaper scans periodically and evicts sessions whose last
|
|
30
|
+
* observed request exceeds the TTL. Default 30 min idle / 60s scan;
|
|
31
|
+
* override via sessionIdleTtlMs / reaperIntervalMs options or env vars.
|
|
32
|
+
* This catches the case where transport.onclose doesn't fire (e.g.,
|
|
33
|
+
* network partition, TCP RST) and prevents the sessions map from leaking.
|
|
34
|
+
*
|
|
35
|
+
* Session lifecycle:
|
|
36
|
+
* 1. Client POSTs to /mcp without Mcp-Session-Id → server creates a new
|
|
37
|
+
* session (transport + server pair), runs initialize, returns the
|
|
38
|
+
* session ID in the response header. New sessions over maxSessions are
|
|
39
|
+
* rejected with HTTP 503.
|
|
40
|
+
* 2. Subsequent requests with that Mcp-Session-Id route to the same pair
|
|
41
|
+
* and refresh its lastActiveAt timestamp.
|
|
42
|
+
* 3. transport.onclose fires on graceful disconnect → session removed.
|
|
43
|
+
* 4. Reaper sweeps idle sessions out periodically as a backstop.
|
|
44
|
+
*/
|
|
45
|
+
export async function startHttpServer(options) {
|
|
46
|
+
const host = options.host ?? "127.0.0.1";
|
|
47
|
+
const maxSessions = options.maxSessions
|
|
48
|
+
?? (Number(process.env.CEDAR_MAX_HTTP_SESSIONS) || DEFAULT_MAX_SESSIONS);
|
|
49
|
+
const sessionIdleTtlMs = options.sessionIdleTtlMs
|
|
50
|
+
?? (Number(process.env.CEDAR_HTTP_SESSION_IDLE_TTL_MS) || DEFAULT_SESSION_IDLE_TTL_MS);
|
|
51
|
+
const reaperIntervalMs = options.reaperIntervalMs ?? DEFAULT_REAPER_INTERVAL_MS;
|
|
52
|
+
// Load deployer-configured roots before the server starts accepting traffic.
|
|
53
|
+
// Shared by all sessions via the storeManager singleton.
|
|
54
|
+
if (options.roots && options.roots.length > 0) {
|
|
55
|
+
storeManager.loadFromRoots(options.roots.map((r) => ({ uri: `file://${r.path}`, name: r.name })));
|
|
56
|
+
}
|
|
57
|
+
const sessions = new Map();
|
|
58
|
+
async function createSession() {
|
|
59
|
+
const transport = new StreamableHTTPServerTransport({
|
|
60
|
+
sessionIdGenerator: () => randomUUID(),
|
|
61
|
+
});
|
|
62
|
+
const server = createServer();
|
|
63
|
+
await server.connect(transport);
|
|
64
|
+
transport.onclose = () => {
|
|
65
|
+
// Only drop the session-map entry here. Do NOT call server.close():
|
|
66
|
+
// server.close() triggers transport close, which fires this handler
|
|
67
|
+
// again. The McpServer becomes unreferenced and gets GC'd. The shared
|
|
68
|
+
// WASM module lives at process scope, not per-session.
|
|
69
|
+
const sid = transport.sessionId;
|
|
70
|
+
if (sid && sessions.has(sid)) {
|
|
71
|
+
sessions.delete(sid);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
return { transport, server, lastActiveAt: Date.now() };
|
|
75
|
+
}
|
|
76
|
+
// Reaper: evict sessions whose lastActiveAt is older than the idle TTL.
|
|
77
|
+
// Collect-then-delete pattern avoids mid-iteration map mutation when the
|
|
78
|
+
// transport.close() callback re-enters the sessions map.
|
|
79
|
+
const reaper = setInterval(() => {
|
|
80
|
+
const cutoff = Date.now() - sessionIdleTtlMs;
|
|
81
|
+
const toEvict = [];
|
|
82
|
+
for (const [sid, sess] of sessions.entries()) {
|
|
83
|
+
if (sess.lastActiveAt < cutoff)
|
|
84
|
+
toEvict.push(sid);
|
|
85
|
+
}
|
|
86
|
+
for (const sid of toEvict) {
|
|
87
|
+
const sess = sessions.get(sid);
|
|
88
|
+
if (sess) {
|
|
89
|
+
sessions.delete(sid);
|
|
90
|
+
void sess.transport.close().catch(() => { });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}, reaperIntervalMs);
|
|
94
|
+
// Don't keep the Node event loop alive just for the reaper — let the
|
|
95
|
+
// process exit cleanly when the HTTP server closes.
|
|
96
|
+
reaper.unref();
|
|
97
|
+
const app = createMcpExpressApp({ host });
|
|
98
|
+
app.use(express.json({ limit: "10mb" }));
|
|
99
|
+
app.post("/mcp", async (req, res) => {
|
|
100
|
+
try {
|
|
101
|
+
const sessionIdHeader = req.headers["mcp-session-id"];
|
|
102
|
+
const sessionId = Array.isArray(sessionIdHeader) ? sessionIdHeader[0] : sessionIdHeader;
|
|
103
|
+
let session;
|
|
104
|
+
if (sessionId && sessions.has(sessionId)) {
|
|
105
|
+
session = sessions.get(sessionId);
|
|
106
|
+
if (session)
|
|
107
|
+
session.lastActiveAt = Date.now();
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
// New session — apply backpressure if at cap. We do this BEFORE
|
|
111
|
+
// creating the transport/server pair to avoid leaking resources
|
|
112
|
+
// on rejection.
|
|
113
|
+
if (sessions.size >= maxSessions) {
|
|
114
|
+
res.status(503).json({
|
|
115
|
+
error: "Too many concurrent MCP sessions",
|
|
116
|
+
message: `Server is at the max-session limit of ${maxSessions}. Try again later, or run multiple processes for higher capacity.`,
|
|
117
|
+
active_sessions: sessions.size,
|
|
118
|
+
max_sessions: maxSessions,
|
|
119
|
+
});
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
session = await createSession();
|
|
123
|
+
}
|
|
124
|
+
if (!session) {
|
|
125
|
+
res.status(400).json({ error: "Could not establish MCP session" });
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
await session.transport.handleRequest(req, res, req.body);
|
|
129
|
+
// After the initialize request completes the transport will have set
|
|
130
|
+
// its sessionId. Register the session under that ID so subsequent
|
|
131
|
+
// requests find it.
|
|
132
|
+
const sidAfter = session.transport.sessionId;
|
|
133
|
+
if (sidAfter && !sessions.has(sidAfter)) {
|
|
134
|
+
sessions.set(sidAfter, session);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
catch (e) {
|
|
138
|
+
if (!res.headersSent) {
|
|
139
|
+
res.status(500).json({
|
|
140
|
+
error: "Internal MCP transport error",
|
|
141
|
+
message: e instanceof Error ? e.message : String(e),
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
app.get("/health", (_req, res) => {
|
|
147
|
+
res.json({
|
|
148
|
+
status: "ok",
|
|
149
|
+
transport: "streamable-http",
|
|
150
|
+
mode: "stateful",
|
|
151
|
+
active_sessions: sessions.size,
|
|
152
|
+
max_sessions: maxSessions,
|
|
153
|
+
session_idle_ttl_ms: sessionIdleTtlMs,
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
const httpServer = createHttpServer(app);
|
|
157
|
+
await new Promise((resolve, reject) => {
|
|
158
|
+
httpServer.once("error", reject);
|
|
159
|
+
httpServer.listen(options.port, host, () => {
|
|
160
|
+
httpServer.off("error", reject);
|
|
161
|
+
resolve();
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
// eslint-disable-next-line no-console
|
|
165
|
+
console.error(`[cedar-mcp-server] Streamable HTTP listening on http://${host}:${options.port}/mcp (max_sessions=${maxSessions}, idle_ttl=${Math.round(sessionIdleTtlMs / 1000)}s)`);
|
|
166
|
+
if (options.roots && options.roots.length > 0) {
|
|
167
|
+
// eslint-disable-next-line no-console
|
|
168
|
+
console.error(`[cedar-mcp-server] Loaded ${options.roots.length} root(s): ${options.roots.map((r) => r.name).join(", ")}`);
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
// eslint-disable-next-line no-console
|
|
172
|
+
console.error("[cedar-mcp-server] WARNING: no --root flags supplied; tools that depend on a configured store will error.");
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
httpServer,
|
|
176
|
+
port: options.port,
|
|
177
|
+
host,
|
|
178
|
+
async close() {
|
|
179
|
+
clearInterval(reaper);
|
|
180
|
+
// Close all sessions first so each McpServer cleans up its WASM state
|
|
181
|
+
for (const session of sessions.values()) {
|
|
182
|
+
try {
|
|
183
|
+
await session.server.close();
|
|
184
|
+
}
|
|
185
|
+
catch { /* ignore */ }
|
|
186
|
+
}
|
|
187
|
+
sessions.clear();
|
|
188
|
+
await new Promise((resolve, reject) => {
|
|
189
|
+
httpServer.close((err) => (err ? reject(err) : resolve()));
|
|
190
|
+
});
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
//# sourceMappingURL=http-server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http-server.js","sourceRoot":"","sources":["../src/http-server.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Synchronously populate StoreManager with the cwd-fallback store, if the
|
|
4
|
+
* cwd looks like a Cedar workspace. Returns the loaded root descriptor when
|
|
5
|
+
* a store was loaded, or null otherwise.
|
|
6
|
+
*
|
|
7
|
+
* Round 5 dogfood (Scenario E) found that emitting `notifications/resources/list_changed`
|
|
8
|
+
* AFTER an async cwd-fallback (kickoff-11 11a) was insufficient: Claude Code's
|
|
9
|
+
* `listMcpResources` does not honor `list_changed` for cache invalidation, so a
|
|
10
|
+
* client that snapshots `resources/list` once on the initialize response stays
|
|
11
|
+
* stuck on the empty pre-fallback snapshot regardless of any later notification.
|
|
12
|
+
*
|
|
13
|
+
* The fix is structural: populate StoreManager BEFORE the transport accepts
|
|
14
|
+
* any client requests. `process.cwd()` is available at startup; there is no
|
|
15
|
+
* need to wait for the transport. By the time the client can send any
|
|
16
|
+
* request, the store already exists.
|
|
17
|
+
*
|
|
18
|
+
* Security: rejects filesystem-root cwds (`/`) and any cwd whose basename is
|
|
19
|
+
* empty after normalization. Without this guard, the cwd-fallback would push
|
|
20
|
+
* an empty-path root into StoreManager, and the per-store path sandbox
|
|
21
|
+
* (`isPathAllowed`, which uses `startsWith(store.path)`) would return true
|
|
22
|
+
* for every filesystem path. StoreManager.loadFromRoots also refuses
|
|
23
|
+
* empty-path roots as a second layer of defense, but rejecting here keeps
|
|
24
|
+
* the cwd-fallback's intent narrow (workspace-shaped cwds only).
|
|
25
|
+
*
|
|
26
|
+
* Exported so unit tests can exercise it without spawning a stdio process.
|
|
27
|
+
*/
|
|
28
|
+
export declare function populateCwdFallback(cwd: string): {
|
|
29
|
+
uri: string;
|
|
30
|
+
name: string;
|
|
31
|
+
} | null;
|
|
32
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAyBA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAMrF"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { RootsListChangedNotificationSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import { join, basename } from "node:path";
|
|
6
|
+
import { createServer } from "./server.js";
|
|
7
|
+
import { storeManager } from "./resources/store-manager.js";
|
|
8
|
+
import { startHttpServer } from "./http-server.js";
|
|
9
|
+
/**
|
|
10
|
+
* A directory "looks like a Cedar workspace" if it has at least one of:
|
|
11
|
+
* - schema.cedarschema (preferred)
|
|
12
|
+
* - schema.json (fallback)
|
|
13
|
+
* - policies/ (per-file policies layout)
|
|
14
|
+
*
|
|
15
|
+
* This is the same convention StoreManager uses to read a loaded root.
|
|
16
|
+
*/
|
|
17
|
+
function looksLikeCedarWorkspace(path) {
|
|
18
|
+
return (existsSync(join(path, "schema.cedarschema")) ||
|
|
19
|
+
existsSync(join(path, "schema.json")) ||
|
|
20
|
+
existsSync(join(path, "policies")));
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Synchronously populate StoreManager with the cwd-fallback store, if the
|
|
24
|
+
* cwd looks like a Cedar workspace. Returns the loaded root descriptor when
|
|
25
|
+
* a store was loaded, or null otherwise.
|
|
26
|
+
*
|
|
27
|
+
* Round 5 dogfood (Scenario E) found that emitting `notifications/resources/list_changed`
|
|
28
|
+
* AFTER an async cwd-fallback (kickoff-11 11a) was insufficient: Claude Code's
|
|
29
|
+
* `listMcpResources` does not honor `list_changed` for cache invalidation, so a
|
|
30
|
+
* client that snapshots `resources/list` once on the initialize response stays
|
|
31
|
+
* stuck on the empty pre-fallback snapshot regardless of any later notification.
|
|
32
|
+
*
|
|
33
|
+
* The fix is structural: populate StoreManager BEFORE the transport accepts
|
|
34
|
+
* any client requests. `process.cwd()` is available at startup; there is no
|
|
35
|
+
* need to wait for the transport. By the time the client can send any
|
|
36
|
+
* request, the store already exists.
|
|
37
|
+
*
|
|
38
|
+
* Security: rejects filesystem-root cwds (`/`) and any cwd whose basename is
|
|
39
|
+
* empty after normalization. Without this guard, the cwd-fallback would push
|
|
40
|
+
* an empty-path root into StoreManager, and the per-store path sandbox
|
|
41
|
+
* (`isPathAllowed`, which uses `startsWith(store.path)`) would return true
|
|
42
|
+
* for every filesystem path. StoreManager.loadFromRoots also refuses
|
|
43
|
+
* empty-path roots as a second layer of defense, but rejecting here keeps
|
|
44
|
+
* the cwd-fallback's intent narrow (workspace-shaped cwds only).
|
|
45
|
+
*
|
|
46
|
+
* Exported so unit tests can exercise it without spawning a stdio process.
|
|
47
|
+
*/
|
|
48
|
+
export function populateCwdFallback(cwd) {
|
|
49
|
+
if (cwd === "/" || basename(cwd).length === 0)
|
|
50
|
+
return null;
|
|
51
|
+
if (!looksLikeCedarWorkspace(cwd))
|
|
52
|
+
return null;
|
|
53
|
+
const cwdRoot = { uri: `file://${cwd}`, name: basename(cwd) };
|
|
54
|
+
storeManager.loadFromRoots([cwdRoot]);
|
|
55
|
+
return cwdRoot;
|
|
56
|
+
}
|
|
57
|
+
function parseArgs(argv) {
|
|
58
|
+
const args = argv.slice(2);
|
|
59
|
+
const out = { mode: "stdio", roots: [] };
|
|
60
|
+
for (let i = 0; i < args.length; i++) {
|
|
61
|
+
const a = args[i];
|
|
62
|
+
if (a === "--help" || a === "-h") {
|
|
63
|
+
return { mode: "help", roots: [] };
|
|
64
|
+
}
|
|
65
|
+
else if (a === "--http") {
|
|
66
|
+
const portArg = args[i + 1];
|
|
67
|
+
if (!portArg || portArg.startsWith("--")) {
|
|
68
|
+
return { mode: "help", roots: [], error: "--http requires a port number (e.g. --http 3000)" };
|
|
69
|
+
}
|
|
70
|
+
const port = Number(portArg);
|
|
71
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
72
|
+
return { mode: "help", roots: [], error: `--http port must be an integer between 1 and 65535 (got '${portArg}')` };
|
|
73
|
+
}
|
|
74
|
+
out.mode = "http";
|
|
75
|
+
out.port = port;
|
|
76
|
+
i++;
|
|
77
|
+
}
|
|
78
|
+
else if (a === "--host") {
|
|
79
|
+
const hostArg = args[i + 1];
|
|
80
|
+
if (!hostArg || hostArg.startsWith("--")) {
|
|
81
|
+
return { mode: "help", roots: [], error: "--host requires a hostname (e.g. --host 0.0.0.0)" };
|
|
82
|
+
}
|
|
83
|
+
out.host = hostArg;
|
|
84
|
+
i++;
|
|
85
|
+
}
|
|
86
|
+
else if (a === "--root") {
|
|
87
|
+
const rootArg = args[i + 1];
|
|
88
|
+
if (!rootArg || rootArg.startsWith("--")) {
|
|
89
|
+
return { mode: "help", roots: [], error: "--root requires a name=path value (e.g. --root production=/etc/cedar/prod)" };
|
|
90
|
+
}
|
|
91
|
+
const eq = rootArg.indexOf("=");
|
|
92
|
+
if (eq <= 0 || eq === rootArg.length - 1) {
|
|
93
|
+
return { mode: "help", roots: [], error: `--root must be name=path (got '${rootArg}')` };
|
|
94
|
+
}
|
|
95
|
+
const name = rootArg.slice(0, eq);
|
|
96
|
+
const path = rootArg.slice(eq + 1);
|
|
97
|
+
out.roots.push({ name, path });
|
|
98
|
+
i++;
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
return { mode: "help", roots: [], error: `Unknown argument: ${a}` };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (out.mode === "stdio" && out.roots.length > 0) {
|
|
105
|
+
return { mode: "help", roots: [], error: "--root flags are only used with --http mode; in stdio mode roots come from the MCP client" };
|
|
106
|
+
}
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
function printUsage(error) {
|
|
110
|
+
if (error) {
|
|
111
|
+
// eslint-disable-next-line no-console
|
|
112
|
+
console.error(`Error: ${error}\n`);
|
|
113
|
+
}
|
|
114
|
+
// eslint-disable-next-line no-console
|
|
115
|
+
console.error(`cedar-mcp-server — MCP server for Cedar policy language
|
|
116
|
+
|
|
117
|
+
Usage:
|
|
118
|
+
cedar-mcp-server Start in stdio mode (default; for npx/Claude Code)
|
|
119
|
+
cedar-mcp-server --http <port> [options] Start in Streamable HTTP mode for shared team deployment
|
|
120
|
+
|
|
121
|
+
HTTP options:
|
|
122
|
+
--http <port> Listen port (1-65535)
|
|
123
|
+
--host <host> Bind host (default: 127.0.0.1; use 0.0.0.0 for non-localhost; you handle auth via reverse proxy)
|
|
124
|
+
--root <name>=<path> Repeatable; deployer-configured policy store ("production=/etc/cedar/prod")
|
|
125
|
+
|
|
126
|
+
Notes:
|
|
127
|
+
- Stdio mode: roots are negotiated with the MCP client via listRoots(); --root flags are not allowed.
|
|
128
|
+
- HTTP mode: stateful Streamable HTTP transport with session IDs. ALL clients share the same roots and policy stores (deployment model: one server per policy-store set). For per-tenant isolation, run multiple processes.
|
|
129
|
+
- HTTP mode default-binds to localhost with DNS-rebinding protection. Non-localhost binding is on you to secure (reverse proxy + auth).
|
|
130
|
+
|
|
131
|
+
Examples:
|
|
132
|
+
cedar-mcp-server
|
|
133
|
+
cedar-mcp-server --http 3000 --root production=/etc/cedar/production --root staging=/etc/cedar/staging
|
|
134
|
+
cedar-mcp-server --http 3000 --host 0.0.0.0 --root prod=/etc/cedar/prod
|
|
135
|
+
`);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Reconcile StoreManager state with whatever the MCP client advertises via
|
|
139
|
+
* `listRoots()`. Called from `oninitialized` AND on every
|
|
140
|
+
* `notifications/roots/list_changed` from the client.
|
|
141
|
+
*
|
|
142
|
+
* Precedence rules:
|
|
143
|
+
* - Client-advertised roots REPLACE any sync-loaded cwd-fallback. A client
|
|
144
|
+
* that explicitly advertises roots is stating authoritative intent; the
|
|
145
|
+
* cwd-fallback was an "if you didn't tell me anything" default.
|
|
146
|
+
* - Client returns ZERO roots (or doesn't support listRoots): preserve the
|
|
147
|
+
* sync-loaded cwd-fallback. We do NOT call `loadFromRoots([])` here
|
|
148
|
+
* because StoreManager.loadFromRoots clears the store as its first step,
|
|
149
|
+
* which would wipe the cwd-fallback populated synchronously at startup.
|
|
150
|
+
*
|
|
151
|
+
* `sendResourceListChanged` always fires at the end: cache-aware clients use
|
|
152
|
+
* it to refetch when the store membership changes. Idempotent if nothing
|
|
153
|
+
* actually changed.
|
|
154
|
+
*/
|
|
155
|
+
async function loadRootsStdio(server) {
|
|
156
|
+
let clientRoots = [];
|
|
157
|
+
let clientSupportsRoots = true;
|
|
158
|
+
try {
|
|
159
|
+
const result = await server.server.listRoots();
|
|
160
|
+
clientRoots = result.roots;
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
clientSupportsRoots = false;
|
|
164
|
+
}
|
|
165
|
+
// Reconcile StoreManager state from the current (clientRoots, cwd) tuple.
|
|
166
|
+
// Stateless re-derivation: each call to loadRootsStdio computes the right
|
|
167
|
+
// state from scratch rather than mutating the previous state. This matters
|
|
168
|
+
// when a client advertised roots earlier and then retracts them via
|
|
169
|
+
// `roots/list_changed`; without re-derivation the stale advertised roots
|
|
170
|
+
// would leak forward instead of falling back to cwd.
|
|
171
|
+
if (clientRoots.length > 0) {
|
|
172
|
+
storeManager.loadFromRoots(clientRoots);
|
|
173
|
+
console.error(`[cedar-mcp-server] Loaded ${clientRoots.length} root(s) from MCP client: ${clientRoots.map((r) => r.uri).join(", ")} (replaces any sync-loaded cwd-fallback).`);
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
const fallback = populateCwdFallback(process.cwd());
|
|
177
|
+
if (fallback) {
|
|
178
|
+
console.error(`[cedar-mcp-server] MCP client advertised 0 roots; using cwd-fallback store "${fallback.name}" (re-derived).`);
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
storeManager.loadFromRoots([]);
|
|
182
|
+
if (clientSupportsRoots) {
|
|
183
|
+
console.error("[cedar-mcp-server] MCP client returned 0 roots and cwd does not look like a Cedar workspace (no schema.cedarschema, schema.json, or policies/ dir). Cedar tools will require inline inputs.");
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
console.error("[cedar-mcp-server] MCP client does not support roots/list and cwd does not look like a Cedar workspace. Cedar tools will require inline inputs.");
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// kickoff-11 11a notification: cache-aware clients refetch on this. The
|
|
191
|
+
// synchronous cwd-fallback from runStdio means the FIRST resources/list
|
|
192
|
+
// already sees the populated store (this is the Round 5 fix), so this
|
|
193
|
+
// notification is most useful for the late-arriving-roots case (client
|
|
194
|
+
// sends roots/list_changed mid-session, swapping the store set). McpServer
|
|
195
|
+
// makes it a no-op when not connected, so it's safe to call from any path.
|
|
196
|
+
server.sendResourceListChanged();
|
|
197
|
+
}
|
|
198
|
+
async function runStdio() {
|
|
199
|
+
const server = createServer();
|
|
200
|
+
// Round 5 fix: populate StoreManager BEFORE the transport accepts any
|
|
201
|
+
// client requests. The previous shape (cwd-fallback inside `oninitialized`
|
|
202
|
+
// → kickoff-11 sendResourceListChanged) was contract-correct against the
|
|
203
|
+
// MCP spec but did not hold against Claude Code, which snapshots
|
|
204
|
+
// resources/list once on the initialize response and does not honor
|
|
205
|
+
// list_changed for the `listMcpResources` cache. Synchronous population
|
|
206
|
+
// before connect closes the window entirely.
|
|
207
|
+
//
|
|
208
|
+
// Edge case (kickoff-10 audit Probe C): if process.cwd() is the
|
|
209
|
+
// cedar-mcp-server repo itself, it currently has none of schema.cedarschema,
|
|
210
|
+
// schema.json, or policies/ — looksLikeCedarWorkspace returns false. If a
|
|
211
|
+
// future commit adds a top-level policies/ directory (for examples or
|
|
212
|
+
// similar) the fallback would self-load. Flag in CHANGELOG if that
|
|
213
|
+
// ever happens.
|
|
214
|
+
const loaded = populateCwdFallback(process.cwd());
|
|
215
|
+
if (loaded) {
|
|
216
|
+
console.error(`[cedar-mcp-server] Synchronously auto-loaded cwd as workspace store: "${loaded.name}" (${loaded.uri}). StoreManager populated before transport accepts requests.`);
|
|
217
|
+
}
|
|
218
|
+
const transport = new StdioServerTransport();
|
|
219
|
+
await server.connect(transport);
|
|
220
|
+
server.server.oninitialized = async () => {
|
|
221
|
+
await loadRootsStdio(server);
|
|
222
|
+
};
|
|
223
|
+
server.server.setNotificationHandler(RootsListChangedNotificationSchema, async () => {
|
|
224
|
+
await loadRootsStdio(server);
|
|
225
|
+
});
|
|
226
|
+
process.on("SIGINT", async () => {
|
|
227
|
+
await server.close();
|
|
228
|
+
process.exit(0);
|
|
229
|
+
});
|
|
230
|
+
process.on("SIGTERM", async () => {
|
|
231
|
+
await server.close();
|
|
232
|
+
process.exit(0);
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
async function runHttp(parsed) {
|
|
236
|
+
const running = await startHttpServer({
|
|
237
|
+
port: parsed.port,
|
|
238
|
+
host: parsed.host,
|
|
239
|
+
roots: parsed.roots,
|
|
240
|
+
});
|
|
241
|
+
const shutdown = async () => {
|
|
242
|
+
try {
|
|
243
|
+
await running.close();
|
|
244
|
+
}
|
|
245
|
+
finally {
|
|
246
|
+
process.exit(0);
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
process.on("SIGINT", shutdown);
|
|
250
|
+
process.on("SIGTERM", shutdown);
|
|
251
|
+
}
|
|
252
|
+
async function main() {
|
|
253
|
+
const parsed = parseArgs(process.argv);
|
|
254
|
+
if (parsed.mode === "help") {
|
|
255
|
+
printUsage(parsed.error);
|
|
256
|
+
process.exit(parsed.error ? 1 : 0);
|
|
257
|
+
}
|
|
258
|
+
if (parsed.mode === "http") {
|
|
259
|
+
await runHttp(parsed);
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
await runStdio();
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
main().catch((err) => {
|
|
266
|
+
// eslint-disable-next-line no-console
|
|
267
|
+
console.error("Fatal error:", err);
|
|
268
|
+
process.exit(1);
|
|
269
|
+
});
|
|
270
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|