Skip to content

feat: TypeScript Cedar schema generation from MCP tool descriptions (protect-mcp)#63

Closed
tomjwxf wants to merge 1 commit intocedar-policy:mainfrom
tomjwxf:feat/protect-mcp-integration-example
Closed

feat: TypeScript Cedar schema generation from MCP tool descriptions (protect-mcp)#63
tomjwxf wants to merge 1 commit intocedar-policy:mainfrom
tomjwxf:feat/protect-mcp-integration-example

Conversation

@tomjwxf
Copy link
Copy Markdown

@tomjwxf tomjwxf commented Apr 2, 2026

Summary

Adds a TypeScript integration example showing how to auto-generate Cedar authorization schemas from MCP tool descriptions, enabling typed policies for runtime tool governance.

This builds on the discussion in cedar-policy/cedar#2266, where @lianah recommended integrating the cedar-policy-mcp-schema-generator and using static schemas with Cedar for validation. This example provides a TypeScript-native path for Node.js environments where the Rust binary is not available.

What this adds

js/protect-mcp-cedar-integration/ — a working example with:

File Purpose
README.md Documentation with type mapping table, example policies, compatibility notes
example.mjs Runnable demo: generates Cedar schema from 5 MCP tools
test.mjs 14 tests covering type mapping, structure, edge cases
policies/mcp-governance.cedar Example policies using the generated schema
package.json Depends on protect-mcp (npm, MIT)

How it works

import { generateCedarSchema } from 'protect-mcp';

const tools = [
  { name: 'read_file', inputSchema: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] } },
  { name: 'execute_command', inputSchema: { type: 'object', properties: { command: { type: 'string' } }, required: ['command'] } },
];

const schema = generateCedarSchema(tools);
// schema.schemaText → human-readable .cedarschema
// schema.schemaJson → JSON for Cedar WASM isAuthorized()

This enables typed policies:

permit(principal, action == Action::"read_file", resource)
when { context.input.path like "./workspace/*" };

Compatibility with the Rust generator

  • Same entity model: Agent (principal), Tool (resource)
  • Same action hierarchy: per-tool actions as children of MCP::Tool::call
  • Same JSON Schema → Cedar type mapping
  • Schema stubs use @mcp_principal / @mcp_resource annotations
  • The TypeScript version covers the common MCP use case; the Rust generator provides additional features (union types, tagged entities, output schemas)

Context

protect-mcp (v0.5.2, MIT) is a security gateway for MCP servers and Claude Code that uses Cedar WASM for runtime policy enforcement. Every allow/deny decision produces an Ed25519-signed receipt following the IETF Internet-Draft format.

This PR was motivated by @lianah's recommendation in cedar-policy/cedar#2266 to move from schema: null to validated schemas and to integrate with the cedar-for-agents schema generation tooling.

Adds js/protect-mcp-cedar-integration/ — a working example showing how
to auto-generate Cedar authorization schemas from MCP tools/list responses
and use them for runtime policy enforcement via protect-mcp.

- Generates per-tool Cedar actions with typed input context
- Maps JSON Schema types to Cedar types (String, Long, Bool, Set<T>)
- Compatible with the Rust cedar-policy-mcp-schema-generator
- Includes example policies, schema generation demo, and tests
- TypeScript-native (no Rust binary dependency)
- Every allow/deny decision produces an Ed25519-signed receipt

Reference: cedar-policy/cedar#2266

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@lianah
Copy link
Copy Markdown

lianah commented Apr 3, 2026

Thank you for your contribution! It's great to see increased adoption of Cedar for agents. One concern we have with the approach of re-implementing the schema generator code in TypeScript is that the two implementations might diverge, and we would want to have consistent behavior across the two languages. Something we have done in the past is WASM bindings for the Rust code. Would this work for your use case or are there any reasons why that is not a good fit?

There are a couple of corner cases in the current schema generator we had to be careful about handling correctly that we would want to make sure are consistent across the two versions:

  • JSON number can actually be either int or float, so representing it as Long won't be the same behavior. Cedar does not support float so we have an option to model it as decimal. See this code comment for details
  • Modeling objects as entities vs records. Records are nice because they support structural equality, but they do not support tags. In the JSON schema there is this notion of additionalProperties that our generator models as Cedar tags so for those types we use entities. See the code here
  • Reusing record/entity types between sub-objects: the Rust schema generator introduces names and namespaces for the JSON objects based on the field name to avoid conflicts e.g. if multiple MCP tools have the same address field but with different structures

