#
tokens: 32534/50000 34/34 files
lines: on (toggle) GitHub
raw markdown copy reset
# 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:
--------------------------------------------------------------------------------

```
1 | 3.13
2 | 
```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
 1 | # Python-generated files
 2 | __pycache__/
 3 | *.py[oc]
 4 | build/
 5 | dist/
 6 | wheels/
 7 | *.egg-info
 8 | 
 9 | # Virtual environments
10 | .venv
11 | 
```

--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------

```
1 | # Rhino/Grasshopper configuration
2 | RHINO_PATH=C:/Program Files/Rhino 7/System
3 | COMPUTE_API_KEY=your_compute_key_here
4 | COMPUTE_URL=https://compute.rhino3d.com
5 | 
6 | # MCP server configuration
7 | SERVER_NAME=Grasshopper MCP
8 | SERVER_PORT=8080
9 | 
```

--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------

```
 1 | # Rhino/Grasshopper configuration
 2 | USE_RHINO3DM=true # using Rhino3dm (for mac), switch to true
 3 | RHINO_PATH=C:/Program Files/Rhino 7/System
 4 | USE_COMPUTE_API=false
 5 | COMPUTE_API_KEY=your_compute_key_here
 6 | COMPUTE_URL=https://compute.rhino3d.com
 7 | 
 8 | # MCP server configuration
 9 | SERVER_NAME=Grasshopper_MCP
10 | SERVER_PORT=8080
11 | 
12 | 
13 | 
14 | 
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | # GH_mcp_server
  2 | 
  3 | 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.
  4 | 
  5 | >  This project is **still under construction** — and we’d love your help!
  6 | >
  7 | > - Feel free to **open an issue** if you encounter bugs or have ideas.
  8 | > - Pull requests are always welcome.
  9 | > - 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**!
 10 | 
 11 | ![Alt text](examples/zaha01.png)
 12 | 
 13 | ## Requirements
 14 | 
 15 | - Rhino 7 or 8
 16 | 
 17 | - Install `RhinoPython`: https://github.com/jingcheng-chen/RhinoPythonForVscode/tree/master?tab=readme-ov-file
 18 | 
 19 | - `uv`
 20 | 
 21 |   - ```
 22 |     # For MacOS and Linux
 23 |     curl -LsSf https://astral.sh/uv/install.sh | sh
 24 |     ```
 25 | 
 26 |   - ``````
 27 |     # For Windows
 28 |     powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
 29 |     ``````
 30 | 
 31 | - Claude Desktop
 32 | 
 33 | ## Installation
 34 | 
 35 | ### 1. Clone the repository
 36 | 
 37 | ```
 38 | git clone [email protected]:veoery/GH_mcp_server.git
 39 | cd GH_mcp_server
 40 | ```
 41 | 
 42 | ------
 43 | 
 44 | ### 2. Set up the environment
 45 | 
 46 | We recommend using `uv`:
 47 | 
 48 | #### macOS/Linux
 49 | 
 50 | ```
 51 | uv venv
 52 | source .venv/bin/activate
 53 | uv pip install -e .
 54 | ```
 55 | 
 56 | #### Windows
 57 | 
 58 | ```
 59 | uv venv
 60 | .venv\Scripts\activate
 61 | uv pip install -e .
 62 | ```
 63 | 
 64 | > Make sure the virtual environment is activated before running or developing the project.
 65 | 
 66 | ### 3. Configuration
 67 | 
 68 | 1. In the Claude Desktop, Navigate to Settings->Developer. You will see ```Edit Config```.
 69 | 
 70 | 2. Click the ```Edit Config``` and open the file ```claude_desktop_config.json```
 71 | 
 72 | 3. Place the following code to the json file:
 73 |    ```python
 74 |    {
 75 |      "mcpServers": {
 76 |        "grasshopper": {
 77 |          "command": "path_to_GH_mcp_server/.venv/bin/python",
 78 |          "args": [
 79 |            "path_to_GH_mcp_server/run_server.py"
 80 |          ]
 81 |        }
 82 |      }
 83 |    }
 84 |    ```
 85 | 
 86 | 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.
 87 | 
 88 | ## Usage
 89 | 
 90 | 1. Start Rhino
 91 | 
 92 | 2. Type command `CodeListener`. You should see `VS Code Listener Started...`.
 93 | 
 94 | 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:
 95 | 
 96 |    ```
 97 |    Read the file "D:\test01.3dm" first and analyse the objects in this file.
 98 |    ```
 99 | 
100 |    ```
101 |    write GHpython to create a tower referring to zaha and write the ghpython code to "D:\zaha01.py"
102 |    ```
103 | 
104 | ​	
```

--------------------------------------------------------------------------------
/grasshopper_mcp/__init__.py:
--------------------------------------------------------------------------------

```python
1 | 
```

--------------------------------------------------------------------------------
/grasshopper_mcp/prompts/__init__.py:
--------------------------------------------------------------------------------

```python
1 | 
```

--------------------------------------------------------------------------------
/grasshopper_mcp/resources/__init__.py:
--------------------------------------------------------------------------------

```python
1 | 
```

--------------------------------------------------------------------------------
/grasshopper_mcp/rhino/__init__.py:
--------------------------------------------------------------------------------

```python
1 | 
```

--------------------------------------------------------------------------------
/grasshopper_mcp/tools/__init__.py:
--------------------------------------------------------------------------------

```python
1 | 
```

--------------------------------------------------------------------------------
/grasshopper_mcp/utils/__init__.py:
--------------------------------------------------------------------------------

```python
1 | 
```

--------------------------------------------------------------------------------
/main.py:
--------------------------------------------------------------------------------

```python
1 | def main():
2 |     print("Hello from gh-mcp-server!")
3 | 
4 | 
5 | if __name__ == "__main__":
6 |     main()
7 | 
```

--------------------------------------------------------------------------------
/run_server.py:
--------------------------------------------------------------------------------

```python
 1 | #!/usr/bin/env python
 2 | import sys
 3 | import os
 4 | 
 5 | # Set up the Python path to find the package
 6 | sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
 7 | 
 8 | # Import and run the server module
 9 | from grasshopper_mcp.server import main
10 | 
11 | if __name__ == "__main__":
12 |     main()
13 | 
```

--------------------------------------------------------------------------------
/examples/test_CL_GH.py:
--------------------------------------------------------------------------------

```python
 1 | import Rhino
 2 | import rhinoscriptsyntax as rs
 3 | import scriptcontext as sc
 4 | import System
 5 | from Rhino.Geometry import *
 6 | 
 7 | 
 8 | def main():
 9 |     center = Point3d(10, 50, 0)
10 |     circle = Circle(Plane.WorldXY, center, 20)
11 |     return circle
12 | 
13 | 
14 | if __name__ == "__main__":
15 |     circle = main()
16 | 
```

--------------------------------------------------------------------------------
/examples/rhinopython.py:
--------------------------------------------------------------------------------

```python
 1 | import rhinoscriptsyntax as rs
 2 | import ghpythonlib.components as ghcomp
 3 | import scriptcontext
 4 | 
 5 | #points = rs.GetPoints(True, True)
 6 | #if points:
 7 | #    curves = ghcomp.Voronoi(points)
 8 | #    for curve in curves:
 9 | #        scriptcontext.doc.Objects.AddCurve(curve)
10 | #    for point in points:
11 | #        scriptcontext.doc.Objects.AddPoint(point)
12 | #    scriptcontext.doc.Views.Redraw()
13 |     
14 | p=ghcomp.ConstructPoint(0,0,0)
15 | scriptcontext.doc.Objects.AddPoint(p)
16 | #scriptcontext.doc.Views.Redraw()
```

--------------------------------------------------------------------------------
/examples/test_GH.py:
--------------------------------------------------------------------------------

```python
 1 | # Grasshopper Python Component: CreateCircle
 2 | # Generated from prompt: Create a circle with center point at coordinates (10,20,30) and radius of 50
 3 | 
 4 | import Rhino
 5 | import rhinoscriptsyntax as rs
 6 | import scriptcontext as sc
 7 | import Rhino.Geometry as rg
 8 | import ghpythonlib.components as ghcomp
 9 | import math
10 | 
11 | # Create a circle based on prompt: Create a circle with center point at coordinates (10,20,30) and radius of 50
12 | center = rg.Point3d(10, 20, 30)
13 | circle = rg.Circle(rg.Plane.WorldXY, center, 50)
14 | print("Created a circle!")
15 | 
```

--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------

```toml
 1 | [build-system]
 2 | requires = ["setuptools>=42", "wheel"]
 3 | build-backend = "setuptools.build_meta"
 4 | 
 5 | [project]
 6 | name = "grasshopper-mcp"
 7 | version = "0.1.0"
 8 | description = "MCP server for Grasshopper 3D modeling"
 9 | readme = "README.md"
10 | requires-python = ">=3.10"
11 | license = {text = "MIT"}
12 | dependencies = [
13 |     "mcp>=1.4.0",
14 |     "rhinoinside; platform_system=='Windows'",  # For Windows
15 |     "rhino3dm>=7.15.0",                        
16 |     "requests",
17 |     "python-dotenv",
18 | ]
19 | 
20 | [project.optional-dependencies]
21 | dev = [
22 |     "pytest",
23 |     "black",
24 |     "isort",
25 | ]
26 | 
27 | [project.scripts]
28 | grasshopper-mcp = "grasshopper_mcp.server:main"
29 | 
```

--------------------------------------------------------------------------------
/grasshopper_mcp/prompts/templates.py:
--------------------------------------------------------------------------------

```python
 1 | from mcp.server.fastmcp import FastMCP
 2 | import mcp.types as types
 3 | 
 4 | 
 5 | def register_prompts(mcp: FastMCP) -> None:
 6 |     """Register prompt templates with the MCP server."""
 7 | 
 8 |     @mcp.prompt()
 9 |     def create_parametric_model(component_description: str) -> str:
10 |         """Create a parametric model based on a description."""
11 |         return f"""
12 | Please help me create a parametric 3D model based on this description:
13 | 
14 | {component_description}
15 | 
16 | 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.
17 | """
18 | 
```

--------------------------------------------------------------------------------
/examples/test_rhino3dm.py:
--------------------------------------------------------------------------------

```python
 1 | import rhino3dm
 2 | 
 3 | model = rhino3dm.File3dm()
 4 | 
 5 | # create geometry
 6 | sphere1 = rhino3dm.Sphere(rhino3dm.Point3d(0, 0, 0), 10)
 7 | sphere2 = rhino3dm.Sphere(rhino3dm.Point3d(10, 10, 10), 4)
 8 | geometry = (sphere1.ToBrep(), sphere2.ToBrep())
 9 | 
10 | # create attributes
11 | attr1 = rhino3dm.ObjectAttributes()
12 | attr1.Name = "Sphere 1"
13 | attr2 = rhino3dm.ObjectAttributes()
14 | attr2.Name = "Sphere 2"
15 | attributes = (attr1, attr2)
16 | basepoint = rhino3dm.Point3d(0, 0, 0)
17 | 
18 | # create idef
19 | index = model.InstanceDefinitions.Add("name", "description", "url", "urltag", basepoint, geometry, attributes)
20 | print("Index of new idef: " + str(index))
21 | 
22 | # create iref
23 | idef = model.InstanceDefinitions.FindIndex(index)
24 | xf = rhino3dm.Transform(10.00)
25 | iref = rhino3dm.InstanceReference(idef.Id, xf)
26 | uuid = model.Objects.Add(iref, None)
27 | print("id of new iref: " + str(uuid))
28 | 
29 | # save file
30 | model.Write("./examples/test_rhino3dm.3dm", 7)
31 | 
```

--------------------------------------------------------------------------------
/grasshopper_mcp/tools/rhino_code_gen.py:
--------------------------------------------------------------------------------

```python
 1 | from mcp.server.fastmcp import FastMCP
 2 | from typing import Dict, Optional, Any
 3 | 
 4 | 
 5 | def register_rhino_code_generation_tools(mcp: FastMCP) -> None:
 6 |     """Register code generation tools with the MCP server."""
 7 | 
 8 |     @mcp.tool()
 9 |     async def generate_rhino_code(prompt: str, parameters: Optional[Dict[str, Any]] = None) -> str:
10 |         """Generate and execute Rhino Python code based on a description.
11 | 
12 |         Args:
13 |             prompt: Description of what you want the code to do
14 |             parameters: Optional parameters to use in the code generation
15 | 
16 |         Returns:
17 |             Result of the operation
18 |         """
19 |         ctx = mcp.get_context()
20 |         rhino = ctx.request_context.lifespan_context.rhino
21 | 
22 |         result = await rhino.generate_and_execute_rhino_code(prompt, parameters)
23 | 
24 |         if result["result"] == "error":
25 |             return f"Error generating or executing code: {result['error']}"
26 | 
27 |         return f"""Generated and executed code successfully:
28 | {result['code']}"""
29 | 
```

--------------------------------------------------------------------------------
/examples/compute_line.py:
--------------------------------------------------------------------------------

```python
 1 | import compute_rhino3d.Util
 2 | import compute_rhino3d.Grasshopper as gh
 3 | import rhino3dm
 4 | import json
 5 | 
 6 | compute_rhino3d.Util.url = "http://localhost:8000/"
 7 | # compute_rhino3d.Util.apiKey = ""
 8 | 
 9 | pt1 = rhino3dm.Point3d(0, 0, 0)
10 | circle = rhino3dm.Circle(pt1, 5)
11 | angle = 20
12 | 
13 | # convert circle to curve and stringify
14 | curve = json.dumps(circle.ToNurbsCurve().Encode())
15 | 
16 | # create list of input trees
17 | curve_tree = gh.DataTree("curve")
18 | curve_tree.Append([0], [curve])
19 | rotate_tree = gh.DataTree("rotate")
20 | rotate_tree.Append([0], [angle])
21 | trees = [curve_tree, rotate_tree]
22 | 
23 | output = gh.EvaluateDefinition("twisty.gh", trees)
24 | print(output)
25 | 
26 | # decode results
27 | branch = output["values"][0]["InnerTree"]["{0;0}"]
28 | lines = [rhino3dm.CommonObject.Decode(json.loads(item["data"])) for item in branch]
29 | 
30 | filename = "twisty.3dm"
31 | 
32 | print("Writing {} lines to {}".format(len(lines), filename))
33 | 
34 | # create a 3dm file with results
35 | model = rhino3dm.File3dm()
36 | for l in lines:
37 |     model.Objects.AddCurve(l)  # they're actually LineCurves...
38 | 
39 | model.Write(filename)
40 | 
```

--------------------------------------------------------------------------------
/grasshopper_mcp/config.py:
--------------------------------------------------------------------------------

```python
 1 | import os
 2 | from dataclasses import dataclass
 3 | from typing import Optional
 4 | 
 5 | 
 6 | @dataclass
 7 | class ServerConfig:
 8 |     """Server configuration."""
 9 | 
