Skip to main content
Glama
rhino_bridge.py11.1 kB
""" Rhino/Grasshopper Bridge Module Connects to running Rhino instances via TCP socket to the bridge listener """ import json import socket from typing import Optional, Any from pathlib import Path class RhinoBridge: """Bridge to communicate with Rhino/Grasshopper via TCP listener""" def __init__(self, host: str = "localhost", port: int = 8080): self.host = host self.port = port self.timeout = 30.0 # 30 second timeout for operations def _send_command(self, command: dict) -> dict: """Send a command to the Rhino listener and get response""" try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(self.timeout) sock.connect((self.host, self.port)) # Send command as JSON with double newline terminator message = json.dumps(command) + "\n\n" sock.sendall(message.encode('utf-8')) # Receive response data = b"" while True: chunk = sock.recv(65536) if not chunk: break data += chunk if b"\n\n" in data: break sock.close() # Parse response response_text = data.decode('utf-8').strip() if response_text: return json.loads(response_text) else: return {"success": False, "error": "Empty response from Rhino"} except socket.timeout: return {"success": False, "error": "Connection timed out"} except ConnectionRefusedError: return { "success": False, "error": "Connection refused - Rhino listener not running", "hint": "Start Rhino and run the rhino_bridge_listener.py script" } except Exception as e: return {"success": False, "error": str(e)} def is_rhino_running(self) -> bool: """Check if Rhino is running and listening""" result = self._send_command({"command": "ping"}) return result.get("success", False) def get_connection_status(self) -> dict: """Get current connection status""" result = self._send_command({"command": "status"}) if result.get("success"): return { "host": self.host, "port": self.port, "connected": True, **result } else: return { "host": self.host, "port": self.port, "connected": False, "error": result.get("error", "Unknown error"), "hint": result.get("hint", "") } async def execute_python(self, code: str) -> dict: """ Execute Python code in Rhino's Python environment. Requires Rhino to have the bridge listener running. """ if not code.strip(): return {"success": False, "error": "No code provided"} result = self._send_command({ "command": "execute", "code": code }) return result async def get_grasshopper_state(self) -> dict: """Get current state of Grasshopper canvas""" return self._send_command({"command": "gh_state"}) async def load_gh_definition(self, file_path: str) -> dict: """Load a .gh/.ghx file into Grasshopper""" path = Path(file_path) if not path.exists(): return {"success": False, "error": f"File not found: {file_path}"} if path.suffix.lower() not in [".gh", ".ghx"]: return {"success": False, "error": "File must be .gh or .ghx"} return self._send_command({ "command": "load_gh", "file_path": str(path.resolve()) }) async def solve_definition(self) -> dict: """Trigger a solve of the current Grasshopper definition""" return self._send_command({"command": "solve"}) async def highlight_components(self, guids: list, color: tuple) -> dict: """ Highlight components on the Grasshopper canvas. Args: guids: List of component GUIDs to highlight color: RGB tuple (r, g, b) for highlight color """ return self._send_command({ "command": "highlight_components", "guids": guids, "color": list(color) }) async def clear_highlights(self, guids: list = None) -> dict: """ Clear component highlights. Args: guids: Optional list of GUIDs to clear. If None, clears all. """ return self._send_command({ "command": "clear_highlights", "guids": guids }) # ========================================================================= # Component Creation & Manipulation # ========================================================================= async def add_component( self, name: str, x: float, y: float, nickname: str = None, category: str = None, subcategory: str = None, delay: float = 0 ) -> dict: """ Add a component to the Grasshopper canvas. Args: name: Component name (e.g., "Number Slider", "Addition", "Point") x: X position on canvas y: Y position on canvas nickname: Optional nickname for the component category: Optional category hint for disambiguation subcategory: Optional subcategory hint delay: Delay in seconds after creation for visual feedback (default: 0) Returns: {"success": True, "guid": "...", "name": "..."} or error """ return self._send_command({ "command": "add_component", "name": name, "x": x, "y": y, "nickname": nickname, "category": category, "subcategory": subcategory, "delay": delay }) async def connect_components( self, source_guid: str, source_output: int, target_guid: str, target_input: int ) -> dict: """ Connect two components with a wire. Args: source_guid: GUID of source component source_output: Output parameter index (0-based) target_guid: GUID of target component target_input: Input parameter index (0-based) Returns: {"success": True} or error """ return self._send_command({ "command": "connect_components", "source_guid": source_guid, "source_output": source_output, "target_guid": target_guid, "target_input": target_input }) async def disconnect_components( self, source_guid: str, source_output: int, target_guid: str, target_input: int ) -> dict: """ Disconnect a wire between two components. Args: source_guid: GUID of source component source_output: Output parameter index target_guid: GUID of target component target_input: Input parameter index Returns: {"success": True} or error """ return self._send_command({ "command": "disconnect_components", "source_guid": source_guid, "source_output": source_output, "target_guid": target_guid, "target_input": target_input }) async def delete_component(self, guid: str) -> dict: """ Delete a component from the canvas. Args: guid: GUID of component to delete Returns: {"success": True} or error """ return self._send_command({ "command": "delete_component", "guid": guid }) async def set_component_value( self, guid: str, value: any, param_index: int = 0 ) -> dict: """ Set the value of a component (e.g., Number Slider, Panel, Boolean). Args: guid: GUID of component value: Value to set (number, string, boolean, list) param_index: Parameter index for multi-param components Returns: {"success": True} or error """ return self._send_command({ "command": "set_value", "guid": guid, "value": value, "param_index": param_index }) async def move_component(self, guid: str, x: float, y: float) -> dict: """ Move a component to a new position. Args: guid: GUID of component x: New X position y: New Y position Returns: {"success": True} or error """ return self._send_command({ "command": "move_component", "guid": guid, "x": x, "y": y }) async def create_group( self, guids: list, name: str = None, color: tuple = None ) -> dict: """ Create a group containing the specified components. Args: guids: List of component GUIDs to group name: Optional group name color: Optional RGB color tuple Returns: {"success": True, "group_guid": "..."} or error """ return self._send_command({ "command": "create_group", "guids": guids, "name": name, "color": list(color) if color else None }) async def get_component_info(self, guid: str) -> dict: """ Get detailed info about a component. Args: guid: GUID of component Returns: Component details including inputs, outputs, values """ return self._send_command({ "command": "get_component_info", "guid": guid }) async def new_definition(self) -> dict: """ Create a new empty Grasshopper definition. Returns: {"success": True} or error """ return self._send_command({ "command": "new_definition" }) async def save_definition(self, file_path: str) -> dict: """ Save the current Grasshopper definition to a file. Args: file_path: Path to save the .gh or .ghx file Returns: {"success": True, "path": "..."} or error """ return self._send_command({ "command": "save_definition", "file_path": file_path }) # Singleton instance _bridge: Optional[RhinoBridge] = None def get_bridge(host: str = "localhost", port: int = 8080) -> RhinoBridge: """Get or create the bridge instance""" global _bridge if _bridge is None: _bridge = RhinoBridge(host, port) return _bridge

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/dongwoosuk/grasshopper-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server