# Directory Structure ``` ├── .gitignore ├── GH_MCP │ ├── GH_MCP │ │ ├── Commands │ │ │ ├── ComponentCommandHandler.cs │ │ │ ├── ConnectionCommandHandler.cs │ │ │ ├── DocumentCommandHandler.cs │ │ │ ├── GeometryCommandHandler.cs │ │ │ ├── GrasshopperCommandRegistry.cs │ │ │ └── IntentCommandHandler.cs │ │ ├── GH_MCP.csproj │ │ ├── GH_MCPComponent.cs │ │ ├── GH_MCPInfo.cs │ │ ├── Models │ │ │ ├── Connection.cs │ │ │ └── GrasshopperCommand.cs │ │ ├── Properties │ │ │ └── launchSettings.json │ │ ├── Resources │ │ │ └── ComponentKnowledgeBase.json │ │ └── Utils │ │ ├── FuzzyMatcher.cs │ │ └── IntentRecognizer.cs │ └── GH_MCP.sln ├── grasshopper_mcp │ ├── __init__.py │ └── bridge.py ├── grasshopper_prompt_template.txt ├── LICENSE ├── README.md ├── releases │ └── GH_MCP.gha └── setup.py ``` # Files -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` # Python __pycache__/ *.py[cod] *$py.class *.so .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # Visual Studio .vs/ bin/ obj/ *.user *.userosscache *.suo *.userprefs *.dbmdl *.dbproj.schemaview *.jfm *.pfx *.publishsettings orleans.codegen.cs # Rhino and Grasshopper *.rhi *.ghx *.gh~ *.3dm.rhl *.3dmbak *.3dm.rhl *.3dm.bak # IDE .idea/ .vscode/ *.swp *.swo # OS specific .DS_Store Thumbs.db ehthumbs.db Desktop.ini $RECYCLE.BIN/ # Project specific Grasshopper Tutorial for Beginners.pdf grasshopper_mcp_bridge.py ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown # Grasshopper MCP Bridge Grasshopper MCP Bridge is a bridging server that connects Grasshopper and Claude Desktop using the Model Context Protocol (MCP) standard. ## Features - Connects Grasshopper and Claude Desktop through the MCP protocol - Provides intuitive tool functions for creating and connecting Grasshopper components - Supports high-level intent recognition, automatically creating complex component patterns from simple descriptions - Includes a component knowledge base that understands parameters and connection rules for common components - Provides component guidance resources to help Claude Desktop correctly connect components ## System Architecture The system consists of the following parts: 1. **Grasshopper MCP Component (GH_MCP.gha)**: A plugin installed in Grasshopper that provides a TCP server to receive commands 2. **Python MCP Bridge Server**: A bridge server that connects Claude Desktop and the Grasshopper MCP component 3. **Component Knowledge Base**: JSON files containing component information, patterns, and intents ## Installation Instructions ### Prerequisites - Rhino 7 or higher - Grasshopper - Python 3.8 or higher - Claude Desktop ### Installation Steps 1. **Install the Grasshopper MCP Component** **Method 1: Download the pre-compiled GH_MCP.gha file (Recommended)** Download the [GH_MCP.gha](https://github.com/alfredatnycu/grasshopper-mcp/raw/master/releases/GH_MCP.gha) file directly from the GitHub repository and copy it to the Grasshopper components folder: ``` %APPDATA%\Grasshopper\Libraries\ ``` **Method 2: Build from source** If you prefer to build from source, clone the repository and build the C# project using Visual Studio. 2. **Install the Python MCP Bridge Server** **Method 1: Install from PyPI (Recommended)** The simplest method is to install directly from PyPI using pip: ``` pip install grasshopper-mcp ``` **Method 2: Install from GitHub** You can also install the latest version from GitHub: ``` pip install git+https://github.com/alfredatnycu/grasshopper-mcp.git ``` **Method 3: Install from Source Code** If you need to modify the code or develop new features, you can clone the repository and install: ``` git clone https://github.com/alfredatnycu/grasshopper-mcp.git cd grasshopper-mcp pip install -e . ``` **Install a Specific Version** If you need to install a specific version, you can use: ``` pip install grasshopper-mcp==0.1.0 ``` Or install from a specific GitHub tag: ``` pip install git+https://github.com/alfredatnycu/[email protected] ``` ## Usage 1. **Start Rhino and Grasshopper** Launch Rhino and open Grasshopper. 2. **Add the GH_MCP Component to Your Canvas** Find the GH_MCP component in the Grasshopper component panel and add it to your canvas. 3. **Start the Python MCP Bridge Server** Open a terminal and run: ``` python -m grasshopper_mcp.bridge ``` > **Note**: The command `grasshopper-mcp` might not work directly due to Python script path issues. Using `python -m grasshopper_mcp.bridge` is the recommended and more reliable method. 4. **Connect Claude Desktop to the MCP Bridge** **Method 1: Manual Connection** In Claude Desktop, connect to the MCP Bridge server using the following settings: - Protocol: MCP - Host: localhost - Port: 8080 **Method 2: Configure Claude Desktop to Auto-Start the Bridge** You can configure Claude Desktop to automatically start the MCP Bridge server by modifying its configuration: ```json "grasshopper": { "command": "python", "args": ["-m", "grasshopper_mcp.bridge"] } ``` This configuration tells Claude Desktop to use the command `python -m grasshopper_mcp.bridge` to start the MCP server. 5. **Start Using Grasshopper with Claude Desktop** You can now use Claude Desktop to control Grasshopper through natural language commands. ## Example Commands Here are some example commands you can use with Claude Desktop: - "Create a circle with radius 5 at point (0,0,0)" - "Connect the circle to a extrude component with a height of 10" - "Create a grid of points with 5 rows and 5 columns" - "Apply a random rotation to all selected objects" ## Troubleshooting If you encounter issues, check the following: 1. **GH_MCP Component Not Loading** - Ensure the .gha file is in the correct location - In Grasshopper, go to File > Preferences > Libraries and click "Unblock" to unblock new components - Restart Rhino and Grasshopper 2. **Bridge Server Won't Start** - If `grasshopper-mcp` command doesn't work, use `python -m grasshopper_mcp.bridge` instead - Ensure all required Python dependencies are installed - Check if port 8080 is already in use by another application 3. **Claude Desktop Can't Connect** - Ensure the bridge server is running - Verify you're using the correct connection settings (localhost:8080) - Check the console output of the bridge server for any error messages 4. **Commands Not Executing** - Verify the GH_MCP component is on your Grasshopper canvas - Check the bridge server console for error messages - Ensure Claude Desktop is properly connected to the bridge server ## Development ### Project Structure ``` grasshopper-mcp/ ├── grasshopper_mcp/ # Python bridge server │ ├── __init__.py │ └── bridge.py # Main bridge server implementation ├── GH_MCP/ # Grasshopper component (C#) │ └── ... ├── releases/ # Pre-compiled binaries │ └── GH_MCP.gha # Compiled Grasshopper component ├── setup.py # Python package setup └── README.md # This file ``` ### Contributing Contributions are welcome! Please feel free to submit a Pull Request. ## License This project is licensed under the MIT License - see the LICENSE file for details. ## Acknowledgments - Thanks to the Rhino and Grasshopper community for their excellent tools - Thanks to Anthropic for Claude Desktop and the MCP protocol ## Contact For questions or support, please open an issue on the GitHub repository. ``` -------------------------------------------------------------------------------- /grasshopper_mcp/__init__.py: -------------------------------------------------------------------------------- ```python """ Grasshopper MCP Bridge Server """ __version__ = "0.1.0" ``` -------------------------------------------------------------------------------- /GH_MCP/GH_MCP/Properties/launchSettings.json: -------------------------------------------------------------------------------- ```json { "profiles": { "Rhino 8 - netcore": { "commandName": "Executable", "executablePath": "C:\\Program Files\\Rhino 8\\System\\Rhino.exe", "commandLineArgs": "/netcore /runscript=\"_Grasshopper\"", "environmentVariables": { "RHINO_PACKAGE_DIRS": "$(ProjectDir)$(OutputPath)\\" } }, "Rhino 8 - netfx": { "commandName": "Executable", "executablePath": "C:\\Program Files\\Rhino 8\\System\\Rhino.exe", "commandLineArgs": "/netfx /runscript=\"_Grasshopper\"", "environmentVariables": { "RHINO_PACKAGE_DIRS": "$(ProjectDir)$(OutputPath)\\" } }, } } ``` -------------------------------------------------------------------------------- /GH_MCP/GH_MCP/GH_MCPInfo.cs: -------------------------------------------------------------------------------- ```csharp using System; using System.Drawing; using Grasshopper; using Grasshopper.Kernel; namespace GrasshopperMCP { public class GH_MCPInfo : GH_AssemblyInfo { public override string Name => "GH_MCP"; //Return a 24x24 pixel bitmap to represent this GHA library. public override Bitmap Icon => null; //Return a short string describing the purpose of this GHA library. public override string Description => ""; public override Guid Id => new Guid("1b472cf6-015c-496a-a0a1-7ced4df994a3"); //Return a string identifying you or your company. public override string AuthorName => ""; //Return a string representing your preferred contact details. public override string AuthorContact => ""; //Return a string representing the version. This returns the same version as the assembly. public override string AssemblyVersion => GetType().Assembly.GetName().Version.ToString(); } } ``` -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- ```python from setuptools import setup, find_packages import os # 讀取 README.md 作為長描述 with open("README.md", "r", encoding="utf-8") as fh: long_description = fh.read() setup( name="grasshopper-mcp", version="0.1.0", packages=find_packages(), include_package_data=True, install_requires=[ "mcp>=0.1.0", "websockets>=10.0", "aiohttp>=3.8.0", ], entry_points={ "console_scripts": [ "grasshopper-mcp=grasshopper_mcp.bridge:main", ], }, author="Alfred Chen", author_email="[email protected]", description="Grasshopper MCP Bridge Server", long_description=long_description, long_description_content_type="text/markdown", keywords="grasshopper, mcp, bridge, server", url="https://github.com/alfredatnycu/grasshopper-mcp", classifiers=[ "Development Status :: 3 - Alpha", "Intended Audience :: Developers", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", ], python_requires=">=3.8", ) ``` -------------------------------------------------------------------------------- /GH_MCP/GH_MCP/Commands/GeometryCommandHandler.cs: -------------------------------------------------------------------------------- ```csharp using System; using System.Collections.Generic; using GrasshopperMCP.Models; using Grasshopper.Kernel; using Rhino.Geometry; using Newtonsoft.Json.Linq; using System.Linq; using Rhino; namespace GrasshopperMCP.Commands { /// <summary> /// 處理幾何相關命令的處理器 /// </summary> public static class GeometryCommandHandler { /// <summary> /// 創建點 /// </summary> /// <param name="command">包含點坐標的命令</param> /// <returns>創建的點信息</returns> public static object CreatePoint(Command command) { double x = command.GetParameter<double>("x"); double y = command.GetParameter<double>("y"); double z = command.GetParameter<double>("z"); // 創建點 Point3d point = new Point3d(x, y, z); // 返回點信息 return new { id = Guid.NewGuid().ToString(), x = point.X, y = point.Y, z = point.Z }; } /// <summary> /// 創建曲線 /// </summary> /// <param name="command">包含曲線點的命令</param> /// <returns>創建的曲線信息</returns> public static object CreateCurve(Command command) { var pointsData = command.GetParameter<JArray>("points"); if (pointsData == null || pointsData.Count < 2) { throw new ArgumentException("At least 2 points are required to create a curve"); } // 將 JSON 點數據轉換為 Point3d 列表 List<Point3d> points = new List<Point3d>(); foreach (var pointData in pointsData) { double x = pointData["x"].Value<double>(); double y = pointData["y"].Value<double>(); double z = pointData["z"]?.Value<double>() ?? 0.0; points.Add(new Point3d(x, y, z)); } // 創建曲線 Curve curve; if (points.Count == 2) { // 如果只有兩個點,創建一條直線 curve = new LineCurve(points[0], points[1]); } else { // 如果有多個點,創建一條內插曲線 curve = Curve.CreateInterpolatedCurve(points, 3); } // 返回曲線信息 return new { id = Guid.NewGuid().ToString(), pointCount = points.Count, length = curve.GetLength() }; } /// <summary> /// 創建圓 /// </summary> /// <param name="command">包含圓心和半徑的命令</param> /// <returns>創建的圓信息</returns> public static object CreateCircle(Command command) { var centerData = command.GetParameter<JObject>("center"); double radius = command.GetParameter<double>("radius"); if (centerData == null) { throw new ArgumentException("Center point is required"); } if (radius <= 0) { throw new ArgumentException("Radius must be greater than 0"); } // 解析圓心 double x = centerData["x"].Value<double>(); double y = centerData["y"].Value<double>(); double z = centerData["z"]?.Value<double>() ?? 0.0; Point3d center = new Point3d(x, y, z); // 創建圓 Circle circle = new Circle(center, radius); // 返回圓信息 return new { id = Guid.NewGuid().ToString(), center = new { x = center.X, y = center.Y, z = center.Z }, radius = circle.Radius, circumference = circle.Circumference }; } } } ``` -------------------------------------------------------------------------------- /GH_MCP/GH_MCP/Commands/IntentCommandHandler.cs: -------------------------------------------------------------------------------- ```csharp using System; using System.Collections.Generic; using System.Linq; using System.Threading; using GrasshopperMCP.Models; using GrasshopperMCP.Commands; using GH_MCP.Models; using GH_MCP.Utils; using Rhino; using Newtonsoft.Json; namespace GH_MCP.Commands { /// <summary> /// 處理高層次意圖命令的處理器 /// </summary> public class IntentCommandHandler { private static Dictionary<string, string> _componentIdMap = new Dictionary<string, string>(); /// <summary> /// 處理創建模式命令 /// </summary> /// <param name="command">命令對象</param> /// <returns>命令執行結果</returns> public static object CreatePattern(Command command) { // 獲取模式名稱或描述 if (!command.Parameters.TryGetValue("description", out object descriptionObj) || descriptionObj == null) { return Response.CreateError("Missing required parameter: description"); } string description = descriptionObj.ToString(); // 識別意圖 string patternName = IntentRecognizer.RecognizeIntent(description); if (string.IsNullOrEmpty(patternName)) { return Response.CreateError($"Could not recognize intent from description: {description}"); } RhinoApp.WriteLine($"Recognized intent: {patternName}"); // 獲取模式詳細信息 var (components, connections) = IntentRecognizer.GetPatternDetails(patternName); if (components.Count == 0) { return Response.CreateError($"Pattern '{patternName}' has no components defined"); } // 清空組件 ID 映射 _componentIdMap.Clear(); // 創建所有組件 foreach (var component in components) { try { // 創建組件命令 var addCommand = new Command( "add_component", new Dictionary<string, object> { { "type", component.Type }, { "x", component.X }, { "y", component.Y } } ); // 如果有設置,添加設置 if (component.Settings != null) { foreach (var setting in component.Settings) { addCommand.Parameters.Add(setting.Key, setting.Value); } } // 執行添加組件命令 var result = ComponentCommandHandler.AddComponent(addCommand); if (result is Response response && response.Success && response.Data != null) { // 保存組件 ID 映射 string componentId = response.Data.ToString(); _componentIdMap[component.Id] = componentId; RhinoApp.WriteLine($"Created component {component.Type} with ID {componentId}"); } else { RhinoApp.WriteLine($"Failed to create component {component.Type}"); } } catch (Exception ex) { RhinoApp.WriteLine($"Error creating component {component.Type}: {ex.Message}"); } // 添加短暫延遲,確保組件創建完成 Thread.Sleep(100); } // 創建所有連接 foreach (var connection in connections) { try { // 檢查源和目標組件 ID 是否存在 if (!_componentIdMap.TryGetValue(connection.SourceId, out string sourceId) || !_componentIdMap.TryGetValue(connection.TargetId, out string targetId)) { RhinoApp.WriteLine($"Could not find component IDs for connection {connection.SourceId} -> {connection.TargetId}"); continue; } // 創建連接命令 var connectCommand = new Command( "connect_components", new Dictionary<string, object> { { "sourceId", sourceId }, { "sourceParam", connection.SourceParam }, { "targetId", targetId }, { "targetParam", connection.TargetParam } } ); // 執行連接命令 var result = ConnectionCommandHandler.ConnectComponents(connectCommand); if (result is Response response && response.Success) { RhinoApp.WriteLine($"Connected {connection.SourceId}.{connection.SourceParam} -> {connection.TargetId}.{connection.TargetParam}"); } else { RhinoApp.WriteLine($"Failed to connect {connection.SourceId}.{connection.SourceParam} -> {connection.TargetId}.{connection.TargetParam}"); } } catch (Exception ex) { RhinoApp.WriteLine($"Error creating connection: {ex.Message}"); } // 添加短暫延遲,確保連接創建完成 Thread.Sleep(100); } // 返回成功結果 return Response.Ok(new { Pattern = patternName, ComponentCount = components.Count, ConnectionCount = connections.Count }); } /// <summary> /// 獲取可用的模式列表 /// </summary> /// <param name="command">命令對象</param> /// <returns>命令執行結果</returns> public static object GetAvailablePatterns(Command command) { // 初始化意圖識別器 IntentRecognizer.Initialize(); // 獲取所有可用的模式 var patterns = new List<string>(); if (command.Parameters.TryGetValue("query", out object queryObj) && queryObj != null) { string query = queryObj.ToString(); string patternName = IntentRecognizer.RecognizeIntent(query); if (!string.IsNullOrEmpty(patternName)) { patterns.Add(patternName); } } else { // 如果沒有查詢,返回所有模式 // 這裡需要擴展 IntentRecognizer 以支持獲取所有模式 // 暫時返回空列表 } // 返回成功結果 return Response.Ok(patterns); } } } ``` -------------------------------------------------------------------------------- /GH_MCP/GH_MCP/GH_MCPComponent.cs: -------------------------------------------------------------------------------- ```csharp using System; using System.Collections.Generic; using System.Drawing; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading; using System.Threading.Tasks; using GH_MCP.Commands; using GrasshopperMCP.Models; using Grasshopper.Kernel; using Rhino; using Newtonsoft.Json; using System.IO; namespace GrasshopperMCP { /// <summary> /// Grasshopper MCP 組件,用於與 Python 伺服器通信 /// </summary> public class GrasshopperMCPComponent : GH_Component { private static TcpListener listener; private static bool isRunning = false; private static int grasshopperPort = 8080; /// <summary> /// 初始化 GrasshopperMCPComponent 類的新實例 /// </summary> public GrasshopperMCPComponent() : base("Grasshopper MCP", "MCP", "Machine Control Protocol for Grasshopper", "Params", "Util") { } /// <summary> /// 註冊輸入參數 /// </summary> protected override void RegisterInputParams(GH_Component.GH_InputParamManager pManager) { pManager.AddBooleanParameter("Enabled", "E", "Enable or disable the MCP server", GH_ParamAccess.item, false); pManager.AddIntegerParameter("Port", "P", "Port to listen on", GH_ParamAccess.item, grasshopperPort); } /// <summary> /// 註冊輸出參數 /// </summary> protected override void RegisterOutputParams(GH_Component.GH_OutputParamManager pManager) { pManager.AddTextParameter("Status", "S", "Server status", GH_ParamAccess.item); pManager.AddTextParameter("LastCommand", "C", "Last received command", GH_ParamAccess.item); } /// <summary> /// 解決組件 /// </summary> protected override void SolveInstance(IGH_DataAccess DA) { bool enabled = false; int port = grasshopperPort; // 獲取輸入參數 if (!DA.GetData(0, ref enabled)) return; if (!DA.GetData(1, ref port)) return; // 更新端口 grasshopperPort = port; // 根據啟用狀態啟動或停止伺服器 if (enabled && !isRunning) { Start(); DA.SetData(0, $"Running on port {grasshopperPort}"); } else if (!enabled && isRunning) { Stop(); DA.SetData(0, "Stopped"); } else if (enabled && isRunning) { DA.SetData(0, $"Running on port {grasshopperPort}"); } else { DA.SetData(0, "Stopped"); } // 設置最後接收的命令 DA.SetData(1, LastCommand); } /// <summary> /// 組件 GUID /// </summary> public override Guid ComponentGuid => new Guid("12345678-1234-1234-1234-123456789012"); /// <summary> /// 暴露圖標 /// </summary> protected override Bitmap Icon => null; /// <summary> /// 最後接收的命令 /// </summary> public static string LastCommand { get; private set; } = "None"; /// <summary> /// 啟動 MCP 伺服器 /// </summary> public static void Start() { if (isRunning) return; // 初始化命令註冊表 GrasshopperCommandRegistry.Initialize(); // 啟動 TCP 監聽器 isRunning = true; listener = new TcpListener(IPAddress.Loopback, grasshopperPort); listener.Start(); RhinoApp.WriteLine($"GrasshopperMCPBridge started on port {grasshopperPort}."); // 開始接收連接 Task.Run(ListenerLoop); } /// <summary> /// 停止 MCP 伺服器 /// </summary> public static void Stop() { if (!isRunning) return; isRunning = false; listener.Stop(); RhinoApp.WriteLine("GrasshopperMCPBridge stopped."); } /// <summary> /// 監聽循環,處理傳入的連接 /// </summary> private static async Task ListenerLoop() { try { while (isRunning) { // 等待客戶端連接 var client = await listener.AcceptTcpClientAsync(); RhinoApp.WriteLine("GrasshopperMCPBridge: Client connected."); // 處理客戶端連接 _ = Task.Run(() => HandleClient(client)); } } catch (Exception ex) { if (isRunning) { RhinoApp.WriteLine($"GrasshopperMCPBridge error: {ex.Message}"); isRunning = false; } } } /// <summary> /// 處理客戶端連接 /// </summary> /// <param name="client">TCP 客戶端</param> private static async Task HandleClient(TcpClient client) { using (client) using (var stream = client.GetStream()) using (var reader = new StreamReader(stream, Encoding.UTF8)) using (var writer = new StreamWriter(stream, Encoding.UTF8) { AutoFlush = true }) { try { // 讀取命令 string commandJson = await reader.ReadLineAsync(); if (string.IsNullOrEmpty(commandJson)) { return; } // 更新最後接收的命令 LastCommand = commandJson; // 解析命令 Command command = JsonConvert.DeserializeObject<Command>(commandJson); RhinoApp.WriteLine($"GrasshopperMCPBridge: Received command: {command.Type}"); // 執行命令 Response response = GrasshopperCommandRegistry.ExecuteCommand(command); // 發送響應 string responseJson = JsonConvert.SerializeObject(response); await writer.WriteLineAsync(responseJson); RhinoApp.WriteLine($"GrasshopperMCPBridge: Command {command.Type} executed with result: {(response.Success ? "Success" : "Error")}"); } catch (Exception ex) { RhinoApp.WriteLine($"GrasshopperMCPBridge error handling client: {ex.Message}"); // 發送錯誤響應 Response errorResponse = Response.CreateError($"Server error: {ex.Message}"); string errorResponseJson = JsonConvert.SerializeObject(errorResponse); await writer.WriteLineAsync(errorResponseJson); } } } } } ``` -------------------------------------------------------------------------------- /GH_MCP/GH_MCP/Resources/ComponentKnowledgeBase.json: -------------------------------------------------------------------------------- ```json { "components": [ { "name": "Point", "category": "Params", "subcategory": "Geometry", "description": "Creates a point at the specified coordinates", "inputs": [ {"name": "X", "type": "Number", "description": "X coordinate"}, {"name": "Y", "type": "Number", "description": "Y coordinate"}, {"name": "Z", "type": "Number", "description": "Z coordinate"} ], "outputs": [ {"name": "Pt", "type": "Point", "description": "Point"} ] }, { "name": "XY Plane", "category": "Vector", "subcategory": "Plane", "description": "Creates an XY plane at the world origin or at a specified point", "inputs": [ {"name": "Origin", "type": "Point", "description": "Origin point", "optional": true} ], "outputs": [ {"name": "Plane", "type": "Plane", "description": "XY plane"} ] }, { "name": "Box", "category": "Surface", "subcategory": "Primitive", "description": "Creates a box from a base plane and dimensions", "inputs": [ {"name": "Base", "type": "Plane", "description": "Base plane"}, {"name": "X Size", "type": "Number", "description": "Size in X direction"}, {"name": "Y Size", "type": "Number", "description": "Size in Y direction"}, {"name": "Z Size", "type": "Number", "description": "Size in Z direction"} ], "outputs": [ {"name": "Box", "type": "Brep", "description": "Box geometry"} ] }, { "name": "Circle", "category": "Curve", "subcategory": "Primitive", "description": "Creates a circle from a plane and radius", "inputs": [ {"name": "Plane", "type": "Plane", "description": "Circle plane"}, {"name": "Radius", "type": "Number", "description": "Circle radius"} ], "outputs": [ {"name": "Circle", "type": "Curve", "description": "Circle curve"} ] }, { "name": "Number Slider", "category": "Params", "subcategory": "Input", "description": "Slider for numeric input", "inputs": [], "outputs": [ {"name": "Number", "type": "Number", "description": "Slider value"} ], "defaultSettings": { "min": 0, "max": 10, "value": 5 } }, { "name": "Panel", "category": "Params", "subcategory": "Input", "description": "Text panel for input or output", "inputs": [ {"name": "Input", "type": "Any", "description": "Any input", "optional": true} ], "outputs": [ {"name": "Output", "type": "Text", "description": "Panel text"} ] }, { "name": "Voronoi", "category": "Surface", "subcategory": "Triangulation", "description": "Creates a Voronoi diagram from points", "inputs": [ {"name": "Points", "type": "Point", "description": "Input points"}, {"name": "Radius", "type": "Number", "description": "Cell radius", "optional": true}, {"name": "Plane", "type": "Plane", "description": "Base plane", "optional": true} ], "outputs": [ {"name": "Cells", "type": "Curve", "description": "Voronoi cells"}, {"name": "Vertices", "type": "Point", "description": "Voronoi vertices"} ] }, { "name": "Populate 3D", "category": "Vector", "subcategory": "Grid", "description": "Creates a 3D grid of points", "inputs": [ {"name": "Base", "type": "Plane", "description": "Base plane"}, {"name": "Size X", "type": "Number", "description": "Size in X direction"}, {"name": "Size Y", "type": "Number", "description": "Size in Y direction"}, {"name": "Size Z", "type": "Number", "description": "Size in Z direction"}, {"name": "Count X", "type": "Integer", "description": "Count in X direction"}, {"name": "Count Y", "type": "Integer", "description": "Count in Y direction"}, {"name": "Count Z", "type": "Integer", "description": "Count in Z direction"} ], "outputs": [ {"name": "Points", "type": "Point", "description": "3D grid of points"} ] }, { "name": "Boundary Surfaces", "category": "Surface", "subcategory": "Freeform", "description": "Creates boundary surfaces from curves", "inputs": [ {"name": "Curves", "type": "Curve", "description": "Input curves"} ], "outputs": [ {"name": "Surfaces", "type": "Surface", "description": "Boundary surfaces"} ] }, { "name": "Extrude", "category": "Surface", "subcategory": "Freeform", "description": "Extrudes curves or surfaces", "inputs": [ {"name": "Base", "type": "Geometry", "description": "Base geometry"}, {"name": "Direction", "type": "Vector", "description": "Extrusion direction"}, {"name": "Distance", "type": "Number", "description": "Extrusion distance"} ], "outputs": [ {"name": "Result", "type": "Brep", "description": "Extruded geometry"} ] } ], "patterns": [ { "name": "3D Box", "description": "Creates a simple 3D box", "components": [ {"type": "XY Plane", "x": 100, "y": 100, "id": "plane"}, {"type": "Number Slider", "x": 100, "y": 200, "id": "sliderX", "settings": {"min": 0, "max": 50, "value": 20}}, {"type": "Number Slider", "x": 100, "y": 250, "id": "sliderY", "settings": {"min": 0, "max": 50, "value": 20}}, {"type": "Number Slider", "x": 100, "y": 300, "id": "sliderZ", "settings": {"min": 0, "max": 50, "value": 20}}, {"type": "Box", "x": 400, "y": 200, "id": "box"} ], "connections": [ {"source": "plane", "sourceParam": "Plane", "target": "box", "targetParam": "Base"}, {"source": "sliderX", "sourceParam": "Number", "target": "box", "targetParam": "X Size"}, {"source": "sliderY", "sourceParam": "Number", "target": "box", "targetParam": "Y Size"}, {"source": "sliderZ", "sourceParam": "Number", "target": "box", "targetParam": "Z Size"} ] }, { "name": "3D Voronoi", "description": "Creates a 3D Voronoi pattern within a box", "components": [ {"type": "XY Plane", "x": 100, "y": 100, "id": "plane"}, {"type": "Number Slider", "x": 100, "y": 200, "id": "sizeX", "settings": {"min": 0, "max": 100, "value": 50}}, {"type": "Number Slider", "x": 100, "y": 250, "id": "sizeY", "settings": {"min": 0, "max": 100, "value": 50}}, {"type": "Number Slider", "x": 100, "y": 300, "id": "sizeZ", "settings": {"min": 0, "max": 100, "value": 50}}, {"type": "Number Slider", "x": 100, "y": 350, "id": "countX", "settings": {"min": 1, "max": 20, "value": 10}}, {"type": "Number Slider", "x": 100, "y": 400, "id": "countY", "settings": {"min": 1, "max": 20, "value": 10}}, {"type": "Number Slider", "x": 100, "y": 450, "id": "countZ", "settings": {"min": 1, "max": 20, "value": 10}}, {"type": "Populate 3D", "x": 400, "y": 250, "id": "populate"}, {"type": "Voronoi", "x": 600, "y": 250, "id": "voronoi"} ], "connections": [ {"source": "plane", "sourceParam": "Plane", "target": "populate", "targetParam": "Base"}, {"source": "sizeX", "sourceParam": "Number", "target": "populate", "targetParam": "Size X"}, {"source": "sizeY", "sourceParam": "Number", "target": "populate", "targetParam": "Size Y"}, {"source": "sizeZ", "sourceParam": "Number", "target": "populate", "targetParam": "Size Z"}, {"source": "countX", "sourceParam": "Number", "target": "populate", "targetParam": "Count X"}, {"source": "countY", "sourceParam": "Number", "target": "populate", "targetParam": "Count Y"}, {"source": "countZ", "sourceParam": "Number", "target": "populate", "targetParam": "Count Z"}, {"source": "populate", "sourceParam": "Points", "target": "voronoi", "targetParam": "Points"} ] }, { "name": "Circle", "description": "Creates a simple circle", "components": [ {"type": "XY Plane", "x": 100, "y": 100, "id": "plane"}, {"type": "Number Slider", "x": 100, "y": 200, "id": "radius", "settings": {"min": 0, "max": 50, "value": 10}}, {"type": "Circle", "x": 400, "y": 150, "id": "circle"} ], "connections": [ {"source": "plane", "sourceParam": "Plane", "target": "circle", "targetParam": "Plane"}, {"source": "radius", "sourceParam": "Number", "target": "circle", "targetParam": "Radius"} ] } ], "intents": [ { "keywords": ["box", "cube", "rectangular", "prism"], "pattern": "3D Box" }, { "keywords": ["voronoi", "cell", "diagram", "3d", "cellular"], "pattern": "3D Voronoi" }, { "keywords": ["circle", "round", "disc"], "pattern": "Circle" } ] } ``` -------------------------------------------------------------------------------- /GH_MCP/GH_MCP/Commands/ConnectionCommandHandler.cs: -------------------------------------------------------------------------------- ```csharp using System; using System.Collections.Generic; using System.Linq; using System.Threading; using GrasshopperMCP.Models; using GH_MCP.Models; using Grasshopper; using Grasshopper.Kernel; using Grasshopper.Kernel.Parameters; using Rhino; using Newtonsoft.Json; using GH_MCP.Utils; namespace GH_MCP.Commands { /// <summary> /// 處理組件連接相關的命令 /// </summary> public class ConnectionCommandHandler { /// <summary> /// 連接兩個組件 /// </summary> /// <param name="command">命令對象</param> /// <returns>命令執行結果</returns> public static object ConnectComponents(Command command) { // 獲取源組件 ID if (!command.Parameters.TryGetValue("sourceId", out object sourceIdObj) || sourceIdObj == null) { return Response.CreateError("Missing required parameter: sourceId"); } string sourceId = sourceIdObj.ToString(); // 獲取源參數名稱或索引 string sourceParam = null; int? sourceParamIndex = null; if (command.Parameters.TryGetValue("sourceParam", out object sourceParamObj) && sourceParamObj != null) { sourceParam = sourceParamObj.ToString(); // 使用模糊匹配獲取標準化的參數名稱 sourceParam = FuzzyMatcher.GetClosestParameterName(sourceParam); } else if (command.Parameters.TryGetValue("sourceParamIndex", out object sourceParamIndexObj) && sourceParamIndexObj != null) { if (int.TryParse(sourceParamIndexObj.ToString(), out int index)) { sourceParamIndex = index; } } // 獲取目標組件 ID if (!command.Parameters.TryGetValue("targetId", out object targetIdObj) || targetIdObj == null) { return Response.CreateError("Missing required parameter: targetId"); } string targetId = targetIdObj.ToString(); // 獲取目標參數名稱或索引 string targetParam = null; int? targetParamIndex = null; if (command.Parameters.TryGetValue("targetParam", out object targetParamObj) && targetParamObj != null) { targetParam = targetParamObj.ToString(); // 使用模糊匹配獲取標準化的參數名稱 targetParam = FuzzyMatcher.GetClosestParameterName(targetParam); } else if (command.Parameters.TryGetValue("targetParamIndex", out object targetParamIndexObj) && targetParamIndexObj != null) { if (int.TryParse(targetParamIndexObj.ToString(), out int index)) { targetParamIndex = index; } } // 記錄連接信息 RhinoApp.WriteLine($"Connecting: sourceId={sourceId}, sourceParam={sourceParam}, targetId={targetId}, targetParam={targetParam}"); // 創建連接對象 var connection = new ConnectionPairing { Source = new Connection { ComponentId = sourceId, ParameterName = sourceParam, ParameterIndex = sourceParamIndex }, Target = new Connection { ComponentId = targetId, ParameterName = targetParam, ParameterIndex = targetParamIndex } }; // 檢查連接是否有效 if (!connection.IsValid()) { return Response.CreateError("Invalid connection parameters"); } // 在 UI 線程上執行連接操作 object result = null; Exception exception = null; RhinoApp.InvokeOnUiThread(new Action(() => { try { // 獲取當前文檔 var doc = Instances.ActiveCanvas?.Document; if (doc == null) { exception = new InvalidOperationException("No active Grasshopper document"); return; } // 查找源組件 Guid sourceGuid; if (!Guid.TryParse(connection.Source.ComponentId, out sourceGuid)) { exception = new ArgumentException($"Invalid source component ID: {connection.Source.ComponentId}"); return; } var sourceComponent = doc.FindObject(sourceGuid, true); if (sourceComponent == null) { exception = new ArgumentException($"Source component not found: {connection.Source.ComponentId}"); return; } // 查找目標組件 Guid targetGuid; if (!Guid.TryParse(connection.Target.ComponentId, out targetGuid)) { exception = new ArgumentException($"Invalid target component ID: {connection.Target.ComponentId}"); return; } var targetComponent = doc.FindObject(targetGuid, true); if (targetComponent == null) { exception = new ArgumentException($"Target component not found: {connection.Target.ComponentId}"); return; } // 檢查源組件是否為輸入參數組件 if (sourceComponent is IGH_Param && ((IGH_Param)sourceComponent).Kind == GH_ParamKind.input) { exception = new ArgumentException("Source component cannot be an input parameter"); return; } // 檢查目標組件是否為輸出參數組件 if (targetComponent is IGH_Param && ((IGH_Param)targetComponent).Kind == GH_ParamKind.output) { exception = new ArgumentException("Target component cannot be an output parameter"); return; } // 獲取源參數 IGH_Param sourceParameter = GetParameter(sourceComponent, connection.Source, false); if (sourceParameter == null) { exception = new ArgumentException($"Source parameter not found: {connection.Source.ParameterName ?? connection.Source.ParameterIndex.ToString()}"); return; } // 獲取目標參數 IGH_Param targetParameter = GetParameter(targetComponent, connection.Target, true); if (targetParameter == null) { exception = new ArgumentException($"Target parameter not found: {connection.Target.ParameterName ?? connection.Target.ParameterIndex.ToString()}"); return; } // 檢查參數類型相容性 if (!AreParametersCompatible(sourceParameter, targetParameter)) { exception = new ArgumentException($"Parameters are not compatible: {sourceParameter.GetType().Name} cannot connect to {targetParameter.GetType().Name}"); return; } // 移除現有連接(如果需要) if (targetParameter.SourceCount > 0) { targetParameter.RemoveAllSources(); } // 連接參數 targetParameter.AddSource(sourceParameter); // 刷新數據 targetParameter.CollectData(); targetParameter.ComputeData(); // 刷新畫布 doc.NewSolution(false); // 返回結果 result = new { success = true, message = "Connection created successfully", sourceId = connection.Source.ComponentId, targetId = connection.Target.ComponentId, sourceParam = sourceParameter.Name, targetParam = targetParameter.Name, sourceType = sourceParameter.GetType().Name, targetType = targetParameter.GetType().Name, sourceDescription = sourceParameter.Description, targetDescription = targetParameter.Description }; } catch (Exception ex) { exception = ex; RhinoApp.WriteLine($"Error in ConnectComponents: {ex.Message}"); } })); // 等待 UI 線程操作完成 while (result == null && exception == null) { Thread.Sleep(10); } // 如果有異常,拋出 if (exception != null) { return Response.CreateError($"Error executing command 'connect_components': {exception.Message}"); } return Response.Ok(result); } /// <summary> /// 獲取組件的參數 /// </summary> /// <param name="docObj">文檔對象</param> /// <param name="connection">連接信息</param> /// <param name="isInput">是否為輸入參數</param> /// <returns>參數對象</returns> private static IGH_Param GetParameter(IGH_DocumentObject docObj, Connection connection, bool isInput) { // 處理參數組件 if (docObj is IGH_Param param) { return param; } // 處理一般組件 if (docObj is IGH_Component component) { // 獲取參數集合 IList<IGH_Param> parameters = isInput ? component.Params.Input : component.Params.Output; // 檢查參數集合是否為空 if (parameters == null || parameters.Count == 0) { return null; } // 如果只有一個參數,直接返回(只有在未指定名稱或索引時) if (parameters.Count == 1 && string.IsNullOrEmpty(connection.ParameterName) && !connection.ParameterIndex.HasValue) { return parameters[0]; } // 按名稱查找參數 if (!string.IsNullOrEmpty(connection.ParameterName)) { // 精確匹配 foreach (var p in parameters) { if (string.Equals(p.Name, connection.ParameterName, StringComparison.OrdinalIgnoreCase)) { return p; } } // 模糊匹配 foreach (var p in parameters) { if (p.Name.IndexOf(connection.ParameterName, StringComparison.OrdinalIgnoreCase) >= 0) { return p; } } // 嘗試匹配 NickName foreach (var p in parameters) { if (string.Equals(p.NickName, connection.ParameterName, StringComparison.OrdinalIgnoreCase)) { return p; } } } // 按索引查找參數 if (connection.ParameterIndex.HasValue) { int index = connection.ParameterIndex.Value; if (index >= 0 && index < parameters.Count) { return parameters[index]; } } } return null; } /// <summary> /// 檢查兩個參數是否相容 /// </summary> /// <param name="source">源參數</param> /// <param name="target">目標參數</param> /// <returns>是否相容</returns> private static bool AreParametersCompatible(IGH_Param source, IGH_Param target) { // 如果參數類型完全匹配,則相容 if (source.GetType() == target.GetType()) { return true; } // 檢查數據類型是否兼容 var sourceType = source.Type; var targetType = target.Type; // 記錄參數類型信息,用於調試 RhinoApp.WriteLine($"Parameter types: source={sourceType.Name}, target={targetType.Name}"); RhinoApp.WriteLine($"Parameter names: source={source.Name}, target={target.Name}"); // 檢查數字類型的兼容性 bool isSourceNumeric = IsNumericType(source); bool isTargetNumeric = IsNumericType(target); if (isSourceNumeric && isTargetNumeric) { return true; } // 曲線和幾何體之間的特殊處理 bool isSourceCurve = source is Param_Curve; bool isTargetCurve = target is Param_Curve; bool isSourceGeometry = source is Param_Geometry; bool isTargetGeometry = target is Param_Geometry; if ((isSourceCurve && isTargetGeometry) || (isSourceGeometry && isTargetCurve)) { return true; } // 點和向量之間的特殊處理 bool isSourcePoint = source is Param_Point; bool isTargetPoint = target is Param_Point; bool isSourceVector = source is Param_Vector; bool isTargetVector = target is Param_Vector; if ((isSourcePoint && isTargetVector) || (isSourceVector && isTargetPoint)) { return true; } // 檢查組件的 GUID,確保連接到正確的元件類型 // 獲取參數所屬的組件 var sourceDoc = source.OnPingDocument(); var targetDoc = target.OnPingDocument(); if (sourceDoc != null && targetDoc != null) { // 嘗試查找參數所屬的組件 IGH_Component sourceComponent = FindComponentForParam(sourceDoc, source); IGH_Component targetComponent = FindComponentForParam(targetDoc, target); // 如果找到了源組件和目標組件 if (sourceComponent != null && targetComponent != null) { // 記錄組件信息,用於調試 RhinoApp.WriteLine($"Components: source={sourceComponent.Name}, target={targetComponent.Name}"); RhinoApp.WriteLine($"Component GUIDs: source={sourceComponent.ComponentGuid}, target={targetComponent.ComponentGuid}"); // 特殊處理平面到幾何元件的連接 if (IsPlaneComponent(sourceComponent) && RequiresPlaneInput(targetComponent)) { RhinoApp.WriteLine("Connecting plane component to geometry component that requires plane input"); return true; } // 如果源是滑塊且目標是圓,確保目標是創建圓的組件 if (sourceComponent.Name.Contains("Number") && targetComponent.Name.Contains("Circle")) { // 檢查目標是否為正確的圓元件 (使用 GUID 或描述) if (targetComponent.ComponentGuid.ToString() == "d1028c72-ff86-4057-9eb0-36c687a4d98c") { // 這是錯誤的圓元件 (參數容器) RhinoApp.WriteLine("Detected connection to Circle parameter container instead of Circle component"); return false; } if (targetComponent.ComponentGuid.ToString() == "807b86e3-be8d-4970-92b5-f8cdcb45b06b") { // 這是正確的圓元件 (創建圓) return true; } } // 如果源是平面且目標是立方體,允許連接 if (IsPlaneComponent(sourceComponent) && targetComponent.Name.Contains("Box")) { RhinoApp.WriteLine("Connecting plane component to box component"); return true; } } } // 默認允許連接,讓 Grasshopper 在運行時決定是否相容 return true; } /// <summary> /// 檢查參數是否為數字類型 /// </summary> /// <param name="param">參數</param> /// <returns>是否為數字類型</returns> private static bool IsNumericType(IGH_Param param) { return param is Param_Integer || param is Param_Number || param is Param_Time; } /// <summary> /// 查找參數所屬的組件 /// </summary> /// <param name="doc">文檔</param> /// <param name="param">參數</param> /// <returns>參數所屬的組件</returns> private static IGH_Component FindComponentForParam(GH_Document doc, IGH_Param param) { foreach (var obj in doc.Objects) { if (obj is IGH_Component comp) { // 檢查輸出參數 foreach (var outParam in comp.Params.Output) { if (outParam.InstanceGuid == param.InstanceGuid) { return comp; } } // 檢查輸入參數 foreach (var inParam in comp.Params.Input) { if (inParam.InstanceGuid == param.InstanceGuid) { return comp; } } } } return null; } /// <summary> /// 檢查組件是否為平面組件 /// </summary> /// <param name="component">組件</param> /// <returns>是否為平面組件</returns> private static bool IsPlaneComponent(IGH_Component component) { if (component == null) return false; // 檢查組件名稱 string name = component.Name.ToLowerInvariant(); if (name.Contains("plane")) return true; // 檢查 XY Plane 組件的 GUID if (component.ComponentGuid.ToString() == "896a1e5e-c2ac-4996-a6d8-5b61157080b3") return true; return false; } /// <summary> /// 檢查組件是否需要平面輸入 /// </summary> /// <param name="component">組件</param> /// <returns>是否需要平面輸入</returns> private static bool RequiresPlaneInput(IGH_Component component) { if (component == null) return false; // 檢查組件是否有名為 "Plane" 或 "Base" 的輸入參數 foreach (var param in component.Params.Input) { string paramName = param.Name.ToLowerInvariant(); if (paramName.Contains("plane") || paramName.Contains("base")) return true; } // 檢查特定類型的組件 string name = component.Name.ToLowerInvariant(); return name.Contains("box") || name.Contains("rectangle") || name.Contains("circle") || name.Contains("cylinder") || name.Contains("cone"); } } public class ConnectionPairing { public Connection Source { get; set; } public Connection Target { get; set; } public bool IsValid() { return Source != null && Target != null; } } public class Connection { public string ComponentId { get; set; } public string ParameterName { get; set; } public int? ParameterIndex { get; set; } } } ``` -------------------------------------------------------------------------------- /GH_MCP/GH_MCP/Commands/ComponentCommandHandler.cs: -------------------------------------------------------------------------------- ```csharp using System; using System.Collections.Generic; using GrasshopperMCP.Models; using Grasshopper.Kernel; using Grasshopper.Kernel.Parameters; using Grasshopper.Kernel.Special; using Rhino; using Rhino.Geometry; using Grasshopper; using System.Linq; using Grasshopper.Kernel.Components; using System.Threading; using GH_MCP.Utils; namespace GrasshopperMCP.Commands { /// <summary> /// 處理組件相關命令的處理器 /// </summary> public static class ComponentCommandHandler { /// <summary> /// 添加組件 /// </summary> /// <param name="command">包含組件類型和位置的命令</param> /// <returns>添加的組件信息</returns> public static object AddComponent(Command command) { string type = command.GetParameter<string>("type"); double x = command.GetParameter<double>("x"); double y = command.GetParameter<double>("y"); if (string.IsNullOrEmpty(type)) { throw new ArgumentException("Component type is required"); } // 使用模糊匹配獲取標準化的元件名稱 string normalizedType = FuzzyMatcher.GetClosestComponentName(type); // 記錄請求信息 RhinoApp.WriteLine($"AddComponent request: type={type}, normalized={normalizedType}, x={x}, y={y}"); object result = null; Exception exception = null; // 在 UI 線程上執行 RhinoApp.InvokeOnUiThread(new Action(() => { try { // 獲取 Grasshopper 文檔 var doc = Grasshopper.Instances.ActiveCanvas?.Document; if (doc == null) { throw new InvalidOperationException("No active Grasshopper document"); } // 創建組件 IGH_DocumentObject component = null; // 記錄可用的組件類型(僅在第一次調用時記錄) bool loggedComponentTypes = false; if (!loggedComponentTypes) { var availableTypes = Grasshopper.Instances.ComponentServer.ObjectProxies .Select(p => p.Desc.Name) .OrderBy(n => n) .ToList(); RhinoApp.WriteLine($"Available component types: {string.Join(", ", availableTypes.Take(50))}..."); loggedComponentTypes = true; } // 根據類型創建不同的組件 switch (normalizedType.ToLowerInvariant()) { // 平面元件 case "xy plane": component = CreateComponentByName("XY Plane"); break; case "xz plane": component = CreateComponentByName("XZ Plane"); break; case "yz plane": component = CreateComponentByName("YZ Plane"); break; case "plane 3pt": component = CreateComponentByName("Plane 3Pt"); break; // 基本幾何元件 case "box": component = CreateComponentByName("Box"); break; case "sphere": component = CreateComponentByName("Sphere"); break; case "cylinder": component = CreateComponentByName("Cylinder"); break; case "cone": component = CreateComponentByName("Cone"); break; case "circle": component = CreateComponentByName("Circle"); break; case "rectangle": component = CreateComponentByName("Rectangle"); break; case "line": component = CreateComponentByName("Line"); break; // 參數元件 case "point": case "pt": case "pointparam": case "param_point": component = new Param_Point(); break; case "curve": case "crv": case "curveparam": case "param_curve": component = new Param_Curve(); break; case "circleparam": case "param_circle": component = new Param_Circle(); break; case "lineparam": case "param_line": component = new Param_Line(); break; case "panel": case "gh_panel": component = new GH_Panel(); break; case "slider": case "numberslider": case "gh_numberslider": var slider = new GH_NumberSlider(); slider.SetInitCode("0.0 < 0.5 < 1.0"); component = slider; break; case "number": case "num": case "integer": case "int": case "param_number": case "param_integer": component = new Param_Number(); break; case "construct point": case "constructpoint": case "pt xyz": case "xyz": // 嘗試查找構造點組件 var pointProxy = Grasshopper.Instances.ComponentServer.ObjectProxies .FirstOrDefault(p => p.Desc.Name.Equals("Construct Point", StringComparison.OrdinalIgnoreCase)); if (pointProxy != null) { component = pointProxy.CreateInstance(); } else { throw new ArgumentException("Construct Point component not found"); } break; default: // 嘗試通過 Guid 查找組件 Guid componentGuid; if (Guid.TryParse(type, out componentGuid)) { component = Grasshopper.Instances.ComponentServer.EmitObject(componentGuid); RhinoApp.WriteLine($"Attempting to create component by GUID: {componentGuid}"); } if (component == null) { // 嘗試通過名稱查找組件(不區分大小寫) RhinoApp.WriteLine($"Attempting to find component by name: {type}"); var obj = Grasshopper.Instances.ComponentServer.ObjectProxies .FirstOrDefault(p => p.Desc.Name.Equals(type, StringComparison.OrdinalIgnoreCase)); if (obj != null) { RhinoApp.WriteLine($"Found component: {obj.Desc.Name}"); component = obj.CreateInstance(); } else { // 嘗試通過部分名稱匹配 RhinoApp.WriteLine($"Attempting to find component by partial name match: {type}"); obj = Grasshopper.Instances.ComponentServer.ObjectProxies .FirstOrDefault(p => p.Desc.Name.IndexOf(type, StringComparison.OrdinalIgnoreCase) >= 0); if (obj != null) { RhinoApp.WriteLine($"Found component by partial match: {obj.Desc.Name}"); component = obj.CreateInstance(); } } } if (component == null) { // 記錄一些可能的組件類型 var possibleMatches = Grasshopper.Instances.ComponentServer.ObjectProxies .Where(p => p.Desc.Name.IndexOf(type, StringComparison.OrdinalIgnoreCase) >= 0) .Select(p => p.Desc.Name) .Take(10) .ToList(); var errorMessage = $"Unknown component type: {type}"; if (possibleMatches.Any()) { errorMessage += $". Possible matches: {string.Join(", ", possibleMatches)}"; } throw new ArgumentException(errorMessage); } break; } // 設置組件位置 if (component != null) { // 確保組件有有效的屬性對象 if (component.Attributes == null) { RhinoApp.WriteLine("Component attributes are null, creating new attributes"); component.CreateAttributes(); } // 設置位置 component.Attributes.Pivot = new System.Drawing.PointF((float)x, (float)y); // 添加到文檔 doc.AddObject(component, false); // 刷新畫布 doc.NewSolution(false); // 返回組件信息 result = new { id = component.InstanceGuid.ToString(), type = component.GetType().Name, name = component.NickName, x = component.Attributes.Pivot.X, y = component.Attributes.Pivot.Y }; } else { throw new InvalidOperationException("Failed to create component"); } } catch (Exception ex) { exception = ex; RhinoApp.WriteLine($"Error in AddComponent: {ex.Message}"); } })); // 等待 UI 線程操作完成 while (result == null && exception == null) { Thread.Sleep(10); } // 如果有異常,拋出 if (exception != null) { throw exception; } return result; } /// <summary> /// 連接組件 /// </summary> /// <param name="command">包含源和目標組件信息的命令</param> /// <returns>連接信息</returns> public static object ConnectComponents(Command command) { var fromData = command.GetParameter<Dictionary<string, object>>("from"); var toData = command.GetParameter<Dictionary<string, object>>("to"); if (fromData == null || toData == null) { throw new ArgumentException("Source and target component information are required"); } object result = null; Exception exception = null; // 在 UI 線程上執行 RhinoApp.InvokeOnUiThread(new Action(() => { try { // 獲取 Grasshopper 文檔 var doc = Grasshopper.Instances.ActiveCanvas?.Document; if (doc == null) { throw new InvalidOperationException("No active Grasshopper document"); } // 解析源組件信息 string fromIdStr = fromData["id"].ToString(); string fromParamName = fromData["parameterName"].ToString(); // 解析目標組件信息 string toIdStr = toData["id"].ToString(); string toParamName = toData["parameterName"].ToString(); // 將字符串 ID 轉換為 Guid Guid fromId, toId; if (!Guid.TryParse(fromIdStr, out fromId) || !Guid.TryParse(toIdStr, out toId)) { throw new ArgumentException("Invalid component ID format"); } // 查找源和目標組件 IGH_Component fromComponent = doc.FindComponent(fromId) as IGH_Component; IGH_Component toComponent = doc.FindComponent(toId) as IGH_Component; if (fromComponent == null || toComponent == null) { throw new ArgumentException("Source or target component not found"); } // 查找源輸出參數 IGH_Param fromParam = null; foreach (var param in fromComponent.Params.Output) { if (param.Name.Equals(fromParamName, StringComparison.OrdinalIgnoreCase)) { fromParam = param; break; } } // 查找目標輸入參數 IGH_Param toParam = null; foreach (var param in toComponent.Params.Input) { if (param.Name.Equals(toParamName, StringComparison.OrdinalIgnoreCase)) { toParam = param; break; } } if (fromParam == null || toParam == null) { throw new ArgumentException("Source or target parameter not found"); } // 連接參數 toParam.AddSource(fromParam); // 刷新畫布 doc.NewSolution(false); // 返回連接信息 result = new { from = new { id = fromComponent.InstanceGuid.ToString(), name = fromComponent.NickName, parameter = fromParam.Name }, to = new { id = toComponent.InstanceGuid.ToString(), name = toComponent.NickName, parameter = toParam.Name } }; } catch (Exception ex) { exception = ex; RhinoApp.WriteLine($"Error in ConnectComponents: {ex.Message}"); } })); // 等待 UI 線程操作完成 while (result == null && exception == null) { Thread.Sleep(10); } // 如果有異常,拋出 if (exception != null) { throw exception; } return result; } /// <summary> /// 設置組件值 /// </summary> /// <param name="command">包含組件 ID 和值的命令</param> /// <returns>操作結果</returns> public static object SetComponentValue(Command command) { string idStr = command.GetParameter<string>("id"); string value = command.GetParameter<string>("value"); if (string.IsNullOrEmpty(idStr)) { throw new ArgumentException("Component ID is required"); } object result = null; Exception exception = null; // 在 UI 線程上執行 RhinoApp.InvokeOnUiThread(new Action(() => { try { // 獲取 Grasshopper 文檔 var doc = Grasshopper.Instances.ActiveCanvas?.Document; if (doc == null) { throw new InvalidOperationException("No active Grasshopper document"); } // 將字符串 ID 轉換為 Guid Guid id; if (!Guid.TryParse(idStr, out id)) { throw new ArgumentException("Invalid component ID format"); } // 查找組件 IGH_DocumentObject component = doc.FindObject(id, true); if (component == null) { throw new ArgumentException($"Component with ID {idStr} not found"); } // 根據組件類型設置值 if (component is GH_Panel panel) { panel.UserText = value; } else if (component is GH_NumberSlider slider) { double doubleValue; if (double.TryParse(value, out doubleValue)) { slider.SetSliderValue((decimal)doubleValue); } else { throw new ArgumentException("Invalid slider value format"); } } else if (component is IGH_Component ghComponent) { // 嘗試設置第一個輸入參數的值 if (ghComponent.Params.Input.Count > 0) { var param = ghComponent.Params.Input[0]; if (param is Param_String stringParam) { stringParam.PersistentData.Clear(); stringParam.PersistentData.Append(new Grasshopper.Kernel.Types.GH_String(value)); } else if (param is Param_Number numberParam) { double doubleValue; if (double.TryParse(value, out doubleValue)) { numberParam.PersistentData.Clear(); numberParam.PersistentData.Append(new Grasshopper.Kernel.Types.GH_Number(doubleValue)); } else { throw new ArgumentException("Invalid number value format"); } } else { throw new ArgumentException($"Cannot set value for parameter type {param.GetType().Name}"); } } else { throw new ArgumentException("Component has no input parameters"); } } else { throw new ArgumentException($"Cannot set value for component type {component.GetType().Name}"); } // 刷新畫布 doc.NewSolution(false); // 返回操作結果 result = new { id = component.InstanceGuid.ToString(), type = component.GetType().Name, value = value }; } catch (Exception ex) { exception = ex; RhinoApp.WriteLine($"Error in SetComponentValue: {ex.Message}"); } })); // 等待 UI 線程操作完成 while (result == null && exception == null) { Thread.Sleep(10); } // 如果有異常,拋出 if (exception != null) { throw exception; } return result; } /// <summary> /// 獲取組件信息 /// </summary> /// <param name="command">包含組件 ID 的命令</param> /// <returns>組件信息</returns> public static object GetComponentInfo(Command command) { string idStr = command.GetParameter<string>("id"); if (string.IsNullOrEmpty(idStr)) { throw new ArgumentException("Component ID is required"); } object result = null; Exception exception = null; // 在 UI 線程上執行 RhinoApp.InvokeOnUiThread(new Action(() => { try { // 獲取 Grasshopper 文檔 var doc = Grasshopper.Instances.ActiveCanvas?.Document; if (doc == null) { throw new InvalidOperationException("No active Grasshopper document"); } // 將字符串 ID 轉換為 Guid Guid id; if (!Guid.TryParse(idStr, out id)) { throw new ArgumentException("Invalid component ID format"); } // 查找組件 IGH_DocumentObject component = doc.FindObject(id, true); if (component == null) { throw new ArgumentException($"Component with ID {idStr} not found"); } // 收集組件信息 var componentInfo = new Dictionary<string, object> { { "id", component.InstanceGuid.ToString() }, { "type", component.GetType().Name }, { "name", component.NickName }, { "description", component.Description } }; // 如果是 IGH_Component,收集輸入和輸出參數信息 if (component is IGH_Component ghComponent) { var inputs = new List<Dictionary<string, object>>(); foreach (var param in ghComponent.Params.Input) { inputs.Add(new Dictionary<string, object> { { "name", param.Name }, { "nickname", param.NickName }, { "description", param.Description }, { "type", param.GetType().Name }, { "dataType", param.TypeName } }); } componentInfo["inputs"] = inputs; var outputs = new List<Dictionary<string, object>>(); foreach (var param in ghComponent.Params.Output) { outputs.Add(new Dictionary<string, object> { { "name", param.Name }, { "nickname", param.NickName }, { "description", param.Description }, { "type", param.GetType().Name }, { "dataType", param.TypeName } }); } componentInfo["outputs"] = outputs; } // 如果是 GH_Panel,獲取其文本值 if (component is GH_Panel panel) { componentInfo["value"] = panel.UserText; } // 如果是 GH_NumberSlider,獲取其值和範圍 if (component is GH_NumberSlider slider) { componentInfo["value"] = (double)slider.CurrentValue; componentInfo["minimum"] = (double)slider.Slider.Minimum; componentInfo["maximum"] = (double)slider.Slider.Maximum; } result = componentInfo; } catch (Exception ex) { exception = ex; RhinoApp.WriteLine($"Error in GetComponentInfo: {ex.Message}"); } })); // 等待 UI 線程操作完成 while (result == null && exception == null) { Thread.Sleep(10); } // 如果有異常,拋出 if (exception != null) { throw exception; } return result; } private static IGH_DocumentObject CreateComponentByName(string name) { var obj = Grasshopper.Instances.ComponentServer.ObjectProxies .FirstOrDefault(p => p.Desc.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); if (obj != null) { return obj.CreateInstance(); } else { throw new ArgumentException($"Component with name {name} not found"); } } } } ``` -------------------------------------------------------------------------------- /grasshopper_mcp/bridge.py: -------------------------------------------------------------------------------- ```python import socket import json import os import sys import traceback from typing import Dict, Any, Optional, List # 使用 MCP 服務器 from mcp.server.fastmcp import FastMCP # 設置 Grasshopper MCP 連接參數 GRASSHOPPER_HOST = "localhost" GRASSHOPPER_PORT = 8080 # 默認端口,可以根據需要修改 # 創建 MCP 服務器 server = FastMCP("Grasshopper Bridge") def send_to_grasshopper(command_type: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: """向 Grasshopper MCP 發送命令""" if params is None: params = {} # 創建命令 command = { "type": command_type, "parameters": params } try: print(f"Sending command to Grasshopper: {command_type} with params: {params}", file=sys.stderr) # 連接到 Grasshopper MCP client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) client.connect((GRASSHOPPER_HOST, GRASSHOPPER_PORT)) # 發送命令 command_json = json.dumps(command) client.sendall((command_json + "\n").encode("utf-8")) print(f"Command sent: {command_json}", file=sys.stderr) # 接收響應 response_data = b"" while True: chunk = client.recv(4096) if not chunk: break response_data += chunk if response_data.endswith(b"\n"): break # 處理可能的 BOM response_str = response_data.decode("utf-8-sig").strip() print(f"Response received: {response_str}", file=sys.stderr) # 解析 JSON 響應 response = json.loads(response_str) client.close() return response except Exception as e: print(f"Error communicating with Grasshopper: {str(e)}", file=sys.stderr) traceback.print_exc(file=sys.stderr) return { "success": False, "error": f"Error communicating with Grasshopper: {str(e)}" } # 註冊 MCP 工具 @server.tool("add_component") def add_component(component_type: str, x: float, y: float): """ Add a component to the Grasshopper canvas Args: component_type: Component type (point, curve, circle, line, panel, slider) x: X coordinate on the canvas y: Y coordinate on the canvas Returns: Result of adding the component """ # 處理常見的組件名稱混淆問題 component_mapping = { # Number Slider 的各種可能輸入方式 "number slider": "Number Slider", "numeric slider": "Number Slider", "num slider": "Number Slider", "slider": "Number Slider", # 當只提到 slider 且上下文是數值時,預設為 Number Slider # 其他組件的標準化名稱 "md slider": "MD Slider", "multidimensional slider": "MD Slider", "multi-dimensional slider": "MD Slider", "graph mapper": "Graph Mapper", # 數學運算組件 "add": "Addition", "addition": "Addition", "plus": "Addition", "sum": "Addition", "subtract": "Subtraction", "subtraction": "Subtraction", "minus": "Subtraction", "difference": "Subtraction", "multiply": "Multiplication", "multiplication": "Multiplication", "times": "Multiplication", "product": "Multiplication", "divide": "Division", "division": "Division", # 輸出組件 "panel": "Panel", "text panel": "Panel", "output panel": "Panel", "display": "Panel" } # 檢查並修正組件類型 normalized_type = component_type.lower() if normalized_type in component_mapping: component_type = component_mapping[normalized_type] print(f"Component type normalized from '{normalized_type}' to '{component_mapping[normalized_type]}'", file=sys.stderr) params = { "type": component_type, "x": x, "y": y } return send_to_grasshopper("add_component", params) @server.tool("clear_document") def clear_document(): """Clear the Grasshopper document""" return send_to_grasshopper("clear_document") @server.tool("save_document") def save_document(path: str): """ Save the Grasshopper document Args: path: Save path Returns: Result of the save operation """ params = { "path": path } return send_to_grasshopper("save_document", params) @server.tool("load_document") def load_document(path: str): """ Load a Grasshopper document Args: path: Document path Returns: Result of the load operation """ params = { "path": path } return send_to_grasshopper("load_document", params) @server.tool("get_document_info") def get_document_info(): """Get information about the Grasshopper document""" return send_to_grasshopper("get_document_info") @server.tool("connect_components") def connect_components(source_id: str, target_id: str, source_param: str = None, target_param: str = None, source_param_index: int = None, target_param_index: int = None): """ Connect two components in the Grasshopper canvas Args: source_id: ID of the source component (output) target_id: ID of the target component (input) source_param: Name of the source parameter (optional) target_param: Name of the target parameter (optional) source_param_index: Index of the source parameter (optional, used if source_param is not provided) target_param_index: Index of the target parameter (optional, used if target_param is not provided) Returns: Result of connecting the components """ # 獲取目標組件的信息,檢查是否已有連接 target_info = send_to_grasshopper("get_component_info", {"componentId": target_id}) # 檢查組件類型,如果是需要多個輸入的組件(如 Addition, Subtraction 等),智能分配輸入 if target_info and "result" in target_info and "type" in target_info["result"]: component_type = target_info["result"]["type"] # 獲取現有連接 connections = send_to_grasshopper("get_connections") existing_connections = [] if connections and "result" in connections: for conn in connections["result"]: if conn.get("targetId") == target_id: existing_connections.append(conn) # 對於特定需要多個輸入的組件,自動選擇正確的輸入端口 if component_type in ["Addition", "Subtraction", "Multiplication", "Division", "Math"]: # 如果沒有指定目標參數,且已有連接到第一個輸入,則自動連接到第二個輸入 if target_param is None and target_param_index is None: # 檢查第一個輸入是否已被佔用 first_input_occupied = False for conn in existing_connections: if conn.get("targetParam") == "A" or conn.get("targetParamIndex") == 0: first_input_occupied = True break # 如果第一個輸入已被佔用,則連接到第二個輸入 if first_input_occupied: target_param = "B" # 第二個輸入通常命名為 B else: target_param = "A" # 否則連接到第一個輸入 params = { "sourceId": source_id, "targetId": target_id } if source_param is not None: params["sourceParam"] = source_param elif source_param_index is not None: params["sourceParamIndex"] = source_param_index if target_param is not None: params["targetParam"] = target_param elif target_param_index is not None: params["targetParamIndex"] = target_param_index return send_to_grasshopper("connect_components", params) @server.tool("create_pattern") def create_pattern(description: str): """ Create a pattern of components based on a high-level description Args: description: High-level description of what to create (e.g., '3D voronoi cube') Returns: Result of creating the pattern """ params = { "description": description } return send_to_grasshopper("create_pattern", params) @server.tool("get_available_patterns") def get_available_patterns(query: str): """ Get a list of available patterns that match a query Args: query: Query to search for patterns Returns: List of available patterns """ params = { "query": query } return send_to_grasshopper("get_available_patterns", params) @server.tool("get_component_info") def get_component_info(component_id: str): """ Get detailed information about a specific component Args: component_id: ID of the component to get information about Returns: Detailed information about the component, including inputs, outputs, and current values """ params = { "componentId": component_id } result = send_to_grasshopper("get_component_info", params) # 增強返回結果,添加更多參數信息 if result and "result" in result: component_data = result["result"] # 獲取組件類型 if "type" in component_data: component_type = component_data["type"] # 查詢組件庫,獲取該類型組件的詳細參數信息 component_library = get_component_library() if "components" in component_library: for lib_component in component_library["components"]: if lib_component.get("name") == component_type or lib_component.get("fullName") == component_type: # 將組件庫中的參數信息合併到返回結果中 if "settings" in lib_component: component_data["availableSettings"] = lib_component["settings"] if "inputs" in lib_component: component_data["inputDetails"] = lib_component["inputs"] if "outputs" in lib_component: component_data["outputDetails"] = lib_component["outputs"] if "usage_examples" in lib_component: component_data["usageExamples"] = lib_component["usage_examples"] if "common_issues" in lib_component: component_data["commonIssues"] = lib_component["common_issues"] break # 特殊處理某些組件類型 if component_type == "Number Slider": # 嘗試從組件數據中獲取當前滑桿的實際設置 if "currentSettings" not in component_data: component_data["currentSettings"] = { "min": component_data.get("min", 0), "max": component_data.get("max", 10), "value": component_data.get("value", 5), "rounding": component_data.get("rounding", 0.1), "type": component_data.get("type", "float") } # 添加組件的連接信息 connections = send_to_grasshopper("get_connections") if connections and "result" in connections: # 查找與該組件相關的所有連接 related_connections = [] for conn in connections["result"]: if conn.get("sourceId") == component_id or conn.get("targetId") == component_id: related_connections.append(conn) if related_connections: component_data["connections"] = related_connections return result @server.tool("get_all_components") def get_all_components(): """ Get a list of all components in the current document Returns: List of all components in the document with their IDs, types, and positions """ result = send_to_grasshopper("get_all_components") # 增強返回結果,為每個組件添加更多參數信息 if result and "result" in result: components = result["result"] component_library = get_component_library() # 獲取所有連接信息 connections = send_to_grasshopper("get_connections") connections_data = connections.get("result", []) if connections else [] # 為每個組件添加詳細信息 for component in components: if "id" in component and "type" in component: component_id = component["id"] component_type = component["type"] # 添加組件的詳細參數信息 if "components" in component_library: for lib_component in component_library["components"]: if lib_component.get("name") == component_type or lib_component.get("fullName") == component_type: # 將組件庫中的參數信息合併到組件數據中 if "settings" in lib_component: component["availableSettings"] = lib_component["settings"] if "inputs" in lib_component: component["inputDetails"] = lib_component["inputs"] if "outputs" in lib_component: component["outputDetails"] = lib_component["outputs"] break # 添加組件的連接信息 related_connections = [] for conn in connections_data: if conn.get("sourceId") == component_id or conn.get("targetId") == component_id: related_connections.append(conn) if related_connections: component["connections"] = related_connections # 特殊處理某些組件類型 if component_type == "Number Slider": # 嘗試獲取滑桿的當前設置 component_info = send_to_grasshopper("get_component_info", {"componentId": component_id}) if component_info and "result" in component_info: info_data = component_info["result"] component["currentSettings"] = { "min": info_data.get("min", 0), "max": info_data.get("max", 10), "value": info_data.get("value", 5), "rounding": info_data.get("rounding", 0.1) } return result @server.tool("get_connections") def get_connections(): """ Get a list of all connections between components in the current document Returns: List of all connections between components """ return send_to_grasshopper("get_connections") @server.tool("search_components") def search_components(query: str): """ Search for components by name or category Args: query: Search query Returns: List of components matching the search query """ params = { "query": query } return send_to_grasshopper("search_components", params) @server.tool("get_component_parameters") def get_component_parameters(component_type: str): """ Get a list of parameters for a specific component type Args: component_type: Type of component to get parameters for Returns: List of input and output parameters for the component type """ params = { "componentType": component_type } return send_to_grasshopper("get_component_parameters", params) @server.tool("validate_connection") def validate_connection(source_id: str, target_id: str, source_param: str = None, target_param: str = None): """ Validate if a connection between two components is possible Args: source_id: ID of the source component (output) target_id: ID of the target component (input) source_param: Name of the source parameter (optional) target_param: Name of the target parameter (optional) Returns: Whether the connection is valid and any potential issues """ params = { "sourceId": source_id, "targetId": target_id } if source_param is not None: params["sourceParam"] = source_param if target_param is not None: params["targetParam"] = target_param return send_to_grasshopper("validate_connection", params) # 註冊 MCP 資源 @server.resource("grasshopper://status") def get_grasshopper_status(): """Get Grasshopper status""" try: # 獲取文檔信息 doc_info = send_to_grasshopper("get_document_info") # 獲取所有組件(使用增強版的 get_all_components) components_result = get_all_components() components = components_result.get("result", []) if components_result else [] # 獲取所有連接 connections = send_to_grasshopper("get_connections") # 添加常用組件的提示信息 component_hints = { "Number Slider": { "description": "Single numeric value slider with adjustable range", "common_usage": "Use for single numeric inputs like radius, height, count, etc.", "parameters": ["min", "max", "value", "rounding", "type"], "NOT_TO_BE_CONFUSED_WITH": "MD Slider (which is for multi-dimensional values)" }, "MD Slider": { "description": "Multi-dimensional slider for vector input", "common_usage": "Use for vector inputs, NOT for simple numeric values", "NOT_TO_BE_CONFUSED_WITH": "Number Slider (which is for single numeric values)" }, "Panel": { "description": "Displays text or numeric data", "common_usage": "Use for displaying outputs and debugging" }, "Addition": { "description": "Adds two or more numbers", "common_usage": "Connect two Number Sliders to inputs A and B", "parameters": ["A", "B"], "connection_tip": "First slider should connect to input A, second to input B" } } # 為每個組件添加當前參數值的摘要 component_summaries = [] for component in components: summary = { "id": component.get("id", ""), "type": component.get("type", ""), "position": { "x": component.get("x", 0), "y": component.get("y", 0) } } # 添加組件特定的參數信息 if "currentSettings" in component: summary["settings"] = component["currentSettings"] elif component.get("type") == "Number Slider": # 嘗試從組件信息中提取滑桿設置 summary["settings"] = { "min": component.get("min", 0), "max": component.get("max", 10), "value": component.get("value", 5), "rounding": component.get("rounding", 0.1) } # 添加連接信息摘要 if "connections" in component: conn_summary = [] for conn in component["connections"]: if conn.get("sourceId") == component.get("id"): conn_summary.append({ "type": "output", "to": conn.get("targetId", ""), "sourceParam": conn.get("sourceParam", ""), "targetParam": conn.get("targetParam", "") }) else: conn_summary.append({ "type": "input", "from": conn.get("sourceId", ""), "sourceParam": conn.get("sourceParam", ""), "targetParam": conn.get("targetParam", "") }) if conn_summary: summary["connections"] = conn_summary component_summaries.append(summary) return { "status": "Connected to Grasshopper", "document": doc_info.get("result", {}), "components": component_summaries, "connections": connections.get("result", []), "component_hints": component_hints, "recommendations": [ "When needing a simple numeric input control, ALWAYS use 'Number Slider', not MD Slider", "For vector inputs (like 3D points), use 'MD Slider' or 'Construct Point' with multiple Number Sliders", "Use 'Panel' to display outputs and debug values", "When connecting multiple sliders to Addition, first slider goes to input A, second to input B" ], "canvas_summary": f"Current canvas has {len(component_summaries)} components and {len(connections.get('result', []))} connections" } except Exception as e: print(f"Error getting Grasshopper status: {str(e)}", file=sys.stderr) traceback.print_exc(file=sys.stderr) return { "status": f"Error: {str(e)}", "document": {}, "components": [], "connections": [] } @server.resource("grasshopper://component_guide") def get_component_guide(): """Get guide for Grasshopper components and connections""" return { "title": "Grasshopper Component Guide", "description": "Guide for creating and connecting Grasshopper components", "components": [ { "name": "Point", "category": "Params", "description": "Creates a point at specific coordinates", "inputs": [ {"name": "X", "type": "Number"}, {"name": "Y", "type": "Number"}, {"name": "Z", "type": "Number"} ], "outputs": [ {"name": "Pt", "type": "Point"} ] }, { "name": "Circle", "category": "Curve", "description": "Creates a circle", "inputs": [ {"name": "Plane", "type": "Plane", "description": "Base plane for the circle"}, {"name": "Radius", "type": "Number", "description": "Circle radius"} ], "outputs": [ {"name": "C", "type": "Circle"} ] }, { "name": "XY Plane", "category": "Vector", "description": "Creates an XY plane at the world origin or at a specified point", "inputs": [ {"name": "Origin", "type": "Point", "description": "Origin point", "optional": True} ], "outputs": [ {"name": "Plane", "type": "Plane", "description": "XY plane"} ] }, { "name": "Addition", "fullName": "Addition", "description": "Adds two or more numbers", "inputs": [ {"name": "A", "type": "Number", "description": "First input value"}, {"name": "B", "type": "Number", "description": "Second input value"} ], "outputs": [ {"name": "Result", "type": "Number", "description": "Sum of inputs"} ], "usage_examples": [ "Connect two Number Sliders to inputs A and B to add their values", "Connect multiple values to add them all together" ], "common_issues": [ "When connecting multiple sliders, ensure they connect to different inputs (A and B)", "The first slider should connect to input A, the second to input B" ] }, { "name": "Number Slider", "fullName": "Number Slider", "description": "Creates a slider for numeric input with adjustable range and precision", "inputs": [], "outputs": [ {"name": "N", "type": "Number", "description": "Number output"} ], "settings": { "min": {"description": "Minimum value of the slider", "default": 0}, "max": {"description": "Maximum value of the slider", "default": 10}, "value": {"description": "Current value of the slider", "default": 5}, "rounding": {"description": "Rounding precision (0.01, 0.1, 1, etc.)", "default": 0.1}, "type": {"description": "Slider type (integer, floating point)", "default": "float"}, "name": {"description": "Custom name for the slider", "default": ""} }, "usage_examples": [ "Create a Number Slider with min=0, max=100, value=50", "Create a Number Slider for radius with min=0.1, max=10, value=2.5, rounding=0.1" ], "common_issues": [ "Confusing with other slider types", "Not setting appropriate min/max values for the intended use" ], "disambiguation": { "similar_components": [ { "name": "MD Slider", "description": "Multi-dimensional slider for vector input, NOT for simple numeric values", "how_to_distinguish": "Use Number Slider for single numeric values; use MD Slider only when you need multi-dimensional control" }, { "name": "Graph Mapper", "description": "Maps values through a graph function, NOT a simple slider", "how_to_distinguish": "Use Number Slider for direct numeric input; use Graph Mapper only for function-based mapping" } ], "correct_usage": "When needing a simple numeric input control, ALWAYS use 'Number Slider', not MD Slider or other variants" } }, { "name": "Panel", "fullName": "Panel", "description": "Displays text or numeric data", "inputs": [ {"name": "Input", "type": "Any"} ], "outputs": [] }, { "name": "Math", "fullName": "Mathematics", "description": "Performs mathematical operations", "inputs": [ {"name": "A", "type": "Number"}, {"name": "B", "type": "Number"} ], "outputs": [ {"name": "Result", "type": "Number"} ], "operations": ["Addition", "Subtraction", "Multiplication", "Division", "Power", "Modulo"] }, { "name": "Construct Point", "fullName": "Construct Point", "description": "Constructs a point from X, Y, Z coordinates", "inputs": [ {"name": "X", "type": "Number"}, {"name": "Y", "type": "Number"}, {"name": "Z", "type": "Number"} ], "outputs": [ {"name": "Pt", "type": "Point"} ] }, { "name": "Line", "fullName": "Line", "description": "Creates a line between two points", "inputs": [ {"name": "Start", "type": "Point"}, {"name": "End", "type": "Point"} ], "outputs": [ {"name": "L", "type": "Line"} ] }, { "name": "Extrude", "fullName": "Extrude", "description": "Extrudes a curve to create a surface or a solid", "inputs": [ {"name": "Base", "type": "Curve"}, {"name": "Direction", "type": "Vector"}, {"name": "Height", "type": "Number"} ], "outputs": [ {"name": "Brep", "type": "Brep"} ] } ], "connectionRules": [ { "from": "Number", "to": "Circle.Radius", "description": "Connect a number to the radius input of a circle" }, { "from": "Point", "to": "Circle.Plane", "description": "Connect a point to the plane input of a circle (not recommended, use XY Plane instead)" }, { "from": "XY Plane", "to": "Circle.Plane", "description": "Connect an XY Plane to the plane input of a circle (recommended)" }, { "from": "Number", "to": "Math.A", "description": "Connect a number to the first input of a Math component" }, { "from": "Number", "to": "Math.B", "description": "Connect a number to the second input of a Math component" }, { "from": "Number", "to": "Construct Point.X", "description": "Connect a number to the X input of a Construct Point component" }, { "from": "Number", "to": "Construct Point.Y", "description": "Connect a number to the Y input of a Construct Point component" }, { "from": "Number", "to": "Construct Point.Z", "description": "Connect a number to the Z input of a Construct Point component" }, { "from": "Point", "to": "Line.Start", "description": "Connect a point to the start input of a Line component" }, { "from": "Point", "to": "Line.End", "description": "Connect a point to the end input of a Line component" }, { "from": "Circle", "to": "Extrude.Base", "description": "Connect a circle to the base input of an Extrude component" }, { "from": "Number", "to": "Extrude.Height", "description": "Connect a number to the height input of an Extrude component" } ], "commonIssues": [ "Using Point component instead of XY Plane for inputs that require planes", "Not specifying parameter names when connecting components", "Using incorrect component names (e.g., 'addition' instead of 'Math' with Addition operation)", "Trying to connect incompatible data types", "Not providing all required inputs for a component", "Using incorrect parameter names (e.g., 'A' and 'B' for Math component instead of the actual parameter names)", "Not checking if a connection was successful before proceeding" ], "tips": [ "Always use XY Plane component for plane inputs", "Specify parameter names when connecting components", "For Circle components, make sure to use the correct inputs (Plane and Radius)", "Test simple connections before creating complex geometry", "Avoid using components that require selection from Rhino", "Use get_component_info to check the actual parameter names of a component", "Use get_connections to verify if connections were established correctly", "Use search_components to find the correct component name before adding it", "Use validate_connection to check if a connection is possible before attempting it" ] } @server.resource("grasshopper://component_library") def get_component_library(): """Get a comprehensive library of Grasshopper components""" # 這個資源提供了一個更全面的組件庫,包括常用組件的詳細信息 return { "categories": [ { "name": "Params", "components": [ { "name": "Point", "fullName": "Point Parameter", "description": "Creates a point parameter", "inputs": [ {"name": "X", "type": "Number", "description": "X coordinate"}, {"name": "Y", "type": "Number", "description": "Y coordinate"}, {"name": "Z", "type": "Number", "description": "Z coordinate"} ], "outputs": [ {"name": "Pt", "type": "Point", "description": "Point output"} ] }, { "name": "Number Slider", "fullName": "Number Slider", "description": "Creates a slider for numeric input with adjustable range and precision", "inputs": [], "outputs": [ {"name": "N", "type": "Number", "description": "Number output"} ], "settings": { "min": {"description": "Minimum value of the slider", "default": 0}, "max": {"description": "Maximum value of the slider", "default": 10}, "value": {"description": "Current value of the slider", "default": 5}, "rounding": {"description": "Rounding precision (0.01, 0.1, 1, etc.)", "default": 0.1}, "type": {"description": "Slider type (integer, floating point)", "default": "float"}, "name": {"description": "Custom name for the slider", "default": ""} }, "usage_examples": [ "Create a Number Slider with min=0, max=100, value=50", "Create a Number Slider for radius with min=0.1, max=10, value=2.5, rounding=0.1" ], "common_issues": [ "Confusing with other slider types", "Not setting appropriate min/max values for the intended use" ], "disambiguation": { "similar_components": [ { "name": "MD Slider", "description": "Multi-dimensional slider for vector input, NOT for simple numeric values", "how_to_distinguish": "Use Number Slider for single numeric values; use MD Slider only when you need multi-dimensional control" }, { "name": "Graph Mapper", "description": "Maps values through a graph function, NOT a simple slider", "how_to_distinguish": "Use Number Slider for direct numeric input; use Graph Mapper only for function-based mapping" } ], "correct_usage": "When needing a simple numeric input control, ALWAYS use 'Number Slider', not MD Slider or other variants" } }, { "name": "Panel", "fullName": "Panel", "description": "Displays text or numeric data", "inputs": [ {"name": "Input", "type": "Any", "description": "Any input data"} ], "outputs": [] } ] }, { "name": "Maths", "components": [ { "name": "Math", "fullName": "Mathematics", "description": "Performs mathematical operations", "inputs": [ {"name": "A", "type": "Number", "description": "First number"}, {"name": "B", "type": "Number", "description": "Second number"} ], "outputs": [ {"name": "Result", "type": "Number", "description": "Result of the operation"} ], "operations": ["Addition", "Subtraction", "Multiplication", "Division", "Power", "Modulo"] } ] }, { "name": "Vector", "components": [ { "name": "XY Plane", "fullName": "XY Plane", "description": "Creates an XY plane at the world origin or at a specified point", "inputs": [ {"name": "Origin", "type": "Point", "description": "Origin point", "optional": True} ], "outputs": [ {"name": "Plane", "type": "Plane", "description": "XY plane"} ] }, { "name": "Construct Point", "fullName": "Construct Point", "description": "Constructs a point from X, Y, Z coordinates", "inputs": [ {"name": "X", "type": "Number", "description": "X coordinate"}, {"name": "Y", "type": "Number", "description": "Y coordinate"}, {"name": "Z", "type": "Number", "description": "Z coordinate"} ], "outputs": [ {"name": "Pt", "type": "Point", "description": "Constructed point"} ] } ] }, { "name": "Curve", "components": [ { "name": "Circle", "fullName": "Circle", "description": "Creates a circle", "inputs": [ {"name": "Plane", "type": "Plane", "description": "Base plane for the circle"}, {"name": "Radius", "type": "Number", "description": "Circle radius"} ], "outputs": [ {"name": "C", "type": "Circle", "description": "Circle output"} ] }, { "name": "Line", "fullName": "Line", "description": "Creates a line between two points", "inputs": [ {"name": "Start", "type": "Point", "description": "Start point"}, {"name": "End", "type": "Point", "description": "End point"} ], "outputs": [ {"name": "L", "type": "Line", "description": "Line output"} ] } ] }, { "name": "Surface", "components": [ { "name": "Extrude", "fullName": "Extrude", "description": "Extrudes a curve to create a surface or a solid", "inputs": [ {"name": "Base", "type": "Curve", "description": "Base curve to extrude"}, {"name": "Direction", "type": "Vector", "description": "Direction of extrusion", "optional": True}, {"name": "Height", "type": "Number", "description": "Height of extrusion"} ], "outputs": [ {"name": "Brep", "type": "Brep", "description": "Extruded brep"} ] } ] } ], "dataTypes": [ { "name": "Number", "description": "A numeric value", "compatibleWith": ["Number", "Integer", "Double"] }, { "name": "Point", "description": "A 3D point in space", "compatibleWith": ["Point3d", "Point"] }, { "name": "Vector", "description": "A 3D vector", "compatibleWith": ["Vector3d", "Vector"] }, { "name": "Plane", "description": "A plane in 3D space", "compatibleWith": ["Plane"] }, { "name": "Circle", "description": "A circle curve", "compatibleWith": ["Circle", "Curve"] }, { "name": "Line", "description": "A line segment", "compatibleWith": ["Line", "Curve"] }, { "name": "Curve", "description": "A curve object", "compatibleWith": ["Curve", "Circle", "Line", "Arc", "Polyline"] }, { "name": "Brep", "description": "A boundary representation object", "compatibleWith": ["Brep", "Surface", "Solid"] } ] } def main(): """Main entry point for the Grasshopper MCP Bridge Server""" try: # 啟動 MCP 服務器 print("Starting Grasshopper MCP Bridge Server...", file=sys.stderr) print("Please add this MCP server to Claude Desktop", file=sys.stderr) server.run() except Exception as e: print(f"Error starting MCP server: {str(e)}", file=sys.stderr) traceback.print_exc(file=sys.stderr) sys.exit(1) if __name__ == "__main__": main() ```