Compare commits

...

1 Commits

Author SHA1 Message Date
Brooklyn Nicholson
2e1dc5c4b8 feat(skills): add optional Rive MCP skill
Add an optional creative/rive-mcp skill for Rive animation workflows. The skill
supports the official desktop Rive HTTP MCP at 127.0.0.1:9791 and documents the
third-party headless RiveMCP stdio path for .riv/.rev generation.

The skill stays opt-in under optional-skills and ships only a small preflight
doctor plus research notes; no core tool or dependency is added.
2026-06-25 15:34:19 -05:00
4 changed files with 336 additions and 0 deletions

View File

@@ -0,0 +1,140 @@
---
name: rive-mcp
description: "Create and edit Rive animations through MCP."
version: 1.0.0
author: Hermes Agent
license: MIT
platforms: [linux, macos, windows]
metadata:
hermes:
category: creative
tags: [Rive, Animation, MCP, Motion, State Machines, Design-to-Code]
related_skills: [blender-mcp]
prerequisites:
commands: [hermes]
---
# Rive Skill
Optional skill — **not active until installed**:
```bash
hermes skills install official/creative/rive-mcp
```
After install, scripts live under `~/.hermes/skills/creative/rive-mcp/`. In prose
below, `{SKILL_DIR}` means that directory (or this repo path before install).
Use Rive from Hermes through MCP. There are two real integration paths:
- **Official Rive desktop MCP** — local HTTP server in the Rive desktop editor
at `http://127.0.0.1:9791/mcp`.
- **RiveMCP** — third-party standalone stdio MCP (`npx -y rivemcp`) that writes
`.riv` / `.rev` files headlessly.
Do not invent a Rive CLI export path. Official Rive exports runtime `.riv`
files from the editor UI; official MCP requires the desktop editor to be open.
## When to Use
- The user wants to create or edit Rive artboards, shapes, animations, state
machines, View Models, scripts, shaders, or runtime `.riv` files.
- The user already has the Rive desktop app open and wants AI to manipulate the
current file.
- The user wants headless `.riv` / `.rev` generation and accepts RiveMCP's
third-party licensing model.
## Prerequisites
Run the doctor first:
```bash
python {SKILL_DIR}/scripts/rive_doctor.py
```
Official path:
- Rive desktop editor / Early Access app on macOS or Windows.
- Rive app open, with a file and artboard created.
- MCP endpoint reachable at `http://127.0.0.1:9791/mcp`.
RiveMCP path:
- `node` / `npx`, or a downloaded RiveMCP binary.
- RiveMCP has 3 free exports per machine; after that it requires
`RIVEMCP_LICENSE_KEY`.
## How to Run
### Path A — Official Rive desktop MCP
Use this when the user is working in the Rive editor and wants live changes.
```bash
hermes mcp add rive --url http://127.0.0.1:9791/mcp
```
When prompted whether the server requires auth, answer **no**. The Rive app
must be open before probing. Start a new Hermes session after adding the MCP so
the tools load.
Rive's official docs say to finish a prompt by typing `End Prompt` so the Rive
editor applies the AI changes. Treat this as part of the interaction contract.
### Path B — RiveMCP headless stdio
Use this when the user wants automation, CI, or file generation without the
Rive desktop app.
```bash
hermes mcp add rivemcp --command npx --args -y rivemcp
```
If the user has a paid key:
```bash
export RIVEMCP_LICENSE_KEY=...
hermes mcp add rivemcp --command npx --env RIVEMCP_LICENSE_KEY=$RIVEMCP_LICENSE_KEY --args -y rivemcp
```
Start a new Hermes session after adding the MCP.
## Quick Reference
| Goal | Path |
| --- | --- |
| Live-edit current Rive file | official `rive` HTTP MCP |
| Headless `.riv` / `.rev` creation | third-party `rivemcp` stdio MCP |
| Inspect official endpoint | `python {SKILL_DIR}/scripts/rive_doctor.py` |
| Configure official MCP | `hermes mcp add rive --url http://127.0.0.1:9791/mcp` |
| Configure RiveMCP | `hermes mcp add rivemcp --command npx --args -y rivemcp` |
## Procedure
1. Ask which path the user wants: **official desktop** or **headless RiveMCP**.
2. Run `rive_doctor.py`.
3. Configure the selected MCP using the commands above.
4. Start a new Hermes session so MCP tools load.
5. For official Rive, keep the Rive app open and use `End Prompt` when the
editor asks for confirmation.
6. For RiveMCP, export `.riv` for runtime or `.rev` for editor handoff.
## Pitfalls
- **Official Rive MCP is desktop-bound.** It is not a headless server and is
currently documented for macOS / Windows desktop editor only.
- **RiveMCP is third-party and licensed.** It offers free exports, then needs a
license key. Make that explicit before choosing it.
- **Do not confuse `.riv` and `.rev`.** `.riv` is runtime output; `.rev` opens
in the Rive editor for further manual editing.
- **Tool surfaces evolve.** After connecting, inspect the actual MCP tools with
`hermes mcp configure rive` or `hermes mcp configure rivemcp`.
## Verification
- Official path: `rive_doctor.py` shows port `9791` reachable, and Hermes MCP
probe lists Rive tools.
- RiveMCP path: Hermes MCP probe lists RiveMCP tools, then export a trivial
`.riv` and open it in a Rive runtime/editor.
See `references/research.md` for source links and verified constraints.

View File

@@ -0,0 +1,60 @@
# Rive integration research
## Official Rive desktop MCP
Source: <https://rive.mintlify.dev/docs/editor/ai/mcp>
- Official MCP is currently documented as available in the desktop Editor for
Windows and macOS.
- The endpoint is local HTTP:
```json
{
"mcpServers": {
"rive": {
"url": "http://127.0.0.1:9791/mcp"
}
}
}
```
- Rive desktop / Early Access app must be open.
- User should have a Rive file open and an artboard created.
- Rive docs describe a confirmation flow: after prompt processing, type
`End Prompt` to allow changes.
- Supported feature areas include files/artboards, scene hierarchy edits,
shapes/layout/components/assets, animations, state machines, View Models,
data binding, Luau scripts, and WGSL shaders.
## RiveMCP
Source: <https://github.com/paradoxsyn/rivemcp-releases>
- Third-party standalone MCP server for programmatically creating/editing Rive
animations.
- Recommended config:
```json
{
"mcpServers": {
"rivemcp": {
"command": "npx",
"args": ["-y", "rivemcp"]
}
}
}
```
- Exposes 139+ tools across project, shapes, styling, animation, state
machines, text, images, bones, nested artboards, events, data binding,
advanced layout/scroll/audio/scripting, HLAPI, sprites, import/export, edit.
- Can export runtime `.riv` and editor `.rev`.
- 3 free exports per machine; license required after that via
`RIVEMCP_LICENSE_KEY`.
## Non-paths
- Official Rive runtime export is an editor action, not an official CLI export
command.
- Do not claim headless official Rive support; use RiveMCP if the user needs
headless generation.

View File

@@ -0,0 +1,76 @@
#!/usr/bin/env python3
"""Preflight checks for the optional Rive MCP skill.
No installs, no MCP process spawning. Reports whether the official Rive
desktop MCP port is reachable and whether the optional headless RiveMCP path
has the Node/npx bits needed to launch `npx -y rivemcp`.
"""
from __future__ import annotations
import argparse
import json
import shutil
import socket
RIVE_HOST = "127.0.0.1"
RIVE_PORT = 9791
def _port_open(host: str, port: int, timeout: float = 0.35) -> bool:
try:
with socket.create_connection((host, port), timeout=timeout):
return True
except OSError:
return False
def check(*, which=shutil.which, port_open=_port_open) -> dict:
node, npx, rivemcp = which("node"), which("npx"), which("rivemcp")
return {
"official_rive": {
"url": f"http://{RIVE_HOST}:{RIVE_PORT}/mcp",
"reachable": port_open(RIVE_HOST, RIVE_PORT),
},
"rivemcp": {
"node": bool(node),
"npx": bool(npx),
"global_binary": bool(rivemcp),
"launch": "npx -y rivemcp",
},
}
def _summary(s: dict) -> str:
official, headless = s["official_rive"], s["rivemcp"]
lines = [
(
f"✓ official Rive MCP: {official['url']} reachable"
if official["reachable"]
else f"✗ official Rive MCP: {official['url']} not reachable "
"(open the Rive desktop editor / Early Access app)"
)
]
if headless["npx"]:
lines.append("✓ RiveMCP launch path: npx -y rivemcp")
elif headless["global_binary"]:
lines.append("✓ RiveMCP global binary found: rivemcp")
else:
lines.append("✗ RiveMCP: need npx or a downloaded rivemcp binary")
if not headless["node"] and not headless["global_binary"]:
lines.append("⚠ node not found; npm/npx RiveMCP install path unavailable")
return "\n".join(lines)
def main(argv=None) -> int:
ap = argparse.ArgumentParser(description="Check Rive MCP integration paths.")
ap.add_argument("--json", action="store_true", help="Emit JSON")
args = ap.parse_args(argv)
status = check()
print(json.dumps(status, indent=2) if args.json else _summary(status))
return 0 if status["official_rive"]["reachable"] or status["rivemcp"]["npx"] else 1
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,60 @@
"""Tests for the optional Rive MCP skill."""
from __future__ import annotations
import importlib.util
import re
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
SKILL_DIR = ROOT / "optional-skills" / "creative" / "rive-mcp"
def _load(path: Path, name: str):
spec = importlib.util.spec_from_file_location(name, path)
assert spec and spec.loader
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
def _description(skill_dir: Path) -> str:
text = (skill_dir / "SKILL.md").read_text()
match = re.search(r'^description:\s*"?([^"\n]+)"?\s*$', text, re.MULTILINE)
assert match
return match.group(1)
class TestRiveMcpSkillShape:
def test_skill_lives_under_optional_skills(self):
assert (SKILL_DIR / "SKILL.md").is_file()
assert not (ROOT / "skills" / "creative" / "rive-mcp").exists()
def test_description_is_catalog_sized(self):
desc = _description(SKILL_DIR)
assert len(desc) <= 60, desc
assert desc.endswith(".")
def test_optional_skill_source_fetches_skill(self):
from tools.skills_hub import OptionalSkillSource
source = OptionalSkillSource()
assert source.fetch("official/creative/rive-mcp").name == "rive-mcp"
class TestRiveDoctor:
def test_official_port_reachable(self):
doctor = _load(SKILL_DIR / "scripts" / "rive_doctor.py", "rive_doctor")
status = doctor.check(
which={"node": "/bin/node", "npx": "/bin/npx"}.get,
port_open=lambda host, port: (host, port) == ("127.0.0.1", 9791),
)
assert status["official_rive"]["reachable"] is True
assert status["rivemcp"]["npx"] is True
def test_no_paths_available(self):
doctor = _load(SKILL_DIR / "scripts" / "rive_doctor.py", "rive_doctor_none")
status = doctor.check(which=lambda _name: None, port_open=lambda *_a: False)
assert status["official_rive"]["reachable"] is False
assert status["rivemcp"]["npx"] is False