Initial commit: RhinoMCP project structure

- MCP Server (Python) with full tool definitions for Rhino/Grasshopper
- Rhino Plugin (C#) with HTTP server for command execution
- Support for geometry creation, manipulation, boolean operations
- Grasshopper integration: sliders, toggles, components, connections
- MIT License, README with architecture documentation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
architeur
2025-12-29 21:09:28 +01:00
commit e066f9fce5
11 changed files with 2283 additions and 0 deletions

43
.gitignore vendored Normal file
View File

@@ -0,0 +1,43 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
venv/
ENV/
.env
*.egg-info/
dist/
build/
# C# / Visual Studio
bin/
obj/
*.user
*.suo
*.userosscache
*.sln.docstates
.vs/
*.dll
*.exe
*.pdb
*.rhp
# IDE
.idea/
.vscode/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Project specific
*.log
temp/
# Keep examples
!examples/*.gh
!examples/*.3dm

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 RhinoMCP Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

107
README.md Normal file
View File

@@ -0,0 +1,107 @@
# RhinoMCP
Ein MCP-Server (Model Context Protocol) für die Integration von Claude AI mit Rhino 3D und Grasshopper.
## Features
- **Rhino-Geometrie erstellen** - Punkte, Linien, Kurven, Flächen, Volumenkörper
- **Grasshopper-Definitionen** - Komponenten erstellen, verbinden, Parameter setzen
- **GH-Dateien laden/modifizieren** - Bestehende Grasshopper-Definitionen bearbeiten
- **Geometrie analysieren** - Maße, Abstände, Flächen, Volumen berechnen
- **Export** - Verschiedene Formate (3DM, STEP, IGES, STL, etc.)
## Architektur
```
┌─────────────────────────────────────────────────────────┐
│ Claude Code / Claude.ai │
└──────────────────┬──────────────────────────────────────┘
│ MCP Protocol (stdio/SSE)
┌──────────────────▼──────────────────────────────────────┐
│ MCP-Server (Python) │
│ - Tool-Definitionen │
│ - Request/Response Handling │
└──────────────────┬──────────────────────────────────────┘
│ HTTP (localhost:9000)
┌──────────────────▼──────────────────────────────────────┐
│ Rhino-Plugin (C# .rhp) │
│ ├─ RhinoCommon API (Geometrie) │
│ ├─ Grasshopper SDK (Komponenten, Verbindungen) │
│ └─ HTTP-Server (empfängt Befehle) │
└─────────────────────────────────────────────────────────┘
```
## Voraussetzungen
- Rhino 8 oder Rhino 9 WIP
- Python 3.10+
- .NET Framework 4.8 / .NET 7.0 (für Rhino 8)
## Installation
### 1. MCP-Server (Python)
```bash
cd src/mcp-server
pip install -r requirements.txt
```
### 2. Rhino-Plugin
1. Öffne `src/rhino-plugin/RhinoMCP.sln` in Visual Studio
2. Build das Projekt
3. Kopiere `RhinoMCP.rhp` nach `%APPDATA%\McNeel\Rhinoceros\8.0\Plug-ins\`
4. Starte Rhino und aktiviere das Plugin
### 3. Claude Code Konfiguration
Füge in `~/.claude/claude_desktop_config.json` hinzu:
```json
{
"mcpServers": {
"rhino": {
"command": "python",
"args": ["C:/Entwicklung/Rhino-MCP/src/mcp-server/server.py"]
}
}
}
```
## Verwendung
Nach der Installation kann Claude direkt mit Rhino kommunizieren:
```
"Erstelle einen Würfel mit 10mm Kantenlänge"
"Öffne die Grasshopper-Definition und setze den Slider 'Breite' auf 500"
"Exportiere die aktuelle Geometrie als STEP-Datei"
```
## Projektstruktur
```
RhinoMCP/
├── src/
│ ├── mcp-server/ # Python MCP-Server
│ │ ├── server.py # Hauptserver (stdio + SSE)
│ │ ├── tools/ # Tool-Implementierungen
│ │ └── requirements.txt
│ └── rhino-plugin/ # C# Rhino-Plugin
│ └── RhinoMCP/
│ ├── RhinoMCP.csproj
│ ├── Plugin.cs
│ └── HttpServer.cs
├── docs/ # Dokumentation
├── examples/ # Beispiel-Definitionen
├── LICENSE
└── README.md
```
## Lizenz
MIT License - siehe [LICENSE](LICENSE)
## Mitwirken
Beiträge sind willkommen! Bitte erstelle einen Issue oder Pull Request.

View File

@@ -0,0 +1,5 @@
mcp>=1.0.0
httpx>=0.25.0
pydantic>=2.0.0
uvicorn>=0.24.0
starlette>=0.32.0

705
src/mcp-server/server.py Normal file
View File

@@ -0,0 +1,705 @@
#!/usr/bin/env python3
"""
RhinoMCP - MCP Server for Rhino 3D and Grasshopper integration.
This server provides tools for Claude to interact with Rhino and Grasshopper
via the Model Context Protocol (MCP).
"""
import asyncio
import json
import logging
from typing import Any
import httpx
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("rhino-mcp")
# Rhino Plugin HTTP endpoint
RHINO_ENDPOINT = "http://localhost:9000"
# Create MCP server instance
server = Server("rhino-mcp")
async def call_rhino(action: str, params: dict[str, Any] = None) -> dict[str, Any]:
"""Send a command to the Rhino plugin and return the response."""
async with httpx.AsyncClient(timeout=30.0) as client:
try:
response = await client.post(
f"{RHINO_ENDPOINT}/execute",
json={"action": action, "params": params or {}}
)
response.raise_for_status()
return response.json()
except httpx.ConnectError:
return {"error": "Cannot connect to Rhino. Is the RhinoMCP plugin running?"}
except Exception as e:
return {"error": str(e)}
@server.list_tools()
async def list_tools() -> list[Tool]:
"""Return the list of available tools."""
return [
# === Rhino Document Tools ===
Tool(
name="rhino_get_document_info",
description="Get information about the current Rhino document (file path, units, layers, object count)",
inputSchema={
"type": "object",
"properties": {},
"required": []
}
),
Tool(
name="rhino_get_selected_objects",
description="Get information about currently selected objects in Rhino",
inputSchema={
"type": "object",
"properties": {},
"required": []
}
),
Tool(
name="rhino_get_layers",
description="Get all layers in the current Rhino document",
inputSchema={
"type": "object",
"properties": {},
"required": []
}
),
# === Geometry Creation Tools ===
Tool(
name="rhino_create_point",
description="Create a point in Rhino at the specified coordinates",
inputSchema={
"type": "object",
"properties": {
"x": {"type": "number", "description": "X coordinate"},
"y": {"type": "number", "description": "Y coordinate"},
"z": {"type": "number", "description": "Z coordinate", "default": 0}
},
"required": ["x", "y"]
}
),
Tool(
name="rhino_create_line",
description="Create a line between two points",
inputSchema={
"type": "object",
"properties": {
"start": {
"type": "object",
"properties": {
"x": {"type": "number"},
"y": {"type": "number"},
"z": {"type": "number", "default": 0}
},
"required": ["x", "y"]
},
"end": {
"type": "object",
"properties": {
"x": {"type": "number"},
"y": {"type": "number"},
"z": {"type": "number", "default": 0}
},
"required": ["x", "y"]
}
},
"required": ["start", "end"]
}
),
Tool(
name="rhino_create_polyline",
description="Create a polyline from a list of points",
inputSchema={
"type": "object",
"properties": {
"points": {
"type": "array",
"items": {
"type": "object",
"properties": {
"x": {"type": "number"},
"y": {"type": "number"},
"z": {"type": "number", "default": 0}
},
"required": ["x", "y"]
},
"description": "List of points defining the polyline"
},
"closed": {"type": "boolean", "default": False, "description": "Whether to close the polyline"}
},
"required": ["points"]
}
),
Tool(
name="rhino_create_circle",
description="Create a circle with center point and radius",
inputSchema={
"type": "object",
"properties": {
"center": {
"type": "object",
"properties": {
"x": {"type": "number"},
"y": {"type": "number"},
"z": {"type": "number", "default": 0}
},
"required": ["x", "y"]
},
"radius": {"type": "number", "description": "Circle radius"}
},
"required": ["center", "radius"]
}
),
Tool(
name="rhino_create_rectangle",
description="Create a rectangle from corner point, width and height",
inputSchema={
"type": "object",
"properties": {
"corner": {
"type": "object",
"properties": {
"x": {"type": "number"},
"y": {"type": "number"},
"z": {"type": "number", "default": 0}
},
"required": ["x", "y"]
},
"width": {"type": "number", "description": "Rectangle width"},
"height": {"type": "number", "description": "Rectangle height"}
},
"required": ["corner", "width", "height"]
}
),
Tool(
name="rhino_create_sphere",
description="Create a sphere with center point and radius",
inputSchema={
"type": "object",
"properties": {
"center": {
"type": "object",
"properties": {
"x": {"type": "number"},
"y": {"type": "number"},
"z": {"type": "number", "default": 0}
},
"required": ["x", "y"]
},
"radius": {"type": "number", "description": "Sphere radius"}
},
"required": ["center", "radius"]
}
),
Tool(
name="rhino_create_box",
description="Create a box (cuboid) from corner point and dimensions",
inputSchema={
"type": "object",
"properties": {
"corner": {
"type": "object",
"properties": {
"x": {"type": "number"},
"y": {"type": "number"},
"z": {"type": "number", "default": 0}
},
"required": ["x", "y"]
},
"width": {"type": "number", "description": "Box width (X)"},
"depth": {"type": "number", "description": "Box depth (Y)"},
"height": {"type": "number", "description": "Box height (Z)"}
},
"required": ["corner", "width", "depth", "height"]
}
),
Tool(
name="rhino_create_cylinder",
description="Create a cylinder from base center, radius and height",
inputSchema={
"type": "object",
"properties": {
"base_center": {
"type": "object",
"properties": {
"x": {"type": "number"},
"y": {"type": "number"},
"z": {"type": "number", "default": 0}
},
"required": ["x", "y"]
},
"radius": {"type": "number", "description": "Cylinder radius"},
"height": {"type": "number", "description": "Cylinder height"}
},
"required": ["base_center", "radius", "height"]
}
),
Tool(
name="rhino_create_surface_from_curves",
description="Create a surface from boundary curves (planar surface or loft)",
inputSchema={
"type": "object",
"properties": {
"curve_ids": {
"type": "array",
"items": {"type": "string"},
"description": "List of curve GUIDs to create surface from"
},
"method": {
"type": "string",
"enum": ["planar", "loft"],
"default": "planar",
"description": "Surface creation method"
}
},
"required": ["curve_ids"]
}
),
Tool(
name="rhino_extrude_curve",
description="Extrude a curve along a direction to create a surface or solid",
inputSchema={
"type": "object",
"properties": {
"curve_id": {"type": "string", "description": "GUID of the curve to extrude"},
"direction": {
"type": "object",
"properties": {
"x": {"type": "number", "default": 0},
"y": {"type": "number", "default": 0},
"z": {"type": "number", "default": 1}
},
"description": "Extrusion direction vector"
},
"distance": {"type": "number", "description": "Extrusion distance"},
"cap": {"type": "boolean", "default": True, "description": "Cap the ends to create a solid"}
},
"required": ["curve_id", "distance"]
}
),
# === Object Manipulation Tools ===
Tool(
name="rhino_move_object",
description="Move an object by a translation vector",
inputSchema={
"type": "object",
"properties": {
"object_id": {"type": "string", "description": "GUID of the object to move"},
"translation": {
"type": "object",
"properties": {
"x": {"type": "number"},
"y": {"type": "number"},
"z": {"type": "number", "default": 0}
},
"required": ["x", "y"]
}
},
"required": ["object_id", "translation"]
}
),
Tool(
name="rhino_copy_object",
description="Copy an object with optional translation",
inputSchema={
"type": "object",
"properties": {
"object_id": {"type": "string", "description": "GUID of the object to copy"},
"translation": {
"type": "object",
"properties": {
"x": {"type": "number", "default": 0},
"y": {"type": "number", "default": 0},
"z": {"type": "number", "default": 0}
},
"description": "Optional translation for the copy"
}
},
"required": ["object_id"]
}
),
Tool(
name="rhino_rotate_object",
description="Rotate an object around an axis",
inputSchema={
"type": "object",
"properties": {
"object_id": {"type": "string", "description": "GUID of the object to rotate"},
"angle_degrees": {"type": "number", "description": "Rotation angle in degrees"},
"axis": {
"type": "string",
"enum": ["x", "y", "z"],
"default": "z",
"description": "Rotation axis"
},
"center": {
"type": "object",
"properties": {
"x": {"type": "number", "default": 0},
"y": {"type": "number", "default": 0},
"z": {"type": "number", "default": 0}
},
"description": "Rotation center point"
}
},
"required": ["object_id", "angle_degrees"]
}
),
Tool(
name="rhino_scale_object",
description="Scale an object from a base point",
inputSchema={
"type": "object",
"properties": {
"object_id": {"type": "string", "description": "GUID of the object to scale"},
"scale_factor": {"type": "number", "description": "Uniform scale factor"},
"base_point": {
"type": "object",
"properties": {
"x": {"type": "number", "default": 0},
"y": {"type": "number", "default": 0},
"z": {"type": "number", "default": 0}
},
"description": "Base point for scaling"
}
},
"required": ["object_id", "scale_factor"]
}
),
Tool(
name="rhino_delete_object",
description="Delete an object by its GUID",
inputSchema={
"type": "object",
"properties": {
"object_id": {"type": "string", "description": "GUID of the object to delete"}
},
"required": ["object_id"]
}
),
Tool(
name="rhino_set_layer",
description="Move an object to a specific layer",
inputSchema={
"type": "object",
"properties": {
"object_id": {"type": "string", "description": "GUID of the object"},
"layer_name": {"type": "string", "description": "Name of the target layer"}
},
"required": ["object_id", "layer_name"]
}
),
# === Analysis Tools ===
Tool(
name="rhino_get_object_info",
description="Get detailed information about an object (type, dimensions, area, volume)",
inputSchema={
"type": "object",
"properties": {
"object_id": {"type": "string", "description": "GUID of the object"}
},
"required": ["object_id"]
}
),
Tool(
name="rhino_measure_distance",
description="Measure distance between two points",
inputSchema={
"type": "object",
"properties": {
"point1": {
"type": "object",
"properties": {
"x": {"type": "number"},
"y": {"type": "number"},
"z": {"type": "number", "default": 0}
},
"required": ["x", "y"]
},
"point2": {
"type": "object",
"properties": {
"x": {"type": "number"},
"y": {"type": "number"},
"z": {"type": "number", "default": 0}
},
"required": ["x", "y"]
}
},
"required": ["point1", "point2"]
}
),
# === Boolean Operations ===
Tool(
name="rhino_boolean_union",
description="Create a boolean union of multiple objects",
inputSchema={
"type": "object",
"properties": {
"object_ids": {
"type": "array",
"items": {"type": "string"},
"description": "List of object GUIDs to union"
},
"delete_input": {"type": "boolean", "default": True, "description": "Delete input objects after union"}
},
"required": ["object_ids"]
}
),
Tool(
name="rhino_boolean_difference",
description="Subtract one or more objects from a base object",
inputSchema={
"type": "object",
"properties": {
"base_id": {"type": "string", "description": "GUID of the base object"},
"subtract_ids": {
"type": "array",
"items": {"type": "string"},
"description": "List of object GUIDs to subtract"
},
"delete_input": {"type": "boolean", "default": True, "description": "Delete input objects after operation"}
},
"required": ["base_id", "subtract_ids"]
}
),
Tool(
name="rhino_boolean_intersection",
description="Create a boolean intersection of objects",
inputSchema={
"type": "object",
"properties": {
"object_ids": {
"type": "array",
"items": {"type": "string"},
"description": "List of object GUIDs to intersect"
},
"delete_input": {"type": "boolean", "default": True, "description": "Delete input objects after operation"}
},
"required": ["object_ids"]
}
),
# === Export Tools ===
Tool(
name="rhino_export",
description="Export geometry to a file",
inputSchema={
"type": "object",
"properties": {
"file_path": {"type": "string", "description": "Output file path"},
"format": {
"type": "string",
"enum": ["3dm", "step", "iges", "stl", "obj", "fbx", "dxf"],
"description": "Export format"
},
"object_ids": {
"type": "array",
"items": {"type": "string"},
"description": "Optional: specific objects to export (empty = all)"
}
},
"required": ["file_path", "format"]
}
),
# === Grasshopper Tools ===
Tool(
name="grasshopper_open_definition",
description="Open a Grasshopper definition file",
inputSchema={
"type": "object",
"properties": {
"file_path": {"type": "string", "description": "Path to the .gh or .ghx file"}
},
"required": ["file_path"]
}
),
Tool(
name="grasshopper_get_definition_info",
description="Get information about the current Grasshopper definition (components, parameters, connections)",
inputSchema={
"type": "object",
"properties": {},
"required": []
}
),
Tool(
name="grasshopper_set_slider_value",
description="Set the value of a number slider in Grasshopper",
inputSchema={
"type": "object",
"properties": {
"slider_name": {"type": "string", "description": "Name or nickname of the slider"},
"value": {"type": "number", "description": "New value for the slider"}
},
"required": ["slider_name", "value"]
}
),
Tool(
name="grasshopper_set_toggle_value",
description="Set the value of a boolean toggle in Grasshopper",
inputSchema={
"type": "object",
"properties": {
"toggle_name": {"type": "string", "description": "Name or nickname of the toggle"},
"value": {"type": "boolean", "description": "New value for the toggle"}
},
"required": ["toggle_name", "value"]
}
),
Tool(
name="grasshopper_set_panel_text",
description="Set the text content of a panel in Grasshopper",
inputSchema={
"type": "object",
"properties": {
"panel_name": {"type": "string", "description": "Name or nickname of the panel"},
"text": {"type": "string", "description": "New text content"}
},
"required": ["panel_name", "text"]
}
),
Tool(
name="grasshopper_add_component",
description="Add a component to the Grasshopper canvas",
inputSchema={
"type": "object",
"properties": {
"component_name": {"type": "string", "description": "Name of the component (e.g., 'Circle', 'Move', 'Extrude')"},
"position": {
"type": "object",
"properties": {
"x": {"type": "number", "description": "X position on canvas"},
"y": {"type": "number", "description": "Y position on canvas"}
},
"required": ["x", "y"]
}
},
"required": ["component_name", "position"]
}
),
Tool(
name="grasshopper_connect_components",
description="Connect two components in Grasshopper",
inputSchema={
"type": "object",
"properties": {
"source_component": {"type": "string", "description": "Name/ID of source component"},
"source_output": {"type": "string", "description": "Name of output parameter"},
"target_component": {"type": "string", "description": "Name/ID of target component"},
"target_input": {"type": "string", "description": "Name of input parameter"}
},
"required": ["source_component", "source_output", "target_component", "target_input"]
}
),
Tool(
name="grasshopper_add_slider",
description="Add a number slider to the Grasshopper canvas",
inputSchema={
"type": "object",
"properties": {
"name": {"type": "string", "description": "Nickname for the slider"},
"min_value": {"type": "number", "description": "Minimum value"},
"max_value": {"type": "number", "description": "Maximum value"},
"current_value": {"type": "number", "description": "Current/default value"},
"position": {
"type": "object",
"properties": {
"x": {"type": "number"},
"y": {"type": "number"}
},
"required": ["x", "y"]
}
},
"required": ["name", "min_value", "max_value", "current_value", "position"]
}
),
Tool(
name="grasshopper_bake",
description="Bake Grasshopper geometry to Rhino",
inputSchema={
"type": "object",
"properties": {
"component_name": {"type": "string", "description": "Name of the component to bake (optional, bakes all if not specified)"},
"layer_name": {"type": "string", "description": "Target layer for baked geometry"}
},
"required": []
}
),
Tool(
name="grasshopper_save_definition",
description="Save the current Grasshopper definition",
inputSchema={
"type": "object",
"properties": {
"file_path": {"type": "string", "description": "Path to save the file (optional, saves to current if not specified)"}
},
"required": []
}
),
Tool(
name="grasshopper_recompute",
description="Force recompute of the Grasshopper definition",
inputSchema={
"type": "object",
"properties": {},
"required": []
}
),
# === Rhino Commands ===
Tool(
name="rhino_run_command",
description="Run a Rhino command by name (advanced usage)",
inputSchema={
"type": "object",
"properties": {
"command": {"type": "string", "description": "Rhino command string (e.g., '_Zoom _All')"}
},
"required": ["command"]
}
),
]
@server.call_tool()
async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]:
"""Handle tool calls by forwarding them to Rhino."""
logger.info(f"Tool called: {name} with arguments: {arguments}")
# Forward the tool call to Rhino
result = await call_rhino(name, arguments)
return [TextContent(type="text", text=json.dumps(result, indent=2, ensure_ascii=False))]
async def main():
"""Run the MCP server."""
logger.info("Starting RhinoMCP server...")
async with stdio_server() as (read_stream, write_stream):
await server.run(
read_stream,
write_stream,
server.create_initialization_options()
)
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -0,0 +1,18 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RhinoMCP", "RhinoMCP\RhinoMCP.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

View File

@@ -0,0 +1,660 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json.Linq;
using Rhino;
using Rhino.DocObjects;
using Rhino.Geometry;
namespace RhinoMCP
{
/// <summary>
/// Handles all commands from the MCP Server and executes them in Rhino.
/// </summary>
public static class CommandHandler
{
public static object Execute(string action, JObject parameters)
{
var doc = RhinoDoc.ActiveDoc;
if (doc == null && !action.StartsWith("rhino_get"))
return new { error = "No active Rhino document" };
return action switch
{
// Document Info
"rhino_get_document_info" => GetDocumentInfo(doc),
"rhino_get_selected_objects" => GetSelectedObjects(doc),
"rhino_get_layers" => GetLayers(doc),
// Geometry Creation
"rhino_create_point" => CreatePoint(doc, parameters),
"rhino_create_line" => CreateLine(doc, parameters),
"rhino_create_polyline" => CreatePolyline(doc, parameters),
"rhino_create_circle" => CreateCircle(doc, parameters),
"rhino_create_rectangle" => CreateRectangle(doc, parameters),
"rhino_create_sphere" => CreateSphere(doc, parameters),
"rhino_create_box" => CreateBox(doc, parameters),
"rhino_create_cylinder" => CreateCylinder(doc, parameters),
"rhino_create_surface_from_curves" => CreateSurfaceFromCurves(doc, parameters),
"rhino_extrude_curve" => ExtrudeCurve(doc, parameters),
// Object Manipulation
"rhino_move_object" => MoveObject(doc, parameters),
"rhino_copy_object" => CopyObject(doc, parameters),
"rhino_rotate_object" => RotateObject(doc, parameters),
"rhino_scale_object" => ScaleObject(doc, parameters),
"rhino_delete_object" => DeleteObject(doc, parameters),
"rhino_set_layer" => SetObjectLayer(doc, parameters),
// Analysis
"rhino_get_object_info" => GetObjectInfo(doc, parameters),
"rhino_measure_distance" => MeasureDistance(parameters),
// Boolean Operations
"rhino_boolean_union" => BooleanUnion(doc, parameters),
"rhino_boolean_difference" => BooleanDifference(doc, parameters),
"rhino_boolean_intersection" => BooleanIntersection(doc, parameters),
// Export
"rhino_export" => ExportGeometry(doc, parameters),
// Rhino Commands
"rhino_run_command" => RunCommand(parameters),
// Grasshopper (delegated to GrasshopperHandler)
var gh when gh.StartsWith("grasshopper_") => GrasshopperHandler.Execute(action, parameters),
_ => new { error = $"Unknown action: {action}" }
};
}
#region Document Info
private static object GetDocumentInfo(RhinoDoc? doc)
{
if (doc == null)
return new { error = "No active document" };
return new
{
file_path = doc.Path ?? "(unsaved)",
name = doc.Name ?? "Untitled",
units = doc.ModelUnitSystem.ToString(),
object_count = doc.Objects.Count,
layer_count = doc.Layers.Count
};
}
private static object GetSelectedObjects(RhinoDoc? doc)
{
if (doc == null)
return new { objects = Array.Empty<object>() };
var selected = doc.Objects.GetSelectedObjects(false, false);
var objects = selected.Select(obj => new
{
id = obj.Id.ToString(),
type = obj.ObjectType.ToString(),
name = obj.Name ?? "",
layer = doc.Layers[obj.Attributes.LayerIndex].Name
}).ToList();
return new { objects, count = objects.Count };
}
private static object GetLayers(RhinoDoc? doc)
{
if (doc == null)
return new { layers = Array.Empty<object>() };
var layers = doc.Layers.Select(l => new
{
name = l.Name,
full_path = l.FullPath,
visible = l.IsVisible,
locked = l.IsLocked,
color = $"#{l.Color.R:X2}{l.Color.G:X2}{l.Color.B:X2}"
}).ToList();
return new { layers };
}
#endregion
#region Geometry Creation
private static Point3d ParsePoint(JObject? p)
{
if (p == null) return Point3d.Origin;
return new Point3d(
p["x"]?.Value<double>() ?? 0,
p["y"]?.Value<double>() ?? 0,
p["z"]?.Value<double>() ?? 0
);
}
private static Vector3d ParseVector(JObject? v)
{
if (v == null) return Vector3d.ZAxis;
return new Vector3d(
v["x"]?.Value<double>() ?? 0,
v["y"]?.Value<double>() ?? 0,
v["z"]?.Value<double>() ?? 1
);
}
private static object CreatePoint(RhinoDoc doc, JObject p)
{
var pt = new Point3d(
p["x"]?.Value<double>() ?? 0,
p["y"]?.Value<double>() ?? 0,
p["z"]?.Value<double>() ?? 0
);
var id = doc.Objects.AddPoint(pt);
doc.Views.Redraw();
return new { success = true, id = id.ToString(), point = new { pt.X, pt.Y, pt.Z } };
}
private static object CreateLine(RhinoDoc doc, JObject p)
{
var start = ParsePoint(p["start"] as JObject);
var end = ParsePoint(p["end"] as JObject);
var line = new Line(start, end);
var id = doc.Objects.AddLine(line);
doc.Views.Redraw();
return new { success = true, id = id.ToString(), length = line.Length };
}
private static object CreatePolyline(RhinoDoc doc, JObject p)
{
var points = (p["points"] as JArray)?.Select(pt => ParsePoint(pt as JObject)).ToList();
if (points == null || points.Count < 2)
return new { error = "At least 2 points required" };
var closed = p["closed"]?.Value<bool>() ?? false;
if (closed && points.First() != points.Last())
points.Add(points.First());
var polyline = new Polyline(points);
var id = doc.Objects.AddPolyline(polyline);
doc.Views.Redraw();
return new { success = true, id = id.ToString(), point_count = points.Count };
}
private static object CreateCircle(RhinoDoc doc, JObject p)
{
var center = ParsePoint(p["center"] as JObject);
var radius = p["radius"]?.Value<double>() ?? 1;
var circle = new Circle(center, radius);
var id = doc.Objects.AddCircle(circle);
doc.Views.Redraw();
return new { success = true, id = id.ToString(), radius, circumference = circle.Circumference };
}
private static object CreateRectangle(RhinoDoc doc, JObject p)
{
var corner = ParsePoint(p["corner"] as JObject);
var width = p["width"]?.Value<double>() ?? 1;
var height = p["height"]?.Value<double>() ?? 1;
var plane = new Plane(corner, Vector3d.ZAxis);
var rect = new Rectangle3d(plane, width, height);
var id = doc.Objects.AddRectangle(rect);
doc.Views.Redraw();
return new { success = true, id = id.ToString(), width, height, area = width * height };
}
private static object CreateSphere(RhinoDoc doc, JObject p)
{
var center = ParsePoint(p["center"] as JObject);
var radius = p["radius"]?.Value<double>() ?? 1;
var sphere = new Sphere(center, radius);
var id = doc.Objects.AddSphere(sphere);
doc.Views.Redraw();
return new { success = true, id = id.ToString(), radius, volume = sphere.Volume() };
}
private static object CreateBox(RhinoDoc doc, JObject p)
{
var corner = ParsePoint(p["corner"] as JObject);
var width = p["width"]?.Value<double>() ?? 1;
var depth = p["depth"]?.Value<double>() ?? 1;
var height = p["height"]?.Value<double>() ?? 1;
var plane = new Plane(corner, Vector3d.ZAxis);
var interval_x = new Interval(0, width);
var interval_y = new Interval(0, depth);
var interval_z = new Interval(0, height);
var box = new Box(plane, interval_x, interval_y, interval_z);
var brep = box.ToBrep();
var id = doc.Objects.AddBrep(brep);
doc.Views.Redraw();
return new { success = true, id = id.ToString(), width, depth, height, volume = width * depth * height };
}
private static object CreateCylinder(RhinoDoc doc, JObject p)
{
var baseCenter = ParsePoint(p["base_center"] as JObject);
var radius = p["radius"]?.Value<double>() ?? 1;
var height = p["height"]?.Value<double>() ?? 1;
var plane = new Plane(baseCenter, Vector3d.ZAxis);
var circle = new Circle(plane, radius);
var cylinder = new Cylinder(circle, height);
var brep = cylinder.ToBrep(true, true);
var id = doc.Objects.AddBrep(brep);
doc.Views.Redraw();
var volume = Math.PI * radius * radius * height;
return new { success = true, id = id.ToString(), radius, height, volume };
}
private static object CreateSurfaceFromCurves(RhinoDoc doc, JObject p)
{
var curveIds = (p["curve_ids"] as JArray)?.Select(id => Guid.Parse(id.ToString())).ToList();
if (curveIds == null || curveIds.Count == 0)
return new { error = "No curve IDs provided" };
var curves = curveIds.Select(id => doc.Objects.Find(id)?.Geometry as Curve).Where(c => c != null).ToList();
if (curves.Count == 0)
return new { error = "No valid curves found" };
var method = p["method"]?.ToString() ?? "planar";
Brep[]? breps = null;
if (method == "planar")
{
breps = Brep.CreatePlanarBreps(curves!, RhinoDoc.ActiveDoc.ModelAbsoluteTolerance);
}
else if (method == "loft")
{
var loftBreps = Brep.CreateFromLoft(curves!, Point3d.Unset, Point3d.Unset, LoftType.Normal, false);
breps = loftBreps;
}
if (breps == null || breps.Length == 0)
return new { error = "Failed to create surface" };
var ids = breps.Select(b => doc.Objects.AddBrep(b).ToString()).ToList();
doc.Views.Redraw();
return new { success = true, ids, count = ids.Count };
}
private static object ExtrudeCurve(RhinoDoc doc, JObject p)
{
var curveId = Guid.Parse(p["curve_id"]?.ToString() ?? "");
var curve = doc.Objects.Find(curveId)?.Geometry as Curve;
if (curve == null)
return new { error = "Curve not found" };
var direction = ParseVector(p["direction"] as JObject);
var distance = p["distance"]?.Value<double>() ?? 1;
var cap = p["cap"]?.Value<bool>() ?? true;
var extrudeDir = direction * distance;
var surface = Surface.CreateExtrusion(curve, extrudeDir);
if (surface == null)
return new { error = "Failed to create extrusion" };
var brep = surface.ToBrep();
if (cap && curve.IsClosed)
{
brep = brep.CapPlanarHoles(doc.ModelAbsoluteTolerance);
}
var id = doc.Objects.AddBrep(brep);
doc.Views.Redraw();
return new { success = true, id = id.ToString() };
}
#endregion
#region Object Manipulation
private static object MoveObject(RhinoDoc doc, JObject p)
{
var objId = Guid.Parse(p["object_id"]?.ToString() ?? "");
var translation = ParseVector(p["translation"] as JObject);
var obj = doc.Objects.Find(objId);
if (obj == null)
return new { error = "Object not found" };
var xform = Transform.Translation(translation);
doc.Objects.Transform(objId, xform, true);
doc.Views.Redraw();
return new { success = true, id = objId.ToString() };
}
private static object CopyObject(RhinoDoc doc, JObject p)
{
var objId = Guid.Parse(p["object_id"]?.ToString() ?? "");
var translation = ParseVector(p["translation"] as JObject);
var obj = doc.Objects.Find(objId);
if (obj == null)
return new { error = "Object not found" };
var xform = Transform.Translation(translation);
var newId = doc.Objects.Transform(objId, xform, false);
doc.Views.Redraw();
return new { success = true, original_id = objId.ToString(), new_id = newId.ToString() };
}
private static object RotateObject(RhinoDoc doc, JObject p)
{
var objId = Guid.Parse(p["object_id"]?.ToString() ?? "");
var angleDegrees = p["angle_degrees"]?.Value<double>() ?? 0;
var axis = p["axis"]?.ToString() ?? "z";
var center = ParsePoint(p["center"] as JObject);
var obj = doc.Objects.Find(objId);
if (obj == null)
return new { error = "Object not found" };
var axisVector = axis.ToLower() switch
{
"x" => Vector3d.XAxis,
"y" => Vector3d.YAxis,
_ => Vector3d.ZAxis
};
var angleRadians = angleDegrees * Math.PI / 180.0;
var xform = Transform.Rotation(angleRadians, axisVector, center);
doc.Objects.Transform(objId, xform, true);
doc.Views.Redraw();
return new { success = true, id = objId.ToString() };
}
private static object ScaleObject(RhinoDoc doc, JObject p)
{
var objId = Guid.Parse(p["object_id"]?.ToString() ?? "");
var scaleFactor = p["scale_factor"]?.Value<double>() ?? 1;
var basePoint = ParsePoint(p["base_point"] as JObject);
var obj = doc.Objects.Find(objId);
if (obj == null)
return new { error = "Object not found" };
var xform = Transform.Scale(basePoint, scaleFactor);
doc.Objects.Transform(objId, xform, true);
doc.Views.Redraw();
return new { success = true, id = objId.ToString() };
}
private static object DeleteObject(RhinoDoc doc, JObject p)
{
var objId = Guid.Parse(p["object_id"]?.ToString() ?? "");
var success = doc.Objects.Delete(objId, true);
doc.Views.Redraw();
return new { success, id = objId.ToString() };
}
private static object SetObjectLayer(RhinoDoc doc, JObject p)
{
var objId = Guid.Parse(p["object_id"]?.ToString() ?? "");
var layerName = p["layer_name"]?.ToString() ?? "";
var obj = doc.Objects.Find(objId);
if (obj == null)
return new { error = "Object not found" };
var layerIndex = doc.Layers.FindByFullPath(layerName, -1);
if (layerIndex < 0)
layerIndex = doc.Layers.Add(layerName, System.Drawing.Color.Black);
var attributes = obj.Attributes.Duplicate();
attributes.LayerIndex = layerIndex;
doc.Objects.ModifyAttributes(objId, attributes, true);
doc.Views.Redraw();
return new { success = true, id = objId.ToString(), layer = layerName };
}
#endregion
#region Analysis
private static object GetObjectInfo(RhinoDoc doc, JObject p)
{
var objId = Guid.Parse(p["object_id"]?.ToString() ?? "");
var obj = doc.Objects.Find(objId);
if (obj == null)
return new { error = "Object not found" };
var info = new Dictionary<string, object>
{
["id"] = objId.ToString(),
["type"] = obj.ObjectType.ToString(),
["name"] = obj.Name ?? "",
["layer"] = doc.Layers[obj.Attributes.LayerIndex].Name
};
var geometry = obj.Geometry;
var bbox = geometry.GetBoundingBox(true);
info["bounding_box"] = new
{
min = new { bbox.Min.X, bbox.Min.Y, bbox.Min.Z },
max = new { bbox.Max.X, bbox.Max.Y, bbox.Max.Z },
diagonal = bbox.Diagonal.Length
};
if (geometry is Curve curve)
{
info["length"] = curve.GetLength();
info["is_closed"] = curve.IsClosed;
}
else if (geometry is Brep brep)
{
var area = AreaMassProperties.Compute(brep);
var volume = VolumeMassProperties.Compute(brep);
if (area != null) info["area"] = area.Area;
if (volume != null) info["volume"] = volume.Volume;
}
else if (geometry is Surface surface)
{
var area = AreaMassProperties.Compute(surface);
if (area != null) info["area"] = area.Area;
}
return info;
}
private static object MeasureDistance(JObject p)
{
var pt1 = ParsePoint(p["point1"] as JObject);
var pt2 = ParsePoint(p["point2"] as JObject);
var distance = pt1.DistanceTo(pt2);
return new
{
distance,
point1 = new { pt1.X, pt1.Y, pt1.Z },
point2 = new { pt2.X, pt2.Y, pt2.Z }
};
}
#endregion
#region Boolean Operations
private static object BooleanUnion(RhinoDoc doc, JObject p)
{
var objectIds = (p["object_ids"] as JArray)?.Select(id => Guid.Parse(id.ToString())).ToList();
var deleteInput = p["delete_input"]?.Value<bool>() ?? true;
if (objectIds == null || objectIds.Count < 2)
return new { error = "At least 2 objects required for union" };
var breps = objectIds
.Select(id => doc.Objects.Find(id)?.Geometry as Brep)
.Where(b => b != null)
.ToList();
if (breps.Count < 2)
return new { error = "At least 2 valid breps required" };
var result = Brep.CreateBooleanUnion(breps!, doc.ModelAbsoluteTolerance);
if (result == null || result.Length == 0)
return new { error = "Boolean union failed" };
if (deleteInput)
{
foreach (var id in objectIds)
doc.Objects.Delete(id, true);
}
var newIds = result.Select(b => doc.Objects.AddBrep(b).ToString()).ToList();
doc.Views.Redraw();
return new { success = true, ids = newIds };
}
private static object BooleanDifference(RhinoDoc doc, JObject p)
{
var baseId = Guid.Parse(p["base_id"]?.ToString() ?? "");
var subtractIds = (p["subtract_ids"] as JArray)?.Select(id => Guid.Parse(id.ToString())).ToList();
var deleteInput = p["delete_input"]?.Value<bool>() ?? true;
if (subtractIds == null || subtractIds.Count == 0)
return new { error = "No objects to subtract" };
var baseBrep = doc.Objects.Find(baseId)?.Geometry as Brep;
if (baseBrep == null)
return new { error = "Base object not found or not a brep" };
var subtractBreps = subtractIds
.Select(id => doc.Objects.Find(id)?.Geometry as Brep)
.Where(b => b != null)
.ToList();
var result = Brep.CreateBooleanDifference(baseBrep, subtractBreps!, doc.ModelAbsoluteTolerance);
if (result == null || result.Length == 0)
return new { error = "Boolean difference failed" };
if (deleteInput)
{
doc.Objects.Delete(baseId, true);
foreach (var id in subtractIds)
doc.Objects.Delete(id, true);
}
var newIds = result.Select(b => doc.Objects.AddBrep(b).ToString()).ToList();
doc.Views.Redraw();
return new { success = true, ids = newIds };
}
private static object BooleanIntersection(RhinoDoc doc, JObject p)
{
var objectIds = (p["object_ids"] as JArray)?.Select(id => Guid.Parse(id.ToString())).ToList();
var deleteInput = p["delete_input"]?.Value<bool>() ?? true;
if (objectIds == null || objectIds.Count < 2)
return new { error = "At least 2 objects required for intersection" };
var breps = objectIds
.Select(id => doc.Objects.Find(id)?.Geometry as Brep)
.Where(b => b != null)
.ToList();
if (breps.Count < 2)
return new { error = "At least 2 valid breps required" };
var result = Brep.CreateBooleanIntersection(breps![0], breps[1], doc.ModelAbsoluteTolerance);
if (result == null || result.Length == 0)
return new { error = "Boolean intersection failed" };
if (deleteInput)
{
foreach (var id in objectIds)
doc.Objects.Delete(id, true);
}
var newIds = result.Select(b => doc.Objects.AddBrep(b).ToString()).ToList();
doc.Views.Redraw();
return new { success = true, ids = newIds };
}
#endregion
#region Export
private static object ExportGeometry(RhinoDoc doc, JObject p)
{
var filePath = p["file_path"]?.ToString() ?? "";
var format = p["format"]?.ToString() ?? "3dm";
var objectIds = (p["object_ids"] as JArray)?.Select(id => Guid.Parse(id.ToString())).ToList();
if (string.IsNullOrEmpty(filePath))
return new { error = "File path required" };
// Select objects for export if specified
if (objectIds != null && objectIds.Count > 0)
{
doc.Objects.UnselectAll();
foreach (var id in objectIds)
{
var obj = doc.Objects.Find(id);
if (obj != null)
obj.Select(true);
}
}
var command = format.ToLower() switch
{
"3dm" => $"_-SaveAs \"{filePath}\"",
"step" => $"_-Export \"{filePath}\" _Enter",
"iges" => $"_-Export \"{filePath}\" _Enter",
"stl" => $"_-Export \"{filePath}\" _Enter",
"obj" => $"_-Export \"{filePath}\" _Enter",
"fbx" => $"_-Export \"{filePath}\" _Enter",
"dxf" => $"_-Export \"{filePath}\" _Enter",
_ => null
};
if (command == null)
return new { error = $"Unknown format: {format}" };
var result = RhinoApp.RunScript(command, false);
return new { success = result, file_path = filePath, format };
}
#endregion
#region Rhino Commands
private static object RunCommand(JObject p)
{
var command = p["command"]?.ToString() ?? "";
if (string.IsNullOrEmpty(command))
return new { error = "No command specified" };
var result = RhinoApp.RunScript(command, true);
return new { success = result, command };
}
#endregion
}
}

View File

@@ -0,0 +1,527 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using Grasshopper;
using Grasshopper.Kernel;
using Grasshopper.Kernel.Parameters;
using Grasshopper.Kernel.Special;
using Newtonsoft.Json.Linq;
using Rhino;
namespace RhinoMCP
{
/// <summary>
/// Handles all Grasshopper-related commands from the MCP Server.
/// </summary>
public static class GrasshopperHandler
{
public static object Execute(string action, JObject parameters)
{
return action switch
{
"grasshopper_open_definition" => OpenDefinition(parameters),
"grasshopper_get_definition_info" => GetDefinitionInfo(),
"grasshopper_set_slider_value" => SetSliderValue(parameters),
"grasshopper_set_toggle_value" => SetToggleValue(parameters),
"grasshopper_set_panel_text" => SetPanelText(parameters),
"grasshopper_add_component" => AddComponent(parameters),
"grasshopper_connect_components" => ConnectComponents(parameters),
"grasshopper_add_slider" => AddSlider(parameters),
"grasshopper_bake" => BakeGeometry(parameters),
"grasshopper_save_definition" => SaveDefinition(parameters),
"grasshopper_recompute" => Recompute(),
_ => new { error = $"Unknown Grasshopper action: {action}" }
};
}
private static GH_Document? GetActiveDocument()
{
var editor = Instances.ActiveCanvas?.Document;
return editor;
}
private static object OpenDefinition(JObject p)
{
var filePath = p["file_path"]?.ToString() ?? "";
if (string.IsNullOrEmpty(filePath))
return new { error = "File path required" };
try
{
// Ensure Grasshopper is open
if (Instances.ActiveCanvas == null)
{
RhinoApp.RunScript("_Grasshopper", false);
System.Threading.Thread.Sleep(1000); // Wait for GH to open
}
var io = new GH_DocumentIO();
if (!io.Open(filePath))
return new { error = $"Failed to open file: {filePath}" };
var doc = io.Document;
if (doc == null)
return new { error = "Failed to load document" };
Instances.ActiveCanvas.Document = doc;
Instances.ActiveCanvas.Refresh();
return new
{
success = true,
file_path = filePath,
component_count = doc.ObjectCount,
name = doc.DisplayName
};
}
catch (Exception ex)
{
return new { error = ex.Message };
}
}
private static object GetDefinitionInfo()
{
var doc = GetActiveDocument();
if (doc == null)
return new { error = "No active Grasshopper document" };
var components = new List<object>();
var sliders = new List<object>();
var toggles = new List<object>();
var panels = new List<object>();
foreach (var obj in doc.Objects)
{
var info = new Dictionary<string, object>
{
["instance_guid"] = obj.InstanceGuid.ToString(),
["name"] = obj.Name ?? "",
["nickname"] = obj.NickName ?? "",
["category"] = obj.Category ?? "",
["subcategory"] = obj.SubCategory ?? "",
["position"] = new { x = obj.Attributes.Pivot.X, y = obj.Attributes.Pivot.Y }
};
if (obj is GH_NumberSlider slider)
{
sliders.Add(new
{
info["instance_guid"],
info["nickname"],
info["position"],
current_value = slider.CurrentValue,
min_value = (double)slider.Slider.Minimum,
max_value = (double)slider.Slider.Maximum,
type = slider.Slider.Type.ToString()
});
}
else if (obj is GH_BooleanToggle toggle)
{
toggles.Add(new
{
info["instance_guid"],
info["nickname"],
info["position"],
value = toggle.Value
});
}
else if (obj is GH_Panel panel)
{
panels.Add(new
{
info["instance_guid"],
info["nickname"],
info["position"],
text = panel.UserText
});
}
else if (obj is IGH_Component component)
{
var inputs = component.Params.Input.Select(p => new
{
name = p.Name,
nickname = p.NickName,
type = p.TypeName,
source_count = p.SourceCount
}).ToList();
var outputs = component.Params.Output.Select(p => new
{
name = p.Name,
nickname = p.NickName,
type = p.TypeName,
recipient_count = p.Recipients.Count
}).ToList();
components.Add(new
{
info["instance_guid"],
info["name"],
info["nickname"],
info["category"],
info["subcategory"],
info["position"],
inputs,
outputs
});
}
}
return new
{
name = doc.DisplayName,
file_path = doc.FilePath ?? "(unsaved)",
object_count = doc.ObjectCount,
components,
sliders,
toggles,
panels
};
}
private static object SetSliderValue(JObject p)
{
var doc = GetActiveDocument();
if (doc == null)
return new { error = "No active Grasshopper document" };
var sliderName = p["slider_name"]?.ToString() ?? "";
var value = p["value"]?.Value<double>() ?? 0;
var slider = doc.Objects
.OfType<GH_NumberSlider>()
.FirstOrDefault(s => s.NickName == sliderName || s.Name == sliderName);
if (slider == null)
return new { error = $"Slider '{sliderName}' not found" };
// Clamp value to slider range
var min = (double)slider.Slider.Minimum;
var max = (double)slider.Slider.Maximum;
value = Math.Max(min, Math.Min(max, value));
slider.SetSliderValue((decimal)value);
slider.ExpireSolution(true);
return new { success = true, slider = sliderName, value, min, max };
}
private static object SetToggleValue(JObject p)
{
var doc = GetActiveDocument();
if (doc == null)
return new { error = "No active Grasshopper document" };
var toggleName = p["toggle_name"]?.ToString() ?? "";
var value = p["value"]?.Value<bool>() ?? false;
var toggle = doc.Objects
.OfType<GH_BooleanToggle>()
.FirstOrDefault(t => t.NickName == toggleName || t.Name == toggleName);
if (toggle == null)
return new { error = $"Toggle '{toggleName}' not found" };
toggle.Value = value;
toggle.ExpireSolution(true);
return new { success = true, toggle = toggleName, value };
}
private static object SetPanelText(JObject p)
{
var doc = GetActiveDocument();
if (doc == null)
return new { error = "No active Grasshopper document" };
var panelName = p["panel_name"]?.ToString() ?? "";
var text = p["text"]?.ToString() ?? "";
var panel = doc.Objects
.OfType<GH_Panel>()
.FirstOrDefault(pnl => pnl.NickName == panelName || pnl.Name == panelName);
if (panel == null)
return new { error = $"Panel '{panelName}' not found" };
panel.UserText = text;
panel.ExpireSolution(true);
return new { success = true, panel = panelName, text };
}
private static object AddComponent(JObject p)
{
var doc = GetActiveDocument();
if (doc == null)
{
// Try to open Grasshopper
RhinoApp.RunScript("_Grasshopper", false);
System.Threading.Thread.Sleep(1000);
doc = GetActiveDocument();
if (doc == null)
return new { error = "Could not open Grasshopper" };
}
var componentName = p["component_name"]?.ToString() ?? "";
var position = p["position"] as JObject;
var x = position?["x"]?.Value<float>() ?? 0;
var y = position?["y"]?.Value<float>() ?? 0;
// Find component by name in the component server
var proxy = Instances.ComponentServer.FindObjectByName(componentName, true, true);
if (proxy == null)
return new { error = $"Component '{componentName}' not found" };
try
{
var obj = proxy.CreateInstance() as IGH_DocumentObject;
if (obj == null)
return new { error = $"Could not create instance of '{componentName}'" };
obj.CreateAttributes();
obj.Attributes.Pivot = new PointF(x, y);
doc.AddObject(obj, false);
Instances.ActiveCanvas?.Refresh();
return new
{
success = true,
component = componentName,
instance_guid = obj.InstanceGuid.ToString(),
position = new { x, y }
};
}
catch (Exception ex)
{
return new { error = ex.Message };
}
}
private static object ConnectComponents(JObject p)
{
var doc = GetActiveDocument();
if (doc == null)
return new { error = "No active Grasshopper document" };
var sourceId = p["source_component"]?.ToString() ?? "";
var sourceOutput = p["source_output"]?.ToString() ?? "";
var targetId = p["target_component"]?.ToString() ?? "";
var targetInput = p["target_input"]?.ToString() ?? "";
// Find source component
var sourceObj = FindComponent(doc, sourceId);
if (sourceObj == null)
return new { error = $"Source component '{sourceId}' not found" };
// Find target component
var targetObj = FindComponent(doc, targetId);
if (targetObj == null)
return new { error = $"Target component '{targetId}' not found" };
// Get output parameter
IGH_Param? outputParam = null;
if (sourceObj is IGH_Component sourceComp)
{
outputParam = sourceComp.Params.Output.FirstOrDefault(o =>
o.Name == sourceOutput || o.NickName == sourceOutput);
}
else if (sourceObj is IGH_Param param)
{
outputParam = param;
}
if (outputParam == null)
return new { error = $"Output '{sourceOutput}' not found on source component" };
// Get input parameter
IGH_Param? inputParam = null;
if (targetObj is IGH_Component targetComp)
{
inputParam = targetComp.Params.Input.FirstOrDefault(i =>
i.Name == targetInput || i.NickName == targetInput);
}
else if (targetObj is IGH_Param param)
{
inputParam = param;
}
if (inputParam == null)
return new { error = $"Input '{targetInput}' not found on target component" };
// Create connection
inputParam.AddSource(outputParam);
doc.NewSolution(true);
Instances.ActiveCanvas?.Refresh();
return new
{
success = true,
connection = new
{
source = new { component = sourceId, output = sourceOutput },
target = new { component = targetId, input = targetInput }
}
};
}
private static IGH_DocumentObject? FindComponent(GH_Document doc, string identifier)
{
// Try to find by GUID
if (Guid.TryParse(identifier, out var guid))
{
return doc.Objects.FirstOrDefault(o => o.InstanceGuid == guid);
}
// Try to find by name or nickname
return doc.Objects.FirstOrDefault(o =>
o.Name == identifier || o.NickName == identifier);
}
private static object AddSlider(JObject p)
{
var doc = GetActiveDocument();
if (doc == null)
{
RhinoApp.RunScript("_Grasshopper", false);
System.Threading.Thread.Sleep(1000);
doc = GetActiveDocument();
if (doc == null)
return new { error = "Could not open Grasshopper" };
}
var name = p["name"]?.ToString() ?? "Slider";
var minValue = p["min_value"]?.Value<decimal>() ?? 0;
var maxValue = p["max_value"]?.Value<decimal>() ?? 100;
var currentValue = p["current_value"]?.Value<decimal>() ?? 50;
var position = p["position"] as JObject;
var x = position?["x"]?.Value<float>() ?? 0;
var y = position?["y"]?.Value<float>() ?? 0;
var slider = new GH_NumberSlider();
slider.CreateAttributes();
slider.Attributes.Pivot = new PointF(x, y);
slider.NickName = name;
slider.Slider.Minimum = minValue;
slider.Slider.Maximum = maxValue;
slider.SetSliderValue(currentValue);
doc.AddObject(slider, false);
Instances.ActiveCanvas?.Refresh();
return new
{
success = true,
slider = name,
instance_guid = slider.InstanceGuid.ToString(),
min = minValue,
max = maxValue,
value = currentValue
};
}
private static object BakeGeometry(JObject p)
{
var doc = GetActiveDocument();
if (doc == null)
return new { error = "No active Grasshopper document" };
var componentName = p["component_name"]?.ToString();
var layerName = p["layer_name"]?.ToString() ?? "Baked";
var rhinoDoc = RhinoDoc.ActiveDoc;
if (rhinoDoc == null)
return new { error = "No active Rhino document" };
// Ensure layer exists
var layerIndex = rhinoDoc.Layers.FindByFullPath(layerName, -1);
if (layerIndex < 0)
layerIndex = rhinoDoc.Layers.Add(layerName, Color.Black);
var bakedCount = 0;
foreach (var obj in doc.Objects)
{
if (obj is IGH_Component component)
{
// Skip if we're looking for a specific component
if (!string.IsNullOrEmpty(componentName) &&
component.Name != componentName &&
component.NickName != componentName)
continue;
foreach (var output in component.Params.Output)
{
foreach (var item in output.VolatileData.AllData(true))
{
if (item is Grasshopper.Kernel.Types.IGH_BakeAwareData bakeAware)
{
var attributes = new Rhino.DocObjects.ObjectAttributes
{
LayerIndex = layerIndex
};
Guid objId;
if (bakeAware.BakeGeometry(rhinoDoc, attributes, out objId))
bakedCount++;
}
}
}
}
}
rhinoDoc.Views.Redraw();
return new { success = true, baked_count = bakedCount, layer = layerName };
}
private static object SaveDefinition(JObject p)
{
var doc = GetActiveDocument();
if (doc == null)
return new { error = "No active Grasshopper document" };
var filePath = p["file_path"]?.ToString();
try
{
var io = new GH_DocumentIO(doc);
bool success;
if (string.IsNullOrEmpty(filePath))
{
if (string.IsNullOrEmpty(doc.FilePath))
return new { error = "No file path specified and document has no existing path" };
success = io.Save();
filePath = doc.FilePath;
}
else
{
success = io.SaveAs(filePath);
}
return new { success, file_path = filePath };
}
catch (Exception ex)
{
return new { error = ex.Message };
}
}
private static object Recompute()
{
var doc = GetActiveDocument();
if (doc == null)
return new { error = "No active Grasshopper document" };
doc.NewSolution(true);
Instances.ActiveCanvas?.Refresh();
return new { success = true, message = "Definition recomputed" };
}
}
}

View File

@@ -0,0 +1,110 @@
using System;
using System.Threading.Tasks;
using EmbedIO;
using EmbedIO.Routing;
using EmbedIO.WebApi;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Rhino;
namespace RhinoMCP
{
/// <summary>
/// HTTP Server that receives commands from the MCP Server.
/// </summary>
public class HttpServer : IDisposable
{
private readonly int _port;
private WebServer? _server;
public HttpServer(int port)
{
_port = port;
}
public void Start()
{
_server = new WebServer(o => o
.WithUrlPrefix($"http://localhost:{_port}/")
.WithMode(HttpListenerMode.EmbedIO))
.WithCors()
.WithWebApi("/", m => m.WithController<RhinoApiController>());
_server.StateChanged += (s, e) =>
{
RhinoApp.WriteLine($"RhinoMCP: Server state changed to {e.NewState}");
};
_server.RunAsync();
}
public void Stop()
{
_server?.Dispose();
}
public void Dispose()
{
Stop();
}
}
/// <summary>
/// Web API Controller handling all Rhino/Grasshopper commands.
/// </summary>
public class RhinoApiController : WebApiController
{
[Route(HttpVerbs.Get, "/health")]
public object GetHealth()
{
return new { status = "ok", version = "1.0.0", rhino_version = RhinoApp.Version.ToString() };
}
[Route(HttpVerbs.Post, "/execute")]
public async Task<object> Execute()
{
try
{
var body = await HttpContext.GetRequestBodyAsStringAsync();
var request = JsonConvert.DeserializeObject<JObject>(body);
if (request == null)
return new { error = "Invalid request body" };
var action = request["action"]?.ToString();
var parameters = request["params"] as JObject ?? new JObject();
if (string.IsNullOrEmpty(action))
return new { error = "Missing action parameter" };
// Execute on Rhino's main thread
object? result = null;
Exception? exception = null;
RhinoApp.InvokeOnUiThread(new Action(() =>
{
try
{
result = CommandHandler.Execute(action, parameters);
}
catch (Exception ex)
{
exception = ex;
}
}));
// Wait a bit for UI thread execution
await Task.Delay(100);
if (exception != null)
return new { error = exception.Message };
return result ?? new { success = true };
}
catch (Exception ex)
{
return new { error = ex.Message };
}
}
}
}

View File

@@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0-windows</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<!-- Rhino Plugin Settings -->
<EnableDynamicLoading>true</EnableDynamicLoading>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<!-- Assembly Info -->
<AssemblyName>RhinoMCP</AssemblyName>
<RootNamespace>RhinoMCP</RootNamespace>
<Version>1.0.0</Version>
<Authors>RhinoMCP Contributors</Authors>
<Description>MCP Server integration for Rhino and Grasshopper</Description>
</PropertyGroup>
<ItemGroup>
<!-- Rhino 8 SDK -->
<PackageReference Include="RhinoCommon" Version="8.0.23304.9001" />
<PackageReference Include="Grasshopper" Version="8.0.23304.9001" />
<!-- HTTP Server -->
<PackageReference Include="EmbedIO" Version="3.5.2" />
<!-- JSON Handling -->
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,54 @@
using System;
using Rhino;
using Rhino.PlugIns;
namespace RhinoMCP
{
/// <summary>
/// RhinoMCP Plugin - Provides HTTP API for Claude AI integration.
/// </summary>
public class RhinoMcpPlugin : PlugIn
{
private HttpServer? _httpServer;
public static RhinoMcpPlugin? Instance { get; private set; }
public RhinoMcpPlugin()
{
Instance = this;
}
protected override LoadReturnCode OnLoad(ref string errorMessage)
{
RhinoApp.WriteLine("RhinoMCP: Plugin loading...");
try
{
// Start HTTP server on port 9000
_httpServer = new HttpServer(9000);
_httpServer.Start();
RhinoApp.WriteLine("RhinoMCP: HTTP server started on http://localhost:9000");
RhinoApp.WriteLine("RhinoMCP: Ready for Claude AI connections!");
return LoadReturnCode.Success;
}
catch (Exception ex)
{
errorMessage = $"RhinoMCP: Failed to start HTTP server: {ex.Message}";
RhinoApp.WriteLine(errorMessage);
return LoadReturnCode.ErrorShowDialog;
}
}
protected override void OnShutdown()
{
RhinoApp.WriteLine("RhinoMCP: Plugin shutting down...");
_httpServer?.Stop();
_httpServer?.Dispose();
base.OnShutdown();
}
}
}