There is also some work on utility code to map from the actual MCP tool inputs to a Cedar request to make integration easier.

@tomjwxf
Copy link
Copy Markdown
Author

tomjwxf commented Apr 3, 2026

@lianah -- you are right. Implementation divergence is the real risk here, and the three edge cases you flagged (number/float handling, additionalProperties as tagged entities, and namespaced type deduplication) are exactly the places where a TypeScript reimplementation would silently produce different schemas.

WASM bindings for the Rust generator is the better path. I checked and the crate does not have wasm-bindgen today -- would it be useful if I contributed WASM bindings as a PR to the schema generator crate? The scope would be:

  1. Add wasm-bindgen to cedar-policy-mcp-schema-generator
  2. Expose SchemaGenerator and RequestGenerator to JavaScript/TypeScript via WASM
  3. Publish as an npm package (something like @cedar-policy/mcp-schema-generator-wasm)
  4. Update this PR to use the WASM bindings instead of the TypeScript reimplementation

This way protect-mcp (and any other TypeScript/Node.js MCP tool) gets the exact same behavior as the Rust generator, including all the edge cases around decimal encoding, entity vs record modeling, and namespace deduplication.

I am already using @cedar-policy/cedar-wasm (v4.9.1) for policy evaluation in protect-mcp, so the WASM integration pattern is familiar. Happy to start on this if the approach works for you.

On the utility code for mapping MCP tool inputs to Cedar requests -- I built a basic version of this in protect-mcp (maps tool_name to resource, agent tier to principal, tool input to context). Would be glad to align it with whatever interface the Rust RequestGenerator exposes through WASM.

@tomjwxf
Copy link
Copy Markdown
Author

tomjwxf commented Apr 3, 2026

@lianah -- I built the WASM bindings as a proof-of-concept and they work. Here is a working demo:

WASM crate: `cedar-policy-mcp-schema-generator-wasm` -- a thin `wasm-bindgen` wrapper around the existing Rust `SchemaGenerator`.

What it exposes to JavaScript/TypeScript:

```js
import { generateSchema, WasmSchemaConfig } from 'cedar-policy-mcp-schema-generator-wasm';

const result = generateSchema(stubCedarSchema, toolsJson, configJson);
// result.schema -> Cedar schema as human-readable text
// result.error -> null if successful
// result.isOk -> boolean
```

Tested in Node.js -- produces correct output:

```
namespace MyServer {
type execute_commandInput = {
command: String,
timeout?: Long
};

type read_fileInput = {
encoding?: String,
path: String
};

entity McpServer;
entity User = { id: String };

action "call_tool";
action "execute_command" in [Action::"call_tool"] appliesTo {
principal: [User],
resource: [McpServer],
context: { input: execute_commandInput }
};
action "read_file" in [Action::"call_tool"] appliesTo {
principal: [User],
resource: [McpServer],
context: { input: read_fileInput }
};
}
```

Implementation details:

  • Parses schema stub via `Fragment::from_cedarschema_str`
  • Parses tool descriptions via `ServerDescription::from_json_str`
  • Exposes all `SchemaGeneratorConfig` options (include_outputs, objects_as_records, flatten_namespaces, numbers_as_decimal, erase_annotations)
  • Required `uuid` `js` feature + `getrandom` `wasm_js` feature for WASM compat
  • 2.5MB WASM binary (could be reduced with wasm-opt and feature gating)
  • 3 Rust tests passing

