内容
<p align="center">
<img src="assets/banner.png" alt="cavalry-mcp — Cavalry 的 MCP 服务器" width="100%">
</p>
# cavalry-mcp
[](https://nodejs.org)
[](https://www.typescriptlang.org/)
[](https://modelcontextprotocol.io)
**通过大语言模型驱动 [Cavalry](https://cavalry.scenegroup.co/)。** 一个 [MCP](https://modelcontextprotocol.io) 服务器,暴露 Cavalry 的 2D 动态图形引擎为 28 个结构化工具 — 创建图层,连接程序化图表,设置关键帧,应用缓动,并渲染帧,均可通过自然语言完成。
将 Claude 指向一个合成画面,并说 *"为我创建一个带有 8 个圆圈上的循环加载动画"* — 它会检查场景,构建复制器图表,为旋转设置关键帧,并渲染一个 PNG 以检查其自身的工作。
---
## 目录
- [为什么](#为什么)
- [架构](#架构)
- [安装](#安装)
- [配置](#配置)
- [工具参考](#工具参考)
- [属性参考](#属性参考)
- [批处理](#批处理)
- [安全](#安全)
- [故障排除](#故障排除)
- [开发](#开发)
---
## 为什么
Cavalry 提供了一个丰富的 JavaScript API,但它位于应用程序内编辑器之后 — 没有进程外的入口点。`cavalry-mcp` 添加了一个,并将其封装在一个 **类型化的、经过验证的分派器** 中,而不是直接交给模型一个原始的 `eval`。
这种区别是整个设计的核心:
| | 原始脚本执行 | `cavalry-mcp` |
|---|---|---|
| 输入验证 | 无 | 每个工具的 Zod 模式 |
| 注入面 | 完整 | 关闭 — 参数为 JSON,从不插入源代码 |
| 失败模式 | 静默控制台错误 | 类型错误分类 |
| 爆炸半径 | 文件系统 + 子进程 | 场景图仅 |
模型获得了它需要的确切动词,并且每个动词在到达 Cavalry 之前都会被检查。
---
## 架构
```mermaid
graph LR
A[MCP 客户端<br/>Claude 代码 / 桌面版] -->|stdio<br/>JSON-RPC| B[cavalry-mcp<br/>Node + TypeScript]
B -->|HTTP 127.0.0.1:8080| C[cavalry-bridge.js<br/>应用程序内 Web 服务器]
C -->|api.*| D[Cavalry<br/>场景图]
```
三个进程,两个跳跃:
1. **MCP 服务器** (`dist/src/index.js`) — 通过 stdio 进行 JSON-RPC,验证工具参数并序列化每个调用为版本化的调度有效载荷。
2. **桥梁** (`cavalry-bridge.js`) — 通过 `api.WebServer` 在 Cavalry 内部运行,监听环回地址。它将操作路由到 `api.*` 调用并返回结果。
3. **Cavalry** — 宿主应用程序。
### 请求生命周期
桥梁处理 Cavalry 的实时回调(~60 fps)并存储结果以供检索,因此传输是 **POST-then-poll** 而不是请求/响应:
```
POST /post { payloadVersion: 1, op: "keyframe", args: {...}, _rid: <uuid> }
GET /get → 每 30 毫秒轮询,直到正文中包含我们的 _rid
```
每个请求都带有 UUID `_rid`。客户端只接受包含其自身 ID 的响应,因此并发调用永远不会读取彼此的结果。
结果在哨兵信封内返回,因为 `api.log()` 可能会写入同一个缓冲区:
```
<CAVALRY_MCP_RESULT>{"ok":true,...}<CAVALRY_MCP_RESULT>
```
解析器扫描 **最后一个** 匹配的标签对,使其能够容忍日志噪音中可能包含类似标签的文本。
### 错误分类
失败被分类而不是折叠成字符串,以便代理可以区分“Cavalry 未运行”和“您传递了错误的属性路径”:
| 类型 | 含义 |
|---|---|
| `validation` | 错误的输入,在调度前被拒绝 |
| `transport` | 连接被拒绝或超时 |
| `http` | 桥梁的非 200 状态码 |
| `envelope` | 响应正文无法解析 |
| `remote` | Cavalry 返回 `{ ok: false }` |
---
## 安装
### 先决条件
- **Node.js ≥ 18** (传输使用全局 `fetch`)
- **Cavalry 2.7+**
- 一个 MCP 客户端 — [Claude 代码](https://claude.com/claude-code),Claude 桌面版,或任何 MCP 兼容的主机
### 1. 构建服务器
```bash
git clone https://github.com/nxnai/cavalry-mcp.git
cd cavalry-mcp
npm install
npm run build
```
使用离线测试套件验证构建(无需运行 Cavalry):
```bash
npm test
```
### 2. 安装桥梁
将 `cavalry-bridge.js` 复制到 Cavalry 的用户脚本文件夹中:
| 操作系统 | 路径 |
|---|---|
| Windows | `%APPDATA%\Cavalry\Scripts\` |
| macOS | `~/Library/Application Support/Cavalry/Scripts/` |
该文件夹中的脚本会立即出现在 **`Window > Scripts`** 下 — 无需重启 Cavalry。 从该菜单运行 **`cavalry-bridge`** 以启动监听器。
> [!IMPORTANT]
> 桥梁是 **会话范围** 的。每次重启 Cavalry 时,请从脚本菜单重新运行它。MCP 服务器无需重启。
> [!WARNING]
> **Cavalry 必须保持在最前面**,而工具运行时。Cavalry 在应用程序在后台时暂停脚本回调,这会使桥梁停止,直到您重新聚焦它。请求会被排队而不是丢失,客户端会快速失败并出现 `frozen` 错误而不是挂起。参见 [故障排除](#故障排除)。
或者,将脚本粘贴到 **`Window > JavaScript 编辑器`** 并在那里运行。
### 3. 注册 MCP 服务器
在您的 MCP 客户端配置中添加以下内容 — `.mcp.json` 在您的项目根目录中用于 Claude 代码,或 `claude_desktop_config.json` 用于 Claude 桌面版:
```json
{
"mcpServers": {
"cavalry": {
"command": "node",
"args": ["/绝对路径/到/cavalry-mcp/dist/src/index.js"],
"env": {
"CAVALRY_MCP_HOST": "127.0.0.1",
"CAVALRY_MCP_PORT": "8080",
"CAVALRY_MCP_TIMEOUT_MS": "15000"
}
}
}
}
```
> [!NOTE]
> 在 Windows 上,转义 JSON 中的反斜杠(`C:\\Users\\...`)或使用正斜杠。
重启您的 MCP 客户端,然后确认往返:
```
cavalry_ping → { "ok": true, "platform": "Windows" }
```
---
## 配置
所有配置均通过环境变量进行,在启动时解析和验证。无效值会快速失败并显示描述性错误,而不是静默地回落。
| 变量 | 默认值 | 描述 |
|---|---|---|
| `CAVALRY_MCP_HOST` | `127.0.0.1` | 桥梁主机 |
| `CAVALRY_MCP_PORT` | `8080` | 桥梁端口 — 必须与 `cavalry-bridge.js` 中的 `PORT` 匹配 |
| `CAVALRY_MCP_TIMEOUT_MS` | `15000` | 每个请求的超时时间 |
| `CAVALRY_MCP_ENABLE_RUN_SCRIPT` | *(未设置)* | 设置为 `1` 以公开 `cavalry_run_script`。参见 [安全](#安全)。 |
更改端口需要编辑 **两侧** — 环境变量和 `cavalry-bridge.js` 顶部的 `PORT`。
---
## 工具参考
默认情况下有 28 个工具;启用 `cavalry_run_script` 时有 29 个工具。
### 自省
| 工具 | 参数 | 返回值 |
|---|---|---|
| `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` | 属性值 |
| `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]
> **永远不要猜测属性路径 — 先调用 `cavalry_list_attributes`。** Cavalry 文档列出了属性的 UI 标签,而不是其脚本 ID,而这两者并不总是匹配:文档中的“分组”和“自定义字符串”在 Shuffle 字符串操纵器上根本不存在于脚本路径中。错误的路径在写入时 **静默** 失败,因此猜测会产生报告成功但无效果的更改。
>
> 同样,`cavalry_create_layer` 需要脚本类型 ID,而不是显示名称 — “Shuffle 字符串操纵器”是 `shuffleString`。使用 `cavalry_list_layer_types` 而不是推断。
### 创建
| 工具 | 参数 |
|---|---|
| `cavalry_create_layer` | `layerType`, `name?` |
| `cavalry_create_primitive` | `generator`, `name?` |
`generator` ∈ `ellipse` · `rectangle` · `star` · `polygon` · `ring` · `arrow` · `superEllipse` · `line`
`layerType` ∈ `textShape` · `null` · `duplicator` · `stagger` · `basicShape`
> [!TIP]
> 优先使用 `cavalry_create_primitive` 而不是 `create_layer` + `set_generator` — 一次往返而不是两次。
### 修改
| 工具 | 参数 |
|---|---|
| `cavalry_set_attribute` | `layerId`, `attributes` *(路径到值的映射)* |
| `cavalry_set_generator` | `layerId`, `generator`, `attrId?` *(默认为 `"generator"`)* |
| `cavalry_select` | `layerIds` |
| `cavalry_delete_layers` | `layerIds` |
| `cavalry_duplicate` | `layerIds` — 恢复之前的选择 |
| `cavalry_parent` | `childIds`, `parentId` |
| `cavalry_add_dynamic_attribute` | `layerId`, `attrName`, `attrType` |
| `cavalry_connect` | `fromLayerId`, `fromAttrId`, `toLayerId`, `toAttrId`, `force?` |
### 动画
| 工具 | 参数 |
|---|---|
| `cavalry_keyframe` | `layerId`, `frame`, `attributes` |
| `cavalry_magic_easing` | `layerId`, `attrPath`, `frame`, `easingType` |
| `cavalry_delete_keyframe` | `layerId`, `attrPath`, `frame` |
| `cavalry_set_frame` | `frame` |
| `cavalry_play` | — |
`easingType` ∈ `SlowIn` · `SlowOut` · `SlowInSlowOut` · `VerySlowIn` · `VerySlowOut` · `SpringIn` · `SpringOut` · `BounceIn` · `BounceOut` · `Custom` · `None`
> [!WARNING]
> Magic Easing 没有 `Linear`、`EaseIn`、`EaseOut`、`BackIn` 或 `ElasticIn`。枚举在模式层强制执行,因此在到达 Cavalry 之前拒绝无效名称。
### 输入/输出
| 工具 | 参数 |
|---|---|
| `cavalry_save_scene` | `filePath?` — 省略则在原位保存 |
| `cavalry_load_scene` | `filePath` |
| `cavalry_render_png` | `filePath`, `frame?`, `scale?` *(默认为 `100`)* |
### 批处理
| 工具 | 参数 |
|---|---|
| `cavalry_batch` | `ops: [{ op, args? }]` |
---
## 属性参考
Cavalry 的属性路径并不总是可以猜测的,而错误的路径 **静默** 失败。以下是已验证的路径。
### 变换
| 路径 | 类型 | 备注 |
|---|---|---|
| `position.x` / `position.y` | number | |
| `scale.x` / `scale.y` | number | `1.0` = 100%。**这是调整形状大小的方法。** |
| `rotation.z` | number | 度。`rotation` 是 3D 矢量 — 在 2D 中始终使用 `.z`。 |
| `opacity` | number | 0–100 |
### 外观
| 路径 | 类型 | 备注 |
|---|---|---|
| `material.materialColor` | string | 十六进制,`"#rrggbb"` |
| `generator.dimensions` | object | **仅 GET。** 返回 `{x, y}`;设置它会抛出控制台错误。 |
### 复制器
| 路径 | 类型 | 备注 |
|---|---|---|
| `generator.count` | number | 分布点数 |
| `generator.radius` | number | 圆形分布半径 |
| `generator.startAngle` / `generator.angle` | number | 弧控制 |
| `shapeScale.x` / `shapeScale.y` | number | 每个副本的缩放比例 |
| `shapeRotation` | number | 每个副本的旋转 |
| `shapeOpacity` | number | 每个副本的不透明度 |
### 文本
| 路径 | 类型 | 备注 |
|---|---|---|
| `text` | string | 纯字符串 |
| `fontSize` | number | |
| `autoWidth` | boolean | 默认为 `false`,导致换行 |
| `horizontalAlignment` | number | `0` 左 · `1` 居中 · `2` 右 |
### 组成
| 路径 | 类型 | 备注 |
|---|---|---|
| `resolution.x` / `resolution.y` | number | 只读 |
| `startFrame` / `endFrame` / `fps` | number | |
### 常见陷阱
| 代替 | 使用 |
|---|---|
| `width`, `height`, `size` | `scale.x` / `scale.y` |
| `fill.color`, `color`, `material.color` | `material.materialColor` |
| `rotation` | `rotation.z` |
| 设置 `generator.dimensions` | `scale.x` / `scale.y` |
### 图层 ID
ID 遵循 `typeName#number` — `basicShape#1`、`duplicator#2`、`textShape#3`。计数器 **全局递增并永不重置**,即使在删除后也是如此。始终使用 `cavalry_list_layers` 发现它们;永远不要假设。
### 大小示例
原始大小为 200×200。对于 1920×1080 的背景:
```
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"
})
```
### 复制器示例
形状 **连接** 到复制器,而不是其父级:
```
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]
> 源形状的自身 `scale` **不影响** 复制的副本。使用复制器上的 `shapeScale.x` / `shapeScale.y`。
---
## 批处理
`cavalry_batch` 将一系列操作合并为单个 HTTP 往返。对于多步骤构建,这意味着 20 次轮询与一次轮询的差异。
```json
{
"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 } } }
]
}
```
支持的操作:`create` · `createPrimitive` · `set` · `setGenerator` · `select` · `delete` · `duplicate` · `parent` · `addDynamic` · `keyframe` · `magicEasing` · `deleteKeyframe` · `setFrame` · `play` · `saveScene` · `loadScene` · `renderPng`
两个约束值得内部化:
> [!WARNING]
> **快速失败,而不是原子。** 如果操作 4 失败,则操作 1-3 已被应用。没有回滚。
> [!WARNING]
> **无 ID 替换。** 在批处理中创建的 ID 不会在后续操作中插入。或者预测顺序 ID,或将批处理拆分并在两者之间调用 `cavalry_list_layers`。
---
## 安全
### 参数注入在构造上被关闭
工具参数作为 **JSON 数据** 传输,并由 `op` 字符串调度到 Cavalry 中。它们从未连接为 JavaScript 源代码,因此没有字符串转义边界需要正确。
测试套件将此视为首要不变式,包含专门的案例:
- 反引号/模板字符串注入
- 双引号转义和链接调用
- `</script>` 标签注入
- U+2028 / U+2029 行分隔符注入
### `cavalry_run_script` 按需启用
设置 `CAVALRY_MCP_ENABLE_RUN_SCRIPT=1` 会注册一个执行 **任意 JavaScript** 的工具,绕过调度程序直接进入 Cavalry。Cavalry 的 `api` 命名空间包括文件系统和子进程访问,因此这授予模型两者。
默认情况下它是关闭的,服务器在打开时向 stderr 打印警告。**保持禁用**,除非您正在调试桥梁本身。
### 网络暴露
桥梁绑定到 `127.0.0.1` 并执行 **无身份验证** — 任何本地进程都可以在运行时驱动它。不要将其绑定到可路由接口,并在不使用时停止它。
---
## 故障排除
**`[transport] Cannot reach cavalry-mcp bridge at http://127.0.0.1:8080`**
桥梁未运行。Cavalry 打开并不够 — 运行 `Window > Scripts > cavalry-bridge`。确认端口是否活动:
```bash
# Windows
netstat -ano | findstr :8080
# macOS
lsof -i :8080
```
记住,桥梁在 Cavalry 会话结束时死亡。
**`[frozen] Cavalry accepted the request but never processed it`**
当 Cavalry 不是活动应用程序时,它会暂停脚本回调,因此桥梁停止排空其队列。把 Cavalry 带到前面 — 后备队列在聚焦时运行,丢失的内容很少。
这是 Cavalry 级别的门控,而不是调优问题。测量:15/15 个请求在 81 秒内失败,而 15/15 个请求在聚焦后成功。`setRealtime()`(60/s)、`setHighFrequency()`(1/s)和默认(3/s)都是相同的轮询驱动程序,间隔为三个,因此更改速率不会产生影响。保持 Cavalry 在前进行代理驱动的会话。
**客户端工具不显示**
在编辑配置后重启 MCP 客户端。检查 `args` 是否指向 `dist/src/index.js` — 请注意 `src` 段,后者遵循 `tsconfig.json` 中的 `rootDir: "."`。如果缺少 `dist/`,请运行 `npm run build`。
**属性设置报告成功但无变化**
路径几乎肯定是错误的 — Cavalry 对未知路径不报错。 使用 `cavalry_get_attribute` 读取它,交叉检查 [属性参考](#属性参考),并使用 `cavalry_bounding_box` 或 `cavalry_render_png` 直观验证。
**`[envelope] No envelope tags found in response`**
桥梁返回的输出不包含完整的信标对 — 通常是过时或部分粘贴的桥梁脚本。重新复制 `cavalry-bridge.js` 并在 Cavalry 中重新运行。
**请求在负载下超时**
重渲染可能超过默认的 15 秒。提高 `CAVALRY_MCP_TIMEOUT_MS`。
---
## 开发
```bash
npm install # 安装依赖
npm run build # tsc → dist/
npm test # 18 个单元测试,无需运行 Cavalry
npm start # 独立运行服务器
```
### 布局
```
src/
├── index.ts # 服务器入口 — 工具注册
├── config.ts # 环境变量解析,在导入时验证
├── errors.ts # CavalryError 分类
├── helpers.ts # 调度 + MCP 结果整形
├── bridge/
│ ├── dispatch.ts # 有效载荷构造
│ ├── dispatcher-source.ts
│ └── envelope.ts # 信标标签解析器
├── transport/
│ └── bridge.ts # POST-then-poll HTTP 客户端
└── tools/
├── introspection.ts creation.ts mutation.ts
├── animation.ts io.ts batch.ts
└── run-script.ts # 在环境标志后启用
cavalry-bridge.js # 在 Cavalry 中运行
test/test.ts # 调度器、信封、注入、协议
```
### 添加工具
1. 在 `cavalry-bridge.js` 中将 `op` 分支添加到 `dispatch()`。
2. 在匹配的 `src/tools/*.ts` 中使用 Zod 模式注册工具。
3. 如果应该支持批处理,请将操作名称添加到 `src/tools/batch.ts` 中的枚举中。
4. 在 `test/test.ts` 中覆盖路由。
构建输出必须与桥梁保持同步 — 编辑 `cavalry-bridge.js` 后,请将其复制到脚本文件夹并在 Cavalry 中重新运行。
MCP 配置
以下是此 MCP Server 的配置信息,可直接复制到 Cursor 或其他 MCP 客户端中使用。
mcp.json
连接信息
猜你喜欢
暂无推荐服务器