git clone https://git.lucas.co/hou-control.git
mcpserver/houdini_mcp.py (3K)
1 #!/usr/bin/env python3
2 import os
3 import rpyc
4 from mcp.server.fastmcp import FastMCP
5
6 HOUDINI_HOST = os.environ.get("HOUDINI_MCP_HOST", "localhost")
7 HOUDINI_PORT = int(os.environ.get("HOUDINI_MCP_PORT", "18811"))
8
9 mcp = FastMCP("houdini")
10
11
12 _REMOTE_RUNNER_SRC = r'''
13 def _hcrun(code):
14 import io, contextlib, traceback, sys
15 buf = io.StringIO()
16 result_repr = None
17 error = None
18 main_ns = sys.modules["__main__"].__dict__
19 try:
20 import hou
21 if "hou" not in main_ns:
22 main_ns["hou"] = hou
23 except ImportError:
24 pass
25 try:
26 with contextlib.redirect_stdout(buf), contextlib.redirect_stderr(buf):
27 try:
28 result = eval(compile(code, "<mcp>", "eval"), main_ns)
29 if result is not None:
30 try:
31 result_repr = repr(result)
32 except Exception as _e:
33 result_repr = "<unrepr-able: " + repr(_e) + ">"
34 except SyntaxError:
35 exec(compile(code, "<mcp>", "exec"), main_ns)
36 except Exception:
37 error = traceback.format_exc()
38 return buf.getvalue(), result_repr, error
39 '''
40
41
42 @mcp.tool()
43 def houdini_eval(code: str) -> str:
44 """Run Python code inside the running Houdini session.
45
46 The `hou` module and anything in __main__ is available. Captures stdout/stderr.
47 If the code is a single expression, its repr is returned too.
48 Requires Houdini to be running with hrpyc.start_server() called (happens
49 automatically in uiready.py for this project).
50 """
51 try:
52 conn = rpyc.classic.connect(HOUDINI_HOST, HOUDINI_PORT)
53 except ConnectionRefusedError:
54 return (
55 f"ERROR: Could not connect to Houdini at {HOUDINI_HOST}:{HOUDINI_PORT}. "
56 "Is Houdini running with hrpyc started?"
57 )
58
59 try:
60 conn.execute(_REMOTE_RUNNER_SRC)
61 stdout, result_repr, error = conn.namespace["_hcrun"](code)
62 stdout = str(stdout) if stdout else ""
63 result_repr = str(result_repr) if result_repr else None
64 error = str(error) if error else None
65 finally:
66 try:
67 conn.close()
68 except Exception:
69 pass
70
71 parts = []
72 if stdout:
73 parts.append(stdout.rstrip())
74 if result_repr is not None:
75 parts.append(f"=> {result_repr}")
76 if error:
77 parts.append(error.rstrip())
78 return "\n".join(parts) if parts else "(no output)"
79
80
81 @mcp.tool()
82 def houdini_ping() -> str:
83 """Check whether the Houdini RPC server is reachable."""
84 try:
85 conn = rpyc.classic.connect(HOUDINI_HOST, HOUDINI_PORT)
86 try:
87 version = conn.modules.hou.applicationVersionString()
88 hip = conn.modules.hou.hipFile.path()
89 return f"OK — Houdini {version}, hip: {hip}"
90 finally:
91 conn.close()
92 except ConnectionRefusedError:
93 return f"Unreachable at {HOUDINI_HOST}:{HOUDINI_PORT}"
94 except Exception as e:
95 return f"Error: {e!r}"
96
97
98 if __name__ == "__main__":
99 mcp.run()