From 2e1dc5c4b8f966b37d665bad0a3c0d4a33ae5dcc Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Thu, 25 Jun 2026 15:34:19 -0500 Subject: [PATCH] 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. --- optional-skills/creative/rive-mcp/SKILL.md | 140 ++++++++++++++++++ .../creative/rive-mcp/references/research.md | 60 ++++++++ .../creative/rive-mcp/scripts/rive_doctor.py | 76 ++++++++++ tests/skills/test_rive_mcp_optional_skill.py | 60 ++++++++ 4 files changed, 336 insertions(+) create mode 100644 optional-skills/creative/rive-mcp/SKILL.md create mode 100644 optional-skills/creative/rive-mcp/references/research.md create mode 100644 optional-skills/creative/rive-mcp/scripts/rive_doctor.py create mode 100644 tests/skills/test_rive_mcp_optional_skill.py diff --git a/optional-skills/creative/rive-mcp/SKILL.md b/optional-skills/creative/rive-mcp/SKILL.md new file mode 100644 index 00000000000..53e66d37034 --- /dev/null +++ b/optional-skills/creative/rive-mcp/SKILL.md @@ -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. diff --git a/optional-skills/creative/rive-mcp/references/research.md b/optional-skills/creative/rive-mcp/references/research.md new file mode 100644 index 00000000000..cc3c304d35e --- /dev/null +++ b/optional-skills/creative/rive-mcp/references/research.md @@ -0,0 +1,60 @@ +# Rive integration research + +## Official Rive desktop MCP + +Source: + +- 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: + +- 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. diff --git a/optional-skills/creative/rive-mcp/scripts/rive_doctor.py b/optional-skills/creative/rive-mcp/scripts/rive_doctor.py new file mode 100644 index 00000000000..8af75ed8fd1 --- /dev/null +++ b/optional-skills/creative/rive-mcp/scripts/rive_doctor.py @@ -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()) diff --git a/tests/skills/test_rive_mcp_optional_skill.py b/tests/skills/test_rive_mcp_optional_skill.py new file mode 100644 index 00000000000..bc14367c10c --- /dev/null +++ b/tests/skills/test_rive_mcp_optional_skill.py @@ -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