# Directory Structure ``` ├── .DS_Store ├── .env ├── .env.example ├── .gitignore ├── .python-version ├── examples │ ├── compute_line.py │ ├── rhinopython.py │ ├── test_CL_GH.py │ ├── test_CodeListener.py │ ├── test_GH.py │ ├── test_rhino3dm.3dm │ ├── test_rhino3dm.3dmbak │ ├── test_rhino3dm.py │ ├── test01.gh │ ├── test02.gh │ ├── test03.gh │ ├── zaha01.png │ └── zaha01.py ├── grasshopper_mcp │ ├── __init__.py │ ├── .DS_Store │ ├── config.py │ ├── prompts │ │ ├── __init__.py │ │ ├── grasshopper_prompts.py │ │ └── templates.py │ ├── resources │ │ ├── __init__.py │ │ └── model_data.py │ ├── rhino │ │ ├── __init__.py │ │ └── connection.py │ ├── server.py │ ├── tools │ │ ├── __init__.py │ │ ├── advanced_grasshopper.py │ │ ├── analysis.py │ │ ├── grasshopper.py │ │ ├── modeling.py │ │ └── rhino_code_gen.py │ └── utils │ ├── __init__.py │ └── request.py ├── LICENSE ├── main.py ├── pyproject.toml ├── README.md ├── run_server.py ├── scripts │ └── install.py └── uv.lock ``` # Files -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- ``` 3.13 ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` # Python-generated files __pycache__/ *.py[oc] build/ dist/ wheels/ *.egg-info # Virtual environments .venv ``` -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- ``` # Rhino/Grasshopper configuration RHINO_PATH=C:/Program Files/Rhino 7/System COMPUTE_API_KEY=your_compute_key_here COMPUTE_URL=https://compute.rhino3d.com # MCP server configuration SERVER_NAME=Grasshopper MCP SERVER_PORT=8080 ``` -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- ``` # Rhino/Grasshopper configuration USE_RHINO3DM=true # using Rhino3dm (for mac), switch to true RHINO_PATH=C:/Program Files/Rhino 7/System USE_COMPUTE_API=false COMPUTE_API_KEY=your_compute_key_here COMPUTE_URL=https://compute.rhino3d.com # MCP server configuration SERVER_NAME=Grasshopper_MCP SERVER_PORT=8080 ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown # GH_mcp_server GH_mcp_server provides an approach that allows designer to interact with Rhino and Grasshopper directly via LLMs, including to analyse .3dm file, do 3D modeling and generate GHPython automatically in Grasshopper based on user’s guidance. > This project is **still under construction** — and we’d love your help! > > - Feel free to **open an issue** if you encounter bugs or have ideas. > - Pull requests are always welcome. > - If you're interested in collaborating long-term, feel free to reach out to [email protected] — we’d love to **have you on the team**!  ## Requirements - Rhino 7 or 8 - Install `RhinoPython`: https://github.com/jingcheng-chen/RhinoPythonForVscode/tree/master?tab=readme-ov-file - `uv` - ``` # For MacOS and Linux curl -LsSf https://astral.sh/uv/install.sh | sh ``` - `````` # For Windows powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" `````` - Claude Desktop ## Installation ### 1. Clone the repository ``` git clone [email protected]:veoery/GH_mcp_server.git cd GH_mcp_server ``` ------ ### 2. Set up the environment We recommend using `uv`: #### macOS/Linux ``` uv venv source .venv/bin/activate uv pip install -e . ``` #### Windows ``` uv venv .venv\Scripts\activate uv pip install -e . ``` > Make sure the virtual environment is activated before running or developing the project. ### 3. Configuration 1. In the Claude Desktop, Navigate to Settings->Developer. You will see ```Edit Config```. 2. Click the ```Edit Config``` and open the file ```claude_desktop_config.json``` 3. Place the following code to the json file: ```python { "mcpServers": { "grasshopper": { "command": "path_to_GH_mcp_server/.venv/bin/python", "args": [ "path_to_GH_mcp_server/run_server.py" ] } } } ``` 4. Restart the Claude Desktop. If you are able to see a hammer icon, the configuration is successful. Click the hammer icon to check all the attached MCP tools. ## Usage 1. Start Rhino 2. Type command `CodeListener`. You should see `VS Code Listener Started...`. 3. Open the Claude Desktop and type the prompts to interact with GH_mcp_server tools. Please also check the file `examples\zaha01.gh` as a reference for interacting with Grasshopper. Here are some examples: ``` Read the file "D:\test01.3dm" first and analyse the objects in this file. ``` ``` write GHpython to create a tower referring to zaha and write the ghpython code to "D:\zaha01.py" ``` ``` -------------------------------------------------------------------------------- /grasshopper_mcp/__init__.py: -------------------------------------------------------------------------------- ```python ``` -------------------------------------------------------------------------------- /grasshopper_mcp/prompts/__init__.py: -------------------------------------------------------------------------------- ```python ``` -------------------------------------------------------------------------------- /grasshopper_mcp/resources/__init__.py: -------------------------------------------------------------------------------- ```python ``` -------------------------------------------------------------------------------- /grasshopper_mcp/rhino/__init__.py: -------------------------------------------------------------------------------- ```python ``` -------------------------------------------------------------------------------- /grasshopper_mcp/tools/__init__.py: -------------------------------------------------------------------------------- ```python ``` -------------------------------------------------------------------------------- /grasshopper_mcp/utils/__init__.py: -------------------------------------------------------------------------------- ```python ``` -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- ```python def main(): print("Hello from gh-mcp-server!") if __name__ == "__main__": main() ``` -------------------------------------------------------------------------------- /run_server.py: -------------------------------------------------------------------------------- ```python #!/usr/bin/env python import sys import os # Set up the Python path to find the package sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) # Import and run the server module from grasshopper_mcp.server import main if __name__ == "__main__": main() ``` -------------------------------------------------------------------------------- /examples/test_CL_GH.py: -------------------------------------------------------------------------------- ```python import Rhino import rhinoscriptsyntax as rs import scriptcontext as sc import System from Rhino.Geometry import * def main(): center = Point3d(10, 50, 0) circle = Circle(Plane.WorldXY, center, 20) return circle if __name__ == "__main__": circle = main() ``` -------------------------------------------------------------------------------- /examples/rhinopython.py: -------------------------------------------------------------------------------- ```python import rhinoscriptsyntax as rs import ghpythonlib.components as ghcomp import scriptcontext #points = rs.GetPoints(True, True) #if points: # curves = ghcomp.Voronoi(points) # for curve in curves: # scriptcontext.doc.Objects.AddCurve(curve) # for point in points: # scriptcontext.doc.Objects.AddPoint(point) # scriptcontext.doc.Views.Redraw() p=ghcomp.ConstructPoint(0,0,0) scriptcontext.doc.Objects.AddPoint(p) #scriptcontext.doc.Views.Redraw() ``` -------------------------------------------------------------------------------- /examples/test_GH.py: -------------------------------------------------------------------------------- ```python # Grasshopper Python Component: CreateCircle # Generated from prompt: Create a circle with center point at coordinates (10,20,30) and radius of 50 import Rhino import rhinoscriptsyntax as rs import scriptcontext as sc import Rhino.Geometry as rg import ghpythonlib.components as ghcomp import math # Create a circle based on prompt: Create a circle with center point at coordinates (10,20,30) and radius of 50 center = rg.Point3d(10, 20, 30) circle = rg.Circle(rg.Plane.WorldXY, center, 50) print("Created a circle!") ``` -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- ```toml [build-system] requires = ["setuptools>=42", "wheel"] build-backend = "setuptools.build_meta" [project] name = "grasshopper-mcp" version = "0.1.0" description = "MCP server for Grasshopper 3D modeling" readme = "README.md" requires-python = ">=3.10" license = {text = "MIT"} dependencies = [ "mcp>=1.4.0", "rhinoinside; platform_system=='Windows'", # For Windows "rhino3dm>=7.15.0", "requests", "python-dotenv", ] [project.optional-dependencies] dev = [ "pytest", "black", "isort", ] [project.scripts] grasshopper-mcp = "grasshopper_mcp.server:main" ``` -------------------------------------------------------------------------------- /grasshopper_mcp/prompts/templates.py: -------------------------------------------------------------------------------- ```python from mcp.server.fastmcp import FastMCP import mcp.types as types def register_prompts(mcp: FastMCP) -> None: """Register prompt templates with the MCP server.""" @mcp.prompt() def create_parametric_model(component_description: str) -> str: """Create a parametric model based on a description.""" return f""" Please help me create a parametric 3D model based on this description: {component_description} First, analyze what I want to build. Then generate the Python code to create this model using Rhino's geometry classes. Focus on creating a parametric design where key dimensions can be changed easily. """ ``` -------------------------------------------------------------------------------- /examples/test_rhino3dm.py: -------------------------------------------------------------------------------- ```python import rhino3dm model = rhino3dm.File3dm() # create geometry sphere1 = rhino3dm.Sphere(rhino3dm.Point3d(0, 0, 0), 10) sphere2 = rhino3dm.Sphere(rhino3dm.Point3d(10, 10, 10), 4) geometry = (sphere1.ToBrep(), sphere2.ToBrep()) # create attributes attr1 = rhino3dm.ObjectAttributes() attr1.Name = "Sphere 1" attr2 = rhino3dm.ObjectAttributes() attr2.Name = "Sphere 2" attributes = (attr1, attr2) basepoint = rhino3dm.Point3d(0, 0, 0) # create idef index = model.InstanceDefinitions.Add("name", "description", "url", "urltag", basepoint, geometry, attributes) print("Index of new idef: " + str(index)) # create iref idef = model.InstanceDefinitions.FindIndex(index) xf = rhino3dm.Transform(10.00) iref = rhino3dm.InstanceReference(idef.Id, xf) uuid = model.Objects.Add(iref, None) print("id of new iref: " + str(uuid)) # save file model.Write("./examples/test_rhino3dm.3dm", 7) ``` -------------------------------------------------------------------------------- /grasshopper_mcp/tools/rhino_code_gen.py: -------------------------------------------------------------------------------- ```python from mcp.server.fastmcp import FastMCP from typing import Dict, Optional, Any def register_rhino_code_generation_tools(mcp: FastMCP) -> None: """Register code generation tools with the MCP server.""" @mcp.tool() async def generate_rhino_code(prompt: str, parameters: Optional[Dict[str, Any]] = None) -> str: """Generate and execute Rhino Python code based on a description. Args: prompt: Description of what you want the code to do parameters: Optional parameters to use in the code generation Returns: Result of the operation """ ctx = mcp.get_context() rhino = ctx.request_context.lifespan_context.rhino result = await rhino.generate_and_execute_rhino_code(prompt, parameters) if result["result"] == "error": return f"Error generating or executing code: {result['error']}" return f"""Generated and executed code successfully: {result['code']}""" ``` -------------------------------------------------------------------------------- /examples/compute_line.py: -------------------------------------------------------------------------------- ```python import compute_rhino3d.Util import compute_rhino3d.Grasshopper as gh import rhino3dm import json compute_rhino3d.Util.url = "http://localhost:8000/" # compute_rhino3d.Util.apiKey = "" pt1 = rhino3dm.Point3d(0, 0, 0) circle = rhino3dm.Circle(pt1, 5) angle = 20 # convert circle to curve and stringify curve = json.dumps(circle.ToNurbsCurve().Encode()) # create list of input trees curve_tree = gh.DataTree("curve") curve_tree.Append([0], [curve]) rotate_tree = gh.DataTree("rotate") rotate_tree.Append([0], [angle]) trees = [curve_tree, rotate_tree] output = gh.EvaluateDefinition("twisty.gh", trees) print(output) # decode results branch = output["values"][0]["InnerTree"]["{0;0}"] lines = [rhino3dm.CommonObject.Decode(json.loads(item["data"])) for item in branch] filename = "twisty.3dm" print("Writing {} lines to {}".format(len(lines), filename)) # create a 3dm file with results model = rhino3dm.File3dm() for l in lines: model.Objects.AddCurve(l) # they're actually LineCurves... model.Write(filename) ``` -------------------------------------------------------------------------------- /grasshopper_mcp/config.py: -------------------------------------------------------------------------------- ```python import os from dataclasses import dataclass from typing import Optional @dataclass class ServerConfig: """Server configuration.""" # Rhino/Grasshopper configuration rhino_path: Optional[str] = None # Path to Rhino installation use_compute_api: bool = False # Whether to use compute.rhino3d.com use_rhino3dm: bool = False # Whether to use rhino3dm library compute_url: Optional[str] = None # Compute API URL compute_api_key: Optional[str] = None # Compute API key # Server configuration server_name: str = "Grasshopper MCP" server_port: int = 8080 @classmethod def from_env(cls) -> "ServerConfig": """Create configuration from environment variables.""" use_compute = os.getenv("USE_COMPUTE_API", "false").lower() == "true" use_rhino3dm = os.getenv("USE_RHINO3DM", "true").lower() == "true" return cls( rhino_path=os.getenv("RHINO_PATH"), use_compute_api=use_compute, use_rhino3dm=use_rhino3dm, compute_url=os.getenv("COMPUTE_URL"), compute_api_key=os.getenv("COMPUTE_API_KEY"), server_name=os.getenv("SERVER_NAME", "Grasshopper MCP"), server_port=int(os.getenv("SERVER_PORT", "8080")), ) ``` -------------------------------------------------------------------------------- /grasshopper_mcp/prompts/grasshopper_prompts.py: -------------------------------------------------------------------------------- ```python from mcp.server.fastmcp import FastMCP def register_grasshopper_code_prompts(mcp: FastMCP) -> None: """Register prompt templates with the MCP server.""" @mcp.prompt() def grasshopper_GHpython_generation_prompt(task_description: str) -> str: """Creates a prompt template for generating Grasshopper Python code with proper imports and grammar.""" return """ When writing Python code for Grasshopper, please follow these guidelines: 0. Add "Used the prompts from mcp.prompt()" at the beginning of python file. 1. Always start by including the following import statements: ```python import Rhino.Geometry as rg import ghpythonlib.components as ghcomp import rhinoscriptsyntax as rs 2. Structure your code with the following sections: Import statements at the top Global variables and constants Function definitions if needed Main execution code 3. Use descriptive variable names that follow Python naming conventions Use snake_case for variables and functions Use UPPER_CASE for constants 4. Include comments explaining complex logic or non-obvious operations 5. Carefully check grammar in all comments and docstrings 6. Ensure proper indentation and consistent code style 7. Use proper error handling when appropriate 8. Optimize for Grasshopper's data tree structure when handling multiple data items 9. Save the output to "result". """ ``` -------------------------------------------------------------------------------- /grasshopper_mcp/utils/request.py: -------------------------------------------------------------------------------- ```python import socket import json import tempfile import os def test_codelistener_with_file(host="127.0.0.1", port=614): """Test CodeListener by creating a temporary file and sending its path.""" try: # Create a temporary Python file fd, temp_path = tempfile.mkstemp(suffix=".py") try: # Write Python code to the file with os.fdopen(fd, "w") as f: f.write( """ import Rhino import scriptcontext as sc # Get Rhino version version = Rhino.RhinoApp.Version print("Hello from CodeListener!") print("Rhino version: ", version) # Access the active document doc = sc.doc if doc is not None: print("Active document: ", doc.Name) else: print("No active document") """ ) # Create JSON message object msg_obj = {"filename": temp_path, "run": True, "reset": False, "temp": True} # Convert to JSON json_msg = json.dumps(msg_obj) # Connect to CodeListener sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(10) sock.connect((host, port)) # Send the JSON message print(f"Sending request to execute file: {temp_path}") sock.sendall(json_msg.encode("utf-8")) # Receive response print("Waiting for response...") response = sock.recv(4096).decode("utf-8") print(f"Response received: {response}") sock.close() return True finally: # Clean up - remove temporary file try: os.unlink(temp_path) except: pass except Exception as e: print(f"Error: {e}") return False # Run the test if __name__ == "__main__": test_codelistener_with_file() ``` -------------------------------------------------------------------------------- /grasshopper_mcp/server.py: -------------------------------------------------------------------------------- ```python from contextlib import asynccontextmanager from dataclasses import dataclass from typing import AsyncIterator import os from mcp.server.fastmcp import Context, FastMCP from dotenv import load_dotenv from grasshopper_mcp.rhino.connection import RhinoConnection from grasshopper_mcp.config import ServerConfig import sys print("Rhino MCP Server starting up...", file=sys.stderr) load_dotenv() # Load environment variables from .env file @dataclass class AppContext: """Application context with initialized connections.""" rhino: RhinoConnection config: ServerConfig @asynccontextmanager async def app_lifespan(server: FastMCP) -> AsyncIterator[AppContext]: """Initialize and manage server resources.""" # Load configuration config = ServerConfig.from_env() # Initialize Rhino/Grasshopper connection rhino_connection = RhinoConnection(config) try: await rhino_connection.initialize() # Provide context to request handlers yield AppContext(rhino=rhino_connection, config=config) finally: # Cleanup on shutdown await rhino_connection.close() # Create the MCP server mcp = FastMCP("Grasshopper 3D Modeling", lifespan=app_lifespan) # Import tool definitions from grasshopper_mcp.tools.modeling import register_modeling_tools from grasshopper_mcp.tools.analysis import register_analysis_tools from grasshopper_mcp.resources.model_data import register_model_resources from grasshopper_mcp.tools.grasshopper import register_grasshopper_tools from grasshopper_mcp.tools.advanced_grasshopper import register_advanced_grasshopper_tools from grasshopper_mcp.tools.rhino_code_gen import register_rhino_code_generation_tools # Import prompt definitions from grasshopper_mcp.prompts.grasshopper_prompts import register_grasshopper_code_prompts # Register tools register_modeling_tools(mcp) register_analysis_tools(mcp) register_model_resources(mcp) register_grasshopper_tools(mcp) # register_advanced_grasshopper_tools(mcp) # register_rhino_code_generation_tools(mcp) # Register prompts register_grasshopper_code_prompts(mcp) def main(): """Run the server.""" mcp.run() if __name__ == "__main__": main() ``` -------------------------------------------------------------------------------- /scripts/install.py: -------------------------------------------------------------------------------- ```python #!/usr/bin/env python3 """ Helper script to install the Grasshopper MCP server in Claude Desktop. """ import os import json import platform import argparse import sys from pathlib import Path def get_config_path(): """Get the path to the Claude Desktop config file.""" if platform.system() == "Darwin": # macOS return os.path.expanduser("~/Library/Application Support/Claude/claude_desktop_config.json") elif platform.system() == "Windows": return os.path.join(os.environ.get("APPDATA", ""), "Claude", "claude_desktop_config.json") else: print("Unsupported platform. Only macOS and Windows are supported.") sys.exit(1) def main(): parser = argparse.ArgumentParser(description="Install Grasshopper MCP server in Claude Desktop") parser.add_argument("--name", default="grasshopper", help="Name for the server in Claude Desktop") args = parser.parse_args() # Get the path to this script's directory script_dir = Path(__file__).parent.absolute() project_dir = script_dir.parent # Get the path to the server script server_script = project_dir / "grasshopper_mcp" / "server.py" if not server_script.exists(): print(f"Server script not found at {server_script}") sys.exit(1) config_path = get_config_path() config_dir = os.path.dirname(config_path) # Create config directory if it doesn't exist os.makedirs(config_dir, exist_ok=True) # Load existing config or create new one if os.path.exists(config_path): with open(config_path, "r") as f: try: config = json.load(f) except json.JSONDecodeError: config = {} else: config = {} # Ensure mcpServers exists if "mcpServers" not in config: config["mcpServers"] = {} # Add our server python_path = sys.executable config["mcpServers"][args.name] = {"command": python_path, "args": [str(server_script)]} # Write updated config with open(config_path, "w") as f: json.dump(config, f, indent=2) print(f"Grasshopper MCP server installed as '{args.name}' in Claude Desktop") print(f"Configuration written to: {config_path}") if __name__ == "__main__": main() ``` -------------------------------------------------------------------------------- /examples/test_CodeListener.py: -------------------------------------------------------------------------------- ```python # import os # import sys # print(1) # scriptcontext_path = os.path.join( # os.environ["APPDATA"], # "McNeel", # "Rhinoceros", # "7.0", # "Plug-ins", # "IronPython (814d908a-e25c-493d-97e9-ee3861957f49)", # "settings", # "lib", # ) # sys.path.append(scriptcontext_path) # # import rhinoinside # # rhinoinside.load() # import rhinoscriptsyntax as rs # import scriptcontext as sc # import Rhino ################################################################################ # SampleAddNurbsCurves.py # Copyright (c) 2017 Robert McNeel & Associates. # See License.md in the root of this repository for details. ################################################################################ import Rhino import scriptcontext def SampleAddNurbsCurves(): # World 3-D, or Euclidean, locations pt0 = Rhino.Geometry.Point3d(-8.0, -3.0, 0.0) pt1 = Rhino.Geometry.Point3d(-4.0, 3.0, 2.0) pt2 = Rhino.Geometry.Point3d(4.0, 3.0, 2.0) pt3 = Rhino.Geometry.Point3d(8.0, -3.0, 0.0) # Create NURBS curve: # Dimension = 3 # Rational = False # Order (Degree + 1) = 4 # Control point count = 4 # Knot count = Control point count + degree - 1 nc0 = Rhino.Geometry.NurbsCurve(3, False, 4, 4) # World 3-D, or Euclidean, control points, nc0.Points[0] = Rhino.Geometry.ControlPoint(pt0) nc0.Points[1] = Rhino.Geometry.ControlPoint(pt1) nc0.Points[2] = Rhino.Geometry.ControlPoint(pt2) nc0.Points[3] = Rhino.Geometry.ControlPoint(pt3) # Clamped knots nc0.Knots[0] = 0 nc0.Knots[1] = 0 nc0.Knots[2] = 0 nc0.Knots[3] = 1 nc0.Knots[4] = 1 nc0.Knots[5] = 1 # Create NURBS curve: # Dimension = 3 # Rational = True # Order (Degree + 1) = 4 # Control point count = 4 # Knot count = Control point count + degree - 1 nc1 = Rhino.Geometry.NurbsCurve(3, True, 4, 4) # Control points from a world 3-D, or Euclidean, locations and a weight nc1.Points[0] = Rhino.Geometry.ControlPoint(pt0, 1.0) nc1.Points[1] = Rhino.Geometry.ControlPoint(pt1, 2.0) nc1.Points[2] = Rhino.Geometry.ControlPoint(pt2, 4.0) nc1.Points[3] = Rhino.Geometry.ControlPoint(pt3, 1.0) # Clamped knots nc1.Knots[0] = 0 nc1.Knots[1] = 0 nc1.Knots[2] = 0 nc1.Knots[3] = 1 nc1.Knots[4] = 1 nc1.Knots[5] = 1 # Create NURBS curve: # Dimension = 3 # Rational = True # Order (Degree + 1) = 4 # Control point count = 4 # Knot count = Control point count + degree - 1 nc2 = Rhino.Geometry.NurbsCurve(3, True, 4, 4) # Homogeneous control points nc2.Points[0] = Rhino.Geometry.ControlPoint(-8.0, -3.0, 0.0, 1.0) nc2.Points[1] = Rhino.Geometry.ControlPoint(-4.0, 3.0, 2.0, 2.0) nc2.Points[2] = Rhino.Geometry.ControlPoint(4.0, 3.0, 2.0, 4.0) nc2.Points[3] = Rhino.Geometry.ControlPoint(8.0, -3.0, 0.0, 1.0) # Clamped knots nc2.Knots[0] = 0 nc2.Knots[1] = 0 nc2.Knots[2] = 0 nc2.Knots[3] = 1 nc2.Knots[4] = 1 nc2.Knots[5] = 1 # Add to document scriptcontext.doc.Objects.Add(nc0) scriptcontext.doc.Objects.Add(nc1) scriptcontext.doc.Objects.Add(nc2) scriptcontext.doc.Views.Redraw() # Check to see if this file is being executed as the "main" python # script instead of being used as a module by some other python script # This allows us to use the module which ever way we want. if __name__ == "__main__": SampleAddNurbsCurves() # rhino_path = "C:\\Program Files\\Rhino 7\\System" # sys.path.append(rhino_path) # # print(sys.path) # # import scriptcontext as sc # # import rhinoscriptsyntax as rs # import rhinoinside # rhinoinside.load() # print("rhinoinside installed.") # # Import Rhino components # import Rhino # print("Rhino installed.") ``` -------------------------------------------------------------------------------- /grasshopper_mcp/tools/analysis.py: -------------------------------------------------------------------------------- ```python from mcp.server.fastmcp import FastMCP, Context def register_analysis_tools(mcp: FastMCP) -> None: """Register analysis tools with the MCP server.""" @mcp.tool() async def analyze_rhino_file(file_path: str) -> str: """Analyze a Rhino (.3dm) file. Args: file_path: Path to the .3dm file Returns: Analysis of the file contents """ # Get context using the FastMCP mechanism ctx = mcp.get_context() rhino = ctx.request_context.lifespan_context.rhino result = await rhino.read_3dm_file(file_path) if result["result"] == "error": return f"Error: {result['error']}" model = result["model"] # Use r3d directly for rhino3dm mode if rhino.rhino_instance.get("use_rhino3dm", False): r3d = rhino.rhino_instance["r3d"] # Collect file information info = { "unit_system": str(model.Settings.ModelUnitSystem), "object_count": len(model.Objects), "layer_count": len(model.Layers), } # Get object types object_types = {} for obj in model.Objects: geom = obj.Geometry if geom: geom_type = str(geom.ObjectType) object_types[geom_type] = object_types.get(geom_type, 0) + 1 info["object_types"] = object_types # Format output output = [f"Analysis of {file_path}:"] output.append(f"- Unit System: {info['unit_system']}") output.append(f"- Total Objects: {info['object_count']}") output.append(f"- Total Layers: {info['layer_count']}") output.append("- Object Types:") for obj_type, count in info["object_types"].items(): output.append(f" - {obj_type}: {count}") return "\n".join(output) else: # RhinoInside mode (Windows) # Similar implementation using Rhino SDK return "RhinoInside implementation not provided" @mcp.tool() async def list_objects(file_path: str) -> str: """List all objects in a Rhino file. Args: file_path: Path to the .3dm file Returns: Information about objects in the file """ ctx = mcp.get_context() rhino = ctx.request_context.lifespan_context.rhino result = await rhino.read_3dm_file(file_path) if result["result"] == "error": return f"Error: {result['error']}" model = result["model"] # Use rhino3dm for cross-platform support if rhino.rhino_instance.get("use_rhino3dm", False): # Gather object information objects_info = [] for i, obj in enumerate(model.Objects): geom = obj.Geometry if geom: attrs = obj.Attributes name = attrs.Name or f"Object {i}" layer_index = attrs.LayerIndex # Get layer name if available layer_name = "Unknown" if 0 <= layer_index < len(model.Layers): layer_name = model.Layers[layer_index].Name obj_info = {"name": name, "type": str(geom.ObjectType), "layer": layer_name, "index": i} objects_info.append(obj_info) # Format output output = [f"Objects in {file_path}:"] for info in objects_info: output.append(f"{info['index']}. {info['name']} (Type: {info['type']}, Layer: {info['layer']})") return "\n".join(output) else: # RhinoInside mode return "RhinoInside implementation not provided" ``` -------------------------------------------------------------------------------- /grasshopper_mcp/resources/model_data.py: -------------------------------------------------------------------------------- ```python from mcp.server.fastmcp import FastMCP def register_model_resources(mcp: FastMCP) -> None: """Register model data resources with the MCP server.""" @mcp.resource("rhino://{file_path}") async def get_rhino_file_info(file_path: str) -> str: """Get information about a Rhino file.""" ctx = mcp.get_context() rhino = ctx.request_context.lifespan_context.rhino result = await rhino.read_3dm_file(file_path) if result["result"] == "error": return f"Error: {result['error']}" model = result["model"] # Use rhino3dm if rhino.rhino_instance.get("use_rhino3dm", False): # Basic file information info = { "file_path": file_path, "unit_system": str(model.Settings.ModelUnitSystem), "object_count": len(model.Objects), "layer_count": len(model.Layers), "material_count": len(model.Materials), "notes": model.Notes or "No notes", } # Format output output = [f"# Rhino File: {file_path}"] output.append(f"- Unit System: {info['unit_system']}") output.append(f"- Objects: {info['object_count']}") output.append(f"- Layers: {info['layer_count']}") output.append(f"- Materials: {info['material_count']}") output.append(f"- Notes: {info['notes']}") return "\n".join(output) else: # RhinoInside mode return "RhinoInside implementation not provided" @mcp.resource("rhino://{file_path}/object/{index}") async def get_object_info(file_path: str, index: int) -> str: """Get information about a specific object in a Rhino file.""" ctx = mcp.get_context() rhino = ctx.request_context.lifespan_context.rhino result = await rhino.read_3dm_file(file_path) if result["result"] == "error": return f"Error: {result['error']}" model = result["model"] index = int(index) # Convert to integer # Check if index is valid if index < 0 or index >= len(model.Objects): return f"Error: Invalid object index. File has {len(model.Objects)} objects." # Use rhino3dm if rhino.rhino_instance.get("use_rhino3dm", False): r3d = rhino.rhino_instance["r3d"] obj = model.Objects[index] geom = obj.Geometry attrs = obj.Attributes # Basic object information info = { "name": attrs.Name or f"Object {index}", "type": str(geom.ObjectType), "layer_index": attrs.LayerIndex, "material_index": attrs.MaterialIndex, "visible": not attrs.IsHidden, } # Get bounding box bbox = geom.BoundingBox if hasattr(geom, "BoundingBox") else geom.GetBoundingBox() if bbox: info["bounding_box"] = { "min": [bbox.Min.X, bbox.Min.Y, bbox.Min.Z], "max": [bbox.Max.X, bbox.Max.Y, bbox.Max.Z], } # Type-specific properties if hasattr(geom, "ObjectType"): if geom.ObjectType == r3d.ObjectType.Curve: info["length"] = geom.GetLength() if hasattr(geom, "GetLength") else "Unknown" info["is_closed"] = geom.IsClosed if hasattr(geom, "IsClosed") else "Unknown" elif geom.ObjectType == r3d.ObjectType.Brep: info["faces"] = len(geom.Faces) if hasattr(geom, "Faces") else "Unknown" info["edges"] = len(geom.Edges) if hasattr(geom, "Edges") else "Unknown" info["is_solid"] = geom.IsSolid if hasattr(geom, "IsSolid") else "Unknown" info["volume"] = geom.GetVolume() if hasattr(geom, "GetVolume") else "Unknown" elif geom.ObjectType == r3d.ObjectType.Mesh: info["vertices"] = len(geom.Vertices) if hasattr(geom, "Vertices") else "Unknown" info["faces"] = len(geom.Faces) if hasattr(geom, "Faces") else "Unknown" # Format output output = [f"# Object {index}: {info['name']}"] output.append(f"- Type: {info['type']}") output.append(f"- Layer Index: {info['layer_index']}") output.append(f"- Material Index: {info['material_index']}") output.append(f"- Visible: {info['visible']}") if "bounding_box" in info: bbox = info["bounding_box"] output.append("- Bounding Box:") output.append(f" - Min: ({bbox['min'][0]}, {bbox['min'][1]}, {bbox['min'][2]})") output.append(f" - Max: ({bbox['max'][0]}, {bbox['max'][1]}, {bbox['max'][2]})") for key, value in info.items(): if key not in ["name", "type", "layer_index", "material_index", "visible", "bounding_box"]: output.append(f"- {key.replace('_', ' ').title()}: {value}") return "\n".join(output) else: # RhinoInside mode return "RhinoInside implementation not provided" ``` -------------------------------------------------------------------------------- /examples/zaha01.py: -------------------------------------------------------------------------------- ```python import Rhino.Geometry as rg import ghpythonlib.components as ghcomp import math import random # === INPUT PARAMETERS === height = 200.0 # Total height of the tower base_radius = 30.0 # Radius at the base of the tower top_radius = 15.0 # Radius at the top of the tower twist_angle = 75.0 # Total twist angle from base to top (degrees) floors = 35 # Number of floors curvature_factor = 0.3 # Factor controlling central spine curvature (0-1) organic_factor = 0.4 # Factor controlling the organic deformation of floor plates (0-1) # === OUTPUTS === tower_surfaces = [] # Collection of surfaces forming the tower floor_curves = [] # Collection of floor plate curves central_spine = None # Central spine curve # === HELPER FUNCTIONS === def create_organic_floor_curve(center, radius, segments, organic_factor, phase): """ Creates an organic floor curve with controlled deformation. Args: center: Center point of the floor curve radius: Base radius of the floor curve segments: Number of segments for the curve (smoothness) organic_factor: Amount of organic deformation (0-1) phase: Phase shift for the organic deformation pattern Returns: A closed curve representing the floor shape """ points = [] for i in range(segments): angle = (math.pi * 2.0 * i) / segments # Create organic variation using multiple sine waves with different frequencies variation = 1.0 + organic_factor * ( 0.4 * math.sin(angle * 2 + phase) + 0.3 * math.sin(angle * 3 + phase * 1.7) + 0.2 * math.sin(angle * 5 + phase * 0.8) ) # Calculate point coordinates x = center.X + radius * variation * math.cos(angle) y = center.Y + radius * variation * math.sin(angle) point = rg.Point3d(x, y, center.Z) points.append(point) # Close the curve by adding the first point again points.append(points[0]) # Create interpolated curve through points # Degree 3 for smooth, flowing curves characteristic of Zaha Hadid's work return rg.Curve.CreateInterpolatedCurve(points, 3) def ease_in_out(t): """ Provides a smooth ease-in-out interpolation. Used for more natural transitions characteristic of Hadid's fluid forms. Args: t: Input value (0-1) Returns: Eased value (0-1) """ return 0.5 - 0.5 * math.cos(t * math.pi) # === MAIN ALGORITHM === # 1. Create the central spine with a gentle S-curve (Hadid's sinuous forms) spine_points = [] for i in range(floors + 1): # Calculate height position z = i * (height / floors) t = z / height # Normalized height (0-1) # Create an S-curve using sine function # This creates the flowing, undulating central spine typical in Hadid's work curve_x = math.sin(t * math.pi) * base_radius * curvature_factor curve_y = math.sin(t * math.pi * 0.5) * base_radius * curvature_factor * 0.7 spine_points.append(rg.Point3d(curve_x, curve_y, z)) # Create a smooth interpolated curve through the spine points central_spine = rg.Curve.CreateInterpolatedCurve(spine_points, 3) # 2. Create floor curves with organic shapes and twisting for i in range(floors + 1): # Calculate height position z = i * (height / floors) t = z / height # Normalized height (0-1) # Get point on spine at this height spine_param = central_spine.Domain.ParameterAt(t) center = central_spine.PointAt(spine_param) # Calculate radius with smooth transition from base to top # Using ease_in_out for more natural, fluid transition eased_t = ease_in_out(t) radius = base_radius * (1 - eased_t) + top_radius * eased_t # Add Hadid-like bulges at strategic points if 0.3 < t < 0.7: # Create a subtle bulge in the middle section bulge_factor = math.sin((t - 0.3) * math.pi / 0.4) * 0.15 radius *= (1 + bulge_factor) # Calculate twist angle based on height angle_rad = math.radians(twist_angle * t) # Create a plane for the floor curve # First get the spine's tangent at this point tangent = central_spine.TangentAt(spine_param) tangent.Unitize() # Create perpendicular vectors for the plane x_dir = rg.Vector3d.CrossProduct(tangent, rg.Vector3d.ZAxis) if x_dir.Length < 0.001: x_dir = rg.Vector3d.XAxis x_dir.Unitize() y_dir = rg.Vector3d.CrossProduct(tangent, x_dir) y_dir.Unitize() # Apply twist rotation rotated_x = x_dir * math.cos(angle_rad) - y_dir * math.sin(angle_rad) rotated_y = x_dir * math.sin(angle_rad) + y_dir * math.cos(angle_rad) floor_plane = rg.Plane(center, rotated_x, rotated_y) # Phase shift creates variation in organic patterns between floors # This creates the flowing, continuous aesthetic of Hadid's work phase_shift = t * 8.0 # Create organic floor curve segments = 24 # Number of segments for smoothness curve = create_organic_floor_curve(floor_plane.Origin, radius, segments, organic_factor * (1 + 0.5 * math.sin(t * math.pi)), phase_shift) floor_curves.append(curve) # 3. Create surfaces between floor curves for i in range(len(floor_curves) - 1): # Create loft surface between consecutive floors # Using Tight loft type for more fluid transitions loft_curves = [floor_curves[i], floor_curves[i+1]] loft_type = rg.LoftType.Tight try: # Create loft surfaces loft = ghcomp.Loft(loft_curves, loft_type) if isinstance(loft, list): tower_surfaces.extend(loft) else: tower_surfaces.append(loft) except: # Skip if loft creation fails pass # === ASSIGN OUTPUTS === a = tower_surfaces # Tower surfaces b = floor_curves # Floor curves c = central_spine # Central spine curve ``` -------------------------------------------------------------------------------- /grasshopper_mcp/tools/grasshopper.py: -------------------------------------------------------------------------------- ```python from mcp.server.fastmcp import FastMCP from typing import Dict, List, Any, Optional def register_grasshopper_tools(mcp: FastMCP) -> None: """Register Grasshopper-specific tools with the MCP server.""" # @mcp.tool() # async def generate_grasshopper_code( # description: str, # file_path: str, # parameters: Dict[str, Any] = None, # component_name: Optional[str] = None, # ) -> str: # """Generate Python code for a Grasshopper component based on a description. # Args: # description: Description of what the code should do # file_path: Path where the generated code will be saved # parameters: Dictionary of parameters to use in the code generation. # Can include the following keys: # - code_override: String containing complete code to use instead of generating # - center_x, center_y, center_z: Numeric values for geometric operations # - radius: Numeric value for circles or spheres # - width, height, depth: Dimensions for rectangular forms # - [Other commonly used parameters...] # component_name: Optional name for the GH component # Returns: # Result of the operation including the file path to the generated code # """ # ctx = mcp.get_context() # rhino = ctx.request_context.lifespan_context.rhino # result = await rhino.generate_and_execute_gh_code(description, file_path, parameters, component_name) # if result["result"] == "error": # return f"Error generating Grasshopper code: {result['error']}" # return f"""Generated Grasshopper Python code successfully: # {result['code']}""" @mcp.tool() async def execute_grasshopper_code(code: str, file_path: str) -> str: """Execute given Python code. Args: code: The given code to execute file_path: Path where the generated code will be saved Returns: Result of the executing code """ ctx = mcp.get_context() rhino = ctx.request_context.lifespan_context.rhino result = await rhino.send_code_to_gh(code, file_path) @mcp.tool() async def add_grasshopper_component( component_name: str, component_type: str, parameters: Dict[str, Any] ) -> str: """Add a component from an existing Grasshopper plugin. Args: component_name: Name of the component component_type: Type/category of the component parameters: Component parameters and settings Returns: Result of the operation """ ctx = mcp.get_context() rhino = ctx.request_context.lifespan_context.rhino result = await rhino.add_gh_component( component_name=component_name, component_type=component_type, parameters=parameters ) if result["result"] == "error": return f"Error adding component: {result['error']}" return f"""Successfully added Grasshopper component: - Component: {component_name} ({component_type}) - Component ID: {result.get('component_id', 'Unknown')} """ @mcp.tool() async def connect_grasshopper_components( source_id: str, source_param: str, target_id: str, target_param: str ) -> str: """Connect parameters between Grasshopper components. Args: source_id: Source component ID source_param: Source parameter name target_id: Target component ID target_param: Target parameter name Returns: Result of the operation """ ctx = mcp.get_context() rhino = ctx.request_context.lifespan_context.rhino result = await rhino.connect_gh_components( source_id=source_id, source_param=source_param, target_id=target_id, target_param=target_param ) if result["result"] == "error": return f"Error connecting components: {result['error']}" return f"""Successfully connected Grasshopper components: - Connected: {source_id}.{source_param} → {target_id}.{target_param} """ @mcp.tool() async def run_grasshopper_definition( file_path: Optional[str] = None, save_output: bool = False, output_path: Optional[str] = None ) -> str: """Run a Grasshopper definition. Args: file_path: Path to the .gh file (or None for current definition) save_output: Whether to save the output output_path: Path to save the output (if save_output is True) Returns: Result of the operation """ ctx = mcp.get_context() rhino = ctx.request_context.lifespan_context.rhino result = await rhino.run_gh_definition( file_path=file_path, save_output=save_output, output_path=output_path ) if result["result"] == "error": return f"Error running definition: {result['error']}" return f"""Successfully ran Grasshopper definition: - Execution time: {result.get('execution_time', 'Unknown')} seconds - Outputs: {result.get('output_summary', 'No output summary available')} """ async def generate_python_code( rhino_connection, description: str, inputs: List[Dict[str, Any]], outputs: List[Dict[str, Any]] ) -> Dict[str, Any]: """Generate Python code for Grasshopper based on description and parameters. This is a simplified implementation. In a production system, this might call an LLM or use templates. """ # Build code header with imports code = "import Rhino.Geometry as rg\n" code += "import scriptcontext as sc\n" code += "import ghpythonlib.components as ghcomp\n\n" # Add description as comment code += f"# {description}\n\n" # Process inputs input_vars = [] for i, inp in enumerate(inputs): var_name = inp["name"] input_vars.append(var_name) # Add comments about input parameters code += f"# Input: {var_name} ({inp['type']}) - {inp.get('description', '')}\n" code += "\n# Processing\n" # Add basic implementation based on description # This is where you might want to call an LLM or use more sophisticated templates if "circle" in description.lower(): code += """if radius is not None: circle = rg.Circle(rg.Point3d(0, 0, 0), radius) circle = circle.ToNurbsCurve() else: circle = None """ elif "box" in description.lower(): code += """if width is not None and height is not None and depth is not None: box = rg.Box( rg.Plane.WorldXY, rg.Interval(0, width), rg.Interval(0, height), rg.Interval(0, depth) ) else: box = None """ else: # Generic code template code += "# Add your implementation here based on the description\n" code += "# Use the input parameters to generate the desired output\n\n" # Process outputs output_assignments = [] for output in outputs: var_name = output["name"] # Assign a dummy value to each output output_assignments.append(f"{var_name} = {var_name}") # Add output assignments code += "\n# Outputs\n" code += "\n".join(output_assignments) return {"result": "success", "code": code} ``` -------------------------------------------------------------------------------- /grasshopper_mcp/tools/modeling.py: -------------------------------------------------------------------------------- ```python from mcp.server.fastmcp import FastMCP def register_modeling_tools(mcp: FastMCP) -> None: """Register geometry access tools with the MCP server.""" @mcp.tool() async def extract_geometry(file_path: str, object_index: int) -> str: """Extract geometric data from an existing object. Args: file_path: Path to the .3dm file object_index: Index of the object to extract data from Returns: Geometric data in a readable format """ ctx = mcp.get_context() rhino = ctx.request_context.lifespan_context.rhino result = await rhino.read_3dm_file(file_path) if result["result"] == "error": return f"Error: {result['error']}" model = result["model"] # Check if index is valid try: index = int(object_index) if index < 0 or index >= len(model.Objects): return f"Error: Invalid object index. File has {len(model.Objects)} objects." except ValueError: return f"Error: Object index must be a number." # Extract geometry data using rhino3dm obj = model.Objects[index] geom = obj.Geometry if not geom: return f"Error: No geometry found for object at index {index}." r3d = rhino.rhino_instance["r3d"] # Extract data based on geometry type geometry_data = {"type": str(geom.ObjectType), "id": str(obj.Id) if hasattr(obj, "Id") else "Unknown"} # Get bounding box bbox = geom.GetBoundingBox() if hasattr(geom, "GetBoundingBox") else None if bbox: geometry_data["bounding_box"] = { "min": [bbox.Min.X, bbox.Min.Y, bbox.Min.Z], "max": [bbox.Max.X, bbox.Max.Y, bbox.Max.Z], "dimensions": [bbox.Max.X - bbox.Min.X, bbox.Max.Y - bbox.Min.Y, bbox.Max.Z - bbox.Min.Z], } # Type-specific data extraction if hasattr(geom, "ObjectType"): if geom.ObjectType == r3d.ObjectType.Point: point = geom.Location geometry_data["coordinates"] = [point.X, point.Y, point.Z] elif geom.ObjectType == r3d.ObjectType.Curve: # For curves, extract key points geometry_data["length"] = geom.GetLength() if hasattr(geom, "GetLength") else "Unknown" geometry_data["is_closed"] = geom.IsClosed if hasattr(geom, "IsClosed") else "Unknown" # Get start and end points if not closed if hasattr(geom, "PointAtStart") and hasattr(geom, "PointAtEnd"): start = geom.PointAtStart end = geom.PointAtEnd geometry_data["start_point"] = [start.X, start.Y, start.Z] geometry_data["end_point"] = [end.X, end.Y, end.Z] elif geom.ObjectType == r3d.ObjectType.Brep: # For solids, extract volume and surface area geometry_data["volume"] = geom.GetVolume() if hasattr(geom, "GetVolume") else "Unknown" geometry_data["area"] = geom.GetArea() if hasattr(geom, "GetArea") else "Unknown" geometry_data["is_solid"] = geom.IsSolid if hasattr(geom, "IsSolid") else "Unknown" # Count faces, edges, vertices if hasattr(geom, "Faces") and hasattr(geom, "Edges"): geometry_data["face_count"] = len(geom.Faces) geometry_data["edge_count"] = len(geom.Edges) elif geom.ObjectType == r3d.ObjectType.Mesh: # For meshes, extract vertex and face counts if hasattr(geom, "Vertices") and hasattr(geom, "Faces"): geometry_data["vertex_count"] = len(geom.Vertices) geometry_data["face_count"] = len(geom.Faces) # Format output as readable text output = [f"# Geometry Data for Object {index}"] output.append(f"- Type: {geometry_data['type']}") output.append(f"- ID: {geometry_data['id']}") if "bounding_box" in geometry_data: bbox = geometry_data["bounding_box"] output.append("- Bounding Box:") output.append(f" - Min: ({bbox['min'][0]:.2f}, {bbox['min'][1]:.2f}, {bbox['min'][2]:.2f})") output.append(f" - Max: ({bbox['max'][0]:.2f}, {bbox['max'][1]:.2f}, {bbox['max'][2]:.2f})") output.append( f" - Dimensions: {bbox['dimensions'][0]:.2f} × {bbox['dimensions'][1]:.2f} × {bbox['dimensions'][2]:.2f}" ) # Add remaining data with nice formatting for key, value in geometry_data.items(): if key not in ["type", "id", "bounding_box"]: # Format key nicely formatted_key = key.replace("_", " ").title() # Format value based on type if isinstance(value, list) and len(value) == 3: formatted_value = f"({value[0]:.2f}, {value[1]:.2f}, {value[2]:.2f})" elif isinstance(value, float): formatted_value = f"{value:.4f}" else: formatted_value = str(value) output.append(f"- {formatted_key}: {formatted_value}") return "\n".join(output) @mcp.tool() async def measure_distance(file_path: str, object_index1: int, object_index2: int) -> str: """Measure the distance between two objects in a Rhino file. Args: file_path: Path to the .3dm file object_index1: Index of the first object object_index2: Index of the second object Returns: Distance measurement information """ ctx = mcp.get_context() rhino = ctx.request_context.lifespan_context.rhino result = await rhino.read_3dm_file(file_path) if result["result"] == "error": return f"Error: {result['error']}" model = result["model"] r3d = rhino.rhino_instance["r3d"] # Validate indices try: idx1 = int(object_index1) idx2 = int(object_index2) if idx1 < 0 or idx1 >= len(model.Objects) or idx2 < 0 or idx2 >= len(model.Objects): return ( f"Error: Invalid object indices. File has {len(model.Objects)} objects (0-{len(model.Objects)-1})." ) except ValueError: return "Error: Object indices must be numbers." # Get geometries obj1 = model.Objects[idx1] obj2 = model.Objects[idx2] geom1 = obj1.Geometry geom2 = obj2.Geometry if not geom1 or not geom2: return "Error: One or both objects don't have geometry." # Calculate distances using bounding boxes (simple approach) bbox1 = geom1.GetBoundingBox() if hasattr(geom1, "GetBoundingBox") else None bbox2 = geom2.GetBoundingBox() if hasattr(geom2, "GetBoundingBox") else None if not bbox1 or not bbox2: return "Error: Couldn't get bounding boxes for the objects." # Calculate center points center1 = r3d.Point3d( (bbox1.Min.X + bbox1.Max.X) / 2, (bbox1.Min.Y + bbox1.Max.Y) / 2, (bbox1.Min.Z + bbox1.Max.Z) / 2 ) center2 = r3d.Point3d( (bbox2.Min.X + bbox2.Max.X) / 2, (bbox2.Min.Y + bbox2.Max.Y) / 2, (bbox2.Min.Z + bbox2.Max.Z) / 2 ) # Calculate distance between centers distance = center1.DistanceTo(center2) # Get object names name1 = obj1.Attributes.Name or f"Object {idx1}" name2 = obj2.Attributes.Name or f"Object {idx2}" return f"""Measurement between '{name1}' and '{name2}': - Center-to-center distance: {distance:.4f} units - Object 1 center: ({center1.X:.2f}, {center1.Y:.2f}, {center1.Z:.2f}) - Object 2 center: ({center2.X:.2f}, {center2.Y:.2f}, {center2.Z:.2f}) Note: This is an approximate center-to-center measurement using bounding boxes. """ ``` -------------------------------------------------------------------------------- /grasshopper_mcp/tools/advanced_grasshopper.py: -------------------------------------------------------------------------------- ```python from mcp.server.fastmcp import FastMCP from typing import Dict, List, Any, Optional def register_advanced_grasshopper_tools(mcp: FastMCP) -> None: """Register advanced Grasshopper operations with the MCP server.""" @mcp.tool() async def create_parametric_definition( description: str, parameters: Dict[str, Any], output_file: Optional[str] = None ) -> str: """Create a complete parametric definition in Grasshopper based on a description. Args: description: Detailed description of the parametric model to create parameters: Dictionary of parameter names and values output_file: Optional path to save the definition Returns: Result of the operation """ ctx = mcp.get_context() rhino = ctx.request_context.lifespan_context.rhino # First, analyze the description to determine required components # This would ideally be done with an LLM or a sophisticated parsing system # Here we're using a simplified approach # Generate a basic workflow based on the description workflow = await generate_grasshopper_workflow(rhino, description, parameters) if workflow["result"] == "error": return f"Error generating workflow: {workflow['error']}" # Create the components in the definition component_ids = {} # Parameter components (sliders, panels, etc.) for param_name, param_info in workflow["parameters"].items(): result = await rhino.add_gh_component( component_name=param_info["component"], component_type="Params", parameters={"NickName": param_name, "Value": param_info.get("value")}, ) if result["result"] == "error": return f"Error creating parameter component: {result['error']}" component_ids[param_name] = result["component_id"] # Processing components for comp_name, comp_info in workflow["components"].items(): result = await rhino.add_gh_component( component_name=comp_info["component"], component_type=comp_info["type"], parameters={"NickName": comp_name}, ) if result["result"] == "error": return f"Error creating component: {result['error']}" component_ids[comp_name] = result["component_id"] # Python script components for script_name, script_info in workflow["scripts"].items(): result = await rhino.create_gh_script_component( description=script_name, inputs=script_info["inputs"], outputs=script_info["outputs"], code=script_info["code"], ) if result["result"] == "error": return f"Error creating script component: {result['error']}" component_ids[script_name] = result["component_id"] # Connect the components for connection in workflow["connections"]: source = connection["from"].split(".") target = connection["to"].split(".") source_id = component_ids.get(source[0]) target_id = component_ids.get(target[0]) if not source_id or not target_id: continue result = await rhino.connect_gh_components( source_id=source_id, source_param=source[1], target_id=target_id, target_param=target[1] ) if result["result"] == "error": return f"Error connecting components: {result['error']}" # Run the definition to validate result = await rhino.run_gh_definition() if result["result"] == "error": return f"Error running definition: {result['error']}" # Save if output file is specified if output_file: save_result = await rhino.run_gh_definition(file_path=None, save_output=True, output_path=output_file) if save_result["result"] == "error": return f"Error saving definition: {save_result['error']}" return f"""Successfully created parametric Grasshopper definition: - Description: {description} - Components: {len(component_ids)} created - Parameters: {len(workflow['parameters'])} - Saved to: {output_file if output_file else 'Not saved to file'} """ @mcp.tool() async def call_grasshopper_plugin( plugin_name: str, component_name: str, inputs: Dict[str, Any], file_path: Optional[str] = None ) -> str: """Call a specific component from a Grasshopper plugin. Args: plugin_name: Name of the plugin (e.g., 'Kangaroo', 'Ladybug') component_name: Name of the component to use inputs: Dictionary of input parameter names and values file_path: Optional path to a GH file to append to Returns: Result of the operation """ ctx = mcp.get_context() rhino = ctx.request_context.lifespan_context.rhino # If file path provided, open that definition if file_path: open_result = await rhino.run_gh_definition(file_path=file_path) if open_result["result"] == "error": return f"Error opening file: {open_result['error']}" # Add the plugin component plugin_comp_result = await rhino.add_gh_component( component_name=component_name, component_type=plugin_name, parameters={} ) if plugin_comp_result["result"] == "error": return f"Error adding plugin component: {plugin_comp_result['error']}" plugin_comp_id = plugin_comp_result["component_id"] # Add input parameters input_comp_ids = {} for input_name, input_value in inputs.items(): # Determine the appropriate parameter component if isinstance(input_value, (int, float)): comp_type = "Number" elif isinstance(input_value, str): comp_type = "Text" elif isinstance(input_value, bool): comp_type = "Boolean" else: return f"Unsupported input type for {input_name}: {type(input_value)}" # Create the parameter component input_result = await rhino.add_gh_component( component_name=comp_type, component_type="Params", parameters={"NickName": input_name, "Value": input_value}, ) if input_result["result"] == "error": return f"Error creating input parameter {input_name}: {input_result['error']}" input_comp_ids[input_name] = input_result["component_id"] # Connect to the plugin component connect_result = await rhino.connect_gh_components( source_id=input_result["component_id"], source_param="output", target_id=plugin_comp_id, target_param=input_name, ) if connect_result["result"] == "error": return f"Error connecting {input_name}: {connect_result['error']}" # Run the definition run_result = await rhino.run_gh_definition() if run_result["result"] == "error": return f"Error running definition with plugin: {run_result['error']}" return f"""Successfully called Grasshopper plugin component: - Plugin: {plugin_name} - Component: {component_name} - Inputs: {', '.join(inputs.keys())} - Execution time: {run_result.get('execution_time', 'Unknown')} seconds """ @mcp.tool() async def edit_gh_script_component(file_path: str, component_id: str, new_code: str) -> str: """Edit the code in an existing Python script component. Args: file_path: Path to the Grasshopper file component_id: ID of the component to edit new_code: New Python script for the component Returns: Result of the operation """ ctx = mcp.get_context() rhino = ctx.request_context.lifespan_context.rhino # Open the file open_result = await rhino.run_gh_definition(file_path=file_path) if open_result["result"] == "error": return f"Error opening file: {open_result['error']}" # Edit the component execution_code = """ import Rhino import Grasshopper # Access the current Grasshopper document gh_doc = Grasshopper.Instances.ActiveCanvas.Document # Find the component by ID target_component = None for obj in gh_doc.Objects: if str(obj.ComponentGuid) == component_id: target_component = obj break if target_component is None: raise ValueError(f"Component with ID {component_id} not found") # Check if it's a Python component if not hasattr(target_component, "ScriptSource"): raise ValueError(f"Component is not a Python script component") # Update the code target_component.ScriptSource = new_code # Update the document gh_doc.NewSolution(True) result = { "component_name": target_component.NickName, "success": True } """ edit_result = await rhino._execute_rhino(execution_code, {"component_id": component_id, "new_code": new_code}) if edit_result["result"] == "error": return f"Error editing component: {edit_result['error']}" return f"""Successfully edited Python script component: - Component: {edit_result.get('data', {}).get('component_name', 'Unknown')} - Updated code length: {len(new_code)} characters """ async def generate_grasshopper_workflow( rhino_connection, description: str, parameters: Dict[str, Any] ) -> Dict[str, Any]: """Generate a Grasshopper workflow based on a description. This is a simplified implementation that parses the description to determine the necessary components, parameters, and connections. In a production system, this would likely use an LLM to generate the workflow. """ # Initialize workflow structure workflow = {"parameters": {}, "components": {}, "scripts": {}, "connections": [], "result": "success"} # Analyze description to determine what we're building description_lower = description.lower() # Default to a basic parametric box if no specific shape is mentioned if "box" in description_lower or "cube" in description_lower: # Create a parametric box workflow["parameters"] = { "Width": {"component": "Number Slider", "value": parameters.get("Width", 10)}, "Height": {"component": "Number Slider", "value": parameters.get("Height", 10)}, "Depth": {"component": "Number Slider", "value": parameters.get("Depth", 10)}, } workflow["components"] = { "BoxOrigin": {"component": "Construct Point", "type": "Vector"}, "Box": {"component": "Box", "type": "Surface"}, } workflow["connections"] = [ {"from": "Width.output", "to": "Box.X Size"}, {"from": "Height.output", "to": "Box.Y Size"}, {"from": "Depth.output", "to": "Box.Z Size"}, {"from": "BoxOrigin.Point", "to": "Box.Base Point"}, ] elif "cylinder" in description_lower: # Create a parametric cylinder workflow["parameters"] = { "Radius": {"component": "Number Slider", "value": parameters.get("Radius", 5)}, "Height": {"component": "Number Slider", "value": parameters.get("Height", 20)}, } workflow["components"] = { "BasePoint": {"component": "Construct Point", "type": "Vector"}, "Circle": {"component": "Circle", "type": "Curve"}, "Cylinder": {"component": "Extrude", "type": "Surface"}, } workflow["connections"] = [ {"from": "Radius.output", "to": "Circle.Radius"}, {"from": "BasePoint.Point", "to": "Circle.Base"}, {"from": "Circle.Circle", "to": "Cylinder.Base"}, {"from": "Height.output", "to": "Cylinder.Direction"}, ] elif "loft" in description_lower or "surface" in description_lower: # Create a lofted surface between curves workflow["parameters"] = { "Points": {"component": "Number Slider", "value": parameters.get("Points", 5)}, "Height": {"component": "Number Slider", "value": parameters.get("Height", 20)}, "RadiusBottom": {"component": "Number Slider", "value": parameters.get("RadiusBottom", 10)}, "RadiusTop": {"component": "Number Slider", "value": parameters.get("RadiusTop", 5)}, } workflow["components"] = { "BasePoint": {"component": "Construct Point", "type": "Vector"}, "TopPoint": {"component": "Construct Point", "type": "Vector"}, "CircleBottom": {"component": "Circle", "type": "Curve"}, "CircleTop": {"component": "Circle", "type": "Curve"}, "Loft": {"component": "Loft", "type": "Surface"}, } # For more complex workflows, we can use Python script components workflow["scripts"] = { "HeightVector": { "inputs": [{"name": "height", "type": "float", "description": "Height of the loft"}], "outputs": [{"name": "vector", "type": "vector", "description": "Height vector"}], "code": """ import Rhino.Geometry as rg # Create a vertical vector for the height vector = rg.Vector3d(0, 0, height) """, } } workflow["connections"] = [ {"from": "Height.output", "to": "HeightVector.height"}, {"from": "HeightVector.vector", "to": "TopPoint.Z"}, {"from": "RadiusBottom.output", "to": "CircleBottom.Radius"}, {"from": "RadiusTop.output", "to": "CircleTop.Radius"}, {"from": "BasePoint.Point", "to": "CircleBottom.Base"}, {"from": "TopPoint.Point", "to": "CircleTop.Base"}, {"from": "CircleBottom.Circle", "to": "Loft.Curves"}, {"from": "CircleTop.Circle", "to": "Loft.Curves"}, ] else: # Generic parametric object with Python script workflow["parameters"] = { "Parameter1": {"component": "Number Slider", "value": parameters.get("Parameter1", 10)}, "Parameter2": {"component": "Number Slider", "value": parameters.get("Parameter2", 20)}, } workflow["scripts"] = { "CustomGeometry": { "inputs": [ {"name": "param1", "type": "float", "description": "First parameter"}, {"name": "param2", "type": "float", "description": "Second parameter"}, ], "outputs": [{"name": "geometry", "type": "geometry", "description": "Resulting geometry"}], "code": """ import Rhino.Geometry as rg import math # Create custom geometry based on parameters point = rg.Point3d(0, 0, 0) radius = param1 height = param2 # Default to a simple cylinder if nothing specific is mentioned cylinder = rg.Cylinder( new rg.Circle(point, radius), height ) geometry = cylinder.ToBrep(True, True) """, } } workflow["connections"] = [ {"from": "Parameter1.output", "to": "CustomGeometry.param1"}, {"from": "Parameter2.output", "to": "CustomGeometry.param2"}, ] return workflow ``` -------------------------------------------------------------------------------- /grasshopper_mcp/rhino/connection.py: -------------------------------------------------------------------------------- ```python import platform import os import json import uuid from typing import Any, Dict, List, Optional import time import platform import sys import socket import tempfile from ..config import ServerConfig def find_scriptcontext_path(): scriptcontext_path = os.path.join( os.environ["APPDATA"], "McNeel", "Rhinoceros", "7.0", "Plug-ins", "IronPython (814d908a-e25c-493d-97e9-ee3861957f49)", "settings", "lib", ) if not os.path.exists(scriptcontext_path): # If the specific path doesn't exist, try to find it import glob appdata = os.environ["APPDATA"] potential_paths = glob.glob( os.path.join(appdata, "McNeel", "Rhinoceros", "7.0", "Plug-ins", "IronPython*", "settings", "lib") ) if potential_paths: scriptcontext_path # sys.path.append(potential_paths[0]) return scriptcontext_path def find_RhinoPython_path(rhino_path): appdata = os.environ["APPDATA"] rhino_python_paths = [ # Standard Rhino Python lib paths os.path.join(os.path.dirname(rhino_path), "Plug-ins", "IronPython"), os.path.join(os.path.dirname(rhino_path), "Plug-ins", "IronPython", "Lib"), os.path.join(os.path.dirname(rhino_path), "Plug-ins", "PythonPlugins"), os.path.join(os.path.dirname(rhino_path), "Scripts"), # Try to find RhinoPython in various locations os.path.join(os.path.dirname(rhino_path), "Plug-ins"), os.path.join(appdata, "McNeel", "Rhinoceros", "7.0", "Plug-ins"), os.path.join(appdata, "McNeel", "Rhinoceros", "7.0", "Scripts"), # Common Rhino installation paths for plugins "C:\\Program Files\\Rhino 7\\Plug-ins", "C:\\Program Files\\Rhino 7\\Plug-ins\\PythonPlugins", ] return rhino_python_paths class RhinoConnection: """Connection to Rhino/Grasshopper.""" def __init__(self, config: ServerConfig): self.config = config self.connected = False self.rhino_instance = None self.is_mac = platform.system() == "Darwin" self.codelistener_host = "127.0.0.1" self.codelistener_port = 614 # Default CodeListener port async def initialize(self) -> None: """Initialize connection to Rhino/Grasshopper.""" if self.config.use_compute_api: # Setup compute API connection self._initialize_compute() else: # Setup direct connection self._initialize_rhino() self.connected = True def _initialize_rhino(self) -> None: """Initialize Rhino geometry access.""" if platform.system() == "Windows" and not self.config.use_rhino3dm: # Windows-specific RhinoInside implementation import sys rhino_path = self.config.rhino_path if not rhino_path or not os.path.exists(rhino_path): raise ValueError(f"Invalid Rhino path: {rhino_path}") # print(rhino_path) sys.path.append(rhino_path) # Add the specific path for scriptcontext scriptcontext_path = find_scriptcontext_path() sys.path.append(scriptcontext_path) RhinoPython_path = find_RhinoPython_path(rhino_path) for path in RhinoPython_path: if os.path.exists(path): sys.path.append(path) print(path) try: import rhinoinside rhinoinside.load() print("rhinoinside installed.") # Import Rhino components import Rhino print("Rhino installed.") import Rhino.Geometry as rg print("Rhino.Geometry installed.") import scriptcontext as sc # Store references self.rhino_instance = {"Rhino": Rhino, "rg": rg, "sc": sc, "use_rhino3dm": False} except ImportError as e: raise ImportError(f"Error importing RhinoInside or Rhino components: {e}") else: # Cross-platform rhino3dm implementation try: import rhino3dm as r3d self.rhino_instance = {"r3d": r3d, "use_rhino3dm": True} except ImportError: raise ImportError("Please install rhino3dm: uv add rhino3dm") async def send_code_to_rhino(self, code: str) -> Dict[str, Any]: """Send Python code to Rhino via CodeListener. Args: code: Python code to execute in Rhino Returns: Dictionary with result and response or error """ try: # Create a temporary Python file fd, temp_path = tempfile.mkstemp(suffix=".py") try: # Write the code to the file with os.fdopen(fd, "w") as f: f.write(code) # Create message object msg_obj = {"filename": temp_path, "run": True, "reset": False, "temp": True} # Convert to JSON json_msg = json.dumps(msg_obj) # Create a TCP socket sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(10) sock.connect((self.codelistener_host, self.codelistener_port)) # Send the JSON message sock.sendall(json_msg.encode("utf-8")) # Receive the response response = sock.recv(4096).decode("utf-8") # Close the socket sock.close() return {"result": "success", "response": response} finally: # Clean up - remove temporary file after execution try: os.unlink(temp_path) except Exception as cleanup_error: print(f"Error cleaning up temporary file: {cleanup_error}") except Exception as e: return {"result": "error", "error": str(e)} async def generate_and_execute_rhino_code( self, prompt: str, model_context: Optional[Dict[str, Any]] = None ) -> Dict[str, Any]: """Generate Rhino Python code from a prompt and execute it. Args: prompt: Description of what code to generate model_context: Optional context about the model (dimensions, parameters, etc.) Returns: Result dictionary with code, execution result, and any output """ # Step 1: Generate Python code based on the prompt code = await self._generate_code_from_prompt(prompt, model_context) # Step 2: Execute the generated code in Rhino result = await self.send_code_to_rhino(code) # Return both the code and the execution result return { "result": result.get("result", "error"), "code": code, "response": result.get("response", ""), "error": result.get("error", ""), } async def _generate_code_from_prompt( self, prompt: str, model_context: Optional[Dict[str, Any]] = None ) -> str: """Generate Rhino Python code from a text prompt. Args: prompt: Description of what the code should do model_context: Optional context about the model Returns: Generated Python code as a string """ # Add standard imports for Rhino Python code code = """ import Rhino import rhinoscriptsyntax as rs import scriptcontext as sc import System from Rhino.Geometry import * # Disable redraw to improve performance rs.EnableRedraw(False) """ # Add code based on the prompt prompt_lower = prompt.lower() if "circle" in prompt_lower: radius = model_context.get("radius", 10.0) if model_context else 10.0 center_x = model_context.get("center_x", 0.0) if model_context else 0.0 center_y = model_context.get("center_y", 0.0) if model_context else 0.0 center_z = model_context.get("center_z", 0.0) if model_context else 0.0 code += f""" # Create a circle based on prompt: {prompt} center = Point3d({center_x}, {center_y}, {center_z}) circle = Circle(Plane.WorldXY, center, {radius}) circle_id = sc.doc.Objects.AddCircle(circle) if circle_id: rs.ObjectName(circle_id, "GeneratedCircle") print("Created a circle!") else: print("Failed to create circle") """ return code async def send_code_to_gh(self, code: str, file_path: str) -> Dict[str, Any]: """Send Python code to a file for Grasshopper to use. Args: code: Python code to save for Grasshopper file_path: Path where the Python file should be saved Returns: Dictionary with result and file path """ try: # Write the code to the file with open(file_path, "w") as f: f.write(code) return { "result": "success", "file_path": file_path, "message": f"Grasshopper Python file created at {file_path}", } except Exception as e: return {"result": "error", "error": str(e)} async def generate_and_execute_gh_code( self, prompt: str, file_path: str, model_context: Optional[Dict[str, Any]] = None, component_name: Optional[str] = None, ) -> Dict[str, Any]: """Generate Grasshopper Python code from a prompt and save it for execution. Args: prompt: Description of what code to generate model_context: Optional context about the model (dimensions, parameters, etc.) component_name: Optional name for the GH Python component Returns: Result dictionary with code, file path, and any output """ # Step 1: Generate Python code based on the prompt code = await self._generate_gh_code_from_prompt(prompt, model_context, component_name) # Step 2: Save the generated code for Grasshopper to use result = await self.send_code_to_gh(code, file_path) # Return both the code and the result return { "result": result.get("result", "error"), "code": code, "file_path": result.get("file_path", ""), "response": result.get("response", ""), "error": result.get("error", ""), } async def _generate_gh_code_from_prompt( self, prompt: str, model_context: Optional[Dict[str, Any]] = None, component_name: Optional[str] = None, ) -> str: """Generate Grasshopper Python code from a text prompt. Args: prompt: Description of what the code should do model_context: Optional context about the model component_name: Optional name for the component Returns: Generated Python code as a string """ # Add component name as a comment if component_name: code = f"""# Grasshopper Python Component: {component_name} # Generated from prompt: {prompt} """ else: code = f"""# Grasshopper Python Component # Generated from prompt: {prompt} """ # Add standard imports for Grasshopper Python code code += """ import Rhino import rhinoscriptsyntax as rs import scriptcontext as sc import Rhino.Geometry as rg import ghpythonlib.components as ghcomp import math """ # Add code based on the prompt prompt_lower = prompt.lower() if "circle" in prompt_lower: radius = model_context.get("radius", 10.0) if model_context else 10.0 center_x = model_context.get("center_x", 0.0) if model_context else 0.0 center_y = model_context.get("center_y", 0.0) if model_context else 0.0 center_z = model_context.get("center_z", 0.0) if model_context else 0.0 code += f""" # Create a circle based on prompt: {prompt} center = rg.Point3d({center_x}, {center_y}, {center_z}) circle = rg.Circle(rg.Plane.WorldXY, center, {radius}) print("Created a circle!") """ return code def _initialize_compute(self) -> None: """Initialize connection to compute.rhino3d.com.""" if not self.config.compute_url or not self.config.compute_api_key: raise ValueError("Compute API URL and key required for compute API connection") # We'll use requests for API calls async def close(self) -> None: """Close connection to Rhino/Grasshopper.""" # Cleanup as needed self.connected = False async def execute_code(self, code: str, parameters: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: """Execute operations on Rhino geometry.""" if not self.connected: raise RuntimeError("Not connected to Rhino geometry system") if self.config.use_compute_api: return await self._execute_compute(code, parameters) else: # Check if we're using rhino3dm if self.rhino_instance.get("use_rhino3dm", False): return await self._execute_rhino3dm(code, parameters) else: return await self._execute_rhino(code, parameters) async def _execute_rhino(self, code: str, parameters: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: """Execute code directly in Rhino (Windows only).""" # Existing Rhino execution code globals_dict = dict(self.rhino_instance) # Add parameters to context if parameters: globals_dict.update(parameters) # Execute the code locals_dict = {} try: exec(code, globals_dict, locals_dict) return {"result": "success", "data": locals_dict.get("result", None)} except Exception as e: # More detailed error reporting for Windows import traceback error_trace = traceback.format_exc() return {"result": "error", "error": str(e), "traceback": error_trace} async def _execute_rhino3dm( self, code: str, parameters: Optional[Dict[str, Any]] = None ) -> Dict[str, Any]: """Execute code using rhino3dm library.""" # Create execution context with rhino3dm r3d = self.rhino_instance["r3d"] globals_dict = {"r3d": r3d, "parameters": parameters or {}} # Add parameters to context if parameters: globals_dict.update(parameters) # Execute the code locals_dict = {} try: exec(code, globals_dict, locals_dict) return {"result": "success", "data": locals_dict.get("result", None)} except Exception as e: return {"result": "error", "error": str(e)} async def _execute_compute( self, code: str, parameters: Optional[Dict[str, Any]] = None ) -> Dict[str, Any]: """Execute code via compute.rhino3d.com API.""" # Existing Compute API code import requests url = f"{self.config.compute_url}/grasshopper" # Prepare request payload payload = {"algo": code, "pointer": None, "values": parameters or {}} headers = { "Authorization": f"Bearer {self.config.compute_api_key}", "Content-Type": "application/json", } try: response = requests.post(url, json=payload, headers=headers) response.raise_for_status() return {"result": "success", "data": response.json()} except Exception as e: return {"result": "error", "error": str(e)} async def read_3dm_file(self, file_path: str) -> Dict[str, Any]: """Read a .3dm file and return its model.""" if not self.connected: raise RuntimeError("Not connected to Rhino geometry system") try: if self.rhino_instance.get("use_rhino3dm", False): r3d = self.rhino_instance["r3d"] model = r3d.File3dm.Read(file_path) if model: return {"result": "success", "model": model} else: return {"result": "error", "error": f"Failed to open file: {file_path}"} else: # For RhinoInside on Windows, use a different approach code = """ import Rhino result = { "model": Rhino.FileIO.File3dm.Read(file_path) } """ return await self._execute_rhino(code, {"file_path": file_path}) except Exception as e: return {"result": "error", "error": str(e)} ### For Grasshopper async def create_gh_script_component( self, description: str, inputs: List[Dict[str, Any]], outputs: List[Dict[str, Any]], code: str ) -> Dict[str, Any]: """Create a Python script component in a Grasshopper definition. Args: description: Description of the component inputs: List of input parameters outputs: List of output parameters code: Python code for the component Returns: Result dictionary with component_id on success """ if not self.connected: return {"result": "error", "error": "Not connected to Rhino/Grasshopper"} # Generate a unique component ID component_id = f"py_{str(uuid.uuid4())[:8]}" if self.config.use_compute_api: # Implementation for compute API return await self._create_gh_script_component_compute( component_id, description, inputs, outputs, code ) elif platform.system() == "Windows" and not self.rhino_instance.get("use_rhino3dm", True): # Implementation for RhinoInside (Windows) return await self._create_gh_script_component_rhinoinside( component_id, description, inputs, outputs, code ) else: # We can't directly create Grasshopper components with rhino3dm return { "result": "error", "error": "Creating Grasshopper components requires RhinoInside or Compute API", } async def _create_gh_script_component_rhinoinside( self, component_id: str, description: str, inputs: List[Dict[str, Any]], outputs: List[Dict[str, Any]], code: str, ) -> Dict[str, Any]: """Create a Python script component using RhinoInside.""" # Using the RhinoInside context execution_code = """ import Rhino import Grasshopper import GhPython from Grasshopper.Kernel import GH_Component # Access the current Grasshopper document gh_doc = Grasshopper.Instances.ActiveCanvas.Document # Create a new Python component py_comp = GhPython.Component.PythonComponent() py_comp.NickName = description py_comp.Name = description py_comp.Description = description py_comp.ComponentGuid = System.Guid(component_id) # Set up inputs for i, inp in enumerate(inputs): name = inp["name"] param_type = inp.get("type", "object") # Convert param_type to Grasshopper parameter type access_type = 0 # 0 = item access py_comp.Params.Input[i].Name = name py_comp.Params.Input[i].NickName = name py_comp.Params.Input[i].Description = inp.get("description", "") # Set up outputs for i, out in enumerate(outputs): name = out["name"] param_type = out.get("type", "object") # Convert param_type to Grasshopper parameter type py_comp.Params.Output[i].Name = name py_comp.Params.Output[i].NickName = name py_comp.Params.Output[i].Description = out.get("description", "") # Set the Python code py_comp.ScriptSource = code # Add the component to the document gh_doc.AddObject(py_comp, False) # Set the position on canvas (centered) py_comp.Attributes.Pivot = Grasshopper.Kernel.GH_Convert.ToPoint( Rhino.Geometry.Point2d(gh_doc.Bounds.Center.X, gh_doc.Bounds.Center.Y) ) # Update the document gh_doc.NewSolution(True) # Store component for reference result = { "component_id": str(component_id) } """ # Execute the code return await self._execute_rhino( execution_code, { "component_id": component_id, "description": description, "inputs": inputs, "outputs": outputs, "code": code, }, ) async def _create_gh_script_component_compute( self, component_id: str, description: str, inputs: List[Dict[str, Any]], outputs: List[Dict[str, Any]], code: str, ) -> Dict[str, Any]: """Create a Python script component using Compute API.""" import requests url = f"{self.config.compute_url}/grasshopper/scriptcomponent" # Prepare payload payload = { "id": component_id, "name": description, "description": description, "inputs": inputs, "outputs": outputs, "code": code, } headers = { "Authorization": f"Bearer {self.config.compute_api_key}", "Content-Type": "application/json", } try: response = requests.post(url, json=payload, headers=headers) response.raise_for_status() return {"result": "success", "component_id": component_id, "data": response.json()} except Exception as e: return {"result": "error", "error": str(e)} async def add_gh_component( self, component_name: str, component_type: str, parameters: Dict[str, Any] ) -> Dict[str, Any]: """Add a component from an existing Grasshopper plugin. Args: component_name: Name of the component component_type: Type/category of the component parameters: Component parameters and settings Returns: Result dictionary with component_id on success """ if not self.connected: return {"result": "error", "error": "Not connected to Rhino/Grasshopper"} # Generate a unique component ID component_id = f"comp_{str(uuid.uuid4())[:8]}" if self.config.use_compute_api: # Implementation for compute API return await self._add_gh_component_compute( component_id, component_name, component_type, parameters ) elif platform.system() == "Windows" and not self.rhino_instance.get("use_rhino3dm", True): # Implementation for RhinoInside return await self._add_gh_component_rhinoinside( component_id, component_name, component_type, parameters ) else: return { "result": "error", "error": "Adding Grasshopper components requires RhinoInside or Compute API", } async def _add_gh_component_rhinoinside( self, component_id: str, component_name: str, component_type: str, parameters: Dict[str, Any] ) -> Dict[str, Any]: """Add a Grasshopper component using RhinoInside.""" execution_code = """ import Rhino import Grasshopper from Grasshopper.Kernel import GH_ComponentServer # Access the current Grasshopper document gh_doc = Grasshopper.Instances.ActiveCanvas.Document # Find the component by name and type server = GH_ComponentServer.FindServer(component_name, component_type) if server is None: raise ValueError(f"Component '{component_name}' of type '{component_type}' not found") # Create the component instance component = server.Create() component.ComponentGuid = System.Guid(component_id) # Set parameters for param_name, param_value in parameters.items(): if hasattr(component, param_name): setattr(component, param_name, param_value) # Add the component to the document gh_doc.AddObject(component, False) # Set the position on canvas component.Attributes.Pivot = Grasshopper.Kernel.GH_Convert.ToPoint( Rhino.Geometry.Point2d(gh_doc.Bounds.Center.X, gh_doc.Bounds.Center.Y) ) # Update the document gh_doc.NewSolution(True) # Return component info result = { "component_id": str(component_id) } """ return await self._execute_rhino( execution_code, { "component_id": component_id, "component_name": component_name, "component_type": component_type, "parameters": parameters, }, ) async def _add_gh_component_compute( self, component_id: str, component_name: str, component_type: str, parameters: Dict[str, Any] ) -> Dict[str, Any]: """Add a Grasshopper component using Compute API.""" import requests url = f"{self.config.compute_url}/grasshopper/component" # Prepare payload payload = { "id": component_id, "name": component_name, "type": component_type, "parameters": parameters, } headers = { "Authorization": f"Bearer {self.config.compute_api_key}", "Content-Type": "application/json", } try: response = requests.post(url, json=payload, headers=headers) response.raise_for_status() return {"result": "success", "component_id": component_id, "data": response.json()} except Exception as e: return {"result": "error", "error": str(e)} async def connect_gh_components( self, source_id: str, source_param: str, target_id: str, target_param: str ) -> Dict[str, Any]: """Connect parameters between Grasshopper components. Args: source_id: Source component ID source_param: Source parameter name target_id: Target component ID target_param: Target parameter name Returns: Result dictionary """ if not self.connected: return {"result": "error", "error": "Not connected to Rhino/Grasshopper"} if self.config.use_compute_api: return await self._connect_gh_components_compute(source_id, source_param, target_id, target_param) elif platform.system() == "Windows" and not self.rhino_instance.get("use_rhino3dm", True): return await self._connect_gh_components_rhinoinside( source_id, source_param, target_id, target_param ) else: return { "result": "error", "error": "Connecting Grasshopper components requires RhinoInside or Compute API", } async def _connect_gh_components_rhinoinside( self, source_id: str, source_param: str, target_id: str, target_param: str ) -> Dict[str, Any]: """Connect Grasshopper components using RhinoInside.""" execution_code = """ import Rhino import Grasshopper from Grasshopper.Kernel import GH_Document # Access the current Grasshopper document gh_doc = Grasshopper.Instances.ActiveCanvas.Document # Find the source component source = None for obj in gh_doc.Objects: if str(obj.ComponentGuid) == source_id: source = obj break if source is None: raise ValueError(f"Source component with ID {source_id} not found") # Find the target component target = None for obj in gh_doc.Objects: if str(obj.ComponentGuid) == target_id: target = obj break if target is None: raise ValueError(f"Target component with ID {target_id} not found") # Find the source output parameter source_output = None for i, param in enumerate(source.Params.Output): if param.Name == source_param: source_output = param break if source_output is None: raise ValueError(f"Source parameter {source_param} not found on component {source_id}") # Find the target input parameter target_input = None for i, param in enumerate(target.Params.Input): if param.Name == target_param: target_input = param break if target_input is None: raise ValueError(f"Target parameter {target_param} not found on component {target_id}") # Connect the parameters gh_doc.GraftIO(source_output.Recipients, target_input.Sources) # Update the document gh_doc.NewSolution(True) result = { "success": True } """ return await self._execute_rhino( execution_code, { "source_id": source_id, "source_param": source_param, "target_id": target_id, "target_param": target_param, }, ) async def _connect_gh_components_compute( self, source_id: str, source_param: str, target_id: str, target_param: str ) -> Dict[str, Any]: """Connect Grasshopper components using Compute API.""" import requests url = f"{self.config.compute_url}/grasshopper/connect" # Prepare payload payload = { "source_id": source_id, "source_param": source_param, "target_id": target_id, "target_param": target_param, } headers = { "Authorization": f"Bearer {self.config.compute_api_key}", "Content-Type": "application/json", } try: response = requests.post(url, json=payload, headers=headers) response.raise_for_status() return {"result": "success", "data": response.json()} except Exception as e: return {"result": "error", "error": str(e)} async def run_gh_definition( self, file_path: Optional[str] = None, save_output: bool = False, output_path: Optional[str] = None ) -> Dict[str, Any]: """Run a Grasshopper definition. Args: file_path: Path to the .gh file (or None for current definition) save_output: Whether to save the output output_path: Path to save the output (if save_output is True) Returns: Result dictionary with execution information """ if not self.connected: return {"result": "error", "error": "Not connected to Rhino/Grasshopper"} if self.config.use_compute_api: return await self._run_gh_definition_compute(file_path, save_output, output_path) elif platform.system() == "Windows" and not self.rhino_instance.get("use_rhino3dm", True): return await self._run_gh_definition_rhinoinside(file_path, save_output, output_path) else: return { "result": "error", "error": "Running Grasshopper definitions requires RhinoInside or Compute API", } async def _run_gh_definition_rhinoinside( self, file_path: Optional[str] = None, save_output: bool = False, output_path: Optional[str] = None ) -> Dict[str, Any]: """Run a Grasshopper definition using RhinoInside.""" execution_code = """ import Rhino import Grasshopper import time start_time = time.time() if file_path: # Open the specified Grasshopper definition gh_doc = Grasshopper.Kernel.GH_Document() gh_doc.LoadDocumentObject(file_path) else: # Use the current document gh_doc = Grasshopper.Instances.ActiveCanvas.Document # Run the solution gh_doc.NewSolution(True) # Wait for solution to complete while gh_doc.SolutionState != Grasshopper.Kernel.GH_ProcessStep.Finished: time.sleep(0.1) execution_time = time.time() - start_time # Save if requested if save_output and output_path: gh_doc.SaveAs(output_path, False) # Get a summary of the outputs output_summary = [] for obj in gh_doc.Objects: if obj.Attributes.GetTopLevel.DocObject is not None: for param in obj.Params.Output: if param.VolatileDataCount > 0: output_summary.append({ "component": obj.NickName, "param": param.Name, "data_count": param.VolatileDataCount }) result = { "execution_time": execution_time, "output_summary": output_summary } """ return await self._execute_rhino( execution_code, {"file_path": file_path, "save_output": save_output, "output_path": output_path} ) async def _run_gh_definition_compute( self, file_path: Optional[str] = None, save_output: bool = False, output_path: Optional[str] = None ) -> Dict[str, Any]: """Run a Grasshopper definition using Compute API.""" import requests url = f"{self.config.compute_url}/grasshopper/run" # Prepare payload payload = {"file_path": file_path, "save_output": save_output, "output_path": output_path} headers = { "Authorization": f"Bearer {self.config.compute_api_key}", "Content-Type": "application/json", } try: response = requests.post(url, json=payload, headers=headers) response.raise_for_status() return {"result": "success", "data": response.json()} except Exception as e: return {"result": "error", "error": str(e)} ```