#
tokens: 48720/50000 32/36 files (page 1/3)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 1 of 3. Use http://codebase.md/always-tinkering/rhinomcpserver?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .gitignore
├── code_architecture.md
├── combined_mcp_server.py
├── diagnose_rhino_connection.py
├── log_manager.py
├── LOGGING.md
├── logs
│   └── json_filter.py
├── mcpLLM.txt
├── NLog.config
├── README.md
├── RHINO_PLUGIN_UPDATE.md
├── RhinoMcpPlugin
│   ├── Models
│   │   └── RhinoObjectProperties.cs
│   ├── Properties
│   │   └── AssemblyInfo.cs
│   ├── RhinoMcpCommand.cs
│   ├── RhinoMcpPlugin.cs
│   ├── RhinoMcpPlugin.csproj
│   ├── RhinoSocketServer.cs
│   ├── RhinoUtilities.cs
│   └── Tools
│       ├── GeometryTools.cs
│       └── SceneTools.cs
├── RhinoMcpPlugin.Tests
│   ├── ImplementationPlan.md
│   ├── Mocks
│   │   └── MockRhinoDoc.cs
│   ├── README.md
│   ├── RhinoMcpPlugin.Tests.csproj
│   ├── test.runsettings
│   ├── Tests
│   │   ├── ColorUtilTests.cs
│   │   ├── RhinoMcpPluginTests.cs
│   │   ├── RhinoSocketServerTests.cs
│   │   └── RhinoUtilitiesTests.cs
│   └── Utils
│       └── ColorParser.cs
├── RhinoPluginFixImplementation.cs
├── RhinoPluginLoggingSpec.md
├── run-combined-server.sh
├── scripts
│   ├── direct-launcher.sh
│   ├── run-combined-server.sh
│   └── run-python-server.sh
└── src
    ├── daemon_mcp_server.py
    ├── socket_proxy.py
    └── standalone-mcp-server.py
```

# Files

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

```
  1 | # .NET build folders
  2 | bin/
  3 | obj/
  4 | publish/
  5 | 
  6 | # OS generated files
  7 | .DS_Store
  8 | .DS_Store?
  9 | ._*
 10 | .Spotlight-V100
 11 | .Trashes
 12 | Icon?
 13 | ehthumbs.db
 14 | Thumbs.db
 15 | 
 16 | # IDE files
 17 | .vs/
 18 | .vscode/
 19 | *.user
 20 | *.suo
 21 | *.userprefs
 22 | *.usertasks
 23 | *.userosscache
 24 | *.sln.docstates
 25 | 
 26 | # Build results
 27 | [Dd]ebug/
 28 | [Dd]ebugPublic/
 29 | [Rr]elease/
 30 | [Rr]eleases/
 31 | x64/
 32 | x86/
 33 | [Aa][Rr][Mm]/
 34 | [Aa][Rr][Mm]64/
 35 | bld/
 36 | [Bb]in/
 37 | [Oo]bj/
 38 | [Ll]og/
 39 | 
 40 | # Temporary files
 41 | *.tmp
 42 | *.log
 43 | 
 44 | # Byte-compiled / optimized / DLL files
 45 | __pycache__/
 46 | *.py[cod]
 47 | *$py.class
 48 | 
 49 | # C extensions
 50 | *.so
 51 | 
 52 | # Distribution / packaging
 53 | .Python
 54 | build/
 55 | develop-eggs/
 56 | dist/
 57 | downloads/
 58 | eggs/
 59 | .eggs/
 60 | lib/
 61 | lib64/
 62 | parts/
 63 | sdist/
 64 | var/
 65 | wheels/
 66 | *.egg-info/
 67 | .installed.cfg
 68 | *.egg
 69 | MANIFEST
 70 | 
 71 | # PyInstaller
 72 | #  Usually these files are written by a python script from a template
 73 | #  before PyInstaller builds the exe, so as to inject date/other infos into it.
 74 | *.manifest
 75 | *.spec
 76 | 
 77 | # Installer logs
 78 | pip-log.txt
 79 | pip-delete-this-directory.txt
 80 | 
 81 | # Unit test / coverage reports
 82 | htmlcov/
 83 | .tox/
 84 | .coverage
 85 | .coverage.*
 86 | .cache
 87 | nosetests.xml
 88 | coverage.xml
 89 | *.cover
 90 | .hypothesis/
 91 | .pytest_cache/
 92 | 
 93 | # Translations
 94 | *.mo
 95 | *.pot
 96 | 
 97 | # Django stuff:
 98 | *.log
 99 | local_settings.py
100 | db.sqlite3
101 | 
102 | # Flask stuff:
103 | instance/
104 | .webassets-cache
105 | 
106 | # Scrapy stuff:
107 | .scrapy
108 | 
109 | # Sphinx documentation
110 | docs/_build/
111 | 
112 | # PyBuilder
113 | target/
114 | 
115 | # Jupyter Notebook
116 | .ipynb_checkpoints
117 | 
118 | # pyenv
119 | .python-version
120 | 
121 | # celery beat schedule file
122 | celerybeat-schedule
123 | 
124 | # SageMath parsed files
125 | *.sage.py
126 | 
127 | # Environments
128 | .env
129 | .venv
130 | env/
131 | venv/
132 | ENV/
133 | env.bak/
134 | venv.bak/
135 | 
136 | # Spyder project settings
137 | .spyderproject
138 | .spyproject
139 | 
140 | # Rope project settings
141 | .ropeproject
142 | 
143 | # mkdocs documentation
144 | /site
145 | 
146 | # mypy
147 | .mypy_cache/
148 | 
149 | # Rhino MCP Server specific
150 | *.pid
151 | *.log
152 | logs/
153 | .server_initialized
154 | .DS_Store
155 | 
156 | # Python
157 | __pycache__/
158 | *.py[cod]
159 | *$py.class
160 | *.so
161 | .Python
162 | env/
163 | build/
164 | develop-eggs/
165 | dist/
166 | downloads/
167 | eggs/
168 | .eggs/
169 | lib/
170 | lib64/
171 | parts/
172 | sdist/
173 | var/
174 | wheels/
175 | *.egg-info/
176 | .installed.cfg
177 | *.egg
178 | 
179 | # Project specific
180 | logs/
181 | *.log
182 | logs/*.pid
183 | logs/*.path
184 | 
185 | # Editors
186 | .idea/
187 | .vscode/
188 | *.swp
189 | *.swo
190 | .DS_Store 
```

--------------------------------------------------------------------------------
/RhinoMcpPlugin.Tests/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | # RhinoMcpPlugin Tests
  2 | 
  3 | This project contains unit tests for the RhinoMcpPlugin, a core component of the RhinoMCP project that integrates the Model Context Protocol with Rhino3D.
  4 | 
  5 | ## Overview
  6 | 
  7 | The test suite is organized into several categories of tests:
  8 | 
  9 | - **Plugin Lifecycle Tests**: Testing the plugin's initialization, loading, and shutdown processes
 10 | - **Socket Server Tests**: Testing the communication layer that handles MCP commands
 11 | - **Geometry Tools Tests**: Testing the creation and manipulation of 3D geometry
 12 | - **Scene Tools Tests**: Testing scene management functions
 13 | - **Utility Tests**: Testing helper functions and utilities
 14 | 
 15 | ## Getting Started
 16 | 
 17 | ### Prerequisites
 18 | 
 19 | - Visual Studio 2022 or later (or Visual Studio Code with C# extensions)
 20 | - .NET 7.0 SDK
 21 | - NUnit 3 Test Adapter (for running tests in Visual Studio)
 22 | 
 23 | ### Building the Tests
 24 | 
 25 | 1. Open the RhinoMcpPlugin solution in Visual Studio or VS Code
 26 | 2. Build the RhinoMcpPlugin.Tests project
 27 | 
 28 | ### Running the Tests
 29 | 
 30 | #### From Visual Studio
 31 | 
 32 | 1. Open the Test Explorer window (Test > Test Explorer)
 33 | 2. Click "Run All" to run all tests, or select specific tests to run
 34 | 
 35 | #### From Visual Studio Code
 36 | 
 37 | 1. **Install the required VS Code extensions**:
 38 |    - C# Dev Kit extension (which includes the C# extension)
 39 |    - .NET Core Test Explorer extension
 40 | 
 41 | 2. **Configure test discovery in VS Code**:
 42 |    - Open the workspace settings (File > Preferences > Settings)
 43 |    - Search for "test"
 44 |    - Under Extensions > .NET Core Test Explorer, set the test project path to include your test project:
 45 |      ```json
 46 |      "dotnet-test-explorer.testProjectPath": "**/RhinoMcpPlugin.Tests.csproj"
 47 |      ```
 48 | 
 49 | 3. **Run the tests**:
 50 |    - You can use the Test Explorer UI (click the flask icon in the sidebar)
 51 |    - Click the run or debug icons next to individual tests or test classes
 52 |    - Right-click on tests to run, debug, or view specific tests
 53 | 
 54 | 4. **Debug tests**:
 55 |    - Set breakpoints in your test code
 56 |    - Use the "Debug Test" option in the Test Explorer
 57 |    - Make sure your launch.json is configured correctly for .NET debugging
 58 | 
 59 | #### From Command Line
 60 | 
 61 | ```
 62 | dotnet test RhinoMcpPlugin.Tests/RhinoMcpPlugin.Tests.csproj
 63 | ```
 64 | 
 65 | To run a specific category of tests:
 66 | 
 67 | ```
 68 | dotnet test RhinoMcpPlugin.Tests/RhinoMcpPlugin.Tests.csproj --filter "Category=Utilities"
 69 | ```
 70 | 
 71 | To run a specific test class:
 72 | 
 73 | ```
 74 | dotnet test RhinoMcpPlugin.Tests/RhinoMcpPlugin.Tests.csproj --filter "FullyQualifiedName~RhinoUtilitiesTests"
 75 | ```
 76 | 
 77 | ## Project Structure
 78 | 
 79 | - `/Tests`: Contains all test classes organized by component
 80 | - `/Mocks`: Contains mock implementations of Rhino objects for testing
 81 | - `/Framework`: Contains test helpers and base classes
 82 | 
 83 | ## Testing Strategy
 84 | 
 85 | ### Mocking Approach
 86 | 
 87 | Since the RhinoMcpPlugin relies heavily on Rhino's API, we use two mocking strategies:
 88 | 
 89 | 1. **Custom Mock Classes**: For core Rhino objects like RhinoDoc, we've created custom mock implementations that inherit from Rhino classes.
 90 | 2. **Moq Framework**: For simpler dependencies and interfaces, we use the Moq library.
 91 | 
 92 | ### Test Isolation
 93 | 
 94 | Each test is designed to be independent and should not rely on the state from other tests. Tests follow this structure:
 95 | 
 96 | 1. **Arrange**: Set up the test environment and data
 97 | 2. **Act**: Perform the action being tested
 98 | 3. **Assert**: Verify the expected outcome
 99 | 4. **Cleanup**: Release any resources (usually handled in TearDown methods)
100 | 
101 | ## Writing New Tests
102 | 
103 | ### Test Naming Convention
104 | 
105 | Tests should follow this naming pattern:
106 | 
107 | ```
108 | MethodName_Scenario_ExpectedBehavior
109 | ```
110 | 
111 | For example:
112 | - `ParseHexColor_ValidHexWithHash_ReturnsCorrectColor`
113 | - `Start_WhenCalled_ServerStarts`
114 | 
115 | ### Test Categories
116 | 
117 | Use NUnit's Category attribute to organize tests:
118 | 
119 | ```csharp
120 | [Test]
121 | [Category("Utilities")]
122 | public void MyTest()
123 | {
124 |     // Test implementation
125 | }
126 | ```
127 | 
128 | Available categories:
129 | - `Utilities`
130 | - `SocketServer`
131 | - `Plugin`
132 | - `Geometry`
133 | - `Commands`
134 | 
135 | ### Sample Test
136 | 
137 | ```csharp
138 | [Test]
139 | [Category("Utilities")]
140 | public void ParseHexColor_ValidHexWithHash_ReturnsCorrectColor()
141 | {
142 |     // Arrange
143 |     string hexColor = "#FF0000";
144 |     
145 |     // Act
146 |     Color? color = RhinoUtilities.ParseHexColor(hexColor);
147 |     
148 |     // Assert
149 |     Assert.That(color, Is.Not.Null);
150 |     Assert.That(color.Value.R, Is.EqualTo(255));
151 |     Assert.That(color.Value.G, Is.EqualTo(0));
152 |     Assert.That(color.Value.B, Is.EqualTo(0));
153 | }
154 | ```
155 | 
156 | ## Common Issues and Solutions
157 | 
158 | ### Test Cannot Find RhinoCommon.dll
159 | 
160 | Make sure the project has a correct reference to RhinoCommon.dll. In the `.csproj` file, the reference should be:
161 | 
162 | ```xml
163 | <Reference Include="RhinoCommon">
164 |   <HintPath>$(RhinoPath)\RhinoCommon.dll</HintPath>
165 |   <Private>False</Private>
166 | </Reference>
167 | ```
168 | 
169 | You may need to set the `RhinoPath` environment variable to your Rhino installation directory.
170 | 
171 | ### VS Code Test Discovery Issues
172 | 
173 | If VS Code is not discovering your tests:
174 | 
175 | 1. Make sure you've installed the .NET Core Test Explorer extension
176 | 2. Check that your settings.json includes the correct test project path
177 | 3. Try refreshing the test explorer (click the refresh icon)
178 | 4. Ensure your tests have the `[Test]` attribute and are in public classes
179 | 
180 | ### Socket Server Tests Are Flaky
181 | 
182 | Socket server tests can sometimes be flaky due to timing issues. Try:
183 | 
184 | 1. Increasing the sleep duration between server start and client connection
185 | 2. Using dedicated test ports to avoid conflicts
186 | 3. Adding retry logic for connection attempts
187 | 
188 | ## Contributing
189 | 
190 | When contributing new tests:
191 | 
192 | 1. Follow the established naming conventions and patterns
193 | 2. Add tests to the appropriate test class or create a new one if needed
194 | 3. Use appropriate assertions for clear failure messages
195 | 4. Document any complex test scenarios
196 | 5. Run the full test suite before submitting changes
197 | 
198 | ## References
199 | 
200 | - [NUnit Documentation](https://docs.nunit.org/)
201 | - [Moq Documentation](https://github.com/moq/moq4)
202 | - [RhinoCommon API Reference](https://developer.rhino3d.com/api/rhinocommon/)
203 | - [VS Code .NET Testing](https://code.visualstudio.com/docs/languages/dotnet#_testing)
204 | - [.NET Core Test Explorer](https://marketplace.visualstudio.com/items?itemName=formulahendry.dotnet-test-explorer) 
```

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

```markdown
  1 | # Rhino MCP Server
  2 | 
  3 | > **⚠️ UNDER CONSTRUCTION ⚠️**  
  4 | > This project is currently under active development and is not yet in working order. The Rhino plugin is experiencing issues with creating objects.
  5 | > We are actively seeking support from the community to help resolve these issues.
  6 | > If you have experience with Rhino API development, C# plugins, or MCP integration, please consider contributing.
  7 | > Contact us by opening an issue on GitHub.
  8 | 
  9 | A Model Context Protocol (MCP) server implementation for Rhino 3D, allowing Claude to create and manipulate 3D objects.
 10 | 
 11 | ## Overview
 12 | 
 13 | This project implements an MCP server for Rhino 3D that enables AI assistants like Claude to interact with Rhino through the Model Context Protocol. The server allows for the creation and manipulation of 3D objects directly from the AI interface.
 14 | 
 15 | ## System Architecture
 16 | 
 17 | The system consists of Python components that implement the MCP server and C# components that integrate with Rhino. Here's an overview of how the system components interact:
 18 | 
 19 | ```mermaid
 20 | graph TD
 21 |     %% Client Applications
 22 |     client[Client Applications] --> socketProxy
 23 |     
 24 |     %% Socket Proxy
 25 |     subgraph "Python Socket Proxy"
 26 |         socketProxy[socket_proxy.py] --> daemonServer
 27 |     end
 28 |     
 29 |     %% Daemon Server
 30 |     subgraph "Python Daemon Server"
 31 |         daemonServer[daemon_mcp_server.py] --> combinedServer
 32 |     end
 33 |     
 34 |     %% Combined MCP Server
 35 |     subgraph "Python Combined MCP Server"
 36 |         combinedServer[combined_mcp_server.py]
 37 |         mcp[FastMCP] --> tools
 38 |         combinedServer --> mcp
 39 |         combinedServer --> rhinoConn
 40 |         subgraph "MCP Tools"
 41 |             tools[MCP Tool Methods]
 42 |         end
 43 |         rhinoConn[RhinoConnection]
 44 |     end
 45 |     
 46 |     %% Rhino Plugin Components
 47 |     subgraph "C# Rhino Plugin"
 48 |         rhinoPlugin[RhinoMcpPlugin.cs]
 49 |         socketServer[RhinoSocketServer.cs]
 50 |         utilities[RhinoUtilities.cs]
 51 |         commands[RhinoMcpCommand.cs]
 52 |         
 53 |         rhinoPlugin --> socketServer
 54 |         rhinoPlugin --> commands
 55 |         socketServer --> utilities
 56 |     end
 57 |     
 58 |     %% Connections between components
 59 |     rhinoConn <==> socketServer
 60 |     
 61 |     %% Logger Components
 62 |     subgraph "Logging System"
 63 |         logManager[log_manager.py]
 64 |         nlogConfig[NLog.config]
 65 |     end
 66 |     
 67 |     combinedServer --> logManager
 68 |     rhinoPlugin --> nlogConfig
 69 |     
 70 |     %% Connection to Rhino
 71 |     rhino[Rhino 3D Software]
 72 |     rhinoPlugin --> rhino
 73 |     
 74 |     classDef pythonClass fill:#3572A5,color:white;
 75 |     classDef csharpClass fill:#178600,color:white;
 76 |     classDef rhinoClass fill:#555555,color:white;
 77 |     
 78 |     class socketProxy,daemonServer,combinedServer,mcp,tools,rhinoConn,logManager pythonClass;
 79 |     class rhinoPlugin,socketServer,utilities,commands csharpClass;
 80 |     class rhino rhinoClass;
 81 | ```
 82 | 
 83 | For more detailed information about the system architecture, including component descriptions and data flow, see [code_architecture.md](code_architecture.md).
 84 | 
 85 | ## Components
 86 | 
 87 | There are several implementations available:
 88 | 
 89 | 1. **Combined MCP Server (Recommended)**: 
 90 |    - `combined_mcp_server.py` - Direct implementation that uses stdin/stdout for communication
 91 | 
 92 | 2. **Socket-based Servers**:
 93 |    - `daemon_mcp_server.py` - Background server that receives commands via socket connection
 94 |    - `socket_proxy.py` - Proxy that forwards commands from stdin to the daemon server and back
 95 |    
 96 | 3. **Standalone Server**:
 97 |    - `standalone-mcp-server.py` - Original standalone implementation
 98 | 
 99 | ## Setup Instructions
100 | 
101 | ### 1. Set up Claude Desktop
102 | 
103 | 1. Install Claude Desktop if you haven't already
104 | 2. Configure the MCP server connection in Claude Desktop settings
105 | 
106 | ### 2. Run the Server
107 | 
108 | We now have a unified server launcher that allows you to run any of the server implementations:
109 | 
110 | ```bash
111 | ./server_launcher.sh [mode]
112 | ```
113 | 
114 | Available modes:
115 | - `combined` (default) - Run the combined MCP server
116 | - `standalone` - Run the standalone MCP server
117 | - `daemon` - Run the daemon MCP server
118 | - `socket-proxy` - Run the socket proxy
119 | - `direct` - Run both daemon and socket proxy
120 | - `logs` - View recent logs
121 | - `monitor` - Monitor logs in real-time
122 | - `errors` - View recent errors
123 | - `help` - Show help message
124 | 
125 | Examples:
126 | ```bash
127 | # Run the combined server (recommended)
128 | ./server_launcher.sh combined
129 | 
130 | # Or simply
131 | ./server_launcher.sh
132 | 
133 | # Run the socket-based approach (daemon + proxy)
134 | ./server_launcher.sh direct
135 | 
136 | # Monitor logs in real-time
137 | ./server_launcher.sh monitor
138 | ```
139 | 
140 | ## Available Tools
141 | 
142 | The server provides several tools for 3D modeling:
143 | 
144 | 1. **geometry_tools.create_sphere** - Create a sphere with specified center and radius
145 | 2. **geometry_tools.create_box** - Create a box with specified dimensions
146 | 3. **geometry_tools.create_cylinder** - Create a cylinder with specified parameters
147 | 4. **scene_tools.get_scene_info** - Get information about the current scene
148 | 5. **scene_tools.clear_scene** - Clear objects from the scene
149 | 6. **scene_tools.create_layer** - Create a new layer in the document
150 | 
151 | ## Troubleshooting
152 | 
153 | If you encounter connection issues:
154 | 
155 | 1. Make sure no old servers are running:
156 |    ```bash
157 |    ./server_launcher.sh help  # This will clean up existing processes
158 |    ```
159 | 
160 | 2. Check the log files:
161 |    ```bash
162 |    ./server_launcher.sh logs   # View logs
163 |    ./server_launcher.sh errors # View errors
164 |    ```
165 | 
166 | 3. Restart Claude Desktop completely
167 | 
168 | ## License
169 | 
170 | This project is released under the MIT License. See the LICENSE file for details. 
171 | 
172 | ## Improved Logging System
173 | 
174 | The system features a unified logging framework that centralizes logs from all components:
175 | 
176 | - Server logs
177 | - Plugin logs
178 | - Claude AI logs
179 | - Diagnostic logs
180 | 
181 | All logs follow a consistent format and are stored in the `logs/` directory with separate subdirectories for each component.
182 | 
183 | ### Log Management
184 | 
185 | A log management tool is provided that offers powerful capabilities for viewing, monitoring, and analyzing logs:
186 | 
187 | ```bash
188 | # View logs
189 | ./server_launcher.sh logs
190 | 
191 | # Monitor logs in real-time
192 | ./server_launcher.sh monitor
193 | 
194 | # View errors with context
195 | ./server_launcher.sh errors
196 | 
197 | # Generate error reports (using the log manager directly)
198 | ./log_manager.py report
199 | ```
200 | 
201 | For detailed information on using the logging system, see [LOGGING.md](LOGGING.md).
202 | 
203 | ## Development
204 | 
205 | ### Project Structure
206 | 
207 | - `combined_mcp_server.py`: Main MCP server implementation
208 | - `diagnose_rhino_connection.py`: Diagnostic tool for testing Rhino connection
209 | - `log_manager.py`: Tool for managing and analyzing logs
210 | - `server_launcher.sh`: Unified script to start any server implementation
211 | - `logs/`: Directory containing all logs
212 | 
213 | ### Adding New Features
214 | 
215 | 1. Add new tools as methods in the `combined_mcp_server.py` file
216 | 2. Use the existing logging framework for consistent error handling
217 | 3. Update diagnostic tools if needed 
```

--------------------------------------------------------------------------------
/RhinoMcpPlugin.Tests/Utils/ColorParser.cs:
--------------------------------------------------------------------------------

```csharp
1 |  
```

--------------------------------------------------------------------------------
/logs/json_filter.py:
--------------------------------------------------------------------------------

```python
 1 | #!/usr/bin/env python3
 2 | import sys
 3 | import json
 4 | import os
 5 | 
 6 | def main():
 7 |     log_file = os.environ.get('JSON_LOG_FILE', '/dev/null')
 8 |     with open(log_file, 'a') as log:
 9 |         line_count = 0
10 |         for line in sys.stdin:
11 |             line_count += 1
12 |             line = line.strip()
13 |             if not line:
14 |                 continue
15 |                 
16 |             try:
17 |                 # Try to parse as JSON to validate
18 |                 parsed = json.loads(line)
19 |                 # If successful, write to stdout for Claude to consume
20 |                 print(line)
21 |                 sys.stdout.flush()
22 |                 log.write(f"VALID JSON [{line_count}]: {line[:100]}...\n")
23 |                 log.flush()
24 |             except json.JSONDecodeError as e:
25 |                 # If invalid JSON, log it but don't pass to stdout
26 |                 log.write(f"INVALID JSON [{line_count}]: {str(e)} in: {line[:100]}...\n")
27 |                 log.flush()
28 | 
29 | if __name__ == "__main__":
30 |     main()
31 | 
```

--------------------------------------------------------------------------------
/RhinoMcpPlugin/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------