10 |     # Rhino/Grasshopper configuration
11 |     rhino_path: Optional[str] = None  # Path to Rhino installation
12 |     use_compute_api: bool = False  # Whether to use compute.rhino3d.com
13 |     use_rhino3dm: bool = False  # Whether to use rhino3dm library
14 |     compute_url: Optional[str] = None  # Compute API URL
15 |     compute_api_key: Optional[str] = None  # Compute API key
16 | 
17 |     # Server configuration
18 |     server_name: str = "Grasshopper MCP"
19 |     server_port: int = 8080
20 | 
21 |     @classmethod
22 |     def from_env(cls) -> "ServerConfig":
23 |         """Create configuration from environment variables."""
24 |         use_compute = os.getenv("USE_COMPUTE_API", "false").lower() == "true"
25 |         use_rhino3dm = os.getenv("USE_RHINO3DM", "true").lower() == "true"
26 | 
27 |         return cls(
28 |             rhino_path=os.getenv("RHINO_PATH"),
29 |             use_compute_api=use_compute,
30 |             use_rhino3dm=use_rhino3dm,
31 |             compute_url=os.getenv("COMPUTE_URL"),
32 |             compute_api_key=os.getenv("COMPUTE_API_KEY"),
33 |             server_name=os.getenv("SERVER_NAME", "Grasshopper MCP"),
34 |             server_port=int(os.getenv("SERVER_PORT", "8080")),
35 |         )
36 | 
```

--------------------------------------------------------------------------------
/grasshopper_mcp/prompts/grasshopper_prompts.py:
--------------------------------------------------------------------------------

```python
 1 | from mcp.server.fastmcp import FastMCP
 2 | 
 3 | 
 4 | def register_grasshopper_code_prompts(mcp: FastMCP) -> None:
 5 |     """Register prompt templates with the MCP server."""
 6 | 
 7 |     @mcp.prompt()
 8 |     def grasshopper_GHpython_generation_prompt(task_description: str) -> str:
 9 |         """Creates a prompt template for generating Grasshopper Python code with proper imports and grammar."""
10 |         return """
11 | When writing Python code for Grasshopper, please follow these guidelines:
12 | 
13 | 0. Add "Used the prompts from mcp.prompt()" at the beginning of python file.
14 | 
15 | 1. Always start by including the following import statements:
16 | ```python
17 | import Rhino.Geometry as rg
18 | import ghpythonlib.components as ghcomp
19 | import rhinoscriptsyntax as rs
20 | 
21 | 2. Structure your code with the following sections:
22 | Import statements at the top
23 | Global variables and constants
24 | Function definitions if needed
25 | Main execution code
26 | 
27 | 3. Use descriptive variable names that follow Python naming conventions
28 | Use snake_case for variables and functions
29 | Use UPPER_CASE for constants
30 | 
31 | 4. Include comments explaining complex logic or non-obvious operations
32 | 5. Carefully check grammar in all comments and docstrings
33 | 6. Ensure proper indentation and consistent code style
34 | 7. Use proper error handling when appropriate
35 | 8. Optimize for Grasshopper's data tree structure when handling multiple data items
36 | 9. Save the output to "result".
37 | """
38 | 
```

--------------------------------------------------------------------------------
/grasshopper_mcp/utils/request.py:
--------------------------------------------------------------------------------

```python
 1 | import socket
 2 | import json
 3 | import tempfile
 4 | import os
 5 | 
 6 | 
 7 | def test_codelistener_with_file(host="127.0.0.1", port=614):
 8 |     """Test CodeListener by creating a temporary file and sending its path."""
 9 |     try:
10 |         # Create a temporary Python file
11 |         fd, temp_path = tempfile.mkstemp(suffix=".py")
12 |         try:
13 |             # Write Python code to the file
14 |             with os.fdopen(fd, "w") as f:
15 |                 f.write(
16 |                     """
17 | import Rhino
18 | import scriptcontext as sc
19 | 
20 | # Get Rhino version
21 | version = Rhino.RhinoApp.Version
22 | print("Hello from CodeListener!")
23 | print("Rhino version: ", version)
24 | 
25 | # Access the active document
26 | doc = sc.doc
27 | if doc is not None:
28 |     print("Active document: ", doc.Name)
29 | else:
30 |     print("No active document")
31 | """
32 |                 )
33 | 
34 |             # Create JSON message object
35 |             msg_obj = {"filename": temp_path, "run": True, "reset": False, "temp": True}
36 | 
37 |             # Convert to JSON
38 |             json_msg = json.dumps(msg_obj)
39 | 
40 |             # Connect to CodeListener
41 |             sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
42 |             sock.settimeout(10)
43 |             sock.connect((host, port))
44 | 
45 |             # Send the JSON message
46 |             print(f"Sending request to execute file: {temp_path}")
47 |             sock.sendall(json_msg.encode("utf-8"))
48 | 
49 |             # Receive response
50 |             print("Waiting for response...")
51 |             response = sock.recv(4096).decode("utf-8")
52 |             print(f"Response received: {response}")
53 | 
54 |             sock.close()
55 |             return True
56 | 
57 |         finally:
58 |             # Clean up - remove temporary file
59 |             try:
60 |                 os.unlink(temp_path)
61 |             except:
62 |                 pass
63 | 
64 |     except Exception as e:
65 |         print(f"Error: {e}")
66 |         return False
67 | 
68 | 
69 | # Run the test
70 | if __name__ == "__main__":
71 |     test_codelistener_with_file()
72 | 
```

--------------------------------------------------------------------------------
/grasshopper_mcp/server.py:
--------------------------------------------------------------------------------

```python
 1 | from contextlib import asynccontextmanager
 2 | from dataclasses import dataclass
 3 | from typing import AsyncIterator
 4 | import os
 5 | 
 6 | from mcp.server.fastmcp import Context, FastMCP
 7 | from dotenv import load_dotenv
 8 | 
 9 | from grasshopper_mcp.rhino.connection import RhinoConnection
10 | from grasshopper_mcp.config import ServerConfig
11 | 
12 | 
13 | import sys
14 | 
15 | print("Rhino MCP Server starting up...", file=sys.stderr)
16 | 
17 | load_dotenv()  # Load environment variables from .env file
18 | 
19 | 
20 | @dataclass
21 | class AppContext:
22 |     """Application context with initialized connections."""
23 | 
24 |     rhino: RhinoConnection
25 |     config: ServerConfig
26 | 
27 | 
28 | @asynccontextmanager
29 | async def app_lifespan(server: FastMCP) -> AsyncIterator[AppContext]:
30 |     """Initialize and manage server resources."""
31 |     # Load configuration
32 |     config = ServerConfig.from_env()
33 | 
34 |     # Initialize Rhino/Grasshopper connection
35 |     rhino_connection = RhinoConnection(config)
36 |     try:
37 |         await rhino_connection.initialize()
38 | 
39 |         # Provide context to request handlers
40 |         yield AppContext(rhino=rhino_connection, config=config)
41 |     finally:
42 |         # Cleanup on shutdown
43 |         await rhino_connection.close()
44 | 
45 | 
46 | # Create the MCP server
47 | mcp = FastMCP("Grasshopper 3D Modeling", lifespan=app_lifespan)
48 | 
49 | # Import tool definitions
50 | from grasshopper_mcp.tools.modeling import register_modeling_tools
51 | from grasshopper_mcp.tools.analysis import register_analysis_tools
52 | from grasshopper_mcp.resources.model_data import register_model_resources
53 | from grasshopper_mcp.tools.grasshopper import register_grasshopper_tools
54 | from grasshopper_mcp.tools.advanced_grasshopper import register_advanced_grasshopper_tools
55 | from grasshopper_mcp.tools.rhino_code_gen import register_rhino_code_generation_tools
56 | 
57 | # Import prompt definitions
58 | from grasshopper_mcp.prompts.grasshopper_prompts import register_grasshopper_code_prompts
59 | 
60 | # Register tools
61 | register_modeling_tools(mcp)
62 | register_analysis_tools(mcp)
63 | register_model_resources(mcp)
64 | 
65 | register_grasshopper_tools(mcp)
66 | 
67 | # register_advanced_grasshopper_tools(mcp)
68 | # register_rhino_code_generation_tools(mcp)
69 | 
70 | # Register prompts
71 | register_grasshopper_code_prompts(mcp)
72 | 
73 | 
74 | def main():
75 |     """Run the server."""
76 |     mcp.run()
77 | 
78 | 
79 | if __name__ == "__main__":
80 |     main()
81 | 
```

--------------------------------------------------------------------------------
/scripts/install.py:
--------------------------------------------------------------------------------

```python
 1 | #!/usr/bin/env python3
 2 | """
 3 | Helper script to install the Grasshopper MCP server in Claude Desktop.
 4 | """
 5 | import os
 6 | import json
 7 | import platform
 8 | import argparse
 9 | import sys
10 | from pathlib import Path
11 | 
12 | 
13 | def get_config_path():
14 |     """Get the path to the Claude Desktop config file."""
15 |     if platform.system() == "Darwin":  # macOS
16 |         return os.path.expanduser("~/Library/Application Support/Claude/claude_desktop_config.json")
17 |     elif platform.system() == "Windows":
18 |         return os.path.join(os.environ.get("APPDATA", ""), "Claude", "claude_desktop_config.json")
19 |     else:
20 |         print("Unsupported platform. Only macOS and Windows are supported.")
21 |         sys.exit(1)
22 | 
23 | 
24 | def main():
25 |     parser = argparse.ArgumentParser(description="Install Grasshopper MCP server in Claude Desktop")
26 |     parser.add_argument("--name", default="grasshopper", help="Name for the server in Claude Desktop")
27 |     args = parser.parse_args()
28 | 
29 |     # Get the path to this script's directory
30 |     script_dir = Path(__file__).parent.absolute()
31 |     project_dir = script_dir.parent
32 | 
33 |     # Get the path to the server script
34 |     server_script = project_dir / "grasshopper_mcp" / "server.py"
35 | 
36 |     if not server_script.exists():
37 |         print(f"Server script not found at {server_script}")
38 |         sys.exit(1)
39 | 
40 |     config_path = get_config_path()
41 |     config_dir = os.path.dirname(config_path)
42 | 
43 |     # Create config directory if it doesn't exist
44 |     os.makedirs(config_dir, exist_ok=True)
45 | 
46 |     # Load existing config or create new one
47 |     if os.path.exists(config_path):
48 |         with open(config_path, "r") as f:
49 |             try:
50 |                 config = json.load(f)
51 |             except json.JSONDecodeError:
52 |                 config = {}
53 |     else:
54 |         config = {}
55 | 
56 |     # Ensure mcpServers exists
57 |     if "mcpServers" not in config:
58 |         config["mcpServers"] = {}
59 | 
60 |     # Add our server
61 |     python_path = sys.executable
62 |     config["mcpServers"][args.name] = {"command": python_path, "args": [str(server_script)]}
63 | 
64 |     # Write updated config
65 |     with open(config_path, "w") as f:
66 |         json.dump(config, f, indent=2)
67 | 
68 |     print(f"Grasshopper MCP server installed as '{args.name}' in Claude Desktop")
69 |     print(f"Configuration written to: {config_path}")
70 | 
71 | 
72 | if __name__ == "__main__":
73 |     main()
74 | 
```

--------------------------------------------------------------------------------
/examples/test_CodeListener.py:
--------------------------------------------------------------------------------

```python
  1 | # import os
  2 | # import sys
  3 | 
  4 | # print(1)
  5 | 
  6 | 
  7 | # scriptcontext_path = os.path.join(
  8 | #     os.environ["APPDATA"],
  9 | #     "McNeel",
 10 | #     "Rhinoceros",
 11 | #     "7.0",
 12 | #     "Plug-ins",
 13 | #     "IronPython (814d908a-e25c-493d-97e9-ee3861957f49)",
 14 | #     "settings",
 15 | #     "lib",
 16 | # )
 17 | 
 18 | # sys.path.append(scriptcontext_path)
 19 | 
 20 | # # import rhinoinside
 21 | 
 22 | # # rhinoinside.load()
 23 | # import rhinoscriptsyntax as rs
 24 | # import scriptcontext as sc
 25 | # import Rhino
 26 | 
 27 | ################################################################################
 28 | # SampleAddNurbsCurves.py
 29 | # Copyright (c) 2017 Robert McNeel & Associates.
 30 | # See License.md in the root of this repository for details.
 31 | ################################################################################
 32 | import Rhino
 33 | import scriptcontext
 34 | 
 35 | 
 36 | def SampleAddNurbsCurves():
 37 | 
 38 |     # World 3-D, or Euclidean, locations
 39 |     pt0 = Rhino.Geometry.Point3d(-8.0, -3.0, 0.0)
 40 |     pt1 = Rhino.Geometry.Point3d(-4.0, 3.0, 2.0)
 41 |     pt2 = Rhino.Geometry.Point3d(4.0, 3.0, 2.0)
 42 |     pt3 = Rhino.Geometry.Point3d(8.0, -3.0, 0.0)
 43 | 
 44 |     # Create NURBS curve:
 45 |     #   Dimension = 3
 46 |     #   Rational = False
 47 |     #   Order (Degree + 1) = 4
 48 |     #   Control point count = 4
 49 |     #   Knot count = Control point count + degree - 1
 50 |     nc0 = Rhino.Geometry.NurbsCurve(3, False, 4, 4)
 51 |     # World 3-D, or Euclidean, control points,
 52 |     nc0.Points[0] = Rhino.Geometry.ControlPoint(pt0)
 53 |     nc0.Points[1] = Rhino.Geometry.ControlPoint(pt1)
 54 |     nc0.Points[2] = Rhino.Geometry.ControlPoint(pt2)
 55 |     nc0.Points[3] = Rhino.Geometry.ControlPoint(pt3)
 56 |     # Clamped knots
 57 |     nc0.Knots[0] = 0
 58 |     nc0.Knots[1] = 0
 59 |     nc0.Knots[2] = 0
 60 |     nc0.Knots[3] = 1
 61 |     nc0.Knots[4] = 1
 62 |     nc0.Knots[5] = 1
 63 | 
 64 |     # Create NURBS curve:
 65 |     #   Dimension = 3
 66 |     #   Rational = True
 67 |     #   Order (Degree + 1) = 4
 68 |     #   Control point count = 4
 69 |     #   Knot count = Control point count + degree - 1
 70 |     nc1 = Rhino.Geometry.NurbsCurve(3, True, 4, 4)
 71 |     # Control points from a world 3-D, or Euclidean, locations and a weight
 72 |     nc1.Points[0] = Rhino.Geometry.ControlPoint(pt0, 1.0)
 73 |     nc1.Points[1] = Rhino.Geometry.ControlPoint(pt1, 2.0)
 74 |     nc1.Points[2] = Rhino.Geometry.ControlPoint(pt2, 4.0)
 75 |     nc1.Points[3] = Rhino.Geometry.ControlPoint(pt3, 1.0)
 76 |     # Clamped knots
 77 |     nc1.Knots[0] = 0
 78 |     nc1.Knots[1] = 0
 79 |     nc1.Knots[2] = 0
 80 |     nc1.Knots[3] = 1
 81 |     nc1.Knots[4] = 1
 82 |     nc1.Knots[5] = 1
 83 | 
 84 |     # Create NURBS curve:
 85 |     #   Dimension = 3
 86 |     #   Rational = True
 87 |     #   Order (Degree + 1) = 4
 88 |     #   Control point count = 4
 89 |     #   Knot count = Control point count + degree - 1
 90 |     nc2 = Rhino.Geometry.NurbsCurve(3, True, 4, 4)
 91 |     # Homogeneous control points
 92 |     nc2.Points[0] = Rhino.Geometry.ControlPoint(-8.0, -3.0, 0.0, 1.0)
 93 |     nc2.Points[1] = Rhino.Geometry.ControlPoint(-4.0, 3.0, 2.0, 2.0)
 94 |     nc2.Points[2] = Rhino.Geometry.ControlPoint(4.0, 3.0, 2.0, 4.0)
 95 |     nc2.Points[3] = Rhino.Geometry.ControlPoint(8.0, -3.0, 0.0, 1.0)
 96 |     # Clamped knots
 97 |     nc2.Knots[0] = 0
 98 |     nc2.Knots[1] = 0
 99 |     nc2.Knots[2] = 0
