cavalry-mcp

cavalry-mcp

MCP server for driving Cavalry's 2D motion-graphics engine from LLMs, enabling scene creation and animation via natural language.

Category
Visit Server

README

<p align="center"> <img src="assets/banner.png" alt="cavalry-mcp — MCP server for Cavalry" width="100%"> </p>

cavalry-mcp

Node TypeScript MCP

Drive Cavalry from an LLM. An MCP server that exposes Cavalry's 2D motion-graphics engine as 28 structured tools — create layers, wire procedural graphs, set keyframes, apply easing, and render frames, all from natural language.

Point Claude at a composition and say "build me a looping loader animation with eight dots on a circle" — it introspects the scene, builds the duplicator graph, keyframes the rotation, and renders a PNG to check its own work.


Contents


Why

Cavalry ships a rich JavaScript API, but it lives behind an in-app editor — there is no out-of-process entry point. cavalry-mcp adds one, and wraps it in a typed, validated dispatcher rather than handing the model a raw eval.

That distinction is the whole design:

Raw script execution cavalry-mcp
Input validation none Zod schema per tool
Injection surface full closed — args are JSON, never interpolated into source
Failure mode silent console error typed error taxonomy
Blast radius filesystem + subprocess scene graph only

The model gets exactly the verbs it needs, and every one of them is checked before it reaches Cavalry.


Architecture

graph LR
    A[MCP Client<br/>Claude Code / Desktop] -->|stdio<br/>JSON-RPC| B[cavalry-mcp<br/>Node + TypeScript]
    B -->|HTTP 127.0.0.1:8080| C[cavalry-bridge.js<br/>in-app WebServer]
    C -->|api.*| D[Cavalry<br/>scene graph]

Three processes, two hops:

  1. MCP server (dist/src/index.js) — speaks JSON-RPC over stdio, validates tool arguments with Zod, and serialises each call into a versioned dispatch payload.
  2. Bridge (cavalry-bridge.js) — runs inside Cavalry via api.WebServer, listening on loopback. It routes ops to api.* calls and returns results.
  3. Cavalry — the host application.

Request lifecycle

The bridge processes POSTs on Cavalry's realtime callback (~60 fps) and stashes the result for retrieval, so the transport is POST-then-poll rather than request/response:

POST /post  { payloadVersion: 1, op: "keyframe", args: {...}, _rid: <uuid> }
GET  /get   → poll every 30 ms until the body contains our _rid

Every request carries a UUID _rid. The client only accepts a response containing its own id, so concurrent calls can never read each other's results.

Results come back inside a sentinel envelope, because api.log() may write to the same buffer:

<CAVALRY_MCP_RESULT>{"ok":true,...}<CAVALRY_MCP_RESULT>

The parser scans for the last matching tag pair, making it resilient to log noise that happens to contain tag-like text.

Error taxonomy

Failures are classified rather than collapsed into a string, so an agent can tell "Cavalry isn't running" from "you passed a bad attribute path":

Kind Meaning
validation Bad input, rejected before dispatch
transport Connection refused or timed out
http Non-200 from the bridge
envelope Response body unparseable
remote Cavalry returned { ok: false }

Installation

Prerequisites

  • Node.js ≥ 18 (the transport uses global fetch)
  • Cavalry 2.7+
  • An MCP client — Claude Code, Claude Desktop, or any MCP-compatible host

1. Build the server

git clone https://github.com/nxnai/cavalry-mcp.git
cd cavalry-mcp
npm install
npm run build

Verify the build with the offline test suite (no running Cavalry required):

npm test

2. Install the bridge

Copy cavalry-bridge.js into Cavalry's user scripts folder:

OS Path
Windows %APPDATA%\Cavalry\Scripts\
macOS ~/Library/Application Support/Cavalry/Scripts/

Scripts in that folder appear under Window > Scripts immediately — no Cavalry restart needed. Run cavalry-bridge from that menu to start the listener.

[!IMPORTANT] The bridge is session-scoped. Re-run it from the Scripts menu each time you restart Cavalry. The MCP server does not need restarting.

[!WARNING] Cavalry must stay frontmost while tools run. Cavalry pauses script callbacks when the app is in the background, which stalls the bridge until you refocus it. Requests are queued rather than lost, and the client fails fast with a frozen error instead of hanging. See Troubleshooting.

Alternatively, paste the script into Window > JavaScript Editor and run it there.

3. Register the MCP server

Add to your MCP client config — .mcp.json in your project root for Claude Code, or claude_desktop_config.json for Claude Desktop:

{
  "mcpServers": {
    "cavalry": {
      "command": "node",
      "args": ["/absolute/path/to/cavalry-mcp/dist/src/index.js"],
      "env": {
        "CAVALRY_MCP_HOST": "127.0.0.1",
        "CAVALRY_MCP_PORT": "8080",
        "CAVALRY_MCP_TIMEOUT_MS": "15000"
      }
    }
  }
}

[!NOTE] On Windows, escape backslashes in JSON (C:\\Users\\...) or use forward slashes.

Restart your MCP client, then confirm the round-trip:

cavalry_ping  →  { "ok": true, "platform": "Windows" }

Configuration

All configuration is via environment variables, parsed and validated once at startup. Invalid values fail fast with a descriptive error rather than silently falling back.

Variable Default Description
CAVALRY_MCP_HOST 127.0.0.1 Bridge host
CAVALRY_MCP_PORT 8080 Bridge port — must match PORT in cavalry-bridge.js
CAVALRY_MCP_TIMEOUT_MS 15000 Per-request timeout
CAVALRY_MCP_ENABLE_RUN_SCRIPT (unset) Set to 1 to expose cavalry_run_script. See Security.

Changing the port requires editing both sides — the env var and PORT at the top of cavalry-bridge.js.


Tool Reference

28 tools by default; 29 with cavalry_run_script enabled.

Introspection

Tool Arguments Returns
cavalry_ping { ok, platform }
cavalry_list_layers { layers: [{ id, type, name }] }
cavalry_get_selection { selection: string[] }
cavalry_composition_info { compId, width, height, startFrame, endFrame, fps, frame }
cavalry_get_attribute layerId, attrPath attribute value
cavalry_bounding_box layerId, worldSpace? { x, y, width, height, centre, left, right, top, bottom }
cavalry_list_attributes layerId, includeValues? { layerType, count, attributes: [{ id, type, niceName, animated, children? }] }
cavalry_list_layer_types includeExperimental? { count, layerTypes: string[] }
cavalry_inspect_connections layerId { incoming, outgoing, animated }

[!TIP] Never guess an attribute path — call cavalry_list_attributes first. Cavalry's docs list attributes by their UI label, not their scripting id, and the two do not reliably match: the documented "Grouping" and "Custom String" on a Shuffle String Manipulator simply do not exist as scripting paths. A wrong path fails silently on write, so guessing produces changes that report success and do nothing.

Likewise, cavalry_create_layer takes a scripting type id, not a display name — "Shuffle String Manipulator" is shuffleString. Use cavalry_list_layer_types rather than inferring it.

Creation

Tool Arguments
cavalry_create_layer layerType, name?
cavalry_create_primitive generator, name?

generatorellipse · rectangle · star · polygon · ring · arrow · superEllipse · line

layerTypetextShape · null · duplicator · stagger · basicShape

[!TIP] Prefer cavalry_create_primitive over create_layer + set_generator — one round-trip instead of two.

Mutation

Tool Arguments
cavalry_set_attribute layerId, attributes (map of path → value)
cavalry_set_generator layerId, generator, attrId? (default "generator")
cavalry_select layerIds
cavalry_delete_layers layerIds
cavalry_duplicate layerIds — restores prior selection afterward
cavalry_parent childIds, parentId
cavalry_add_dynamic_attribute layerId, attrName, attrType
cavalry_connect fromLayerId, fromAttrId, toLayerId, toAttrId, force?

Animation

Tool Arguments
cavalry_keyframe layerId, frame, attributes
cavalry_magic_easing layerId, attrPath, frame, easingType
cavalry_delete_keyframe layerId, attrPath, frame
cavalry_set_frame frame
cavalry_play

easingTypeSlowIn · SlowOut · SlowInSlowOut · VerySlowIn · VerySlowOut · SpringIn · SpringOut · BounceIn · BounceOut · Custom · None

[!WARNING] Magic Easing has no Linear, EaseIn, EaseOut, BackIn, or ElasticIn. The enum is enforced at the schema layer, so invalid names are rejected before reaching Cavalry.

I/O

Tool Arguments
cavalry_save_scene filePath? — omit to save in place
cavalry_load_scene filePath
cavalry_render_png filePath, frame?, scale? (default 100)

Batch

Tool Arguments
cavalry_batch ops: [{ op, args? }]

Attribute Reference

Cavalry's attribute paths are not always guessable, and a wrong path fails silently — the call succeeds, nothing changes. These are the verified ones.

Transform

Path Type Notes
position.x / position.y number
scale.x / scale.y number 1.0 = 100%. This is how you resize a shape.
rotation.z number Degrees. rotation is a 3D vector — always use .z in 2D.
opacity number 0–100

Appearance

Path Type Notes
material.materialColor string Hex, "#rrggbb"
generator.dimensions object GET only. Returns {x, y}; setting it throws a console error.

Duplicator

Path Type Notes
generator.count number Distribution point count
generator.radius number Circle distribution radius
generator.startAngle / generator.angle number Arc control
shapeScale.x / shapeScale.y number Per-copy scale
shapeRotation number Per-copy rotation
shapeOpacity number Per-copy opacity

Text

Path Type Notes
text string Plain string
fontSize number
autoWidth boolean false by default, which causes wrapping
horizontalAlignment number 0 left · 1 centre · 2 right

Composition

Path Type Notes
resolution.x / resolution.y number Read-only
startFrame / endFrame / fps number

Common pitfalls

Instead of Use
width, height, size scale.x / scale.y
fill.color, color, material.color material.materialColor
rotation rotation.z
setting generator.dimensions scale.x / scale.y

Layer IDs

IDs follow typeName#numberbasicShape#1, duplicator#2, textShape#3. Counters increment globally per type and never reset, even after deletion. Always discover them with cavalry_list_layers; never assume.

Sizing worked example

Primitives are created at 200×200. For a 1920×1080 background:

cavalry_create_primitive(generator: "rectangle", name: "BG")
cavalry_set_attribute("basicShape#1", {
  "scale.x": 9.6,          // 1920 / 200
  "scale.y": 5.4,          // 1080 / 200
  "material.materialColor": "#101014"
})

Duplicator worked example

Shapes are connected to a duplicator, not parented to it:

cavalry_create_primitive(generator: "ellipse", name: "Dot")
cavalry_create_layer(layerType: "duplicator", name: "Ring")
cavalry_set_generator("duplicator#1", generator: "circleDistribution")
cavalry_set_attribute("duplicator#1", { "generator.count": 8, "generator.radius": 200 })
cavalry_connect(
  fromLayerId: "basicShape#1", fromAttrId: "id",
  toLayerId:   "duplicator#1", toAttrId:   "shapes"
)

[!IMPORTANT] The source shape's own scale does not affect the duplicated copies. Use shapeScale.x / shapeScale.y on the duplicator.


Batching

cavalry_batch collapses a sequence of operations into a single HTTP round-trip. On a multi-step build this is the difference between 20 polls and one.

{
  "ops": [
    { "op": "createPrimitive", "args": { "generator": "ellipse", "name": "Dot" } },
    { "op": "set", "args": { "layerId": "basicShape#1",
                             "attributes": { "scale.x": 0.25, "scale.y": 0.25 } } },
    { "op": "keyframe", "args": { "layerId": "basicShape#1", "frame": 0,
                                  "attributes": { "rotation.z": 0 } } },
    { "op": "keyframe", "args": { "layerId": "basicShape#1", "frame": 250,
                                  "attributes": { "rotation.z": 360 } } }
  ]
}

Supported ops: create · createPrimitive · set · setGenerator · select · delete · duplicate · parent · addDynamic · keyframe · magicEasing · deleteKeyframe · setFrame · play · saveScene · loadScene · renderPng

Two constraints worth internalising:

[!WARNING] Fail-fast, not atomic. If op 4 fails, ops 1–3 have already been applied. There is no rollback.

[!WARNING] No ID substitution. IDs created earlier in a batch are not interpolated into later ops. Either predict the sequential IDs, or split the batch and call cavalry_list_layers in between.


Security

Argument injection is closed by construction

Tool arguments are transmitted as JSON data and dispatched by an op string inside Cavalry. They are never concatenated into JavaScript source, so there is no string-escaping boundary to get wrong.

The test suite treats this as a first-class invariant, with dedicated cases for:

  • backtick / template-literal injection
  • double-quote escape and chained calls
  • </script> tag injection
  • U+2028 / U+2029 line-separator injection

cavalry_run_script is opt-in

Setting CAVALRY_MCP_ENABLE_RUN_SCRIPT=1 registers a tool that executes arbitrary JavaScript in Cavalry, bypassing the dispatcher entirely. Cavalry's api namespace includes filesystem and subprocess access, so this grants the model both.

It is off by default, and the server prints a warning to stderr when it is on. Leave it disabled unless you are debugging the bridge itself.

Network exposure

The bridge binds to 127.0.0.1 and performs no authentication — any local process can drive it while it is running. Do not bind it to a routable interface, and stop it when you are not using it.


Troubleshooting

[transport] Cannot reach cavalry-mcp bridge at http://127.0.0.1:8080

The bridge isn't running. Cavalry being open is not sufficient — run Window > Scripts > cavalry-bridge. Confirm the port is live:

# Windows
netstat -ano | findstr :8080
# macOS
lsof -i :8080

Remember the bridge dies with the Cavalry session.

[frozen] Cavalry accepted the request but never processed it

Cavalry pauses script callbacks whenever it is not the active application, so the bridge stops draining its queue the moment you switch to another window. Bring Cavalry to the front — the backlog runs on refocus and nothing is lost.

This is a Cavalry-level gate, not a tuning problem. Measured: 15/15 requests fail over 81s while backgrounded, 15/15 succeed focused. setRealtime() (60/s), setHighFrequency() (1/s) and the default (3/s) are the same poll driver at three intervals, and all three are equally paused — so changing the rate does nothing. Keep Cavalry frontmost for the duration of an agent-driven session.

Tools don't appear in the client

Restart the MCP client after editing its config. Check that args points at dist/src/index.js — note the src segment, which follows from rootDir: "." in tsconfig.json. Run npm run build if dist/ is missing.

An attribute set reports success but nothing changes

The path is almost certainly wrong — Cavalry does not error on unknown paths. Read it back with cavalry_get_attribute, cross-check the Attribute Reference, and verify visually with cavalry_bounding_box or cavalry_render_png.

[envelope] No envelope tags found in response

The bridge returned output without a complete sentinel pair — usually a stale or partially-pasted bridge script. Re-copy cavalry-bridge.js and re-run it.

Requests time out under load

Heavy renders can exceed the default 15 s. Raise CAVALRY_MCP_TIMEOUT_MS.


Development

npm install       # install dependencies
npm run build     # tsc → dist/
npm test          # 18 unit tests, no live Cavalry needed
npm start         # run the server standalone

Layout

src/
├── index.ts              # server entry — tool registration
├── config.ts             # env parsing, validated at import
├── errors.ts             # CavalryError taxonomy
├── helpers.ts            # dispatch + MCP result shaping
├── bridge/
│   ├── dispatch.ts       # payload construction
│   ├── dispatcher-source.ts
│   └── envelope.ts       # sentinel-tag parser
├── transport/
│   └── bridge.ts         # POST-then-poll HTTP client
└── tools/
    ├── introspection.ts  creation.ts  mutation.ts
    ├── animation.ts      io.ts        batch.ts
    └── run-script.ts     # gated behind env flag

cavalry-bridge.js         # runs inside Cavalry
test/test.ts              # dispatcher, envelope, injection, protocol

Adding a tool

  1. Add the op branch to dispatch() in cavalry-bridge.js.
  2. Register the tool with a Zod schema in the matching src/tools/*.ts.
  3. If it should be batchable, add the op name to the enum in src/tools/batch.ts.
  4. Cover the routing in test/test.ts.

The build output must stay in sync with the bridge — after editing cavalry-bridge.js, re-copy it to the Scripts folder and re-run it in Cavalry.

Recommended Servers

playwright-mcp

playwright-mcp

A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.

Official
Featured
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.

Official
Featured
Local
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.

Official
Featured
Local
TypeScript
VeyraX MCP

VeyraX MCP

Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.

Official
Featured
Local
graphlit-mcp-server

graphlit-mcp-server

The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.

Official
Featured
TypeScript
Kagi MCP Server

Kagi MCP Server

An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.

Official
Featured
Python
E2B

E2B

Using MCP to run code via e2b.

Official
Featured
Neon Database

Neon Database

MCP server for interacting with Neon Management API and databases

Official
Featured
Exa Search

Exa Search

A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.

Official
Featured
Qdrant Server

Qdrant Server

This repository is an example of how to create a MCP server for Qdrant, a vector search engine.

Official
Featured