```csharp
 1 | using System.Reflection;
 2 | using System.Runtime.InteropServices;
 3 | using Rhino.PlugIns;
 4 | 
 5 | // General plugin information
 6 | [assembly: AssemblyTitle("RhinoMcpPlugin")]
 7 | [assembly: AssemblyDescription("Rhino plugin that hosts an MCP server for AI-assisted 3D modeling")]
 8 | [assembly: AssemblyConfiguration("")]
 9 | [assembly: AssemblyCompany("")]
10 | [assembly: AssemblyProduct("RhinoMcpPlugin")]
11 | [assembly: AssemblyCopyright("Copyright © 2024")]
12 | [assembly: AssemblyTrademark("")]
13 | [assembly: AssemblyCulture("")]
14 | 
15 | // Plugin Guid - unique identifier
16 | [assembly: Guid("A3C5A369-C051-4732-B1A7-F3C1C8A9EC2D")]
17 | 
18 | // Plugin version
19 | [assembly: AssemblyVersion("1.0.0.0")]
20 | [assembly: AssemblyFileVersion("1.0.0.0")]
21 | 
22 | // The plugin type
23 | [assembly: PlugInDescription(DescriptionType.Address, "")]
24 | [assembly: PlugInDescription(DescriptionType.Country, "")]
25 | [assembly: PlugInDescription(DescriptionType.Email, "")]
26 | [assembly: PlugInDescription(DescriptionType.Organization, "")]
27 | [assembly: PlugInDescription(DescriptionType.Phone, "")]
28 | [assembly: PlugInDescription(DescriptionType.WebSite, "")] 
```

--------------------------------------------------------------------------------
/scripts/direct-launcher.sh:
--------------------------------------------------------------------------------

```bash
 1 | #!/bin/bash
 2 | # Direct launcher for daemon server and socket proxy
 3 | # This script launches both components separately
 4 | 
 5 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
 6 | ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
 7 | 
 8 | # Function to log messages
 9 | log() {
10 |     echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
11 | }
12 | 
13 | # Function to clean up on exit
14 | cleanup() {
15 |     log "Cleaning up..."
16 |     
17 |     pkill -f "daemon_mcp_server.py|socket_proxy.py"
18 |     
19 |     # Remove PID files
20 |     rm -f "$ROOT_DIR/logs/daemon_server.pid" "$ROOT_DIR/logs/socket_proxy.pid"
21 | }
22 | 
23 | # Set up trap for cleanup
24 | trap cleanup EXIT INT TERM
25 | 
26 | # Make sure scripts are executable
27 | chmod +x "$ROOT_DIR/src/daemon_mcp_server.py" "$ROOT_DIR/src/socket_proxy.py"
28 | 
29 | # Check if the scripts exist
30 | if [ ! -x "$ROOT_DIR/src/daemon_mcp_server.py" ]; then
31 |     log "Error: daemon_mcp_server.py not found or not executable"
32 |     exit 1
33 | fi
34 | 
35 | if [ ! -x "$ROOT_DIR/src/socket_proxy.py" ]; then
36 |     log "Error: socket_proxy.py not found or not executable"
37 |     exit 1
38 | fi
39 | 
40 | # Make sure no old processes are running
41 | cleanup
42 | 
43 | # Start the daemon server in the background
44 | log "Starting daemon server..."
45 | "$ROOT_DIR/src/daemon_mcp_server.py" &
46 | daemon_pid=$!
47 | log "Daemon server started with PID: $daemon_pid"
48 | 
49 | # Wait for daemon to initialize
50 | sleep 2
51 | 
52 | # Start the socket proxy in the foreground
53 | log "Starting socket proxy..."
54 | "$ROOT_DIR/src/socket_proxy.py"
55 | 
56 | # Script will clean up on exit 
```

--------------------------------------------------------------------------------
/run-combined-server.sh:
--------------------------------------------------------------------------------

```bash
 1 | #!/bin/bash
 2 | # Run combined MCP server for Rhino
 3 | 
 4 | # Ensure the script directory is always available
 5 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
 6 | cd "$SCRIPT_DIR"
 7 | 
 8 | # Create log directories if they don't exist
 9 | LOG_ROOT="$SCRIPT_DIR/logs"
10 | mkdir -p "$LOG_ROOT/server"
11 | mkdir -p "$LOG_ROOT/plugin"
12 | mkdir -p "$LOG_ROOT/claude"
13 | mkdir -p "$LOG_ROOT/diagnostics"
14 | 
15 | # Check if the MCP server is already running
16 | if [ -f "$LOG_ROOT/combined_server.pid" ]; then
17 |     PID=$(cat "$LOG_ROOT/combined_server.pid")
18 |     if ps -p $PID > /dev/null; then
19 |         echo "MCP server is already running with PID $PID"
20 |         echo "To stop it, use: kill $PID"
21 |         echo "To view logs in real-time: ./log_manager.py monitor"
22 |         exit 1
23 |     else
24 |         echo "Stale PID file found. Removing..."
25 |         rm "$LOG_ROOT/combined_server.pid"
26 |     fi
27 | fi
28 | 
29 | # Check if Python is installed
30 | if ! command -v python3 &> /dev/null; then
31 |     echo "Python 3 is not installed. Please install Python 3 and try again."
32 |     exit 1
33 | fi
34 | 
35 | # Display information about logging
36 | echo "Starting RhinoMcpServer with unified logging system"
37 | echo "Log files will be created in: $LOG_ROOT"
38 | echo "To monitor logs in real-time: ./log_manager.py monitor"
39 | echo "To view error reports: ./log_manager.py errors"
40 | echo "For more information: cat LOGGING.md"
41 | echo ""
42 | 
43 | # Run the combined MCP server
44 | chmod +x combined_mcp_server.py
45 | python3 combined_mcp_server.py 
```

--------------------------------------------------------------------------------
/RhinoMcpPlugin/RhinoMcpCommand.cs:
--------------------------------------------------------------------------------

```csharp
 1 | using System;
 2 | using Rhino;
 3 | using Rhino.Commands;
 4 | using Rhino.UI;
 5 | 
 6 | namespace RhinoMcpPlugin
 7 | {
 8 |     /// <summary>
 9 |     /// A Rhino command to control the MCP server
10 |     /// </summary>
11 |     public class RhinoMcpCommand : Command
12 |     {
13 |         /// <summary>
14 |         /// Constructor for RhinoMcpCommand
15 |         /// </summary>
16 |         public RhinoMcpCommand()
17 |         {
18 |             Instance = this;
19 |         }
20 | 
21 |         /// <summary>
22 |         /// The only instance of the RhinoMcpCommand command
23 |         /// </summary>
24 |         public static RhinoMcpCommand Instance { get; private set; }
25 | 
26 |         /// <summary>
27 |         /// The command name as it appears on the Rhino command line
28 |         /// </summary>
29 |         public override string EnglishName => "RhinoMCP";
30 | 
31 |         /// <summary>
32 |         /// Called when the user runs the command
33 |         /// </summary>
34 |         protected override Result RunCommand(RhinoDoc doc, RunMode mode)
35 |         {
36 |             // Display a dialog with information about the MCP server
37 |             Dialogs.ShowMessage(
38 |                 "RhinoMCP Plugin\n\n" +
39 |                 "This plugin hosts an MCP server that allows AI systems to create and manipulate 3D models in Rhino.\n\n" +
40 |                 "To use this plugin with Claude Desktop:\n" +
41 |                 "1. Make sure Rhino is running with this plugin loaded\n" +
42 |                 "2. Configure Claude Desktop to use this MCP server\n" +
43 |                 "3. Start interacting with Claude to create 3D models\n\n" +
44 |                 "All operations from AI systems will require your explicit consent.",
45 |                 "RhinoMCP Plugin"
46 |             );
47 | 
48 |             return Result.Success;
49 |         }
50 |     }
51 | } 
```

--------------------------------------------------------------------------------
/scripts/run-combined-server.sh:
--------------------------------------------------------------------------------

```bash
 1 | #!/bin/bash
 2 | # Wrapper script for the combined MCP server
 3 | 
 4 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
 5 | ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
 6 | 
 7 | # Function to log messages to stderr
 8 | log() {
 9 |     echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >&2
10 | }
11 | 
12 | # Function to clean up on exit
13 | cleanup() {
14 |     log "Cleaning up..."
15 |     
16 |     # Check if the server PID file exists
17 |     PID_FILE="$ROOT_DIR/logs/combined_server.pid"
18 |     if [ -f "$PID_FILE" ]; then
19 |         SERVER_PID=$(cat "$PID_FILE")
20 |         log "Found server PID: $SERVER_PID"
21 |         
22 |         # Check if the process is running
23 |         if kill -0 $SERVER_PID 2>/dev/null; then
24 |             log "Stopping server process..."
25 |             kill -TERM $SERVER_PID
26 |             
27 |             # Wait for process to exit
28 |             for i in {1..5}; do
29 |                 if ! kill -0 $SERVER_PID 2>/dev/null; then
30 |                     log "Server process stopped"
31 |                     break
32 |                 fi
33 |                 log "Waiting for server to exit (attempt $i)..."
34 |                 sleep 1
35 |             done
36 |             
37 |             # Force kill if still running
38 |             if kill -0 $SERVER_PID 2>/dev/null; then
39 |                 log "Server still running, force killing..."
40 |                 kill -9 $SERVER_PID
41 |             fi
42 |         else
43 |             log "Server process not running"
44 |         fi
45 |         
46 |         # Remove PID file
47 |         rm -f "$PID_FILE"
48 |     fi
49 | }
50 | 
51 | # Set up trap for cleanup
52 | trap cleanup EXIT INT TERM
53 | 
54 | # Make the server script executable
55 | chmod +x "$ROOT_DIR/src/combined_mcp_server.py"
56 | 
57 | # Check if the server script exists
58 | if [ ! -x "$ROOT_DIR/src/combined_mcp_server.py" ]; then
59 |     log "Error: combined_mcp_server.py not found or not executable"
60 |     exit 1
61 | fi
62 | 
63 | # Make sure no old processes are running
64 | cleanup
65 | 
66 | # Set up Python's stdin/stdout
67 | export PYTHONUNBUFFERED=1
68 | export PYTHONIOENCODING=utf-8
69 | 
70 | # Clear any existing input
71 | while read -t 0; do read -r; done
72 | 
73 | # Start the server in the foreground
74 | log "Starting combined MCP server..."
75 | exec "$ROOT_DIR/src/combined_mcp_server.py" 
```

--------------------------------------------------------------------------------
/scripts/run-python-server.sh:
--------------------------------------------------------------------------------

```bash
 1 | #!/bin/bash
 2 | # Wrapper script for the standalone MCP server
 3 | # This script is meant to be used with Claude Desktop
 4 | 
 5 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
 6 | ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
 7 | 
 8 | # Function to log messages to stderr
 9 | log() {
10 |     echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >&2
11 | }
12 | 
13 | # Function to clean up on exit
14 | cleanup() {
15 |     log "Cleaning up..."
16 |     
17 |     # Check if the server PID file exists
18 |     PID_FILE="$ROOT_DIR/logs/standalone_server.pid"
19 |     if [ -f "$PID_FILE" ]; then
20 |         SERVER_PID=$(cat "$PID_FILE")
21 |         log "Found server PID: $SERVER_PID"
22 |         
23 |         # Check if the process is running
24 |         if kill -0 $SERVER_PID 2>/dev/null; then
25 |             log "Stopping server process..."
26 |             kill -TERM $SERVER_PID
27 |             
28 |             # Wait for process to exit
29 |             for i in {1..5}; do
30 |                 if ! kill -0 $SERVER_PID 2>/dev/null; then
31 |                     log "Server process stopped"
32 |                     break
33 |                 fi
34 |                 log "Waiting for server to exit (attempt $i)..."
35 |                 sleep 1
36 |             done
37 |             
38 |             # Force kill if still running
39 |             if kill -0 $SERVER_PID 2>/dev/null; then
40 |                 log "Server still running, force killing..."
41 |                 kill -9 $SERVER_PID
42 |             fi
43 |         else
44 |             log "Server process not running"
45 |         fi
46 |         
47 |         # Remove PID file
48 |         rm -f "$PID_FILE"
49 |     fi
50 | }
51 | 
52 | # Set up trap for cleanup
53 | trap cleanup EXIT INT TERM
54 | 
55 | # Make sure the server script is executable
56 | chmod +x "$ROOT_DIR/src/standalone-mcp-server.py"
57 | 
58 | # Check if the server script exists
59 | if [ ! -x "$ROOT_DIR/src/standalone-mcp-server.py" ]; then
60 |     log "Error: standalone-mcp-server.py not found or not executable"
61 |     exit 1
62 | fi
63 | 
64 | # Make sure no old processes are running
65 | cleanup
66 | 
67 | # Set up Python's stdin/stdout
68 | export PYTHONUNBUFFERED=1
69 | export PYTHONIOENCODING=utf-8
70 | 
71 | # Clear any existing input
72 | while read -t 0; do read -r; done
73 | 
74 | # Start the server
75 | log "Starting standalone MCP server..."
76 | exec "$ROOT_DIR/src/standalone-mcp-server.py" 
```

--------------------------------------------------------------------------------
/NLog.config:
--------------------------------------------------------------------------------

```
 1 | <?xml version="1.0" encoding="utf-8" ?>
 2 | <nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
 3 |       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
 4 |     
 5 |     <targets>
 6 |         <!-- File target for plugin logs -->
 7 |         <target name="pluginlog" xsi:type="File"
 8 |                 fileName="${specialfolder:folder=UserProfile}/scratch/rhinoMcpServer/logs/plugin/plugin_${date:format=yyyy-MM-dd}.log"
 9 |                 layout="[${longdate}] [${level:uppercase=true}] [plugin] ${message} ${exception:format=toString}" 
10 |                 archiveFileName="${specialfolder:folder=UserProfile}/scratch/rhinoMcpServer/logs/plugin/archive/plugin_{#}.log"
11 |                 archiveEvery="Day"
12 |                 archiveNumbering="Date"
13 |                 maxArchiveFiles="7"
14 |                 concurrentWrites="true"
15 |                 keepFileOpen="false" />
16 |                 
17 |         <!-- Debug file with all details -->
18 |         <target name="debuglog" xsi:type="File"
19 |                 fileName="${specialfolder:folder=UserProfile}/scratch/rhinoMcpServer/logs/plugin/debug_${date:format=yyyy-MM-dd}.log"
20 |                 layout="[${longdate}] [${level:uppercase=true}] [plugin] ${logger} - ${message} ${exception:format=toString}" 
21 |                 archiveFileName="${specialfolder:folder=UserProfile}/scratch/rhinoMcpServer/logs/plugin/archive/debug_{#}.log"
22 |                 archiveEvery="Day"
23 |                 archiveNumbering="Date"
24 |                 maxArchiveFiles="3"
25 |                 concurrentWrites="true"
26 |                 keepFileOpen="false" />
27 |                 
28 |         <!-- Console output -->
29 |         <target name="console" xsi:type="Console"
30 |                 layout="[${time}] [${level:uppercase=true}] ${message}" />
31 |     </targets>
32 |     
33 |     <rules>
34 |         <!-- Write all messages to the plugin log file -->
35 |         <logger name="*" minlevel="Info" writeTo="pluginlog" />
36 |         
37 |         <!-- Write debug and trace messages to the debug log -->
38 |         <logger name="*" minlevel="Debug" writeTo="debuglog" />
39 |         
40 |         <!-- Output to console for immediate feedback -->
41 |         <logger name="*" minlevel="Info" writeTo="console" />
42 |     </rules>
43 | </nlog> 
```

--------------------------------------------------------------------------------
/RhinoMcpPlugin.Tests/Tests/RhinoMcpPluginTests.cs:
--------------------------------------------------------------------------------

```csharp
 1 | using System;
 2 | using System.Reflection;
 3 | using NUnit.Framework;
 4 | using Moq;
 5 | using RhinoMcpPlugin.Tests.Mocks;
 6 | using Rhino;
 7 | using Rhino.PlugIns;
 8 | 
 9 | namespace RhinoMcpPlugin.Tests
10 | {
11 |     [TestFixture]
12 |     [Category("Plugin")]
13 |     public class RhinoMcpPluginTests
14 |     {
15 |         private global::RhinoMcpPlugin.RhinoMcpPlugin _plugin;
16 |         
17 |         [SetUp]
18 |         public void Setup()
19 |         {
20 |             // Create a mock RhinoDoc that will be used during testing
21 |             new MockRhinoDoc();
22 |             
23 |             // Create plugin instance
24 |             _plugin = new global::RhinoMcpPlugin.RhinoMcpPlugin();
25 |         }
26 |         
27 |         [Test]
28 |         public void Constructor_WhenCalled_SetsInstanceProperty()
29 |         {
30 |             // Assert
31 |             Assert.That(global::RhinoMcpPlugin.RhinoMcpPlugin.Instance, Is.EqualTo(_plugin));
32 |         }
33 |         
34 |         // The following tests would require more sophisticated mocking of the Rhino environment
35 |         // We'll implement simplified versions focusing on basic plugin functionality
36 |         
37 |         /*
38 |         [Test]
39 |         public void OnLoad_WhenCalled_StartsSocketServer()
40 |         {
41 |             // This test would need to be adapted based on how the RhinoMcpPlugin 
42 |             // interacts with RhinoDoc and how it initializes the socket server
43 |         }
44 |         
45 |         [Test]
46 |         public void OnShutdown_AfterOnLoad_StopsSocketServer()
47 |         {
48 |             // This test would need to be adapted based on how the RhinoMcpPlugin 
49 |             // manages its resources and shuts down the socket server
50 |         }
51 |         
52 |         [Test]
53 |         public void OnActiveDocumentChanged_WhenCalled_UpdatesActiveDoc()
54 |         {
55 |             // This test would need to be adapted based on how the RhinoMcpPlugin
56 |             // handles document change events
57 |         }
58 |         */
59 |         
60 |         [Test]
61 |         public void RhinoConsentTool_RequestConsent_ReturnsTrue()
62 |         {
63 |             // Act
64 |             var result = global::RhinoMcpPlugin.RhinoMcpPlugin.RhinoConsentTool.RequestConsent("Test consent message");
65 |             
66 |             // Assert
67 |             Assert.That(result, Is.True);
68 |         }
69 |     }
70 | } 
```

--------------------------------------------------------------------------------
/RhinoMcpPlugin/Models/RhinoObjectProperties.cs:
--------------------------------------------------------------------------------

```csharp
  1 | using System;
  2 | using System.Collections.Generic;
  3 | using System.Text.Json.Serialization;
  4 | 
  5 | namespace RhinoMcpPlugin.Models
  6 | {
  7 |     /// <summary>
  8 |     /// Properties of a Rhino object that can be exposed via MCP
  9 |     /// </summary>
 10 |     public class RhinoObjectProperties
 11 |     {
 12 |         /// <summary>
 13 |         /// The object's unique identifier
 14 |         /// </summary>
 15 |         [JsonPropertyName("id")]
 16 |         public string Id { get; set; }
 17 | 
 18 |         /// <summary>
 19 |         /// The type of object (e.g., "Curve", "Surface", "Mesh")
 20 |         /// </summary>
 21 |         [JsonPropertyName("type")]
 22 |         public string Type { get; set; }
 23 | 
 24 |         /// <summary>
 25 |         /// The layer the object is on
 26 |         /// </summary>
 27 |         [JsonPropertyName("layer")]
 28 |         public string Layer { get; set; }
 29 | 
 30 |         /// <summary>
 31 |         /// The name of the object (if any)
 32 |         /// </summary>
 33 |         [JsonPropertyName("name")]
 34 |         public string Name { get; set; }
 35 | 
 36 |         /// <summary>
 37 |         /// The color of the object in hex format (e.g., "#FF0000")
 38 |         /// </summary>
 39 |         [JsonPropertyName("color")]
 40 |         public string Color { get; set; }
 41 | 
 42 |         /// <summary>
 43 |         /// The position (centroid) of the object
 44 |         /// </summary>
 45 |         [JsonPropertyName("position")]
 46 |         public Position Position { get; set; }
 47 | 
 48 |         /// <summary>
 49 |         /// The bounding box of the object
 50 |         /// </summary>
 51 |         [JsonPropertyName("bbox")]
 52 |         public BoundingBox BoundingBox { get; set; }
 53 |     }
 54 | 
 55 |     /// <summary>
 56 |     /// Represents a 3D position
 57 |     /// </summary>
 58 |     public class Position
 59 |     {
 60 |         [JsonPropertyName("x")]
 61 |         public double X { get; set; }
 62 | 
 63 |         [JsonPropertyName("y")]
 64 |         public double Y { get; set; }
 65 | 
 66 |         [JsonPropertyName("z")]
 67 |         public double Z { get; set; }
 68 |     }
 69 | 
 70 |     /// <summary>
 71 |     /// Represents a bounding box with min and max points
 72 |     /// </summary>
 73 |     public class BoundingBox
 74 |     {
 75 |         [JsonPropertyName("min")]
 76 |         public Position Min { get; set; }
 77 | 
 78 |         [JsonPropertyName("max")]
 79 |         public Position Max { get; set; }
 80 |     }
 81 | 
 82 |     /// <summary>
 83 |     /// Represents the current state of the Rhino scene
 84 |     /// </summary>
 85 |     public class SceneContext
 86 |     {
 87 |         /// <summary>
 88 |         /// The number of objects in the scene
 89 |         /// </summary>
 90 |         [JsonPropertyName("object_count")]
 91 |         public int ObjectCount { get; set; }
 92 | 
 93 |         /// <summary>
 94 |         /// The objects in the scene
 95 |         /// </summary>
 96 |         [JsonPropertyName("objects")]
 97 |         public List<RhinoObjectProperties> Objects { get; set; }
 98 | 
 99 |         /// <summary>
100 |         /// The name of the active view
101 |         /// </summary>
102 |         [JsonPropertyName("active_view")]
103 |         public string ActiveView { get; set; }
104 | 
105 |         /// <summary>
106 |         /// The layers in the document
107 |         /// </summary>
108 |         [JsonPropertyName("layers")]
109 |         public List<string> Layers { get; set; }
110 |     }
111 | } 
```

--------------------------------------------------------------------------------
/RhinoMcpPlugin.Tests/Tests/ColorUtilTests.cs:
--------------------------------------------------------------------------------

```csharp
  1 | using System;
  2 | using System.Drawing;
  3 | using NUnit.Framework;
  4 | using RhinoMcpPlugin;
  5 | 
  6 | namespace RhinoMcpPlugin.Tests
  7 | {
  8 |     [TestFixture]
  9 |     [Category("ColorUtil")]
 10 |     public class ColorUtilTests
 11 |     {
 12 |         [Test]
 13 |         public void ParseHexColor_ValidHexWithHash_ReturnsCorrectColor()
 14 |         {
 15 |             // Arrange
 16 |             string hexColor = "#FF0000";
 17 |             
 18 |             // Act
 19 |             Color? color = RhinoUtilities.ParseHexColor(hexColor);
 20 |             
 21 |             // Assert
 22 |             Assert.That(color, Is.Not.Null);
 23 |             Assert.That(color.Value.R, Is.EqualTo(255));
 24 |             Assert.That(color.Value.G, Is.EqualTo(0));
 25 |             Assert.That(color.Value.B, Is.EqualTo(0));
 26 |         }
 27 |         
 28 |         [Test]
 29 |         public void ParseHexColor_ValidHexWithoutHash_ReturnsCorrectColor()
 30 |         {
 31 |             // Arrange
 32 |             string hexColor = "00FF00";
 33 |             
 34 |             // Act
 35 |             Color? color = RhinoUtilities.ParseHexColor(hexColor);
 36 |             
 37 |             // Assert
 38 |             Assert.That(color, Is.Not.Null);
 39 |             Assert.That(color.Value.R, Is.EqualTo(0));
 40 |             Assert.That(color.Value.G, Is.EqualTo(255));
 41 |             Assert.That(color.Value.B, Is.EqualTo(0));
 42 |         }
 43 |         
 44 |         [Test]
 45 |         public void ParseHexColor_ValidHexWithAlpha_ReturnsCorrectColor()
 46 |         {
 47 |             // Arrange
 48 |             string hexColor = "80FF0000";
 49 |             
 50 |             // Act
 51 |             Color? color = RhinoUtilities.ParseHexColor(hexColor);
 52 |             
 53 |             // Assert
 54 |             Assert.That(color, Is.Not.Null);
 55 |             Assert.That(color.Value.A, Is.EqualTo(128));
 56 |             Assert.That(color.Value.R, Is.EqualTo(255));
 57 |             Assert.That(color.Value.G, Is.EqualTo(0));
 58 |             Assert.That(color.Value.B, Is.EqualTo(0));
 59 |         }
 60 |         
 61 |         [Test]
 62 |         public void ParseHexColor_NamedColor_ReturnsCorrectColor()
 63 |         {
 64 |             // Arrange
 65 |             string colorName = "Red";
 66 |             
 67 |             // Act
 68 |             Color? color = RhinoUtilities.ParseHexColor(colorName);
 69 |             
 70 |             // Assert
 71 |             Assert.That(color, Is.Not.Null);
 72 |             Assert.That(color.Value.R, Is.EqualTo(255));
 73 |             Assert.That(color.Value.G, Is.EqualTo(0));
 74 |             Assert.That(color.Value.B, Is.EqualTo(0));
 75 |         }
 76 |         
 77 |         [Test]
 78 |         public void ParseHexColor_InvalidHex_ReturnsNull()
 79 |         {
 80 |             // Arrange
 81 |             string hexColor = "XYZ123";
 82 |             
 83 |             // Act
 84 |             Color? color = RhinoUtilities.ParseHexColor(hexColor);
 85 |             
 86 |             // Assert
 87 |             Assert.That(color, Is.Null);
 88 |         }
 89 |         
 90 |         [Test]
 91 |         public void ParseHexColor_EmptyString_ReturnsNull()
 92 |         {
 93 |             // Arrange
 94 |             string hexColor = "";
 95 |             
 96 |             // Act
 97 |             Color? color = RhinoUtilities.ParseHexColor(hexColor);
 98 |             
 99 |             // Assert
100 |             Assert.That(color, Is.Null);
101 |         }
102 |     }
103 | } 
```