100 |     nc2.Knots[3] = 1
101 |     nc2.Knots[4] = 1
102 |     nc2.Knots[5] = 1
103 | 
104 |     # Add to document
105 |     scriptcontext.doc.Objects.Add(nc0)
106 |     scriptcontext.doc.Objects.Add(nc1)
107 |     scriptcontext.doc.Objects.Add(nc2)
108 |     scriptcontext.doc.Views.Redraw()
109 | 
110 | 
111 | # Check to see if this file is being executed as the "main" python
112 | # script instead of being used as a module by some other python script
113 | # This allows us to use the module which ever way we want.
114 | if __name__ == "__main__":
115 |     SampleAddNurbsCurves()
116 | 
117 | 
118 | # rhino_path = "C:\\Program Files\\Rhino 7\\System"
119 | # sys.path.append(rhino_path)
120 | # # print(sys.path)
121 | # # import scriptcontext as sc
122 | 
123 | # # import rhinoscriptsyntax as rs
124 | # import rhinoinside
125 | 
126 | # rhinoinside.load()
127 | # print("rhinoinside installed.")
128 | # # Import Rhino components
129 | # import Rhino
130 | 
131 | # print("Rhino installed.")
132 | 
```

--------------------------------------------------------------------------------
/grasshopper_mcp/tools/analysis.py:
--------------------------------------------------------------------------------

```python
  1 | from mcp.server.fastmcp import FastMCP, Context
  2 | 
  3 | 
  4 | def register_analysis_tools(mcp: FastMCP) -> None:
  5 |     """Register analysis tools with the MCP server."""
  6 | 
  7 |     @mcp.tool()
  8 |     async def analyze_rhino_file(file_path: str) -> str:
  9 |         """Analyze a Rhino (.3dm) file.
 10 | 
 11 |         Args:
 12 |             file_path: Path to the .3dm file
 13 | 
 14 |         Returns:
 15 |             Analysis of the file contents
 16 |         """
 17 |         # Get context using the FastMCP mechanism
 18 |         ctx = mcp.get_context()
 19 |         rhino = ctx.request_context.lifespan_context.rhino
 20 | 
 21 |         result = await rhino.read_3dm_file(file_path)
 22 | 
 23 |         if result["result"] == "error":
 24 |             return f"Error: {result['error']}"
 25 | 
 26 |         model = result["model"]
 27 | 
 28 |         # Use r3d directly for rhino3dm mode
 29 |         if rhino.rhino_instance.get("use_rhino3dm", False):
 30 |             r3d = rhino.rhino_instance["r3d"]
 31 | 
 32 |             # Collect file information
 33 |             info = {
 34 |                 "unit_system": str(model.Settings.ModelUnitSystem),
 35 |                 "object_count": len(model.Objects),
 36 |                 "layer_count": len(model.Layers),
 37 |             }
 38 | 
 39 |             # Get object types
 40 |             object_types = {}
 41 |             for obj in model.Objects:
 42 |                 geom = obj.Geometry
 43 |                 if geom:
 44 |                     geom_type = str(geom.ObjectType)
 45 |                     object_types[geom_type] = object_types.get(geom_type, 0) + 1
 46 | 
 47 |             info["object_types"] = object_types
 48 | 
 49 |             # Format output
 50 |             output = [f"Analysis of {file_path}:"]
 51 |             output.append(f"- Unit System: {info['unit_system']}")
 52 |             output.append(f"- Total Objects: {info['object_count']}")
 53 |             output.append(f"- Total Layers: {info['layer_count']}")
 54 |             output.append("- Object Types:")
 55 |             for obj_type, count in info["object_types"].items():
 56 |                 output.append(f"  - {obj_type}: {count}")
 57 | 
 58 |             return "\n".join(output)
 59 |         else:
 60 |             # RhinoInside mode (Windows)
 61 |             # Similar implementation using Rhino SDK
 62 |             return "RhinoInside implementation not provided"
 63 | 
 64 |     @mcp.tool()
 65 |     async def list_objects(file_path: str) -> str:
 66 |         """List all objects in a Rhino file.
 67 | 
 68 |         Args:
 69 |             file_path: Path to the .3dm file
 70 | 
 71 |         Returns:
 72 |             Information about objects in the file
 73 |         """
 74 |         ctx = mcp.get_context()
 75 |         rhino = ctx.request_context.lifespan_context.rhino
 76 | 
 77 |         result = await rhino.read_3dm_file(file_path)
 78 | 
 79 |         if result["result"] == "error":
 80 |             return f"Error: {result['error']}"
 81 | 
 82 |         model = result["model"]
 83 | 
 84 |         # Use rhino3dm for cross-platform support
 85 |         if rhino.rhino_instance.get("use_rhino3dm", False):
 86 |             # Gather object information
 87 |             objects_info = []
 88 |             for i, obj in enumerate(model.Objects):
 89 |                 geom = obj.Geometry
 90 |                 if geom:
 91 |                     attrs = obj.Attributes
 92 |                     name = attrs.Name or f"Object {i}"
 93 |                     layer_index = attrs.LayerIndex
 94 | 
 95 |                     # Get layer name if available
 96 |                     layer_name = "Unknown"
 97 |                     if 0 <= layer_index < len(model.Layers):
 98 |                         layer_name = model.Layers[layer_index].Name
 99 | 
100 |                     obj_info = {"name": name, "type": str(geom.ObjectType), "layer": layer_name, "index": i}
101 |                     objects_info.append(obj_info)
102 | 
103 |             # Format output
104 |             output = [f"Objects in {file_path}:"]
105 |             for info in objects_info:
106 |                 output.append(f"{info['index']}. {info['name']} (Type: {info['type']}, Layer: {info['layer']})")
107 | 
108 |             return "\n".join(output)
109 |         else:
110 |             # RhinoInside mode
111 |             return "RhinoInside implementation not provided"
112 | 
```

--------------------------------------------------------------------------------
/grasshopper_mcp/resources/model_data.py:
--------------------------------------------------------------------------------

```python
  1 | from mcp.server.fastmcp import FastMCP
  2 | 
  3 | 
  4 | def register_model_resources(mcp: FastMCP) -> None:
  5 |     """Register model data resources with the MCP server."""
  6 | 
  7 |     @mcp.resource("rhino://{file_path}")
  8 |     async def get_rhino_file_info(file_path: str) -> str:
  9 |         """Get information about a Rhino file."""
 10 |         ctx = mcp.get_context()
 11 |         rhino = ctx.request_context.lifespan_context.rhino
 12 | 
 13 |         result = await rhino.read_3dm_file(file_path)
 14 | 
 15 |         if result["result"] == "error":
 16 |             return f"Error: {result['error']}"
 17 | 
 18 |         model = result["model"]
 19 | 
 20 |         # Use rhino3dm
 21 |         if rhino.rhino_instance.get("use_rhino3dm", False):
 22 |             # Basic file information
 23 |             info = {
 24 |                 "file_path": file_path,
 25 |                 "unit_system": str(model.Settings.ModelUnitSystem),
 26 |                 "object_count": len(model.Objects),
 27 |                 "layer_count": len(model.Layers),
 28 |                 "material_count": len(model.Materials),
 29 |                 "notes": model.Notes or "No notes",
 30 |             }
 31 | 
 32 |             # Format output
 33 |             output = [f"# Rhino File: {file_path}"]
 34 |             output.append(f"- Unit System: {info['unit_system']}")
 35 |             output.append(f"- Objects: {info['object_count']}")
 36 |             output.append(f"- Layers: {info['layer_count']}")
 37 |             output.append(f"- Materials: {info['material_count']}")
 38 |             output.append(f"- Notes: {info['notes']}")
 39 | 
 40 |             return "\n".join(output)
 41 |         else:
 42 |             # RhinoInside mode
 43 |             return "RhinoInside implementation not provided"
 44 | 
 45 |     @mcp.resource("rhino://{file_path}/object/{index}")
 46 |     async def get_object_info(file_path: str, index: int) -> str:
 47 |         """Get information about a specific object in a Rhino file."""
 48 |         ctx = mcp.get_context()
 49 |         rhino = ctx.request_context.lifespan_context.rhino
 50 | 
 51 |         result = await rhino.read_3dm_file(file_path)
 52 | 
 53 |         if result["result"] == "error":
 54 |             return f"Error: {result['error']}"
 55 | 
 56 |         model = result["model"]
 57 |         index = int(index)  # Convert to integer
 58 | 
 59 |         # Check if index is valid
 60 |         if index < 0 or index >= len(model.Objects):
 61 |             return f"Error: Invalid object index. File has {len(model.Objects)} objects."
 62 | 
 63 |         # Use rhino3dm
 64 |         if rhino.rhino_instance.get("use_rhino3dm", False):
 65 |             r3d = rhino.rhino_instance["r3d"]
 66 |             obj = model.Objects[index]
 67 |             geom = obj.Geometry
 68 |             attrs = obj.Attributes
 69 | 
 70 |             # Basic object information
 71 |             info = {
 72 |                 "name": attrs.Name or f"Object {index}",
 73 |                 "type": str(geom.ObjectType),
 74 |                 "layer_index": attrs.LayerIndex,
 75 |                 "material_index": attrs.MaterialIndex,
 76 |                 "visible": not attrs.IsHidden,
 77 |             }
 78 | 
 79 |             # Get bounding box
 80 |             bbox = geom.BoundingBox if hasattr(geom, "BoundingBox") else geom.GetBoundingBox()
 81 |             if bbox:
 82 |                 info["bounding_box"] = {
 83 |                     "min": [bbox.Min.X, bbox.Min.Y, bbox.Min.Z],
 84 |                     "max": [bbox.Max.X, bbox.Max.Y, bbox.Max.Z],
 85 |                 }
 86 | 
 87 |             # Type-specific properties
 88 |             if hasattr(geom, "ObjectType"):
 89 |                 if geom.ObjectType == r3d.ObjectType.Curve:
 90 |                     info["length"] = geom.GetLength() if hasattr(geom, "GetLength") else "Unknown"
 91 |                     info["is_closed"] = geom.IsClosed if hasattr(geom, "IsClosed") else "Unknown"
 92 |                 elif geom.ObjectType == r3d.ObjectType.Brep:
 93 |                     info["faces"] = len(geom.Faces) if hasattr(geom, "Faces") else "Unknown"
 94 |                     info["edges"] = len(geom.Edges) if hasattr(geom, "Edges") else "Unknown"
 95 |                     info["is_solid"] = geom.IsSolid if hasattr(geom, "IsSolid") else "Unknown"
 96 |                     info["volume"] = geom.GetVolume() if hasattr(geom, "GetVolume") else "Unknown"
 97 |                 elif geom.ObjectType == r3d.ObjectType.Mesh:
 98 |                     info["vertices"] = len(geom.Vertices) if hasattr(geom, "Vertices") else "Unknown"
 99 |                     info["faces"] = len(geom.Faces) if hasattr(geom, "Faces") else "Unknown"
100 | 
101 |             # Format output
102 |             output = [f"# Object {index}: {info['name']}"]
103 |             output.append(f"- Type: {info['type']}")
104 |             output.append(f"- Layer Index: {info['layer_index']}")
105 |             output.append(f"- Material Index: {info['material_index']}")
106 |             output.append(f"- Visible: {info['visible']}")
107 | 
108 |             if "bounding_box" in info:
109 |                 bbox = info["bounding_box"]
110 |                 output.append("- Bounding Box:")
111 |                 output.append(f"  - Min: ({bbox['min'][0]}, {bbox['min'][1]}, {bbox['min'][2]})")
112 |                 output.append(f"  - Max: ({bbox['max'][0]}, {bbox['max'][1]}, {bbox['max'][2]})")
113 | 
114 |             for key, value in info.items():
115 |                 if key not in ["name", "type", "layer_index", "material_index", "visible", "bounding_box"]:
116 |                     output.append(f"- {key.replace('_', ' ').title()}: {value}")
117 | 
118 |             return "\n".join(output)
119 |         else:
120 |             # RhinoInside mode
121 |             return "RhinoInside implementation not provided"
122 | 
```

--------------------------------------------------------------------------------
/examples/zaha01.py:
--------------------------------------------------------------------------------

```python
  1 | import Rhino.Geometry as rg
  2 | import ghpythonlib.components as ghcomp
  3 | import math
  4 | import random
  5 | 
  6 | # === INPUT PARAMETERS ===
  7 | height = 200.0           # Total height of the tower
  8 | base_radius = 30.0       # Radius at the base of the tower
  9 | top_radius = 15.0        # Radius at the top of the tower
 10 | twist_angle = 75.0       # Total twist angle from base to top (degrees)
 11 | floors = 35              # Number of floors
 12 | curvature_factor = 0.3   # Factor controlling central spine curvature (0-1)
 13 | organic_factor = 0.4     # Factor controlling the organic deformation of floor plates (0-1)
 14 | 
 15 | # === OUTPUTS ===
 16 | tower_surfaces = []      # Collection of surfaces forming the tower
 17 | floor_curves = []        # Collection of floor plate curves
 18 | central_spine = None     # Central spine curve
 19 | 
 20 | # === HELPER FUNCTIONS ===
 21 | def create_organic_floor_curve(center, radius, segments, organic_factor, phase):
 22 |     """
 23 |     Creates an organic floor curve with controlled deformation.
 24 |     
 25 |     Args:
 26 |         center: Center point of the floor curve
 27 |         radius: Base radius of the floor curve
 28 |         segments: Number of segments for the curve (smoothness)
 29 |         organic_factor: Amount of organic deformation (0-1)
 30 |         phase: Phase shift for the organic deformation pattern
 31 |         
 32 |     Returns:
 33 |         A closed curve representing the floor shape
 34 |     """
 35 |     points = []
 36 |     for i in range(segments):
 37 |         angle = (math.pi * 2.0 * i) / segments
 38 |         
 39 |         # Create organic variation using multiple sine waves with different frequencies
 40 |         variation = 1.0 + organic_factor * (
 41 |             0.4 * math.sin(angle * 2 + phase) + 
 42 |             0.3 * math.sin(angle * 3 + phase * 1.7) +
 43 |             0.2 * math.sin(angle * 5 + phase * 0.8)
 44 |         )
 45 |         
 46 |         # Calculate point coordinates
 47 |         x = center.X + radius * variation * math.cos(angle)
 48 |         y = center.Y + radius * variation * math.sin(angle)
 49 |         point = rg.Point3d(x, y, center.Z)
 50 |         points.append(point)
 51 |     
 52 |     # Close the curve by adding the first point again
 53 |     points.append(points[0])
 54 |     
 55 |     # Create interpolated curve through points
 56 |     # Degree 3 for smooth, flowing curves characteristic of Zaha Hadid's work
 57 |     return rg.Curve.CreateInterpolatedCurve(points, 3)
 58 | 
 59 | def ease_in_out(t):
 60 |     """
 61 |     Provides a smooth ease-in-out interpolation.
 62 |     Used for more natural transitions characteristic of Hadid's fluid forms.
 63 |     
 64 |     Args:
 65 |         t: Input value (0-1)
 66 |         
 67 |     Returns:
 68 |         Eased value (0-1)
 69 |     """
 70 |     return 0.5 - 0.5 * math.cos(t * math.pi)
 71 | 
 72 | # === MAIN ALGORITHM ===
 73 | 
 74 | # 1. Create the central spine with a gentle S-curve (Hadid's sinuous forms)
 75 | spine_points = []
 76 | for i in range(floors + 1):
 77 |     # Calculate height position
 78 |     z = i * (height / floors)
 79 |     t = z / height  # Normalized height (0-1)
 80 |     
 81 |     # Create an S-curve using sine function
 82 |     # This creates the flowing, undulating central spine typical in Hadid's work
 83 |     curve_x = math.sin(t * math.pi) * base_radius * curvature_factor
 84 |     curve_y = math.sin(t * math.pi * 0.5) * base_radius * curvature_factor * 0.7
 85 |     
 86 |     spine_points.append(rg.Point3d(curve_x, curve_y, z))
 87 | 
 88 | # Create a smooth interpolated curve through the spine points
 89 | central_spine = rg.Curve.CreateInterpolatedCurve(spine_points, 3)
 90 | 
 91 | # 2. Create floor curves with organic shapes and twisting
 92 | for i in range(floors + 1):
 93 |     # Calculate height position
 94 |     z = i * (height / floors)
 95 |     t = z / height  # Normalized height (0-1)
 96 |     
 97 |     # Get point on spine at this height
 98 |     spine_param = central_spine.Domain.ParameterAt(t)
 99 |     center = central_spine.PointAt(spine_param)
100 |     
101 |     # Calculate radius with smooth transition from base to top
102 |     # Using ease_in_out for more natural, fluid transition
103 |     eased_t = ease_in_out(t)
104 |     radius = base_radius * (1 - eased_t) + top_radius * eased_t
105 |     
106 |     # Add Hadid-like bulges at strategic points
107 |     if 0.3 < t < 0.7:
108 |         # Create a subtle bulge in the middle section
109 |         bulge_factor = math.sin((t - 0.3) * math.pi / 0.4) * 0.15
110 |         radius *= (1 + bulge_factor)
111 |     
112 |     # Calculate twist angle based on height
113 |     angle_rad = math.radians(twist_angle * t)
114 |     
115 |     # Create a plane for the floor curve
116 |     # First get the spine's tangent at this point
117 |     tangent = central_spine.TangentAt(spine_param)
118 |     tangent.Unitize()
119 |     
120 |     # Create perpendicular vectors for the plane
121 |     x_dir = rg.Vector3d.CrossProduct(tangent, rg.Vector3d.ZAxis)
122 |     if x_dir.Length < 0.001:
123 |         x_dir = rg.Vector3d.XAxis
124 |     x_dir.Unitize()
125 |     
126 |     y_dir = rg.Vector3d.CrossProduct(tangent, x_dir)
127 |     y_dir.Unitize()
128 |     
129 |     # Apply twist rotation
130 |     rotated_x = x_dir * math.cos(angle_rad) - y_dir * math.sin(angle_rad)
131 |     rotated_y = x_dir * math.sin(angle_rad) + y_dir * math.cos(angle_rad)
132 |     
133 |     floor_plane = rg.Plane(center, rotated_x, rotated_y)
134 |     
135 |     # Phase shift creates variation in organic patterns between floors
136 |     # This creates the flowing, continuous aesthetic of Hadid's work
137 |     phase_shift = t * 8.0
138 |     
139 |     # Create organic floor curve
140 |     segments = 24  # Number of segments for smoothness
141 |     curve = create_organic_floor_curve(floor_plane.Origin, radius, segments, 
142 |                                        organic_factor * (1 + 0.5 * math.sin(t * math.pi)), 
143 |                                        phase_shift)
144 |     
145 |     floor_curves.append(curve)
146 | 
147 | # 3. Create surfaces between floor curves
148 | for i in range(len(floor_curves) - 1):
149 |     # Create loft surface between consecutive floors
150 |     # Using Tight loft type for more fluid transitions
151 |     loft_curves = [floor_curves[i], floor_curves[i+1]]
152 |     loft_type = rg.LoftType.Tight
153 |     
154 |     try:
155 |         # Create loft surfaces
156 |         loft = ghcomp.Loft(loft_curves, loft_type)
157 |         if isinstance(loft, list):
158 |             tower_surfaces.extend(loft)
159 |         else:
160 |             tower_surfaces.append(loft)
161 |     except:
162 |         # Skip if loft creation fails
163 |         pass
164 | 
165 | # === ASSIGN OUTPUTS ===
166 | a = tower_surfaces  # Tower surfaces
167 | b = floor_curves    # Floor curves
168 | c = central_spine   # Central spine curve
```

--------------------------------------------------------------------------------
/grasshopper_mcp/tools/grasshopper.py:
--------------------------------------------------------------------------------

```python
  1 | from mcp.server.fastmcp import FastMCP
  2 | from typing import Dict, List, Any, Optional
  3 | 
  4 | 
  5 | def register_grasshopper_tools(mcp: FastMCP) -> None:
  6 |     """Register Grasshopper-specific tools with the MCP server."""
  7 | 
  8 |     #     @mcp.tool()
  9 |     #     async def generate_grasshopper_code(
 10 |     #         description: str,
 11 |     #         file_path: str,
 12 |     #         parameters: Dict[str, Any] = None,
 13 |     #         component_name: Optional[str] = None,
 14 |     #     ) -> str:
 15 |     #         """Generate Python code for a Grasshopper component based on a description.
 16 | 
 17 |     #         Args:
 18 |     #             description: Description of what the code should do
 19 |     #             file_path: Path where the generated code will be saved
 20 |     #             parameters: Dictionary of parameters to use in the code generation.
 21 |     #                         Can include the following keys:
 22 |     #                         - code_override: String containing complete code to use instead of generating
 23 |     #                         - center_x, center_y, center_z: Numeric values for geometric operations
 24 |     #                         - radius: Numeric value for circles or spheres
 25 |     #                         - width, height, depth: Dimensions for rectangular forms
 26 |     #                         - [Other commonly used parameters...]
 27 |     #             component_name: Optional name for the GH component
 28 | 
 29 |     #         Returns:
 30 |     #             Result of the operation including the file path to the generated code
 31 |     #         """
 32 |     #         ctx = mcp.get_context()
 33 |     #         rhino = ctx.request_context.lifespan_context.rhino
 34 | 
 35 |     #         result = await rhino.generate_and_execute_gh_code(description, file_path, parameters, component_name)
 36 | 
 37 |     #         if result["result"] == "error":
 38 |     #             return f"Error generating Grasshopper code: {result['error']}"
 39 | 
 40 |     #         return f"""Generated Grasshopper Python code successfully:
 41 |     # {result['code']}"""
 42 | 
 43 |     @mcp.tool()
 44 |     async def execute_grasshopper_code(code: str, file_path: str) -> str:
 45 |         """Execute given Python code.
 46 | 
 47 |         Args:
 48 |         code: The given code to execute
 49 |         file_path: Path where the generated code will be saved
 50 | 
 51 |         Returns:
 52 |             Result of the executing code
 53 |         """
 54 |         ctx = mcp.get_context()
 55 |         rhino = ctx.request_context.lifespan_context.rhino
 56 | 
 57 |         result = await rhino.send_code_to_gh(code, file_path)
 58 | 
 59 |     @mcp.tool()
 60 |     async def add_grasshopper_component(
 61 |         component_name: str, component_type: str, parameters: Dict[str, Any]
 62 |     ) -> str:
 63 |         """Add a component from an existing Grasshopper plugin.
 64 | 
 65 |         Args:
 66 |             component_name: Name of the component
 67 |             component_type: Type/category of the component
 68 |             parameters: Component parameters and settings
 69 | 
 70 |         Returns:
 71 |             Result of the operation
 72 |         """
 73 |         ctx = mcp.get_context()
 74 |         rhino = ctx.request_context.lifespan_context.rhino
 75 | 
 76 |         result = await rhino.add_gh_component(
 77 |             component_name=component_name, component_type=component_type, parameters=parameters
 78 |         )
 79 | 
 80 |         if result["result"] == "error":
 81 |             return f"Error adding component: {result['error']}"
 82 | 
 83 |         return f"""Successfully added Grasshopper component:
 84 | - Component: {component_name} ({component_type})
 85 | - Component ID: {result.get('component_id', 'Unknown')}
 86 | """
 87 | 
 88 |     @mcp.tool()
 89 |     async def connect_grasshopper_components(
 90 |         source_id: str, source_param: str, target_id: str, target_param: str
 91 |     ) -> str:
 92 |         """Connect parameters between Grasshopper components.
 93 | 
 94 |         Args:
 95 |             source_id: Source component ID
 96 |             source_param: Source parameter name
 97 |             target_id: Target component ID
 98 |             target_param: Target parameter name
 99 | 
100 |         Returns:
101 |             Result of the operation
102 |         """
103 |         ctx = mcp.get_context()
104 |         rhino = ctx.request_context.lifespan_context.rhino
105 | 
106 |         result = await rhino.connect_gh_components(
107 |             source_id=source_id, source_param=source_param, target_id=target_id, target_param=target_param
108 |         )
109 | 
110 |         if result["result"] == "error":
111 |             return f"Error connecting components: {result['error']}"
112 | 
113 |         return f"""Successfully connected Grasshopper components:
114 | - Connected: {source_id}.{source_param} → {target_id}.{target_param}
115 | """
116 | 
117 |     @mcp.tool()
118 |     async def run_grasshopper_definition(
119 |         file_path: Optional[str] = None, save_output: bool = False, output_path: Optional[str] = None
120 |     ) -> str:
121 |         """Run a Grasshopper definition.
122 | 
123 |         Args:
124 |             file_path: Path to the .gh file (or None for current definition)
125 |             save_output: Whether to save the output
126 |             output_path: Path to save the output (if save_output is True)
127 | 
128 |         Returns:
129 |             Result of the operation
130 |         """
131 |         ctx = mcp.get_context()
132 |         rhino = ctx.request_context.lifespan_context.rhino
133 | 
134 |         result = await rhino.run_gh_definition(
135 |             file_path=file_path, save_output=save_output, output_path=output_path
136 |         )
137 | 
138 |         if result["result"] == "error":
139 |             return f"Error running definition: {result['error']}"
140 | 
141 |         return f"""Successfully ran Grasshopper definition:
142 | - Execution time: {result.get('execution_time', 'Unknown')} seconds
143 | - Outputs: {result.get('output_summary', 'No output summary available')}
144 | """
145 | 
146 | 
147 | async def generate_python_code(
148 |     rhino_connection, description: str, inputs: List[Dict[str, Any]], outputs: List[Dict[str, Any]]
149 | ) -> Dict[str, Any]:
150 |     """Generate Python code for Grasshopper based on description and parameters.
151 | 
152 |     This is a simplified implementation. In a production system,
153 |     this might call an LLM or use templates.
154 |     """
155 |     # Build code header with imports
156 |     code = "import Rhino.Geometry as rg\n"
157 |     code += "import scriptcontext as sc\n"
158 |     code += "import ghpythonlib.components as ghcomp\n\n"
159 | 
160 |     # Add description as comment
161 |     code += f"# {description}\n\n"
162 | 
163 |     # Process inputs
164 |     input_vars = []
165 |     for i, inp in enumerate(inputs):
166 |         var_name = inp["name"]
167 |         input_vars.append(var_name)
168 | 
169 |         # Add comments about input parameters
170 |         code += f"# Input: {var_name} ({inp['type']}) - {inp.get('description', '')}\n"
171 | 
172 |     code += "\n# Processing\n"
173 | 
174 |     # Add basic implementation based on description
175 |     # This is where you might want to call an LLM or use more sophisticated templates
176 |     if "circle" in description.lower():
177 |         code += """if radius is not None:
178 |     circle = rg.Circle(rg.Point3d(0, 0, 0), radius)
179 |     circle = circle.ToNurbsCurve()
180 | else:
181 |     circle = None
182 | """
183 |     elif "box" in description.lower():
184 |         code += """if width is not None and height is not None and depth is not None:
185 |     box = rg.Box(
186 |         rg.Plane.WorldXY,
187 |         rg.Interval(0, width),
188 |         rg.Interval(0, height),
189 |         rg.Interval(0, depth)
190 |     )
191 | else:
192 |     box = None
193 | """
194 |     else:
195 |         # Generic code template
196 |         code += "# Add your implementation here based on the description\n"
197 |         code += "# Use the input parameters to generate the desired output\n\n"
198 | 
199 |     # Process outputs
200 |     output_assignments = []
201 |     for output in outputs:
202 |         var_name = output["name"]
203 |         # Assign a dummy value to each output
204 |         output_assignments.append(f"{var_name} = {var_name}")
205 | 
206 |     # Add output assignments
207 |     code += "\n# Outputs\n"
208 |     code += "\n".join(output_assignments)
209 | 
210 |     return {"result": "success", "code": code}
211 | 
```

--------------------------------------------------------------------------------
/grasshopper_mcp/tools/modeling.py:
--------------------------------------------------------------------------------

```python
  1 | from mcp.server.fastmcp import FastMCP
  2 | 
  3 | 
  4 | def register_modeling_tools(mcp: FastMCP) -> None:
  5 |     """Register geometry access tools with the MCP server."""
  6 | 
  7 |     @mcp.tool()
  8 |     async def extract_geometry(file_path: str, object_index: int) -> str:
  9 |         """Extract geometric data from an existing object.
 10 | 
 11 |         Args:
 12 |             file_path: Path to the .3dm file
 13 |             object_index: Index of the object to extract data from
 14 | 
 15 |         Returns:
 16 |             Geometric data in a readable format
 17 |         """
 18 |         ctx = mcp.get_context()
 19 |         rhino = ctx.request_context.lifespan_context.rhino
 20 | 
 21 |         result = await rhino.read_3dm_file(file_path)
 22 | 
 23 |         if result["result"] == "error":
 24 |             return f"Error: {result['error']}"
 25 | 
 26 |         model = result["model"]
 27 | 
 28 |         # Check if index is valid
 29 |         try:
 30 |             index = int(object_index)
 31 |             if index < 0 or index >= len(model.Objects):
 32 |                 return f"Error: Invalid object index. File has {len(model.Objects)} objects."
 33 |         except ValueError:
 34 |             return f"Error: Object index must be a number."
 35 | 
 36 |         # Extract geometry data using rhino3dm
 37 |         obj = model.Objects[index]
 38 |         geom = obj.Geometry
 39 | 
 40 |         if not geom:
 41 |             return f"Error: No geometry found for object at index {index}."
 42 | 
 43 |         r3d = rhino.rhino_instance["r3d"]
 44 | 
 45 |         # Extract data based on geometry type
 46 |         geometry_data = {"type": str(geom.ObjectType), "id": str(obj.Id) if hasattr(obj, "Id") else "Unknown"}
 47 | 
 48 |         # Get bounding box
 49 |         bbox = geom.GetBoundingBox() if hasattr(geom, "GetBoundingBox") else None
 50 |         if bbox:
 51 |             geometry_data["bounding_box"] = {
 52 |                 "min": [bbox.Min.X, bbox.Min.Y, bbox.Min.Z],
 53 |                 "max": [bbox.Max.X, bbox.Max.Y, bbox.Max.Z],
 54 |                 "dimensions": [bbox.Max.X - bbox.Min.X, bbox.Max.Y - bbox.Min.Y, bbox.Max.Z - bbox.Min.Z],
 55 |             }
 56 | 
 57 |         # Type-specific data extraction
 58 |         if hasattr(geom, "ObjectType"):
 59 |             if geom.ObjectType == r3d.ObjectType.Point:
 60 |                 point = geom.Location
 61 |                 geometry_data["coordinates"] = [point.X, point.Y, point.Z]
 62 | 
 63 |             elif geom.ObjectType == r3d.ObjectType.Curve:
 64 |                 # For curves, extract key points
 65 |                 geometry_data["length"] = geom.GetLength() if hasattr(geom, "GetLength") else "Unknown"
 66 |                 geometry_data["is_closed"] = geom.IsClosed if hasattr(geom, "IsClosed") else "Unknown"
 67 | 
 68 |                 # Get start and end points if not closed
 69 |                 if hasattr(geom, "PointAtStart") and hasattr(geom, "PointAtEnd"):
 70 |                     start = geom.PointAtStart
 71 |                     end = geom.PointAtEnd
 72 |                     geometry_data["start_point"] = [start.X, start.Y, start.Z]
 73 |                     geometry_data["end_point"] = [end.X, end.Y, end.Z]
 74 | 
 75 |             elif geom.ObjectType == r3d.ObjectType.Brep:
 76 |                 # For solids, extract volume and surface area
 77 |                 geometry_data["volume"] = geom.GetVolume() if hasattr(geom, "GetVolume") else "Unknown"
 78 |                 geometry_data["area"] = geom.GetArea() if hasattr(geom, "GetArea") else "Unknown"
 79 |                 geometry_data["is_solid"] = geom.IsSolid if hasattr(geom, "IsSolid") else "Unknown"
 80 | 
 81 |                 # Count faces, edges, vertices
 82 |                 if hasattr(geom, "Faces") and hasattr(geom, "Edges"):
 83 |                     geometry_data["face_count"] = len(geom.Faces)
 84 |                     geometry_data["edge_count"] = len(geom.Edges)
 85 | 
 86 |             elif geom.ObjectType == r3d.ObjectType.Mesh:
 87 |                 # For meshes, extract vertex and face counts
 88 |                 if hasattr(geom, "Vertices") and hasattr(geom, "Faces"):
 89 |                     geometry_data["vertex_count"] = len(geom.Vertices)
 90 |                     geometry_data["face_count"] = len(geom.Faces)
 91 | 
 92 |         # Format output as readable text
 93 |         output = [f"# Geometry Data for Object {index}"]
 94 |         output.append(f"- Type: {geometry_data['type']}")
 95 |         output.append(f"- ID: {geometry_data['id']}")
 96 | 
 97 |         if "bounding_box" in geometry_data:
 98 |             bbox = geometry_data["bounding_box"]
 99 |             output.append("- Bounding Box:")
100 |             output.append(f"  - Min: ({bbox['min'][0]:.2f}, {bbox['min'][1]:.2f}, {bbox['min'][2]:.2f})")
101 |             output.append(f"  - Max: ({bbox['max'][0]:.2f}, {bbox['max'][1]:.2f}, {bbox['max'][2]:.2f})")
102 |             output.append(
103 |                 f"  - Dimensions: {bbox['dimensions'][0]:.2f} × {bbox['dimensions'][1]:.2f} × {bbox['dimensions'][2]:.2f}"
104 |             )
105 | 
106 |         # Add remaining data with nice formatting
107 |         for key, value in geometry_data.items():
108 |             if key not in ["type", "id", "bounding_box"]:
109 |                 # Format key nicely
110 |                 formatted_key = key.replace("_", " ").title()
111 | 
112 |                 # Format value based on type
113 |                 if isinstance(value, list) and len(value) == 3:
114 |                     formatted_value = f"({value[0]:.2f}, {value[1]:.2f}, {value[2]:.2f})"
115 |                 elif isinstance(value, float):
116 |                     formatted_value = f"{value:.4f}"
117 |                 else:
118 |                     formatted_value = str(value)
119 | 
120 |                 output.append(f"- {formatted_key}: {formatted_value}")
121 | 
122 |         return "\n".join(output)
123 | 
124 |     @mcp.tool()
125 |     async def measure_distance(file_path: str, object_index1: int, object_index2: int) -> str:
126 |         """Measure the distance between two objects in a Rhino file.
127 | 
128 |         Args:
129 |             file_path: Path to the .3dm file
130 |             object_index1: Index of the first object
131 |             object_index2: Index of the second object
132 | 
133 |         Returns:
134 |             Distance measurement information
135 |         """
136 |         ctx = mcp.get_context()
137 |         rhino = ctx.request_context.lifespan_context.rhino
138 | 
139 |         result = await rhino.read_3dm_file(file_path)
140 | 
141 |         if result["result"] == "error":
142 |             return f"Error: {result['error']}"
143 | 
144 |         model = result["model"]
145 |         r3d = rhino.rhino_instance["r3d"]
146 | 
147 |         # Validate indices
148 |         try:
149 |             idx1 = int(object_index1)
150 |             idx2 = int(object_index2)
151 | 
152 |             if idx1 < 0 or idx1 >= len(model.Objects) or idx2 < 0 or idx2 >= len(model.Objects):
153 |                 return (
154 |                     f"Error: Invalid object indices. File has {len(model.Objects)} objects (0-{len(model.Objects)-1})."
155 |                 )
156 |         except ValueError:
157 |             return "Error: Object indices must be numbers."
158 | 
159 |         # Get geometries
160 |         obj1 = model.Objects[idx1]
161 |         obj2 = model.Objects[idx2]
162 |         geom1 = obj1.Geometry
163 |         geom2 = obj2.Geometry
164 | 
165 |         if not geom1 or not geom2:
166 |             return "Error: One or both objects don't have geometry."
167 | 
168 |         # Calculate distances using bounding boxes (simple approach)
169 |         bbox1 = geom1.GetBoundingBox() if hasattr(geom1, "GetBoundingBox") else None
170 |         bbox2 = geom2.GetBoundingBox() if hasattr(geom2, "GetBoundingBox") else None
171 | 
172 |         if not bbox1 or not bbox2:
173 |             return "Error: Couldn't get bounding boxes for the objects."
174 | 
175 |         # Calculate center points
176 |         center1 = r3d.Point3d(
177 |             (bbox1.Min.X + bbox1.Max.X) / 2, (bbox1.Min.Y + bbox1.Max.Y) / 2, (bbox1.Min.Z + bbox1.Max.Z) / 2
178 |         )
179 | 
180 |         center2 = r3d.Point3d(
181 |             (bbox2.Min.X + bbox2.Max.X) / 2, (bbox2.Min.Y + bbox2.Max.Y) / 2, (bbox2.Min.Z + bbox2.Max.Z) / 2
182 |         )
183 | 
184 |         # Calculate distance between centers
185 |         distance = center1.DistanceTo(center2)
186 | 
187 |         # Get object names
188 |         name1 = obj1.Attributes.Name or f"Object {idx1}"
189 |         name2 = obj2.Attributes.Name or f"Object {idx2}"
190 | 
191 |         return f"""Measurement between '{name1}' and '{name2}':
192 | - Center-to-center distance: {distance:.4f} units
193 | - Object 1 center: ({center1.X:.2f}, {center1.Y:.2f}, {center1.Z:.2f})
194 | - Object 2 center: ({center2.X:.2f}, {center2.Y:.2f}, {center2.Z:.2f})
195 | 
196 | Note: This is an approximate center-to-center measurement using bounding boxes.
197 | """
198 | 
```

--------------------------------------------------------------------------------
/grasshopper_mcp/tools/advanced_grasshopper.py:
--------------------------------------------------------------------------------

```python
  1 | from mcp.server.fastmcp import FastMCP
  2 | from typing import Dict, List, Any, Optional
  3 | 
  4 | 
  5 | def register_advanced_grasshopper_tools(mcp: FastMCP) -> None:
  6 |     """Register advanced Grasshopper operations with the MCP server."""
  7 | 
  8 |     @mcp.tool()
  9 |     async def create_parametric_definition(
 10 |         description: str, parameters: Dict[str, Any], output_file: Optional[str] = None
 11 |     ) -> str:
 12 |         """Create a complete parametric definition in Grasshopper based on a description.
 13 | 
 14 |         Args:
 15 |             description: Detailed description of the parametric model to create
 16 |             parameters: Dictionary of parameter names and values
 17 |             output_file: Optional path to save the definition
 18 | 
 19 |         Returns:
 20 |             Result of the operation
 21 |         """
 22 |         ctx = mcp.get_context()
 23 |         rhino = ctx.request_context.lifespan_context.rhino
 24 | 
 25 |         # First, analyze the description to determine required components
 26 |         # This would ideally be done with an LLM or a sophisticated parsing system
 27 |         # Here we're using a simplified approach
 28 | 
 29 |         # Generate a basic workflow based on the description
 30 |         workflow = await generate_grasshopper_workflow(rhino, description, parameters)
 31 | 
 32 |         if workflow["result"] == "error":
 33 |             return f"Error generating workflow: {workflow['error']}"
 34 | 
 35 |         # Create the components in the definition
 36 |         component_ids = {}
 37 | 
 38 |         # Parameter components (sliders, panels, etc.)
 39 |         for param_name, param_info in workflow["parameters"].items():
 40 |             result = await rhino.add_gh_component(
 41 |                 component_name=param_info["component"],
 42 |                 component_type="Params",
 43 |                 parameters={"NickName": param_name, "Value": param_info.get("value")},
 44 |             )
 45 | 
 46 |             if result["result"] == "error":
 47 |                 return f"Error creating parameter component: {result['error']}"
 48 | 
 49 |             component_ids[param_name] = result["component_id"]
 50 | 
 51 |         # Processing components
 52 |         for comp_name, comp_info in workflow["components"].items():
 53 |             result = await rhino.add_gh_component(
 54 |                 component_name=comp_info["component"],
 55 |                 component_type=comp_info["type"],
 56 |                 parameters={"NickName": comp_name},
 57 |             )
 58 | 
 59 |             if result["result"] == "error":
 60 |                 return f"Error creating component: {result['error']}"
 61 | 
 62 |             component_ids[comp_name] = result["component_id"]
 63 | 
 64 |         # Python script components
 65 |         for script_name, script_info in workflow["scripts"].items():
 66 |             result = await rhino.create_gh_script_component(
 67 |                 description=script_name,
 68 |                 inputs=script_info["inputs"],
 69 |                 outputs=script_info["outputs"],
 70 |                 code=script_info["code"],
 71 |             )
 72 | 
 73 |             if result["result"] == "error":
 74 |                 return f"Error creating script component: {result['error']}"
 75 | 
 76 |             component_ids[script_name] = result["component_id"]
 77 | 
 78 |         # Connect the components
 79 |         for connection in workflow["connections"]:
 80 |             source = connection["from"].split(".")
 81 |             target = connection["to"].split(".")
 82 | 
 83 |             source_id = component_ids.get(source[0])
 84 |             target_id = component_ids.get(target[0])
 85 | 
 86 |             if not source_id or not target_id:
 87 |                 continue
 88 | 
 89 |             result = await rhino.connect_gh_components(
 90 |                 source_id=source_id, source_param=source[1], target_id=target_id, target_param=target[1]
 91 |             )
 92 | 
 93 |             if result["result"] == "error":
 94 |                 return f"Error connecting components: {result['error']}"
 95 | 
 96 |         # Run the definition to validate
 97 |         result = await rhino.run_gh_definition()
 98 | 
 99 |         if result["result"] == "error":
100 |             return f"Error running definition: {result['error']}"
101 | 
102 |         # Save if output file is specified
103 |         if output_file:
104 |             save_result = await rhino.run_gh_definition(file_path=None, save_output=True, output_path=output_file)
105 | 
106 |             if save_result["result"] == "error":
107 |                 return f"Error saving definition: {save_result['error']}"
108 | 
109 |         return f"""Successfully created parametric Grasshopper definition:
110 | - Description: {description}
111 | - Components: {len(component_ids)} created
112 | - Parameters: {len(workflow['parameters'])}
113 | - Saved to: {output_file if output_file else 'Not saved to file'}
114 | """
115 | 
116 |     @mcp.tool()
117 |     async def call_grasshopper_plugin(
118 |         plugin_name: str, component_name: str, inputs: Dict[str, Any], file_path: Optional[str] = None
119 |     ) -> str:
120 |         """Call a specific component from a Grasshopper plugin.
121 | 
122 |         Args:
123 |             plugin_name: Name of the plugin (e.g., 'Kangaroo', 'Ladybug')
124 |             component_name: Name of the component to use
125 |             inputs: Dictionary of input parameter names and values
126 |             file_path: Optional path to a GH file to append to
127 | 
128 |         Returns:
129 |             Result of the operation
130 |         """
131 |         ctx = mcp.get_context()
132 |         rhino = ctx.request_context.lifespan_context.rhino
133 | 
134 |         # If file path provided, open that definition
135 |         if file_path:
136 |             open_result = await rhino.run_gh_definition(file_path=file_path)
137 |             if open_result["result"] == "error":
138 |                 return f"Error opening file: {open_result['error']}"
139 | 
140 |         # Add the plugin component
141 |         plugin_comp_result = await rhino.add_gh_component(
142 |             component_name=component_name, component_type=plugin_name, parameters={}
143 |         )
144 | 
145 |         if plugin_comp_result["result"] == "error":
146 |             return f"Error adding plugin component: {plugin_comp_result['error']}"
147 | 
148 |         plugin_comp_id = plugin_comp_result["component_id"]
149 | 
150 |         # Add input parameters
151 |         input_comp_ids = {}
152 |         for input_name, input_value in inputs.items():
153 |             # Determine the appropriate parameter component
154 |             if isinstance(input_value, (int, float)):
155 |                 comp_type = "Number"
156 |             elif isinstance(input_value, str):
157 |                 comp_type = "Text"
158 |             elif isinstance(input_value, bool):
159 |                 comp_type = "Boolean"
160 |             else:
161 |                 return f"Unsupported input type for {input_name}: {type(input_value)}"
162 | 
163 |             # Create the parameter component
164 |             input_result = await rhino.add_gh_component(
165 |                 component_name=comp_type,
166 |                 component_type="Params",
167 |                 parameters={"NickName": input_name, "Value": input_value},
168 |             )
169 | 
170 |             if input_result["result"] == "error":
171 |                 return f"Error creating input parameter {input_name}: {input_result['error']}"
172 | 
173 |             input_comp_ids[input_name] = input_result["component_id"]
174 | 
175 |             # Connect to the plugin component
176 |             connect_result = await rhino.connect_gh_components(
177 |                 source_id=input_result["component_id"],
178 |                 source_param="output",
179 |                 target_id=plugin_comp_id,
180 |                 target_param=input_name,
181 |             )
182 | 
183 |             if connect_result["result"] == "error":
184 |                 return f"Error connecting {input_name}: {connect_result['error']}"
185 | 
186 |         # Run the definition
187 |         run_result = await rhino.run_gh_definition()
188 | 
189 |         if run_result["result"] == "error":
190 |             return f"Error running definition with plugin: {run_result['error']}"
191 | 
192 |         return f"""Successfully called Grasshopper plugin component:
193 | - Plugin: {plugin_name}
194 | - Component: {component_name}
195 | - Inputs: {', '.join(inputs.keys())}
196 | - Execution time: {run_result.get('execution_time', 'Unknown')} seconds
197 | """
198 | 
199 |     @mcp.tool()
200 |     async def edit_gh_script_component(file_path: str, component_id: str, new_code: str) -> str:
201 |         """Edit the code in an existing Python script component.
202 | 
203 |         Args:
204 |             file_path: Path to the Grasshopper file
205 |             component_id: ID of the component to edit
206 |             new_code: New Python script for the component
207 | 
208 |         Returns:
209 |             Result of the operation
210 |         """
211 |         ctx = mcp.get_context()
212 |         rhino = ctx.request_context.lifespan_context.rhino
213 | 
214 |         # Open the file
215 |         open_result = await rhino.run_gh_definition(file_path=file_path)
216 |         if open_result["result"] == "error":
217 |             return f"Error opening file: {open_result['error']}"
218 | 
219 |         # Edit the component
220 |         execution_code = """
221 |         import Rhino
222 |         import Grasshopper
223 |         
224 |         # Access the current Grasshopper document
225 |         gh_doc = Grasshopper.Instances.ActiveCanvas.Document
226 |         
227 |         # Find the component by ID
228 |         target_component = None
229 |         for obj in gh_doc.Objects:
230 |             if str(obj.ComponentGuid) == component_id:
231 |                 target_component = obj
232 |                 break
233 |         
234 |         if target_component is None:
235 |             raise ValueError(f"Component with ID {component_id} not found")
236 |             
237 |         # Check if it's a Python component
238 |         if not hasattr(target_component, "ScriptSource"):
239 |             raise ValueError(f"Component is not a Python script component")
240 |             
241 |         # Update the code
242 |         target_component.ScriptSource = new_code
243 |         
244 |         # Update the document
245 |         gh_doc.NewSolution(True)
246 |         
247 |         result = {
248 |             "component_name": target_component.NickName,
249 |             "success": True
250 |         }
251 |         """
252 | 
253 |         edit_result = await rhino._execute_rhino(execution_code, {"component_id": component_id, "new_code": new_code})
254 | 
255 |         if edit_result["result"] == "error":
256 |             return f"Error editing component: {edit_result['error']}"
257 | 
258 |         return f"""Successfully edited Python script component:
259 | - Component: {edit_result.get('data', {}).get('component_name', 'Unknown')}
260 | - Updated code length: {len(new_code)} characters
261 | """
262 | 
263 | 
264 | async def generate_grasshopper_workflow(
265 |     rhino_connection, description: str, parameters: Dict[str, Any]
266 | ) -> Dict[str, Any]:
267 |     """Generate a Grasshopper workflow based on a description.
268 | 
269 |     This is a simplified implementation that parses the description to determine
270 |     the necessary components, parameters, and connections.
271 | 
272 |     In a production system, this would likely use an LLM to generate the workflow.
273 |     """
274 |     # Initialize workflow structure
275 |     workflow = {"parameters": {}, "components": {}, "scripts": {}, "connections": [], "result": "success"}
276 | 
277 |     # Analyze description to determine what we're building
278 |     description_lower = description.lower()
279 | 
280 |     # Default to a basic parametric box if no specific shape is mentioned
281 |     if "box" in description_lower or "cube" in description_lower:
282 |         # Create a parametric box
283 |         workflow["parameters"] = {
284 |             "Width": {"component": "Number Slider", "value": parameters.get("Width", 10)},
285 |             "Height": {"component": "Number Slider", "value": parameters.get("Height", 10)},
286 |             "Depth": {"component": "Number Slider", "value": parameters.get("Depth", 10)},
287 |         }
288 | 
289 |         workflow["components"] = {
290 |             "BoxOrigin": {"component": "Construct Point", "type": "Vector"},
291 |             "Box": {"component": "Box", "type": "Surface"},
292 |         }
293 | 
294 |         workflow["connections"] = [
295 |             {"from": "Width.output", "to": "Box.X Size"},
296 |             {"from": "Height.output", "to": "Box.Y Size"},
297 |             {"from": "Depth.output", "to": "Box.Z Size"},
298 |             {"from": "BoxOrigin.Point", "to": "Box.Base Point"},
299 |         ]
300 | 
301 |     elif "cylinder" in description_lower:
302 |         # Create a parametric cylinder
303 |         workflow["parameters"] = {
304 |             "Radius": {"component": "Number Slider", "value": parameters.get("Radius", 5)},
305 |             "Height": {"component": "Number Slider", "value": parameters.get("Height", 20)},
306 |         }
307 | 
308 |         workflow["components"] = {
309 |             "BasePoint": {"component": "Construct Point", "type": "Vector"},
310 |             "Circle": {"component": "Circle", "type": "Curve"},
311 |             "Cylinder": {"component": "Extrude", "type": "Surface"},
312 |         }
313 | 
314 |         workflow["connections"] = [
315 |             {"from": "Radius.output", "to": "Circle.Radius"},
316 |             {"from": "BasePoint.Point", "to": "Circle.Base"},
317 |             {"from": "Circle.Circle", "to": "Cylinder.Base"},
318 |             {"from": "Height.output", "to": "Cylinder.Direction"},
319 |         ]
320 | 
321 |     elif "loft" in description_lower or "surface" in description_lower:
322 |         # Create a lofted surface between curves
323 |         workflow["parameters"] = {
324 |             "Points": {"component": "Number Slider", "value": parameters.get("Points", 5)},
325 |             "Height": {"component": "Number Slider", "value": parameters.get("Height", 20)},
326 |             "RadiusBottom": {"component": "Number Slider", "value": parameters.get("RadiusBottom", 10)},
327 |             "RadiusTop": {"component": "Number Slider", "value": parameters.get("RadiusTop", 5)},
328 |         }
329 | 
330 |         workflow["components"] = {
331 |             "BasePoint": {"component": "Construct Point", "type": "Vector"},
332 |             "TopPoint": {"component": "Construct Point", "type": "Vector"},
333 |             "CircleBottom": {"component": "Circle", "type": "Curve"},
334 |             "CircleTop": {"component": "Circle", "type": "Curve"},
335 |             "Loft": {"component": "Loft", "type": "Surface"},
336 |         }
337 | 
338 |         # For more complex workflows, we can use Python script components
339 |         workflow["scripts"] = {
340 |             "HeightVector": {
341 |                 "inputs": [{"name": "height", "type": "float", "description": "Height of the loft"}],
342 |                 "outputs": [{"name": "vector", "type": "vector", "description": "Height vector"}],
343 |                 "code": """
344 | import Rhino.Geometry as rg
345 | 
346 | # Create a vertical vector for the height
347 | vector = rg.Vector3d(0, 0, height)
348 | """,
349 |             }
350 |         }
351 | 
352 |         workflow["connections"] = [
353 |             {"from": "Height.output", "to": "HeightVector.height"},
354 |             {"from": "HeightVector.vector", "to": "TopPoint.Z"},
355 |             {"from": "RadiusBottom.output", "to": "CircleBottom.Radius"},
356 |             {"from": "RadiusTop.output", "to": "CircleTop.Radius"},
357 |             {"from": "BasePoint.Point", "to": "CircleBottom.Base"},
358 |             {"from": "TopPoint.Point", "to": "CircleTop.Base"},
359 |             {"from": "CircleBottom.Circle", "to": "Loft.Curves"},
360 |             {"from": "CircleTop.Circle", "to": "Loft.Curves"},
361 |         ]
362 | 
363 |     else:
364 |         # Generic parametric object with Python script
365 |         workflow["parameters"] = {
366 |             "Parameter1": {"component": "Number Slider", "value": parameters.get("Parameter1", 10)},
367 |             "Parameter2": {"component": "Number Slider", "value": parameters.get("Parameter2", 20)},
368 |         }
369 | 
370 |         workflow["scripts"] = {
371 |             "CustomGeometry": {
372 |                 "inputs": [
373 |                     {"name": "param1", "type": "float", "description": "First parameter"},
374 |                     {"name": "param2", "type": "float", "description": "Second parameter"},
375 |                 ],
376 |                 "outputs": [{"name": "geometry", "type": "geometry", "description": "Resulting geometry"}],
377 |                 "code": """
378 | import Rhino.Geometry as rg
379 | import math
380 | 
381 | # Create custom geometry based on parameters
382 | point = rg.Point3d(0, 0, 0)
383 | radius = param1
384 | height = param2
385 | 
386 | # Default to a simple cylinder if nothing specific is mentioned
387 | cylinder = rg.Cylinder(
388 |     new rg.Circle(point, radius),
389 |     height
390 | )
391 | 
392 | geometry = cylinder.ToBrep(True, True)
393 | """,
394 |             }
395 |         }
396 | 
397 |         workflow["connections"] = [
398 |             {"from": "Parameter1.output", "to": "CustomGeometry.param1"},
399 |             {"from": "Parameter2.output", "to": "CustomGeometry.param2"},
400 |         ]
401 | 
402 |     return workflow
403 | 
```

--------------------------------------------------------------------------------
/grasshopper_mcp/rhino/connection.py:
--------------------------------------------------------------------------------

```python
  1 | import platform
  2 | import os
  3 | import json
  4 | import uuid
  5 | from typing import Any, Dict, List, Optional
  6 | import time
  7 | import platform
  8 | import sys
  9 | import socket
 10 | import tempfile
 11 | 
 12 | from ..config import ServerConfig
 13 | 
 14 | 
 15 | def find_scriptcontext_path():
 16 |     scriptcontext_path = os.path.join(
 17 |         os.environ["APPDATA"],
 18 |         "McNeel",
 19 |         "Rhinoceros",
 20 |         "7.0",
 21 |         "Plug-ins",
 22 |         "IronPython (814d908a-e25c-493d-97e9-ee3861957f49)",
 23 |         "settings",
 24 |         "lib",
 25 |     )
 26 | 
 27 |     if not os.path.exists(scriptcontext_path):
 28 |         # If the specific path doesn't exist, try to find it
 29 |         import glob
 30 | 
 31 |         appdata = os.environ["APPDATA"]
 32 |         potential_paths = glob.glob(
 33 |             os.path.join(appdata, "McNeel", "Rhinoceros", "7.0", "Plug-ins", "IronPython*", "settings", "lib")
 34 |         )
 35 |         if potential_paths:
 36 |             scriptcontext_path
 37 |             # sys.path.append(potential_paths[0])
 38 | 
 39 |     return scriptcontext_path
 40 | 
 41 | 
 42 | def find_RhinoPython_path(rhino_path):
 43 |     appdata = os.environ["APPDATA"]
 44 |     rhino_python_paths = [
 45 |         # Standard Rhino Python lib paths
 46 |         os.path.join(os.path.dirname(rhino_path), "Plug-ins", "IronPython"),
 47 |         os.path.join(os.path.dirname(rhino_path), "Plug-ins", "IronPython", "Lib"),
 48 |         os.path.join(os.path.dirname(rhino_path), "Plug-ins", "PythonPlugins"),
 49 |         os.path.join(os.path.dirname(rhino_path), "Scripts"),
 50 |         # Try to find RhinoPython in various locations
 51 |         os.path.join(os.path.dirname(rhino_path), "Plug-ins"),
 52 |         os.path.join(appdata, "McNeel", "Rhinoceros", "7.0", "Plug-ins"),
 53 |         os.path.join(appdata, "McNeel", "Rhinoceros", "7.0", "Scripts"),
 54 |         # Common Rhino installation paths for plugins
 55 |         "C:\\Program Files\\Rhino 7\\Plug-ins",
 56 |         "C:\\Program Files\\Rhino 7\\Plug-ins\\PythonPlugins",
 57 |     ]
 58 | 
 59 |     return rhino_python_paths
 60 | 
 61 | 
 62 | class RhinoConnection:
 63 |     """Connection to Rhino/Grasshopper."""
 64 | 
 65 |     def __init__(self, config: ServerConfig):
 66 |         self.config = config
 67 |         self.connected = False
 68 |         self.rhino_instance = None
 69 |         self.is_mac = platform.system() == "Darwin"
 70 | 
 71 |         self.codelistener_host = "127.0.0.1"
 72 |         self.codelistener_port = 614  # Default CodeListener port
 73 | 
 74 |     async def initialize(self) -> None:
 75 |         """Initialize connection to Rhino/Grasshopper."""
 76 |         if self.config.use_compute_api:
 77 |             # Setup compute API connection
 78 |             self._initialize_compute()
 79 |         else:
 80 |             # Setup direct connection
 81 |             self._initialize_rhino()
 82 | 
 83 |         self.connected = True
 84 | 
 85 |     def _initialize_rhino(self) -> None:
 86 |         """Initialize Rhino geometry access."""
 87 |         if platform.system() == "Windows" and not self.config.use_rhino3dm:
 88 |             # Windows-specific RhinoInside implementation
 89 |             import sys
 90 | 
 91 |             rhino_path = self.config.rhino_path
 92 | 
 93 |             if not rhino_path or not os.path.exists(rhino_path):
 94 |                 raise ValueError(f"Invalid Rhino path: {rhino_path}")
 95 |             # print(rhino_path)
 96 |             sys.path.append(rhino_path)
 97 | 
 98 |             # Add the specific path for scriptcontext
 99 |             scriptcontext_path = find_scriptcontext_path()
100 |             sys.path.append(scriptcontext_path)
101 | 
102 |             RhinoPython_path = find_RhinoPython_path(rhino_path)
103 |             for path in RhinoPython_path:
104 |                 if os.path.exists(path):
105 |                     sys.path.append(path)
106 |                     print(path)
107 | 
108 |             try:
109 |                 import rhinoinside
110 | 
111 |                 rhinoinside.load()
112 |                 print("rhinoinside installed.")
113 |                 # Import Rhino components
114 |                 import Rhino
115 | 
116 |                 print("Rhino installed.")
117 |                 import Rhino.Geometry as rg
118 | 
119 |                 print("Rhino.Geometry installed.")
120 |                 import scriptcontext as sc
121 | 
122 |                 # Store references
123 |                 self.rhino_instance = {"Rhino": Rhino, "rg": rg, "sc": sc, "use_rhino3dm": False}
124 |             except ImportError as e:
125 |                 raise ImportError(f"Error importing RhinoInside or Rhino components: {e}")
126 |         else:
127 |             # Cross-platform rhino3dm implementation
128 |             try:
129 |                 import rhino3dm as r3d
130 | 
131 |                 self.rhino_instance = {"r3d": r3d, "use_rhino3dm": True}
132 |             except ImportError:
133 |                 raise ImportError("Please install rhino3dm: uv add rhino3dm")
134 | 
135 |     async def send_code_to_rhino(self, code: str) -> Dict[str, Any]:
136 |         """Send Python code to Rhino via CodeListener.
137 | 
138 |         Args:
139 |             code: Python code to execute in Rhino
140 | 
141 |         Returns:
142 |             Dictionary with result and response or error
143 |         """
144 |         try:
145 |             # Create a temporary Python file
146 |             fd, temp_path = tempfile.mkstemp(suffix=".py")
147 |             try:
148 |                 # Write the code to the file
149 |                 with os.fdopen(fd, "w") as f:
150 |                     f.write(code)
151 | 
152 |                 # Create message object
153 |                 msg_obj = {"filename": temp_path, "run": True, "reset": False, "temp": True}
154 | 
155 |                 # Convert to JSON
156 |                 json_msg = json.dumps(msg_obj)
157 | 
158 |                 # Create a TCP socket
159 |                 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
160 |                 sock.settimeout(10)
161 |                 sock.connect((self.codelistener_host, self.codelistener_port))
162 | 
163 |                 # Send the JSON message
164 |                 sock.sendall(json_msg.encode("utf-8"))
165 | 
166 |                 # Receive the response
167 |                 response = sock.recv(4096).decode("utf-8")
168 | 
169 |                 # Close the socket
170 |                 sock.close()
171 | 
172 |                 return {"result": "success", "response": response}
173 | 
174 |             finally:
175 |                 # Clean up - remove temporary file after execution
176 |                 try:
177 |                     os.unlink(temp_path)
178 |                 except Exception as cleanup_error:
179 |                     print(f"Error cleaning up temporary file: {cleanup_error}")
180 | 
181 |         except Exception as e:
182 |             return {"result": "error", "error": str(e)}
183 | 
184 |     async def generate_and_execute_rhino_code(
185 |         self, prompt: str, model_context: Optional[Dict[str, Any]] = None
186 |     ) -> Dict[str, Any]:
187 |         """Generate Rhino Python code from a prompt and execute it.
188 | 
189 |         Args:
190 |             prompt: Description of what code to generate
191 |             model_context: Optional context about the model (dimensions, parameters, etc.)
192 | 
193 |         Returns:
194 |             Result dictionary with code, execution result, and any output
195 |         """
196 |         # Step 1: Generate Python code based on the prompt
197 |         code = await self._generate_code_from_prompt(prompt, model_context)
198 | 
199 |         # Step 2: Execute the generated code in Rhino
200 |         result = await self.send_code_to_rhino(code)
201 | 
202 |         # Return both the code and the execution result
203 |         return {
204 |             "result": result.get("result", "error"),
205 |             "code": code,
206 |             "response": result.get("response", ""),
207 |             "error": result.get("error", ""),
208 |         }
209 | 
210 |     async def _generate_code_from_prompt(
211 |         self, prompt: str, model_context: Optional[Dict[str, Any]] = None
212 |     ) -> str:
213 |         """Generate Rhino Python code from a text prompt.
214 | 
215 |         Args:
216 |             prompt: Description of what the code should do
217 |             model_context: Optional context about the model
218 | 
219 |         Returns:
220 |             Generated Python code as a string
221 |         """
222 |         # Add standard imports for Rhino Python code
223 |         code = """
224 | import Rhino
225 | import rhinoscriptsyntax as rs
226 | import scriptcontext as sc
227 | import System
228 | from Rhino.Geometry import *
229 | 
230 | # Disable redraw to improve performance
231 | rs.EnableRedraw(False)
232 | """
233 | 
234 |         # Add code based on the prompt
235 |         prompt_lower = prompt.lower()
236 | 
237 |         if "circle" in prompt_lower:
238 |             radius = model_context.get("radius", 10.0) if model_context else 10.0
239 |             center_x = model_context.get("center_x", 0.0) if model_context else 0.0
240 |             center_y = model_context.get("center_y", 0.0) if model_context else 0.0
241 |             center_z = model_context.get("center_z", 0.0) if model_context else 0.0
242 |             code += f"""
243 | # Create a circle based on prompt: {prompt}
244 | center = Point3d({center_x}, {center_y}, {center_z})
245 | circle = Circle(Plane.WorldXY, center, {radius})
246 | circle_id = sc.doc.Objects.AddCircle(circle)
247 | if circle_id:
248 |     rs.ObjectName(circle_id, "GeneratedCircle")
249 |     print("Created a circle!")
250 | else:
251 |     print("Failed to create circle")
252 | """
253 |         return code
254 | 
255 |     async def send_code_to_gh(self, code: str, file_path: str) -> Dict[str, Any]:
256 |         """Send Python code to a file for Grasshopper to use.
257 | 
258 |         Args:
259 |             code: Python code to save for Grasshopper
260 |             file_path: Path where the Python file should be saved
261 | 
262 |         Returns:
263 |             Dictionary with result and file path
264 |         """
265 |         try:
266 |             # Write the code to the file
267 |             with open(file_path, "w") as f:
268 |                 f.write(code)
269 | 
270 |             return {
271 |                 "result": "success",
272 |                 "file_path": file_path,
273 |                 "message": f"Grasshopper Python file created at {file_path}",
274 |             }
275 | 
276 |         except Exception as e:
277 |             return {"result": "error", "error": str(e)}
278 | 
279 |     async def generate_and_execute_gh_code(
280 |         self,
281 |         prompt: str,
282 |         file_path: str,
283 |         model_context: Optional[Dict[str, Any]] = None,
284 |         component_name: Optional[str] = None,
285 |     ) -> Dict[str, Any]:
286 |         """Generate Grasshopper Python code from a prompt and save it for execution.
287 | 
288 |         Args:
289 |             prompt: Description of what code to generate
290 |             model_context: Optional context about the model (dimensions, parameters, etc.)
291 |             component_name: Optional name for the GH Python component
292 | 
293 |         Returns:
294 |             Result dictionary with code, file path, and any output
295 |         """
296 |         # Step 1: Generate Python code based on the prompt
297 |         code = await self._generate_gh_code_from_prompt(prompt, model_context, component_name)
298 | 
299 |         # Step 2: Save the generated code for Grasshopper to use
300 |         result = await self.send_code_to_gh(code, file_path)
301 | 
302 |         # Return both the code and the result
303 |         return {
304 |             "result": result.get("result", "error"),
305 |             "code": code,
306 |             "file_path": result.get("file_path", ""),
307 |             "response": result.get("response", ""),
308 |             "error": result.get("error", ""),
309 |         }
310 | 
311 |     async def _generate_gh_code_from_prompt(
312 |         self,
313 |         prompt: str,
314 |         model_context: Optional[Dict[str, Any]] = None,
315 |         component_name: Optional[str] = None,
316 |     ) -> str:
317 |         """Generate Grasshopper Python code from a text prompt.
318 | 
319 |         Args:
320 |             prompt: Description of what the code should do
321 |             model_context: Optional context about the model
322 |             component_name: Optional name for the component
323 | 
324 |         Returns:
325 |             Generated Python code as a string
326 |         """
327 |         # Add component name as a comment
328 |         if component_name:
329 |             code = f"""# Grasshopper Python Component: {component_name}
330 | # Generated from prompt: {prompt}
331 | """
332 |         else:
333 |             code = f"""# Grasshopper Python Component
334 | # Generated from prompt: {prompt}
335 | """
336 | 
337 |         # Add standard imports for Grasshopper Python code
338 |         code += """
339 | import Rhino
340 | import rhinoscriptsyntax as rs
341 | import scriptcontext as sc
342 | import Rhino.Geometry as rg
343 | import ghpythonlib.components as ghcomp
344 | import math
345 | """
346 |         # Add code based on the prompt
347 |         prompt_lower = prompt.lower()
348 | 
349 |         if "circle" in prompt_lower:
350 |             radius = model_context.get("radius", 10.0) if model_context else 10.0
351 |             center_x = model_context.get("center_x", 0.0) if model_context else 0.0
352 |             center_y = model_context.get("center_y", 0.0) if model_context else 0.0
353 |             center_z = model_context.get("center_z", 0.0) if model_context else 0.0
354 |             code += f"""
355 | # Create a circle based on prompt: {prompt}
356 | center = rg.Point3d({center_x}, {center_y}, {center_z})
357 | circle = rg.Circle(rg.Plane.WorldXY, center, {radius})
358 | print("Created a circle!")
359 | """
360 |         return code
361 | 
362 |     def _initialize_compute(self) -> None:
363 |         """Initialize connection to compute.rhino3d.com."""
364 |         if not self.config.compute_url or not self.config.compute_api_key:
365 |             raise ValueError("Compute API URL and key required for compute API connection")
366 | 
367 |         # We'll use requests for API calls
368 | 
369 |     async def close(self) -> None:
370 |         """Close connection to Rhino/Grasshopper."""
371 |         # Cleanup as needed
372 |         self.connected = False
373 | 
374 |     async def execute_code(self, code: str, parameters: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
375 |         """Execute operations on Rhino geometry."""
376 |         if not self.connected:
377 |             raise RuntimeError("Not connected to Rhino geometry system")
378 | 
379 |         if self.config.use_compute_api:
380 |             return await self._execute_compute(code, parameters)
381 |         else:
382 |             # Check if we're using rhino3dm
383 |             if self.rhino_instance.get("use_rhino3dm", False):
384 |                 return await self._execute_rhino3dm(code, parameters)
385 |             else:
386 |                 return await self._execute_rhino(code, parameters)
387 | 
388 |     async def _execute_rhino(self, code: str, parameters: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
389 |         """Execute code directly in Rhino (Windows only)."""
390 |         # Existing Rhino execution code
391 |         globals_dict = dict(self.rhino_instance)
392 | 
393 |         # Add parameters to context
394 |         if parameters:
395 |             globals_dict.update(parameters)
396 | 
397 |         # Execute the code
398 |         locals_dict = {}
399 |         try:
400 |             exec(code, globals_dict, locals_dict)
401 |             return {"result": "success", "data": locals_dict.get("result", None)}
402 |         except Exception as e:
403 |             # More detailed error reporting for Windows
404 |             import traceback
405 | 
406 |             error_trace = traceback.format_exc()
407 |             return {"result": "error", "error": str(e), "traceback": error_trace}
408 | 
409 |     async def _execute_rhino3dm(
410 |         self, code: str, parameters: Optional[Dict[str, Any]] = None
411 |     ) -> Dict[str, Any]:
412 |         """Execute code using rhino3dm library."""
413 |         # Create execution context with rhino3dm
414 |         r3d = self.rhino_instance["r3d"]
415 |         globals_dict = {"r3d": r3d, "parameters": parameters or {}}
416 | 
417 |         # Add parameters to context
418 |         if parameters:
419 |             globals_dict.update(parameters)
420 | 
421 |         # Execute the code
422 |         locals_dict = {}
423 |         try:
424 |             exec(code, globals_dict, locals_dict)
425 |             return {"result": "success", "data": locals_dict.get("result", None)}
426 |         except Exception as e:
427 |             return {"result": "error", "error": str(e)}
428 | 
429 |     async def _execute_compute(
430 |         self, code: str, parameters: Optional[Dict[str, Any]] = None
431 |     ) -> Dict[str, Any]:
432 |         """Execute code via compute.rhino3d.com API."""
433 |         # Existing Compute API code
434 |         import requests
435 | 
436 |         url = f"{self.config.compute_url}/grasshopper"
437 | 
438 |         # Prepare request payload
439 |         payload = {"algo": code, "pointer": None, "values": parameters or {}}
440 | 
441 |         headers = {
442 |             "Authorization": f"Bearer {self.config.compute_api_key}",
443 |             "Content-Type": "application/json",
444 |         }
445 | 
446 |         try:
447 |             response = requests.post(url, json=payload, headers=headers)
448 |             response.raise_for_status()
449 |             return {"result": "success", "data": response.json()}
450 |         except Exception as e:
451 |             return {"result": "error", "error": str(e)}
452 | 
453 |     async def read_3dm_file(self, file_path: str) -> Dict[str, Any]:
454 |         """Read a .3dm file and return its model."""
455 |         if not self.connected:
456 |             raise RuntimeError("Not connected to Rhino geometry system")
457 | 
458 |         try:
459 |             if self.rhino_instance.get("use_rhino3dm", False):
460 |                 r3d = self.rhino_instance["r3d"]
461 |                 model = r3d.File3dm.Read(file_path)
462 |                 if model:
463 |                     return {"result": "success", "model": model}
464 |                 else:
465 |                     return {"result": "error", "error": f"Failed to open file: {file_path}"}
466 |             else:
467 |                 # For RhinoInside on Windows, use a different approach
468 |                 code = """
469 |                 import Rhino
470 |                 result = {
471 |                     "model": Rhino.FileIO.File3dm.Read(file_path)
472 |                 }
473 |                 """
474 |                 return await self._execute_rhino(code, {"file_path": file_path})
475 |         except Exception as e:
476 |             return {"result": "error", "error": str(e)}
477 | 
478 |     ### For Grasshopper
479 |     async def create_gh_script_component(
480 |         self, description: str, inputs: List[Dict[str, Any]], outputs: List[Dict[str, Any]], code: str
481 |     ) -> Dict[str, Any]:
482 |         """Create a Python script component in a Grasshopper definition.
483 | 
484 |         Args:
485 |             description: Description of the component
486 |             inputs: List of input parameters
487 |             outputs: List of output parameters
488 |             code: Python code for the component
489 | 
490 |         Returns:
491 |             Result dictionary with component_id on success
492 |         """
493 |         if not self.connected:
494 |             return {"result": "error", "error": "Not connected to Rhino/Grasshopper"}
495 | 
496 |         # Generate a unique component ID
497 |         component_id = f"py_{str(uuid.uuid4())[:8]}"
498 | 
499 |         if self.config.use_compute_api:
500 |             # Implementation for compute API
501 |             return await self._create_gh_script_component_compute(
502 |                 component_id, description, inputs, outputs, code
503 |             )
504 |         elif platform.system() == "Windows" and not self.rhino_instance.get("use_rhino3dm", True):
505 |             # Implementation for RhinoInside (Windows)
506 |             return await self._create_gh_script_component_rhinoinside(
507 |                 component_id, description, inputs, outputs, code
508 |             )
509 |         else:
510 |             # We can't directly create Grasshopper components with rhino3dm
511 |             return {
512 |                 "result": "error",
513 |                 "error": "Creating Grasshopper components requires RhinoInside or Compute API",
514 |             }
515 | 
516 |     async def _create_gh_script_component_rhinoinside(
517 |         self,
518 |         component_id: str,
519 |         description: str,
520 |         inputs: List[Dict[str, Any]],
521 |         outputs: List[Dict[str, Any]],
522 |         code: str,
523 |     ) -> Dict[str, Any]:
524 |         """Create a Python script component using RhinoInside."""
525 |         # Using the RhinoInside context
526 |         execution_code = """
527 |         import Rhino
528 |         import Grasshopper
529 |         import GhPython
530 |         from Grasshopper.Kernel import GH_Component
531 |         
532 |         # Access the current Grasshopper document
533 |         gh_doc = Grasshopper.Instances.ActiveCanvas.Document
534 |         
535 |         # Create a new Python component
536 |         py_comp = GhPython.Component.PythonComponent()
537 |         py_comp.NickName = description
538 |         py_comp.Name = description
539 |         py_comp.Description = description
540 |         py_comp.ComponentGuid = System.Guid(component_id)
541 |         
542 |         # Set up inputs
543 |         for i, inp in enumerate(inputs):
544 |             name = inp["name"]
545 |             param_type = inp.get("type", "object")
546 |             # Convert param_type to Grasshopper parameter type
547 |             access_type = 0  # 0 = item access
548 |             py_comp.Params.Input[i].Name = name
549 |             py_comp.Params.Input[i].NickName = name
550 |             py_comp.Params.Input[i].Description = inp.get("description", "")
551 |         
552 |         # Set up outputs
553 |         for i, out in enumerate(outputs):
554 |             name = out["name"]
555 |             param_type = out.get("type", "object")
556 |             # Convert param_type to Grasshopper parameter type
557 |             py_comp.Params.Output[i].Name = name
558 |             py_comp.Params.Output[i].NickName = name
559 |             py_comp.Params.Output[i].Description = out.get("description", "")
560 |         
561 |         # Set the Python code
562 |         py_comp.ScriptSource = code
563 |         
564 |         # Add the component to the document
565 |         gh_doc.AddObject(py_comp, False)
566 |         
567 |         # Set the position on canvas (centered)
568 |         py_comp.Attributes.Pivot = Grasshopper.Kernel.GH_Convert.ToPoint(
569 |             Rhino.Geometry.Point2d(gh_doc.Bounds.Center.X, gh_doc.Bounds.Center.Y)
570 |         )
571 |         
572 |         # Update the document
573 |         gh_doc.NewSolution(True)
574 |         
575 |         # Store component for reference
576 |         result = {
577 |             "component_id": str(component_id)
578 |         }
579 |         """
580 | 
581 |         # Execute the code
582 |         return await self._execute_rhino(
583 |             execution_code,
584 |             {
585 |                 "component_id": component_id,
586 |                 "description": description,
587 |                 "inputs": inputs,
588 |                 "outputs": outputs,
589 |                 "code": code,
590 |             },
591 |         )
592 | 
593 |     async def _create_gh_script_component_compute(
594 |         self,
595 |         component_id: str,
596 |         description: str,
597 |         inputs: List[Dict[str, Any]],
598 |         outputs: List[Dict[str, Any]],
599 |         code: str,
600 |     ) -> Dict[str, Any]:
601 |         """Create a Python script component using Compute API."""
602 |         import requests
603 | 
604 |         url = f"{self.config.compute_url}/grasshopper/scriptcomponent"
605 | 
606 |         # Prepare payload
607 |         payload = {
608 |             "id": component_id,
609 |             "name": description,
610 |             "description": description,
611 |             "inputs": inputs,
612 |             "outputs": outputs,
613 |             "code": code,
614 |         }
615 | 
616 |         headers = {
617 |             "Authorization": f"Bearer {self.config.compute_api_key}",
618 |             "Content-Type": "application/json",
619 |         }
620 | 
621 |         try:
622 |             response = requests.post(url, json=payload, headers=headers)
623 |             response.raise_for_status()
624 |             return {"result": "success", "component_id": component_id, "data": response.json()}
625 |         except Exception as e:
626 |             return {"result": "error", "error": str(e)}
627 | 
628 |     async def add_gh_component(
629 |         self, component_name: str, component_type: str, parameters: Dict[str, Any]
630 |     ) -> Dict[str, Any]:
631 |         """Add a component from an existing Grasshopper plugin.
632 | 
633 |         Args:
634 |             component_name: Name of the component
635 |             component_type: Type/category of the component
636 |             parameters: Component parameters and settings
637 | 
638 |         Returns:
639 |             Result dictionary with component_id on success
640 |         """
641 |         if not self.connected:
642 |             return {"result": "error", "error": "Not connected to Rhino/Grasshopper"}
643 | 
644 |         # Generate a unique component ID
645 |         component_id = f"comp_{str(uuid.uuid4())[:8]}"
646 | 
647 |         if self.config.use_compute_api:
648 |             # Implementation for compute API
649 |             return await self._add_gh_component_compute(
650 |                 component_id, component_name, component_type, parameters
651 |             )
652 |         elif platform.system() == "Windows" and not self.rhino_instance.get("use_rhino3dm", True):
653 |             # Implementation for RhinoInside
654 |             return await self._add_gh_component_rhinoinside(
655 |                 component_id, component_name, component_type, parameters
656 |             )
657 |         else:
658 |             return {
659 |                 "result": "error",
660 |                 "error": "Adding Grasshopper components requires RhinoInside or Compute API",
661 |             }
662 | 
663 |     async def _add_gh_component_rhinoinside(
664 |         self, component_id: str, component_name: str, component_type: str, parameters: Dict[str, Any]
665 |     ) -> Dict[str, Any]:
666 |         """Add a Grasshopper component using RhinoInside."""
667 |         execution_code = """
668 |         import Rhino
669 |         import Grasshopper
670 |         from Grasshopper.Kernel import GH_ComponentServer
671 |         
672 |         # Access the current Grasshopper document
673 |         gh_doc = Grasshopper.Instances.ActiveCanvas.Document
674 |         
675 |         # Find the component by name and type
676 |         server = GH_ComponentServer.FindServer(component_name, component_type)
677 |         if server is None:
678 |             raise ValueError(f"Component '{component_name}' of type '{component_type}' not found")
679 |         
680 |         # Create the component instance
681 |         component = server.Create()
682 |         component.ComponentGuid = System.Guid(component_id)
683 |         
684 |         # Set parameters
685 |         for param_name, param_value in parameters.items():
686 |             if hasattr(component, param_name):
687 |                 setattr(component, param_name, param_value)
688 |         
689 |         # Add the component to the document
690 |         gh_doc.AddObject(component, False)
691 |         
692 |         # Set the position on canvas
693 |         component.Attributes.Pivot = Grasshopper.Kernel.GH_Convert.ToPoint(
694 |             Rhino.Geometry.Point2d(gh_doc.Bounds.Center.X, gh_doc.Bounds.Center.Y)
695 |         )
696 |         
697 |         # Update the document
698 |         gh_doc.NewSolution(True)
699 |         
700 |         # Return component info
701 |         result = {
702 |             "component_id": str(component_id)
703 |         }
704 |         """
705 | 
706 |         return await self._execute_rhino(
707 |             execution_code,
708 |             {
709 |                 "component_id": component_id,
710 |                 "component_name": component_name,
711 |                 "component_type": component_type,
712 |                 "parameters": parameters,
713 |             },
714 |         )
715 | 
716 |     async def _add_gh_component_compute(
717 |         self, component_id: str, component_name: str, component_type: str, parameters: Dict[str, Any]
718 |     ) -> Dict[str, Any]:
719 |         """Add a Grasshopper component using Compute API."""
720 |         import requests
721 | 
722 |         url = f"{self.config.compute_url}/grasshopper/component"
723 | 
724 |         # Prepare payload
725 |         payload = {
726 |             "id": component_id,
727 |             "name": component_name,
728 |             "type": component_type,
729 |             "parameters": parameters,
730 |         }
731 | 
732 |         headers = {
733 |             "Authorization": f"Bearer {self.config.compute_api_key}",
734 |             "Content-Type": "application/json",
735 |         }
736 | 
737 |         try:
738 |             response = requests.post(url, json=payload, headers=headers)
739 |             response.raise_for_status()
740 |             return {"result": "success", "component_id": component_id, "data": response.json()}
741 |         except Exception as e:
742 |             return {"result": "error", "error": str(e)}
743 | 
744 |     async def connect_gh_components(
745 |         self, source_id: str, source_param: str, target_id: str, target_param: str
746 |     ) -> Dict[str, Any]:
747 |         """Connect parameters between Grasshopper components.
748 | 
749 |         Args:
750 |             source_id: Source component ID
751 |             source_param: Source parameter name
752 |             target_id: Target component ID
753 |             target_param: Target parameter name
754 | 
755 |         Returns:
756 |             Result dictionary
757 |         """
758 |         if not self.connected:
759 |             return {"result": "error", "error": "Not connected to Rhino/Grasshopper"}
760 | 
761 |         if self.config.use_compute_api:
762 |             return await self._connect_gh_components_compute(source_id, source_param, target_id, target_param)
763 |         elif platform.system() == "Windows" and not self.rhino_instance.get("use_rhino3dm", True):
764 |             return await self._connect_gh_components_rhinoinside(
765 |                 source_id, source_param, target_id, target_param
766 |             )
767 |         else:
768 |             return {
769 |                 "result": "error",
770 |                 "error": "Connecting Grasshopper components requires RhinoInside or Compute API",
771 |             }
772 | 
773 |     async def _connect_gh_components_rhinoinside(
774 |         self, source_id: str, source_param: str, target_id: str, target_param: str
775 |     ) -> Dict[str, Any]:
776 |         """Connect Grasshopper components using RhinoInside."""
777 |         execution_code = """
778 |         import Rhino
779 |         import Grasshopper
780 |         from Grasshopper.Kernel import GH_Document
781 |         
782 |         # Access the current Grasshopper document
783 |         gh_doc = Grasshopper.Instances.ActiveCanvas.Document
784 |         
785 |         # Find the source component
786 |         source = None
787 |         for obj in gh_doc.Objects:
788 |             if str(obj.ComponentGuid) == source_id:
789 |                 source = obj
790 |                 break
791 |         
792 |         if source is None:
793 |             raise ValueError(f"Source component with ID {source_id} not found")
794 |         
795 |         # Find the target component
796 |         target = None
797 |         for obj in gh_doc.Objects:
798 |             if str(obj.ComponentGuid) == target_id:
799 |                 target = obj
800 |                 break
801 |         
802 |         if target is None:
803 |             raise ValueError(f"Target component with ID {target_id} not found")
804 |         
805 |         # Find the source output parameter
806 |         source_output = None
807 |         for i, param in enumerate(source.Params.Output):
808 |             if param.Name == source_param:
809 |                 source_output = param
810 |                 break
811 |         
812 |         if source_output is None:
813 |             raise ValueError(f"Source parameter {source_param} not found on component {source_id}")
814 |         
815 |         # Find the target input parameter
816 |         target_input = None
817 |         for i, param in enumerate(target.Params.Input):
818 |             if param.Name == target_param:
819 |                 target_input = param
820 |                 break
821 |         
822 |         if target_input is None:
823 |             raise ValueError(f"Target parameter {target_param} not found on component {target_id}")
824 |         
825 |         # Connect the parameters
826 |         gh_doc.GraftIO(source_output.Recipients, target_input.Sources)
827 |         
828 |         # Update the document
829 |         gh_doc.NewSolution(True)
830 |         
831 |         result = {
832 |             "success": True
833 |         }
834 |         """
835 | 
836 |         return await self._execute_rhino(
837 |             execution_code,
838 |             {
839 |                 "source_id": source_id,
840 |                 "source_param": source_param,
841 |                 "target_id": target_id,
842 |                 "target_param": target_param,
843 |             },
844 |         )
845 | 
846 |     async def _connect_gh_components_compute(
847 |         self, source_id: str, source_param: str, target_id: str, target_param: str
848 |     ) -> Dict[str, Any]:
849 |         """Connect Grasshopper components using Compute API."""
850 |         import requests
851 | 
852 |         url = f"{self.config.compute_url}/grasshopper/connect"
853 | 
854 |         # Prepare payload
855 |         payload = {
856 |             "source_id": source_id,
857 |             "source_param": source_param,
858 |             "target_id": target_id,
859 |             "target_param": target_param,
860 |         }
861 | 
862 |         headers = {
863 |             "Authorization": f"Bearer {self.config.compute_api_key}",
864 |             "Content-Type": "application/json",
865 |         }
866 | 
867 |         try:
868 |             response = requests.post(url, json=payload, headers=headers)
869 |             response.raise_for_status()
870 |             return {"result": "success", "data": response.json()}
871 |         except Exception as e:
872 |             return {"result": "error", "error": str(e)}
873 | 
874 |     async def run_gh_definition(
875 |         self, file_path: Optional[str] = None, save_output: bool = False, output_path: Optional[str] = None
876 |     ) -> Dict[str, Any]:
877 |         """Run a Grasshopper definition.
878 | 
879 |         Args:
880 |             file_path: Path to the .gh file (or None for current definition)
881 |             save_output: Whether to save the output
882 |             output_path: Path to save the output (if save_output is True)
883 | 
884 |         Returns:
885 |             Result dictionary with execution information
886 |         """
887 |         if not self.connected:
888 |             return {"result": "error", "error": "Not connected to Rhino/Grasshopper"}
889 | 
890 |         if self.config.use_compute_api:
891 |             return await self._run_gh_definition_compute(file_path, save_output, output_path)
892 |         elif platform.system() == "Windows" and not self.rhino_instance.get("use_rhino3dm", True):
893 |             return await self._run_gh_definition_rhinoinside(file_path, save_output, output_path)
894 |         else:
895 |             return {
896 |                 "result": "error",
897 |                 "error": "Running Grasshopper definitions requires RhinoInside or Compute API",
898 |             }
899 | 
900 |     async def _run_gh_definition_rhinoinside(
901 |         self, file_path: Optional[str] = None, save_output: bool = False, output_path: Optional[str] = None
902 |     ) -> Dict[str, Any]:
903 |         """Run a Grasshopper definition using RhinoInside."""
904 |         execution_code = """
905 |         import Rhino
906 |         import Grasshopper
907 |         import time
908 |         
909 |         start_time = time.time()
910 |         
911 |         if file_path:
912 |             # Open the specified Grasshopper definition
913 |             gh_doc = Grasshopper.Kernel.GH_Document()
914 |             gh_doc.LoadDocumentObject(file_path)
915 |         else:
916 |             # Use the current document
917 |             gh_doc = Grasshopper.Instances.ActiveCanvas.Document
918 |         
919 |         # Run the solution
920 |         gh_doc.NewSolution(True)
921 |         
922 |         # Wait for solution to complete
923 |         while gh_doc.SolutionState != Grasshopper.Kernel.GH_ProcessStep.Finished:
924 |             time.sleep(0.1)
925 |         
926 |         execution_time = time.time() - start_time
927 |         
928 |         # Save if requested
929 |         if save_output and output_path:
930 |             gh_doc.SaveAs(output_path, False)
931 |         
932 |         # Get a summary of the outputs
933 |         output_summary = []
934 |         for obj in gh_doc.Objects:
935 |             if obj.Attributes.GetTopLevel.DocObject is not None:
936 |                 for param in obj.Params.Output:
937 |                     if param.VolatileDataCount > 0:
938 |                         output_summary.append({
939 |                             "component": obj.NickName,
940 |                             "param": param.Name,
941 |                             "data_count": param.VolatileDataCount
942 |                         })
943 |         
944 |         result = {
945 |             "execution_time": execution_time,
946 |             "output_summary": output_summary
947 |         }
948 |         """
949 | 
950 |         return await self._execute_rhino(
951 |             execution_code, {"file_path": file_path, "save_output": save_output, "output_path": output_path}
952 |         )
953 | 
954 |     async def _run_gh_definition_compute(
955 |         self, file_path: Optional[str] = None, save_output: bool = False, output_path: Optional[str] = None
956 |     ) -> Dict[str, Any]:
957 |         """Run a Grasshopper definition using Compute API."""
958 |         import requests
959 | 
960 |         url = f"{self.config.compute_url}/grasshopper/run"
961 | 
962 |         # Prepare payload
963 |         payload = {"file_path": file_path, "save_output": save_output, "output_path": output_path}
964 | 
965 |         headers = {
966 |             "Authorization": f"Bearer {self.config.compute_api_key}",
967 |             "Content-Type": "application/json",
968 |         }
969 | 
970 |         try:
971 |             response = requests.post(url, json=payload, headers=headers)
972 |             response.raise_for_status()
973 |             return {"result": "success", "data": response.json()}
974 |         except Exception as e:
975 |             return {"result": "error", "error": str(e)}
976 | 
```