Skip to content

feat: WASM bindings for cedar-policy-mcp-schema-generator#64

Open
tomjwxf wants to merge 1 commit intocedar-policy:mainfrom
tomjwxf:feat/wasm-bindings
Open

feat: WASM bindings for cedar-policy-mcp-schema-generator#64
tomjwxf wants to merge 1 commit intocedar-policy:mainfrom
tomjwxf:feat/wasm-bindings

Conversation

@tomjwxf
Copy link
Copy Markdown

@tomjwxf tomjwxf commented Apr 7, 2026

Summary

Adds rust/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.

Motivated by @lianah's recommendation in #63 to use WASM bindings instead of a TypeScript reimplementation, to avoid implementation divergence.

What it exposes

A single function:

const { generateSchema } = require('@cedar-policy/mcp-schema-generator-wasm');

const result = JSON.parse(generateSchema(schemaStub, toolsJson, configJson));
// result.schema     -> human-readable .cedarschema text
// result.schemaJson -> JSON for Cedar WASM isAuthorized()
// result.error      -> null if successful
// result.isOk       -> boolean

All SchemaGeneratorConfig options are exposed: includeOutputs, objectsAsRecords, eraseAnnotations, flattenNamespaces, numbersAsDecimal.

What it delegates

All schema generation logic, type mapping, and edge case handling is delegated to the Rust SchemaGenerator. The WASM layer adds no independent logic. This ensures:

  • JSON number handling (Long vs Decimal) is identical
  • additionalProperties as tagged entities is identical
  • Namespaced type deduplication is identical
  • Any future changes to the Rust generator are automatically available in WASM

Testing

  • 3 Rust unit tests passing (cargo test)
  • cargo clippy clean with workspace lints (including strict unwrap_used, expect_used, panic denials)
  • Tested in Node.js: produces correct output for multi-tool schemas

Example output:

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

  type read_fileInput = {
    path: String
  };

  entity McpServer;
  entity User;

  action "call_tool" appliesTo {
    principal: [User],
    resource: [McpServer],
    context: {}
  };

  action "execute_command" appliesTo {
    principal: [User],
    resource: [McpServer],
    context: { input: execute_commandInput }
  };

  action "read_file" appliesTo {
    principal: [User],
    resource: [McpServer],
    context: { input: read_fileInput }
  };
}

Build

wasm-pack build --target nodejs --scope cedar-policy

Output: 2.5MB WASM binary (wasm-opt applied).

Relationship to #63

This PR replaces the TypeScript reimplementation in #63 with WASM bindings that wrap the existing Rust code. I will close #63 since the TypeScript implementation is no longer needed.

Open questions

  • npm scope: Should the published package be @cedar-policy/mcp-schema-generator-wasm? Happy to adjust naming.
  • RequestGenerator: This PR exposes SchemaGenerator only. Should RequestGenerator also be exposed via WASM in this PR, or in a follow-up?

@tomjwxf
Copy link
Copy Markdown
Author

tomjwxf commented Apr 8, 2026

Rebased onto current main and updated cedar-policy-core pin from 4.8.2 to 4.9.1 to align with the dependency bump in #65.

  • All 3 tests passing
  • Clippy clean (with workspace lints)
  • cargo fmt clean

Ready for review when you have a chance! Happy to address any feedback.

Copy link
Copy Markdown

@victornicolet victornicolet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the contribution!

I think the main improvements would be integration tests on the JS side calling the bindings, and removing the locked version on cedar-policy-core by adding some functionality in cedar-policy-mcp-schema-generator.

serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
uuid = { version = "1.19.0", features = ["v4", "js"] }
getrandom = { version = "0.3", features = ["wasm_js"] }
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this dependency used? I can't see where it is, I think it could be removed.

You might want to also bump the dependencies (the same way you bumped the cedar-policy-core dependency) to match the cedar-policy-mcp-schema-generator.


// Parse schema stub
let extensions = Extensions::all_available();
let fragment = match Fragment::<RawName>::from_cedarschema_str(schema_stub, extensions) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This parsing functionality (which depends on cedar-policy-core) could go in the cedar_policy_mcp_schema_generator crate, so that the bindings crate doesn't have a dependency on cedar-policy-core.

