# 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: -------------------------------------------------------------------------------- ``` 1 | # Python 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | *.so 6 | .Python 7 | build/ 8 | develop-eggs/ 9 | dist/ 10 | downloads/ 11 | eggs/ 12 | .eggs/ 13 | lib/ 14 | lib64/ 15 | parts/ 16 | sdist/ 17 | var/ 18 | wheels/ 19 | *.egg-info/ 20 | .installed.cfg 21 | *.egg 22 | MANIFEST 23 | 24 | # Visual Studio 25 | .vs/ 26 | bin/ 27 | obj/ 28 | *.user 29 | *.userosscache 30 | *.suo 31 | *.userprefs 32 | *.dbmdl 33 | *.dbproj.schemaview 34 | *.jfm 35 | *.pfx 36 | *.publishsettings 37 | orleans.codegen.cs 38 | 39 | # Rhino and Grasshopper 40 | *.rhi 41 | *.ghx 42 | *.gh~ 43 | *.3dm.rhl 44 | *.3dmbak 45 | *.3dm.rhl 46 | *.3dm.bak 47 | 48 | # IDE 49 | .idea/ 50 | .vscode/ 51 | *.swp 52 | *.swo 53 | 54 | # OS specific 55 | .DS_Store 56 | Thumbs.db 57 | ehthumbs.db 58 | Desktop.ini 59 | $RECYCLE.BIN/ 60 | 61 | # Project specific 62 | Grasshopper Tutorial for Beginners.pdf 63 | grasshopper_mcp_bridge.py 64 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # Grasshopper MCP Bridge 2 | 3 | Grasshopper MCP Bridge is a bridging server that connects Grasshopper and Claude Desktop using the Model Context Protocol (MCP) standard. 4 | 5 | ## Features 6 | 7 | - Connects Grasshopper and Claude Desktop through the MCP protocol 8 | - Provides intuitive tool functions for creating and connecting Grasshopper components 9 | - Supports high-level intent recognition, automatically creating complex component patterns from simple descriptions 10 | - Includes a component knowledge base that understands parameters and connection rules for common components 11 | - Provides component guidance resources to help Claude Desktop correctly connect components 12 | 13 | ## System Architecture 14 | 15 | The system consists of the following parts: 16 | 17 | 1. **Grasshopper MCP Component (GH_MCP.gha)**: A plugin installed in Grasshopper that provides a TCP server to receive commands 18 | 2. **Python MCP Bridge Server**: A bridge server that connects Claude Desktop and the Grasshopper MCP component 19 | 3. **Component Knowledge Base**: JSON files containing component information, patterns, and intents 20 | 21 | ## Installation Instructions 22 | 23 | ### Prerequisites 24 | 25 | - Rhino 7 or higher 26 | - Grasshopper 27 | - Python 3.8 or higher 28 | - Claude Desktop 29 | 30 | ### Installation Steps 31 | 32 | 1. **Install the Grasshopper MCP Component** 33 | 34 | **Method 1: Download the pre-compiled GH_MCP.gha file (Recommended)** 35 | 36 | 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: 37 | ``` 38 | %APPDATA%\Grasshopper\Libraries\ 39 | ``` 40 | 41 | **Method 2: Build from source** 42 | 43 | If you prefer to build from source, clone the repository and build the C# project using Visual Studio. 44 | 45 | 2. **Install the Python MCP Bridge Server** 46 | 47 | **Method 1: Install from PyPI (Recommended)** 48 | 49 | The simplest method is to install directly from PyPI using pip: 50 | ``` 51 | pip install grasshopper-mcp 52 | ``` 53 | 54 | **Method 2: Install from GitHub** 55 | 56 | You can also install the latest version from GitHub: 57 | ``` 58 | pip install git+https://github.com/alfredatnycu/grasshopper-mcp.git 59 | ``` 60 | 61 | **Method 3: Install from Source Code** 62 | 63 | If you need to modify the code or develop new features, you can clone the repository and install: 64 | ``` 65 | git clone https://github.com/alfredatnycu/grasshopper-mcp.git 66 | cd grasshopper-mcp 67 | pip install -e . 68 | ``` 69 | 70 | **Install a Specific Version** 71 | 72 | If you need to install a specific version, you can use: 73 | ``` 74 | pip install grasshopper-mcp==0.1.0 75 | ``` 76 | Or install from a specific GitHub tag: 77 | ``` 78 | pip install git+https://github.com/alfredatnycu/[email protected] 79 | ``` 80 | 81 | ## Usage 82 | 83 | 1. **Start Rhino and Grasshopper** 84 | 85 | Launch Rhino and open Grasshopper. 86 | 87 | 2. **Add the GH_MCP Component to Your Canvas** 88 | 89 | Find the GH_MCP component in the Grasshopper component panel and add it to your canvas. 90 | 91 | 3. **Start the Python MCP Bridge Server** 92 | 93 | Open a terminal and run: 94 | ``` 95 | python -m grasshopper_mcp.bridge 96 | ``` 97 | 98 | > **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. 99 | 100 | 4. **Connect Claude Desktop to the MCP Bridge** 101 | 102 | **Method 1: Manual Connection** 103 | 104 | In Claude Desktop, connect to the MCP Bridge server using the following settings: 105 | - Protocol: MCP 106 | - Host: localhost 107 | - Port: 8080 108 | 109 | **Method 2: Configure Claude Desktop to Auto-Start the Bridge** 110 | 111 | You can configure Claude Desktop to automatically start the MCP Bridge server by modifying its configuration: 112 | 113 | ```json 114 | "grasshopper": { 115 | "command": "python", 116 | "args": ["-m", "grasshopper_mcp.bridge"] 117 | } 118 | ``` 119 | 120 | This configuration tells Claude Desktop to use the command `python -m grasshopper_mcp.bridge` to start the MCP server. 121 | 122 | 5. **Start Using Grasshopper with Claude Desktop** 123 | 124 | You can now use Claude Desktop to control Grasshopper through natural language commands. 125 | 126 | ## Example Commands 127 | 128 | Here are some example commands you can use with Claude Desktop: 129 | 130 | - "Create a circle with radius 5 at point (0,0,0)" 131 | - "Connect the circle to a extrude component with a height of 10" 132 | - "Create a grid of points with 5 rows and 5 columns" 133 | - "Apply a random rotation to all selected objects" 134 | 135 | ## Troubleshooting 136 | 137 | If you encounter issues, check the following: 138 | 139 | 1. **GH_MCP Component Not Loading** 140 | - Ensure the .gha file is in the correct location 141 | - In Grasshopper, go to File > Preferences > Libraries and click "Unblock" to unblock new components 142 | - Restart Rhino and Grasshopper 143 | 144 | 2. **Bridge Server Won't Start** 145 | - If `grasshopper-mcp` command doesn't work, use `python -m grasshopper_mcp.bridge` instead 146 | - Ensure all required Python dependencies are installed 147 | - Check if port 8080 is already in use by another application 148 | 149 | 3. **Claude Desktop Can't Connect** 150 | - Ensure the bridge server is running 151 | - Verify you're using the correct connection settings (localhost:8080) 152 | - Check the console output of the bridge server for any error messages 153 | 154 | 4. **Commands Not Executing** 155 | - Verify the GH_MCP component is on your Grasshopper canvas 156 | - Check the bridge server console for error messages 157 | - Ensure Claude Desktop is properly connected to the bridge server 158 | 159 | ## Development 160 | 161 | ### Project Structure 162 | 163 | ``` 164 | grasshopper-mcp/ 165 | ├── grasshopper_mcp/ # Python bridge server 166 | │ ├── __init__.py 167 | │ └── bridge.py # Main bridge server implementation 168 | ├── GH_MCP/ # Grasshopper component (C#) 169 | │ └── ... 170 | ├── releases/ # Pre-compiled binaries 171 | │ └── GH_MCP.gha # Compiled Grasshopper component 172 | ├── setup.py # Python package setup 173 | └── README.md # This file 174 | ``` 175 | 176 | ### Contributing 177 | 178 | Contributions are welcome! Please feel free to submit a Pull Request. 179 | 180 | ## License 181 | 182 | This project is licensed under the MIT License - see the LICENSE file for details. 183 | 184 | ## Acknowledgments 185 | 186 | - Thanks to the Rhino and Grasshopper community for their excellent tools 187 | - Thanks to Anthropic for Claude Desktop and the MCP protocol 188 | 189 | ## Contact 190 | 191 | For questions or support, please open an issue on the GitHub repository. 192 | ``` -------------------------------------------------------------------------------- /grasshopper_mcp/__init__.py: -------------------------------------------------------------------------------- ```python 1 | """ 2 | Grasshopper MCP Bridge Server 3 | """ 4 | 5 | __version__ = "0.1.0" 6 | ``` -------------------------------------------------------------------------------- /GH_MCP/GH_MCP/Properties/launchSettings.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "profiles": { 3 | "Rhino 8 - netcore": { 4 | "commandName": "Executable", 5 | "executablePath": "C:\\Program Files\\Rhino 8\\System\\Rhino.exe", 6 | "commandLineArgs": "/netcore /runscript=\"_Grasshopper\"", 7 | "environmentVariables": { 8 | "RHINO_PACKAGE_DIRS": "$(ProjectDir)$(OutputPath)\\" 9 | } 10 | }, 11 | "Rhino 8 - netfx": { 12 | "commandName": "Executable", 13 | "executablePath": "C:\\Program Files\\Rhino 8\\System\\Rhino.exe", 14 | "commandLineArgs": "/netfx /runscript=\"_Grasshopper\"", 15 | "environmentVariables": { 16 | "RHINO_PACKAGE_DIRS": "$(ProjectDir)$(OutputPath)\\" 17 | } 18 | }, 19 | } 20 | } ``` -------------------------------------------------------------------------------- /GH_MCP/GH_MCP/GH_MCPInfo.cs: -------------------------------------------------------------------------------- ```csharp 1 | using System; 2 | using System.Drawing; 3 | using Grasshopper; 4 | using Grasshopper.Kernel; 5 | 6 | namespace GrasshopperMCP 7 | { 8 | public class GH_MCPInfo : GH_AssemblyInfo 9 | { 10 | public override string Name => "GH_MCP"; 11 | 12 | //Return a 24x24 pixel bitmap to represent this GHA library. 13 | public override Bitmap Icon => null; 14 | 15 | //Return a short string describing the purpose of this GHA library. 16 | public override string Description => ""; 17 | 18 | public override Guid Id => new Guid("1b472cf6-015c-496a-a0a1-7ced4df994a3"); 19 | 20 | //Return a string identifying you or your company. 21 | public override string AuthorName => ""; 22 | 23 | //Return a string representing your preferred contact details. 24 | public override string AuthorContact => ""; 25 | 26 | //Return a string representing the version. This returns the same version as the assembly. 27 | public override string AssemblyVersion => GetType().Assembly.GetName().Version.ToString(); 28 | } 29 | } ``` -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- ```python 1 | from setuptools import setup, find_packages 2 | import os 3 | 4 | # 讀取 README.md 作為長描述 5 | with open("README.md", "r", encoding="utf-8") as fh: 6 | long_description = fh.read() 7 | 8 | setup( 9 | name="grasshopper-mcp", 10 | version="0.1.0", 11 | packages=find_packages(), 12 | include_package_data=True, 13 | install_requires=[ 14 | "mcp>=0.1.0", 15 | "websockets>=10.0", 16 | "aiohttp>=3.8.0", 17 | ], 18 | entry_points={ 19 | "console_scripts": [ 20 | "grasshopper-mcp=grasshopper_mcp.bridge:main", 21 | ], 22 | }, 23 | author="Alfred Chen", 24 | author_email="[email protected]", 25 | description="Grasshopper MCP Bridge Server", 26 | long_description=long_description, 27 | long_description_content_type="text/markdown", 28 | keywords="grasshopper, mcp, bridge, server", 29 | url="https://github.com/alfredatnycu/grasshopper-mcp", 30 | classifiers=[ 31 | "Development Status :: 3 - Alpha", 32 | "Intended Audience :: Developers", 33 | "Programming Language :: Python :: 3", 34 | "Programming Language :: Python :: 3.8", 35 | "Programming Language :: Python :: 3.9", 36 | ], 37 | python_requires=">=3.8", 38 | ) 39 | ``` -------------------------------------------------------------------------------- /GH_MCP/GH_MCP/Commands/GeometryCommandHandler.cs: -------------------------------------------------------------------------------- ```csharp 1 | using System; 2 | using System.Collections.Generic; 3 | using GrasshopperMCP.Models; 4 | using Grasshopper.Kernel; 5 | using Rhino.Geometry; 6 | using Newtonsoft.Json.Linq; 7 | using System.Linq; 8 | using Rhino; 9 | 10 | namespace GrasshopperMCP.Commands 11 | { 12 | /// <summary> 13 | /// 處理幾何相關命令的處理器 14 | /// </summary> 15 | public static class GeometryCommandHandler 16 | { 17 | /// <summary> 18 | /// 創建點 19 | /// </summary> 20 | /// <param name="command">包含點坐標的命令</param> 21 | /// <returns>創建的點信息</returns> 22 | public static object CreatePoint(Command command) 23 | { 24 | double x = command.GetParameter<double>("x"); 25 | double y = command.GetParameter<double>("y"); 26 | double z = command.GetParameter<double>("z"); 27 | 28 | // 創建點 29 | Point3d point = new Point3d(x, y, z); 30 | 31 | // 返回點信息 32 | return new 33 | { 34 | id = Guid.NewGuid().ToString(), 35 | x = point.X, 36 | y = point.Y, 37 | z = point.Z 38 | }; 39 | } 40 | 41 | /// <summary> 42 | /// 創建曲線 43 | /// </summary> 44 | /// <param name="command">包含曲線點的命令</param> 45 | /// <returns>創建的曲線信息</returns> 46 | public static object CreateCurve(Command command) 47 | { 48 | var pointsData = command.GetParameter<JArray>("points"); 49 | 50 | if (pointsData == null || pointsData.Count < 2) 51 | { 52 | throw new ArgumentException("At least 2 points are required to create a curve"); 53 | } 54 | 55 | // 將 JSON 點數據轉換為 Point3d 列表 56 | List<Point3d> points = new List<Point3d>(); 57 | foreach (var pointData in pointsData) 58 | { 59 | double x = pointData["x"].Value<double>(); 60 | double y = pointData["y"].Value<double>(); 61 | double z = pointData["z"]?.Value<double>() ?? 0.0; 62 | 63 | points.Add(new Point3d(x, y, z)); 64 | } 65 | 66 | // 創建曲線 67 | Curve curve; 68 | if (points.Count == 2) 69 | { 70 | // 如果只有兩個點,創建一條直線 71 | curve = new LineCurve(points[0], points[1]); 72 | } 73 | else 74 | { 75 | // 如果有多個點,創建一條內插曲線 76 | curve = Curve.CreateInterpolatedCurve(points, 3); 77 | } 78 | 79 | // 返回曲線信息 80 | return new 81 | { 82 | id = Guid.NewGuid().ToString(), 83 | pointCount = points.Count, 84 | length = curve.GetLength() 85 | }; 86 | } 87 | 88 | /// <summary> 89 | /// 創建圓 90 | /// </summary> 91 | /// <param name="command">包含圓心和半徑的命令</param> 92 | /// <returns>創建的圓信息</returns> 93 | public static object CreateCircle(Command command) 94 | { 95 | var centerData = command.GetParameter<JObject>("center"); 96 | double radius = command.GetParameter<double>("radius"); 97 | 98 | if (centerData == null) 99 | { 100 | throw new ArgumentException("Center point is required"); 101 | } 102 | 103 | if (radius <= 0) 104 | { 105 | throw new ArgumentException("Radius must be greater than 0"); 106 | } 107 | 108 | // 解析圓心 109 | double x = centerData["x"].Value<double>(); 110 | double y = centerData["y"].Value<double>(); 111 | double z = centerData["z"]?.Value<double>() ?? 0.0; 112 | 113 | Point3d center = new Point3d(x, y, z); 114 | 115 | // 創建圓 116 | Circle circle = new Circle(center, radius); 117 | 118 | // 返回圓信息 119 | return new 120 | { 121 | id = Guid.NewGuid().ToString(), 122 | center = new { x = center.X, y = center.Y, z = center.Z }, 123 | radius = circle.Radius, 124 | circumference = circle.Circumference 125 | }; 126 | } 127 | } 128 | } 129 | ``` -------------------------------------------------------------------------------- /GH_MCP/GH_MCP/Commands/IntentCommandHandler.cs: -------------------------------------------------------------------------------- ```csharp 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading; 5 | using GrasshopperMCP.Models; 6 | using GrasshopperMCP.Commands; 7 | using GH_MCP.Models; 8 | using GH_MCP.Utils; 9 | using Rhino; 10 | using Newtonsoft.Json; 11 | 12 | namespace GH_MCP.Commands 13 | { 14 | /// <summary> 15 | /// 處理高層次意圖命令的處理器 16 | /// </summary> 17 | public class IntentCommandHandler 18 | { 19 | private static Dictionary<string, string> _componentIdMap = new Dictionary<string, string>(); 20 | 21 | /// <summary> 22 | /// 處理創建模式命令 23 | /// </summary> 24 | /// <param name="command">命令對象</param> 25 | /// <returns>命令執行結果</returns> 26 | public static object CreatePattern(Command command) 27 | { 28 | // 獲取模式名稱或描述 29 | if (!command.Parameters.TryGetValue("description", out object descriptionObj) || descriptionObj == null) 30 | { 31 | return Response.CreateError("Missing required parameter: description"); 32 | } 33 | string description = descriptionObj.ToString(); 34 | 35 | // 識別意圖 36 | string patternName = IntentRecognizer.RecognizeIntent(description); 37 | if (string.IsNullOrEmpty(patternName)) 38 | { 39 | return Response.CreateError($"Could not recognize intent from description: {description}"); 40 | } 41 | 42 | RhinoApp.WriteLine($"Recognized intent: {patternName}"); 43 | 44 | // 獲取模式詳細信息 45 | var (components, connections) = IntentRecognizer.GetPatternDetails(patternName); 46 | if (components.Count == 0) 47 | { 48 | return Response.CreateError($"Pattern '{patternName}' has no components defined"); 49 | } 50 | 51 | // 清空組件 ID 映射 52 | _componentIdMap.Clear(); 53 | 54 | // 創建所有組件 55 | foreach (var component in components) 56 | { 57 | try 58 | { 59 | // 創建組件命令 60 | var addCommand = new Command( 61 | "add_component", 62 | new Dictionary<string, object> 63 | { 64 | { "type", component.Type }, 65 | { "x", component.X }, 66 | { "y", component.Y } 67 | } 68 | ); 69 | 70 | // 如果有設置,添加設置 71 | if (component.Settings != null) 72 | { 73 | foreach (var setting in component.Settings) 74 | { 75 | addCommand.Parameters.Add(setting.Key, setting.Value); 76 | } 77 | } 78 | 79 | // 執行添加組件命令 80 | var result = ComponentCommandHandler.AddComponent(addCommand); 81 | if (result is Response response && response.Success && response.Data != null) 82 | { 83 | // 保存組件 ID 映射 84 | string componentId = response.Data.ToString(); 85 | _componentIdMap[component.Id] = componentId; 86 | RhinoApp.WriteLine($"Created component {component.Type} with ID {componentId}"); 87 | } 88 | else 89 | { 90 | RhinoApp.WriteLine($"Failed to create component {component.Type}"); 91 | } 92 | } 93 | catch (Exception ex) 94 | { 95 | RhinoApp.WriteLine($"Error creating component {component.Type}: {ex.Message}"); 96 | } 97 | 98 | // 添加短暫延遲,確保組件創建完成 99 | Thread.Sleep(100); 100 | } 101 | 102 | // 創建所有連接 103 | foreach (var connection in connections) 104 | { 105 | try 106 | { 107 | // 檢查源和目標組件 ID 是否存在 108 | if (!_componentIdMap.TryGetValue(connection.SourceId, out string sourceId) || 109 | !_componentIdMap.TryGetValue(connection.TargetId, out string targetId)) 110 | { 111 | RhinoApp.WriteLine($"Could not find component IDs for connection {connection.SourceId} -> {connection.TargetId}"); 112 | continue; 113 | } 114 | 115 | // 創建連接命令 116 | var connectCommand = new Command( 117 | "connect_components", 118 | new Dictionary<string, object> 119 | { 120 | { "sourceId", sourceId }, 121 | { "sourceParam", connection.SourceParam }, 122 | { "targetId", targetId }, 123 | { "targetParam", connection.TargetParam } 124 | } 125 | ); 126 | 127 | // 執行連接命令 128 | var result = ConnectionCommandHandler.ConnectComponents(connectCommand); 129 | if (result is Response response && response.Success) 130 | { 131 | RhinoApp.WriteLine($"Connected {connection.SourceId}.{connection.SourceParam} -> {connection.TargetId}.{connection.TargetParam}"); 132 | } 133 | else 134 | { 135 | RhinoApp.WriteLine($"Failed to connect {connection.SourceId}.{connection.SourceParam} -> {connection.TargetId}.{connection.TargetParam}"); 136 | } 137 | } 138 | catch (Exception ex) 139 | { 140 | RhinoApp.WriteLine($"Error creating connection: {ex.Message}"); 141 | } 142 | 143 | // 添加短暫延遲,確保連接創建完成 144 | Thread.Sleep(100); 145 | } 146 | 147 | // 返回成功結果 148 | return Response.Ok(new 149 | { 150 | Pattern = patternName, 151 | ComponentCount = components.Count, 152 | ConnectionCount = connections.Count 153 | }); 154 | } 155 | 156 | /// <summary> 157 | /// 獲取可用的模式列表 158 | /// </summary> 159 | /// <param name="command">命令對象</param> 160 | /// <returns>命令執行結果</returns> 161 | public static object GetAvailablePatterns(Command command) 162 | { 163 | // 初始化意圖識別器 164 | IntentRecognizer.Initialize(); 165 | 166 | // 獲取所有可用的模式 167 | var patterns = new List<string>(); 168 | if (command.Parameters.TryGetValue("query", out object queryObj) && queryObj != null) 169 | { 170 | string query = queryObj.ToString(); 171 | string patternName = IntentRecognizer.RecognizeIntent(query); 172 | if (!string.IsNullOrEmpty(patternName)) 173 | { 174 | patterns.Add(patternName); 175 | } 176 | } 177 | else 178 | { 179 | // 如果沒有查詢,返回所有模式 180 | // 這裡需要擴展 IntentRecognizer 以支持獲取所有模式 181 | // 暫時返回空列表 182 | } 183 | 184 | // 返回成功結果 185 | return Response.Ok(patterns); 186 | } 187 | } 188 | } 189 | ``` -------------------------------------------------------------------------------- /GH_MCP/GH_MCP/GH_MCPComponent.cs: -------------------------------------------------------------------------------- ```csharp 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Drawing; 4 | using System.Net; 5 | using System.Net.Sockets; 6 | using System.Text; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | using GH_MCP.Commands; 10 | using GrasshopperMCP.Models; 11 | using Grasshopper.Kernel; 12 | using Rhino; 13 | using Newtonsoft.Json; 14 | using System.IO; 15 | 16 | namespace GrasshopperMCP 17 | { 18 | /// <summary> 19 | /// Grasshopper MCP 組件,用於與 Python 伺服器通信 20 | /// </summary> 21 | public class GrasshopperMCPComponent : GH_Component 22 | { 23 | private static TcpListener listener; 24 | private static bool isRunning = false; 25 | private static int grasshopperPort = 8080; 26 | 27 | /// <summary> 28 | /// 初始化 GrasshopperMCPComponent 類的新實例 29 | /// </summary> 30 | public GrasshopperMCPComponent() 31 | : base("Grasshopper MCP", "MCP", "Machine Control Protocol for Grasshopper", "Params", "Util") 32 | { 33 | } 34 | 35 | /// <summary> 36 | /// 註冊輸入參數 37 | /// </summary> 38 | protected override void RegisterInputParams(GH_Component.GH_InputParamManager pManager) 39 | { 40 | pManager.AddBooleanParameter("Enabled", "E", "Enable or disable the MCP server", GH_ParamAccess.item, false); 41 | pManager.AddIntegerParameter("Port", "P", "Port to listen on", GH_ParamAccess.item, grasshopperPort); 42 | } 43 | 44 | /// <summary> 45 | /// 註冊輸出參數 46 | /// </summary> 47 | protected override void RegisterOutputParams(GH_Component.GH_OutputParamManager pManager) 48 | { 49 | pManager.AddTextParameter("Status", "S", "Server status", GH_ParamAccess.item); 50 | pManager.AddTextParameter("LastCommand", "C", "Last received command", GH_ParamAccess.item); 51 | } 52 | 53 | /// <summary> 54 | /// 解決組件 55 | /// </summary> 56 | protected override void SolveInstance(IGH_DataAccess DA) 57 | { 58 | bool enabled = false; 59 | int port = grasshopperPort; 60 | 61 | // 獲取輸入參數 62 | if (!DA.GetData(0, ref enabled)) return; 63 | if (!DA.GetData(1, ref port)) return; 64 | 65 | // 更新端口 66 | grasshopperPort = port; 67 | 68 | // 根據啟用狀態啟動或停止伺服器 69 | if (enabled && !isRunning) 70 | { 71 | Start(); 72 | DA.SetData(0, $"Running on port {grasshopperPort}"); 73 | } 74 | else if (!enabled && isRunning) 75 | { 76 | Stop(); 77 | DA.SetData(0, "Stopped"); 78 | } 79 | else if (enabled && isRunning) 80 | { 81 | DA.SetData(0, $"Running on port {grasshopperPort}"); 82 | } 83 | else 84 | { 85 | DA.SetData(0, "Stopped"); 86 | } 87 | 88 | // 設置最後接收的命令 89 | DA.SetData(1, LastCommand); 90 | } 91 | 92 | /// <summary> 93 | /// 組件 GUID 94 | /// </summary> 95 | public override Guid ComponentGuid => new Guid("12345678-1234-1234-1234-123456789012"); 96 | 97 | /// <summary> 98 | /// 暴露圖標 99 | /// </summary> 100 | protected override Bitmap Icon => null; 101 | 102 | /// <summary> 103 | /// 最後接收的命令 104 | /// </summary> 105 | public static string LastCommand { get; private set; } = "None"; 106 | 107 | /// <summary> 108 | /// 啟動 MCP 伺服器 109 | /// </summary> 110 | public static void Start() 111 | { 112 | if (isRunning) return; 113 | 114 | // 初始化命令註冊表 115 | GrasshopperCommandRegistry.Initialize(); 116 | 117 | // 啟動 TCP 監聽器 118 | isRunning = true; 119 | listener = new TcpListener(IPAddress.Loopback, grasshopperPort); 120 | listener.Start(); 121 | RhinoApp.WriteLine($"GrasshopperMCPBridge started on port {grasshopperPort}."); 122 | 123 | // 開始接收連接 124 | Task.Run(ListenerLoop); 125 | } 126 | 127 | /// <summary> 128 | /// 停止 MCP 伺服器 129 | /// </summary> 130 | public static void Stop() 131 | { 132 | if (!isRunning) return; 133 | 134 | isRunning = false; 135 | listener.Stop(); 136 | RhinoApp.WriteLine("GrasshopperMCPBridge stopped."); 137 | } 138 | 139 | /// <summary> 140 | /// 監聽循環,處理傳入的連接 141 | /// </summary> 142 | private static async Task ListenerLoop() 143 | { 144 | try 145 | { 146 | while (isRunning) 147 | { 148 | // 等待客戶端連接 149 | var client = await listener.AcceptTcpClientAsync(); 150 | RhinoApp.WriteLine("GrasshopperMCPBridge: Client connected."); 151 | 152 | // 處理客戶端連接 153 | _ = Task.Run(() => HandleClient(client)); 154 | } 155 | } 156 | catch (Exception ex) 157 | { 158 | if (isRunning) 159 | { 160 | RhinoApp.WriteLine($"GrasshopperMCPBridge error: {ex.Message}"); 161 | isRunning = false; 162 | } 163 | } 164 | } 165 | 166 | /// <summary> 167 | /// 處理客戶端連接 168 | /// </summary> 169 | /// <param name="client">TCP 客戶端</param> 170 | private static async Task HandleClient(TcpClient client) 171 | { 172 | using (client) 173 | using (var stream = client.GetStream()) 174 | using (var reader = new StreamReader(stream, Encoding.UTF8)) 175 | using (var writer = new StreamWriter(stream, Encoding.UTF8) { AutoFlush = true }) 176 | { 177 | try 178 | { 179 | // 讀取命令 180 | string commandJson = await reader.ReadLineAsync(); 181 | if (string.IsNullOrEmpty(commandJson)) 182 | { 183 | return; 184 | } 185 | 186 | // 更新最後接收的命令 187 | LastCommand = commandJson; 188 | 189 | // 解析命令 190 | Command command = JsonConvert.DeserializeObject<Command>(commandJson); 191 | RhinoApp.WriteLine($"GrasshopperMCPBridge: Received command: {command.Type}"); 192 | 193 | // 執行命令 194 | Response response = GrasshopperCommandRegistry.ExecuteCommand(command); 195 | 196 | // 發送響應 197 | string responseJson = JsonConvert.SerializeObject(response); 198 | await writer.WriteLineAsync(responseJson); 199 | 200 | RhinoApp.WriteLine($"GrasshopperMCPBridge: Command {command.Type} executed with result: {(response.Success ? "Success" : "Error")}"); 201 | } 202 | catch (Exception ex) 203 | { 204 | RhinoApp.WriteLine($"GrasshopperMCPBridge error handling client: {ex.Message}"); 205 | 206 | // 發送錯誤響應 207 | Response errorResponse = Response.CreateError($"Server error: {ex.Message}"); 208 | string errorResponseJson = JsonConvert.SerializeObject(errorResponse); 209 | await writer.WriteLineAsync(errorResponseJson); 210 | } 211 | } 212 | } 213 | } 214 | } 215 | ``` -------------------------------------------------------------------------------- /GH_MCP/GH_MCP/Resources/ComponentKnowledgeBase.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "components": [ 3 | { 4 | "name": "Point", 5 | "category": "Params", 6 | "subcategory": "Geometry", 7 | "description": "Creates a point at the specified coordinates", 8 | "inputs": [ 9 | {"name": "X", "type": "Number", "description": "X coordinate"}, 10 | {"name": "Y", "type": "Number", "description": "Y coordinate"}, 11 | {"name": "Z", "type": "Number", "description": "Z coordinate"} 12 | ], 13 | "outputs": [ 14 | {"name": "Pt", "type": "Point", "description": "Point"} 15 | ] 16 | }, 17 | { 18 | "name": "XY Plane", 19 | "category": "Vector", 20 | "subcategory": "Plane", 21 | "description": "Creates an XY plane at the world origin or at a specified point", 22 | "inputs": [ 23 | {"name": "Origin", "type": "Point", "description": "Origin point", "optional": true} 24 | ], 25 | "outputs": [ 26 | {"name": "Plane", "type": "Plane", "description": "XY plane"} 27 | ] 28 | }, 29 | { 30 | "name": "Box", 31 | "category": "Surface", 32 | "subcategory": "Primitive", 33 | "description": "Creates a box from a base plane and dimensions", 34 | "inputs": [ 35 | {"name": "Base", "type": "Plane", "description": "Base plane"}, 36 | {"name": "X Size", "type": "Number", "description": "Size in X direction"}, 37 | {"name": "Y Size", "type": "Number", "description": "Size in Y direction"}, 38 | {"name": "Z Size", "type": "Number", "description": "Size in Z direction"} 39 | ], 40 | "outputs": [ 41 | {"name": "Box", "type": "Brep", "description": "Box geometry"} 42 | ] 43 | }, 44 | { 45 | "name": "Circle", 46 | "category": "Curve", 47 | "subcategory": "Primitive", 48 | "description": "Creates a circle from a plane and radius", 49 | "inputs": [ 50 | {"name": "Plane", "type": "Plane", "description": "Circle plane"}, 51 | {"name": "Radius", "type": "Number", "description": "Circle radius"} 52 | ], 53 | "outputs": [ 54 | {"name": "Circle", "type": "Curve", "description": "Circle curve"} 55 | ] 56 | }, 57 | { 58 | "name": "Number Slider", 59 | "category": "Params", 60 | "subcategory": "Input", 61 | "description": "Slider for numeric input", 62 | "inputs": [], 63 | "outputs": [ 64 | {"name": "Number", "type": "Number", "description": "Slider value"} 65 | ], 66 | "defaultSettings": { 67 | "min": 0, 68 | "max": 10, 69 | "value": 5 70 | } 71 | }, 72 | { 73 | "name": "Panel", 74 | "category": "Params", 75 | "subcategory": "Input", 76 | "description": "Text panel for input or output", 77 | "inputs": [ 78 | {"name": "Input", "type": "Any", "description": "Any input", "optional": true} 79 | ], 80 | "outputs": [ 81 | {"name": "Output", "type": "Text", "description": "Panel text"} 82 | ] 83 | }, 84 | { 85 | "name": "Voronoi", 86 | "category": "Surface", 87 | "subcategory": "Triangulation", 88 | "description": "Creates a Voronoi diagram from points", 89 | "inputs": [ 90 | {"name": "Points", "type": "Point", "description": "Input points"}, 91 | {"name": "Radius", "type": "Number", "description": "Cell radius", "optional": true}, 92 | {"name": "Plane", "type": "Plane", "description": "Base plane", "optional": true} 93 | ], 94 | "outputs": [ 95 | {"name": "Cells", "type": "Curve", "description": "Voronoi cells"}, 96 | {"name": "Vertices", "type": "Point", "description": "Voronoi vertices"} 97 | ] 98 | }, 99 | { 100 | "name": "Populate 3D", 101 | "category": "Vector", 102 | "subcategory": "Grid", 103 | "description": "Creates a 3D grid of points", 104 | "inputs": [ 105 | {"name": "Base", "type": "Plane", "description": "Base plane"}, 106 | {"name": "Size X", "type": "Number", "description": "Size in X direction"}, 107 | {"name": "Size Y", "type": "Number", "description": "Size in Y direction"}, 108 | {"name": "Size Z", "type": "Number", "description": "Size in Z direction"}, 109 | {"name": "Count X", "type": "Integer", "description": "Count in X direction"}, 110 | {"name": "Count Y", "type": "Integer", "description": "Count in Y direction"}, 111 | {"name": "Count Z", "type": "Integer", "description": "Count in Z direction"} 112 | ], 113 | "outputs": [ 114 | {"name": "Points", "type": "Point", "description": "3D grid of points"} 115 | ] 116 | }, 117 | { 118 | "name": "Boundary Surfaces", 119 | "category": "Surface", 120 | "subcategory": "Freeform", 121 | "description": "Creates boundary surfaces from curves", 122 | "inputs": [ 123 | {"name": "Curves", "type": "Curve", "description": "Input curves"} 124 | ], 125 | "outputs": [ 126 | {"name": "Surfaces", "type": "Surface", "description": "Boundary surfaces"} 127 | ] 128 | }, 129 | { 130 | "name": "Extrude", 131 | "category": "Surface", 132 | "subcategory": "Freeform", 133 | "description": "Extrudes curves or surfaces", 134 | "inputs": [ 135 | {"name": "Base", "type": "Geometry", "description": "Base geometry"}, 136 | {"name": "Direction", "type": "Vector", "description": "Extrusion direction"}, 137 | {"name": "Distance", "type": "Number", "description": "Extrusion distance"} 138 | ], 139 | "outputs": [ 140 | {"name": "Result", "type": "Brep", "description": "Extruded geometry"} 141 | ] 142 | } 143 | ], 144 | "patterns": [ 145 | { 146 | "name": "3D Box", 147 | "description": "Creates a simple 3D box", 148 | "components": [ 149 | {"type": "XY Plane", "x": 100, "y": 100, "id": "plane"}, 150 | {"type": "Number Slider", "x": 100, "y": 200, "id": "sliderX", "settings": {"min": 0, "max": 50, "value": 20}}, 151 | {"type": "Number Slider", "x": 100, "y": 250, "id": "sliderY", "settings": {"min": 0, "max": 50, "value": 20}}, 152 | {"type": "Number Slider", "x": 100, "y": 300, "id": "sliderZ", "settings": {"min": 0, "max": 50, "value": 20}}, 153 | {"type": "Box", "x": 400, "y": 200, "id": "box"} 154 | ], 155 | "connections": [ 156 | {"source": "plane", "sourceParam": "Plane", "target": "box", "targetParam": "Base"}, 157 | {"source": "sliderX", "sourceParam": "Number", "target": "box", "targetParam": "X Size"}, 158 | {"source": "sliderY", "sourceParam": "Number", "target": "box", "targetParam": "Y Size"}, 159 | {"source": "sliderZ", "sourceParam": "Number", "target": "box", "targetParam": "Z Size"} 160 | ] 161 | }, 162 | { 163 | "name": "3D Voronoi", 164 | "description": "Creates a 3D Voronoi pattern within a box", 165 | "components": [ 166 | {"type": "XY Plane", "x": 100, "y": 100, "id": "plane"}, 167 | {"type": "Number Slider", "x": 100, "y": 200, "id": "sizeX", "settings": {"min": 0, "max": 100, "value": 50}}, 168 | {"type": "Number Slider", "x": 100, "y": 250, "id": "sizeY", "settings": {"min": 0, "max": 100, "value": 50}}, 169 | {"type": "Number Slider", "x": 100, "y": 300, "id": "sizeZ", "settings": {"min": 0, "max": 100, "value": 50}}, 170 | {"type": "Number Slider", "x": 100, "y": 350, "id": "countX", "settings": {"min": 1, "max": 20, "value": 10}}, 171 | {"type": "Number Slider", "x": 100, "y": 400, "id": "countY", "settings": {"min": 1, "max": 20, "value": 10}}, 172 | {"type": "Number Slider", "x": 100, "y": 450, "id": "countZ", "settings": {"min": 1, "max": 20, "value": 10}}, 173 | {"type": "Populate 3D", "x": 400, "y": 250, "id": "populate"}, 174 | {"type": "Voronoi", "x": 600, "y": 250, "id": "voronoi"} 175 | ], 176 | "connections": [ 177 | {"source": "plane", "sourceParam": "Plane", "target": "populate", "targetParam": "Base"}, 178 | {"source": "sizeX", "sourceParam": "Number", "target": "populate", "targetParam": "Size X"}, 179 | {"source": "sizeY", "sourceParam": "Number", "target": "populate", "targetParam": "Size Y"}, 180 | {"source": "sizeZ", "sourceParam": "Number", "target": "populate", "targetParam": "Size Z"}, 181 | {"source": "countX", "sourceParam": "Number", "target": "populate", "targetParam": "Count X"}, 182 | {"source": "countY", "sourceParam": "Number", "target": "populate", "targetParam": "Count Y"}, 183 | {"source": "countZ", "sourceParam": "Number", "target": "populate", "targetParam": "Count Z"}, 184 | {"source": "populate", "sourceParam": "Points", "target": "voronoi", "targetParam": "Points"} 185 | ] 186 | }, 187 | { 188 | "name": "Circle", 189 | "description": "Creates a simple circle", 190 | "components": [ 191 | {"type": "XY Plane", "x": 100, "y": 100, "id": "plane"}, 192 | {"type": "Number Slider", "x": 100, "y": 200, "id": "radius", "settings": {"min": 0, "max": 50, "value": 10}}, 193 | {"type": "Circle", "x": 400, "y": 150, "id": "circle"} 194 | ], 195 | "connections": [ 196 | {"source": "plane", "sourceParam": "Plane", "target": "circle", "targetParam": "Plane"}, 197 | {"source": "radius", "sourceParam": "Number", "target": "circle", "targetParam": "Radius"} 198 | ] 199 | } 200 | ], 201 | "intents": [ 202 | { 203 | "keywords": ["box", "cube", "rectangular", "prism"], 204 | "pattern": "3D Box" 205 | }, 206 | { 207 | "keywords": ["voronoi", "cell", "diagram", "3d", "cellular"], 208 | "pattern": "3D Voronoi" 209 | }, 210 | { 211 | "keywords": ["circle", "round", "disc"], 212 | "pattern": "Circle" 213 | } 214 | ] 215 | } 216 | ``` -------------------------------------------------------------------------------- /GH_MCP/GH_MCP/Commands/ConnectionCommandHandler.cs: -------------------------------------------------------------------------------- ```csharp 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading; 5 | using GrasshopperMCP.Models; 6 | using GH_MCP.Models; 7 | using Grasshopper; 8 | using Grasshopper.Kernel; 9 | using Grasshopper.Kernel.Parameters; 10 | using Rhino; 11 | using Newtonsoft.Json; 12 | using GH_MCP.Utils; 13 | 14 | namespace GH_MCP.Commands 15 | { 16 | /// <summary> 17 | /// 處理組件連接相關的命令 18 | /// </summary> 19 | public class ConnectionCommandHandler 20 | { 21 | /// <summary> 22 | /// 連接兩個組件 23 | /// </summary> 24 | /// <param name="command">命令對象</param> 25 | /// <returns>命令執行結果</returns> 26 | public static object ConnectComponents(Command command) 27 | { 28 | // 獲取源組件 ID 29 | if (!command.Parameters.TryGetValue("sourceId", out object sourceIdObj) || sourceIdObj == null) 30 | { 31 | return Response.CreateError("Missing required parameter: sourceId"); 32 | } 33 | string sourceId = sourceIdObj.ToString(); 34 | 35 | // 獲取源參數名稱或索引 36 | string sourceParam = null; 37 | int? sourceParamIndex = null; 38 | if (command.Parameters.TryGetValue("sourceParam", out object sourceParamObj) && sourceParamObj != null) 39 | { 40 | sourceParam = sourceParamObj.ToString(); 41 | // 使用模糊匹配獲取標準化的參數名稱 42 | sourceParam = FuzzyMatcher.GetClosestParameterName(sourceParam); 43 | } 44 | else if (command.Parameters.TryGetValue("sourceParamIndex", out object sourceParamIndexObj) && sourceParamIndexObj != null) 45 | { 46 | if (int.TryParse(sourceParamIndexObj.ToString(), out int index)) 47 | { 48 | sourceParamIndex = index; 49 | } 50 | } 51 | 52 | // 獲取目標組件 ID 53 | if (!command.Parameters.TryGetValue("targetId", out object targetIdObj) || targetIdObj == null) 54 | { 55 | return Response.CreateError("Missing required parameter: targetId"); 56 | } 57 | string targetId = targetIdObj.ToString(); 58 | 59 | // 獲取目標參數名稱或索引 60 | string targetParam = null; 61 | int? targetParamIndex = null; 62 | if (command.Parameters.TryGetValue("targetParam", out object targetParamObj) && targetParamObj != null) 63 | { 64 | targetParam = targetParamObj.ToString(); 65 | // 使用模糊匹配獲取標準化的參數名稱 66 | targetParam = FuzzyMatcher.GetClosestParameterName(targetParam); 67 | } 68 | else if (command.Parameters.TryGetValue("targetParamIndex", out object targetParamIndexObj) && targetParamIndexObj != null) 69 | { 70 | if (int.TryParse(targetParamIndexObj.ToString(), out int index)) 71 | { 72 | targetParamIndex = index; 73 | } 74 | } 75 | 76 | // 記錄連接信息 77 | RhinoApp.WriteLine($"Connecting: sourceId={sourceId}, sourceParam={sourceParam}, targetId={targetId}, targetParam={targetParam}"); 78 | 79 | // 創建連接對象 80 | var connection = new ConnectionPairing 81 | { 82 | Source = new Connection 83 | { 84 | ComponentId = sourceId, 85 | ParameterName = sourceParam, 86 | ParameterIndex = sourceParamIndex 87 | }, 88 | Target = new Connection 89 | { 90 | ComponentId = targetId, 91 | ParameterName = targetParam, 92 | ParameterIndex = targetParamIndex 93 | } 94 | }; 95 | 96 | // 檢查連接是否有效 97 | if (!connection.IsValid()) 98 | { 99 | return Response.CreateError("Invalid connection parameters"); 100 | } 101 | 102 | // 在 UI 線程上執行連接操作 103 | object result = null; 104 | Exception exception = null; 105 | 106 | RhinoApp.InvokeOnUiThread(new Action(() => 107 | { 108 | try 109 | { 110 | // 獲取當前文檔 111 | var doc = Instances.ActiveCanvas?.Document; 112 | if (doc == null) 113 | { 114 | exception = new InvalidOperationException("No active Grasshopper document"); 115 | return; 116 | } 117 | 118 | // 查找源組件 119 | Guid sourceGuid; 120 | if (!Guid.TryParse(connection.Source.ComponentId, out sourceGuid)) 121 | { 122 | exception = new ArgumentException($"Invalid source component ID: {connection.Source.ComponentId}"); 123 | return; 124 | } 125 | 126 | var sourceComponent = doc.FindObject(sourceGuid, true); 127 | if (sourceComponent == null) 128 | { 129 | exception = new ArgumentException($"Source component not found: {connection.Source.ComponentId}"); 130 | return; 131 | } 132 | 133 | // 查找目標組件 134 | Guid targetGuid; 135 | if (!Guid.TryParse(connection.Target.ComponentId, out targetGuid)) 136 | { 137 | exception = new ArgumentException($"Invalid target component ID: {connection.Target.ComponentId}"); 138 | return; 139 | } 140 | 141 | var targetComponent = doc.FindObject(targetGuid, true); 142 | if (targetComponent == null) 143 | { 144 | exception = new ArgumentException($"Target component not found: {connection.Target.ComponentId}"); 145 | return; 146 | } 147 | 148 | // 檢查源組件是否為輸入參數組件 149 | if (sourceComponent is IGH_Param && ((IGH_Param)sourceComponent).Kind == GH_ParamKind.input) 150 | { 151 | exception = new ArgumentException("Source component cannot be an input parameter"); 152 | return; 153 | } 154 | 155 | // 檢查目標組件是否為輸出參數組件 156 | if (targetComponent is IGH_Param && ((IGH_Param)targetComponent).Kind == GH_ParamKind.output) 157 | { 158 | exception = new ArgumentException("Target component cannot be an output parameter"); 159 | return; 160 | } 161 | 162 | // 獲取源參數 163 | IGH_Param sourceParameter = GetParameter(sourceComponent, connection.Source, false); 164 | if (sourceParameter == null) 165 | { 166 | exception = new ArgumentException($"Source parameter not found: {connection.Source.ParameterName ?? connection.Source.ParameterIndex.ToString()}"); 167 | return; 168 | } 169 | 170 | // 獲取目標參數 171 | IGH_Param targetParameter = GetParameter(targetComponent, connection.Target, true); 172 | if (targetParameter == null) 173 | { 174 | exception = new ArgumentException($"Target parameter not found: {connection.Target.ParameterName ?? connection.Target.ParameterIndex.ToString()}"); 175 | return; 176 | } 177 | 178 | // 檢查參數類型相容性 179 | if (!AreParametersCompatible(sourceParameter, targetParameter)) 180 | { 181 | exception = new ArgumentException($"Parameters are not compatible: {sourceParameter.GetType().Name} cannot connect to {targetParameter.GetType().Name}"); 182 | return; 183 | } 184 | 185 | // 移除現有連接(如果需要) 186 | if (targetParameter.SourceCount > 0) 187 | { 188 | targetParameter.RemoveAllSources(); 189 | } 190 | 191 | // 連接參數 192 | targetParameter.AddSource(sourceParameter); 193 | 194 | // 刷新數據 195 | targetParameter.CollectData(); 196 | targetParameter.ComputeData(); 197 | 198 | // 刷新畫布 199 | doc.NewSolution(false); 200 | 201 | // 返回結果 202 | result = new 203 | { 204 | success = true, 205 | message = "Connection created successfully", 206 | sourceId = connection.Source.ComponentId, 207 | targetId = connection.Target.ComponentId, 208 | sourceParam = sourceParameter.Name, 209 | targetParam = targetParameter.Name, 210 | sourceType = sourceParameter.GetType().Name, 211 | targetType = targetParameter.GetType().Name, 212 | sourceDescription = sourceParameter.Description, 213 | targetDescription = targetParameter.Description 214 | }; 215 | } 216 | catch (Exception ex) 217 | { 218 | exception = ex; 219 | RhinoApp.WriteLine($"Error in ConnectComponents: {ex.Message}"); 220 | } 221 | })); 222 | 223 | // 等待 UI 線程操作完成 224 | while (result == null && exception == null) 225 | { 226 | Thread.Sleep(10); 227 | } 228 | 229 | // 如果有異常,拋出 230 | if (exception != null) 231 | { 232 | return Response.CreateError($"Error executing command 'connect_components': {exception.Message}"); 233 | } 234 | 235 | return Response.Ok(result); 236 | } 237 | 238 | /// <summary> 239 | /// 獲取組件的參數 240 | /// </summary> 241 | /// <param name="docObj">文檔對象</param> 242 | /// <param name="connection">連接信息</param> 243 | /// <param name="isInput">是否為輸入參數</param> 244 | /// <returns>參數對象</returns> 245 | private static IGH_Param GetParameter(IGH_DocumentObject docObj, Connection connection, bool isInput) 246 | { 247 | // 處理參數組件 248 | if (docObj is IGH_Param param) 249 | { 250 | return param; 251 | } 252 | 253 | // 處理一般組件 254 | if (docObj is IGH_Component component) 255 | { 256 | // 獲取參數集合 257 | IList<IGH_Param> parameters = isInput ? component.Params.Input : component.Params.Output; 258 | 259 | // 檢查參數集合是否為空 260 | if (parameters == null || parameters.Count == 0) 261 | { 262 | return null; 263 | } 264 | 265 | // 如果只有一個參數,直接返回(只有在未指定名稱或索引時) 266 | if (parameters.Count == 1 && string.IsNullOrEmpty(connection.ParameterName) && !connection.ParameterIndex.HasValue) 267 | { 268 | return parameters[0]; 269 | } 270 | 271 | // 按名稱查找參數 272 | if (!string.IsNullOrEmpty(connection.ParameterName)) 273 | { 274 | // 精確匹配 275 | foreach (var p in parameters) 276 | { 277 | if (string.Equals(p.Name, connection.ParameterName, StringComparison.OrdinalIgnoreCase)) 278 | { 279 | return p; 280 | } 281 | } 282 | 283 | // 模糊匹配 284 | foreach (var p in parameters) 285 | { 286 | if (p.Name.IndexOf(connection.ParameterName, StringComparison.OrdinalIgnoreCase) >= 0) 287 | { 288 | return p; 289 | } 290 | } 291 | 292 | // 嘗試匹配 NickName 293 | foreach (var p in parameters) 294 | { 295 | if (string.Equals(p.NickName, connection.ParameterName, StringComparison.OrdinalIgnoreCase)) 296 | { 297 | return p; 298 | } 299 | } 300 | } 301 | 302 | // 按索引查找參數 303 | if (connection.ParameterIndex.HasValue) 304 | { 305 | int index = connection.ParameterIndex.Value; 306 | if (index >= 0 && index < parameters.Count) 307 | { 308 | return parameters[index]; 309 | } 310 | } 311 | } 312 | 313 | return null; 314 | } 315 | 316 | /// <summary> 317 | /// 檢查兩個參數是否相容 318 | /// </summary> 319 | /// <param name="source">源參數</param> 320 | /// <param name="target">目標參數</param> 321 | /// <returns>是否相容</returns> 322 | private static bool AreParametersCompatible(IGH_Param source, IGH_Param target) 323 | { 324 | // 如果參數類型完全匹配,則相容 325 | if (source.GetType() == target.GetType()) 326 | { 327 | return true; 328 | } 329 | 330 | // 檢查數據類型是否兼容 331 | var sourceType = source.Type; 332 | var targetType = target.Type; 333 | 334 | // 記錄參數類型信息,用於調試 335 | RhinoApp.WriteLine($"Parameter types: source={sourceType.Name}, target={targetType.Name}"); 336 | RhinoApp.WriteLine($"Parameter names: source={source.Name}, target={target.Name}"); 337 | 338 | // 檢查數字類型的兼容性 339 | bool isSourceNumeric = IsNumericType(source); 340 | bool isTargetNumeric = IsNumericType(target); 341 | 342 | if (isSourceNumeric && isTargetNumeric) 343 | { 344 | return true; 345 | } 346 | 347 | // 曲線和幾何體之間的特殊處理 348 | bool isSourceCurve = source is Param_Curve; 349 | bool isTargetCurve = target is Param_Curve; 350 | bool isSourceGeometry = source is Param_Geometry; 351 | bool isTargetGeometry = target is Param_Geometry; 352 | 353 | if ((isSourceCurve && isTargetGeometry) || (isSourceGeometry && isTargetCurve)) 354 | { 355 | return true; 356 | } 357 | 358 | // 點和向量之間的特殊處理 359 | bool isSourcePoint = source is Param_Point; 360 | bool isTargetPoint = target is Param_Point; 361 | bool isSourceVector = source is Param_Vector; 362 | bool isTargetVector = target is Param_Vector; 363 | 364 | if ((isSourcePoint && isTargetVector) || (isSourceVector && isTargetPoint)) 365 | { 366 | return true; 367 | } 368 | 369 | // 檢查組件的 GUID,確保連接到正確的元件類型 370 | // 獲取參數所屬的組件 371 | var sourceDoc = source.OnPingDocument(); 372 | var targetDoc = target.OnPingDocument(); 373 | 374 | if (sourceDoc != null && targetDoc != null) 375 | { 376 | // 嘗試查找參數所屬的組件 377 | IGH_Component sourceComponent = FindComponentForParam(sourceDoc, source); 378 | IGH_Component targetComponent = FindComponentForParam(targetDoc, target); 379 | 380 | // 如果找到了源組件和目標組件 381 | if (sourceComponent != null && targetComponent != null) 382 | { 383 | // 記錄組件信息,用於調試 384 | RhinoApp.WriteLine($"Components: source={sourceComponent.Name}, target={targetComponent.Name}"); 385 | RhinoApp.WriteLine($"Component GUIDs: source={sourceComponent.ComponentGuid}, target={targetComponent.ComponentGuid}"); 386 | 387 | // 特殊處理平面到幾何元件的連接 388 | if (IsPlaneComponent(sourceComponent) && RequiresPlaneInput(targetComponent)) 389 | { 390 | RhinoApp.WriteLine("Connecting plane component to geometry component that requires plane input"); 391 | return true; 392 | } 393 | 394 | // 如果源是滑塊且目標是圓,確保目標是創建圓的組件 395 | if (sourceComponent.Name.Contains("Number") && targetComponent.Name.Contains("Circle")) 396 | { 397 | // 檢查目標是否為正確的圓元件 (使用 GUID 或描述) 398 | if (targetComponent.ComponentGuid.ToString() == "d1028c72-ff86-4057-9eb0-36c687a4d98c") 399 | { 400 | // 這是錯誤的圓元件 (參數容器) 401 | RhinoApp.WriteLine("Detected connection to Circle parameter container instead of Circle component"); 402 | return false; 403 | } 404 | if (targetComponent.ComponentGuid.ToString() == "807b86e3-be8d-4970-92b5-f8cdcb45b06b") 405 | { 406 | // 這是正確的圓元件 (創建圓) 407 | return true; 408 | } 409 | } 410 | 411 | // 如果源是平面且目標是立方體,允許連接 412 | if (IsPlaneComponent(sourceComponent) && targetComponent.Name.Contains("Box")) 413 | { 414 | RhinoApp.WriteLine("Connecting plane component to box component"); 415 | return true; 416 | } 417 | } 418 | } 419 | 420 | // 默認允許連接,讓 Grasshopper 在運行時決定是否相容 421 | return true; 422 | } 423 | 424 | /// <summary> 425 | /// 檢查參數是否為數字類型 426 | /// </summary> 427 | /// <param name="param">參數</param> 428 | /// <returns>是否為數字類型</returns> 429 | private static bool IsNumericType(IGH_Param param) 430 | { 431 | return param is Param_Integer || 432 | param is Param_Number || 433 | param is Param_Time; 434 | } 435 | 436 | /// <summary> 437 | /// 查找參數所屬的組件 438 | /// </summary> 439 | /// <param name="doc">文檔</param> 440 | /// <param name="param">參數</param> 441 | /// <returns>參數所屬的組件</returns> 442 | private static IGH_Component FindComponentForParam(GH_Document doc, IGH_Param param) 443 | { 444 | foreach (var obj in doc.Objects) 445 | { 446 | if (obj is IGH_Component comp) 447 | { 448 | // 檢查輸出參數 449 | foreach (var outParam in comp.Params.Output) 450 | { 451 | if (outParam.InstanceGuid == param.InstanceGuid) 452 | { 453 | return comp; 454 | } 455 | } 456 | 457 | // 檢查輸入參數 458 | foreach (var inParam in comp.Params.Input) 459 | { 460 | if (inParam.InstanceGuid == param.InstanceGuid) 461 | { 462 | return comp; 463 | } 464 | } 465 | } 466 | } 467 | 468 | return null; 469 | } 470 | 471 | /// <summary> 472 | /// 檢查組件是否為平面組件 473 | /// </summary> 474 | /// <param name="component">組件</param> 475 | /// <returns>是否為平面組件</returns> 476 | private static bool IsPlaneComponent(IGH_Component component) 477 | { 478 | if (component == null) 479 | return false; 480 | 481 | // 檢查組件名稱 482 | string name = component.Name.ToLowerInvariant(); 483 | if (name.Contains("plane")) 484 | return true; 485 | 486 | // 檢查 XY Plane 組件的 GUID 487 | if (component.ComponentGuid.ToString() == "896a1e5e-c2ac-4996-a6d8-5b61157080b3") 488 | return true; 489 | 490 | return false; 491 | } 492 | 493 | /// <summary> 494 | /// 檢查組件是否需要平面輸入 495 | /// </summary> 496 | /// <param name="component">組件</param> 497 | /// <returns>是否需要平面輸入</returns> 498 | private static bool RequiresPlaneInput(IGH_Component component) 499 | { 500 | if (component == null) 501 | return false; 502 | 503 | // 檢查組件是否有名為 "Plane" 或 "Base" 的輸入參數 504 | foreach (var param in component.Params.Input) 505 | { 506 | string paramName = param.Name.ToLowerInvariant(); 507 | if (paramName.Contains("plane") || paramName.Contains("base")) 508 | return true; 509 | } 510 | 511 | // 檢查特定類型的組件 512 | string name = component.Name.ToLowerInvariant(); 513 | return name.Contains("box") || 514 | name.Contains("rectangle") || 515 | name.Contains("circle") || 516 | name.Contains("cylinder") || 517 | name.Contains("cone"); 518 | } 519 | } 520 | 521 | public class ConnectionPairing 522 | { 523 | public Connection Source { get; set; } 524 | public Connection Target { get; set; } 525 | 526 | public bool IsValid() 527 | { 528 | return Source != null && Target != null; 529 | } 530 | } 531 | 532 | public class Connection 533 | { 534 | public string ComponentId { get; set; } 535 | public string ParameterName { get; set; } 536 | public int? ParameterIndex { get; set; } 537 | } 538 | } 539 | ``` -------------------------------------------------------------------------------- /GH_MCP/GH_MCP/Commands/ComponentCommandHandler.cs: -------------------------------------------------------------------------------- ```csharp 1 | using System; 2 | using System.Collections.Generic; 3 | using GrasshopperMCP.Models; 4 | using Grasshopper.Kernel; 5 | using Grasshopper.Kernel.Parameters; 6 | using Grasshopper.Kernel.Special; 7 | using Rhino; 8 | using Rhino.Geometry; 9 | using Grasshopper; 10 | using System.Linq; 11 | using Grasshopper.Kernel.Components; 12 | using System.Threading; 13 | using GH_MCP.Utils; 14 | 15 | namespace GrasshopperMCP.Commands 16 | { 17 | /// <summary> 18 | /// 處理組件相關命令的處理器 19 | /// </summary> 20 | public static class ComponentCommandHandler 21 | { 22 | /// <summary> 23 | /// 添加組件 24 | /// </summary> 25 | /// <param name="command">包含組件類型和位置的命令</param> 26 | /// <returns>添加的組件信息</returns> 27 | public static object AddComponent(Command command) 28 | { 29 | string type = command.GetParameter<string>("type"); 30 | double x = command.GetParameter<double>("x"); 31 | double y = command.GetParameter<double>("y"); 32 | 33 | if (string.IsNullOrEmpty(type)) 34 | { 35 | throw new ArgumentException("Component type is required"); 36 | } 37 | 38 | // 使用模糊匹配獲取標準化的元件名稱 39 | string normalizedType = FuzzyMatcher.GetClosestComponentName(type); 40 | 41 | // 記錄請求信息 42 | RhinoApp.WriteLine($"AddComponent request: type={type}, normalized={normalizedType}, x={x}, y={y}"); 43 | 44 | object result = null; 45 | Exception exception = null; 46 | 47 | // 在 UI 線程上執行 48 | RhinoApp.InvokeOnUiThread(new Action(() => 49 | { 50 | try 51 | { 52 | // 獲取 Grasshopper 文檔 53 | var doc = Grasshopper.Instances.ActiveCanvas?.Document; 54 | if (doc == null) 55 | { 56 | throw new InvalidOperationException("No active Grasshopper document"); 57 | } 58 | 59 | // 創建組件 60 | IGH_DocumentObject component = null; 61 | 62 | // 記錄可用的組件類型(僅在第一次調用時記錄) 63 | bool loggedComponentTypes = false; 64 | if (!loggedComponentTypes) 65 | { 66 | var availableTypes = Grasshopper.Instances.ComponentServer.ObjectProxies 67 | .Select(p => p.Desc.Name) 68 | .OrderBy(n => n) 69 | .ToList(); 70 | 71 | RhinoApp.WriteLine($"Available component types: {string.Join(", ", availableTypes.Take(50))}..."); 72 | loggedComponentTypes = true; 73 | } 74 | 75 | // 根據類型創建不同的組件 76 | switch (normalizedType.ToLowerInvariant()) 77 | { 78 | // 平面元件 79 | case "xy plane": 80 | component = CreateComponentByName("XY Plane"); 81 | break; 82 | case "xz plane": 83 | component = CreateComponentByName("XZ Plane"); 84 | break; 85 | case "yz plane": 86 | component = CreateComponentByName("YZ Plane"); 87 | break; 88 | case "plane 3pt": 89 | component = CreateComponentByName("Plane 3Pt"); 90 | break; 91 | 92 | // 基本幾何元件 93 | case "box": 94 | component = CreateComponentByName("Box"); 95 | break; 96 | case "sphere": 97 | component = CreateComponentByName("Sphere"); 98 | break; 99 | case "cylinder": 100 | component = CreateComponentByName("Cylinder"); 101 | break; 102 | case "cone": 103 | component = CreateComponentByName("Cone"); 104 | break; 105 | case "circle": 106 | component = CreateComponentByName("Circle"); 107 | break; 108 | case "rectangle": 109 | component = CreateComponentByName("Rectangle"); 110 | break; 111 | case "line": 112 | component = CreateComponentByName("Line"); 113 | break; 114 | 115 | // 參數元件 116 | case "point": 117 | case "pt": 118 | case "pointparam": 119 | case "param_point": 120 | component = new Param_Point(); 121 | break; 122 | case "curve": 123 | case "crv": 124 | case "curveparam": 125 | case "param_curve": 126 | component = new Param_Curve(); 127 | break; 128 | case "circleparam": 129 | case "param_circle": 130 | component = new Param_Circle(); 131 | break; 132 | case "lineparam": 133 | case "param_line": 134 | component = new Param_Line(); 135 | break; 136 | case "panel": 137 | case "gh_panel": 138 | component = new GH_Panel(); 139 | break; 140 | case "slider": 141 | case "numberslider": 142 | case "gh_numberslider": 143 | var slider = new GH_NumberSlider(); 144 | slider.SetInitCode("0.0 < 0.5 < 1.0"); 145 | component = slider; 146 | break; 147 | case "number": 148 | case "num": 149 | case "integer": 150 | case "int": 151 | case "param_number": 152 | case "param_integer": 153 | component = new Param_Number(); 154 | break; 155 | case "construct point": 156 | case "constructpoint": 157 | case "pt xyz": 158 | case "xyz": 159 | // 嘗試查找構造點組件 160 | var pointProxy = Grasshopper.Instances.ComponentServer.ObjectProxies 161 | .FirstOrDefault(p => p.Desc.Name.Equals("Construct Point", StringComparison.OrdinalIgnoreCase)); 162 | if (pointProxy != null) 163 | { 164 | component = pointProxy.CreateInstance(); 165 | } 166 | else 167 | { 168 | throw new ArgumentException("Construct Point component not found"); 169 | } 170 | break; 171 | default: 172 | // 嘗試通過 Guid 查找組件 173 | Guid componentGuid; 174 | if (Guid.TryParse(type, out componentGuid)) 175 | { 176 | component = Grasshopper.Instances.ComponentServer.EmitObject(componentGuid); 177 | RhinoApp.WriteLine($"Attempting to create component by GUID: {componentGuid}"); 178 | } 179 | 180 | if (component == null) 181 | { 182 | // 嘗試通過名稱查找組件(不區分大小寫) 183 | RhinoApp.WriteLine($"Attempting to find component by name: {type}"); 184 | var obj = Grasshopper.Instances.ComponentServer.ObjectProxies 185 | .FirstOrDefault(p => p.Desc.Name.Equals(type, StringComparison.OrdinalIgnoreCase)); 186 | 187 | if (obj != null) 188 | { 189 | RhinoApp.WriteLine($"Found component: {obj.Desc.Name}"); 190 | component = obj.CreateInstance(); 191 | } 192 | else 193 | { 194 | // 嘗試通過部分名稱匹配 195 | RhinoApp.WriteLine($"Attempting to find component by partial name match: {type}"); 196 | obj = Grasshopper.Instances.ComponentServer.ObjectProxies 197 | .FirstOrDefault(p => p.Desc.Name.IndexOf(type, StringComparison.OrdinalIgnoreCase) >= 0); 198 | 199 | if (obj != null) 200 | { 201 | RhinoApp.WriteLine($"Found component by partial match: {obj.Desc.Name}"); 202 | component = obj.CreateInstance(); 203 | } 204 | } 205 | } 206 | 207 | if (component == null) 208 | { 209 | // 記錄一些可能的組件類型 210 | var possibleMatches = Grasshopper.Instances.ComponentServer.ObjectProxies 211 | .Where(p => p.Desc.Name.IndexOf(type, StringComparison.OrdinalIgnoreCase) >= 0) 212 | .Select(p => p.Desc.Name) 213 | .Take(10) 214 | .ToList(); 215 | 216 | var errorMessage = $"Unknown component type: {type}"; 217 | if (possibleMatches.Any()) 218 | { 219 | errorMessage += $". Possible matches: {string.Join(", ", possibleMatches)}"; 220 | } 221 | 222 | throw new ArgumentException(errorMessage); 223 | } 224 | break; 225 | } 226 | 227 | // 設置組件位置 228 | if (component != null) 229 | { 230 | // 確保組件有有效的屬性對象 231 | if (component.Attributes == null) 232 | { 233 | RhinoApp.WriteLine("Component attributes are null, creating new attributes"); 234 | component.CreateAttributes(); 235 | } 236 | 237 | // 設置位置 238 | component.Attributes.Pivot = new System.Drawing.PointF((float)x, (float)y); 239 | 240 | // 添加到文檔 241 | doc.AddObject(component, false); 242 | 243 | // 刷新畫布 244 | doc.NewSolution(false); 245 | 246 | // 返回組件信息 247 | result = new 248 | { 249 | id = component.InstanceGuid.ToString(), 250 | type = component.GetType().Name, 251 | name = component.NickName, 252 | x = component.Attributes.Pivot.X, 253 | y = component.Attributes.Pivot.Y 254 | }; 255 | } 256 | else 257 | { 258 | throw new InvalidOperationException("Failed to create component"); 259 | } 260 | } 261 | catch (Exception ex) 262 | { 263 | exception = ex; 264 | RhinoApp.WriteLine($"Error in AddComponent: {ex.Message}"); 265 | } 266 | })); 267 | 268 | // 等待 UI 線程操作完成 269 | while (result == null && exception == null) 270 | { 271 | Thread.Sleep(10); 272 | } 273 | 274 | // 如果有異常,拋出 275 | if (exception != null) 276 | { 277 | throw exception; 278 | } 279 | 280 | return result; 281 | } 282 | 283 | /// <summary> 284 | /// 連接組件 285 | /// </summary> 286 | /// <param name="command">包含源和目標組件信息的命令</param> 287 | /// <returns>連接信息</returns> 288 | public static object ConnectComponents(Command command) 289 | { 290 | var fromData = command.GetParameter<Dictionary<string, object>>("from"); 291 | var toData = command.GetParameter<Dictionary<string, object>>("to"); 292 | 293 | if (fromData == null || toData == null) 294 | { 295 | throw new ArgumentException("Source and target component information are required"); 296 | } 297 | 298 | object result = null; 299 | Exception exception = null; 300 | 301 | // 在 UI 線程上執行 302 | RhinoApp.InvokeOnUiThread(new Action(() => 303 | { 304 | try 305 | { 306 | // 獲取 Grasshopper 文檔 307 | var doc = Grasshopper.Instances.ActiveCanvas?.Document; 308 | if (doc == null) 309 | { 310 | throw new InvalidOperationException("No active Grasshopper document"); 311 | } 312 | 313 | // 解析源組件信息 314 | string fromIdStr = fromData["id"].ToString(); 315 | string fromParamName = fromData["parameterName"].ToString(); 316 | 317 | // 解析目標組件信息 318 | string toIdStr = toData["id"].ToString(); 319 | string toParamName = toData["parameterName"].ToString(); 320 | 321 | // 將字符串 ID 轉換為 Guid 322 | Guid fromId, toId; 323 | if (!Guid.TryParse(fromIdStr, out fromId) || !Guid.TryParse(toIdStr, out toId)) 324 | { 325 | throw new ArgumentException("Invalid component ID format"); 326 | } 327 | 328 | // 查找源和目標組件 329 | IGH_Component fromComponent = doc.FindComponent(fromId) as IGH_Component; 330 | IGH_Component toComponent = doc.FindComponent(toId) as IGH_Component; 331 | 332 | if (fromComponent == null || toComponent == null) 333 | { 334 | throw new ArgumentException("Source or target component not found"); 335 | } 336 | 337 | // 查找源輸出參數 338 | IGH_Param fromParam = null; 339 | foreach (var param in fromComponent.Params.Output) 340 | { 341 | if (param.Name.Equals(fromParamName, StringComparison.OrdinalIgnoreCase)) 342 | { 343 | fromParam = param; 344 | break; 345 | } 346 | } 347 | 348 | // 查找目標輸入參數 349 | IGH_Param toParam = null; 350 | foreach (var param in toComponent.Params.Input) 351 | { 352 | if (param.Name.Equals(toParamName, StringComparison.OrdinalIgnoreCase)) 353 | { 354 | toParam = param; 355 | break; 356 | } 357 | } 358 | 359 | if (fromParam == null || toParam == null) 360 | { 361 | throw new ArgumentException("Source or target parameter not found"); 362 | } 363 | 364 | // 連接參數 365 | toParam.AddSource(fromParam); 366 | 367 | // 刷新畫布 368 | doc.NewSolution(false); 369 | 370 | // 返回連接信息 371 | result = new 372 | { 373 | from = new 374 | { 375 | id = fromComponent.InstanceGuid.ToString(), 376 | name = fromComponent.NickName, 377 | parameter = fromParam.Name 378 | }, 379 | to = new 380 | { 381 | id = toComponent.InstanceGuid.ToString(), 382 | name = toComponent.NickName, 383 | parameter = toParam.Name 384 | } 385 | }; 386 | } 387 | catch (Exception ex) 388 | { 389 | exception = ex; 390 | RhinoApp.WriteLine($"Error in ConnectComponents: {ex.Message}"); 391 | } 392 | })); 393 | 394 | // 等待 UI 線程操作完成 395 | while (result == null && exception == null) 396 | { 397 | Thread.Sleep(10); 398 | } 399 | 400 | // 如果有異常,拋出 401 | if (exception != null) 402 | { 403 | throw exception; 404 | } 405 | 406 | return result; 407 | } 408 | 409 | /// <summary> 410 | /// 設置組件值 411 | /// </summary> 412 | /// <param name="command">包含組件 ID 和值的命令</param> 413 | /// <returns>操作結果</returns> 414 | public static object SetComponentValue(Command command) 415 | { 416 | string idStr = command.GetParameter<string>("id"); 417 | string value = command.GetParameter<string>("value"); 418 | 419 | if (string.IsNullOrEmpty(idStr)) 420 | { 421 | throw new ArgumentException("Component ID is required"); 422 | } 423 | 424 | object result = null; 425 | Exception exception = null; 426 | 427 | // 在 UI 線程上執行 428 | RhinoApp.InvokeOnUiThread(new Action(() => 429 | { 430 | try 431 | { 432 | // 獲取 Grasshopper 文檔 433 | var doc = Grasshopper.Instances.ActiveCanvas?.Document; 434 | if (doc == null) 435 | { 436 | throw new InvalidOperationException("No active Grasshopper document"); 437 | } 438 | 439 | // 將字符串 ID 轉換為 Guid 440 | Guid id; 441 | if (!Guid.TryParse(idStr, out id)) 442 | { 443 | throw new ArgumentException("Invalid component ID format"); 444 | } 445 | 446 | // 查找組件 447 | IGH_DocumentObject component = doc.FindObject(id, true); 448 | if (component == null) 449 | { 450 | throw new ArgumentException($"Component with ID {idStr} not found"); 451 | } 452 | 453 | // 根據組件類型設置值 454 | if (component is GH_Panel panel) 455 | { 456 | panel.UserText = value; 457 | } 458 | else if (component is GH_NumberSlider slider) 459 | { 460 | double doubleValue; 461 | if (double.TryParse(value, out doubleValue)) 462 | { 463 | slider.SetSliderValue((decimal)doubleValue); 464 | } 465 | else 466 | { 467 | throw new ArgumentException("Invalid slider value format"); 468 | } 469 | } 470 | else if (component is IGH_Component ghComponent) 471 | { 472 | // 嘗試設置第一個輸入參數的值 473 | if (ghComponent.Params.Input.Count > 0) 474 | { 475 | var param = ghComponent.Params.Input[0]; 476 | if (param is Param_String stringParam) 477 | { 478 | stringParam.PersistentData.Clear(); 479 | stringParam.PersistentData.Append(new Grasshopper.Kernel.Types.GH_String(value)); 480 | } 481 | else if (param is Param_Number numberParam) 482 | { 483 | double doubleValue; 484 | if (double.TryParse(value, out doubleValue)) 485 | { 486 | numberParam.PersistentData.Clear(); 487 | numberParam.PersistentData.Append(new Grasshopper.Kernel.Types.GH_Number(doubleValue)); 488 | } 489 | else 490 | { 491 | throw new ArgumentException("Invalid number value format"); 492 | } 493 | } 494 | else 495 | { 496 | throw new ArgumentException($"Cannot set value for parameter type {param.GetType().Name}"); 497 | } 498 | } 499 | else 500 | { 501 | throw new ArgumentException("Component has no input parameters"); 502 | } 503 | } 504 | else 505 | { 506 | throw new ArgumentException($"Cannot set value for component type {component.GetType().Name}"); 507 | } 508 | 509 | // 刷新畫布 510 | doc.NewSolution(false); 511 | 512 | // 返回操作結果 513 | result = new 514 | { 515 | id = component.InstanceGuid.ToString(), 516 | type = component.GetType().Name, 517 | value = value 518 | }; 519 | } 520 | catch (Exception ex) 521 | { 522 | exception = ex; 523 | RhinoApp.WriteLine($"Error in SetComponentValue: {ex.Message}"); 524 | } 525 | })); 526 | 527 | // 等待 UI 線程操作完成 528 | while (result == null && exception == null) 529 | { 530 | Thread.Sleep(10); 531 | } 532 | 533 | // 如果有異常,拋出 534 | if (exception != null) 535 | { 536 | throw exception; 537 | } 538 | 539 | return result; 540 | } 541 | 542 | /// <summary> 543 | /// 獲取組件信息 544 | /// </summary> 545 | /// <param name="command">包含組件 ID 的命令</param> 546 | /// <returns>組件信息</returns> 547 | public static object GetComponentInfo(Command command) 548 | { 549 | string idStr = command.GetParameter<string>("id"); 550 | 551 | if (string.IsNullOrEmpty(idStr)) 552 | { 553 | throw new ArgumentException("Component ID is required"); 554 | } 555 | 556 | object result = null; 557 | Exception exception = null; 558 | 559 | // 在 UI 線程上執行 560 | RhinoApp.InvokeOnUiThread(new Action(() => 561 | { 562 | try 563 | { 564 | // 獲取 Grasshopper 文檔 565 | var doc = Grasshopper.Instances.ActiveCanvas?.Document; 566 | if (doc == null) 567 | { 568 | throw new InvalidOperationException("No active Grasshopper document"); 569 | } 570 | 571 | // 將字符串 ID 轉換為 Guid 572 | Guid id; 573 | if (!Guid.TryParse(idStr, out id)) 574 | { 575 | throw new ArgumentException("Invalid component ID format"); 576 | } 577 | 578 | // 查找組件 579 | IGH_DocumentObject component = doc.FindObject(id, true); 580 | if (component == null) 581 | { 582 | throw new ArgumentException($"Component with ID {idStr} not found"); 583 | } 584 | 585 | // 收集組件信息 586 | var componentInfo = new Dictionary<string, object> 587 | { 588 | { "id", component.InstanceGuid.ToString() }, 589 | { "type", component.GetType().Name }, 590 | { "name", component.NickName }, 591 | { "description", component.Description } 592 | }; 593 | 594 | // 如果是 IGH_Component,收集輸入和輸出參數信息 595 | if (component is IGH_Component ghComponent) 596 | { 597 | var inputs = new List<Dictionary<string, object>>(); 598 | foreach (var param in ghComponent.Params.Input) 599 | { 600 | inputs.Add(new Dictionary<string, object> 601 | { 602 | { "name", param.Name }, 603 | { "nickname", param.NickName }, 604 | { "description", param.Description }, 605 | { "type", param.GetType().Name }, 606 | { "dataType", param.TypeName } 607 | }); 608 | } 609 | componentInfo["inputs"] = inputs; 610 | 611 | var outputs = new List<Dictionary<string, object>>(); 612 | foreach (var param in ghComponent.Params.Output) 613 | { 614 | outputs.Add(new Dictionary<string, object> 615 | { 616 | { "name", param.Name }, 617 | { "nickname", param.NickName }, 618 | { "description", param.Description }, 619 | { "type", param.GetType().Name }, 620 | { "dataType", param.TypeName } 621 | }); 622 | } 623 | componentInfo["outputs"] = outputs; 624 | } 625 | 626 | // 如果是 GH_Panel,獲取其文本值 627 | if (component is GH_Panel panel) 628 | { 629 | componentInfo["value"] = panel.UserText; 630 | } 631 | 632 | // 如果是 GH_NumberSlider,獲取其值和範圍 633 | if (component is GH_NumberSlider slider) 634 | { 635 | componentInfo["value"] = (double)slider.CurrentValue; 636 | componentInfo["minimum"] = (double)slider.Slider.Minimum; 637 | componentInfo["maximum"] = (double)slider.Slider.Maximum; 638 | } 639 | 640 | result = componentInfo; 641 | } 642 | catch (Exception ex) 643 | { 644 | exception = ex; 645 | RhinoApp.WriteLine($"Error in GetComponentInfo: {ex.Message}"); 646 | } 647 | })); 648 | 649 | // 等待 UI 線程操作完成 650 | while (result == null && exception == null) 651 | { 652 | Thread.Sleep(10); 653 | } 654 | 655 | // 如果有異常,拋出 656 | if (exception != null) 657 | { 658 | throw exception; 659 | } 660 | 661 | return result; 662 | } 663 | 664 | private static IGH_DocumentObject CreateComponentByName(string name) 665 | { 666 | var obj = Grasshopper.Instances.ComponentServer.ObjectProxies 667 | .FirstOrDefault(p => p.Desc.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); 668 | 669 | if (obj != null) 670 | { 671 | return obj.CreateInstance(); 672 | } 673 | else 674 | { 675 | throw new ArgumentException($"Component with name {name} not found"); 676 | } 677 | } 678 | } 679 | } 680 | ``` -------------------------------------------------------------------------------- /grasshopper_mcp/bridge.py: -------------------------------------------------------------------------------- ```python 1 | import socket 2 | import json 3 | import os 4 | import sys 5 | import traceback 6 | from typing import Dict, Any, Optional, List 7 | 8 | # 使用 MCP 服務器 9 | from mcp.server.fastmcp import FastMCP 10 | 11 | # 設置 Grasshopper MCP 連接參數 12 | GRASSHOPPER_HOST = "localhost" 13 | GRASSHOPPER_PORT = 8080 # 默認端口,可以根據需要修改 14 | 15 | # 創建 MCP 服務器 16 | server = FastMCP("Grasshopper Bridge") 17 | 18 | def send_to_grasshopper(command_type: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: 19 | """向 Grasshopper MCP 發送命令""" 20 | if params is None: 21 | params = {} 22 | 23 | # 創建命令 24 | command = { 25 | "type": command_type, 26 | "parameters": params 27 | } 28 | 29 | try: 30 | print(f"Sending command to Grasshopper: {command_type} with params: {params}", file=sys.stderr) 31 | 32 | # 連接到 Grasshopper MCP 33 | client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 34 | client.connect((GRASSHOPPER_HOST, GRASSHOPPER_PORT)) 35 | 36 | # 發送命令 37 | command_json = json.dumps(command) 38 | client.sendall((command_json + "\n").encode("utf-8")) 39 | print(f"Command sent: {command_json}", file=sys.stderr) 40 | 41 | # 接收響應 42 | response_data = b"" 43 | while True: 44 | chunk = client.recv(4096) 45 | if not chunk: 46 | break 47 | response_data += chunk 48 | if response_data.endswith(b"\n"): 49 | break 50 | 51 | # 處理可能的 BOM 52 | response_str = response_data.decode("utf-8-sig").strip() 53 | print(f"Response received: {response_str}", file=sys.stderr) 54 | 55 | # 解析 JSON 響應 56 | response = json.loads(response_str) 57 | client.close() 58 | return response 59 | except Exception as e: 60 | print(f"Error communicating with Grasshopper: {str(e)}", file=sys.stderr) 61 | traceback.print_exc(file=sys.stderr) 62 | return { 63 | "success": False, 64 | "error": f"Error communicating with Grasshopper: {str(e)}" 65 | } 66 | 67 | # 註冊 MCP 工具 68 | @server.tool("add_component") 69 | def add_component(component_type: str, x: float, y: float): 70 | """ 71 | Add a component to the Grasshopper canvas 72 | 73 | Args: 74 | component_type: Component type (point, curve, circle, line, panel, slider) 75 | x: X coordinate on the canvas 76 | y: Y coordinate on the canvas 77 | 78 | Returns: 79 | Result of adding the component 80 | """ 81 | # 處理常見的組件名稱混淆問題 82 | component_mapping = { 83 | # Number Slider 的各種可能輸入方式 84 | "number slider": "Number Slider", 85 | "numeric slider": "Number Slider", 86 | "num slider": "Number Slider", 87 | "slider": "Number Slider", # 當只提到 slider 且上下文是數值時,預設為 Number Slider 88 | 89 | # 其他組件的標準化名稱 90 | "md slider": "MD Slider", 91 | "multidimensional slider": "MD Slider", 92 | "multi-dimensional slider": "MD Slider", 93 | "graph mapper": "Graph Mapper", 94 | 95 | # 數學運算組件 96 | "add": "Addition", 97 | "addition": "Addition", 98 | "plus": "Addition", 99 | "sum": "Addition", 100 | "subtract": "Subtraction", 101 | "subtraction": "Subtraction", 102 | "minus": "Subtraction", 103 | "difference": "Subtraction", 104 | "multiply": "Multiplication", 105 | "multiplication": "Multiplication", 106 | "times": "Multiplication", 107 | "product": "Multiplication", 108 | "divide": "Division", 109 | "division": "Division", 110 | 111 | # 輸出組件 112 | "panel": "Panel", 113 | "text panel": "Panel", 114 | "output panel": "Panel", 115 | "display": "Panel" 116 | } 117 | 118 | # 檢查並修正組件類型 119 | normalized_type = component_type.lower() 120 | if normalized_type in component_mapping: 121 | component_type = component_mapping[normalized_type] 122 | print(f"Component type normalized from '{normalized_type}' to '{component_mapping[normalized_type]}'", file=sys.stderr) 123 | 124 | params = { 125 | "type": component_type, 126 | "x": x, 127 | "y": y 128 | } 129 | 130 | return send_to_grasshopper("add_component", params) 131 | 132 | @server.tool("clear_document") 133 | def clear_document(): 134 | """Clear the Grasshopper document""" 135 | return send_to_grasshopper("clear_document") 136 | 137 | @server.tool("save_document") 138 | def save_document(path: str): 139 | """ 140 | Save the Grasshopper document 141 | 142 | Args: 143 | path: Save path 144 | 145 | Returns: 146 | Result of the save operation 147 | """ 148 | params = { 149 | "path": path 150 | } 151 | 152 | return send_to_grasshopper("save_document", params) 153 | 154 | @server.tool("load_document") 155 | def load_document(path: str): 156 | """ 157 | Load a Grasshopper document 158 | 159 | Args: 160 | path: Document path 161 | 162 | Returns: 163 | Result of the load operation 164 | """ 165 | params = { 166 | "path": path 167 | } 168 | 169 | return send_to_grasshopper("load_document", params) 170 | 171 | @server.tool("get_document_info") 172 | def get_document_info(): 173 | """Get information about the Grasshopper document""" 174 | return send_to_grasshopper("get_document_info") 175 | 176 | @server.tool("connect_components") 177 | 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): 178 | """ 179 | Connect two components in the Grasshopper canvas 180 | 181 | Args: 182 | source_id: ID of the source component (output) 183 | target_id: ID of the target component (input) 184 | source_param: Name of the source parameter (optional) 185 | target_param: Name of the target parameter (optional) 186 | source_param_index: Index of the source parameter (optional, used if source_param is not provided) 187 | target_param_index: Index of the target parameter (optional, used if target_param is not provided) 188 | 189 | Returns: 190 | Result of connecting the components 191 | """ 192 | # 獲取目標組件的信息,檢查是否已有連接 193 | target_info = send_to_grasshopper("get_component_info", {"componentId": target_id}) 194 | 195 | # 檢查組件類型,如果是需要多個輸入的組件(如 Addition, Subtraction 等),智能分配輸入 196 | if target_info and "result" in target_info and "type" in target_info["result"]: 197 | component_type = target_info["result"]["type"] 198 | 199 | # 獲取現有連接 200 | connections = send_to_grasshopper("get_connections") 201 | existing_connections = [] 202 | 203 | if connections and "result" in connections: 204 | for conn in connections["result"]: 205 | if conn.get("targetId") == target_id: 206 | existing_connections.append(conn) 207 | 208 | # 對於特定需要多個輸入的組件,自動選擇正確的輸入端口 209 | if component_type in ["Addition", "Subtraction", "Multiplication", "Division", "Math"]: 210 | # 如果沒有指定目標參數,且已有連接到第一個輸入,則自動連接到第二個輸入 211 | if target_param is None and target_param_index is None: 212 | # 檢查第一個輸入是否已被佔用 213 | first_input_occupied = False 214 | for conn in existing_connections: 215 | if conn.get("targetParam") == "A" or conn.get("targetParamIndex") == 0: 216 | first_input_occupied = True 217 | break 218 | 219 | # 如果第一個輸入已被佔用,則連接到第二個輸入 220 | if first_input_occupied: 221 | target_param = "B" # 第二個輸入通常命名為 B 222 | else: 223 | target_param = "A" # 否則連接到第一個輸入 224 | 225 | params = { 226 | "sourceId": source_id, 227 | "targetId": target_id 228 | } 229 | 230 | if source_param is not None: 231 | params["sourceParam"] = source_param 232 | elif source_param_index is not None: 233 | params["sourceParamIndex"] = source_param_index 234 | 235 | if target_param is not None: 236 | params["targetParam"] = target_param 237 | elif target_param_index is not None: 238 | params["targetParamIndex"] = target_param_index 239 | 240 | return send_to_grasshopper("connect_components", params) 241 | 242 | @server.tool("create_pattern") 243 | def create_pattern(description: str): 244 | """ 245 | Create a pattern of components based on a high-level description 246 | 247 | Args: 248 | description: High-level description of what to create (e.g., '3D voronoi cube') 249 | 250 | Returns: 251 | Result of creating the pattern 252 | """ 253 | params = { 254 | "description": description 255 | } 256 | 257 | return send_to_grasshopper("create_pattern", params) 258 | 259 | @server.tool("get_available_patterns") 260 | def get_available_patterns(query: str): 261 | """ 262 | Get a list of available patterns that match a query 263 | 264 | Args: 265 | query: Query to search for patterns 266 | 267 | Returns: 268 | List of available patterns 269 | """ 270 | params = { 271 | "query": query 272 | } 273 | 274 | return send_to_grasshopper("get_available_patterns", params) 275 | 276 | @server.tool("get_component_info") 277 | def get_component_info(component_id: str): 278 | """ 279 | Get detailed information about a specific component 280 | 281 | Args: 282 | component_id: ID of the component to get information about 283 | 284 | Returns: 285 | Detailed information about the component, including inputs, outputs, and current values 286 | """ 287 | params = { 288 | "componentId": component_id 289 | } 290 | 291 | result = send_to_grasshopper("get_component_info", params) 292 | 293 | # 增強返回結果,添加更多參數信息 294 | if result and "result" in result: 295 | component_data = result["result"] 296 | 297 | # 獲取組件類型 298 | if "type" in component_data: 299 | component_type = component_data["type"] 300 | 301 | # 查詢組件庫,獲取該類型組件的詳細參數信息 302 | component_library = get_component_library() 303 | if "components" in component_library: 304 | for lib_component in component_library["components"]: 305 | if lib_component.get("name") == component_type or lib_component.get("fullName") == component_type: 306 | # 將組件庫中的參數信息合併到返回結果中 307 | if "settings" in lib_component: 308 | component_data["availableSettings"] = lib_component["settings"] 309 | if "inputs" in lib_component: 310 | component_data["inputDetails"] = lib_component["inputs"] 311 | if "outputs" in lib_component: 312 | component_data["outputDetails"] = lib_component["outputs"] 313 | if "usage_examples" in lib_component: 314 | component_data["usageExamples"] = lib_component["usage_examples"] 315 | if "common_issues" in lib_component: 316 | component_data["commonIssues"] = lib_component["common_issues"] 317 | break 318 | 319 | # 特殊處理某些組件類型 320 | if component_type == "Number Slider": 321 | # 嘗試從組件數據中獲取當前滑桿的實際設置 322 | if "currentSettings" not in component_data: 323 | component_data["currentSettings"] = { 324 | "min": component_data.get("min", 0), 325 | "max": component_data.get("max", 10), 326 | "value": component_data.get("value", 5), 327 | "rounding": component_data.get("rounding", 0.1), 328 | "type": component_data.get("type", "float") 329 | } 330 | 331 | # 添加組件的連接信息 332 | connections = send_to_grasshopper("get_connections") 333 | if connections and "result" in connections: 334 | # 查找與該組件相關的所有連接 335 | related_connections = [] 336 | for conn in connections["result"]: 337 | if conn.get("sourceId") == component_id or conn.get("targetId") == component_id: 338 | related_connections.append(conn) 339 | 340 | if related_connections: 341 | component_data["connections"] = related_connections 342 | 343 | return result 344 | 345 | @server.tool("get_all_components") 346 | def get_all_components(): 347 | """ 348 | Get a list of all components in the current document 349 | 350 | Returns: 351 | List of all components in the document with their IDs, types, and positions 352 | """ 353 | result = send_to_grasshopper("get_all_components") 354 | 355 | # 增強返回結果,為每個組件添加更多參數信息 356 | if result and "result" in result: 357 | components = result["result"] 358 | component_library = get_component_library() 359 | 360 | # 獲取所有連接信息 361 | connections = send_to_grasshopper("get_connections") 362 | connections_data = connections.get("result", []) if connections else [] 363 | 364 | # 為每個組件添加詳細信息 365 | for component in components: 366 | if "id" in component and "type" in component: 367 | component_id = component["id"] 368 | component_type = component["type"] 369 | 370 | # 添加組件的詳細參數信息 371 | if "components" in component_library: 372 | for lib_component in component_library["components"]: 373 | if lib_component.get("name") == component_type or lib_component.get("fullName") == component_type: 374 | # 將組件庫中的參數信息合併到組件數據中 375 | if "settings" in lib_component: 376 | component["availableSettings"] = lib_component["settings"] 377 | if "inputs" in lib_component: 378 | component["inputDetails"] = lib_component["inputs"] 379 | if "outputs" in lib_component: 380 | component["outputDetails"] = lib_component["outputs"] 381 | break 382 | 383 | # 添加組件的連接信息 384 | related_connections = [] 385 | for conn in connections_data: 386 | if conn.get("sourceId") == component_id or conn.get("targetId") == component_id: 387 | related_connections.append(conn) 388 | 389 | if related_connections: 390 | component["connections"] = related_connections 391 | 392 | # 特殊處理某些組件類型 393 | if component_type == "Number Slider": 394 | # 嘗試獲取滑桿的當前設置 395 | component_info = send_to_grasshopper("get_component_info", {"componentId": component_id}) 396 | if component_info and "result" in component_info: 397 | info_data = component_info["result"] 398 | component["currentSettings"] = { 399 | "min": info_data.get("min", 0), 400 | "max": info_data.get("max", 10), 401 | "value": info_data.get("value", 5), 402 | "rounding": info_data.get("rounding", 0.1) 403 | } 404 | 405 | return result 406 | 407 | @server.tool("get_connections") 408 | def get_connections(): 409 | """ 410 | Get a list of all connections between components in the current document 411 | 412 | Returns: 413 | List of all connections between components 414 | """ 415 | return send_to_grasshopper("get_connections") 416 | 417 | @server.tool("search_components") 418 | def search_components(query: str): 419 | """ 420 | Search for components by name or category 421 | 422 | Args: 423 | query: Search query 424 | 425 | Returns: 426 | List of components matching the search query 427 | """ 428 | params = { 429 | "query": query 430 | } 431 | 432 | return send_to_grasshopper("search_components", params) 433 | 434 | @server.tool("get_component_parameters") 435 | def get_component_parameters(component_type: str): 436 | """ 437 | Get a list of parameters for a specific component type 438 | 439 | Args: 440 | component_type: Type of component to get parameters for 441 | 442 | Returns: 443 | List of input and output parameters for the component type 444 | """ 445 | params = { 446 | "componentType": component_type 447 | } 448 | 449 | return send_to_grasshopper("get_component_parameters", params) 450 | 451 | @server.tool("validate_connection") 452 | def validate_connection(source_id: str, target_id: str, source_param: str = None, target_param: str = None): 453 | """ 454 | Validate if a connection between two components is possible 455 | 456 | Args: 457 | source_id: ID of the source component (output) 458 | target_id: ID of the target component (input) 459 | source_param: Name of the source parameter (optional) 460 | target_param: Name of the target parameter (optional) 461 | 462 | Returns: 463 | Whether the connection is valid and any potential issues 464 | """ 465 | params = { 466 | "sourceId": source_id, 467 | "targetId": target_id 468 | } 469 | 470 | if source_param is not None: 471 | params["sourceParam"] = source_param 472 | 473 | if target_param is not None: 474 | params["targetParam"] = target_param 475 | 476 | return send_to_grasshopper("validate_connection", params) 477 | 478 | # 註冊 MCP 資源 479 | @server.resource("grasshopper://status") 480 | def get_grasshopper_status(): 481 | """Get Grasshopper status""" 482 | try: 483 | # 獲取文檔信息 484 | doc_info = send_to_grasshopper("get_document_info") 485 | 486 | # 獲取所有組件(使用增強版的 get_all_components) 487 | components_result = get_all_components() 488 | components = components_result.get("result", []) if components_result else [] 489 | 490 | # 獲取所有連接 491 | connections = send_to_grasshopper("get_connections") 492 | 493 | # 添加常用組件的提示信息 494 | component_hints = { 495 | "Number Slider": { 496 | "description": "Single numeric value slider with adjustable range", 497 | "common_usage": "Use for single numeric inputs like radius, height, count, etc.", 498 | "parameters": ["min", "max", "value", "rounding", "type"], 499 | "NOT_TO_BE_CONFUSED_WITH": "MD Slider (which is for multi-dimensional values)" 500 | }, 501 | "MD Slider": { 502 | "description": "Multi-dimensional slider for vector input", 503 | "common_usage": "Use for vector inputs, NOT for simple numeric values", 504 | "NOT_TO_BE_CONFUSED_WITH": "Number Slider (which is for single numeric values)" 505 | }, 506 | "Panel": { 507 | "description": "Displays text or numeric data", 508 | "common_usage": "Use for displaying outputs and debugging" 509 | }, 510 | "Addition": { 511 | "description": "Adds two or more numbers", 512 | "common_usage": "Connect two Number Sliders to inputs A and B", 513 | "parameters": ["A", "B"], 514 | "connection_tip": "First slider should connect to input A, second to input B" 515 | } 516 | } 517 | 518 | # 為每個組件添加當前參數值的摘要 519 | component_summaries = [] 520 | for component in components: 521 | summary = { 522 | "id": component.get("id", ""), 523 | "type": component.get("type", ""), 524 | "position": { 525 | "x": component.get("x", 0), 526 | "y": component.get("y", 0) 527 | } 528 | } 529 | 530 | # 添加組件特定的參數信息 531 | if "currentSettings" in component: 532 | summary["settings"] = component["currentSettings"] 533 | elif component.get("type") == "Number Slider": 534 | # 嘗試從組件信息中提取滑桿設置 535 | summary["settings"] = { 536 | "min": component.get("min", 0), 537 | "max": component.get("max", 10), 538 | "value": component.get("value", 5), 539 | "rounding": component.get("rounding", 0.1) 540 | } 541 | 542 | # 添加連接信息摘要 543 | if "connections" in component: 544 | conn_summary = [] 545 | for conn in component["connections"]: 546 | if conn.get("sourceId") == component.get("id"): 547 | conn_summary.append({ 548 | "type": "output", 549 | "to": conn.get("targetId", ""), 550 | "sourceParam": conn.get("sourceParam", ""), 551 | "targetParam": conn.get("targetParam", "") 552 | }) 553 | else: 554 | conn_summary.append({ 555 | "type": "input", 556 | "from": conn.get("sourceId", ""), 557 | "sourceParam": conn.get("sourceParam", ""), 558 | "targetParam": conn.get("targetParam", "") 559 | }) 560 | 561 | if conn_summary: 562 | summary["connections"] = conn_summary 563 | 564 | component_summaries.append(summary) 565 | 566 | return { 567 | "status": "Connected to Grasshopper", 568 | "document": doc_info.get("result", {}), 569 | "components": component_summaries, 570 | "connections": connections.get("result", []), 571 | "component_hints": component_hints, 572 | "recommendations": [ 573 | "When needing a simple numeric input control, ALWAYS use 'Number Slider', not MD Slider", 574 | "For vector inputs (like 3D points), use 'MD Slider' or 'Construct Point' with multiple Number Sliders", 575 | "Use 'Panel' to display outputs and debug values", 576 | "When connecting multiple sliders to Addition, first slider goes to input A, second to input B" 577 | ], 578 | "canvas_summary": f"Current canvas has {len(component_summaries)} components and {len(connections.get('result', []))} connections" 579 | } 580 | except Exception as e: 581 | print(f"Error getting Grasshopper status: {str(e)}", file=sys.stderr) 582 | traceback.print_exc(file=sys.stderr) 583 | return { 584 | "status": f"Error: {str(e)}", 585 | "document": {}, 586 | "components": [], 587 | "connections": [] 588 | } 589 | 590 | @server.resource("grasshopper://component_guide") 591 | def get_component_guide(): 592 | """Get guide for Grasshopper components and connections""" 593 | return { 594 | "title": "Grasshopper Component Guide", 595 | "description": "Guide for creating and connecting Grasshopper components", 596 | "components": [ 597 | { 598 | "name": "Point", 599 | "category": "Params", 600 | "description": "Creates a point at specific coordinates", 601 | "inputs": [ 602 | {"name": "X", "type": "Number"}, 603 | {"name": "Y", "type": "Number"}, 604 | {"name": "Z", "type": "Number"} 605 | ], 606 | "outputs": [ 607 | {"name": "Pt", "type": "Point"} 608 | ] 609 | }, 610 | { 611 | "name": "Circle", 612 | "category": "Curve", 613 | "description": "Creates a circle", 614 | "inputs": [ 615 | {"name": "Plane", "type": "Plane", "description": "Base plane for the circle"}, 616 | {"name": "Radius", "type": "Number", "description": "Circle radius"} 617 | ], 618 | "outputs": [ 619 | {"name": "C", "type": "Circle"} 620 | ] 621 | }, 622 | { 623 | "name": "XY Plane", 624 | "category": "Vector", 625 | "description": "Creates an XY plane at the world origin or at a specified point", 626 | "inputs": [ 627 | {"name": "Origin", "type": "Point", "description": "Origin point", "optional": True} 628 | ], 629 | "outputs": [ 630 | {"name": "Plane", "type": "Plane", "description": "XY plane"} 631 | ] 632 | }, 633 | { 634 | "name": "Addition", 635 | "fullName": "Addition", 636 | "description": "Adds two or more numbers", 637 | "inputs": [ 638 | {"name": "A", "type": "Number", "description": "First input value"}, 639 | {"name": "B", "type": "Number", "description": "Second input value"} 640 | ], 641 | "outputs": [ 642 | {"name": "Result", "type": "Number", "description": "Sum of inputs"} 643 | ], 644 | "usage_examples": [ 645 | "Connect two Number Sliders to inputs A and B to add their values", 646 | "Connect multiple values to add them all together" 647 | ], 648 | "common_issues": [ 649 | "When connecting multiple sliders, ensure they connect to different inputs (A and B)", 650 | "The first slider should connect to input A, the second to input B" 651 | ] 652 | }, 653 | { 654 | "name": "Number Slider", 655 | "fullName": "Number Slider", 656 | "description": "Creates a slider for numeric input with adjustable range and precision", 657 | "inputs": [], 658 | "outputs": [ 659 | {"name": "N", "type": "Number", "description": "Number output"} 660 | ], 661 | "settings": { 662 | "min": {"description": "Minimum value of the slider", "default": 0}, 663 | "max": {"description": "Maximum value of the slider", "default": 10}, 664 | "value": {"description": "Current value of the slider", "default": 5}, 665 | "rounding": {"description": "Rounding precision (0.01, 0.1, 1, etc.)", "default": 0.1}, 666 | "type": {"description": "Slider type (integer, floating point)", "default": "float"}, 667 | "name": {"description": "Custom name for the slider", "default": ""} 668 | }, 669 | "usage_examples": [ 670 | "Create a Number Slider with min=0, max=100, value=50", 671 | "Create a Number Slider for radius with min=0.1, max=10, value=2.5, rounding=0.1" 672 | ], 673 | "common_issues": [ 674 | "Confusing with other slider types", 675 | "Not setting appropriate min/max values for the intended use" 676 | ], 677 | "disambiguation": { 678 | "similar_components": [ 679 | { 680 | "name": "MD Slider", 681 | "description": "Multi-dimensional slider for vector input, NOT for simple numeric values", 682 | "how_to_distinguish": "Use Number Slider for single numeric values; use MD Slider only when you need multi-dimensional control" 683 | }, 684 | { 685 | "name": "Graph Mapper", 686 | "description": "Maps values through a graph function, NOT a simple slider", 687 | "how_to_distinguish": "Use Number Slider for direct numeric input; use Graph Mapper only for function-based mapping" 688 | } 689 | ], 690 | "correct_usage": "When needing a simple numeric input control, ALWAYS use 'Number Slider', not MD Slider or other variants" 691 | } 692 | }, 693 | { 694 | "name": "Panel", 695 | "fullName": "Panel", 696 | "description": "Displays text or numeric data", 697 | "inputs": [ 698 | {"name": "Input", "type": "Any"} 699 | ], 700 | "outputs": [] 701 | }, 702 | { 703 | "name": "Math", 704 | "fullName": "Mathematics", 705 | "description": "Performs mathematical operations", 706 | "inputs": [ 707 | {"name": "A", "type": "Number"}, 708 | {"name": "B", "type": "Number"} 709 | ], 710 | "outputs": [ 711 | {"name": "Result", "type": "Number"} 712 | ], 713 | "operations": ["Addition", "Subtraction", "Multiplication", "Division", "Power", "Modulo"] 714 | }, 715 | { 716 | "name": "Construct Point", 717 | "fullName": "Construct Point", 718 | "description": "Constructs a point from X, Y, Z coordinates", 719 | "inputs": [ 720 | {"name": "X", "type": "Number"}, 721 | {"name": "Y", "type": "Number"}, 722 | {"name": "Z", "type": "Number"} 723 | ], 724 | "outputs": [ 725 | {"name": "Pt", "type": "Point"} 726 | ] 727 | }, 728 | { 729 | "name": "Line", 730 | "fullName": "Line", 731 | "description": "Creates a line between two points", 732 | "inputs": [ 733 | {"name": "Start", "type": "Point"}, 734 | {"name": "End", "type": "Point"} 735 | ], 736 | "outputs": [ 737 | {"name": "L", "type": "Line"} 738 | ] 739 | }, 740 | { 741 | "name": "Extrude", 742 | "fullName": "Extrude", 743 | "description": "Extrudes a curve to create a surface or a solid", 744 | "inputs": [ 745 | {"name": "Base", "type": "Curve"}, 746 | {"name": "Direction", "type": "Vector"}, 747 | {"name": "Height", "type": "Number"} 748 | ], 749 | "outputs": [ 750 | {"name": "Brep", "type": "Brep"} 751 | ] 752 | } 753 | ], 754 | "connectionRules": [ 755 | { 756 | "from": "Number", 757 | "to": "Circle.Radius", 758 | "description": "Connect a number to the radius input of a circle" 759 | }, 760 | { 761 | "from": "Point", 762 | "to": "Circle.Plane", 763 | "description": "Connect a point to the plane input of a circle (not recommended, use XY Plane instead)" 764 | }, 765 | { 766 | "from": "XY Plane", 767 | "to": "Circle.Plane", 768 | "description": "Connect an XY Plane to the plane input of a circle (recommended)" 769 | }, 770 | { 771 | "from": "Number", 772 | "to": "Math.A", 773 | "description": "Connect a number to the first input of a Math component" 774 | }, 775 | { 776 | "from": "Number", 777 | "to": "Math.B", 778 | "description": "Connect a number to the second input of a Math component" 779 | }, 780 | { 781 | "from": "Number", 782 | "to": "Construct Point.X", 783 | "description": "Connect a number to the X input of a Construct Point component" 784 | }, 785 | { 786 | "from": "Number", 787 | "to": "Construct Point.Y", 788 | "description": "Connect a number to the Y input of a Construct Point component" 789 | }, 790 | { 791 | "from": "Number", 792 | "to": "Construct Point.Z", 793 | "description": "Connect a number to the Z input of a Construct Point component" 794 | }, 795 | { 796 | "from": "Point", 797 | "to": "Line.Start", 798 | "description": "Connect a point to the start input of a Line component" 799 | }, 800 | { 801 | "from": "Point", 802 | "to": "Line.End", 803 | "description": "Connect a point to the end input of a Line component" 804 | }, 805 | { 806 | "from": "Circle", 807 | "to": "Extrude.Base", 808 | "description": "Connect a circle to the base input of an Extrude component" 809 | }, 810 | { 811 | "from": "Number", 812 | "to": "Extrude.Height", 813 | "description": "Connect a number to the height input of an Extrude component" 814 | } 815 | ], 816 | "commonIssues": [ 817 | "Using Point component instead of XY Plane for inputs that require planes", 818 | "Not specifying parameter names when connecting components", 819 | "Using incorrect component names (e.g., 'addition' instead of 'Math' with Addition operation)", 820 | "Trying to connect incompatible data types", 821 | "Not providing all required inputs for a component", 822 | "Using incorrect parameter names (e.g., 'A' and 'B' for Math component instead of the actual parameter names)", 823 | "Not checking if a connection was successful before proceeding" 824 | ], 825 | "tips": [ 826 | "Always use XY Plane component for plane inputs", 827 | "Specify parameter names when connecting components", 828 | "For Circle components, make sure to use the correct inputs (Plane and Radius)", 829 | "Test simple connections before creating complex geometry", 830 | "Avoid using components that require selection from Rhino", 831 | "Use get_component_info to check the actual parameter names of a component", 832 | "Use get_connections to verify if connections were established correctly", 833 | "Use search_components to find the correct component name before adding it", 834 | "Use validate_connection to check if a connection is possible before attempting it" 835 | ] 836 | } 837 | 838 | @server.resource("grasshopper://component_library") 839 | def get_component_library(): 840 | """Get a comprehensive library of Grasshopper components""" 841 | # 這個資源提供了一個更全面的組件庫,包括常用組件的詳細信息 842 | return { 843 | "categories": [ 844 | { 845 | "name": "Params", 846 | "components": [ 847 | { 848 | "name": "Point", 849 | "fullName": "Point Parameter", 850 | "description": "Creates a point parameter", 851 | "inputs": [ 852 | {"name": "X", "type": "Number", "description": "X coordinate"}, 853 | {"name": "Y", "type": "Number", "description": "Y coordinate"}, 854 | {"name": "Z", "type": "Number", "description": "Z coordinate"} 855 | ], 856 | "outputs": [ 857 | {"name": "Pt", "type": "Point", "description": "Point output"} 858 | ] 859 | }, 860 | { 861 | "name": "Number Slider", 862 | "fullName": "Number Slider", 863 | "description": "Creates a slider for numeric input with adjustable range and precision", 864 | "inputs": [], 865 | "outputs": [ 866 | {"name": "N", "type": "Number", "description": "Number output"} 867 | ], 868 | "settings": { 869 | "min": {"description": "Minimum value of the slider", "default": 0}, 870 | "max": {"description": "Maximum value of the slider", "default": 10}, 871 | "value": {"description": "Current value of the slider", "default": 5}, 872 | "rounding": {"description": "Rounding precision (0.01, 0.1, 1, etc.)", "default": 0.1}, 873 | "type": {"description": "Slider type (integer, floating point)", "default": "float"}, 874 | "name": {"description": "Custom name for the slider", "default": ""} 875 | }, 876 | "usage_examples": [ 877 | "Create a Number Slider with min=0, max=100, value=50", 878 | "Create a Number Slider for radius with min=0.1, max=10, value=2.5, rounding=0.1" 879 | ], 880 | "common_issues": [ 881 | "Confusing with other slider types", 882 | "Not setting appropriate min/max values for the intended use" 883 | ], 884 | "disambiguation": { 885 | "similar_components": [ 886 | { 887 | "name": "MD Slider", 888 | "description": "Multi-dimensional slider for vector input, NOT for simple numeric values", 889 | "how_to_distinguish": "Use Number Slider for single numeric values; use MD Slider only when you need multi-dimensional control" 890 | }, 891 | { 892 | "name": "Graph Mapper", 893 | "description": "Maps values through a graph function, NOT a simple slider", 894 | "how_to_distinguish": "Use Number Slider for direct numeric input; use Graph Mapper only for function-based mapping" 895 | } 896 | ], 897 | "correct_usage": "When needing a simple numeric input control, ALWAYS use 'Number Slider', not MD Slider or other variants" 898 | } 899 | }, 900 | { 901 | "name": "Panel", 902 | "fullName": "Panel", 903 | "description": "Displays text or numeric data", 904 | "inputs": [ 905 | {"name": "Input", "type": "Any", "description": "Any input data"} 906 | ], 907 | "outputs": [] 908 | } 909 | ] 910 | }, 911 | { 912 | "name": "Maths", 913 | "components": [ 914 | { 915 | "name": "Math", 916 | "fullName": "Mathematics", 917 | "description": "Performs mathematical operations", 918 | "inputs": [ 919 | {"name": "A", "type": "Number", "description": "First number"}, 920 | {"name": "B", "type": "Number", "description": "Second number"} 921 | ], 922 | "outputs": [ 923 | {"name": "Result", "type": "Number", "description": "Result of the operation"} 924 | ], 925 | "operations": ["Addition", "Subtraction", "Multiplication", "Division", "Power", "Modulo"] 926 | } 927 | ] 928 | }, 929 | { 930 | "name": "Vector", 931 | "components": [ 932 | { 933 | "name": "XY Plane", 934 | "fullName": "XY Plane", 935 | "description": "Creates an XY plane at the world origin or at a specified point", 936 | "inputs": [ 937 | {"name": "Origin", "type": "Point", "description": "Origin point", "optional": True} 938 | ], 939 | "outputs": [ 940 | {"name": "Plane", "type": "Plane", "description": "XY plane"} 941 | ] 942 | }, 943 | { 944 | "name": "Construct Point", 945 | "fullName": "Construct Point", 946 | "description": "Constructs a point from X, Y, Z coordinates", 947 | "inputs": [ 948 | {"name": "X", "type": "Number", "description": "X coordinate"}, 949 | {"name": "Y", "type": "Number", "description": "Y coordinate"}, 950 | {"name": "Z", "type": "Number", "description": "Z coordinate"} 951 | ], 952 | "outputs": [ 953 | {"name": "Pt", "type": "Point", "description": "Constructed point"} 954 | ] 955 | } 956 | ] 957 | }, 958 | { 959 | "name": "Curve", 960 | "components": [ 961 | { 962 | "name": "Circle", 963 | "fullName": "Circle", 964 | "description": "Creates a circle", 965 | "inputs": [ 966 | {"name": "Plane", "type": "Plane", "description": "Base plane for the circle"}, 967 | {"name": "Radius", "type": "Number", "description": "Circle radius"} 968 | ], 969 | "outputs": [ 970 | {"name": "C", "type": "Circle", "description": "Circle output"} 971 | ] 972 | }, 973 | { 974 | "name": "Line", 975 | "fullName": "Line", 976 | "description": "Creates a line between two points", 977 | "inputs": [ 978 | {"name": "Start", "type": "Point", "description": "Start point"}, 979 | {"name": "End", "type": "Point", "description": "End point"} 980 | ], 981 | "outputs": [ 982 | {"name": "L", "type": "Line", "description": "Line output"} 983 | ] 984 | } 985 | ] 986 | }, 987 | { 988 | "name": "Surface", 989 | "components": [ 990 | { 991 | "name": "Extrude", 992 | "fullName": "Extrude", 993 | "description": "Extrudes a curve to create a surface or a solid", 994 | "inputs": [ 995 | {"name": "Base", "type": "Curve", "description": "Base curve to extrude"}, 996 | {"name": "Direction", "type": "Vector", "description": "Direction of extrusion", "optional": True}, 997 | {"name": "Height", "type": "Number", "description": "Height of extrusion"} 998 | ], 999 | "outputs": [ 1000 | {"name": "Brep", "type": "Brep", "description": "Extruded brep"} 1001 | ] 1002 | } 1003 | ] 1004 | } 1005 | ], 1006 | "dataTypes": [ 1007 | { 1008 | "name": "Number", 1009 | "description": "A numeric value", 1010 | "compatibleWith": ["Number", "Integer", "Double"] 1011 | }, 1012 | { 1013 | "name": "Point", 1014 | "description": "A 3D point in space", 1015 | "compatibleWith": ["Point3d", "Point"] 1016 | }, 1017 | { 1018 | "name": "Vector", 1019 | "description": "A 3D vector", 1020 | "compatibleWith": ["Vector3d", "Vector"] 1021 | }, 1022 | { 1023 | "name": "Plane", 1024 | "description": "A plane in 3D space", 1025 | "compatibleWith": ["Plane"] 1026 | }, 1027 | { 1028 | "name": "Circle", 1029 | "description": "A circle curve", 1030 | "compatibleWith": ["Circle", "Curve"] 1031 | }, 1032 | { 1033 | "name": "Line", 1034 | "description": "A line segment", 1035 | "compatibleWith": ["Line", "Curve"] 1036 | }, 1037 | { 1038 | "name": "Curve", 1039 | "description": "A curve object", 1040 | "compatibleWith": ["Curve", "Circle", "Line", "Arc", "Polyline"] 1041 | }, 1042 | { 1043 | "name": "Brep", 1044 | "description": "A boundary representation object", 1045 | "compatibleWith": ["Brep", "Surface", "Solid"] 1046 | } 1047 | ] 1048 | } 1049 | 1050 | def main(): 1051 | """Main entry point for the Grasshopper MCP Bridge Server""" 1052 | try: 1053 | # 啟動 MCP 服務器 1054 | print("Starting Grasshopper MCP Bridge Server...", file=sys.stderr) 1055 | print("Please add this MCP server to Claude Desktop", file=sys.stderr) 1056 | server.run() 1057 | except Exception as e: 1058 | print(f"Error starting MCP server: {str(e)}", file=sys.stderr) 1059 | traceback.print_exc(file=sys.stderr) 1060 | sys.exit(1) 1061 | 1062 | if __name__ == "__main__": 1063 | main() 1064 | ```