cavalry-mcp
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@cavalry-mcpbuild me a looping loader animation with eight dots on a circle"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
cavalry-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 |
| |
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:
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.Bridge (
cavalry-bridge.js) — runs inside Cavalry viaapi.WebServer, listening on loopback. It routes ops toapi.*calls and returns results.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 _ridEvery 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 |
| Bad input, rejected before dispatch |
| Connection refused or timed out |
| Non-200 from the bridge |
| Response body unparseable |
| Cavalry returned |
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 buildVerify the build with the offline test suite (no running Cavalry required):
npm test2. Install the bridge
Copy cavalry-bridge.js into Cavalry's user scripts folder:
OS | Path |
Windows |
|
macOS |
|
Scripts in that folder appear under Window > Scripts immediately — no Cavalry restart needed. Run cavalry-bridge from that menu to start the listener.
The bridge issession-scoped. Re-run it from the Scripts menu each time you restart Cavalry. The MCP server does not need restarting.
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"
}
}
}
}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 |
|
| Bridge host |
|
| Bridge port — must match |
|
| Per-request timeout |
| (unset) | Set to |
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 |
| — |
|
| — |
|
| — |
|
| — |
|
|
| attribute value |
|
|
|
|
|
|
|
|
|
|
|
|
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 |
|
|
|
|
generator ∈ ellipse · rectangle · star · polygon · ring · arrow · superEllipse · line
layerType ∈ textShape · null · duplicator · stagger · basicShape
Prefercavalry_create_primitive over create_layer + set_generator — one round-trip instead of two.
Mutation
Tool | Arguments |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Animation
Tool | Arguments |
|
|
|
|
|
|
|
|
| — |
easingType ∈ SlowIn · SlowOut · SlowInSlowOut · VerySlowIn · VerySlowOut · SpringIn · SpringOut · BounceIn · BounceOut · Custom · None
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 |
|
|
|
|
|
|
Batch
Tool | Arguments |
|
|
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 |
| number | |
| number |
|
| number | Degrees. |
| number | 0–100 |
Appearance
Path | Type | Notes |
| string | Hex, |
| object | GET only. Returns |
Duplicator
Path | Type | Notes |
| number | Distribution point count |
| number | Circle distribution radius |
| number | Arc control |
| number | Per-copy scale |
| number | Per-copy rotation |
| number | Per-copy opacity |
Text
Path | Type | Notes |
| string | Plain string |
| number | |
| boolean |
|
| number |
|
Composition
Path | Type | Notes |
| number | Read-only |
| number |
Common pitfalls
Instead of | Use |
|
|
|
|
|
|
setting |
|
Layer IDs
IDs follow typeName#number — basicShape#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"
)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:
Fail-fast, not atomic. If op 4 fails, ops 1–3 have already been applied. There is no rollback.
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 injectionU+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 :8080Remember 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 standaloneLayout
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, protocolAdding a tool
Add the
opbranch todispatch()incavalry-bridge.js.Register the tool with a Zod schema in the matching
src/tools/*.ts.If it should be batchable, add the op name to the enum in
src/tools/batch.ts.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.
Maintenance
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
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
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