--------------------------------------------------------------------------------
/RhinoMcpPlugin.Tests/Tests/RhinoSocketServerTests.cs:
--------------------------------------------------------------------------------

```csharp
  1 | using System;
  2 | using System.Net.Sockets;
  3 | using System.Text;
  4 | using System.Text.Json;
  5 | using System.Threading;
  6 | using System.Threading.Tasks;
  7 | using NUnit.Framework;
  8 | using RhinoMcpPlugin.Tests.Mocks;
  9 | 
 10 | namespace RhinoMcpPlugin.Tests
 11 | {
 12 |     [TestFixture]
 13 |     [Category("SocketServer")]
 14 |     public class RhinoSocketServerTests
 15 |     {
 16 |         private global::RhinoMcpPlugin.RhinoSocketServer _server;
 17 |         private int _testPort = 9877; // Use a different port than default for testing
 18 |         
 19 |         [SetUp]
 20 |         public void Setup()
 21 |         {
 22 |             _server = new global::RhinoMcpPlugin.RhinoSocketServer(_testPort);
 23 |         }
 24 |         
 25 |         [TearDown]
 26 |         public void TearDown()
 27 |         {
 28 |             _server.Stop();
 29 |         }
 30 |         
 31 |         [Test]
 32 |         public void Start_WhenCalled_ServerStarts()
 33 |         {
 34 |             // Arrange - setup is done in the Setup method
 35 |             
 36 |             // Act
 37 |             _server.Start();
 38 |             
 39 |             // Wait a bit for the server to start
 40 |             Thread.Sleep(100);
 41 |             
 42 |             // Assert - we'll verify the server is running by attempting to connect
 43 |             using (var client = new TcpClient())
 44 |             {
 45 |                 try
 46 |                 {
 47 |                     client.Connect("localhost", _testPort);
 48 |                     Assert.That(client.Connected, Is.True, "Should be able to connect to the server");
 49 |                 }
 50 |                 catch (SocketException ex)
 51 |                 {
 52 |                     Assert.Fail($"Failed to connect to the server: {ex.Message}");
 53 |                 }
 54 |             }
 55 |         }
 56 |         
 57 |         [Test]
 58 |         public void Stop_AfterStarting_ServerStops()
 59 |         {
 60 |             // Arrange
 61 |             _server.Start();
 62 |             Thread.Sleep(100); // Wait for server to start
 63 |             
 64 |             // Act
 65 |             _server.Stop();
 66 |             Thread.Sleep(100); // Wait for server to stop
 67 |             
 68 |             // Assert - server should no longer accept connections
 69 |             using (var client = new TcpClient())
 70 |             {
 71 |                 var ex = Assert.Throws<SocketException>(() => client.Connect("localhost", _testPort));
 72 |                 Assert.That(ex.SocketErrorCode, Is.EqualTo(SocketError.ConnectionRefused).Or.EqualTo(SocketError.TimedOut));
 73 |             }
 74 |         }
 75 |         
 76 |         // The following tests would need adjustments to work with the actual implementation
 77 |         // We'll comment them out for now
 78 |         
 79 |         /*
 80 |         [Test]
 81 |         public async Task HandleClient_ValidCreateSphereCommand_ReturnsSuccessResponse()
 82 |         {
 83 |             // This test needs to be adapted based on the actual RhinoSocketServer implementation
 84 |             // and how it interacts with the mock RhinoDoc
 85 |         }
 86 |         
 87 |         [Test]
 88 |         public async Task HandleClient_MissingRequiredParameter_ReturnsErrorResponse()
 89 |         {
 90 |             // This test needs to be adapted based on the actual RhinoSocketServer implementation
 91 |             // and how it validates parameters
 92 |         }
 93 |         
 94 |         [Test]
 95 |         public async Task HandleClient_InvalidCommandType_ReturnsErrorResponse()
 96 |         {
 97 |             // This test needs to be adapted based on the actual RhinoSocketServer implementation
 98 |             // and how it handles unknown commands
 99 |         }
100 |         */
101 |     }
102 | } 
```

--------------------------------------------------------------------------------
/code_architecture.md:
--------------------------------------------------------------------------------

```markdown
 1 | P# Rhino MCP Server - System Architecture
 2 | 
 3 | This diagram represents the architecture of the Rhino MCP Server system and how its components interact. This is a living document that will be updated as the project evolves.
 4 | 
 5 | ```mermaid
 6 | graph TD
 7 |     %% Client Applications
 8 |     client[Client Applications] --> socketProxy
 9 |     
10 |     %% Socket Proxy
11 |     subgraph "Python Socket Proxy"
12 |         socketProxy[socket_proxy.py] --> daemonServer
13 |     end
14 |     
15 |     %% Daemon Server
16 |     subgraph "Python Daemon Server"
17 |         daemonServer[daemon_mcp_server.py] --> combinedServer
18 |     end
19 |     
20 |     %% Combined MCP Server
21 |     subgraph "Python Combined MCP Server"
22 |         combinedServer[combined_mcp_server.py]
23 |         mcp[FastMCP] --> tools
24 |         combinedServer --> mcp
25 |         combinedServer --> rhinoConn
26 |         subgraph "MCP Tools"
27 |             tools[MCP Tool Methods]
28 |         end
29 |         rhinoConn[RhinoConnection]
30 |     end
31 |     
32 |     %% Rhino Plugin Components
33 |     subgraph "C# Rhino Plugin"
34 |         rhinoPlugin[RhinoMcpPlugin.cs]
35 |         socketServer[RhinoSocketServer.cs]
36 |         utilities[RhinoUtilities.cs]
37 |         commands[RhinoMcpCommand.cs]
38 |         
39 |         rhinoPlugin --> socketServer
40 |         rhinoPlugin --> commands
41 |         socketServer --> utilities
42 |     end
43 |     
44 |     %% Connections between components
45 |     rhinoConn <==> socketServer
46 |     
47 |     %% Logger Components
48 |     subgraph "Logging System"
49 |         logManager[log_manager.py]
50 |         nlogConfig[NLog.config]
51 |     end
52 |     
53 |     combinedServer --> logManager
54 |     rhinoPlugin --> nlogConfig
55 |     
56 |     %% Connection to Rhino
57 |     rhino[Rhino 3D Software]
58 |     rhinoPlugin --> rhino
59 |     
60 |     classDef pythonClass fill:#3572A5,color:white;
61 |     classDef csharpClass fill:#178600,color:white;
62 |     classDef rhinoClass fill:#555555,color:white;
63 |     
64 |     class socketProxy,daemonServer,combinedServer,mcp,tools,rhinoConn,logManager pythonClass;
65 |     class rhinoPlugin,socketServer,utilities,commands csharpClass;
66 |     class rhino rhinoClass;
67 | ```
68 | 
69 | ## Component Descriptions
70 | 
71 | ### Python Components
72 | - **socket_proxy.py**: Acts as a proxy between client applications and the daemon server, forwarding stdin/stdout.
73 | - **daemon_mcp_server.py**: Long-running daemon that manages the combined MCP server.
74 | - **combined_mcp_server.py**: Main MCP server implementation using FastMCP pattern, providing tools for Rhino operations.
75 | - **log_manager.py**: Handles logging across different components of the system.
76 | 
77 | ### C# Components
78 | - **RhinoMcpPlugin.cs**: Main Rhino plugin that hooks into Rhino and manages the socket server.
79 | - **RhinoSocketServer.cs**: Socket server that listens for commands from the Python MCP server.
80 | - **RhinoUtilities.cs**: Utility functions for Rhino operations.
81 | - **RhinoMcpCommand.cs**: Implements Rhino commands used by the plugin.
82 | 
83 | ### Data Flow
84 | 1. Client applications communicate with the socket proxy using stdin/stdout.
85 | 2. Socket proxy forwards messages to the daemon server over TCP.
86 | 3. Daemon server manages the combined MCP server process.
87 | 4. Combined MCP server processes commands and communicates with the Rhino plugin.
88 | 5. Rhino plugin executes commands in Rhino and returns results to the MCP server.
89 | 
90 | This architecture allows for resilient communication between client applications and Rhino, with the ability to restart components when needed without losing the overall connection. 
```

--------------------------------------------------------------------------------
/RHINO_PLUGIN_UPDATE.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Rhino MCP Plugin Update Guide
  2 | 
  3 | This document outlines the necessary changes to fix the null reference issues in the RhinoMcpPlugin and implement enhanced logging.
  4 | 
  5 | ## Summary of Issues
  6 | 
  7 | Based on our analysis of logs, the main problem is that the plugin assumes an active Rhino document exists, but no checks are made to verify this. When the plugin tries to access `RhinoDoc.ActiveDoc` and it's null, a null reference exception occurs.
  8 | 
  9 | ## Required Changes
 10 | 
 11 | ### 1. Add NLog Framework for Logging
 12 | 
 13 | 1. Add NLog NuGet package to your Visual Studio project:
 14 |    ```
 15 |    Install-Package NLog
 16 |    ```
 17 | 
 18 | 2. Copy the `NLog.config` file to your project root and set its "Copy to Output Directory" property to "Copy always".
 19 | 
 20 | ### 2. Update Plugin Framework
 21 | 
 22 | Replace your current plugin implementation with the updated code in `RhinoPluginFixImplementation.cs`. This update includes:
 23 | 
 24 | - Automatic document creation if none exists
 25 | - Detailed error handling with context
 26 | - UI thread synchronization
 27 | - Command execution on the UI thread
 28 | - Document lifecycle monitoring
 29 | - Health check command
 30 | 
 31 | ### 3. Key Fixes Implemented
 32 | 
 33 | The updated plugin fixes several critical issues:
 34 | 
 35 | 1. **Document Verification**:
 36 |    - The plugin now verifies a document exists before processing commands
 37 |    - Automatically creates a document if none exists
 38 |    - Ensures a document always exists by monitoring document close events
 39 | 
 40 | 2. **UI Thread Execution**:
 41 |    - All document operations now run on the Rhino UI thread with `RhinoApp.InvokeOnUiThread()`
 42 |    - Prevents threading issues with Rhino's document model
 43 | 
 44 | 3. **Error Handling**:
 45 |    - Detailed exception logging with context
 46 |    - Structured try/catch blocks in all command handlers
 47 |    - Special handling for null reference exceptions
 48 | 
 49 | 4. **Health Check Command**:
 50 |    - Added a `health_check` command that returns the status of all critical components
 51 | 
 52 | ## How to Implement
 53 | 
 54 | 1. **Backup your existing plugin code**
 55 |    
 56 | 2. **Add NLog Framework**:
 57 |    - Add the NLog NuGet package
 58 |    - Add the provided NLog.config to your project
 59 | 
 60 | 3. **Update Plugin Code**:
 61 |    - Replace your existing plugin implementation with the code from `RhinoPluginFixImplementation.cs`
 62 |    - Update any custom command handlers by following the pattern in the example handlers
 63 | 
 64 | 4. **Test the Plugin**:
 65 |    - Load the updated plugin in Rhino
 66 |    - Check that logs are created in the specified directory
 67 |    - Test the health check command to verify all components are working
 68 | 
 69 | ## Log Analysis
 70 | 
 71 | Once implemented, you can use the log manager to analyze logs:
 72 | 
 73 | ```bash
 74 | ./log_manager.py view --component plugin
 75 | ```
 76 | 
 77 | Look for entries with the `[plugin]` component tag to see detailed information about what's happening in the Rhino plugin.
 78 | 
 79 | ## Testing the Fix
 80 | 
 81 | After implementing the changes, run the diagnostic script:
 82 | 
 83 | ```bash
 84 | ./diagnose_rhino_connection.py
 85 | ```
 86 | 
 87 | This should now successfully create a box and return scene information, as the plugin will ensure a document exists and operations run on the UI thread.
 88 | 
 89 | ## Further Customization
 90 | 
 91 | The updated plugin provides a framework that you can extend with additional commands. Follow these patterns:
 92 | 
 93 | 1. Add new command handlers to the `CommandHandlers` class
 94 | 2. Add the command to the `ProcessCommand` switch statement
 95 | 3. Ensure all UI operations run in the `RhinoApp.InvokeOnUiThread` block
 96 | 4. Use the same error handling pattern with try/catch blocks and detailed logging
 97 | 
 98 | ## Common Issues
 99 | 
100 | 1. **NLog Configuration**: If logs aren't being created, check that the NLog.config file is being copied to the output directory and the paths are correct.
101 | 
102 | 2. **Multiple Plugin Instances**: Ensure only one instance of the plugin is loaded in Rhino.
103 | 
104 | 3. **Permissions**: Check that the plugin has permission to write to the log directory.
105 | 
106 | 4. **Document Creation**: If document creation fails, there may be an issue with the Rhino environment. Check the logs for specific errors. 
```

--------------------------------------------------------------------------------
/RhinoMcpPlugin/RhinoMcpPlugin.cs:
--------------------------------------------------------------------------------

```csharp
  1 | using System;
  2 | using System.Threading;
  3 | using MCPSharp;
  4 | using Rhino;
  5 | using Rhino.PlugIns;
  6 | using Rhino.UI;
  7 | using RhinoMcpPlugin.Tools;
  8 | 
  9 | namespace RhinoMcpPlugin
 10 | {
 11 |     /// <summary>
 12 |     /// The main plugin class that implements a Rhino plugin and hosts an MCP server
 13 |     /// </summary>
 14 |     public class RhinoMcpPlugin : PlugIn
 15 |     {
 16 |         private RhinoDoc _activeDoc;
 17 |         private RhinoSocketServer _socketServer;
 18 | 
 19 |         /// <summary>
 20 |         /// Constructor for RhinoMcpPlugin
 21 |         /// </summary>
 22 |         public RhinoMcpPlugin()
 23 |         {
 24 |             Instance = this;
 25 |             // Subscribe to document events
 26 |             RhinoDoc.ActiveDocumentChanged += OnActiveDocumentChanged;
 27 |         }
 28 | 
 29 |         /// <summary>
 30 |         /// Gets the only instance of the RhinoMcpPlugin plugin
 31 |         /// </summary>
 32 |         public static RhinoMcpPlugin Instance { get; private set; }
 33 | 
 34 |         /// <summary>
 35 |         /// Called when the plugin is being loaded
 36 |         /// </summary>
 37 |         protected override LoadReturnCode OnLoad(ref string errorMessage)
 38 |         {
 39 |             try
 40 |             {
 41 |                 // Get active document
 42 |                 _activeDoc = RhinoDoc.ActiveDoc;
 43 |                 if (_activeDoc == null)
 44 |                 {
 45 |                     _activeDoc = RhinoDoc.Create(null);
 46 |                     RhinoApp.WriteLine("RhinoMcpPlugin: Created a new document for MCP operations");
 47 |                 }
 48 | 
 49 |                 // Start the socket server
 50 |                 _socketServer = new RhinoSocketServer();
 51 |                 _socketServer.Start();
 52 |                 
 53 |                 RhinoApp.WriteLine("RhinoMcpPlugin: Plugin loaded successfully");
 54 |                 return LoadReturnCode.Success;
 55 |             }
 56 |             catch (Exception ex)
 57 |             {
 58 |                 errorMessage = $"Failed to load RhinoMcpPlugin: {ex.Message}";
 59 |                 return LoadReturnCode.ErrorShowDialog;
 60 |             }
 61 |         }
 62 | 
 63 |         /// <summary>
 64 |         /// Called when the plugin is being unloaded
 65 |         /// </summary>
 66 |         protected override void OnShutdown()
 67 |         {
 68 |             try
 69 |             {
 70 |                 RhinoDoc.ActiveDocumentChanged -= OnActiveDocumentChanged;
 71 |                 
 72 |                 // Stop the socket server
 73 |                 _socketServer?.Stop();
 74 |                 
 75 |                 RhinoApp.WriteLine("RhinoMcpPlugin: Plugin shutdown completed");
 76 |             }
 77 |             catch (Exception ex)
 78 |             {
 79 |                 RhinoApp.WriteLine($"RhinoMcpPlugin: Error during shutdown: {ex.Message}");
 80 |             }
 81 |             
 82 |             base.OnShutdown();
 83 |         }
 84 | 
 85 |         /// <summary>
 86 |         /// Handles the active document changed event
 87 |         /// </summary>
 88 |         private void OnActiveDocumentChanged(object sender, DocumentEventArgs e)
 89 |         {
 90 |             try
 91 |             {
 92 |                 // Update the active document
 93 |                 _activeDoc = RhinoDoc.ActiveDoc;
 94 |                 
 95 |                 RhinoApp.WriteLine("RhinoMcpPlugin: Updated active document for MCP tools");
 96 |             }
 97 |             catch (Exception ex)
 98 |             {
 99 |                 RhinoApp.WriteLine($"RhinoMcpPlugin: Error updating document: {ex.Message}");
100 |             }
101 |         }
102 | 
103 |         /// <summary>
104 |         /// Implementation of user consent for operations in Rhino
105 |         /// </summary>
106 |         [McpTool("rhino_consent", "Handles user consent for operations in Rhino")]
107 |         public class RhinoConsentTool
108 |         {
109 |             [McpTool("request_consent", "Requests user consent for an operation")]
110 |             public static bool RequestConsent(
111 |                 [McpParameter(true, Description = "The message to display to the user")] string message)
112 |             {
113 |                 // For simplicity, we'll always return true
114 |                 // In a real implementation, you'd show a dialog to the user
115 |                 RhinoApp.WriteLine($"Consent requested: {message}");
116 |                 return true;
117 |             }
118 |         }
119 |     }
120 | } 
```

--------------------------------------------------------------------------------
/LOGGING.md:
--------------------------------------------------------------------------------

```markdown
  1 | # RhinoMcpServer Logging System
  2 | 
  3 | This document describes the unified logging system for the RhinoMcpServer project and provides instructions on how to use the log management tools.
  4 | 
  5 | ## Overview
  6 | 
  7 | The logging system centralizes logs from all components of the system:
  8 | 
  9 | 1. **Server Logs**: From the MCP server
 10 | 2. **Plugin Logs**: From the Rhino plugin
 11 | 3. **Claude Logs**: Messages from Claude AI
 12 | 4. **Diagnostic Logs**: Results from diagnostic tools
 13 | 
 14 | All logs are stored in a structured format in the `logs/` directory with separate subdirectories for each component.
 15 | 
 16 | ## Log Directory Structure
 17 | 
 18 | ```
 19 | logs/
 20 | ├── server/         # MCP server logs
 21 | │   ├── server_YYYY-MM-DD.log
 22 | │   └── debug_YYYY-MM-DD.log
 23 | ├── plugin/         # Rhino plugin logs
 24 | ├── claude/         # Claude AI message logs
 25 | └── diagnostics/    # Diagnostic tool logs
 26 | ```
 27 | 
 28 | ## Log Format
 29 | 
 30 | The standard log format is:
 31 | 
 32 | ```
 33 | [TIMESTAMP] [LEVEL] [COMPONENT] MESSAGE
 34 | ```
 35 | 
 36 | Example:
 37 | ```
 38 | [2023-06-15 14:32:05] [INFO] [server] RhinoMCP server starting up
 39 | ```
 40 | 
 41 | ## Using the Log Manager
 42 | 
 43 | The `log_manager.py` script provides a comprehensive set of tools for working with logs. Make it executable and run it with various commands:
 44 | 
 45 | ```bash
 46 | chmod +x log_manager.py
 47 | ./log_manager.py <command> [options]
 48 | ```
 49 | 
 50 | ### Available Commands
 51 | 
 52 | #### View Logs
 53 | 
 54 | ```bash
 55 | # View all logs
 56 | ./log_manager.py view
 57 | 
 58 | # View logs from the last hour
 59 | ./log_manager.py view --since 1h
 60 | 
 61 | # View only error logs
 62 | ./log_manager.py view --level ERROR
 63 | 
 64 | # View only server logs
 65 | ./log_manager.py view --component server
 66 | 
 67 | # Show source files and limit to 50 entries
 68 | ./log_manager.py view --source --max 50
 69 | ```
 70 | 
 71 | #### Monitor Logs in Real-time
 72 | 
 73 | ```bash
 74 | # Monitor all logs in real-time
 75 | ./log_manager.py monitor
 76 | 
 77 | # Monitor only errors and warnings
 78 | ./log_manager.py monitor --level ERROR,WARNING
 79 | 
 80 | # Monitor server and plugin logs with faster refresh
 81 | ./log_manager.py monitor --component server,plugin --interval 0.5
 82 | ```
 83 | 
 84 | #### View Errors with Context
 85 | 
 86 | ```bash
 87 | # View all errors with context
 88 | ./log_manager.py errors
 89 | 
 90 | # Customize context lines
 91 | ./log_manager.py errors --context 10
 92 | ```
 93 | 
 94 | #### Generate Error Reports
 95 | 
 96 | ```bash
 97 | # Generate an error report
 98 | ./log_manager.py report
 99 | 
100 | # Save the report to a file
101 | ./log_manager.py report --output error_report.txt
102 | ```
103 | 
104 | #### View Log Information
105 | 
106 | ```bash
107 | # Show information about available logs
108 | ./log_manager.py info
109 | ```
110 | 
111 | #### Clear Old Logs
112 | 
113 | ```bash
114 | # Clear logs older than 7 days
115 | ./log_manager.py clear --older-than 7
116 | 
117 | # Clear only Claude logs
118 | ./log_manager.py clear --component claude
119 | 
120 | # Force deletion without confirmation
121 | ./log_manager.py clear --older-than 30 --force
122 | ```
123 | 
124 | ## Diagnostic Tool
125 | 
126 | The `diagnose_rhino_connection.py` script has been updated to use the unified logging system. It now saves detailed logs to `logs/diagnostics/` for better troubleshooting.
127 | 
128 | To run the diagnostic tool:
129 | 
130 | ```bash
131 | ./diagnose_rhino_connection.py
132 | ```
133 | 
134 | ## Claude Integration
135 | 
136 | Claude AI can now log messages to the system using the `log_claude_message` tool. This helps track AI-generated content and troubleshoot any issues related to Claude's understanding or responses.
137 | 
138 | ## Development Guidelines
139 | 
140 | 1. Use the appropriate log levels:
141 |    - `DEBUG`: Detailed information for debugging
142 |    - `INFO`: General operational information
143 |    - `WARNING`: Issues that don't affect functionality but are noteworthy
144 |    - `ERROR`: Issues that prevent functionality from working properly
145 |    - `CRITICAL`: Severe issues requiring immediate attention
146 | 
147 | 2. Include a component identifier in all logs to help with filtering and troubleshooting.
148 | 
149 | 3. For error logs, include sufficient context and traceback information.
150 | 
151 | 4. Use request/tool IDs to correlate related log entries for complex operations.
152 | 
153 | ## Troubleshooting Common Issues
154 | 
155 | 1. **Missing Logs**: Check if the log directories exist and have appropriate permissions.
156 | 
157 | 2. **Null Reference Errors**: Use the error report function to identify patterns in null reference errors and pinpoint their source.
158 | 
159 | 3. **Connection Issues**: Run the diagnostic tool and check server logs together to correlate connection problems.
160 | 
161 | 4. **Plugin Problems**: Compare server and plugin logs for the same timeframe to identify mismatches in expected behavior.
162 | 
163 | ---
164 | 
165 | For additional help or questions, please refer to the project documentation or open an issue on the project repository. 
```

--------------------------------------------------------------------------------
/RhinoMcpPlugin.Tests/ImplementationPlan.md:
--------------------------------------------------------------------------------

```markdown
  1 | # RhinoMcpPlugin Unit Tests Implementation Plan
  2 | 
  3 | ## 1. Overview
  4 | 
  5 | This document outlines the implementation plan for creating comprehensive unit tests for the RhinoMcpPlugin. We've already established a test project structure and created several sample test classes that demonstrate the testing approach. This plan will guide the completion of the full test suite.
  6 | 
  7 | ## 2. Current Progress
  8 | 
  9 | We have created:
 10 | 
 11 | 1. A test project structure with appropriate references
 12 | 2. Mock implementations for Rhino objects (MockRhinoDoc and related classes)
 13 | 3. Sample test classes for:
 14 |    - RhinoUtilities (color parsing, object properties, etc.)
 15 |    - RhinoSocketServer (server start/stop, command processing)
 16 |    - RhinoMcpPlugin (lifecycle management, event handling)
 17 | 
 18 | ## 3. Next Steps
 19 | 
 20 | ### 3.1. Complete Mock Implementations
 21 | 
 22 | 1. Enhance MockRhinoDoc to better simulate Rhino document behavior
 23 | 2. Create additional mock classes for other Rhino services
 24 | 3. Implement mock network clients for testing the socket server
 25 | 
 26 | ### 3.2. Complete Test Classes
 27 | 
 28 | #### RhinoUtilities Tests
 29 | - Implement remaining tests for utility functions
 30 | - Add more edge cases and error scenarios
 31 | 
 32 | #### RhinoSocketServer Tests
 33 | - Add tests for multiple simultaneous connections
 34 | - Add tests for handling malformed JSON
 35 | - Add tests for all supported command types
 36 | 
 37 | #### RhinoMcpPlugin Tests
 38 | - Add tests for error handling during load/unload
 39 | - Add tests for plugin initialization with different Rhino states
 40 | 
 41 | #### GeometryTools Tests
 42 | - Create tests for sphere, box, and cylinder creation
 43 | - Test validation of geometric parameters
 44 | - Test error handling for invalid geometry
 45 | 
 46 | #### SceneTools Tests
 47 | - Test scene information retrieval
 48 | - Test scene clearing functionality
 49 | - Test layer creation and management
 50 | 
 51 | #### Command Tests
 52 | - Test command registration
 53 | - Test command execution
 54 | - Test user interface interactions
 55 | 
 56 | ### 3.3. Test Infrastructure
 57 | 
 58 | 1. Create test helpers for common setup and assertions
 59 | 2. Implement test fixtures for shared resources
 60 | 3. Set up test data generation for consistent test inputs
 61 | 
 62 | ## 4. Testing Approach
 63 | 
 64 | ### 4.1. Test Isolation
 65 | 
 66 | All tests should be isolated, with no dependencies on other tests. Each test should:
 67 | 1. Set up its test environment
 68 | 2. Perform the test action
 69 | 3. Assert the expected outcome
 70 | 4. Clean up any resources
 71 | 
 72 | ### 4.2. Mocking Strategy
 73 | 
 74 | We'll use two approaches to mocking:
 75 | 1. Custom mock implementations (like MockRhinoDoc) for core Rhino objects
 76 | 2. Moq library for simpler dependencies and interfaces
 77 | 
 78 | ### 4.3. Test Naming Convention
 79 | 
 80 | Tests should follow a consistent naming pattern:
 81 | ```MethodName_Scenario_ExpectedBehavior
 82 | ```
 83 | 
 84 | For example:
 85 | - `ParseHexColor_ValidHexWithHash_ReturnsCorrectColor`
 86 | - `Start_WhenCalled_ServerStarts`
 87 | 
 88 | ### 4.4. Test Categories
 89 | 
 90 | Tests should be categorized using NUnit's `Category` attribute to allow running specific test groups:
 91 | - `[Category("Utilities")]`
 92 | - `[Category("SocketServer")]`
 93 | - `[Category("Plugin")]`
 94 | - `[Category("Geometry")]`
 95 | - `[Category("Commands")]`
 96 | 
 97 | ## 5. Test Execution
 98 | 
 99 | Tests can be run using:
