git.lucas.co / hou-control
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()