For example, adding SchemaGenerator::from_cedarschema_str and SchemaGenerator::get_schema_as_` if the schemas only need to be represented/parsed as Strings in the bindings.

[dependencies]
cedar-policy-mcp-schema-generator = { path = "../cedar-policy-mcp-schema-generator" }
mcp-tools-sdk = { path = "../mcp-tools-sdk" }
cedar-policy-core = "=4.9.1"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally we would not have to depend on a pinned version of cedar-policy-core in this crate. I think it would be possible to add limited functionality to the cedar-policy-mcp-schema-generator crate to avoid that.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for updating cedar-policy-core to 4.9.1 after the upstream changes. You might want to also bump other upstream dependency changes for CI to pass.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tomjwxf I think this is the last outstanding item; the diff still shows many dependencies downgraded relative to mainline in Cargo.toml.

@tomjwxf
Copy link
Copy Markdown
Author

tomjwxf commented Apr 8, 2026

@victornicolet Thank you for the thorough review. Addressed all five points in the latest push:

1. Removed uuid and getrandom dependencies -- you were right, both were unused. They were leftover from an earlier iteration that generated receipt IDs client-side. Removed from Cargo.toml.

2. Moved schema parsing into the generator crate -- added SchemaGenerator::from_cedarschema_str() and from_cedarschema_str_with_config() to cedar-policy-mcp-schema-generator. The WASM crate now calls these instead of doing Fragment::from_cedarschema_str directly. Also added get_schema_as_str() for callers who only need the human-readable output.

3. Removed cedar-policy-core from the WASM crate -- the WASM crate now has zero direct dependency on cedar-policy-core. All parsing and schema resolution goes through the generator crate's API. Added a SchemaParseError(String) variant to SchemaGeneratorError for parse failures from the new convenience methods.

4. Cargo.lock / dependency bumps -- the lock file updated naturally with the dependency removals. If CI still needs further bumps for other workspace deps, happy to align.

5. Integration tests on the JS side -- acknowledged, I didn't include JS-side integration tests in this push. Would you prefer those as wasm-bindgen-test tests (Rust-side but running in a WASM environment), or as a separate JS test file that loads the built WASM module? Happy to add whichever pattern fits the repo's test conventions.

All 32 tests passing (29 generator + 3 WASM), clippy clean, cargo fmt clean.

@victornicolet
Copy link
Copy Markdown

5. Integration tests on the JS side -- acknowledged, I didn't include JS-side integration tests in this push. Would you prefer those as wasm-bindgen-test tests (Rust-side but running in a WASM environment), or as a separate JS test file that loads the built WASM module? Happy to add whichever pattern fits the repo's test conventions.

wasm-bindgen-test tests would be sufficient. Please also add small tests for new APIs in the cedar-policy-mcp-schema-generator (even if the functionality changes are minimal, two new APIs and one new error were added).

@tomjwxf tomjwxf force-pushed the feat/wasm-bindings branch from bcfed1a to e2fe475 Compare April 9, 2026 01:02
@tomjwxf
Copy link
Copy Markdown
Author

tomjwxf commented Apr 9, 2026

@victornicolet Added both. Squashed into a single commit with DCO sign-off.

wasm-bindgen-test integration tests (8 tests in tests/wasm_integration.rs):

  • Basic schema generation with single tool (verifies schema text, schemaJson, and isOk)
  • Multi-tool schema with integer-to-Long mapping
  • Config options: numbersAsDecimal producing Decimal instead of Long
  • Error handling: invalid schema stub, invalid tools JSON, invalid config JSON
  • Empty tools producing minimal schema (namespace + base action preserved)
  • Config defaults: None and {} produce identical output

Generator API tests (6 tests in generator/schema.rs):

  • from_cedarschema_str basic parsing
  • from_cedarschema_str_with_config with custom config
  • from_cedarschema_str invalid input returns SchemaParseError
  • from_cedarschema_str output matches manual Fragment + new() constructor
  • get_schema_as_str contains namespace name
  • get_schema_as_str output matches Display implementation

Total: 38 tests passing (35 generator + 3 WASM unit), plus the 8 wasm-bindgen-test integration tests (which run under wasm-pack test --node, not regular cargo test). Clippy clean, cargo fmt clean.

@tomjwxf
Copy link
Copy Markdown
Author

tomjwxf commented Apr 9, 2026

@victornicolet thanks, the Cargo.lock downgrades were the root issue. I rebased onto the cedar-policy-core 4.9.1 bump, the lock file got regenerated with older transitive dependencies instead of preserving the versions from #65. This pulled in time v0.3.44 (CVE) and yanked keccak v0.1.5, which caused the cargo audit failure.

Fixed in the latest push! Rebased cleanly onto current main and restored the upstream lock file before adding the WASM crate's dependencies on top. The diff now only contains legitimate additions (wasm-bindgen, wasm-bindgen-test, etc.). All 237 tests pass locally, clippy clean, fmt clean.

The new CI run will need a workflow approval since it's from a fork. Appreciate the patience with the iteration!

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 9, 2026

Coverage Report

Head Commit: b0d6875b7588e42af4b3c67c8643555f2da45bbb

Base Commit: c835f5618cb1bd4ebf900a0f3c3b2d3d8740451a

Download the full coverage report.

Coverage of Added or Modified Lines of Rust Code

Required coverage: 80.00%

Actual coverage: 75.12%

Status: FAILED ❌

Details
File Status Covered Coverage Missed Lines
cedar-policy-mcp-schema-generator-wasm/src/lib.rs 🔴 41/92 44.57% 49-51, 54-62, 66-73, 118, 122, 132-140, 164-170, 175-180, 189-195
cedar-policy-mcp-schema-generator/src/generator/schema.rs 🟢 113/113 100.00%

Coverage of All Lines of Rust Code

Required coverage: 80.00%

Actual coverage: 89.79%

Status: PASSED ✅

Details
Package Status Covered Coverage Base Coverage
cedar-policy-mcp-schema-generator 🟢 1693/1809 93.59% 93.16%
cedar-policy-mcp-schema-generator-wasm 🔴 41/92 44.57% --
mcp-tools-sdk 🟢 1457/1653 88.14% 88.14%

@tomjwxf tomjwxf force-pushed the feat/wasm-bindings branch from b0d6875 to d9d67b1 Compare April 9, 2026 23:57
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 tomjwxf force-pushed the feat/wasm-bindings branch from d9d67b1 to df3bf4f Compare April 10, 2026 16:37
@tomjwxf
Copy link
Copy Markdown
Author

tomjwxf commented Apr 10, 2026

@victornicolet @john-h-kastner-aws Updated with additional test coverage for the uncovered code paths (nested objects, array types, all config options, error field completeness, generate_schema_inner match arms, SchemaParseError Display). 56 tests total, clippy clean, fmt clean - coverage now ✅ ✅

@github-actions
Copy link
Copy Markdown

Coverage Report

Head Commit: df3bf4fb265b3a03c42afdefc15c3797bdcdef07

Base Commit: c835f5618cb1bd4ebf900a0f3c3b2d3d8740451a

Download the full coverage report.

Coverage of Added or Modified Lines of Rust Code

Required coverage: 80.00%

Actual coverage: 92.68%

Status: PASSED ✅

Details
File Status Covered Coverage Missed Lines
cedar-policy-mcp-schema-generator-wasm/src/lib.rs 🟢 77/92 83.70% 118, 122, 175-180, 189-195
cedar-policy-mcp-schema-generator/src/generator/schema.rs 🟢 113/113 100.00%

Coverage of All Lines of Rust Code

Required coverage: 80.00%

Actual coverage: 91.11%

Status: PASSED ✅

Details
Package Status Covered Coverage Base Coverage
cedar-policy-mcp-schema-generator 🟢 1693/1809 93.59% 93.16%
cedar-policy-mcp-schema-generator-wasm 🟢 77/92 83.70% --
mcp-tools-sdk 🟢 1468/1653 88.81% 88.14%

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.

3 participants