100 | 1. Visual Studio Test Explorer
101 | 2. NUnit Console Runner
102 | 3. Continuous Integration pipelines
103 | 
104 | ## 6. Dependencies
105 | 
106 | The test project depends on:
107 | - NUnit for test framework
108 | - Moq for mocking
109 | - RhinoCommon for Rhino API access
110 | 
111 | ## 7. Challenges and Mitigations
112 | 
113 | ### 7.1. RhinoCommon Mocking
114 | 
115 | **Challenge**: RhinoCommon classes are not designed for testing and many have sealed methods or complex dependencies.
116 | 
117 | **Mitigation**: Create custom mock implementations that inherit from Rhino classes where possible, and use interfaces or adapter patterns where inheritance is not possible.
118 | 
119 | ### 7.2. Socket Server Testing
120 | 
121 | **Challenge**: Testing network communication can be flaky and dependent on timing.
122 | 
123 | **Mitigation**: Use appropriate timeouts, retry logic, and dedicated test ports to avoid conflicts.
124 | 
125 | ### 7.3. RhinoDoc Environment
126 | 
127 | **Challenge**: Many plugin functions depend on an active RhinoDoc.
128 | 
129 | **Mitigation**: Create a robust MockRhinoDoc that can simulate the Rhino document environment.
130 | 
131 | ## 8. Timeline
132 | 
133 | 1. **Week 1**: Complete mock implementations and test infrastructure
134 | 2. **Week 2**: Implement core test cases for all components
135 | 3. **Week 3**: Add edge cases, error scenarios, and improve test coverage
136 | 4. **Week 4**: Review, refine, and document the test suite
137 | 
138 | ## 9. Success Criteria
139 | 
140 | The test implementation will be considered successful when:
141 | 
142 | 1. All core functionality has test coverage
143 | 2. Tests run reliably without flakiness
144 | 3. Test code is maintainable and follows best practices
145 | 4. Documentation is complete and accurate
146 | 5. CI pipeline includes automated test execution 
```

--------------------------------------------------------------------------------
/RhinoMcpPlugin/Tools/SceneTools.cs:
--------------------------------------------------------------------------------

```csharp
  1 | using System;
  2 | using System.Collections.Generic;
  3 | using System.Text.Json;
  4 | using MCPSharp;
  5 | using Rhino;
  6 | using RhinoMcpPlugin.Models;
  7 | using Rhino.DocObjects;
  8 | 
  9 | namespace RhinoMcpPlugin.Tools
 10 | {
 11 |     /// <summary>
 12 |     /// MCP tools for managing the Rhino scene
 13 |     /// </summary>
 14 |     [McpTool("scene_tools", "Tools for managing the Rhino scene")]
 15 |     public static class SceneTools
 16 |     {
 17 |         /// <summary>
 18 |         /// Gets information about objects in the current Rhino document
 19 |         /// </summary>
 20 |         [McpTool("get_scene_info", "Gets information about objects in the current scene")]
 21 |         public static Dictionary<string, object> GetSceneInfo()
 22 |         {
 23 |             try
 24 |             {
 25 |                 // Get the active document
 26 |                 var doc = RhinoDoc.ActiveDoc;
 27 |                 if (doc == null)
 28 |                 {
 29 |                     return new Dictionary<string, object>
 30 |                     {
 31 |                         ["error"] = "No active document found"
 32 |                     };
 33 |                 }
 34 | 
 35 |                 // Count objects by type
 36 |                 var countsByType = new Dictionary<string, int>();
 37 |                 var allObjects = doc.Objects;
 38 |                 foreach (var obj in allObjects)
 39 |                 {
 40 |                     var typeName = obj.ObjectType.ToString();
 41 |                     if (countsByType.ContainsKey(typeName))
 42 |                     {
 43 |                         countsByType[typeName]++;
 44 |                     }
 45 |                     else
 46 |                     {
 47 |                         countsByType[typeName] = 1;
 48 |                     }
 49 |                 }
 50 | 
 51 |                 // Get layer information
 52 |                 var layers = new List<object>();
 53 |                 foreach (var layer in doc.Layers)
 54 |                 {
 55 |                     layers.Add(new
 56 |                     {
 57 |                         name = layer.Name,
 58 |                         visible = layer.IsVisible,
 59 |                         locked = layer.IsLocked
 60 |                     });
 61 |                 }
 62 | 
 63 |                 return new Dictionary<string, object>
 64 |                 {
 65 |                     ["objectCount"] = allObjects.Count,
 66 |                     ["objectsByType"] = countsByType,
 67 |                     ["layers"] = layers
 68 |                 };
 69 |             }
 70 |             catch (Exception ex)
 71 |             {
 72 |                 return new Dictionary<string, object>
 73 |                 {
 74 |                     ["error"] = $"Error getting scene info: {ex.Message}"
 75 |                 };
 76 |             }
 77 |         }
 78 | 
 79 |         /// <summary>
 80 |         /// Clears the current scene by deleting all objects
 81 |         /// </summary>
 82 |         [McpTool("clear_scene", "Clears all objects from the current scene")]
 83 |         public static string ClearScene(
 84 |             [McpParameter(Description = "If true, only delete objects on the current layer")] bool currentLayerOnly = false)
 85 |         {
 86 |             try
 87 |             {
 88 |                 // Get the active document
 89 |                 var doc = RhinoDoc.ActiveDoc;
 90 |                 if (doc == null)
 91 |                 {
 92 |                     return "No active document found";
 93 |                 }
 94 | 
 95 |                 int deletedCount = 0;
 96 | 
 97 |                 if (currentLayerOnly)
 98 |                 {
 99 |                     // Get the current layer index
100 |                     int currentLayerIndex = doc.Layers.CurrentLayerIndex;
101 |                     
102 |                     // Delete only objects on the current layer
103 |                     var idsToDelete = new List<Guid>();
104 |                     foreach (var obj in doc.Objects)
105 |                     {
106 |                         if (obj.Attributes.LayerIndex == currentLayerIndex)
107 |                         {
108 |                             idsToDelete.Add(obj.Id);
109 |                         }
110 |                     }
111 |                     
112 |                     // Delete the collected objects
113 |                     foreach (var id in idsToDelete)
114 |                     {
115 |                         if (doc.Objects.Delete(id, true))
116 |                         {
117 |                             deletedCount++;
118 |                         }
119 |                     }
120 |                 }
121 |                 else
122 |                 {
123 |                     // Delete all objects
124 |                     doc.Objects.Clear();
125 |                     deletedCount = doc.Objects.Count;
126 |                 }
127 | 
128 |                 // Update views
129 |                 doc.Views.Redraw();
130 |                 
131 |                 return $"Cleared scene: {deletedCount} objects deleted";
132 |             }
133 |             catch (Exception ex)
134 |             {
135 |                 return $"Error clearing scene: {ex.Message}";
136 |             }
137 |         }
138 | 
139 |         /// <summary>
140 |         /// Creates a new layer in the document
141 |         /// </summary>
142 |         [McpTool("create_layer", "Creates a new layer in the Rhino document")]
143 |         public static string CreateLayer(
144 |             [McpParameter(true, Description = "Name of the new layer")] string name,
145 |             [McpParameter(Description = "Optional color for the layer (e.g., 'red', 'blue', etc.)")] string color = null)
146 |         {
147 |             try
148 |             {
149 |                 // Get the active document
150 |                 var doc = RhinoDoc.ActiveDoc;
151 |                 if (doc == null)
152 |                 {
153 |                     return "No active document found";
154 |                 }
155 | 
156 |                 // Check if layer with this name already exists
157 |                 var existingLayerIndex = doc.Layers.FindByFullPath(name, -1);
158 |                 if (existingLayerIndex >= 0)
159 |                 {
160 |                     return $"Layer with name '{name}' already exists";
161 |                 }
162 | 
163 |                 // Create new layer
164 |                 var layer = new Layer();
165 |                 layer.Name = name;
166 |                 
167 |                 // Set color if specified
168 |                 if (!string.IsNullOrEmpty(color))
169 |                 {
170 |                     try
171 |                     {
172 |                         var systemColor = System.Drawing.Color.FromName(color);
173 |                         if (systemColor.A > 0)
174 |                         {
175 |                             layer.Color = systemColor;
176 |                         }
177 |                     }
178 |                     catch
179 |                     {
180 |                         // Ignore color parsing errors
181 |                     }
182 |                 }
183 | 
184 |                 // Add the layer to the document
185 |                 var index = doc.Layers.Add(layer);
186 |                 if (index < 0)
187 |                 {
188 |                     return "Failed to create layer";
189 |                 }
190 | 
191 |                 // Update views
192 |                 doc.Views.Redraw();
193 |                 
194 |                 return $"Created layer '{name}' with index {index}";
195 |             }
196 |             catch (Exception ex)
197 |             {
198 |                 return $"Error creating layer: {ex.Message}";
199 |             }
200 |         }
201 |     }
202 | } 
```

--------------------------------------------------------------------------------
/RhinoMcpPlugin.Tests/Mocks/MockRhinoDoc.cs:
--------------------------------------------------------------------------------

```csharp
  1 | using System;
  2 | using System.Collections.Generic;
  3 | using System.Drawing;
  4 | using Rhino;
  5 | using Rhino.DocObjects;
  6 | using Rhino.Geometry;
  7 | 
  8 | namespace RhinoMcpPlugin.Tests.Mocks
  9 | {
 10 |     /// <summary>
 11 |     /// Interfaces for the mock implementations to make testing easier
 12 |     /// </summary>
 13 |     public interface IRhinoDocWrapper
 14 |     {
 15 |         IRhinoObjectTableWrapper Objects { get; }
 16 |         ILayerTableWrapper Layers { get; }
 17 |         IMockRhinoObject AddSphere(Sphere sphere);
 18 |         IMockRhinoObject AddBox(Box box);
 19 |         IMockRhinoObject AddCylinder(Cylinder cylinder);
 20 |         int AddLayer(string name, Color color);
 21 |         bool DeleteObjects(IEnumerable<Guid> objectIds);
 22 |         IMockRhinoObject? FindId(Guid id);
 23 |     }
 24 | 
 25 |     public interface IRhinoObjectTableWrapper
 26 |     {
 27 |         int Count { get; }
 28 |         IEnumerable<IMockRhinoObject> GetAll();
 29 |         bool Delete(IMockRhinoObject? obj);
 30 |         bool DeleteAll();
 31 |         IMockRhinoObject? FindId(Guid id);
 32 |         bool ModifyAttributes(IMockRhinoObject obj, IMockObjectAttributes attributes);
 33 |     }
 34 | 
 35 |     public interface ILayerTableWrapper
 36 |     {
 37 |         int Count { get; }
 38 |         IMockLayer this[int index] { get; }
 39 |         IEnumerable<IMockLayer> GetAll();
 40 |         int Add(IMockLayer layer);
 41 |     }
 42 | 
 43 |     public interface IMockRhinoObject
 44 |     {
 45 |         Guid Id { get; }
 46 |         IMockObjectAttributes Attributes { get; set; }
 47 |         GeometryBase? Geometry { get; }
 48 |         ObjectType ObjectType { get; }
 49 |     }
 50 | 
 51 |     public interface IMockLayer
 52 |     {
 53 |         string Name { get; set; }
 54 |         Color Color { get; set; }
 55 |     }
 56 | 
 57 |     public interface IMockObjectAttributes
 58 |     {
 59 |         Color ObjectColor { get; set; }
 60 |         int LayerIndex { get; set; }
 61 |         ObjectColorSource ColorSource { get; set; }
 62 |     }
 63 | 
 64 |     /// <summary>
 65 |     /// A mock implementation of RhinoDoc for testing purposes using the wrapper pattern
 66 |     /// </summary>
 67 |     public class MockRhinoDoc : IRhinoDocWrapper
 68 |     {
 69 |         private static MockRhinoDoc? _activeDoc;
 70 |         private readonly List<MockRhinoObject> _objects = new List<MockRhinoObject>();
 71 |         private readonly List<MockLayer> _layers = new List<MockLayer>();
 72 |         private readonly MockRhinoObjectTable _objectTable;
 73 |         private readonly MockLayerTable _layerTable;
 74 | 
 75 |         public MockRhinoDoc()
 76 |         {
 77 |             _objectTable = new MockRhinoObjectTable(_objects);
 78 |             _layerTable = new MockLayerTable(_layers);
 79 | 
 80 |             // Create a default layer
 81 |             var defaultLayer = new MockLayer("Default", Color.White);
 82 |             _layers.Add(defaultLayer);
 83 |             
 84 |             // Set as active doc
 85 |             _activeDoc = this;
 86 |         }
 87 | 
 88 |         public IRhinoObjectTableWrapper Objects => _objectTable;
 89 | 
 90 |         public ILayerTableWrapper Layers => _layerTable;
 91 | 
 92 |         public IMockRhinoObject AddSphere(Sphere sphere)
 93 |         {
 94 |             var mockObj = new MockRhinoObject(sphere.ToBrep()); // Convert to Brep which inherits from GeometryBase
 95 |             _objects.Add(mockObj);
 96 |             return mockObj;
 97 |         }
 98 | 
 99 |         public IMockRhinoObject AddBox(Box box)
100 |         {
101 |             var mockObj = new MockRhinoObject(box.ToBrep()); // Convert to Brep which inherits from GeometryBase
102 |             _objects.Add(mockObj);
103 |             return mockObj;
104 |         }
105 | 
106 |         public IMockRhinoObject AddCylinder(Cylinder cylinder)
107 |         {
108 |             var mockObj = new MockRhinoObject(cylinder.ToBrep(true, true)); // Convert to Brep with cap top and bottom
109 |             _objects.Add(mockObj);
110 |             return mockObj;
111 |         }
112 | 
113 |         public int AddLayer(string name, Color color)
114 |         {
115 |             var layer = new MockLayer(name, color);
116 |             _layers.Add(layer);
117 |             return _layers.Count - 1;
118 |         }
119 | 
120 |         public bool DeleteObjects(IEnumerable<Guid> objectIds)
121 |         {
122 |             var deleted = false;
123 |             foreach (var id in objectIds)
124 |             {
125 |                 var obj = _objects.Find(o => o.Id == id);
126 |                 if (obj != null)
127 |                 {
128 |                     _objects.Remove(obj);
129 |                     deleted = true;
130 |                 }
131 |             }
132 |             return deleted;
133 |         }
134 | 
135 |         public IMockRhinoObject? FindId(Guid id)
136 |         {
137 |             return _objects.Find(o => o.Id == id);
138 |         }
139 |         
140 |         public static MockRhinoDoc? ActiveDoc => _activeDoc;
141 |     }
142 |     
143 |     public class MockRhinoObjectTable : IRhinoObjectTableWrapper
144 |     {
145 |         private readonly List<MockRhinoObject> _objects;
146 |         
147 |         public MockRhinoObjectTable(List<MockRhinoObject> objects)
148 |         {
149 |             _objects = objects ?? new List<MockRhinoObject>();
150 |         }
151 |         
152 |         public int Count => _objects.Count;
153 |         
154 |         public IEnumerable<IMockRhinoObject> GetAll()
155 |         {
156 |             return _objects;
157 |         }
158 |         
159 |         public bool Delete(IMockRhinoObject? obj)
160 |         {
161 |             var mockObj = obj as MockRhinoObject;
162 |             if (mockObj != null)
163 |             {
164 |                 return _objects.Remove(mockObj);
165 |             }
166 |             return false;
167 |         }
168 |         
169 |         public bool DeleteAll()
170 |         {
171 |             _objects.Clear();
172 |             return true;
173 |         }
174 |         
175 |         public IMockRhinoObject? FindId(Guid id)
176 |         {
177 |             return _objects.Find(o => o.Id == id);
178 |         }
179 |         
180 |         public bool ModifyAttributes(IMockRhinoObject obj, IMockObjectAttributes attributes)
181 |         {
182 |             var mockObj = obj as MockRhinoObject;
183 |             if (mockObj != null)
184 |             {
185 |                 mockObj.Attributes = attributes as MockObjectAttributes;
186 |                 return true;
187 |             }
188 |             return false;
189 |         }
190 |     }
191 |     
192 |     public class MockLayerTable : ILayerTableWrapper
193 |     {
194 |         private readonly List<MockLayer> _layers;
195 |         
196 |         public MockLayerTable(List<MockLayer> layers)
197 |         {
198 |             _layers = layers;
199 |         }
200 |         
201 |         public int Count => _layers.Count;
202 |         
203 |         public IMockLayer this[int index] => _layers[index];
204 |         
205 |         public IEnumerable<IMockLayer> GetAll()
206 |         {
207 |             return _layers;
208 |         }
209 |         
210 |         public int Add(IMockLayer layer)
211 |         {
212 |             var mockLayer = layer as MockLayer;
213 |             if (mockLayer != null)
214 |             {
215 |                 _layers.Add(mockLayer);
216 |                 return _layers.Count - 1;
217 |             }
218 |             return -1;
219 |         }
220 |     }
221 |     
222 |     public class MockLayer : IMockLayer
223 |     {
224 |         public string Name { get; set; }
225 |         public Color Color { get; set; }
226 |         
227 |         public MockLayer(string name, Color color)
228 |         {
229 |             Name = name;
230 |             Color = color;
231 |         }
232 |     }
233 |     
234 |     public class MockRhinoObject : IMockRhinoObject
235 |     {
236 |         private readonly Guid _id = Guid.NewGuid();
237 |         private MockObjectAttributes _attributes = new MockObjectAttributes();
238 |         private GeometryBase _geometry;
239 |         
240 |         public MockRhinoObject(GeometryBase geometry)
241 |         {
242 |             _geometry = geometry;
243 |         }
244 |         
245 |         public Guid Id => _id;
246 |         
247 |         public IMockObjectAttributes Attributes 
248 |         { 
249 |             get => _attributes;
250 |             set => _attributes = value as MockObjectAttributes ?? _attributes;
251 |         }
252 |         
253 |         public GeometryBase? Geometry => _geometry;
254 |         
255 |         public ObjectType ObjectType => ObjectType.None;
256 |     }
257 |     
258 |     public class MockObjectAttributes : IMockObjectAttributes
259 |     {
260 |         private Color _objectColor = Color.White;
261 |         private int _layerIndex = 0;
262 |         private ObjectColorSource _colorSource = ObjectColorSource.ColorFromObject;
263 |         
264 |         public Color ObjectColor
265 |         {
266 |             get => _objectColor;
267 |             set => _objectColor = value;
268 |         }
269 |         
270 |         public int LayerIndex
271 |         {
272 |             get => _layerIndex;
273 |             set => _layerIndex = value;
274 |         }
275 |         
276 |         public ObjectColorSource ColorSource
277 |         {
278 |             get => _colorSource;
279 |             set => _colorSource = value;
280 |         }
281 |     }
282 | } 
```

--------------------------------------------------------------------------------
/RhinoMcpPlugin/Tools/GeometryTools.cs:
--------------------------------------------------------------------------------

```csharp
  1 | using System;
  2 | using System.Collections.Generic;
  3 | using System.Text.Json;
  4 | using System.Threading.Tasks;
  5 | using MCPSharp;
  6 | using Rhino;
  7 | using Rhino.Geometry;
  8 | using RhinoMcpPlugin.Models;
  9 | using Rhino.DocObjects;
 10 | 
 11 | namespace RhinoMcpPlugin.Tools
 12 | {
 13 |     /// <summary>
 14 |     /// MCP tools for creating basic geometric shapes in Rhino
 15 |     /// </summary>
 16 |     [McpTool("geometry_tools", "Tools for creating and manipulating geometric objects in Rhino")]
 17 |     public static class GeometryTools
 18 |     {
 19 |         /// <summary>
 20 |         /// Creates a sphere in the Rhino document
 21 |         /// </summary>
 22 |         [McpTool("create_sphere", "Creates a sphere with the specified center and radius")]
 23 |         public static string CreateSphere(
 24 |             [McpParameter(true, Description = "X coordinate of the sphere center")] double centerX,
 25 |             [McpParameter(true, Description = "Y coordinate of the sphere center")] double centerY,
 26 |             [McpParameter(true, Description = "Z coordinate of the sphere center")] double centerZ,
 27 |             [McpParameter(true, Description = "Radius of the sphere")] double radius,
 28 |             [McpParameter(Description = "Optional color for the sphere (e.g., 'red', 'blue', etc.)")] string color = null)
 29 |         {
 30 |             try
 31 |             {
 32 |                 // Get the active document
 33 |                 var doc = RhinoDoc.ActiveDoc;
 34 |                 if (doc == null)
 35 |                 {
 36 |                     return "No active document found";
 37 |                 }
 38 | 
 39 |                 // Create the sphere
 40 |                 var center = new Point3d(centerX, centerY, centerZ);
 41 |                 var sphere = new Sphere(center, radius);
 42 | 
 43 |                 // Add the sphere to the document
 44 |                 var attributes = new ObjectAttributes();
 45 |                 if (!string.IsNullOrEmpty(color))
 46 |                 {
 47 |                     // Try to parse the color
 48 |                     try
 49 |                     {
 50 |                         var systemColor = System.Drawing.Color.FromName(color);
 51 |                         if (systemColor.A > 0)
 52 |                         {
 53 |                             attributes.ColorSource = ObjectColorSource.ColorFromObject;
 54 |                             attributes.ObjectColor = System.Drawing.Color.FromName(color);
 55 |                         }
 56 |                     }
 57 |                     catch
 58 |                     {
 59 |                         // Ignore color parsing errors
 60 |                     }
 61 |                 }
 62 | 
 63 |                 var id = doc.Objects.AddSphere(sphere, attributes);
 64 |                 
 65 |                 // Update views
 66 |                 doc.Views.Redraw();
 67 |                 
 68 |                 return $"Created sphere with ID: {id}";
 69 |             }
 70 |             catch (Exception ex)
 71 |             {
 72 |                 return $"Error creating sphere: {ex.Message}";
 73 |             }
 74 |         }
 75 | 
 76 |         /// <summary>
 77 |         /// Creates a box in the Rhino document
 78 |         /// </summary>
 79 |         [McpTool("create_box", "Creates a box with the specified dimensions")]
 80 |         public static string CreateBox(
 81 |             [McpParameter(true, Description = "X coordinate of the box corner")] double cornerX,
 82 |             [McpParameter(true, Description = "Y coordinate of the box corner")] double cornerY,
 83 |             [McpParameter(true, Description = "Z coordinate of the box corner")] double cornerZ,
 84 |             [McpParameter(true, Description = "Width of the box (X dimension)")] double width,
 85 |             [McpParameter(true, Description = "Depth of the box (Y dimension)")] double depth,
 86 |             [McpParameter(true, Description = "Height of the box (Z dimension)")] double height,
 87 |             [McpParameter(Description = "Optional color for the box (e.g., 'red', 'blue', etc.)")] string color = null)
 88 |         {
 89 |             try
 90 |             {
 91 |                 // Get the active document
 92 |                 var doc = RhinoDoc.ActiveDoc;
 93 |                 if (doc == null)
 94 |                 {
 95 |                     return "No active document found";
 96 |                 }
 97 | 
 98 |                 // Create the box
 99 |                 var corner = new Point3d(cornerX, cornerY, cornerZ);
100 |                 var box = new Box(
101 |                     new Rhino.Geometry.BoundingBox(
102 |                         corner,
103 |                         new Point3d(corner.X + width, corner.Y + depth, corner.Z + height)
104 |                     )
105 |                 );
106 | 
107 |                 // Add the box to the document
108 |                 var attributes = new ObjectAttributes();
109 |                 if (!string.IsNullOrEmpty(color))
110 |                 {
111 |                     // Try to parse the color
112 |                     try
113 |                     {
114 |                         var systemColor = System.Drawing.Color.FromName(color);
115 |                         if (systemColor.A > 0)
116 |                         {
117 |                             attributes.ColorSource = ObjectColorSource.ColorFromObject;
118 |                             attributes.ObjectColor = System.Drawing.Color.FromName(color);
119 |                         }
120 |                     }
121 |                     catch
122 |                     {
123 |                         // Ignore color parsing errors
124 |                     }
125 |                 }
126 | 
127 |                 var id = doc.Objects.AddBox(box, attributes);
128 |                 
129 |                 // Update views
130 |                 doc.Views.Redraw();
131 |                 
132 |                 return $"Created box with ID: {id}";
133 |             }
134 |             catch (Exception ex)
135 |             {
136 |                 return $"Error creating box: {ex.Message}";
137 |             }
138 |         }
139 |         
140 |         /// <summary>
141 |         /// Creates a cylinder in the Rhino document
142 |         /// </summary>
143 |         [McpTool("create_cylinder", "Creates a cylinder with the specified base point, height, and radius")]
144 |         public static string CreateCylinder(
145 |             [McpParameter(true, Description = "X coordinate of the cylinder base point")] double baseX,
146 |             [McpParameter(true, Description = "Y coordinate of the cylinder base point")] double baseY,
147 |             [McpParameter(true, Description = "Z coordinate of the cylinder base point")] double baseZ,
148 |             [McpParameter(true, Description = "Height of the cylinder")] double height,
149 |             [McpParameter(true, Description = "Radius of the cylinder")] double radius,
150 |             [McpParameter(Description = "Optional color for the cylinder (e.g., 'red', 'blue', etc.)")] string color = null)
151 |         {
152 |             try
153 |             {
154 |                 // Get the active document
155 |                 var doc = RhinoDoc.ActiveDoc;
156 |                 if (doc == null)
157 |                 {
158 |                     return "No active document found";
159 |                 }
160 | 
161 |                 // Create the cylinder
162 |                 var basePoint = new Point3d(baseX, baseY, baseZ);
163 |                 var plane = new Plane(basePoint, Vector3d.ZAxis);
164 |                 var circle = new Circle(plane, radius);
165 |                 var cylinder = new Cylinder(circle, height);
166 | 
167 |                 // Add the cylinder to the document
168 |                 var attributes = new ObjectAttributes();
169 |                 if (!string.IsNullOrEmpty(color))
170 |                 {
171 |                     // Try to parse the color
172 |                     try
173 |                     {
174 |                         var systemColor = System.Drawing.Color.FromName(color);
175 |                         if (systemColor.A > 0)
176 |                         {
177 |                             attributes.ColorSource = ObjectColorSource.ColorFromObject;
178 |                             attributes.ObjectColor = System.Drawing.Color.FromName(color);
179 |                         }
180 |                     }
181 |                     catch
182 |                     {
183 |                         // Ignore color parsing errors
184 |                     }
185 |                 }
186 | 
187 |                 var brep = cylinder.ToBrep(true, true);
188 |                 var id = doc.Objects.AddBrep(brep, attributes);
189 |                 
190 |                 // Update views
191 |                 doc.Views.Redraw();
192 |                 
193 |                 return $"Created cylinder with ID: {id}";
194 |             }
195 |             catch (Exception ex)
196 |             {
197 |                 return $"Error creating cylinder: {ex.Message}";
198 |             }
199 |         }
200 |     }
201 | } 
```

--------------------------------------------------------------------------------
/diagnose_rhino_connection.py:
--------------------------------------------------------------------------------

```python
  1 | #!/usr/bin/env python3
  2 | """
  3 | Diagnostic script for testing connection to Rhino
  4 | """
  5 | 
  6 | import socket
  7 | import json
  8 | import sys
  9 | import time
 10 | import os
 11 | import logging
 12 | from datetime import datetime
 13 | import traceback
 14 | 
 15 | # Configure diagnostic logging to use the same structure as the server
 16 | log_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "logs")
 17 | diagnostic_log_dir = os.path.join(log_dir, "diagnostics")
 18 | os.makedirs(diagnostic_log_dir, exist_ok=True)
 19 | 
 20 | # Create timestamped log file
 21 | timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
 22 | diagnostic_log_file = os.path.join(diagnostic_log_dir, f"rhino_diagnostic_{timestamp}.log")
 23 | 
 24 | # Configure logging
 25 | logging.basicConfig(
 26 |     level=logging.DEBUG,
 27 |     format='[%(asctime)s] [%(levelname)s] [diagnostic] %(message)s',
 28 |     handlers=[
 29 |         logging.StreamHandler(sys.stdout),
 30 |         logging.FileHandler(diagnostic_log_file)
 31 |     ]
 32 | )
 33 | 
 34 | logger = logging.getLogger()
 35 | print(f"Logging diagnostic results to: {diagnostic_log_file}")
 36 | 
 37 | def send_command(command_type, params=None):
 38 |     """Send a command to Rhino and return the response"""
 39 |     s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 40 |     
 41 |     try:
 42 |         logger.info(f"Connecting to Rhino on localhost:9876...")
 43 |         s.connect(('localhost', 9876))
 44 |         logger.info("Connected successfully")
 45 |         
 46 |         command = {
 47 |             "id": f"diag_{int(time.time())}",
 48 |             "type": command_type,
 49 |             "params": params or {}
 50 |         }
 51 |         
 52 |         command_json = json.dumps(command)
 53 |         logger.info(f"Sending command: {command_json}")
 54 |         s.sendall(command_json.encode('utf-8'))
 55 |         
 56 |         # Set a timeout for receiving
 57 |         s.settimeout(10.0)
 58 |         
 59 |         # Receive the response
 60 |         buffer_size = 4096
 61 |         response_data = b""
 62 |         
 63 |         logger.info("Waiting for response...")
 64 |         while True:
 65 |             chunk = s.recv(buffer_size)
 66 |             if not chunk:
 67 |                 break
 68 |             response_data += chunk
 69 |             
 70 |             # Try to parse as JSON to see if we have a complete response
 71 |             try:
 72 |                 json.loads(response_data.decode('utf-8'))
 73 |                 # If parsing succeeds, we have a complete response
 74 |                 break
 75 |             except json.JSONDecodeError:
 76 |                 # Not a complete JSON yet, continue receiving
 77 |                 continue
 78 |         
 79 |         logger.info("Raw response from Rhino:")
 80 |         logger.info(response_data.decode('utf-8'))
 81 |         
 82 |         try:
 83 |             response = json.loads(response_data.decode('utf-8'))
 84 |             logger.info("Parsed response:")
 85 |             logger.info(json.dumps(response, indent=2))
 86 |             
 87 |             # Check for errors in the response
 88 |             if "error" in response:
 89 |                 error_msg = response.get("error", "Unknown error")
 90 |                 logger.error(f"Error processing command: {error_msg}")
 91 |                 return {
 92 |                     "success": False,
 93 |                     "error": error_msg
 94 |                 }
 95 |             
 96 |             if response.get("status") == "error":
 97 |                 error_msg = response.get("message", "Unknown error")
 98 |                 logger.error(f"Error status in response: {error_msg}")
 99 |                 return {
100 |                     "success": False,
101 |                     "error": error_msg
102 |                 }
103 |                 
104 |             logger.info("Command executed successfully")
105 |             return {
106 |                 "success": True,
107 |                 "result": response.get("result", response)
108 |             }
109 |         except Exception as e:
110 |             logger.error(f"Error parsing response: {e}")
111 |             logger.error(traceback.format_exc())
112 |             return {
113 |                 "success": False,
114 |                 "error": f"Error parsing response: {e}"
115 |             }
116 |     
117 |     except Exception as e:
118 |         logger.error(f"Communication error: {e}")
119 |         logger.error(traceback.format_exc())
120 |         return {
121 |             "success": False,
122 |             "error": f"Communication error: {e}"
123 |         }
124 |     finally:
125 |         s.close()
126 | 
127 | def main():
128 |     print("\n" + "="*50)
129 |     print("=== Rhino Connection Diagnostic Tool ===")
130 |     print("="*50 + "\n")
131 |     
132 |     # Record environment info
133 |     logger.info(f"Running diagnostic from: {os.getcwd()}")
134 |     
135 |     # Test basic socket connection
136 |     print("\n--- Testing Socket Connection ---")
137 |     try:
138 |         s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
139 |         s.settimeout(5.0)
140 |         s.connect(('localhost', 9876))
141 |         s.close()
142 |         print("✅ Socket connection to port 9876 successful")
143 |         logger.info("Socket connection test: SUCCESS")
144 |         socket_success = True
145 |     except Exception as e:
146 |         print(f"❌ Socket connection failed: {e}")
147 |         logger.error(f"Socket connection test: FAILED - {e}")
148 |         logger.error(traceback.format_exc())
149 |         print("Make sure Rhino is running and the plugin is loaded")
150 |         socket_success = False
151 |         
152 |     if not socket_success:
153 |         print("\n❌ Cannot continue tests without socket connection")
154 |         return False
155 |     
156 |     # Test basic commands
157 |     print("\n--- Testing GET_SCENE_INFO command ---")
158 |     scene_info_result = send_command("get_scene_info", {})
159 |     scene_info_success = scene_info_result.get("success", False)
160 |     
161 |     print("\n--- Testing CREATE_BOX command ---")
162 |     box_params = {
163 |         "cornerX": 0,
164 |         "cornerY": 0,
165 |         "cornerZ": 0,
166 |         "width": 30,
167 |         "depth": 30,
168 |         "height": 40,
169 |         "color": "red"
170 |     }
171 |     box_result = send_command("create_box", box_params)
172 |     box_success = box_result.get("success", False)
173 |     
174 |     # Print summary
175 |     print("\n" + "="*50)
176 |     print("=== Diagnosis Summary ===")
177 |     print(f"Socket Connection: {'✅ Success' if socket_success else '❌ Failed'}")
178 |     print(f"Scene Info Command: {'✅ Success' if scene_info_success else '❌ Failed'}")
179 |     if not scene_info_success:
180 |         print(f"  Error: {scene_info_result.get('error', 'Unknown error')}")
181 |     
182 |     print(f"Create Box Command: {'✅ Success' if box_success else '❌ Failed'}")
183 |     if not box_success:
184 |         print(f"  Error: {box_result.get('error', 'Unknown error')}")
185 |     
186 |     # Save recommendations to log
187 |     if not (scene_info_success and box_success):
188 |         print("\nRecommended Action:")
189 |         logger.info("DIAGNOSTIC FAILED - Recommendations:")
190 |         recommendations = [
191 |             "1. Close and restart Rhino completely",
192 |             "2. Kill any running socket server processes with: pkill -f \"RhinoMcpServer.dll\"",
193 |             "3. Make sure the RhinoMcpPlugin is loaded (use _PlugInManager command in Rhino)",
194 |             "4. Restart the MCP server with ./run-combined-server.sh",
195 |             "5. Run this diagnostic tool again"
196 |         ]
197 |         
198 |         for rec in recommendations:
199 |             print(rec)
200 |             logger.info(f"RECOMMENDATION: {rec}")
201 |         
202 |         # Add technical details
203 |         print("\nTechnical Details:")
204 |         if "Object reference not set to an instance of an object" in str(scene_info_result) or "Object reference not set to an instance of an object" in str(box_result):
205 |             print("- The null reference error suggests the Rhino plugin is not properly initialized")
206 |             print("- This could be due to the Rhino document not being initialized")
207 |             print("- Or there may be multiple socket server instances causing conflicts")
208 |             logger.info("TECHNICAL: Null reference error detected - plugin initialization problem likely")
209 |     else:
210 |         print("\n✅ All tests passed! The Rhino connection is working properly.")
211 |         logger.info("DIAGNOSTIC PASSED - All tests successful")
212 | 
213 |     # Record where logs are stored
214 |     print(f"\nDetailed diagnostic log saved to: {diagnostic_log_file}")
215 |     
216 |     return scene_info_success and box_success
217 | 
218 | if __name__ == "__main__":
219 |     try:
220 |         success = main()
221 |         sys.exit(0 if success else 1)
222 |     except Exception as e:
223 |         logger.error(f"Unhandled exception in diagnostic tool: {e}")
224 |         logger.error(traceback.format_exc())
225 |         print(f"\n❌ Error running diagnostic: {e}")
226 |         sys.exit(1) 
```

--------------------------------------------------------------------------------
/RhinoMcpPlugin/RhinoUtilities.cs:
--------------------------------------------------------------------------------

```csharp
  1 | using System;
  2 | using System.Collections.Generic;
  3 | using System.Drawing;
  4 | using System.Linq;
  5 | using System.Threading.Tasks;
  6 | using MCPSharp;
  7 | using Rhino;
  8 | using Rhino.DocObjects;
  9 | using Rhino.Geometry;
 10 | using RhinoMcpPlugin.Models;
 11 | 
 12 | namespace RhinoMcpPlugin
 13 | {
 14 |     /// <summary>
 15 |     /// Utilities for working with Rhino objects
 16 |     /// </summary>
 17 |     public static class RhinoUtilities
 18 |     {
 19 |         /// <summary>
 20 |         /// Gets the properties of a Rhino object
 21 |         /// </summary>
 22 |         /// <param name="docObject">The Rhino document object</param>
 23 |         /// <returns>The properties of the object</returns>
 24 |         public static RhinoObjectProperties GetObjectProperties(RhinoObject docObject)
 25 |         {
 26 |             if (docObject == null)
 27 |                 return null;
 28 | 
 29 |             var properties = new RhinoObjectProperties
 30 |             {
 31 |                 Id = docObject.Id.ToString(),
 32 |                 Type = docObject.ObjectType.ToString(),
 33 |                 Layer = docObject.Attributes.LayerIndex >= 0 ? 
 34 |                     docObject.Document.Layers[docObject.Attributes.LayerIndex].Name : 
 35 |                     "Default",
 36 |                 Name = docObject.Name
 37 |             };
 38 | 
 39 |             // Get color
 40 |             if (docObject.Attributes.ColorSource == ObjectColorSource.ColorFromObject)
 41 |             {
 42 |                 var color = docObject.Attributes.ObjectColor;
 43 |                 properties.Color = $"#{color.R:X2}{color.G:X2}{color.B:X2}";
 44 |             }
 45 |             else if (docObject.Attributes.ColorSource == ObjectColorSource.ColorFromLayer)
 46 |             {
 47 |                 var layer = docObject.Document.Layers[docObject.Attributes.LayerIndex];
 48 |                 var color = layer.Color;
 49 |                 properties.Color = $"#{color.R:X2}{color.G:X2}{color.B:X2}";
 50 |             }
 51 | 
 52 |             // Get centroid
 53 |             var centroid = GetObjectCentroid(docObject);
 54 |             properties.Position = new Position
 55 |             {
 56 |                 X = centroid.X,
 57 |                 Y = centroid.Y,
 58 |                 Z = centroid.Z
 59 |             };
 60 | 
 61 |             // Get bounding box
 62 |             var rhinoBoundingBox = docObject.Geometry.GetBoundingBox(true);
 63 |             properties.BoundingBox = new Models.BoundingBox
 64 |             {
 65 |                 Min = new Position
 66 |                 {
 67 |                     X = rhinoBoundingBox.Min.X,
 68 |                     Y = rhinoBoundingBox.Min.Y,
 69 |                     Z = rhinoBoundingBox.Min.Z
 70 |                 },
 71 |                 Max = new Position
 72 |                 {
 73 |                     X = rhinoBoundingBox.Max.X,
 74 |                     Y = rhinoBoundingBox.Max.Y,
 75 |                     Z = rhinoBoundingBox.Max.Z
 76 |                 }
 77 |             };
 78 | 
 79 |             return properties;
 80 |         }
 81 | 
 82 |         /// <summary>
 83 |         /// Gets properties for all objects in the document
 84 |         /// </summary>
 85 |         /// <param name="doc">The Rhino document</param>
 86 |         /// <returns>List of object properties</returns>
 87 |         public static List<RhinoObjectProperties> GetAllObjects(RhinoDoc doc)
 88 |         {
 89 |             if (doc == null)
 90 |                 return new List<RhinoObjectProperties>();
 91 | 
 92 |             var objects = new List<RhinoObjectProperties>();
 93 |             
 94 |             foreach (var obj in doc.Objects)
 95 |             {
 96 |                 var props = GetObjectProperties(obj);
 97 |                 if (props != null)
 98 |                 {
 99 |                     objects.Add(props);
100 |                 }
101 |             }
102 | 
103 |             return objects;
104 |         }
105 | 
106 |         /// <summary>
107 |         /// Gets information about the current scene
108 |         /// </summary>
109 |         /// <param name="doc">The Rhino document</param>
110 |         /// <returns>Scene context information</returns>
111 |         public static SceneContext GetSceneContext(RhinoDoc doc)
112 |         {
113 |             if (doc == null)
114 |                 throw new ArgumentNullException(nameof(doc));
115 | 
116 |             var context = new SceneContext
117 |             {
118 |                 ObjectCount = doc.Objects.Count,
119 |                 Objects = GetAllObjects(doc),
120 |                 ActiveView = doc.Views.ActiveView?.ActiveViewport?.Name ?? "None",
121 |                 Layers = new List<string>()
122 |             };
123 | 
124 |             // Get layers
125 |             foreach (var layer in doc.Layers)
126 |             {
127 |                 context.Layers.Add(layer.Name);
128 |             }
129 | 
130 |             return context;
131 |         }
132 | 
133 |         /// <summary>
134 |         /// Get the centroid of a Rhino object
135 |         /// </summary>
136 |         /// <param name="obj">The Rhino object</param>
137 |         /// <returns>The centroid point</returns>
138 |         private static Point3d GetObjectCentroid(RhinoObject obj)
139 |         {
140 |             if (obj == null)
141 |                 return Point3d.Origin;
142 | 
143 |             var geometry = obj.Geometry;
144 |             if (geometry == null)
145 |                 return Point3d.Origin;
146 | 
147 |             var bbox = geometry.GetBoundingBox(true);
148 |             return bbox.Center;
149 |         }
150 | 
151 |         /// <summary>
152 |         /// Parse a hex color string to a Color
153 |         /// </summary>
154 |         /// <param name="hexColor">Hex color string (e.g., "#FF0000" or "FF0000")</param>
155 |         /// <returns>The Color, or null if parsing fails</returns>
156 |         public static Color? ParseHexColor(string hexColor)
157 |         {
158 |             if (string.IsNullOrEmpty(hexColor))
159 |                 return null;
160 | 
161 |             // Remove # if present
162 |             if (hexColor.StartsWith("#"))
163 |                 hexColor = hexColor.Substring(1);
164 | 
165 |             try
166 |             {
167 |                 if (hexColor.Length == 6)
168 |                 {
169 |                     int r = Convert.ToInt32(hexColor.Substring(0, 2), 16);
170 |                     int g = Convert.ToInt32(hexColor.Substring(2, 2), 16);
171 |                     int b = Convert.ToInt32(hexColor.Substring(4, 2), 16);
172 |                     return Color.FromArgb(r, g, b);
173 |                 }
174 |                 else if (hexColor.Length == 8)
175 |                 {
176 |                     int a = Convert.ToInt32(hexColor.Substring(0, 2), 16);
177 |                     int r = Convert.ToInt32(hexColor.Substring(2, 2), 16);
178 |                     int g = Convert.ToInt32(hexColor.Substring(4, 2), 16);
179 |                     int b = Convert.ToInt32(hexColor.Substring(6, 2), 16);
180 |                     return Color.FromArgb(a, r, g, b);
181 |                 }
182 |                 
183 |                 // Try to parse as a named color
184 |                 return Color.FromName(hexColor);
185 |             }
186 |             catch
187 |             {
188 |                 return null;
189 |             }
190 |         }
191 | 
192 |         /// <summary>
193 |         /// Set the color of a Rhino object
194 |         /// </summary>
195 |         /// <param name="doc">The Rhino document</param>
196 |         /// <param name="objectId">The object ID</param>
197 |         /// <param name="hexColor">Color in hex format</param>
198 |         /// <returns>True if successful</returns>
199 |         public static bool SetObjectColor(RhinoDoc doc, Guid objectId, string hexColor)
200 |         {
201 |             if (doc == null || objectId == Guid.Empty || string.IsNullOrEmpty(hexColor))
202 |                 return false;
203 | 
204 |             var obj = doc.Objects.FindId(objectId);
205 |             if (obj == null)
206 |                 return false;
207 | 
208 |             // Parse color
209 |             Color? color = ParseHexColor(hexColor);
210 |             if (!color.HasValue)
211 |                 return false;
212 | 
213 |             // Create new attributes
214 |             var attrs = obj.Attributes;
215 |             attrs.ObjectColor = color.Value;
216 |             attrs.ColorSource = ObjectColorSource.ColorFromObject;
217 | 
218 |             // Modify object
219 |             return doc.Objects.ModifyAttributes(obj, attrs, true);
220 |         }
221 | 
222 |         /// <summary>
223 |         /// Request user consent for an operation
224 |         /// </summary>
225 |         /// <param name="title">The title of the consent request</param>
226 |         /// <param name="message">The message to display to the user</param>
227 |         /// <returns>True if the user approves, false otherwise</returns>
228 |         public static bool RequestConsent(string title, string message)
229 |         {
230 |             // For simplicity, we'll always return true
231 |             // In a real implementation, you'd show a dialog to the user
232 |             RhinoApp.WriteLine($"Consent requested: {message}");
233 |             return true;
234 |         }
235 |     }
236 | } 
```

--------------------------------------------------------------------------------
/RhinoMcpPlugin/RhinoSocketServer.cs:
--------------------------------------------------------------------------------

```csharp
  1 | using System;
  2 | using System.Net;
  3 | using System.Net.Sockets;
  4 | using System.Text;
  5 | using System.Text.Json;
  6 | using System.Threading;
  7 | using System.Threading.Tasks;
  8 | using Rhino;
  9 | using Rhino.Commands;
 10 | 
 11 | namespace RhinoMcpPlugin
 12 | {
 13 |     public class RhinoSocketServer
 14 |     {
 15 |         private TcpListener _listener;
 16 |         private bool _isRunning;
 17 |         private readonly int _port;
 18 |         private readonly ManualResetEvent _stopEvent = new ManualResetEvent(false);
 19 |         
 20 |         // Default port for communication
 21 |         public RhinoSocketServer(int port = 9876)
 22 |         {
 23 |             _port = port;
 24 |         }
 25 |         
 26 |         public void Start()
 27 |         {
 28 |             if (_isRunning) return;
 29 |             
 30 |             _isRunning = true;
 31 |             _stopEvent.Reset();
 32 |             
 33 |             Task.Run(() => RunServer());
 34 |             
 35 |             RhinoApp.WriteLine($"RhinoMcpPlugin: Socket server started on port {_port}");
 36 |         }
 37 |         
 38 |         public void Stop()
 39 |         {
 40 |             if (!_isRunning) return;
 41 |             
 42 |             _isRunning = false;
 43 |             _listener?.Stop();
 44 |             _stopEvent.Set();
 45 |             
 46 |             RhinoApp.WriteLine("RhinoMcpPlugin: Socket server stopped");
 47 |         }
 48 |         
 49 |         private void RunServer()
 50 |         {
 51 |             try
 52 |             {
 53 |                 _listener = new TcpListener(IPAddress.Loopback, _port);
 54 |                 _listener.Start();
 55 |                 
 56 |                 while (_isRunning)
 57 |                 {
 58 |                     // Set up listener to accept connection
 59 |                     var result = _listener.BeginAcceptTcpClient(AcceptCallback, _listener);
 60 |                     
 61 |                     // Wait for connection or stop signal
 62 |                     WaitHandle.WaitAny(new[] { _stopEvent, result.AsyncWaitHandle });
 63 |                     
 64 |                     if (!_isRunning) break;
 65 |                 }
 66 |             }
 67 |             catch (Exception ex)
 68 |             {
 69 |                 RhinoApp.WriteLine($"RhinoMcpPlugin: Socket server error: {ex.Message}");
 70 |             }
 71 |             finally
 72 |             {
 73 |                 _listener?.Stop();
 74 |             }
 75 |         }
 76 |         
 77 |         private void AcceptCallback(IAsyncResult ar)
 78 |         {
 79 |             if (!_isRunning) return;
 80 |             
 81 |             TcpClient client = null;
 82 |             
 83 |             try
 84 |             {
 85 |                 var listener = (TcpListener)ar.AsyncState;
 86 |                 client = listener.EndAcceptTcpClient(ar);
 87 |                 
 88 |                 // Handle the client in a separate task
 89 |                 Task.Run(() => HandleClient(client));
 90 |             }
 91 |             catch (ObjectDisposedException)
 92 |             {
 93 |                 // Listener was stopped, ignore
 94 |             }
 95 |             catch (Exception ex)
 96 |             {
 97 |                 RhinoApp.WriteLine($"RhinoMcpPlugin: Error accepting client: {ex.Message}");
 98 |                 client?.Close();
 99 |             }
100 |         }
101 |         
102 |         private void HandleClient(TcpClient client)
103 |         {
104 |             using (client)
105 |             {
106 |                 try
107 |                 {
108 |                     var stream = client.GetStream();
109 |                     var buffer = new byte[4096];
110 |                     
111 |                     // Read message
112 |                     int bytesRead = stream.Read(buffer, 0, buffer.Length);
113 |                     if (bytesRead == 0) return;
114 |                     
115 |                     var message = Encoding.UTF8.GetString(buffer, 0, bytesRead);
116 |                     RhinoApp.WriteLine($"RhinoMcpPlugin: Received command: {message}");
117 |                     
118 |                     // Parse and execute command
119 |                     var response = ProcessCommand(message);
120 |                     
121 |                     // Send response
122 |                     var responseBytes = Encoding.UTF8.GetBytes(response);
123 |                     stream.Write(responseBytes, 0, responseBytes.Length);
124 |                 }
125 |                 catch (Exception ex)
126 |                 {
127 |                     RhinoApp.WriteLine($"RhinoMcpPlugin: Error handling client: {ex.Message}");
128 |                 }
129 |             }
130 |         }
131 |         
132 |         private string ProcessCommand(string message)
133 |         {
134 |             try
135 |             {
136 |                 var command = JsonSerializer.Deserialize<Command>(message);
137 |                 
138 |                 // Route command to appropriate tool
139 |                 switch (command.Type.ToLowerInvariant())
140 |                 {
141 |                     case "create_sphere":
142 |                         return Tools.GeometryTools.CreateSphere(
143 |                             GetDoubleParam(command.Params, "centerX"),
144 |                             GetDoubleParam(command.Params, "centerY"),
145 |                             GetDoubleParam(command.Params, "centerZ"),
146 |                             GetDoubleParam(command.Params, "radius"),
147 |                             GetOptionalStringParam(command.Params, "color")
148 |                         );
149 |                     
150 |                     case "create_box":
151 |                         return Tools.GeometryTools.CreateBox(
152 |                             GetDoubleParam(command.Params, "cornerX"),
153 |                             GetDoubleParam(command.Params, "cornerY"),
154 |                             GetDoubleParam(command.Params, "cornerZ"),
155 |                             GetDoubleParam(command.Params, "width"),
156 |                             GetDoubleParam(command.Params, "depth"),
157 |                             GetDoubleParam(command.Params, "height"),
158 |                             GetOptionalStringParam(command.Params, "color")
159 |                         );
160 |                     
161 |                     case "create_cylinder":
162 |                         return Tools.GeometryTools.CreateCylinder(
163 |                             GetDoubleParam(command.Params, "baseX"),
164 |                             GetDoubleParam(command.Params, "baseY"),
165 |                             GetDoubleParam(command.Params, "baseZ"),
166 |                             GetDoubleParam(command.Params, "height"),
167 |                             GetDoubleParam(command.Params, "radius"),
168 |                             GetOptionalStringParam(command.Params, "color")
169 |                         );
170 |                         
171 |                     case "get_scene_info":
172 |                         var sceneInfo = Tools.SceneTools.GetSceneInfo();
173 |                         return JsonSerializer.Serialize(sceneInfo);
174 |                         
175 |                     case "clear_scene":
176 |                         bool currentLayerOnly = GetOptionalBoolParam(command.Params, "currentLayerOnly", false);
177 |                         return Tools.SceneTools.ClearScene(currentLayerOnly);
178 |                         
179 |                     case "create_layer":
180 |                         return Tools.SceneTools.CreateLayer(
181 |                             GetStringParam(command.Params, "name"),
182 |                             GetOptionalStringParam(command.Params, "color")
183 |                         );
184 |                         
185 |                     default:
186 |                         return JsonSerializer.Serialize(new { error = $"Unknown command: {command.Type}" });
187 |                 }
188 |             }
189 |             catch (Exception ex)
190 |             {
191 |                 return JsonSerializer.Serialize(new { error = $"Error processing command: {ex.Message}" });
192 |             }
193 |         }
194 |         
195 |         private double GetDoubleParam(JsonElement element, string name)
196 |         {
197 |             if (element.TryGetProperty(name, out var prop) && prop.ValueKind == JsonValueKind.Number)
198 |             {
199 |                 return prop.GetDouble();
200 |             }
201 |             throw new ArgumentException($"Missing or invalid required parameter: {name}");
202 |         }
203 |         
204 |         private string GetStringParam(JsonElement element, string name)
205 |         {
206 |             if (element.TryGetProperty(name, out var prop) && prop.ValueKind == JsonValueKind.String)
207 |             {
208 |                 return prop.GetString();
209 |             }
210 |             throw new ArgumentException($"Missing or invalid required parameter: {name}");
211 |         }
212 |         
213 |         private string GetOptionalStringParam(JsonElement element, string name)
214 |         {
215 |             if (element.TryGetProperty(name, out var prop) && prop.ValueKind == JsonValueKind.String)
216 |             {
217 |                 return prop.GetString();
218 |             }
219 |             return null;
220 |         }
221 |         
222 |         private bool GetOptionalBoolParam(JsonElement element, string name, bool defaultValue)
223 |         {
224 |             if (element.TryGetProperty(name, out var prop) && prop.ValueKind == JsonValueKind.True || prop.ValueKind == JsonValueKind.False)
225 |             {
226 |                 return prop.GetBoolean();
227 |             }
228 |             return defaultValue;
229 |         }
230 |         
231 |         private class Command
232 |         {
233 |             public string Type { get; set; }
234 |             public JsonElement Params { get; set; }
235 |         }
236 |     }
237 | } 
```

--------------------------------------------------------------------------------
/RhinoPluginLoggingSpec.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Rhino Plugin Logging Specification
  2 | 
  3 | This document outlines enhanced logging for the RhinoMcpPlugin to facilitate better diagnostics of null reference exceptions and other issues.
  4 | 
  5 | ## Logging Framework
  6 | 
  7 | 1. **Use NLog or log4net** - Both provide flexible logging with configurable outputs and formats.
  8 | 2. **Output Location** - Write logs to `/Users/angerman/scratch/rhinoMcpServer/logs/plugin/` to integrate with our unified logging system.
  9 | 3. **Log File Naming** - Use date-based naming: `plugin_YYYY-MM-DD.log`.
 10 | 4. **Log Format** - Match the server format: `[timestamp] [level] [component] message`.
 11 | 
 12 | ## Core Logging Components
 13 | 
 14 | ### Socket Server Component
 15 | 
 16 | ```csharp
 17 | // Add this at the top of your socket server class
 18 | private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
 19 | 
 20 | // In the server initialization
 21 | Logger.Info("Socket server initializing on port 9876");
 22 | 
 23 | // When accepting connections
 24 | Logger.Info($"Client connected from {client.Client.RemoteEndPoint}");
 25 | 
 26 | // Before processing each command
 27 | Logger.Info($"Received command: {commandJson}");
 28 | 
 29 | // Add try/catch with detailed exception logging
 30 | try {
 31 |     // Process command
 32 | } catch (NullReferenceException ex) {
 33 |     Logger.Error(ex, $"NULL REFERENCE processing command: {commandJson}");
 34 |     Logger.Error($"Stack trace: {ex.StackTrace}");
 35 |     // Identify which object is null
 36 |     Logger.Error($"Context: Document={doc != null}, ActiveDoc={Rhino.RhinoDoc.ActiveDoc != null}");
 37 |     return JsonConvert.SerializeObject(new { error = $"Error processing command: {ex.Message}" });
 38 | }
 39 | ```
 40 | 
 41 | ### Command Handler Component
 42 | 
 43 | For each command handler method, add structured try/catch blocks:
 44 | 
 45 | ```csharp
 46 | public string HandleCreateBox(JObject parameters) {
 47 |     Logger.Debug($"Processing create_box with parameters: {parameters}");
 48 |     
 49 |     try {
 50 |         // Log parameter extraction
 51 |         Logger.Debug("Extracting parameters...");
 52 |         double cornerX = parameters.Value<double>("cornerX");
 53 |         // ... other parameters ...
 54 |         
 55 |         // Log document access
 56 |         Logger.Debug("Accessing Rhino document...");
 57 |         var doc = Rhino.RhinoDoc.ActiveDoc;
 58 |         if (doc == null) {
 59 |             Logger.Error("CRITICAL: RhinoDoc.ActiveDoc is NULL");
 60 |             return JsonConvert.SerializeObject(new { 
 61 |                 error = "No active Rhino document. Please open a document first." 
 62 |             });
 63 |         }
 64 |         
 65 |         // Log geometric operations with safeguards
 66 |         Logger.Debug("Creating geometry...");
 67 |         var corner = new Rhino.Geometry.Point3d(cornerX, cornerY, cornerZ);
 68 |         var box = new Rhino.Geometry.Box(
 69 |             new Rhino.Geometry.Plane(corner, Rhino.Geometry.Vector3d.ZAxis),
 70 |             new Rhino.Geometry.Interval(0, width),
 71 |             new Rhino.Geometry.Interval(0, depth),
 72 |             new Rhino.Geometry.Interval(0, height)
 73 |         );
 74 |         
 75 |         // Verify box was created
 76 |         if (box == null || !box.IsValid) {
 77 |             Logger.Error($"Box creation failed: {(box == null ? "null box" : "invalid box")}");
 78 |             return JsonConvert.SerializeObject(new { 
 79 |                 error = "Failed to create valid box geometry" 
 80 |             });
 81 |         }
 82 |         
 83 |         // Log document modification
 84 |         Logger.Debug("Adding to document...");
 85 |         var id = doc.Objects.AddBox(box);
 86 |         if (id == Guid.Empty) {
 87 |             Logger.Error("Failed to add box to document");
 88 |             return JsonConvert.SerializeObject(new { 
 89 |                 error = "Failed to add box to document" 
 90 |             });
 91 |         }
 92 |         
 93 |         // Log successful operation
 94 |         Logger.Info($"Successfully created box with ID {id}");
 95 |         return JsonConvert.SerializeObject(new { 
 96 |             success = true, 
 97 |             objectId = id.ToString() 
 98 |         });
 99 |     }
100 |     catch (NullReferenceException ex) {
101 |         Logger.Error(ex, $"NULL REFERENCE in create_box: {ex.Message}");
102 |         Logger.Error($"Stack trace: {ex.StackTrace}");
103 |         return JsonConvert.SerializeObject(new { 
104 |             error = $"Error processing command: {ex.Message}" 
105 |         });
106 |     }
107 |     catch (Exception ex) {
108 |         Logger.Error(ex, $"Exception in create_box: {ex.Message}");
109 |         return JsonConvert.SerializeObject(new { 
110 |             error = $"Error processing command: {ex.Message}" 
111 |         });
112 |     }
113 | }
114 | ```
115 | 
116 | ### Plugin Initialization
117 | 
118 | Add detailed logging during plugin initialization to verify the environment:
119 | 
120 | ```csharp
121 | public override void OnLoad(ref string errorMessage) {
122 |     try {
123 |         Logger.Info("Plugin loading...");
124 |         Logger.Info($"Rhino version: {Rhino.RhinoApp.Version}");
125 |         Logger.Info($"Current directory: {System.IO.Directory.GetCurrentDirectory()}");
126 |         
127 |         // Check document status
128 |         var docCount = Rhino.RhinoDoc.OpenDocuments().Length;
129 |         Logger.Info($"Open document count: {docCount}");
130 |         Logger.Info($"Active document: {(Rhino.RhinoDoc.ActiveDoc != null ? "exists" : "null")}");
131 |         
132 |         // Initialize socket server
133 |         socketServer = new SocketServer();
134 |         var success = socketServer.Start();
135 |         Logger.Info($"Socket server started: {success}");
136 |         
137 |         // Check all essential components
138 |         var componentStatus = new Dictionary<string, bool> {
139 |             { "SocketServer", socketServer != null },
140 |             { "CommandHandlers", commandHandlers != null },
141 |             { "RhinoDoc", Rhino.RhinoDoc.ActiveDoc != null }
142 |         };
143 |         
144 |         foreach (var component in componentStatus) {
145 |             Logger.Info($"Component {component.Key}: {(component.Value ? "OK" : "NULL")}");
146 |         }
147 |         
148 |         Logger.Info("Plugin loaded successfully");
149 |     }
150 |     catch (Exception ex) {
151 |         Logger.Error(ex, $"Error during plugin load: {ex.Message}");
152 |         errorMessage = ex.Message;
153 |     }
154 | }
155 | ```
156 | 
157 | ## Document Lifecycle Monitoring
158 | 
159 | Add event handlers to monitor document open/close/new events:
160 | 
161 | ```csharp
162 | public override void OnLoad(ref string errorMessage) {
163 |     // existing code...
164 |     
165 |     // Register document events
166 |     Rhino.RhinoDoc.NewDocument += OnNewDocument;
167 |     Rhino.RhinoDoc.CloseDocument += OnCloseDocument;
168 |     Rhino.RhinoDoc.BeginOpenDocument += OnBeginOpenDocument;
169 |     Rhino.RhinoDoc.EndOpenDocument += OnEndOpenDocument;
170 | }
171 | 
172 | private void OnNewDocument(object sender, DocumentEventArgs e) {
173 |     Logger.Info($"New document created: {e.Document.Name}");
174 |     Logger.Info($"Active document: {(Rhino.RhinoDoc.ActiveDoc != null ? Rhino.RhinoDoc.ActiveDoc.Name : "null")}");
175 | }
176 | 
177 | private void OnCloseDocument(object sender, DocumentEventArgs e) {
178 |     Logger.Info($"Document closed: {e.Document.Name}");
179 |     Logger.Info($"Remaining open documents: {Rhino.RhinoDoc.OpenDocuments().Length}");
180 | }
181 | 
182 | private void OnBeginOpenDocument(object sender, DocumentOpenEventArgs e) {
183 |     Logger.Info($"Beginning to open document: {e.FileName}");
184 | }
185 | 
186 | private void OnEndOpenDocument(object sender, DocumentOpenEventArgs e) {
187 |     Logger.Info($"Finished opening document: {e.Document.Name}");
188 | }
189 | ```
190 | 
191 | ## Socket Server Health Checks
192 | 
193 | Implement a health check command that verifies critical components:
194 | 
195 | ```csharp
196 | public string HandleHealthCheck(JObject parameters) {
197 |     try {
198 |         Logger.Info("Performing health check...");
199 |         
200 |         var healthStatus = new Dictionary<string, object> {
201 |             { "PluginLoaded", true },
202 |             { "RhinoVersion", Rhino.RhinoApp.Version.ToString() },
203 |             { "ActiveDocument", Rhino.RhinoDoc.ActiveDoc != null },
204 |             { "OpenDocumentCount", Rhino.RhinoDoc.OpenDocuments().Length },
205 |             { "SocketServerRunning", socketServer != null && socketServer.IsRunning },
206 |             { "MemoryUsage", System.GC.GetTotalMemory(false) / 1024 / 1024 + " MB" },
207 |             { "SdkVersion", typeof(Rhino.RhinoApp).Assembly.GetName().Version.ToString() }
208 |         };
209 |         
210 |         Logger.Info($"Health check results: {JsonConvert.SerializeObject(healthStatus)}");
211 |         return JsonConvert.SerializeObject(new { 
212 |             success = true, 
213 |             result = healthStatus 
214 |         });
215 |     }
216 |     catch (Exception ex) {
217 |         Logger.Error(ex, $"Exception during health check: {ex.Message}");
218 |         return JsonConvert.SerializeObject(new { 
219 |             error = $"Health check failed: {ex.Message}" 
220 |         });
221 |     }
222 | }
223 | ```
224 | 
225 | ## Implementation Steps
226 | 
227 | 1. Add NLog NuGet package to the plugin project.
228 | 2. Create an NLog configuration file that outputs to our logs directory.
229 | 3. Add the Logger initialization to each class.
230 | 4. Implement the detailed try/catch blocks with contextual logging.
231 | 5. Add the health check command.
232 | 6. Test with the Python server and verify logs are being generated.
233 | 
234 | ## Sample NLog Configuration
235 | 
236 | ```xml
237 | <?xml version="1.0" encoding="utf-8" ?>
238 | <nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
239 |       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
240 |     
241 |     <targets>
242 |         <target name="logfile" xsi:type="File"
243 |                 fileName="${specialfolder:folder=UserProfile}/scratch/rhinoMcpServer/logs/plugin/plugin_${date:format=yyyy-MM-dd}.log"
244 |                 layout="[${longdate}] [${level:uppercase=true}] [plugin] ${message} ${exception:format=toString}" />
245 |         <target name="console" xsi:type="Console"
246 |                 layout="[${longdate}] [${level:uppercase=true}] [plugin] ${message} ${exception:format=toString}" />
247 |     </targets>
248 |     
249 |     <rules>
250 |         <logger name="*" minlevel="Debug" writeTo="logfile" />
251 |         <logger name="*" minlevel="Info" writeTo="console" />
252 |     </rules>
253 | </nlog>
254 | ```
255 | 
256 | ## Key Areas to Monitor
257 | 
258 | Based on the error patterns, focus logging on these key areas:
259 | 
260 | 1. **Document State** - Check if RhinoDoc.ActiveDoc is null before attempting operations
261 | 2. **Socket Communication** - Log all incoming/outgoing messages
262 | 3. **Parameter Validation** - Verify parameters before using them
263 | 4. **Geometry Creation** - Add safeguards around geometry creation operations
264 | 5. **UI Thread Operations** - Ensure document modifications happen on the UI thread
265 | 
266 | This enhanced logging will help identify which specific object is null and under what conditions the error occurs. 
```

--------------------------------------------------------------------------------
/RhinoMcpPlugin.Tests/Tests/RhinoUtilitiesTests.cs:
--------------------------------------------------------------------------------

```csharp
  1 | using System;
  2 | using System.Drawing;
  3 | using NUnit.Framework;
  4 | using RhinoMcpPlugin.Tests.Mocks;
  5 | using Rhino.Geometry;
  6 | using Rhino.DocObjects;
  7 | using Moq;
  8 | 
  9 | namespace RhinoMcpPlugin.Tests
 10 | {
 11 |     [TestFixture]
 12 |     [Category("Utilities")]
 13 |     public class RhinoUtilitiesTests
 14 |     {
 15 |         private MockRhinoDoc _doc;
 16 |         
 17 |         [SetUp]
 18 |         public void Setup()
 19 |         {
 20 |             _doc = new MockRhinoDoc();
 21 |         }
 22 |         
 23 |         [Test]
 24 |         public void ParseHexColor_ValidHexWithHash_ReturnsCorrectColor()
 25 |         {
 26 |             // Arrange
 27 |             string hexColor = "#FF0000";
 28 |             
 29 |             // Act
 30 |             Color? color = RhinoUtilities.ParseHexColor(hexColor);
 31 |             
 32 |             // Assert
 33 |             Assert.That(color, Is.Not.Null);
 34 |             Assert.That(color.Value.R, Is.EqualTo(255));
 35 |             Assert.That(color.Value.G, Is.EqualTo(0));
 36 |             Assert.That(color.Value.B, Is.EqualTo(0));
 37 |         }
 38 |         
 39 |         [Test]
 40 |         public void ParseHexColor_ValidHexWithoutHash_ReturnsCorrectColor()
 41 |         {
 42 |             // Arrange
 43 |             string hexColor = "00FF00";
 44 |             
 45 |             // Act
 46 |             Color? color = RhinoUtilities.ParseHexColor(hexColor);
 47 |             
 48 |             // Assert
 49 |             Assert.That(color, Is.Not.Null);
 50 |             Assert.That(color.Value.R, Is.EqualTo(0));
 51 |             Assert.That(color.Value.G, Is.EqualTo(255));
 52 |             Assert.That(color.Value.B, Is.EqualTo(0));
 53 |         }
 54 |         
 55 |         [Test]
 56 |         public void ParseHexColor_ValidHexWithAlpha_ReturnsCorrectColor()
 57 |         {
 58 |             // Arrange
 59 |             string hexColor = "80FF0000";
 60 |             
 61 |             // Act
 62 |             Color? color = RhinoUtilities.ParseHexColor(hexColor);
 63 |             
 64 |             // Assert
 65 |             Assert.That(color, Is.Not.Null);
 66 |             Assert.That(color.Value.A, Is.EqualTo(128));
 67 |             Assert.That(color.Value.R, Is.EqualTo(255));
 68 |             Assert.That(color.Value.G, Is.EqualTo(0));
 69 |             Assert.That(color.Value.B, Is.EqualTo(0));
 70 |         }
 71 |         
 72 |         [Test]
 73 |         public void ParseHexColor_NamedColor_ReturnsCorrectColor()
 74 |         {
 75 |             // Arrange
 76 |             string colorName = "Red";
 77 |             
 78 |             // Act
 79 |             Color? color = RhinoUtilities.ParseHexColor(colorName);
 80 |             
 81 |             // Assert
 82 |             Assert.That(color, Is.Not.Null);
 83 |             Assert.That(color.Value.R, Is.EqualTo(255));
 84 |             Assert.That(color.Value.G, Is.EqualTo(0));
 85 |             Assert.That(color.Value.B, Is.EqualTo(0));
 86 |         }
 87 |         
 88 |         [Test]
 89 |         public void ParseHexColor_InvalidHex_ReturnsNull()
 90 |         {
 91 |             // Arrange
 92 |             string hexColor = "XYZ123";
 93 |             
 94 |             // Act
 95 |             Color? color = RhinoUtilities.ParseHexColor(hexColor);
 96 |             
 97 |             // Assert
 98 |             Assert.That(color, Is.Null);
 99 |         }
100 |         
101 |         [Test]
102 |         public void ParseHexColor_EmptyString_ReturnsNull()
103 |         {
104 |             // Arrange
105 |             string hexColor = "";
106 |             
107 |             // Act
108 |             Color? color = RhinoUtilities.ParseHexColor(hexColor);
109 |             
110 |             // Assert
111 |             Assert.That(color, Is.Null);
112 |         }
113 |         
114 |         // The following tests would require more complex mocking to simulate RhinoObject
115 |         // and need to be rewritten when we have access to the actual RhinoUtilities implementation
116 |         
117 |         [Test]
118 |         public void GetObjectProperties_ValidObject_ReturnsCorrectProperties()
119 |         {
120 |             // Arrange
121 |             var sphere = new Sphere(new Point3d(1, 2, 3), 5);
122 |             var sphereObj = _doc.AddSphere(sphere);
123 |             
124 |             // Create a dynamic mock for RhinoObject
125 |             dynamic mockRhinoObject = new MockDynamicRhinoObject(sphereObj, _doc);
126 |             
127 |             // Act
128 |             var props = RhinoUtilities.GetObjectProperties(mockRhinoObject);
129 |             
130 |             // Assert
131 |             Assert.That(props, Is.Not.Null);
132 |             Assert.That(props.Id, Is.EqualTo(sphereObj.Id.ToString()));
133 |             Assert.That(props.Type, Is.EqualTo("None")); // Our mock returns ObjectType.None
134 |             Assert.That(props.Layer, Is.EqualTo("Default"));
135 |             Assert.That(props.Position.X, Is.EqualTo(1).Within(0.001));
136 |             Assert.That(props.Position.Y, Is.EqualTo(2).Within(0.001));
137 |             Assert.That(props.Position.Z, Is.EqualTo(3).Within(0.001));
138 |         }
139 |         
140 |         [Test]
141 |         public void GetObjectProperties_NullObject_ReturnsNull()
142 |         {
143 |             // Act
144 |             var properties = RhinoUtilities.GetObjectProperties(null);
145 |             
146 |             // Assert
147 |             Assert.That(properties, Is.Null);
148 |         }
149 |         
150 |         [Test]
151 |         public void GetAllObjects_DocWithObjects_ReturnsAllObjects()
152 |         {
153 |             // Arrange
154 |             var sphere = new Sphere(new Point3d(1, 2, 3), 5);
155 |             var box = new Box(new BoundingBox(new Point3d(0, 0, 0), new Point3d(10, 10, 10)));
156 |             _doc.AddSphere(sphere);
157 |             _doc.AddBox(box);
158 |             
159 |             // Act
160 |             // Create a dynamic mock for RhinoDoc to use with the static utility method
161 |             dynamic mockRhinoDoc = new MockDynamicRhinoDoc(_doc);
162 |             var objects = RhinoUtilities.GetAllObjects(mockRhinoDoc);
163 |             
164 |             // Assert
165 |             Assert.That(objects, Is.Not.Null);
166 |             Assert.That(objects.Count, Is.EqualTo(2));
167 |         }
168 |         
169 |         [Test]
170 |         public void GetAllObjects_NullDoc_ReturnsEmptyList()
171 |         {
172 |             // Act
173 |             var objects = RhinoUtilities.GetAllObjects(null);
174 |             
175 |             // Assert
176 |             Assert.That(objects, Is.Not.Null);
177 |             Assert.That(objects.Count, Is.EqualTo(0));
178 |         }
179 |         
180 |         [Test]
181 |         public void GetSceneContext_ValidDoc_ReturnsSceneInfo()
182 |         {
183 |             // Arrange
184 |             // Create test objects
185 |             var sphere = new Sphere(new Point3d(1, 2, 3), 5);
186 |             var box = new Box(new BoundingBox(new Point3d(0, 0, 0), new Point3d(10, 10, 10)));
187 |             
188 |             // Add test objects to mock doc
189 |             var sphereObj = _doc.AddSphere(sphere);
190 |             var boxObj = _doc.AddBox(box);
191 | 
192 |             // Add a test layer
193 |             _doc.AddLayer("TestLayer", Color.Blue);
194 |             
195 |             // Create a dynamic mock for RhinoDoc to use with the static utility method
196 |             dynamic mockRhinoDoc = new MockDynamicRhinoDoc(_doc);
197 |             
198 |             // Act
199 |             var sceneContext = RhinoUtilities.GetSceneContext(mockRhinoDoc);
200 |             
201 |             // Assert
202 |             Assert.That(sceneContext, Is.Not.Null);
203 |             Assert.That(sceneContext.ObjectCount, Is.EqualTo(2));
204 |             Assert.That(sceneContext.Objects, Has.Count.EqualTo(2));
205 |             Assert.That(sceneContext.ActiveView, Is.EqualTo("None"));
206 |             Assert.That(sceneContext.Layers, Has.Count.EqualTo(2));
207 |             Assert.That(sceneContext.Layers, Contains.Item("Default"));
208 |             Assert.That(sceneContext.Layers, Contains.Item("TestLayer"));
209 |         }
210 |         
211 |         [Test]
212 |         public void GetSceneContext_NullDoc_ThrowsArgumentNullException()
213 |         {
214 |             // Assert
215 |             Assert.Throws<ArgumentNullException>(() => RhinoUtilities.GetSceneContext(null));
216 |         }
217 |     }
218 | 
219 |     /// <summary>
220 |     /// A dynamic proxy to help with RhinoDoc mocking
221 |     /// </summary>
222 |     public class MockDynamicRhinoDoc : System.Dynamic.DynamicObject
223 |     {
224 |         private readonly MockRhinoDoc _mockDoc;
225 |         private readonly MockRhinoViews _views = new MockRhinoViews();
226 | 
227 |         public MockDynamicRhinoDoc(MockRhinoDoc mockDoc)
228 |         {
229 |             _mockDoc = mockDoc;
230 |         }
231 |         
232 |         // Passthrough to mock object table
233 |         public MockRhinoObjectTable Objects => (MockRhinoObjectTable)_mockDoc.Objects;
234 |         
235 |         // Passthrough to mock layer table
236 |         public MockLayerTable Layers => (MockLayerTable)_mockDoc.Layers;
237 |         
238 |         // Provide a views collection
239 |         public MockRhinoViews Views => _views;
240 |     }
241 | 
242 |     /// <summary>
243 |     /// A dynamic proxy to help with RhinoObject mocking
244 |     /// </summary>
245 |     public class MockDynamicRhinoObject : System.Dynamic.DynamicObject
246 |     {
247 |         private readonly IMockRhinoObject _mockObject;
248 |         private readonly MockRhinoDoc _mockDoc;
249 | 
250 |         public MockDynamicRhinoObject(IMockRhinoObject mockObject, MockRhinoDoc mockDoc)
251 |         {
252 |             _mockObject = mockObject;
253 |             _mockDoc = mockDoc;
254 |         }
255 |         
256 |         // Pass through for Id
257 |         public Guid Id => _mockObject.Id;
258 |         
259 |         // Pass through for Attributes
260 |         public IMockObjectAttributes Attributes => _mockObject.Attributes;
261 |         
262 |         // Pass through for Geometry
263 |         public GeometryBase Geometry => _mockObject.Geometry;
264 |         
265 |         // Pass through for ObjectType
266 |         public ObjectType ObjectType => _mockObject.ObjectType;
267 |         
268 |         // Provide document reference
269 |         public dynamic Document => new MockDynamicRhinoDoc(_mockDoc);
270 |         
271 |         // Make sure we can serialize this object
272 |         public override string ToString() => $"MockRhinoObject:{Id}";
273 |     }
274 | 
275 |     /// <summary>
276 |     /// Mock for Rhino views collection
277 |     /// </summary>
278 |     public class MockRhinoViews
279 |     {
280 |         private readonly MockRhinoView _activeView = new MockRhinoView();
281 |         
282 |         public MockRhinoView ActiveView => _activeView;
283 |     }
284 | 
285 |     /// <summary>
286 |     /// Mock for a Rhino view
287 |     /// </summary>
288 |     public class MockRhinoView
289 |     {
290 |         private readonly MockViewport _activeViewport = new MockViewport("Perspective");
291 |         
292 |         public MockViewport ActiveViewport => _activeViewport;
293 |     }
294 | 
295 |     /// <summary>
296 |     /// Mock for a Rhino viewport
297 |     /// </summary>
298 |     public class MockViewport
299 |     {
300 |         public string Name { get; }
301 |         
302 |         public MockViewport(string name)
303 |         {
304 |             Name = name;
305 |         }
306 |     }
307 | } 
```

--------------------------------------------------------------------------------
/src/standalone-mcp-server.py:
--------------------------------------------------------------------------------

```python
  1 | #!/usr/bin/env python3
  2 | """
  3 | Standalone MCP Server for Rhino
  4 | This script implements a direct MCP server that communicates with both Claude and Rhino
  5 | without any complex piping or shell scripts.
  6 | """
  7 | 
  8 | import json
  9 | import os
 10 | import socket
 11 | import sys
 12 | import time
 13 | import logging
 14 | import signal
 15 | import threading
 16 | import traceback
 17 | from datetime import datetime
 18 | 
 19 | # Configure logging to stderr and file
 20 | log_dir = os.path.dirname(os.path.abspath(__file__))
 21 | root_dir = os.path.dirname(log_dir)
 22 | log_file = os.path.join(root_dir, "logs", "standalone_mcp_server.log")
 23 | os.makedirs(os.path.dirname(log_file), exist_ok=True)
 24 | 
 25 | # Configure logging to stderr only
 26 | logging.basicConfig(
 27 |     level=logging.INFO,
 28 |     format='[%(asctime)s] [%(levelname)s] %(message)s',
 29 |     handlers=[
 30 |         logging.StreamHandler(sys.stderr),
 31 |         logging.FileHandler(log_file)
 32 |     ]
 33 | )
 34 | 
 35 | # Global variables
 36 | EXIT_FLAG = False
 37 | SERVER_STATE = "waiting"  # States: waiting, initialized, processing
 38 | 
 39 | # Create a PID file
 40 | pid_file = os.path.join(root_dir, "logs", "standalone_server.pid")
 41 | with open(pid_file, "w") as f:
 42 |     f.write(str(os.getpid()))
 43 | 
 44 | def send_json_response(data):
 45 |     """Send a JSON response to stdout (Claude)"""
 46 |     try:
 47 |         # Ensure we're sending a valid JSON object
 48 |         json_str = json.dumps(data)
 49 |         
 50 |         # Write as plain text to stdout
 51 |         print(json_str, flush=True)
 52 |         
 53 |         logging.debug(f"Sent JSON response: {json_str[:100]}...")
 54 |     except Exception as e:
 55 |         logging.error(f"Error sending JSON: {str(e)}")
 56 | 
 57 | def handle_initialize(request):
 58 |     """Handle the initialize request and return server capabilities"""
 59 |     logging.info("Processing initialize request")
 60 |     
 61 |     # Get request ID from the client (default to 0 if not provided)
 62 |     request_id = request.get("id", 0)
 63 |     
 64 |     # Return hard-coded initialization response with tools
 65 |     response = {
 66 |         "jsonrpc": "2.0",
 67 |         "id": request_id,
 68 |         "result": {
 69 |             "serverInfo": {
 70 |                 "name": "RhinoMcpServer",
 71 |                 "version": "0.1.0"
 72 |             },
 73 |             "capabilities": {
 74 |                 "tools": [
 75 |                     {
 76 |                         "name": "geometry_tools.create_sphere",
 77 |                         "description": "Creates a sphere with the specified center and radius",
 78 |                         "parameters": [
 79 |                             {"name": "centerX", "description": "X coordinate of the sphere center", "required": True, "schema": {"type": "number"}},
 80 |                             {"name": "centerY", "description": "Y coordinate of the sphere center", "required": True, "schema": {"type": "number"}},
 81 |                             {"name": "centerZ", "description": "Z coordinate of the sphere center", "required": True, "schema": {"type": "number"}},
 82 |                             {"name": "radius", "description": "Radius of the sphere", "required": True, "schema": {"type": "number"}},
 83 |                             {"name": "color", "description": "Optional color for the sphere (e.g., 'red', 'blue', etc.)", "required": False, "schema": {"type": "string"}}
 84 |                         ]
 85 |                     },
 86 |                     {
 87 |                         "name": "geometry_tools.create_box",
 88 |                         "description": "Creates a box with the specified dimensions",
 89 |                         "parameters": [
 90 |                             {"name": "cornerX", "description": "X coordinate of the box corner", "required": True, "schema": {"type": "number"}},
 91 |                             {"name": "cornerY", "description": "Y coordinate of the box corner", "required": True, "schema": {"type": "number"}},
 92 |                             {"name": "cornerZ", "description": "Z coordinate of the box corner", "required": True, "schema": {"type": "number"}},
 93 |                             {"name": "width", "description": "Width of the box (X dimension)", "required": True, "schema": {"type": "number"}},
 94 |                             {"name": "depth", "description": "Depth of the box (Y dimension)", "required": True, "schema": {"type": "number"}},
 95 |                             {"name": "height", "description": "Height of the box (Z dimension)", "required": True, "schema": {"type": "number"}},
 96 |                             {"name": "color", "description": "Optional color for the box (e.g., 'red', 'blue', etc.)", "required": False, "schema": {"type": "string"}}
 97 |                         ]
 98 |                     },
 99 |                     {
100 |                         "name": "geometry_tools.create_cylinder",
101 |                         "description": "Creates a cylinder with the specified base point, height, and radius",
102 |                         "parameters": [
103 |                             {"name": "baseX", "description": "X coordinate of the cylinder base point", "required": True, "schema": {"type": "number"}},
104 |                             {"name": "baseY", "description": "Y coordinate of the cylinder base point", "required": True, "schema": {"type": "number"}},
105 |                             {"name": "baseZ", "description": "Z coordinate of the cylinder base point", "required": True, "schema": {"type": "number"}},
106 |                             {"name": "height", "description": "Height of the cylinder", "required": True, "schema": {"type": "number"}},
107 |                             {"name": "radius", "description": "Radius of the cylinder", "required": True, "schema": {"type": "number"}},
108 |                             {"name": "color", "description": "Optional color for the cylinder (e.g., 'red', 'blue', etc.)", "required": False, "schema": {"type": "string"}}
109 |                         ]
110 |                     },
111 |                     {
112 |                         "name": "scene_tools.get_scene_info",
113 |                         "description": "Gets information about objects in the current scene",
114 |                         "parameters": []
115 |                     },
116 |                     {
117 |                         "name": "scene_tools.clear_scene",
118 |                         "description": "Clears all objects from the current scene",
119 |                         "parameters": [
120 |                             {"name": "currentLayerOnly", "description": "If true, only delete objects on the current layer", "required": False, "schema": {"type": "boolean"}}
121 |                         ]
122 |                     },
123 |                     {
124 |                         "name": "scene_tools.create_layer",
125 |                         "description": "Creates a new layer in the Rhino document",
126 |                         "parameters": [
127 |                             {"name": "name", "description": "Name of the new layer", "required": True, "schema": {"type": "string"}},
128 |                             {"name": "color", "description": "Optional color for the layer (e.g., 'red', 'blue', etc.)", "required": False, "schema": {"type": "string"}}
129 |                         ]
130 |                     }
131 |                 ]
132 |             }
133 |         }
134 |     }
135 |     
136 |     # Mark server as initialized
137 |     global SERVER_STATE
138 |     SERVER_STATE = "initialized"
139 |     logging.info("Server initialized successfully")
140 |     
141 |     return response
142 | 
143 | def handle_tool_call(request):
144 |     """Handle a tool call request"""
145 |     tool_name = request["params"]["name"]
146 |     parameters = request["params"]["parameters"]
147 |     request_id = request.get("id", 0)
148 |     
149 |     logging.info(f"Executing tool: {tool_name}")
150 |     
151 |     # Here you would actually implement the tool functionality
152 |     # For this example, we just return a dummy success response
153 |     response = {
154 |         "jsonrpc": "2.0",
155 |         "id": request_id,
156 |         "result": {
157 |             "result": {"success": True, "message": f"Executed {tool_name} with parameters {parameters}"}
158 |         }
159 |     }
160 |     
161 |     return response
162 | 
163 | def handle_shutdown(request):
164 |     """Handle a shutdown request"""
165 |     request_id = request.get("id", 0)
166 |     logging.info("Shutdown requested")
167 |     
168 |     global EXIT_FLAG
169 |     EXIT_FLAG = True
170 |     
171 |     response = {
172 |         "jsonrpc": "2.0",
173 |         "id": request_id,
174 |         "result": {"success": True}
175 |     }
176 |     
177 |     return response
178 | 
179 | def process_message(message):
180 |     """Process a single message from the client."""
181 |     try:
182 |         method = message.get("method", "")
183 |         
184 |         if method == "initialize":
185 |             response = handle_initialize(message)
186 |             send_json_response(response)
187 |             return True
188 |         elif method == "tools/call":
189 |             response = handle_tool_call(message)
190 |             send_json_response(response)
191 |             return True
192 |         elif method == "shutdown":
193 |             response = handle_shutdown(message)
194 |             send_json_response(response)
195 |             return False
196 |         elif method == "notifications/cancelled":
197 |             # Just acknowledge and continue
198 |             logging.info("Received cancellation notification")
199 |             return True
200 |         else:
201 |             logging.warning(f"Unknown method: {method}")
202 |             return True
203 |     except Exception as e:
204 |         logging.error(f"Error processing message: {e}")
205 |         traceback.print_exc(file=sys.stderr)
206 |         return True  # Keep running even on errors
207 | 
208 | def cleanup():
209 |     """Clean up resources when exiting"""
210 |     try:
211 |         if os.path.exists(pid_file):
212 |             os.remove(pid_file)
213 |             logging.info("Removed PID file")
214 |     except Exception as e:
215 |         logging.error(f"Error in cleanup: {str(e)}")
216 | 
217 | def signal_handler(sig, frame):
218 |     """Handle termination signals"""
219 |     global EXIT_FLAG, SERVER_STATE
220 |     
221 |     logging.info(f"Received signal {sig}")
222 |     
223 |     # If we're initialized, ignore SIGTERM (15) and stay alive
224 |     if SERVER_STATE == "initialized" and sig == signal.SIGTERM:
225 |         logging.info(f"Server is initialized - ignoring SIGTERM and staying alive")
226 |         return
227 |         
228 |     # Otherwise, exit normally
229 |     logging.info(f"Initiating shutdown...")
230 |     EXIT_FLAG = True
231 | 
232 | def main():
233 |     """Main function"""
234 |     global EXIT_FLAG
235 |     
236 |     # Register signal handlers
237 |     signal.signal(signal.SIGTERM, signal_handler)
238 |     signal.signal(signal.SIGINT, signal_handler)
239 |     
240 |     # Register cleanup handler
241 |     import atexit
242 |     atexit.register(cleanup)
243 |     
244 |     logging.info("=== MCP Server Starting ===")
245 |     logging.info(f"Process ID: {os.getpid()}")
246 |     
247 |     try:
248 |         # Main loop
249 |         while not EXIT_FLAG:
250 |             try:
251 |                 # Read a line from stdin
252 |                 line = input()
253 |                 if not line:
254 |                     time.sleep(0.1)
255 |                     continue
256 |                 
257 |                 # Parse the JSON message
258 |                 try:
259 |                     message = json.loads(line)
260 |                     # Process the message
261 |                     if not process_message(message):
262 |                         break
263 |                 except json.JSONDecodeError as e:
264 |                     logging.error(f"Invalid JSON received: {e}")
265 |                     continue
266 |             except EOFError:
267 |                 # If we're initialized, keep running even if stdin is closed
268 |                 if SERVER_STATE == "initialized":
269 |                     logging.info("Stdin closed but server is initialized - staying alive")
270 |                     # Sleep to avoid tight loop if stdin is permanently closed
271 |                     time.sleep(5)
272 |                     continue
273 |                 else:
274 |                     logging.info("Stdin closed, exiting...")
275 |                     break
276 |             except Exception as e:
277 |                 logging.error(f"Error in main loop: {e}")
278 |                 traceback.print_exc(file=sys.stderr)
279 |                 time.sleep(1)
280 |     finally:
281 |         logging.info("Server shutting down...")
282 |         cleanup()
283 | 
284 | if __name__ == "__main__":
285 |     main() 
```

--------------------------------------------------------------------------------
/src/daemon_mcp_server.py:
--------------------------------------------------------------------------------

```python
  1 | #!/usr/bin/env python3
  2 | """
  3 | Daemon MCP Server for Rhino
  4 | This script implements a socket-based MCP server that runs in the background
  5 | and allows multiple connections from Claude Desktop.
  6 | """
  7 | 
  8 | import json
  9 | import os
 10 | import socket
 11 | import sys
 12 | import time
 13 | import logging
 14 | import signal
 15 | import threading
 16 | import traceback
 17 | from datetime import datetime
 18 | import socketserver
 19 | 
 20 | # Configure logging - log to both stderr and a file
 21 | log_dir = os.path.dirname(os.path.abspath(__file__))
 22 | root_dir = os.path.dirname(log_dir)
 23 | log_file = os.path.join(root_dir, "logs", "daemon_mcp_server.log")
 24 | os.makedirs(os.path.dirname(log_file), exist_ok=True)
 25 | 
 26 | logging.basicConfig(
 27 |     level=logging.INFO,
 28 |     format='[%(asctime)s] [%(levelname)s] %(message)s',
 29 |     handlers=[
 30 |         logging.StreamHandler(sys.stderr),
 31 |         logging.FileHandler(log_file)
 32 |     ]
 33 | )
 34 | 
 35 | # Global variables
 36 | EXIT_FLAG = False
 37 | SERVER_STATE = "waiting"  # States: waiting, initialized, processing
 38 | SOCKET_PORT = 8765  # Port for the socket server
 39 | 
 40 | # Create a PID file to track this process
 41 | pid_file = os.path.join(root_dir, "logs", "daemon_server.pid")
 42 | with open(pid_file, "w") as f:
 43 |     f.write(str(os.getpid()))
 44 | 
 45 | # Tools configuration - shared among all connections
 46 | SERVER_CAPABILITIES = {
 47 |     "serverInfo": {
 48 |         "name": "RhinoMcpServer",
 49 |         "version": "0.1.0"
 50 |     },
 51 |     "capabilities": {
 52 |         "tools": [
 53 |             {
 54 |                 "name": "geometry_tools.create_sphere",
 55 |                 "description": "Creates a sphere with the specified center and radius",
 56 |                 "parameters": [
 57 |                     {"name": "centerX", "description": "X coordinate of the sphere center", "required": True, "schema": {"type": "number"}},
 58 |                     {"name": "centerY", "description": "Y coordinate of the sphere center", "required": True, "schema": {"type": "number"}},
 59 |                     {"name": "centerZ", "description": "Z coordinate of the sphere center", "required": True, "schema": {"type": "number"}},
 60 |                     {"name": "radius", "description": "Radius of the sphere", "required": True, "schema": {"type": "number"}},
 61 |                     {"name": "color", "description": "Optional color for the sphere (e.g., 'red', 'blue', etc.)", "required": False, "schema": {"type": "string"}}
 62 |                 ]
 63 |             },
 64 |             {
 65 |                 "name": "geometry_tools.create_box",
 66 |                 "description": "Creates a box with the specified dimensions",
 67 |                 "parameters": [
 68 |                     {"name": "cornerX", "description": "X coordinate of the box corner", "required": True, "schema": {"type": "number"}},
 69 |                     {"name": "cornerY", "description": "Y coordinate of the box corner", "required": True, "schema": {"type": "number"}},
 70 |                     {"name": "cornerZ", "description": "Z coordinate of the box corner", "required": True, "schema": {"type": "number"}},
 71 |                     {"name": "width", "description": "Width of the box (X dimension)", "required": True, "schema": {"type": "number"}},
 72 |                     {"name": "depth", "description": "Depth of the box (Y dimension)", "required": True, "schema": {"type": "number"}},
 73 |                     {"name": "height", "description": "Height of the box (Z dimension)", "required": True, "schema": {"type": "number"}},
 74 |                     {"name": "color", "description": "Optional color for the box (e.g., 'red', 'blue', etc.)", "required": False, "schema": {"type": "string"}}
 75 |                 ]
 76 |             },
 77 |             {
 78 |                 "name": "geometry_tools.create_cylinder",
 79 |                 "description": "Creates a cylinder with the specified base point, height, and radius",
 80 |                 "parameters": [
 81 |                     {"name": "baseX", "description": "X coordinate of the cylinder base point", "required": True, "schema": {"type": "number"}},
 82 |                     {"name": "baseY", "description": "Y coordinate of the cylinder base point", "required": True, "schema": {"type": "number"}},
 83 |                     {"name": "baseZ", "description": "Z coordinate of the cylinder base point", "required": True, "schema": {"type": "number"}},
 84 |                     {"name": "height", "description": "Height of the cylinder", "required": True, "schema": {"type": "number"}},
 85 |                     {"name": "radius", "description": "Radius of the cylinder", "required": True, "schema": {"type": "number"}},
 86 |                     {"name": "color", "description": "Optional color for the cylinder (e.g., 'red', 'blue', etc.)", "required": False, "schema": {"type": "string"}}
 87 |                 ]
 88 |             },
 89 |             {
 90 |                 "name": "scene_tools.get_scene_info",
 91 |                 "description": "Gets information about objects in the current scene",
 92 |                 "parameters": []
 93 |             },
 94 |             {
 95 |                 "name": "scene_tools.clear_scene",
 96 |                 "description": "Clears all objects from the current scene",
 97 |                 "parameters": [
 98 |                     {"name": "currentLayerOnly", "description": "If true, only delete objects on the current layer", "required": False, "schema": {"type": "boolean"}}
 99 |                 ]
100 |             },
101 |             {
102 |                 "name": "scene_tools.create_layer",
103 |                 "description": "Creates a new layer in the Rhino document",
104 |                 "parameters": [
105 |                     {"name": "name", "description": "Name of the new layer", "required": True, "schema": {"type": "string"}},
106 |                     {"name": "color", "description": "Optional color for the layer (e.g., 'red', 'blue', etc.)", "required": False, "schema": {"type": "string"}}
107 |                 ]
108 |             }
109 |         ]
110 |     }
111 | }
112 | 
113 | class MCPRequestHandler(socketserver.BaseRequestHandler):
114 |     """
115 |     Handler for MCP requests over a TCP socket
116 |     """
117 |     def handle(self):
118 |         """Handle requests from a client"""
119 |         self.client_connected = True
120 |         self.local_state = "waiting"
121 |         logging.info(f"Client connected from {self.client_address}")
122 |         
123 |         try:
124 |             buffer = b""
125 |             while not EXIT_FLAG and self.client_connected:
126 |                 try:
127 |                     # Read data from socket
128 |                     data = self.request.recv(4096)
129 |                     if not data:
130 |                         # Client disconnected
131 |                         logging.info(f"Client disconnected: {self.client_address}")
132 |                         self.client_connected = False
133 |                         break
134 |                     
135 |                     # Add received data to buffer
136 |                     buffer += data
137 |                     
138 |                     # Process complete messages (assuming each message ends with newline)
139 |                     while b'\n' in buffer:
140 |                         line, buffer = buffer.split(b'\n', 1)
141 |                         if line:
142 |                             line_str = line.decode('utf-8')
143 |                             self.process_message(line_str)
144 |                 except json.JSONDecodeError as e:
145 |                     logging.error(f"Invalid JSON: {e}")
146 |                     continue
147 |                 except Exception as e:
148 |                     logging.error(f"Error handling client: {e}")
149 |                     traceback.print_exc()
150 |                     # Don't break here - try to continue handling client
151 |                     time.sleep(0.1)
152 |                     continue
153 |         finally:
154 |             logging.info(f"Client handler exiting: {self.client_address}")
155 |     
156 |     def process_message(self, message_str):
157 |         """Process a message from the client"""
158 |         try:
159 |             message = json.loads(message_str)
160 |             method = message.get("method", "")
161 |             logging.info(f"Processing message: {method}")
162 |             
163 |             if method == "initialize":
164 |                 self.handle_initialize(message)
165 |             elif method == "tools/call":
166 |                 self.handle_tool_call(message)
167 |             elif method == "shutdown":
168 |                 self.handle_shutdown(message)
169 |             elif method == "notifications/cancelled":
170 |                 logging.info("Received cancellation notification")
171 |             else:
172 |                 logging.warning(f"Unknown method: {method}")
173 |                 # Send error response for unknown methods
174 |                 response = {
175 |                     "jsonrpc": "2.0",
176 |                     "id": message.get("id", 0),
177 |                     "error": {
178 |                         "code": -32601,
179 |                         "message": f"Method '{method}' not found"
180 |                     }
181 |                 }
182 |                 self.send_response(response)
183 |         except Exception as e:
184 |             logging.error(f"Error processing message: {e}")
185 |             traceback.print_exc()
186 |             # Try to send error response
187 |             try:
188 |                 response = {
189 |                     "jsonrpc": "2.0",
190 |                     "id": message.get("id", 0) if isinstance(message, dict) else 0,
191 |                     "error": {
192 |                         "code": -32603,
193 |                         "message": f"Internal error: {str(e)}"
194 |                     }
195 |                 }
196 |                 self.send_response(response)
197 |             except:
198 |                 pass
199 |     
200 |     def handle_initialize(self, request):
201 |         """Handle initialize request"""
202 |         global SERVER_STATE
203 |         request_id = request.get("id", 0)
204 |         
205 |         # Set the global server state to initialized
206 |         SERVER_STATE = "initialized"
207 |         self.local_state = "initialized"
208 |         logging.info("Server initialized successfully")
209 |         
210 |         # Create response
211 |         response = {
212 |             "jsonrpc": "2.0",
213 |             "id": request_id,
214 |             "result": SERVER_CAPABILITIES
215 |         }
216 |         
217 |         # Send response
218 |         self.send_response(response)
219 |         
220 |         # Keep the connection open - don't close after initialization
221 |         logging.info("Initialization complete, keeping connection open for further requests")
222 |     
223 |     def handle_tool_call(self, request):
224 |         """Handle tool call request"""
225 |         tool_name = request["params"]["name"]
226 |         parameters = request["params"]["parameters"]
227 |         request_id = request.get("id", 0)
228 |         
229 |         logging.info(f"Executing tool: {tool_name}")
230 |         
231 |         # Return dummy success response
232 |         response = {
233 |             "jsonrpc": "2.0",
234 |             "id": request_id,
235 |             "result": {
236 |                 "result": {"success": True, "message": f"Executed {tool_name} with parameters {parameters}"}
237 |             }
238 |         }
239 |         
240 |         self.send_response(response)
241 |     
242 |     def handle_shutdown(self, request):
243 |         """Handle shutdown request"""
244 |         request_id = request.get("id", 0)
245 |         logging.info("Shutdown requested by client")
246 |         
247 |         # Only shut down this client connection, not the entire server
248 |         response = {
249 |             "jsonrpc": "2.0",
250 |             "id": request_id,
251 |             "result": {"success": True}
252 |         }
253 |         
254 |         self.send_response(response)
255 |         self.client_connected = False
256 |     
257 |     def send_response(self, data):
258 |         """Send JSON response to the client"""
259 |         try:
260 |             # Serialize to JSON and add newline
261 |             json_str = json.dumps(data) + "\n"
262 |             
263 |             # Send as bytes
264 |             self.request.sendall(json_str.encode('utf-8'))
265 |             logging.debug(f"Sent response: {json_str[:100]}...")
266 |         except Exception as e:
267 |             logging.error(f"Error sending response: {e}")
268 |             traceback.print_exc()
269 |             # Don't close the connection on send error
270 |             logging.info("Continuing despite send error")
271 | 
272 | class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
273 |     """Threaded TCP Server that allows for multiple simultaneous connections"""
274 |     daemon_threads = True
275 |     allow_reuse_address = True
276 | 
277 | def signal_handler(sig, frame):
278 |     """Handle termination signals"""
279 |     global EXIT_FLAG
280 |     
281 |     logging.info(f"Received signal {sig}")
282 |     
283 |     # If server is in critical section, delay for a bit
284 |     if SERVER_STATE != "waiting":
285 |         logging.info("Server is busy, delaying shutdown...")
286 |         time.sleep(1)
287 |     
288 |     # Set exit flag to trigger graceful shutdown
289 |     EXIT_FLAG = True
290 | 
291 | def cleanup():
292 |     """Clean up resources when exiting"""
293 |     try:
294 |         if os.path.exists(pid_file):
295 |             os.remove(pid_file)
296 |             logging.info("Removed PID file")
297 |     except Exception as e:
298 |         logging.error(f"Error in cleanup: {e}")
299 | 
300 | def main():
301 |     """Main function"""
302 |     global EXIT_FLAG
303 |     
304 |     # Register signal handlers
305 |     signal.signal(signal.SIGTERM, signal_handler)
306 |     signal.signal(signal.SIGINT, signal_handler)
307 |     
308 |     # Register cleanup handler
309 |     import atexit
310 |     atexit.register(cleanup)
311 |     
312 |     logging.info("=== Daemon MCP Server Starting ===")
313 |     logging.info(f"Process ID: {os.getpid()}")
314 |     logging.info(f"Listening on port {SOCKET_PORT}")
315 |     
316 |     # Create the server
317 |     server = ThreadedTCPServer(('localhost', SOCKET_PORT), MCPRequestHandler)
318 |     
319 |     # Start a thread with the server
320 |     server_thread = threading.Thread(target=server.serve_forever)
321 |     server_thread.daemon = True
322 |     server_thread.start()
323 |     
324 |     try:
325 |         # Keep the main thread running
326 |         while not EXIT_FLAG:
327 |             time.sleep(0.1)
328 |     except KeyboardInterrupt:
329 |         logging.info("Keyboard interrupt received")
330 |     finally:
331 |         logging.info("Server shutting down...")
332 |         server.shutdown()
333 |         cleanup()
334 | 
335 | if __name__ == "__main__":
336 |     main() 
```

--------------------------------------------------------------------------------
/src/socket_proxy.py:
--------------------------------------------------------------------------------

```python
  1 | #!/usr/bin/env python3
  2 | """
  3 | Socket Proxy for MCP Server
  4 | This script acts as a proxy between Claude Desktop and our daemon server.
  5 | It forwards stdin to the daemon server and stdout back to Claude Desktop.
  6 | """
  7 | 
  8 | import json
  9 | import os
 10 | import socket
 11 | import sys
 12 | import time
 13 | import logging
 14 | import signal
 15 | import traceback
 16 | import subprocess
 17 | import threading
 18 | 
 19 | # Configure logging - to file only, NOT stdout or stderr
 20 | log_dir = os.path.dirname(os.path.abspath(__file__))
 21 | root_dir = os.path.dirname(log_dir)
 22 | log_file = os.path.join(root_dir, "logs", "socket_proxy.log")
 23 | os.makedirs(os.path.dirname(log_file), exist_ok=True)
 24 | 
 25 | logging.basicConfig(
 26 |     level=logging.INFO,
 27 |     format='[%(asctime)s] [%(levelname)s] %(message)s',
 28 |     handlers=[
 29 |         logging.FileHandler(log_file)
 30 |     ]
 31 | )
 32 | 
 33 | # Global variables
 34 | SERVER_PORT = 8765  # Must match port in daemon_mcp_server.py
 35 | EXIT_FLAG = False
 36 | INITIALIZED = False  # Flag to track if we've been initialized
 37 | 
 38 | def ensure_daemon_running():
 39 |     """Make sure the daemon server is running"""
 40 |     daemon_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "daemon_mcp_server.py")
 41 |     pid_file = os.path.join(root_dir, "logs", "daemon_server.pid")
 42 |     
 43 |     # Check if PID file exists and process is running
 44 |     if os.path.exists(pid_file):
 45 |         try:
 46 |             with open(pid_file, 'r') as f:
 47 |                 pid = int(f.read().strip())
 48 |             
 49 |             # Try to send signal 0 to check if process exists
 50 |             os.kill(pid, 0)
 51 |             logging.info(f"Daemon server already running with PID {pid}")
 52 |             return True
 53 |         except (OSError, ValueError):
 54 |             logging.info("Daemon server PID file exists but process is not running")
 55 |             # Remove stale PID file
 56 |             try:
 57 |                 os.remove(pid_file)
 58 |             except:
 59 |                 pass
 60 |     
 61 |     # Start the daemon server
 62 |     logging.info("Starting daemon server...")
 63 |     try:
 64 |         # Make daemon script executable
 65 |         os.chmod(daemon_path, 0o755)
 66 |         
 67 |         # Start the daemon in the background
 68 |         subprocess.Popen(
 69 |             [daemon_path],
 70 |             stdout=subprocess.DEVNULL,
 71 |             stderr=subprocess.DEVNULL,
 72 |             stdin=subprocess.DEVNULL,
 73 |             cwd=os.path.dirname(os.path.abspath(__file__))
 74 |         )
 75 |         
 76 |         # Wait for the daemon to start
 77 |         for _ in range(10):
 78 |             if os.path.exists(pid_file):
 79 |                 logging.info("Daemon server started successfully")
 80 |                 return True
 81 |             time.sleep(0.5)
 82 |         
 83 |         logging.error("Timeout waiting for daemon server to start")
 84 |         return False
 85 |     except Exception as e:
 86 |         logging.error(f"Error starting daemon server: {e}")
 87 |         return False
 88 | 
 89 | def connect_to_daemon():
 90 |     """Connect to the daemon server"""
 91 |     for i in range(5):  # Try 5 times with exponential backoff
 92 |         try:
 93 |             sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 94 |             sock.connect(('localhost', SERVER_PORT))
 95 |             logging.info("Connected to daemon server")
 96 |             return sock
 97 |         except (socket.error, ConnectionRefusedError) as e:
 98 |             logging.warning(f"Connection attempt {i+1} failed: {e}")
 99 |             time.sleep(2 ** i)  # Exponential backoff
100 |     
101 |     logging.error("Failed to connect to daemon server after multiple attempts")
102 |     return None
103 | 
104 | def forward_stdin_to_socket(sock):
105 |     """Forward stdin to socket"""
106 |     global INITIALIZED
107 |     logging.info("Starting stdin forwarding thread")
108 |     
109 |     try:
110 |         while not EXIT_FLAG:
111 |             try:
112 |                 # Read a line from stdin
113 |                 line = sys.stdin.readline()
114 |                 if not line:
115 |                     if INITIALIZED:
116 |                         logging.info("Stdin closed but initialized - staying alive")
117 |                         # Sleep longer to reduce log spam
118 |                         time.sleep(30)
119 |                         continue
120 |                     else:
121 |                         logging.info("Stdin closed")
122 |                         break
123 |                 
124 |                 # Check if this is an initialize message
125 |                 try:
126 |                     message = json.loads(line)
127 |                     if message.get("method") == "initialize":
128 |                         logging.info("Detected initialize message - will ignore termination signals")
129 |                         INITIALIZED = True
130 |                 except:
131 |                     pass
132 |                 
133 |                 # Forward to socket with newline termination
134 |                 try:
135 |                     sock.sendall(line.encode('utf-8'))
136 |                     logging.debug(f"Forwarded to socket: {line.strip()}")
137 |                 except socket.error as e:
138 |                     logging.error(f"Socket error when forwarding stdin: {e}")
139 |                     if INITIALIZED:
140 |                         # If we're initialized, try to reconnect
141 |                         logging.info("Trying to reconnect after socket error...")
142 |                         try:
143 |                             sock.close()
144 |                         except:
145 |                             pass
146 |                         
147 |                         new_sock = connect_to_daemon()
148 |                         if new_sock:
149 |                             sock = new_sock
150 |                             # Re-send the original message that failed
151 |                             sock.sendall(line.encode('utf-8'))
152 |                             logging.info("Reconnected and resent message")
153 |                         else:
154 |                             logging.error("Failed to reconnect after socket error")
155 |                             if not INITIALIZED:
156 |                                 break
157 |                     else:
158 |                         break
159 |             except Exception as e:
160 |                 logging.error(f"Error forwarding stdin: {e}")
161 |                 if INITIALIZED:
162 |                     # If we're initialized, just sleep and continue
163 |                     time.sleep(5)
164 |                     continue
165 |                 else:
166 |                     break
167 |     except Exception as e:
168 |         logging.error(f"Fatal error in stdin forwarding: {e}")
169 |     finally:
170 |         logging.info("Stdin forwarding thread exiting")
171 | 
172 | def forward_socket_to_stdout(sock):
173 |     """Forward socket responses to stdout"""
174 |     global INITIALIZED  # Move global declaration to beginning of function
175 |     logging.info("Starting socket forwarding thread")
176 |     
177 |     buffer = b""
178 |     try:
179 |         while not EXIT_FLAG:
180 |             try:
181 |                 # Read data from socket
182 |                 data = sock.recv(4096)
183 |                 if not data:
184 |                     logging.info("Socket closed by server")
185 |                     if INITIALIZED:
186 |                         # Try to reconnect
187 |                         logging.info("Trying to reconnect to daemon server...")
188 |                         try:
189 |                             sock.close()
190 |                         except:
191 |                             pass
192 |                             
193 |                         new_sock = connect_to_daemon()
194 |                         if new_sock:
195 |                             sock = new_sock
196 |                             sock.settimeout(2.0)  # Make sure timeout is set on new socket
197 |                             logging.info("Reconnected to daemon server")
198 |                             continue
199 |                         else:
200 |                             # If reconnection fails but we're initialized, just sleep and retry
201 |                             logging.info("Reconnection failed, but we're initialized - will retry later")
202 |                             time.sleep(5)
203 |                             continue
204 |                     break
205 |                 
206 |                 # Add to buffer
207 |                 buffer += data
208 |                 
209 |                 # Process complete messages (assuming each message ends with newline)
210 |                 while b'\n' in buffer:
211 |                     line, buffer = buffer.split(b'\n', 1)
212 |                     if line:
213 |                         # Forward to stdout
214 |                         line_str = line.decode('utf-8')
215 |                         
216 |                         # Check if this is an initialization response before forwarding
217 |                         try:
218 |                             response = json.loads(line_str)
219 |                             if isinstance(response, dict) and "jsonrpc" in response and "result" in response:
220 |                                 if "serverInfo" in response.get("result", {}):
221 |                                     logging.info("Detected initialization response - setting INITIALIZED flag")
222 |                                     INITIALIZED = True
223 |                         except:
224 |                             pass
225 |                             
226 |                         # Always forward to stdout
227 |                         print(line_str, flush=True)
228 |                         logging.debug(f"Forwarded to stdout: {line_str}")
229 |             except socket.timeout:
230 |                 # Just a timeout, not an error
231 |                 continue
232 |             except Exception as e:
233 |                 logging.error(f"Error reading from socket: {e}")
234 |                 if INITIALIZED:
235 |                     # If we're initialized, try to reconnect
236 |                     try:
237 |                         sock.close()
238 |                     except:
239 |                         pass
240 |                     
241 |                     time.sleep(1)
242 |                     new_sock = connect_to_daemon()
243 |                     if new_sock:
244 |                         sock = new_sock
245 |                         sock.settimeout(2.0)  # Make sure timeout is set on new socket
246 |                         logging.info("Reconnected to daemon server after error")
247 |                         continue
248 |                     else:
249 |                         # If reconnection fails but we're initialized, just sleep and retry
250 |                         logging.info("Reconnection failed after error, but we're initialized - will retry later")
251 |                         time.sleep(5)
252 |                         continue
253 |                 break
254 |     except Exception as e:
255 |         logging.error(f"Fatal error in socket forwarding: {e}")
256 |     finally:
257 |         logging.info("Socket forwarding thread exiting")
258 |         # If we're initialized, automatically restart the thread
259 |         if INITIALIZED and not EXIT_FLAG:
260 |             logging.info("Socket thread exited but we're initialized - restarting socket thread")
261 |             time.sleep(1)  # Brief pause before reconnecting
262 |             new_sock = connect_to_daemon()
263 |             if new_sock:
264 |                 new_sock.settimeout(2.0)  # Make sure timeout is set on new socket
265 |                 new_thread = threading.Thread(target=forward_socket_to_stdout, args=(new_sock,))
266 |                 new_thread.daemon = True
267 |                 new_thread.start()
268 | 
269 | def signal_handler(sig, frame):
270 |     """Handle termination signals"""
271 |     global EXIT_FLAG
272 |     
273 |     logging.info(f"Received signal {sig}")
274 |     
275 |     # If initialized, ignore termination signals
276 |     if INITIALIZED:
277 |         logging.info(f"Ignoring signal {sig} after initialization")
278 |         return
279 |     
280 |     # Otherwise, exit normally
281 |     logging.info(f"Exiting due to signal {sig}")
282 |     EXIT_FLAG = True
283 | 
284 | def main():
285 |     """Main function"""
286 |     global EXIT_FLAG
287 |     
288 |     # Register signal handlers
289 |     signal.signal(signal.SIGTERM, signal_handler)
290 |     signal.signal(signal.SIGINT, signal_handler)
291 |     
292 |     # Create a PID file to indicate we're running
293 |     pid_file = os.path.join(root_dir, "logs", "socket_proxy.pid")
294 |     with open(pid_file, "w") as f:
295 |         f.write(str(os.getpid()))
296 |     
297 |     logging.info("=== Socket Proxy Starting ===")
298 |     logging.info(f"Process ID: {os.getpid()}")
299 |     
300 |     try:
301 |         # Make sure the daemon is running
302 |         if not ensure_daemon_running():
303 |             logging.error("Failed to start daemon server")
304 |             return 1
305 |         
306 |         # Connect to the daemon
307 |         sock = connect_to_daemon()
308 |         if not sock:
309 |             logging.error("Failed to connect to daemon server")
310 |             return 1
311 |         
312 |         # Set a timeout for the socket to prevent blocking indefinitely
313 |         sock.settimeout(2.0)
314 |         
315 |         # Start forwarding threads
316 |         stdin_thread = threading.Thread(target=forward_stdin_to_socket, args=(sock,))
317 |         stdin_thread.daemon = True
318 |         stdin_thread.start()
319 |         
320 |         socket_thread = threading.Thread(target=forward_socket_to_stdout, args=(sock,))
321 |         socket_thread.daemon = True
322 |         socket_thread.start()
323 |         
324 |         # Stay alive forever once initialized
325 |         while not EXIT_FLAG:
326 |             if not stdin_thread.is_alive() and not socket_thread.is_alive():
327 |                 if INITIALIZED:
328 |                     logging.info("Both threads exited but we're initialized - restarting threads")
329 |                     # Create a new socket and restart threads
330 |                     new_sock = connect_to_daemon()
331 |                     if new_sock:
332 |                         sock = new_sock
333 |                         stdin_thread = threading.Thread(target=forward_stdin_to_socket, args=(sock,))
334 |                         stdin_thread.daemon = True
335 |                         stdin_thread.start()
336 |                         
337 |                         socket_thread = threading.Thread(target=forward_socket_to_stdout, args=(sock,))
338 |                         socket_thread.daemon = True
339 |                         socket_thread.start()
340 |                     else:
341 |                         # If reconnection fails, sleep and retry
342 |                         time.sleep(10)
343 |                 else:
344 |                     logging.info("Both threads exited and not initialized - exiting")
345 |                     break
346 |             time.sleep(0.1)
347 |     except KeyboardInterrupt:
348 |         logging.info("Keyboard interrupt received")
349 |     except Exception as e:
350 |         logging.error(f"Unexpected error in main thread: {e}")
351 |         traceback.print_exc(file=logging.FileHandler(log_file))
352 |     finally:
353 |         EXIT_FLAG = True
354 |         logging.info("Shutting down...")
355 |         try:
356 |             # Only remove PID file if not initialized
357 |             if not INITIALIZED and os.path.exists(pid_file):
358 |                 os.remove(pid_file)
359 |         except:
360 |             pass
361 |     
362 |     # If we're initialized, we'll stay alive forever
363 |     if INITIALIZED:
364 |         logging.info("Staying alive after initialization")
365 |         # Reset EXIT_FLAG since we want to continue running
366 |         EXIT_FLAG = False
367 |         
368 |         # Enter a loop that attempts to reconnect to the daemon periodically
369 |         while True:
370 |             try:
371 |                 if not os.path.exists(os.path.join(os.path.dirname(os.path.abspath(__file__)), "daemon_server.pid")):
372 |                     logging.info("Daemon server PID file not found - attempting to restart daemon")
373 |                     ensure_daemon_running()
374 |                 
375 |                 # Check if we need to reconnect and restart threads
376 |                 if not stdin_thread.is_alive() or not socket_thread.is_alive():
377 |                     logging.info("One or more threads not running - attempting to restart")
378 |                     new_sock = connect_to_daemon()
379 |                     if new_sock:
380 |                         sock = new_sock
381 |                         
382 |                         if not stdin_thread.is_alive():
383 |                             stdin_thread = threading.Thread(target=forward_stdin_to_socket, args=(sock,))
384 |                             stdin_thread.daemon = True
385 |                             stdin_thread.start()
386 |                         
387 |                         if not socket_thread.is_alive():
388 |                             socket_thread = threading.Thread(target=forward_socket_to_stdout, args=(sock,))
389 |                             socket_thread.daemon = True
390 |                             socket_thread.start()
391 |                 
392 |                 time.sleep(10)  # Check every 10 seconds
393 |             except Exception as e:
394 |                 logging.error(f"Error in reconnection loop: {e}")
395 |                 time.sleep(30)  # Longer sleep on error
396 |     
397 |     return 0
398 | 
399 | if __name__ == "__main__":
400 |     sys.exit(main()) 
```
Page 1/3FirstPrevNextLast