Two options for how to proceed:

  1. I can submit this as a PR to this repo adding `rust/cedar-policy-mcp-schema-generator-wasm/` alongside the existing crates. The cedar-for-agents team would own and publish it.

  2. I can publish it independently and update my original PR (feat: TypeScript Cedar schema generation from MCP tool descriptions (protect-mcp) #63) to use the WASM bindings instead of the TypeScript reimplementation.

Which approach works better for the cedar-for-agents project? Happy to do either.

@lianah
Copy link
Copy Markdown

lianah commented Apr 6, 2026

That's awesome! I think submitting the WASM bindings as a PR against the cedar-for-agents repo under rust/cedar-policy-mcp-schema-generator-wasm makes a lot of sense. In that case, I don't think we need the TypeScript implementation anymore right?

tomjwxf pushed a commit to tomjwxf/cedar-for-agents that referenced this pull request Apr 7, 2026
Adds `cedar-policy-mcp-schema-generator-wasm`, a thin wasm-bindgen
wrapper around the existing Rust SchemaGenerator. This enables
JavaScript and TypeScript environments (Node.js, browsers) to
generate Cedar schemas from MCP tool descriptions with the exact
same behavior as the Rust implementation.

Exposes a single function `generateSchema(schemaStub, toolsJson,
configJson?)` that returns the generated schema as both human-
readable `.cedarschema` text and JSON for `isAuthorized()`.

All schema generation logic, type mapping (including number/decimal
handling, additionalProperties as tagged entities, and namespaced
type deduplication) is delegated to the Rust crate. The WASM
bindings add no independent logic, ensuring zero divergence.

- 3 tests passing (cargo test)
- clippy clean with workspace lints
- 2.5MB WASM binary (wasm-opt applied)
- Tested in Node.js: correct output for multi-tool schemas

Motivated by the discussion in cedar-policy#63, where @lianah recommended WASM
bindings over a TypeScript reimplementation to avoid implementation
divergence.
@tomjwxf
Copy link
Copy Markdown
Author

tomjwxf commented Apr 7, 2026

@lianah -- thank you! Agreed, the TypeScript reimplementation is no longer needed. I submitted the WASM bindings as #64 instead.

The WASM crate wraps the existing Rust SchemaGenerator via wasm-bindgen with zero independent logic. All type mapping, edge cases, and namespace handling is delegated to the Rust implementation. 3 tests passing, clippy clean with workspace lints, tested in Node.js.

Closing this PR in favor of #64.

Best,
Tom

tomjwxf pushed a commit to tomjwxf/cedar-for-agents that referenced this pull request Apr 8, 2026
Adds `cedar-policy-mcp-schema-generator-wasm`, a thin wasm-bindgen
wrapper around the existing Rust SchemaGenerator. This enables
JavaScript and TypeScript environments (Node.js, browsers) to
generate Cedar schemas from MCP tool descriptions with the exact
same behavior as the Rust implementation.

Exposes a single function `generateSchema(schemaStub, toolsJson,
configJson?)` that returns the generated schema as both human-
readable `.cedarschema` text and JSON for `isAuthorized()`.

All schema generation logic, type mapping (including number/decimal
handling, additionalProperties as tagged entities, and namespaced
type deduplication) is delegated to the Rust crate. The WASM
bindings add no independent logic, ensuring zero divergence.

- 3 tests passing (cargo test)
- clippy clean with workspace lints
- 2.5MB WASM binary (wasm-opt applied)
- Tested in Node.js: correct output for multi-tool schemas

Motivated by the discussion in cedar-policy#63, where @lianah recommended WASM
bindings over a TypeScript reimplementation to avoid implementation
divergence.
@victornicolet
Copy link
Copy Markdown

victornicolet commented Apr 8, 2026

Thank you! Closing this PR, I will review #64 .

tomjwxf pushed a commit to tomjwxf/cedar-for-agents that referenced this pull request Apr 9, 2026
Add `cedar-policy-mcp-schema-generator-wasm`, a thin wasm-bindgen
wrapper around the existing Rust SchemaGenerator. Enables JavaScript
and TypeScript environments (Node.js, browsers) to generate Cedar
schemas from MCP tool descriptions with identical behavior to the
Rust implementation.

Motivated by @lianah's recommendation in cedar-policy#63 to use WASM bindings
instead of a TypeScript reimplementation.

Changes to cedar-policy-mcp-schema-generator:
- Add SchemaGenerator::from_cedarschema_str() and
  from_cedarschema_str_with_config() convenience constructors
- Add SchemaGenerator::get_schema_as_str() for human-readable output
- Add SchemaParseError variant to SchemaGeneratorError
- 6 new unit tests for the above APIs

WASM bindings crate:
- Single generateSchema() function exposed via wasm-bindgen
- All SchemaGeneratorConfig options exposed (camelCase JS naming)
- Returns JSON with schema, schemaJson, error, and isOk fields
- Zero direct dependency on cedar-policy-core (all parsing
  delegated to generator crate's from_cedarschema_str)
- 3 Rust unit tests
- 8 wasm-bindgen-test integration tests (basic generation,
  multi-tool, config options, error handling, config defaults)

Signed-off-by: tommylauren <tfarley@utexas.edu>
tomjwxf pushed a commit to tomjwxf/cedar-for-agents that referenced this pull request Apr 9, 2026
Add `cedar-policy-mcp-schema-generator-wasm`, a thin wasm-bindgen
wrapper around the existing Rust SchemaGenerator. Enables JavaScript
and TypeScript environments (Node.js, browsers) to generate Cedar
schemas from MCP tool descriptions with identical behavior to the
Rust implementation.

Motivated by @lianah's recommendation in cedar-policy#63 to use WASM bindings
instead of a TypeScript reimplementation.

Changes to cedar-policy-mcp-schema-generator:
- Add SchemaGenerator::from_cedarschema_str() and
  from_cedarschema_str_with_config() convenience constructors
- Add SchemaGenerator::get_schema_as_str() for human-readable output
- Add SchemaParseError variant to SchemaGeneratorError
- 6 new unit tests for the above APIs

WASM bindings crate:
- Single generateSchema() function exposed via wasm-bindgen
- All SchemaGeneratorConfig options exposed (camelCase JS naming)
- Returns JSON with schema, schemaJson, error, and isOk fields
- Zero direct dependency on cedar-policy-core (all parsing
  delegated to generator crate's from_cedarschema_str)
- 3 Rust unit tests
- 8 wasm-bindgen-test integration tests (basic generation,
  multi-tool, config options, error handling, config defaults)

Signed-off-by: tommylauren <tfarley@utexas.edu>
tomjwxf pushed a commit to tomjwxf/cedar-for-agents that referenced this pull request Apr 9, 2026
Add `cedar-policy-mcp-schema-generator-wasm`, a thin wasm-bindgen
wrapper around the existing Rust SchemaGenerator. Enables JavaScript
and TypeScript environments (Node.js, browsers) to generate Cedar
schemas from MCP tool descriptions with identical behavior to the
Rust implementation.

Motivated by @lianah's recommendation in cedar-policy#63 to use WASM bindings
instead of a TypeScript reimplementation.

Changes to cedar-policy-mcp-schema-generator:
- Add SchemaGenerator::from_cedarschema_str() and
  from_cedarschema_str_with_config() convenience constructors
- Add SchemaGenerator::get_schema_as_str() for human-readable output
- Add SchemaParseError variant to SchemaGeneratorError
- 6 new unit tests for the above APIs

WASM bindings crate:
- Single generateSchema() function exposed via wasm-bindgen
- All SchemaGeneratorConfig options exposed (camelCase JS naming)
- Returns JSON with schema, schemaJson, error, and isOk fields
- Zero direct dependency on cedar-policy-core (all parsing
  delegated to generator crate's from_cedarschema_str)
- 3 Rust unit tests
- 8 wasm-bindgen-test integration tests (basic generation,
  multi-tool, config options, error handling, config defaults)

Signed-off-by: tommylauren <tfarley@utexas.edu>
tomjwxf pushed a commit to tomjwxf/cedar-for-agents that referenced this pull request Apr 10, 2026
Add `cedar-policy-mcp-schema-generator-wasm`, a thin wasm-bindgen
wrapper around the existing Rust SchemaGenerator. Enables JavaScript
and TypeScript environments (Node.js, browsers) to generate Cedar
schemas from MCP tool descriptions with identical behavior to the
Rust implementation.

Motivated by @lianah's recommendation in cedar-policy#63 to use WASM bindings
instead of a TypeScript reimplementation.

Changes to cedar-policy-mcp-schema-generator:
- Add SchemaGenerator::from_cedarschema_str() and
  from_cedarschema_str_with_config() convenience constructors
- Add SchemaGenerator::get_schema_as_str() for human-readable output
- Add SchemaParseError variant to SchemaGeneratorError
- 6 new unit tests for the above APIs

WASM bindings crate:
- Single generateSchema() function exposed via wasm-bindgen
- All SchemaGeneratorConfig options exposed (camelCase JS naming)
- Returns JSON with schema, schemaJson, error, and isOk fields
- Zero direct dependency on cedar-policy-core (all parsing
  delegated to generator crate's from_cedarschema_str)
- 3 Rust unit tests
- 8 wasm-bindgen-test integration tests (basic generation,
  multi-tool, config options, error handling, config defaults)

Signed-off-by: tommylauren <tfarley@utexas.edu>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants