Skip to main content
Glama

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


Related MCP server: creatomate-mcp-server

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 issession-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

Prefercavalry_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 noLinear, 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 ownscale 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.

Install Server
F
license - not found
B
quality
C
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ZHUYUFAN3-33/cavalry-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server