原始内容
name: rhinoclaw
version: 0.7.2
description: Control Rhino 3D via the RhinoClaw plugin over TCP. Use whenever you need to create, modify, query, measure, or render 3D geometry, manage layers/materials, run Boolean and transform ops, drive Grasshopper definitions, work with VisualARQ BIM objects, or batch multiple steps as one atomic operation. Requires Rhino 7/8 running with tcpstart (the plugin's command line server) on the Rhino host.
RhinoClaw Skill
Direct TCP control of Rhino 3D from an OpenClaw agent. Plugin is running on a Windows or macOS Rhino instance; this skill talks to it from WSL / Linux / wherever you live.
Step 0 — ALWAYS run
preflightfirst. One call returns the complete connection/auth state and an exactnext_action. Only call other tools whendata.auth == "ready". Then read § "Decision Tree" to pick the cheapest tool.
0. Preflight & error-code playbook (read before touching auth)
Never trial-and-error the connection. Run preflight (MCP) — or, on the raw
TCP path, hello then a single ping — and act on the result. hello is
auth-free: it reports the server's state even with no/wrong token and even
while blocked, without tripping the brute-force counter.
preflight.data.auth / error_code |
Meaning | Do exactly this |
|---|---|---|
ready |
Connected, auth ok | Proceed. |
missing_client_token / AUTH_REQUIRED (no token) |
Plugin wants a token, client sends none | Set RHINOCLAW_AUTH_TOKEN on the client (same value as the plugin). Do NOT retry blindly. Using the deployed skill? Re-sync it: scripts/sync-skill.sh. |
token_mismatch / AUTH_REQUIRED (wrong token) |
Both sides have a token, they differ | Make RHINOCLAW_AUTH_TOKEN identical on both sides, then restart Rhino (the plugin reads the token only at start). |
blocked / AUTH_BLOCKED |
Too many failed auth attempts | Wait until blocked_until. Do NOT retry — retries extend the cooldown. |
not connected / CONNECTION_REFUSED |
Server not running / wrong host:port | Open Rhino, run tcpstart (remote) or mcpstart (local). On WSL the Windows host IP changes per session — check RHINOCLAW_HOST. |
TIMEOUT |
Command reached Rhino but blocked the UI thread / too slow | Command-specific — pass a larger timeout. Not an auth problem; do not re-auth. |
Rules of thumb:
- Auth errors are configuration errors, not transient — never loop-retry them.
- The plugin reads
RHINOCLAW_AUTH_TOKENonly at start — changed the env after Rhino was running? Restart Rhino (or the RhinoClaw server). - One source of truth:
~/projects/RhinoClaw. The deployed skill at~/clawd/skills/rhinoclawis generated byscripts/sync-skill.sh— ifpreflightorhellolook stale, re-sync.
Decision Tree — pick the cheapest tool that works
┌──────────────────────────────────┐
│ What do you need to do? │
└──────────────────────────────────┘
│
┌───────────────────┼───────────────────┐
▼ ▼ ▼
single op many ops at once inspect / measure
│ │ │
▼ ▼ ▼
typed module batch_operations scene_summary
(geometry, with rollback find_nearby
transforms, get_relationships
booleans, get_object_info
layers,
materials,
visualarq…)
Preference order — always try in this order, drop one level only when the previous can't express what you need:
- Typed Python helper in
scripts/rhinoclaw_client/<module>.py(geometry.create_box,booleans.union,transforms.copy, …). Validated, undo-aware, never executes arbitrary code. batch_operationswhen you need ≥2 steps in one atomic shot (rollback / abort / continue / best-effort policies).script_exec.run_pythonwithimport rhinoscriptsyntax as rs(rs.AddSphere,rs.LayerNames, …). Use when no typed helper exists. RhinoScript is well documented and agent-friendly.- RhinoCommon directly via
script_exec.run_python. Use whenrhinoscriptsyntaxdoesn't expose what you need. Cheatsheet atdocs/rhinocommon-cookbook.mdin the dev repo. run_native_command— firesRhinoApp.RunScript("_Loft …"). Last-resort hatch for commands that have no RhinoCommon equivalent (or where the native one is much simpler). Allowlisted by the plugin.
If you don't know what tools exist, call list_capabilities once at
session start. It returns a categorised inventory of typed tools,
helpers, native-command allowlist, and RhinoCommon cookbook topics.
Prerequisites
- Rhino 7/8 running on Windows (or macOS).
- RhinoClaw plugin ≥ 0.2.8 installed.
- Plugin server started:
mcpstartfor local-only AI clients (Cursor / Claude Desktop).tcpstartfor remote / WSL / raw-TCP clients.
- Auth token (recommended for tcpstart): set
RHINOCLAW_AUTH_TOKENon both the Rhino host and the client side. See SECURITY.md andscripts/setup-auth-token.ps1in the repo.
Configuration
config.json next to the scripts:
{
"connection": {
"host": "172.31.96.1",
"port": 1999,
"timeout": 15.0
}
}
All four fields can be overridden by env vars: RHINOCLAW_HOST,
RHINOCLAW_PORT, RHINOCLAW_TIMEOUT, RHINOCLAW_AUTH_TOKEN. The
rhino_client.py module attaches the auth token to every TCP command
automatically — agent never has to manage it manually.
Quick Test — run preflight first
cd ~/clawd/skills/rhinoclaw/scripts
python3 rhino_client.py preflight # one call → connection/auth state + next_action
# → {"connected": true, "auth": "ready", "plugin_version": "0.5.0.0", ...}
Act only when "auth": "ready". Otherwise follow next_action exactly
(see §0). python3 rhino_client.py hello shows the raw auth-free handshake;
python3 rhino_client.py ping is the authenticated liveness check.
TCP = MCP parity — what raw TCP can do
The raw client reaches every plugin command via the generic
send_command(cmd_type, params) — the same capabilities as the MCP server:
from rhino_client import RhinoClient
with RhinoClient() as c:
c.preflight() # connection/auth verdict
c.send_command("create_object", {"type": "BOX", "params": {...}})
c.send_command("build_and_bake_gh", {...}) # author + solve + bake a .gh
c.build_and_bake_recipe("box", "C:/tmp/b.gh", # native parametric recipe
params={"x": 40, "y": 20, "z": 10}, layer="Boxes")
c.build_and_bake_recipe("list") # list recipes
Client-side high-level helpers mirror the MCP tools: preflight(), hello(),
build_and_bake_recipe(). Everything else is a direct send_command (the
plugin dispatches it). Recipes live in the plugin, so MCP and TCP behave
identically. Requires a plugin build with these commands (0.5.0+ after rebuild).
Cookbook — common workflows
Five concrete recipes. Adapt freely; always prefer typed helpers over inline Python where possible.
Recipe 1 — "Create a layered box with a material" (atomic)
from rhino_client import RhinoClient
client = RhinoClient(); client.connect()
result = client.send_command("batch_operations", {
"name": "Layered box",
"on_error": "rollback",
"steps": [
{"tool": "create_layer",
"args": {"name": "Walls", "color": [180, 180, 180]}},
{"tool": "create_material",
"args": {"name": "Concrete", "color": [200, 200, 200]}},
{"tool": "assign_material_to_layer",
"args": {"layer_name": "Walls", "material_id": 0}},
{"tool": "create_object",
"args": {"type": "BOX", "layer": "Walls",
"params": {"width": 5, "length": 3, "height": 2.5}}},
],
})
Recipe 2 — "Find every object near origin and inspect relationships"
nearby = client.send_command("find_nearby", {
"point": [0, 0, 0], "radius": 100, "layer": "Walls",
})
for wall in nearby["results"]:
relations = client.send_command("get_relationships", {
"object_id": wall["id"],
"touch_tolerance": 0.05,
})
print(wall["name"], "touches", len(relations["touching"]), "objects")
Recipe 3 — "Boolean a hole through a wall, leave geometry on failure"
# `abort` policy: stop on first failure but keep completed steps. Lets
# the user inspect/fix manually instead of an automatic rollback.
client.send_command("batch_operations", {
"on_error": "abort",
"steps": [
{"tool": "create_object", "args": {"type": "CYLINDER", "params": {...}}},
{"tool": "boolean_operation",
"args": {"operation": "difference",
"primary_id": "<wall>", "secondary_ids": ["<cylinder>"]}},
],
})
Recipe 4 — "Run a Grasshopper definition with the file's own defaults"
# Inspect first to discover what the GH file actually wants — never guess.
info = client.send_command("inspect_grasshopper_definition", {
"file_path": r"C:\path\to\Door.gh",
"only_player_inputs": True,
})
print("Inputs:", [i["name"] for i in info["inputs"]])
# Run with the defaults the GH file already exposed:
client.send_command("run_grasshopper_interactive", {
"file_path": r"C:\path\to\Door.gh",
"inputs": {i["name"]: i.get("default")
for i in info["inputs"] if i.get("default") is not None},
})
Recipe 5 — "Use a native Rhino command when nothing else fits"
# rhinoscriptsyntax has no clean Sweep1 wrapper — use the underlying
# native Rhino command. Plugin allowlists known-safe commands.
client.send_command("run_native_command", {
"command": "_Sweep1",
"args": ["_Pause", "_Pause", "_Enter"],
})
RhinoCommon and rhinoscriptsyntax — when typed tools don't reach
When you need something no typed RhinoClaw tool covers, you have two
scripting paths via script_exec.run_python:
Option A — rhinoscriptsyntax (start here)
Friendlier API, well documented, almost-Rhino-Python-Console-equivalent. Find functions:
- Online: https://developer.rhino3d.com/guides/rhinopython/
- Indexed locally:
get_rhinoscript_python_function_names(["surface", "curve"])returns matching function names. get_rhinoscript_python_code_guide("AddLoftSrf")returns the parameter signature + example.
Example: get all object IDs on a layer
client.send_command("execute_rhinoscript_python_code", {
"code": "import rhinoscriptsyntax as rs; print(rs.ObjectsByLayer('Walls'))",
})
Option B — RhinoCommon directly
Mächtiger, stricter API. Use the cheatsheet at
docs/rhinocommon-cookbook.md in the dev repo for hot-path patterns
(Brep ops, Curve math, Mesh conversion, Transform composition,
Intersection events, …).
When you need a function not in the cookbook:
- Search https://developer.rhino3d.com/api/rhinocommon/?searchtext=
- Note the namespace — most common:
Rhino.Geometry,Rhino.DocObjects,Rhino.Display,Rhino.Render. - Write a tiny test via
execute_python3_codeBEFORE wiring into a long pipeline.
Example (mesh from Brep):
client.send_command("execute_python3_code", {
"code": """
import scriptcontext as sc
import System
from Rhino.Geometry import Mesh, MeshingParameters
brep = sc.doc.Objects.Find(System.Guid.Parse('<id>')).Geometry
meshes = Mesh.CreateFromBrep(brep, MeshingParameters.Default)
print('mesh count:', len(meshes))
""",
})
Negative examples — what NOT to do
- ❌
rs.command("_Move ...")— interactive command, blocks the server. Use the typedmove_object/transforms.moveinstead, orrun_native_commandfor non-interactive scripts. - ❌ Many separate
create_objectcalls whenbatch_operationsworks. TCP latency dominates; batching is 10× faster on Tailscale. - ❌
execute_rhinoscript_python_codefor a single primitive — that's what the typed tools are for. Save scripting for when typed tools can't express the operation. - ❌ Guessing at GH parameter names. Always
inspect_grasshopper_definition(only_player_inputs=True)first. - ❌ Acting on object IDs you didn't just receive. If unsure, call
get_object_infoorfind_nearbyfirst — the user might have deleted, locked, or hidden things since your last call.
Scripts Reference
| Script | Purpose |
|---|---|
rhino_client.py |
Base TCP client; auto-attached auth token + retry |
geometry.py |
Primitives (box, sphere, cylinder, point, line, curve, …) |
transforms.py |
Move, rotate, scale, copy, mirror, arrays |
booleans.py |
Union, difference, intersection |
solids.py |
Solid-specific ops (extrude, fillet edges, shell) |
surfaces.py |
Loft, sweep, revolve, patches |
curves.py |
Offset, fillet, chamfer, join, explode |
selection.py |
Select / list by layer, type, name, IDs |
analysis.py |
Object info, properties, bounding box, volume, distance |
scene.py |
Document-level info, scene summary, find_nearby |
layers.py |
Create / set / list / delete / lock / hide layers |
materials.py |
PBR materials, assignments |
viewport.py |
Views, camera, screenshots |
render.py |
Lights, render settings, render-to-file |
groups.py |
Create / ungroup / list groups |
objects.py |
Generic object ops (delete, rename, recolor, layer change) |
text.py |
Text, text dots, leaders, annotations |
files.py |
Open / save / export / import |
grasshopper.py |
Run / inspect / build / bake GH definitions |
script_exec.py |
Inline Python (rhinoscriptsyntax + RhinoCommon) |
visualarq.py |
VisualARQ BIM objects (walls / doors / windows / IFC) |
presets.py |
Reusable parameter sets |
utils.py |
Common helpers (logging, validation) |
Configuration files
config.json— runtime defaults (host, port, timeout, …).config/presets.yaml— domain presets (door types, wall thicknesses).config/templates.yaml— full pipeline templates.references/commands.md— quick command lookup.
Troubleshooting
| Symptom | First check |
|---|---|
AUTH_REQUIRED |
echo $RHINOCLAW_AUTH_TOKEN matches the value on the Windows side? Both sides restarted after setting the env var? |
Connection refused |
tcpstart already typed in Rhino's command line? Plugin loaded? Try mcpstatus. |
Connection timed out |
RHINOCLAW_HOST correct? On WSL the Windows host IP changes per session. ip route show | awk '/default/ {print $3}' from WSL gives current value. |
AddressAlreadyInUse (port 1999) |
Old Rhino zombie. Get-NetTCPConnection -LocalPort 1999 in PowerShell on the host. |
| Tool returns success but nothing visible | Likely on a hidden layer or off-screen. Run scene_summary + zoom_extents. |
| Long script "hangs" | cancel_rhino_command() is the last resort. Default timeout is 15s; pass timeout=60 for long ones. Server-side max is RHINOCLAW_MAX_TIMEOUT=120. |
Reference
- Plugin source / dev repo: https://github.com/McMuff86/rhinoclaw
- End-user release repo: https://github.com/McMuff86/rhinoclaw-release
- Issues: https://github.com/McMuff86/rhinoclaw/issues
- RhinoCommon API: https://developer.rhino3d.com/api/rhinocommon/
- Rhino.Python guide: https://developer.rhino3d.com/guides/rhinopython/
- rhinoscriptsyntax index: https://developer.rhino3d.com/api/RhinoScriptSyntax/