An MCP (Model Context Protocol) server for controlling SideFX Houdini via hrpyc, enabling AI assistants like Claude, Cursor, and Letta agents to interact with Houdini sessions.
+-----------------+ MCP (HTTP) +------------------+ hrpyc:18811 +-------------+
| Claude/Cursor | <------------------> | MCP Server | <------------------> | Houdini |
| Letta Agents | | (Python/FastMCP)| | Session |
+-----------------+ +------------------+ +-------------+
Architecture Decision Records (ADRs) live in docs/adr/:
- ADR 0001: Language and Process Boundaries
— why the server stays Python for
housemantics and the FastMCP gateway, the module boundaries and allowed dependency direction, and the measured thresholds that would justify a future non-Python sidecar.
- 43 MCP tools across 15 modular categories
- Full hou module access - Execute any Houdini Python code remotely
- Scene management - Create, load, save scenes
- Node operations - Create, delete, modify nodes and parameters
- Rendering - Viewport renders, quad views, Karma GPU/CPU support
- Pane screenshots - Capture NetworkEditor, SceneViewer, and other panes
- Scene serialization - Diff scene states before/after operations
- Connection management - Auto-reconnect with exponential backoff + jitter
- Error handling - Consistent error responses with recovery hints
- Response optimization - Size limits, truncation, AI summarization
- In-memory caching - Node type cache with TTL for performance
-
Houdini with RPC enabled - Start Houdini's RPC server:
- In Houdini:
Windows > Python Shell, then run:import hrpyc hrpyc.start_server(port=18811)
- Or add to your
123.pystartup script for automatic startup
- In Houdini:
-
Network access - The MCP server must be able to reach Houdini's RPC port (default: 18811)
For Codex Desktop on Windows, see the dedicated setup notes in
docs/codex-windows-setup.md.
The Houdini plugin runs the MCP server directly inside Houdini, using stdio transport. This is the simplest setup with no network configuration required.
Installation:
-
Copy the
houdini_pluginfolder to your Houdini packages directory:# Windows copy houdini_plugin %USERPROFILE%\Documents\houdini20.5\packages\houdini_mcp # Linux/Mac cp -r houdini_plugin ~/houdini20.5/packages/houdini_mcp
-
Copy the package JSON:
# Windows copy houdini_plugin\houdini_mcp.json %USERPROFILE%\Documents\houdini20.5\packages\ # Linux/Mac cp houdini_plugin/houdini_mcp.json ~/houdini20.5/packages/
-
Install FastMCP in Houdini's Python:
# Windows (from Houdini's Python) hython -m pip install fastmcp # Or from Houdini's Python Shell import subprocess subprocess.run(["pip", "install", "fastmcp"])
-
Restart Houdini and find the "Houdini MCP" shelf
Usage:
- Click "Start MCP" on the shelf to start the server
- Configure your MCP client (Claude Desktop, Cursor, etc.) to use stdio transport
- Click "Stop MCP" to stop the server
MCP Client Configuration (stdio mode):
{
"mcpServers": {
"houdini": {
"command": "hython",
"args": ["-c", "from houdini_mcp_plugin import start_server; start_server(use_thread=False)"]
}
}
}For production use or when Houdini runs on a different machine, use the Docker-based remote mode. This connects to Houdini via hrpyc/RPyC.
Step 1: Start hrpyc in Houdini
If you have the Houdini MCP plugin installed:
- Click "Start Remote" on the Houdini MCP shelf
- Use "Remote Status" / "Remote Self-Test" to confirm the actual bound endpoint, reachability, and firewall guidance
- By default the listener binds to
127.0.0.1(loopback-only). To let a remote client (e.g. the Docker gateway on another host) connect, setHOUDINI_RPC_BIND_HOSTand opt in withHOUDINI_RPC_TRUSTED_NETWORK=1orHOUDINI_RPC_TOKEN. See docs/remote-listener.md.
Or manually in Houdini's Python Shell (unchanged, still supported):
import hrpyc
hrpyc.start_server(port=18811)Step 2: Run the Docker MCP server
# Clone the repository
git clone https://github.com/oculairmedia/houdini-mcp.git
cd houdini-mcp
# Copy and configure environment
cp .env.example .env
# Edit .env with your Houdini host IP (from Step 1)
# Run with Docker Compose
docker compose up -dStep 3: Configure your MCP client
{
"mcpServers": {
"houdini": {
"url": "http://localhost:3055"
}
}
}Benefits of Remote Mode:
- Houdini can run on a different machine (e.g., render farm)
- MCP server runs in Docker for easy deployment
- Full tool set with advanced features
- Server-side processing capabilities
# Clone and install
git clone https://github.com/oculairmedia/houdini-mcp.git
cd houdini-mcp
pip install -r requirements.txt
# Run
HOUDINI_HOST=192.168.50.90 python -m houdini_mcpEnvironment variables:
| Variable | Default | Description |
|---|---|---|
HOUDINI_HOST |
localhost |
Houdini machine IP/hostname |
HOUDINI_PORT |
18811 |
hrpyc server port |
MCP_PORT |
3055 |
MCP server HTTP port |
MCP_TRANSPORT |
http |
Transport type (http, stdio, sse) |
LOG_LEVEL |
INFO |
Logging level |
The server is organized into 15 modular tool categories in houdini_mcp/tools/:
| Tool | Description |
|---|---|
get_scene_info |
Get current scene info (file, version, nodes) |
serialize_scene |
Serialize scene structure for diffs |
new_scene |
Create empty scene |
save_scene |
Save current scene |
load_scene |
Load a .hip file |
create_node |
Create a new node |
delete_node |
Delete a node by path |
get_node_info |
Get node details, parameters, connections, errors |
list_children |
List child nodes with connection details |
find_nodes |
Find nodes by name pattern or type |
list_node_types |
List available node types by category |
| Tool | Description |
|---|---|
connect_nodes |
Wire nodes together |
disconnect_node_input |
Break a connection |
reorder_inputs |
Reorder node inputs |
set_node_flags |
Set display/render/bypass flags |
layout_children |
Auto-layout child nodes |
set_node_position |
Set node position in network |
set_node_color |
Set node color |
create_network_box |
Create network box around nodes |
| Tool | Description |
|---|---|
set_parameter |
Set a parameter value |
get_parameter_schema |
Get parameter metadata (types, ranges, menus) |
| Tool | Description |
|---|---|
get_geo_summary |
Get geometry statistics and metadata |
create_material |
Create a new material |
assign_material |
Assign material to geometry |
get_material_info |
Get material parameters and shaders |
| Tool | Description |
|---|---|
render_viewport |
Render viewport with camera control |
render_quad_view |
Render Front/Left/Top/Perspective views |
list_render_nodes |
List all ROPs in /out |
get_render_settings |
Get ROP configuration |
set_render_settings |
Modify ROP settings |
create_render_node |
Create new ROP with settings |
| Tool | Description |
|---|---|
capture_pane_screenshot |
Capture any Houdini pane as PNG |
list_visible_panes |
List capturable panes |
capture_multiple_panes |
Batch capture multiple panes |
render_node_network |
Navigate to node and capture network |
| Tool | Description |
|---|---|
execute_code |
Execute Python with hou available |
execute_hscript |
Execute HScript commands |
| Tool | Description |
|---|---|
find_error_nodes |
Find all nodes with cook errors |
| Tool | Description |
|---|---|
get_houdini_help |
Get help for node types |
summarize_response |
AI-summarize large responses |
estimate_tokens |
Estimate token count |
get_summarization_status |
Get summarization config |
- Error handling:
@handle_connection_errorsdecorator - Connection retry: Exponential backoff with jitter
- Response size: Thresholds, truncation, metadata
- Caching: Node type cache with TTL
- Parallel execution:
semaphore_gather,batch_items
Build a complete SOP network from scratch:
# 1. Create geo container
geo = create_node("geo", "/obj", "my_geo")
# 2. Create SOP nodes
sphere = create_node("sphere", geo["node_path"], "sphere1")
xform = create_node("xform", geo["node_path"], "xform1")
color = create_node("color", geo["node_path"], "color1")
out = create_node("null", geo["node_path"], "OUT")
# 3. Wire nodes together
connect_nodes(sphere["node_path"], xform["node_path"])
connect_nodes(xform["node_path"], color["node_path"])
connect_nodes(color["node_path"], out["node_path"])
# 4. Set display flag
set_node_flags(out["node_path"], display=True, render=True)Insert a new node between existing connections:
# 1. Discover existing network
children = list_children("/obj/geo1")
# Find grid and noise nodes from children
# 2. Get current connections
noise_info = get_node_info("/obj/geo1/noise1", include_input_details=True)
# See that noise is connected to grid
# 3. Create new node
mountain = create_node("mountain", "/obj/geo1", "mountain1")
# 4. Rewire: grid → mountain → noise
disconnect_node_input("/obj/geo1/noise1", 0) # Break noise ← grid
connect_nodes("/obj/geo1/grid1", "/obj/geo1/mountain1") # grid → mountain
connect_nodes("/obj/geo1/mountain1", "/obj/geo1/noise1") # mountain → noiseUse parameter schema to set values correctly:
# 1. Discover parameter metadata
schema = get_parameter_schema("/obj/geo1/sphere1", parm_name="rad")
param = schema["parameters"][0]
# 2. Check parameter type
if param["type"] == "vector":
# Set vector parameter correctly
set_parameter("/obj/geo1/sphere1", "rad", [3.0, 3.0, 3.0])
elif param["type"] == "menu":
# Use menu items
first_option = param["menu_items"][0]["value"]
set_parameter("/obj/geo1/sphere1", "type", first_option)Always verify geometry after operations:
# Get comprehensive geometry summary
summary = get_geo_summary(
"/obj/geo1/OUT",
max_sample_points=10,
include_attributes=True
)
# Check cook state
if summary["cook_state"] != "cooked":
# Handle errors
node_info = get_node_info("/obj/geo1/OUT", include_errors=True)
errors = node_info["cook_info"]["errors"]
# Fix errors...
# Verify geometry metrics
assert summary["point_count"] > 0
assert summary["primitive_count"] > 0
# Check bounding box
bbox = summary["bounding_box"]
# Verify expected size/position# 1. Check cook state first
node_info = get_node_info(
node_path,
include_errors=True,
force_cook=True
)
cook_state = node_info["cook_info"]["cook_state"]
# 2. Handle different states
if cook_state == "error":
# Examine errors
errors = node_info["cook_info"]["errors"]
for err in errors:
print(f"Error: {err['message']}")
# Fix errors...
elif cook_state == "cooked":
# Safe to access geometry
geo = get_geo_summary(node_path)# Always check parameter schema before setting
schema = get_parameter_schema(node_path, parm_name="rad")
param = schema["parameters"][0]
if param["type"] == "vector":
# Use list/tuple for vector parameters
set_parameter(node_path, "rad", [5.0, 5.0, 5.0])
else:
# Use scalar for single parameters
set_parameter(node_path, "rad", 5.0)# Connection validation
result = connect_nodes(src_path, dst_path)
if result["status"] == "error":
if "incompatible" in result["message"].lower():
# Different node categories (e.g., SOP vs OBJ)
print("Can't connect nodes of different types")
elif "not found" in result["message"].lower():
# Node doesn't exist
print("Source or destination node not found")# Use include_errors=True to diagnose issues
node_info = get_node_info(
node_path,
include_errors=True,
force_cook=True
)
cook_info = node_info["cook_info"]
# Check for errors
if cook_info["errors"]:
print(f"Node has {len(cook_info['errors'])} errors:")
for error in cook_info["errors"]:
print(f" - {error['message']}")
# Check for warnings
if cook_info["warnings"]:
print(f"Node has {len(cook_info['warnings'])} warnings:")
for warning in cook_info["warnings"]:
print(f" - {warning['message']}")Complete working examples are available in the examples/ directory:
build_from_scratch.py- Build sphere → xform → color → OUT from scratchaugment_existing_scene.py- Insert mountain between grid → noiseparameter_workflow.py- Discover → set → verify parameterserror_handling.py- Detect → fix → verify errors
Run examples:
cd examples
python build_from_scratch.py
python augment_existing_scene.py
python parameter_workflow.py
python error_handling.pyAdd to your MCP client configuration:
{
"mcpServers": {
"houdini": {
"url": "http://localhost:3055"
}
}
}Add as an MCP server in Letta's configuration to give agents Houdini control.
- "Create a sphere → transform → color → OUT network"
- "Insert a mountain node between the grid and noise"
- "Discover what parameters are available on the sphere node"
- "Check if the noise node has any cook errors"
- "Set the sphere radius to 3.0 using the parameter schema"
# Install dev dependencies
pip install -r requirements.txt pytest
# Run tests
pytest tests/
# Run server locally
python -m houdini_mcp- Verify Houdini is running with hrpyc server started
- Check firewall allows port 18811
- Verify HOUDINI_HOST is correct
- hrpyc uses no authentication by default
- Ensure you're on a trusted network
- The plugin refuses to bind a non-loopback address unless you explicitly opt
in (
HOUDINI_RPC_TRUSTED_NETWORK=1) or set a shared secret (HOUDINI_RPC_TOKEN). See docs/remote-listener.md for the full security model and its limitations.
houdini_mcp/
├── server.py # FastMCP server with 43 tool wrappers
├── connection.py # RPyC connection with retry/backoff
└── tools/ # Modular tool implementations
├── _common.py # Shared utilities, error handling
├── cache.py # Node type caching with TTL
├── code.py # Python/HScript execution
├── errors.py # Error node detection
├── geometry.py # Geometry introspection
├── help.py # Houdini help access
├── hscript.py # HScript command execution
├── layout.py # Node layout tools
├── materials.py # Material creation/assignment
├── nodes.py # Node CRUD operations
├── pane_screenshot.py # Pane capture tools
├── parameters.py # Parameter get/set
├── rendering.py # Viewport/Karma rendering
├── scene.py # Scene management
├── summarization.py # AI response summarization
└── wiring.py # Node connection tools
houdini_plugin/ # Houdini plugin for stdio mode
├── python/houdini_mcp_plugin/
├── toolbar/ # Shelf tools
└── houdini_mcp.json # Package descriptor
tests/ # 418 tests (406 passing)
docs/ # Implementation documentation
examples/ # Working example scripts
- Based on hrpyc integration patterns from OpenWebUI Houdini Pipeline
- Inspired by capoomgit/houdini-mcp
MIT