#
tokens: 45819/50000 34/36 files (page 1/3)
lines: off (toggle) GitHub
raw markdown copy
This is page 1 of 3. Use http://codebase.md/always-tinkering/rhinomcpserver?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:
--------------------------------------------------------------------------------

```
# .NET build folders
bin/
obj/
publish/

# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
Icon?
ehthumbs.db
Thumbs.db

# IDE files
.vs/
.vscode/
*.user
*.suo
*.userprefs
*.usertasks
*.userosscache
*.sln.docstates

# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/

# Temporary files
*.tmp
*.log

# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# PyInstaller
#  Usually these files are written by a python script from a template
#  before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py
db.sqlite3

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
target/

# Jupyter Notebook
.ipynb_checkpoints

# pyenv
.python-version

# celery beat schedule file
celerybeat-schedule

# SageMath parsed files
*.sage.py

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# mkdocs documentation
/site

# mypy
.mypy_cache/

# Rhino MCP Server specific
*.pid
*.log
logs/
.server_initialized
.DS_Store

# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg

# Project specific
logs/
*.log
logs/*.pid
logs/*.path

# Editors
.idea/
.vscode/
*.swp
*.swo
.DS_Store 
```

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

```markdown
# RhinoMcpPlugin Tests

This project contains unit tests for the RhinoMcpPlugin, a core component of the RhinoMCP project that integrates the Model Context Protocol with Rhino3D.

## Overview

The test suite is organized into several categories of tests:

- **Plugin Lifecycle Tests**: Testing the plugin's initialization, loading, and shutdown processes
- **Socket Server Tests**: Testing the communication layer that handles MCP commands
- **Geometry Tools Tests**: Testing the creation and manipulation of 3D geometry
- **Scene Tools Tests**: Testing scene management functions
- **Utility Tests**: Testing helper functions and utilities

## Getting Started

### Prerequisites

- Visual Studio 2022 or later (or Visual Studio Code with C# extensions)
- .NET 7.0 SDK
- NUnit 3 Test Adapter (for running tests in Visual Studio)

### Building the Tests

1. Open the RhinoMcpPlugin solution in Visual Studio or VS Code
2. Build the RhinoMcpPlugin.Tests project

### Running the Tests

#### From Visual Studio

1. Open the Test Explorer window (Test > Test Explorer)
2. Click "Run All" to run all tests, or select specific tests to run

#### From Visual Studio Code

1. **Install the required VS Code extensions**:
   - C# Dev Kit extension (which includes the C# extension)
   - .NET Core Test Explorer extension

2. **Configure test discovery in VS Code**:
   - Open the workspace settings (File > Preferences > Settings)
   - Search for "test"
   - Under Extensions > .NET Core Test Explorer, set the test project path to include your test project:
     ```json
     "dotnet-test-explorer.testProjectPath": "**/RhinoMcpPlugin.Tests.csproj"
     ```

3. **Run the tests**:
   - You can use the Test Explorer UI (click the flask icon in the sidebar)
   - Click the run or debug icons next to individual tests or test classes
   - Right-click on tests to run, debug, or view specific tests

4. **Debug tests**:
   - Set breakpoints in your test code
   - Use the "Debug Test" option in the Test Explorer
   - Make sure your launch.json is configured correctly for .NET debugging

#### From Command Line

```
dotnet test RhinoMcpPlugin.Tests/RhinoMcpPlugin.Tests.csproj
```

To run a specific category of tests:

```
dotnet test RhinoMcpPlugin.Tests/RhinoMcpPlugin.Tests.csproj --filter "Category=Utilities"
```

To run a specific test class:

```
dotnet test RhinoMcpPlugin.Tests/RhinoMcpPlugin.Tests.csproj --filter "FullyQualifiedName~RhinoUtilitiesTests"
```

## Project Structure

- `/Tests`: Contains all test classes organized by component
- `/Mocks`: Contains mock implementations of Rhino objects for testing
- `/Framework`: Contains test helpers and base classes

## Testing Strategy

### Mocking Approach

Since the RhinoMcpPlugin relies heavily on Rhino's API, we use two mocking strategies:

1. **Custom Mock Classes**: For core Rhino objects like RhinoDoc, we've created custom mock implementations that inherit from Rhino classes.
2. **Moq Framework**: For simpler dependencies and interfaces, we use the Moq library.

### Test Isolation

Each test is designed to be independent and should not rely on the state from other tests. Tests follow this structure:

1. **Arrange**: Set up the test environment and data
2. **Act**: Perform the action being tested
3. **Assert**: Verify the expected outcome
4. **Cleanup**: Release any resources (usually handled in TearDown methods)

## Writing New Tests

### Test Naming Convention

Tests should follow this naming pattern:

```
MethodName_Scenario_ExpectedBehavior
```

For example:
- `ParseHexColor_ValidHexWithHash_ReturnsCorrectColor`
- `Start_WhenCalled_ServerStarts`

### Test Categories

Use NUnit's Category attribute to organize tests:

```csharp
[Test]
[Category("Utilities")]
public void MyTest()
{
    // Test implementation
}
```

Available categories:
- `Utilities`
- `SocketServer`
- `Plugin`
- `Geometry`
- `Commands`

### Sample Test

```csharp
[Test]
[Category("Utilities")]
public void ParseHexColor_ValidHexWithHash_ReturnsCorrectColor()
{
    // Arrange
    string hexColor = "#FF0000";
    
    // Act
    Color? color = RhinoUtilities.ParseHexColor(hexColor);
    
    // Assert
    Assert.That(color, Is.Not.Null);
    Assert.That(color.Value.R, Is.EqualTo(255));
    Assert.That(color.Value.G, Is.EqualTo(0));
    Assert.That(color.Value.B, Is.EqualTo(0));
}
```

## Common Issues and Solutions

### Test Cannot Find RhinoCommon.dll

Make sure the project has a correct reference to RhinoCommon.dll. In the `.csproj` file, the reference should be:

```xml
<Reference Include="RhinoCommon">
  <HintPath>$(RhinoPath)\RhinoCommon.dll</HintPath>
  <Private>False</Private>
</Reference>
```

You may need to set the `RhinoPath` environment variable to your Rhino installation directory.

### VS Code Test Discovery Issues

If VS Code is not discovering your tests:

1. Make sure you've installed the .NET Core Test Explorer extension
2. Check that your settings.json includes the correct test project path
3. Try refreshing the test explorer (click the refresh icon)
4. Ensure your tests have the `[Test]` attribute and are in public classes

### Socket Server Tests Are Flaky

Socket server tests can sometimes be flaky due to timing issues. Try:

1. Increasing the sleep duration between server start and client connection
2. Using dedicated test ports to avoid conflicts
3. Adding retry logic for connection attempts

## Contributing

When contributing new tests:

1. Follow the established naming conventions and patterns
2. Add tests to the appropriate test class or create a new one if needed
3. Use appropriate assertions for clear failure messages
4. Document any complex test scenarios
5. Run the full test suite before submitting changes

## References

- [NUnit Documentation](https://docs.nunit.org/)
- [Moq Documentation](https://github.com/moq/moq4)
- [RhinoCommon API Reference](https://developer.rhino3d.com/api/rhinocommon/)
- [VS Code .NET Testing](https://code.visualstudio.com/docs/languages/dotnet#_testing)
- [.NET Core Test Explorer](https://marketplace.visualstudio.com/items?itemName=formulahendry.dotnet-test-explorer) 
```

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

```markdown
# Rhino MCP Server

> **⚠️ UNDER CONSTRUCTION ⚠️**  
> This project is currently under active development and is not yet in working order. The Rhino plugin is experiencing issues with creating objects.
> We are actively seeking support from the community to help resolve these issues.
> If you have experience with Rhino API development, C# plugins, or MCP integration, please consider contributing.
> Contact us by opening an issue on GitHub.

A Model Context Protocol (MCP) server implementation for Rhino 3D, allowing Claude to create and manipulate 3D objects.

## Overview

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.

## System Architecture

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:

```mermaid
graph TD
    %% Client Applications
    client[Client Applications] --> socketProxy
    
    %% Socket Proxy
    subgraph "Python Socket Proxy"
        socketProxy[socket_proxy.py] --> daemonServer
    end
    
    %% Daemon Server
    subgraph "Python Daemon Server"
        daemonServer[daemon_mcp_server.py] --> combinedServer
    end
    
    %% Combined MCP Server
    subgraph "Python Combined MCP Server"
        combinedServer[combined_mcp_server.py]
        mcp[FastMCP] --> tools
        combinedServer --> mcp
        combinedServer --> rhinoConn
        subgraph "MCP Tools"
            tools[MCP Tool Methods]
        end
        rhinoConn[RhinoConnection]
    end
    
    %% Rhino Plugin Components
    subgraph "C# Rhino Plugin"
        rhinoPlugin[RhinoMcpPlugin.cs]
        socketServer[RhinoSocketServer.cs]
        utilities[RhinoUtilities.cs]
        commands[RhinoMcpCommand.cs]
        
        rhinoPlugin --> socketServer
        rhinoPlugin --> commands
        socketServer --> utilities
    end
    
    %% Connections between components
    rhinoConn <==> socketServer
    
    %% Logger Components
    subgraph "Logging System"
        logManager[log_manager.py]
        nlogConfig[NLog.config]
    end
    
    combinedServer --> logManager
    rhinoPlugin --> nlogConfig
    
    %% Connection to Rhino
    rhino[Rhino 3D Software]
    rhinoPlugin --> rhino
    
    classDef pythonClass fill:#3572A5,color:white;
    classDef csharpClass fill:#178600,color:white;
    classDef rhinoClass fill:#555555,color:white;
    
    class socketProxy,daemonServer,combinedServer,mcp,tools,rhinoConn,logManager pythonClass;
    class rhinoPlugin,socketServer,utilities,commands csharpClass;
    class rhino rhinoClass;
```

For more detailed information about the system architecture, including component descriptions and data flow, see [code_architecture.md](code_architecture.md).

## Components

There are several implementations available:

1. **Combined MCP Server (Recommended)**: 
   - `combined_mcp_server.py` - Direct implementation that uses stdin/stdout for communication

2. **Socket-based Servers**:
   - `daemon_mcp_server.py` - Background server that receives commands via socket connection
   - `socket_proxy.py` - Proxy that forwards commands from stdin to the daemon server and back
   
3. **Standalone Server**:
   - `standalone-mcp-server.py` - Original standalone implementation

## Setup Instructions

### 1. Set up Claude Desktop

1. Install Claude Desktop if you haven't already
2. Configure the MCP server connection in Claude Desktop settings

### 2. Run the Server

We now have a unified server launcher that allows you to run any of the server implementations:

```bash
./server_launcher.sh [mode]
```

Available modes:
- `combined` (default) - Run the combined MCP server
- `standalone` - Run the standalone MCP server
- `daemon` - Run the daemon MCP server
- `socket-proxy` - Run the socket proxy
- `direct` - Run both daemon and socket proxy
- `logs` - View recent logs
- `monitor` - Monitor logs in real-time
- `errors` - View recent errors
- `help` - Show help message

Examples:
```bash
# Run the combined server (recommended)
./server_launcher.sh combined

# Or simply
./server_launcher.sh

# Run the socket-based approach (daemon + proxy)
./server_launcher.sh direct

# Monitor logs in real-time
./server_launcher.sh monitor
```

## Available Tools

The server provides several tools for 3D modeling:

1. **geometry_tools.create_sphere** - Create a sphere with specified center and radius
2. **geometry_tools.create_box** - Create a box with specified dimensions
3. **geometry_tools.create_cylinder** - Create a cylinder with specified parameters
4. **scene_tools.get_scene_info** - Get information about the current scene
5. **scene_tools.clear_scene** - Clear objects from the scene
6. **scene_tools.create_layer** - Create a new layer in the document

## Troubleshooting

If you encounter connection issues:

1. Make sure no old servers are running:
   ```bash
   ./server_launcher.sh help  # This will clean up existing processes
   ```

2. Check the log files:
   ```bash
   ./server_launcher.sh logs   # View logs
   ./server_launcher.sh errors # View errors
   ```

3. Restart Claude Desktop completely

## License

This project is released under the MIT License. See the LICENSE file for details. 

## Improved Logging System

The system features a unified logging framework that centralizes logs from all components:

- Server logs
- Plugin logs
- Claude AI logs
- Diagnostic logs

All logs follow a consistent format and are stored in the `logs/` directory with separate subdirectories for each component.

### Log Management

A log management tool is provided that offers powerful capabilities for viewing, monitoring, and analyzing logs:

```bash
# View logs
./server_launcher.sh logs

# Monitor logs in real-time
./server_launcher.sh monitor

# View errors with context
./server_launcher.sh errors

# Generate error reports (using the log manager directly)
./log_manager.py report
```

For detailed information on using the logging system, see [LOGGING.md](LOGGING.md).

## Development

### Project Structure

- `combined_mcp_server.py`: Main MCP server implementation
- `diagnose_rhino_connection.py`: Diagnostic tool for testing Rhino connection
- `log_manager.py`: Tool for managing and analyzing logs
- `server_launcher.sh`: Unified script to start any server implementation
- `logs/`: Directory containing all logs

### Adding New Features

1. Add new tools as methods in the `combined_mcp_server.py` file
2. Use the existing logging framework for consistent error handling
3. Update diagnostic tools if needed 
```

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

```csharp
 
```

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

```python
#!/usr/bin/env python3
import sys
import json
import os

def main():
    log_file = os.environ.get('JSON_LOG_FILE', '/dev/null')
    with open(log_file, 'a') as log:
        line_count = 0
        for line in sys.stdin:
            line_count += 1
            line = line.strip()
            if not line:
                continue
                
            try:
                # Try to parse as JSON to validate
                parsed = json.loads(line)
                # If successful, write to stdout for Claude to consume
                print(line)
                sys.stdout.flush()
                log.write(f"VALID JSON [{line_count}]: {line[:100]}...\n")
                log.flush()
            except json.JSONDecodeError as e:
                # If invalid JSON, log it but don't pass to stdout
                log.write(f"INVALID JSON [{line_count}]: {str(e)} in: {line[:100]}...\n")
                log.flush()

if __name__ == "__main__":
    main()

```

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

```csharp
using System.Reflection;
using System.Runtime.InteropServices;
using Rhino.PlugIns;

// General plugin information
[assembly: AssemblyTitle("RhinoMcpPlugin")]
[assembly: AssemblyDescription("Rhino plugin that hosts an MCP server for AI-assisted 3D modeling")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("RhinoMcpPlugin")]
[assembly: AssemblyCopyright("Copyright © 2024")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]

// Plugin Guid - unique identifier
[assembly: Guid("A3C5A369-C051-4732-B1A7-F3C1C8A9EC2D")]

// Plugin version
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

// The plugin type
[assembly: PlugInDescription(DescriptionType.Address, "")]
[assembly: PlugInDescription(DescriptionType.Country, "")]
[assembly: PlugInDescription(DescriptionType.Email, "")]
[assembly: PlugInDescription(DescriptionType.Organization, "")]
[assembly: PlugInDescription(DescriptionType.Phone, "")]
[assembly: PlugInDescription(DescriptionType.WebSite, "")] 
```

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

```bash
#!/bin/bash
# Direct launcher for daemon server and socket proxy
# This script launches both components separately

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"

# Function to log messages
log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
}

# Function to clean up on exit
cleanup() {
    log "Cleaning up..."
    
    pkill -f "daemon_mcp_server.py|socket_proxy.py"
    
    # Remove PID files
    rm -f "$ROOT_DIR/logs/daemon_server.pid" "$ROOT_DIR/logs/socket_proxy.pid"
}

# Set up trap for cleanup
trap cleanup EXIT INT TERM

# Make sure scripts are executable
chmod +x "$ROOT_DIR/src/daemon_mcp_server.py" "$ROOT_DIR/src/socket_proxy.py"

# Check if the scripts exist
if [ ! -x "$ROOT_DIR/src/daemon_mcp_server.py" ]; then
    log "Error: daemon_mcp_server.py not found or not executable"
    exit 1
fi

if [ ! -x "$ROOT_DIR/src/socket_proxy.py" ]; then
    log "Error: socket_proxy.py not found or not executable"
    exit 1
fi

# Make sure no old processes are running
cleanup

# Start the daemon server in the background
log "Starting daemon server..."
"$ROOT_DIR/src/daemon_mcp_server.py" &
daemon_pid=$!
log "Daemon server started with PID: $daemon_pid"

# Wait for daemon to initialize
sleep 2

# Start the socket proxy in the foreground
log "Starting socket proxy..."
"$ROOT_DIR/src/socket_proxy.py"

# Script will clean up on exit 
```

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

```bash
#!/bin/bash
# Run combined MCP server for Rhino

# Ensure the script directory is always available
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
cd "$SCRIPT_DIR"

# Create log directories if they don't exist
LOG_ROOT="$SCRIPT_DIR/logs"
mkdir -p "$LOG_ROOT/server"
mkdir -p "$LOG_ROOT/plugin"
mkdir -p "$LOG_ROOT/claude"
mkdir -p "$LOG_ROOT/diagnostics"

# Check if the MCP server is already running
if [ -f "$LOG_ROOT/combined_server.pid" ]; then
    PID=$(cat "$LOG_ROOT/combined_server.pid")
    if ps -p $PID > /dev/null; then
        echo "MCP server is already running with PID $PID"
        echo "To stop it, use: kill $PID"
        echo "To view logs in real-time: ./log_manager.py monitor"
        exit 1
    else
        echo "Stale PID file found. Removing..."
        rm "$LOG_ROOT/combined_server.pid"
    fi
fi

# Check if Python is installed
if ! command -v python3 &> /dev/null; then
    echo "Python 3 is not installed. Please install Python 3 and try again."
    exit 1
fi

# Display information about logging
echo "Starting RhinoMcpServer with unified logging system"
echo "Log files will be created in: $LOG_ROOT"
echo "To monitor logs in real-time: ./log_manager.py monitor"
echo "To view error reports: ./log_manager.py errors"
echo "For more information: cat LOGGING.md"
echo ""

# Run the combined MCP server
chmod +x combined_mcp_server.py
python3 combined_mcp_server.py 
```

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

```csharp
using System;
using Rhino;
using Rhino.Commands;
using Rhino.UI;

namespace RhinoMcpPlugin
{
    /// <summary>
    /// A Rhino command to control the MCP server
    /// </summary>
    public class RhinoMcpCommand : Command
    {
        /// <summary>
        /// Constructor for RhinoMcpCommand
        /// </summary>
        public RhinoMcpCommand()
        {
            Instance = this;
        }

        /// <summary>
        /// The only instance of the RhinoMcpCommand command
        /// </summary>
        public static RhinoMcpCommand Instance { get; private set; }

        /// <summary>
        /// The command name as it appears on the Rhino command line
        /// </summary>
        public override string EnglishName => "RhinoMCP";

        /// <summary>
        /// Called when the user runs the command
        /// </summary>
        protected override Result RunCommand(RhinoDoc doc, RunMode mode)
        {
            // Display a dialog with information about the MCP server
            Dialogs.ShowMessage(
                "RhinoMCP Plugin\n\n" +
                "This plugin hosts an MCP server that allows AI systems to create and manipulate 3D models in Rhino.\n\n" +
                "To use this plugin with Claude Desktop:\n" +
                "1. Make sure Rhino is running with this plugin loaded\n" +
                "2. Configure Claude Desktop to use this MCP server\n" +
                "3. Start interacting with Claude to create 3D models\n\n" +
                "All operations from AI systems will require your explicit consent.",
                "RhinoMCP Plugin"
            );

            return Result.Success;
        }
    }
} 
```

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

```bash
#!/bin/bash
# Wrapper script for the combined MCP server

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"

# Function to log messages to stderr
log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >&2
}

# Function to clean up on exit
cleanup() {
    log "Cleaning up..."
    
    # Check if the server PID file exists
    PID_FILE="$ROOT_DIR/logs/combined_server.pid"
    if [ -f "$PID_FILE" ]; then
        SERVER_PID=$(cat "$PID_FILE")
        log "Found server PID: $SERVER_PID"
        
        # Check if the process is running
        if kill -0 $SERVER_PID 2>/dev/null; then
            log "Stopping server process..."
            kill -TERM $SERVER_PID
            
            # Wait for process to exit
            for i in {1..5}; do
                if ! kill -0 $SERVER_PID 2>/dev/null; then
                    log "Server process stopped"
                    break
                fi
                log "Waiting for server to exit (attempt $i)..."
                sleep 1
            done
            
            # Force kill if still running
            if kill -0 $SERVER_PID 2>/dev/null; then
                log "Server still running, force killing..."
                kill -9 $SERVER_PID
            fi
        else
            log "Server process not running"
        fi
        
        # Remove PID file
        rm -f "$PID_FILE"
    fi
}

# Set up trap for cleanup
trap cleanup EXIT INT TERM

# Make the server script executable
chmod +x "$ROOT_DIR/src/combined_mcp_server.py"

# Check if the server script exists
if [ ! -x "$ROOT_DIR/src/combined_mcp_server.py" ]; then
    log "Error: combined_mcp_server.py not found or not executable"
    exit 1
fi

# Make sure no old processes are running
cleanup

# Set up Python's stdin/stdout
export PYTHONUNBUFFERED=1
export PYTHONIOENCODING=utf-8

# Clear any existing input
while read -t 0; do read -r; done

# Start the server in the foreground
log "Starting combined MCP server..."
exec "$ROOT_DIR/src/combined_mcp_server.py" 
```

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

```bash
#!/bin/bash
# Wrapper script for the standalone MCP server
# This script is meant to be used with Claude Desktop

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"

# Function to log messages to stderr
log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >&2
}

# Function to clean up on exit
cleanup() {
    log "Cleaning up..."
    
    # Check if the server PID file exists
    PID_FILE="$ROOT_DIR/logs/standalone_server.pid"
    if [ -f "$PID_FILE" ]; then
        SERVER_PID=$(cat "$PID_FILE")
        log "Found server PID: $SERVER_PID"
        
        # Check if the process is running
        if kill -0 $SERVER_PID 2>/dev/null; then
            log "Stopping server process..."
            kill -TERM $SERVER_PID
            
            # Wait for process to exit
            for i in {1..5}; do
                if ! kill -0 $SERVER_PID 2>/dev/null; then
                    log "Server process stopped"
                    break
                fi
                log "Waiting for server to exit (attempt $i)..."
                sleep 1
            done
            
            # Force kill if still running
            if kill -0 $SERVER_PID 2>/dev/null; then
                log "Server still running, force killing..."
                kill -9 $SERVER_PID
            fi
        else
            log "Server process not running"
        fi
        
        # Remove PID file
        rm -f "$PID_FILE"
    fi
}

# Set up trap for cleanup
trap cleanup EXIT INT TERM

# Make sure the server script is executable
chmod +x "$ROOT_DIR/src/standalone-mcp-server.py"

# Check if the server script exists
if [ ! -x "$ROOT_DIR/src/standalone-mcp-server.py" ]; then
    log "Error: standalone-mcp-server.py not found or not executable"
    exit 1
fi

# Make sure no old processes are running
cleanup

# Set up Python's stdin/stdout
export PYTHONUNBUFFERED=1
export PYTHONIOENCODING=utf-8

# Clear any existing input
while read -t 0; do read -r; done

# Start the server
log "Starting standalone MCP server..."
exec "$ROOT_DIR/src/standalone-mcp-server.py" 
```

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

```
<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    
    <targets>
        <!-- File target for plugin logs -->
        <target name="pluginlog" xsi:type="File"
                fileName="${specialfolder:folder=UserProfile}/scratch/rhinoMcpServer/logs/plugin/plugin_${date:format=yyyy-MM-dd}.log"
                layout="[${longdate}] [${level:uppercase=true}] [plugin] ${message} ${exception:format=toString}" 
                archiveFileName="${specialfolder:folder=UserProfile}/scratch/rhinoMcpServer/logs/plugin/archive/plugin_{#}.log"
                archiveEvery="Day"
                archiveNumbering="Date"
                maxArchiveFiles="7"
                concurrentWrites="true"
                keepFileOpen="false" />
                
        <!-- Debug file with all details -->
        <target name="debuglog" xsi:type="File"
                fileName="${specialfolder:folder=UserProfile}/scratch/rhinoMcpServer/logs/plugin/debug_${date:format=yyyy-MM-dd}.log"
                layout="[${longdate}] [${level:uppercase=true}] [plugin] ${logger} - ${message} ${exception:format=toString}" 
                archiveFileName="${specialfolder:folder=UserProfile}/scratch/rhinoMcpServer/logs/plugin/archive/debug_{#}.log"
                archiveEvery="Day"
                archiveNumbering="Date"
                maxArchiveFiles="3"
                concurrentWrites="true"
                keepFileOpen="false" />
                
        <!-- Console output -->
        <target name="console" xsi:type="Console"
                layout="[${time}] [${level:uppercase=true}] ${message}" />
    </targets>
    
    <rules>
        <!-- Write all messages to the plugin log file -->
        <logger name="*" minlevel="Info" writeTo="pluginlog" />
        
        <!-- Write debug and trace messages to the debug log -->
        <logger name="*" minlevel="Debug" writeTo="debuglog" />
        
        <!-- Output to console for immediate feedback -->
        <logger name="*" minlevel="Info" writeTo="console" />
    </rules>
</nlog> 
```

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

```csharp
using System;
using System.Reflection;
using NUnit.Framework;
using Moq;
using RhinoMcpPlugin.Tests.Mocks;
using Rhino;
using Rhino.PlugIns;

namespace RhinoMcpPlugin.Tests
{
    [TestFixture]
    [Category("Plugin")]
    public class RhinoMcpPluginTests
    {
        private global::RhinoMcpPlugin.RhinoMcpPlugin _plugin;
        
        [SetUp]
        public void Setup()
        {
            // Create a mock RhinoDoc that will be used during testing
            new MockRhinoDoc();
            
            // Create plugin instance
            _plugin = new global::RhinoMcpPlugin.RhinoMcpPlugin();
        }
        
        [Test]
        public void Constructor_WhenCalled_SetsInstanceProperty()
        {
            // Assert
            Assert.That(global::RhinoMcpPlugin.RhinoMcpPlugin.Instance, Is.EqualTo(_plugin));
        }
        
        // The following tests would require more sophisticated mocking of the Rhino environment
        // We'll implement simplified versions focusing on basic plugin functionality
        
        /*
        [Test]
        public void OnLoad_WhenCalled_StartsSocketServer()
        {
            // This test would need to be adapted based on how the RhinoMcpPlugin 
            // interacts with RhinoDoc and how it initializes the socket server
        }
        
        [Test]
        public void OnShutdown_AfterOnLoad_StopsSocketServer()
        {
            // This test would need to be adapted based on how the RhinoMcpPlugin 
            // manages its resources and shuts down the socket server
        }
        
        [Test]
        public void OnActiveDocumentChanged_WhenCalled_UpdatesActiveDoc()
        {
            // This test would need to be adapted based on how the RhinoMcpPlugin
            // handles document change events
        }
        */
        
        [Test]
        public void RhinoConsentTool_RequestConsent_ReturnsTrue()
        {
            // Act
            var result = global::RhinoMcpPlugin.RhinoMcpPlugin.RhinoConsentTool.RequestConsent("Test consent message");
            
            // Assert
            Assert.That(result, Is.True);
        }
    }
} 
```

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

```csharp
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;

namespace RhinoMcpPlugin.Models
{
    /// <summary>
    /// Properties of a Rhino object that can be exposed via MCP
    /// </summary>
    public class RhinoObjectProperties
    {
        /// <summary>
        /// The object's unique identifier
        /// </summary>
        [JsonPropertyName("id")]
        public string Id { get; set; }

        /// <summary>
        /// The type of object (e.g., "Curve", "Surface", "Mesh")
        /// </summary>
        [JsonPropertyName("type")]
        public string Type { get; set; }

        /// <summary>
        /// The layer the object is on
        /// </summary>
        [JsonPropertyName("layer")]
        public string Layer { get; set; }

        /// <summary>
        /// The name of the object (if any)
        /// </summary>
        [JsonPropertyName("name")]
        public string Name { get; set; }

        /// <summary>
        /// The color of the object in hex format (e.g., "#FF0000")
        /// </summary>
        [JsonPropertyName("color")]
        public string Color { get; set; }

        /// <summary>
        /// The position (centroid) of the object
        /// </summary>
        [JsonPropertyName("position")]
        public Position Position { get; set; }

        /// <summary>
        /// The bounding box of the object
        /// </summary>
        [JsonPropertyName("bbox")]
        public BoundingBox BoundingBox { get; set; }
    }

    /// <summary>
    /// Represents a 3D position
    /// </summary>
    public class Position
    {
        [JsonPropertyName("x")]
        public double X { get; set; }

        [JsonPropertyName("y")]
        public double Y { get; set; }

        [JsonPropertyName("z")]
        public double Z { get; set; }
    }

    /// <summary>
    /// Represents a bounding box with min and max points
    /// </summary>
    public class BoundingBox
    {
        [JsonPropertyName("min")]
        public Position Min { get; set; }

        [JsonPropertyName("max")]
        public Position Max { get; set; }
    }

    /// <summary>
    /// Represents the current state of the Rhino scene
    /// </summary>
    public class SceneContext
    {
        /// <summary>
        /// The number of objects in the scene
        /// </summary>
        [JsonPropertyName("object_count")]
        public int ObjectCount { get; set; }

        /// <summary>
        /// The objects in the scene
        /// </summary>
        [JsonPropertyName("objects")]
        public List<RhinoObjectProperties> Objects { get; set; }

        /// <summary>
        /// The name of the active view
        /// </summary>
        [JsonPropertyName("active_view")]
        public string ActiveView { get; set; }

        /// <summary>
        /// The layers in the document
        /// </summary>
        [JsonPropertyName("layers")]
        public List<string> Layers { get; set; }
    }
} 
```

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

```csharp
using System;
using System.Drawing;
using NUnit.Framework;
using RhinoMcpPlugin;

namespace RhinoMcpPlugin.Tests
{
    [TestFixture]
    [Category("ColorUtil")]
    public class ColorUtilTests
    {
        [Test]
        public void ParseHexColor_ValidHexWithHash_ReturnsCorrectColor()
        {
            // Arrange
            string hexColor = "#FF0000";
            
            // Act
            Color? color = RhinoUtilities.ParseHexColor(hexColor);
            
            // Assert
            Assert.That(color, Is.Not.Null);
            Assert.That(color.Value.R, Is.EqualTo(255));
            Assert.That(color.Value.G, Is.EqualTo(0));
            Assert.That(color.Value.B, Is.EqualTo(0));
        }
        
        [Test]
        public void ParseHexColor_ValidHexWithoutHash_ReturnsCorrectColor()
        {
            // Arrange
            string hexColor = "00FF00";
            
            // Act
            Color? color = RhinoUtilities.ParseHexColor(hexColor);
            
            // Assert
            Assert.That(color, Is.Not.Null);
            Assert.That(color.Value.R, Is.EqualTo(0));
            Assert.That(color.Value.G, Is.EqualTo(255));
            Assert.That(color.Value.B, Is.EqualTo(0));
        }
        
        [Test]
        public void ParseHexColor_ValidHexWithAlpha_ReturnsCorrectColor()
        {
            // Arrange
            string hexColor = "80FF0000";
            
            // Act
            Color? color = RhinoUtilities.ParseHexColor(hexColor);
            
            // Assert
            Assert.That(color, Is.Not.Null);
            Assert.That(color.Value.A, Is.EqualTo(128));
            Assert.That(color.Value.R, Is.EqualTo(255));
            Assert.That(color.Value.G, Is.EqualTo(0));
            Assert.That(color.Value.B, Is.EqualTo(0));
        }
        
        [Test]
        public void ParseHexColor_NamedColor_ReturnsCorrectColor()
        {
            // Arrange
            string colorName = "Red";
            
            // Act
            Color? color = RhinoUtilities.ParseHexColor(colorName);
            
            // Assert
            Assert.That(color, Is.Not.Null);
            Assert.That(color.Value.R, Is.EqualTo(255));
            Assert.That(color.Value.G, Is.EqualTo(0));
            Assert.That(color.Value.B, Is.EqualTo(0));
        }
        
        [Test]
        public void ParseHexColor_InvalidHex_ReturnsNull()
        {
            // Arrange
            string hexColor = "XYZ123";
            
            // Act
            Color? color = RhinoUtilities.ParseHexColor(hexColor);
            
            // Assert
            Assert.That(color, Is.Null);
        }
        
        [Test]
        public void ParseHexColor_EmptyString_ReturnsNull()
        {
            // Arrange
            string hexColor = "";
            
            // Act
            Color? color = RhinoUtilities.ParseHexColor(hexColor);
            
            // Assert
            Assert.That(color, Is.Null);
        }
    }
} 
```

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

```csharp
using System;
using System.Net.Sockets;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;
using RhinoMcpPlugin.Tests.Mocks;

namespace RhinoMcpPlugin.Tests
{
    [TestFixture]
    [Category("SocketServer")]
    public class RhinoSocketServerTests
    {
        private global::RhinoMcpPlugin.RhinoSocketServer _server;
        private int _testPort = 9877; // Use a different port than default for testing
        
        [SetUp]
        public void Setup()
        {
            _server = new global::RhinoMcpPlugin.RhinoSocketServer(_testPort);
        }
        
        [TearDown]
        public void TearDown()
        {
            _server.Stop();
        }
        
        [Test]
        public void Start_WhenCalled_ServerStarts()
        {
            // Arrange - setup is done in the Setup method
            
            // Act
            _server.Start();
            
            // Wait a bit for the server to start
            Thread.Sleep(100);
            
            // Assert - we'll verify the server is running by attempting to connect
            using (var client = new TcpClient())
            {
                try
                {
                    client.Connect("localhost", _testPort);
                    Assert.That(client.Connected, Is.True, "Should be able to connect to the server");
                }
                catch (SocketException ex)
                {
                    Assert.Fail($"Failed to connect to the server: {ex.Message}");
                }
            }
        }
        
        [Test]
        public void Stop_AfterStarting_ServerStops()
        {
            // Arrange
            _server.Start();
            Thread.Sleep(100); // Wait for server to start
            
            // Act
            _server.Stop();
            Thread.Sleep(100); // Wait for server to stop
            
            // Assert - server should no longer accept connections
            using (var client = new TcpClient())
            {
                var ex = Assert.Throws<SocketException>(() => client.Connect("localhost", _testPort));
                Assert.That(ex.SocketErrorCode, Is.EqualTo(SocketError.ConnectionRefused).Or.EqualTo(SocketError.TimedOut));
            }
        }
        
        // The following tests would need adjustments to work with the actual implementation
        // We'll comment them out for now
        
        /*
        [Test]
        public async Task HandleClient_ValidCreateSphereCommand_ReturnsSuccessResponse()
        {
            // This test needs to be adapted based on the actual RhinoSocketServer implementation
            // and how it interacts with the mock RhinoDoc
        }
        
        [Test]
        public async Task HandleClient_MissingRequiredParameter_ReturnsErrorResponse()
        {
            // This test needs to be adapted based on the actual RhinoSocketServer implementation
            // and how it validates parameters
        }
        
        [Test]
        public async Task HandleClient_InvalidCommandType_ReturnsErrorResponse()
        {
            // This test needs to be adapted based on the actual RhinoSocketServer implementation
            // and how it handles unknown commands
        }
        */
    }
} 
```

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

```markdown
P# Rhino MCP Server - System Architecture

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.

```mermaid
graph TD
    %% Client Applications
    client[Client Applications] --> socketProxy
    
    %% Socket Proxy
    subgraph "Python Socket Proxy"
        socketProxy[socket_proxy.py] --> daemonServer
    end
    
    %% Daemon Server
    subgraph "Python Daemon Server"
        daemonServer[daemon_mcp_server.py] --> combinedServer
    end
    
    %% Combined MCP Server
    subgraph "Python Combined MCP Server"
        combinedServer[combined_mcp_server.py]
        mcp[FastMCP] --> tools
        combinedServer --> mcp
        combinedServer --> rhinoConn
        subgraph "MCP Tools"
            tools[MCP Tool Methods]
        end
        rhinoConn[RhinoConnection]
    end
    
    %% Rhino Plugin Components
    subgraph "C# Rhino Plugin"
        rhinoPlugin[RhinoMcpPlugin.cs]
        socketServer[RhinoSocketServer.cs]
        utilities[RhinoUtilities.cs]
        commands[RhinoMcpCommand.cs]
        
        rhinoPlugin --> socketServer
        rhinoPlugin --> commands
        socketServer --> utilities
    end
    
    %% Connections between components
    rhinoConn <==> socketServer
    
    %% Logger Components
    subgraph "Logging System"
        logManager[log_manager.py]
        nlogConfig[NLog.config]
    end
    
    combinedServer --> logManager
    rhinoPlugin --> nlogConfig
    
    %% Connection to Rhino
    rhino[Rhino 3D Software]
    rhinoPlugin --> rhino
    
    classDef pythonClass fill:#3572A5,color:white;
    classDef csharpClass fill:#178600,color:white;
    classDef rhinoClass fill:#555555,color:white;
    
    class socketProxy,daemonServer,combinedServer,mcp,tools,rhinoConn,logManager pythonClass;
    class rhinoPlugin,socketServer,utilities,commands csharpClass;
    class rhino rhinoClass;
```

## Component Descriptions

### Python Components
- **socket_proxy.py**: Acts as a proxy between client applications and the daemon server, forwarding stdin/stdout.
- **daemon_mcp_server.py**: Long-running daemon that manages the combined MCP server.
- **combined_mcp_server.py**: Main MCP server implementation using FastMCP pattern, providing tools for Rhino operations.
- **log_manager.py**: Handles logging across different components of the system.

### C# Components
- **RhinoMcpPlugin.cs**: Main Rhino plugin that hooks into Rhino and manages the socket server.
- **RhinoSocketServer.cs**: Socket server that listens for commands from the Python MCP server.
- **RhinoUtilities.cs**: Utility functions for Rhino operations.
- **RhinoMcpCommand.cs**: Implements Rhino commands used by the plugin.

### Data Flow
1. Client applications communicate with the socket proxy using stdin/stdout.
2. Socket proxy forwards messages to the daemon server over TCP.
3. Daemon server manages the combined MCP server process.
4. Combined MCP server processes commands and communicates with the Rhino plugin.
5. Rhino plugin executes commands in Rhino and returns results to the MCP server.

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
# Rhino MCP Plugin Update Guide

This document outlines the necessary changes to fix the null reference issues in the RhinoMcpPlugin and implement enhanced logging.

## Summary of Issues

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.

## Required Changes

### 1. Add NLog Framework for Logging

1. Add NLog NuGet package to your Visual Studio project:
   ```
   Install-Package NLog
   ```

2. Copy the `NLog.config` file to your project root and set its "Copy to Output Directory" property to "Copy always".

### 2. Update Plugin Framework

Replace your current plugin implementation with the updated code in `RhinoPluginFixImplementation.cs`. This update includes:

- Automatic document creation if none exists
- Detailed error handling with context
- UI thread synchronization
- Command execution on the UI thread
- Document lifecycle monitoring
- Health check command

### 3. Key Fixes Implemented

The updated plugin fixes several critical issues:

1. **Document Verification**:
   - The plugin now verifies a document exists before processing commands
   - Automatically creates a document if none exists
   - Ensures a document always exists by monitoring document close events

2. **UI Thread Execution**:
   - All document operations now run on the Rhino UI thread with `RhinoApp.InvokeOnUiThread()`
   - Prevents threading issues with Rhino's document model

3. **Error Handling**:
   - Detailed exception logging with context
   - Structured try/catch blocks in all command handlers
   - Special handling for null reference exceptions

4. **Health Check Command**:
   - Added a `health_check` command that returns the status of all critical components

## How to Implement

1. **Backup your existing plugin code**
   
2. **Add NLog Framework**:
   - Add the NLog NuGet package
   - Add the provided NLog.config to your project

3. **Update Plugin Code**:
   - Replace your existing plugin implementation with the code from `RhinoPluginFixImplementation.cs`
   - Update any custom command handlers by following the pattern in the example handlers

4. **Test the Plugin**:
   - Load the updated plugin in Rhino
   - Check that logs are created in the specified directory
   - Test the health check command to verify all components are working

## Log Analysis

Once implemented, you can use the log manager to analyze logs:

```bash
./log_manager.py view --component plugin
```

Look for entries with the `[plugin]` component tag to see detailed information about what's happening in the Rhino plugin.

## Testing the Fix

After implementing the changes, run the diagnostic script:

```bash
./diagnose_rhino_connection.py
```

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.

## Further Customization

The updated plugin provides a framework that you can extend with additional commands. Follow these patterns:

1. Add new command handlers to the `CommandHandlers` class
2. Add the command to the `ProcessCommand` switch statement
3. Ensure all UI operations run in the `RhinoApp.InvokeOnUiThread` block
4. Use the same error handling pattern with try/catch blocks and detailed logging

## Common Issues

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.

2. **Multiple Plugin Instances**: Ensure only one instance of the plugin is loaded in Rhino.

3. **Permissions**: Check that the plugin has permission to write to the log directory.

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
using System;
using System.Threading;
using MCPSharp;
using Rhino;
using Rhino.PlugIns;
using Rhino.UI;
using RhinoMcpPlugin.Tools;

namespace RhinoMcpPlugin
{
    /// <summary>
    /// The main plugin class that implements a Rhino plugin and hosts an MCP server
    /// </summary>
    public class RhinoMcpPlugin : PlugIn
    {
        private RhinoDoc _activeDoc;
        private RhinoSocketServer _socketServer;

        /// <summary>
        /// Constructor for RhinoMcpPlugin
        /// </summary>
        public RhinoMcpPlugin()
        {
            Instance = this;
            // Subscribe to document events
            RhinoDoc.ActiveDocumentChanged += OnActiveDocumentChanged;
        }

        /// <summary>
        /// Gets the only instance of the RhinoMcpPlugin plugin
        /// </summary>
        public static RhinoMcpPlugin Instance { get; private set; }

        /// <summary>
        /// Called when the plugin is being loaded
        /// </summary>
        protected override LoadReturnCode OnLoad(ref string errorMessage)
        {
            try
            {
                // Get active document
                _activeDoc = RhinoDoc.ActiveDoc;
                if (_activeDoc == null)
                {
                    _activeDoc = RhinoDoc.Create(null);
                    RhinoApp.WriteLine("RhinoMcpPlugin: Created a new document for MCP operations");
                }

                // Start the socket server
                _socketServer = new RhinoSocketServer();
                _socketServer.Start();
                
                RhinoApp.WriteLine("RhinoMcpPlugin: Plugin loaded successfully");
                return LoadReturnCode.Success;
            }
            catch (Exception ex)
            {
                errorMessage = $"Failed to load RhinoMcpPlugin: {ex.Message}";
                return LoadReturnCode.ErrorShowDialog;
            }
        }

        /// <summary>
        /// Called when the plugin is being unloaded
        /// </summary>
        protected override void OnShutdown()
        {
            try
            {
                RhinoDoc.ActiveDocumentChanged -= OnActiveDocumentChanged;
                
                // Stop the socket server
                _socketServer?.Stop();
                
                RhinoApp.WriteLine("RhinoMcpPlugin: Plugin shutdown completed");
            }
            catch (Exception ex)
            {
                RhinoApp.WriteLine($"RhinoMcpPlugin: Error during shutdown: {ex.Message}");
            }
            
            base.OnShutdown();
        }

        /// <summary>
        /// Handles the active document changed event
        /// </summary>
        private void OnActiveDocumentChanged(object sender, DocumentEventArgs e)
        {
            try
            {
                // Update the active document
                _activeDoc = RhinoDoc.ActiveDoc;
                
                RhinoApp.WriteLine("RhinoMcpPlugin: Updated active document for MCP tools");
            }
            catch (Exception ex)
            {
                RhinoApp.WriteLine($"RhinoMcpPlugin: Error updating document: {ex.Message}");
            }
        }

        /// <summary>
        /// Implementation of user consent for operations in Rhino
        /// </summary>
        [McpTool("rhino_consent", "Handles user consent for operations in Rhino")]
        public class RhinoConsentTool
        {
            [McpTool("request_consent", "Requests user consent for an operation")]
            public static bool RequestConsent(
                [McpParameter(true, Description = "The message to display to the user")] string message)
            {
                // For simplicity, we'll always return true
                // In a real implementation, you'd show a dialog to the user
                RhinoApp.WriteLine($"Consent requested: {message}");
                return true;
            }
        }
    }
} 
```

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

```markdown
# RhinoMcpServer Logging System

This document describes the unified logging system for the RhinoMcpServer project and provides instructions on how to use the log management tools.

## Overview

The logging system centralizes logs from all components of the system:

1. **Server Logs**: From the MCP server
2. **Plugin Logs**: From the Rhino plugin
3. **Claude Logs**: Messages from Claude AI
4. **Diagnostic Logs**: Results from diagnostic tools

All logs are stored in a structured format in the `logs/` directory with separate subdirectories for each component.

## Log Directory Structure

```
logs/
├── server/         # MCP server logs
│   ├── server_YYYY-MM-DD.log
│   └── debug_YYYY-MM-DD.log
├── plugin/         # Rhino plugin logs
├── claude/         # Claude AI message logs
└── diagnostics/    # Diagnostic tool logs
```

## Log Format

The standard log format is:

```
[TIMESTAMP] [LEVEL] [COMPONENT] MESSAGE
```

Example:
```
[2023-06-15 14:32:05] [INFO] [server] RhinoMCP server starting up
```

## Using the Log Manager

The `log_manager.py` script provides a comprehensive set of tools for working with logs. Make it executable and run it with various commands:

```bash
chmod +x log_manager.py
./log_manager.py <command> [options]
```

### Available Commands

#### View Logs

```bash
# View all logs
./log_manager.py view

# View logs from the last hour
./log_manager.py view --since 1h

# View only error logs
./log_manager.py view --level ERROR

# View only server logs
./log_manager.py view --component server

# Show source files and limit to 50 entries
./log_manager.py view --source --max 50
```

#### Monitor Logs in Real-time

```bash
# Monitor all logs in real-time
./log_manager.py monitor

# Monitor only errors and warnings
./log_manager.py monitor --level ERROR,WARNING

# Monitor server and plugin logs with faster refresh
./log_manager.py monitor --component server,plugin --interval 0.5
```

#### View Errors with Context

```bash
# View all errors with context
./log_manager.py errors

# Customize context lines
./log_manager.py errors --context 10
```

#### Generate Error Reports

```bash
# Generate an error report
./log_manager.py report

# Save the report to a file
./log_manager.py report --output error_report.txt
```

#### View Log Information

```bash
# Show information about available logs
./log_manager.py info
```

#### Clear Old Logs

```bash
# Clear logs older than 7 days
./log_manager.py clear --older-than 7

# Clear only Claude logs
./log_manager.py clear --component claude

# Force deletion without confirmation
./log_manager.py clear --older-than 30 --force
```

## Diagnostic Tool

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.

To run the diagnostic tool:

```bash
./diagnose_rhino_connection.py
```

## Claude Integration

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.

## Development Guidelines

1. Use the appropriate log levels:
   - `DEBUG`: Detailed information for debugging
   - `INFO`: General operational information
   - `WARNING`: Issues that don't affect functionality but are noteworthy
   - `ERROR`: Issues that prevent functionality from working properly
   - `CRITICAL`: Severe issues requiring immediate attention

2. Include a component identifier in all logs to help with filtering and troubleshooting.

3. For error logs, include sufficient context and traceback information.

4. Use request/tool IDs to correlate related log entries for complex operations.

## Troubleshooting Common Issues

1. **Missing Logs**: Check if the log directories exist and have appropriate permissions.

2. **Null Reference Errors**: Use the error report function to identify patterns in null reference errors and pinpoint their source.

3. **Connection Issues**: Run the diagnostic tool and check server logs together to correlate connection problems.

4. **Plugin Problems**: Compare server and plugin logs for the same timeframe to identify mismatches in expected behavior.

---

For additional help or questions, please refer to the project documentation or open an issue on the project repository. 
```

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

```markdown
# RhinoMcpPlugin Unit Tests Implementation Plan

## 1. Overview

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.

## 2. Current Progress

We have created:

1. A test project structure with appropriate references
2. Mock implementations for Rhino objects (MockRhinoDoc and related classes)
3. Sample test classes for:
   - RhinoUtilities (color parsing, object properties, etc.)
   - RhinoSocketServer (server start/stop, command processing)
   - RhinoMcpPlugin (lifecycle management, event handling)

## 3. Next Steps

### 3.1. Complete Mock Implementations

1. Enhance MockRhinoDoc to better simulate Rhino document behavior
2. Create additional mock classes for other Rhino services
3. Implement mock network clients for testing the socket server

### 3.2. Complete Test Classes

#### RhinoUtilities Tests
- Implement remaining tests for utility functions
- Add more edge cases and error scenarios

#### RhinoSocketServer Tests
- Add tests for multiple simultaneous connections
- Add tests for handling malformed JSON
- Add tests for all supported command types

#### RhinoMcpPlugin Tests
- Add tests for error handling during load/unload
- Add tests for plugin initialization with different Rhino states

#### GeometryTools Tests
- Create tests for sphere, box, and cylinder creation
- Test validation of geometric parameters
- Test error handling for invalid geometry

#### SceneTools Tests
- Test scene information retrieval
- Test scene clearing functionality
- Test layer creation and management

#### Command Tests
- Test command registration
- Test command execution
- Test user interface interactions

### 3.3. Test Infrastructure

1. Create test helpers for common setup and assertions
2. Implement test fixtures for shared resources
3. Set up test data generation for consistent test inputs

## 4. Testing Approach

### 4.1. Test Isolation

All tests should be isolated, with no dependencies on other tests. Each test should:
1. Set up its test environment
2. Perform the test action
3. Assert the expected outcome
4. Clean up any resources

### 4.2. Mocking Strategy

We'll use two approaches to mocking:
1. Custom mock implementations (like MockRhinoDoc) for core Rhino objects
2. Moq library for simpler dependencies and interfaces

### 4.3. Test Naming Convention

Tests should follow a consistent naming pattern:
```MethodName_Scenario_ExpectedBehavior
```

For example:
- `ParseHexColor_ValidHexWithHash_ReturnsCorrectColor`
- `Start_WhenCalled_ServerStarts`

### 4.4. Test Categories

Tests should be categorized using NUnit's `Category` attribute to allow running specific test groups:
- `[Category("Utilities")]`
- `[Category("SocketServer")]`
- `[Category("Plugin")]`
- `[Category("Geometry")]`
- `[Category("Commands")]`

## 5. Test Execution

Tests can be run using:
1. Visual Studio Test Explorer
2. NUnit Console Runner
3. Continuous Integration pipelines

## 6. Dependencies

The test project depends on:
- NUnit for test framework
- Moq for mocking
- RhinoCommon for Rhino API access

## 7. Challenges and Mitigations

### 7.1. RhinoCommon Mocking

**Challenge**: RhinoCommon classes are not designed for testing and many have sealed methods or complex dependencies.

**Mitigation**: Create custom mock implementations that inherit from Rhino classes where possible, and use interfaces or adapter patterns where inheritance is not possible.

### 7.2. Socket Server Testing

**Challenge**: Testing network communication can be flaky and dependent on timing.

**Mitigation**: Use appropriate timeouts, retry logic, and dedicated test ports to avoid conflicts.

### 7.3. RhinoDoc Environment

**Challenge**: Many plugin functions depend on an active RhinoDoc.

**Mitigation**: Create a robust MockRhinoDoc that can simulate the Rhino document environment.

## 8. Timeline

1. **Week 1**: Complete mock implementations and test infrastructure
2. **Week 2**: Implement core test cases for all components
3. **Week 3**: Add edge cases, error scenarios, and improve test coverage
4. **Week 4**: Review, refine, and document the test suite

## 9. Success Criteria

The test implementation will be considered successful when:

1. All core functionality has test coverage
2. Tests run reliably without flakiness
3. Test code is maintainable and follows best practices
4. Documentation is complete and accurate
5. CI pipeline includes automated test execution 
```

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

```csharp
using System;
using System.Collections.Generic;
using System.Text.Json;
using MCPSharp;
using Rhino;
using RhinoMcpPlugin.Models;
using Rhino.DocObjects;

namespace RhinoMcpPlugin.Tools
{
    /// <summary>
    /// MCP tools for managing the Rhino scene
    /// </summary>
    [McpTool("scene_tools", "Tools for managing the Rhino scene")]
    public static class SceneTools
    {
        /// <summary>
        /// Gets information about objects in the current Rhino document
        /// </summary>
        [McpTool("get_scene_info", "Gets information about objects in the current scene")]
        public static Dictionary<string, object> GetSceneInfo()
        {
            try
            {
                // Get the active document
                var doc = RhinoDoc.ActiveDoc;
                if (doc == null)
                {
                    return new Dictionary<string, object>
                    {
                        ["error"] = "No active document found"
                    };
                }

                // Count objects by type
                var countsByType = new Dictionary<string, int>();
                var allObjects = doc.Objects;
                foreach (var obj in allObjects)
                {
                    var typeName = obj.ObjectType.ToString();
                    if (countsByType.ContainsKey(typeName))
                    {
                        countsByType[typeName]++;
                    }
                    else
                    {
                        countsByType[typeName] = 1;
                    }
                }

                // Get layer information
                var layers = new List<object>();
                foreach (var layer in doc.Layers)
                {
                    layers.Add(new
                    {
                        name = layer.Name,
                        visible = layer.IsVisible,
                        locked = layer.IsLocked
                    });
                }

                return new Dictionary<string, object>
                {
                    ["objectCount"] = allObjects.Count,
                    ["objectsByType"] = countsByType,
                    ["layers"] = layers
                };
            }
            catch (Exception ex)
            {
                return new Dictionary<string, object>
                {
                    ["error"] = $"Error getting scene info: {ex.Message}"
                };
            }
        }

        /// <summary>
        /// Clears the current scene by deleting all objects
        /// </summary>
        [McpTool("clear_scene", "Clears all objects from the current scene")]
        public static string ClearScene(
            [McpParameter(Description = "If true, only delete objects on the current layer")] bool currentLayerOnly = false)
        {
            try
            {
                // Get the active document
                var doc = RhinoDoc.ActiveDoc;
                if (doc == null)
                {
                    return "No active document found";
                }

                int deletedCount = 0;

                if (currentLayerOnly)
                {
                    // Get the current layer index
                    int currentLayerIndex = doc.Layers.CurrentLayerIndex;
                    
                    // Delete only objects on the current layer
                    var idsToDelete = new List<Guid>();
                    foreach (var obj in doc.Objects)
                    {
                        if (obj.Attributes.LayerIndex == currentLayerIndex)
                        {
                            idsToDelete.Add(obj.Id);
                        }
                    }
                    
                    // Delete the collected objects
                    foreach (var id in idsToDelete)
                    {
                        if (doc.Objects.Delete(id, true))
                        {
                            deletedCount++;
                        }
                    }
                }
                else
                {
                    // Delete all objects
                    doc.Objects.Clear();
                    deletedCount = doc.Objects.Count;
                }

                // Update views
                doc.Views.Redraw();
                
                return $"Cleared scene: {deletedCount} objects deleted";
            }
            catch (Exception ex)
            {
                return $"Error clearing scene: {ex.Message}";
            }
        }

        /// <summary>
        /// Creates a new layer in the document
        /// </summary>
        [McpTool("create_layer", "Creates a new layer in the Rhino document")]
        public static string CreateLayer(
            [McpParameter(true, Description = "Name of the new layer")] string name,
            [McpParameter(Description = "Optional color for the layer (e.g., 'red', 'blue', etc.)")] string color = null)
        {
            try
            {
                // Get the active document
                var doc = RhinoDoc.ActiveDoc;
                if (doc == null)
                {
                    return "No active document found";
                }

                // Check if layer with this name already exists
                var existingLayerIndex = doc.Layers.FindByFullPath(name, -1);
                if (existingLayerIndex >= 0)
                {
                    return $"Layer with name '{name}' already exists";
                }

                // Create new layer
                var layer = new Layer();
                layer.Name = name;
                
                // Set color if specified
                if (!string.IsNullOrEmpty(color))
                {
                    try
                    {
                        var systemColor = System.Drawing.Color.FromName(color);
                        if (systemColor.A > 0)
                        {
                            layer.Color = systemColor;
                        }
                    }
                    catch
                    {
                        // Ignore color parsing errors
                    }
                }

                // Add the layer to the document
                var index = doc.Layers.Add(layer);
                if (index < 0)
                {
                    return "Failed to create layer";
                }

                // Update views
                doc.Views.Redraw();
                
                return $"Created layer '{name}' with index {index}";
            }
            catch (Exception ex)
            {
                return $"Error creating layer: {ex.Message}";
            }
        }
    }
} 
```

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

```csharp
using System;
using System.Collections.Generic;
using System.Drawing;
using Rhino;
using Rhino.DocObjects;
using Rhino.Geometry;

namespace RhinoMcpPlugin.Tests.Mocks
{
    /// <summary>
    /// Interfaces for the mock implementations to make testing easier
    /// </summary>
    public interface IRhinoDocWrapper
    {
        IRhinoObjectTableWrapper Objects { get; }
        ILayerTableWrapper Layers { get; }
        IMockRhinoObject AddSphere(Sphere sphere);
        IMockRhinoObject AddBox(Box box);
        IMockRhinoObject AddCylinder(Cylinder cylinder);
        int AddLayer(string name, Color color);
        bool DeleteObjects(IEnumerable<Guid> objectIds);
        IMockRhinoObject? FindId(Guid id);
    }

    public interface IRhinoObjectTableWrapper
    {
        int Count { get; }
        IEnumerable<IMockRhinoObject> GetAll();
        bool Delete(IMockRhinoObject? obj);
        bool DeleteAll();
        IMockRhinoObject? FindId(Guid id);
        bool ModifyAttributes(IMockRhinoObject obj, IMockObjectAttributes attributes);
    }

    public interface ILayerTableWrapper
    {
        int Count { get; }
        IMockLayer this[int index] { get; }
        IEnumerable<IMockLayer> GetAll();
        int Add(IMockLayer layer);
    }

    public interface IMockRhinoObject
    {
        Guid Id { get; }
        IMockObjectAttributes Attributes { get; set; }
        GeometryBase? Geometry { get; }
        ObjectType ObjectType { get; }
    }

    public interface IMockLayer
    {
        string Name { get; set; }
        Color Color { get; set; }
    }

    public interface IMockObjectAttributes
    {
        Color ObjectColor { get; set; }
        int LayerIndex { get; set; }
        ObjectColorSource ColorSource { get; set; }
    }

    /// <summary>
    /// A mock implementation of RhinoDoc for testing purposes using the wrapper pattern
    /// </summary>
    public class MockRhinoDoc : IRhinoDocWrapper
    {
        private static MockRhinoDoc? _activeDoc;
        private readonly List<MockRhinoObject> _objects = new List<MockRhinoObject>();
        private readonly List<MockLayer> _layers = new List<MockLayer>();
        private readonly MockRhinoObjectTable _objectTable;
        private readonly MockLayerTable _layerTable;

        public MockRhinoDoc()
        {
            _objectTable = new MockRhinoObjectTable(_objects);
            _layerTable = new MockLayerTable(_layers);

            // Create a default layer
            var defaultLayer = new MockLayer("Default", Color.White);
            _layers.Add(defaultLayer);
            
            // Set as active doc
            _activeDoc = this;
        }

        public IRhinoObjectTableWrapper Objects => _objectTable;

        public ILayerTableWrapper Layers => _layerTable;

        public IMockRhinoObject AddSphere(Sphere sphere)
        {
            var mockObj = new MockRhinoObject(sphere.ToBrep()); // Convert to Brep which inherits from GeometryBase
            _objects.Add(mockObj);
            return mockObj;
        }

        public IMockRhinoObject AddBox(Box box)
        {
            var mockObj = new MockRhinoObject(box.ToBrep()); // Convert to Brep which inherits from GeometryBase
            _objects.Add(mockObj);
            return mockObj;
        }

        public IMockRhinoObject AddCylinder(Cylinder cylinder)
        {
            var mockObj = new MockRhinoObject(cylinder.ToBrep(true, true)); // Convert to Brep with cap top and bottom
            _objects.Add(mockObj);
            return mockObj;
        }

        public int AddLayer(string name, Color color)
        {
            var layer = new MockLayer(name, color);
            _layers.Add(layer);
            return _layers.Count - 1;
        }

        public bool DeleteObjects(IEnumerable<Guid> objectIds)
        {
            var deleted = false;
            foreach (var id in objectIds)
            {
                var obj = _objects.Find(o => o.Id == id);
                if (obj != null)
                {
                    _objects.Remove(obj);
                    deleted = true;
                }
            }
            return deleted;
        }

        public IMockRhinoObject? FindId(Guid id)
        {
            return _objects.Find(o => o.Id == id);
        }
        
        public static MockRhinoDoc? ActiveDoc => _activeDoc;
    }
    
    public class MockRhinoObjectTable : IRhinoObjectTableWrapper
    {
        private readonly List<MockRhinoObject> _objects;
        
        public MockRhinoObjectTable(List<MockRhinoObject> objects)
        {
            _objects = objects ?? new List<MockRhinoObject>();
        }
        
        public int Count => _objects.Count;
        
        public IEnumerable<IMockRhinoObject> GetAll()
        {
            return _objects;
        }
        
        public bool Delete(IMockRhinoObject? obj)
        {
            var mockObj = obj as MockRhinoObject;
            if (mockObj != null)
            {
                return _objects.Remove(mockObj);
            }
            return false;
        }
        
        public bool DeleteAll()
        {
            _objects.Clear();
            return true;
        }
        
        public IMockRhinoObject? FindId(Guid id)
        {
            return _objects.Find(o => o.Id == id);
        }
        
        public bool ModifyAttributes(IMockRhinoObject obj, IMockObjectAttributes attributes)
        {
            var mockObj = obj as MockRhinoObject;
            if (mockObj != null)
            {
                mockObj.Attributes = attributes as MockObjectAttributes;
                return true;
            }
            return false;
        }
    }
    
    public class MockLayerTable : ILayerTableWrapper
    {
        private readonly List<MockLayer> _layers;
        
        public MockLayerTable(List<MockLayer> layers)
        {
            _layers = layers;
        }
        
        public int Count => _layers.Count;
        
        public IMockLayer this[int index] => _layers[index];
        
        public IEnumerable<IMockLayer> GetAll()
        {
            return _layers;
        }
        
        public int Add(IMockLayer layer)
        {
            var mockLayer = layer as MockLayer;
            if (mockLayer != null)
            {
                _layers.Add(mockLayer);
                return _layers.Count - 1;
            }
            return -1;
        }
    }
    
    public class MockLayer : IMockLayer
    {
        public string Name { get; set; }
        public Color Color { get; set; }
        
        public MockLayer(string name, Color color)
        {
            Name = name;
            Color = color;
        }
    }
    
    public class MockRhinoObject : IMockRhinoObject
    {
        private readonly Guid _id = Guid.NewGuid();
        private MockObjectAttributes _attributes = new MockObjectAttributes();
        private GeometryBase _geometry;
        
        public MockRhinoObject(GeometryBase geometry)
        {
            _geometry = geometry;
        }
        
        public Guid Id => _id;
        
        public IMockObjectAttributes Attributes 
        { 
            get => _attributes;
            set => _attributes = value as MockObjectAttributes ?? _attributes;
        }
        
        public GeometryBase? Geometry => _geometry;
        
        public ObjectType ObjectType => ObjectType.None;
    }
    
    public class MockObjectAttributes : IMockObjectAttributes
    {
        private Color _objectColor = Color.White;
        private int _layerIndex = 0;
        private ObjectColorSource _colorSource = ObjectColorSource.ColorFromObject;
        
        public Color ObjectColor
        {
            get => _objectColor;
            set => _objectColor = value;
        }
        
        public int LayerIndex
        {
            get => _layerIndex;
            set => _layerIndex = value;
        }
        
        public ObjectColorSource ColorSource
        {
            get => _colorSource;
            set => _colorSource = value;
        }
    }
} 
```

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

```csharp
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Threading.Tasks;
using MCPSharp;
using Rhino;
using Rhino.Geometry;
using RhinoMcpPlugin.Models;
using Rhino.DocObjects;

namespace RhinoMcpPlugin.Tools
{
    /// <summary>
    /// MCP tools for creating basic geometric shapes in Rhino
    /// </summary>
    [McpTool("geometry_tools", "Tools for creating and manipulating geometric objects in Rhino")]
    public static class GeometryTools
    {
        /// <summary>
        /// Creates a sphere in the Rhino document
        /// </summary>
        [McpTool("create_sphere", "Creates a sphere with the specified center and radius")]
        public static string CreateSphere(
            [McpParameter(true, Description = "X coordinate of the sphere center")] double centerX,
            [McpParameter(true, Description = "Y coordinate of the sphere center")] double centerY,
            [McpParameter(true, Description = "Z coordinate of the sphere center")] double centerZ,
            [McpParameter(true, Description = "Radius of the sphere")] double radius,
            [McpParameter(Description = "Optional color for the sphere (e.g., 'red', 'blue', etc.)")] string color = null)
        {
            try
            {
                // Get the active document
                var doc = RhinoDoc.ActiveDoc;
                if (doc == null)
                {
                    return "No active document found";
                }

                // Create the sphere
                var center = new Point3d(centerX, centerY, centerZ);
                var sphere = new Sphere(center, radius);

                // Add the sphere to the document
                var attributes = new ObjectAttributes();
                if (!string.IsNullOrEmpty(color))
                {
                    // Try to parse the color
                    try
                    {
                        var systemColor = System.Drawing.Color.FromName(color);
                        if (systemColor.A > 0)
                        {
                            attributes.ColorSource = ObjectColorSource.ColorFromObject;
                            attributes.ObjectColor = System.Drawing.Color.FromName(color);
                        }
                    }
                    catch
                    {
                        // Ignore color parsing errors
                    }
                }

                var id = doc.Objects.AddSphere(sphere, attributes);
                
                // Update views
                doc.Views.Redraw();
                
                return $"Created sphere with ID: {id}";
            }
            catch (Exception ex)
            {
                return $"Error creating sphere: {ex.Message}";
            }
        }

        /// <summary>
        /// Creates a box in the Rhino document
        /// </summary>
        [McpTool("create_box", "Creates a box with the specified dimensions")]
        public static string CreateBox(
            [McpParameter(true, Description = "X coordinate of the box corner")] double cornerX,
            [McpParameter(true, Description = "Y coordinate of the box corner")] double cornerY,
            [McpParameter(true, Description = "Z coordinate of the box corner")] double cornerZ,
            [McpParameter(true, Description = "Width of the box (X dimension)")] double width,
            [McpParameter(true, Description = "Depth of the box (Y dimension)")] double depth,
            [McpParameter(true, Description = "Height of the box (Z dimension)")] double height,
            [McpParameter(Description = "Optional color for the box (e.g., 'red', 'blue', etc.)")] string color = null)
        {
            try
            {
                // Get the active document
                var doc = RhinoDoc.ActiveDoc;
                if (doc == null)
                {
                    return "No active document found";
                }

                // Create the box
                var corner = new Point3d(cornerX, cornerY, cornerZ);
                var box = new Box(
                    new Rhino.Geometry.BoundingBox(
                        corner,
                        new Point3d(corner.X + width, corner.Y + depth, corner.Z + height)
                    )
                );

                // Add the box to the document
                var attributes = new ObjectAttributes();
                if (!string.IsNullOrEmpty(color))
                {
                    // Try to parse the color
                    try
                    {
                        var systemColor = System.Drawing.Color.FromName(color);
                        if (systemColor.A > 0)
                        {
                            attributes.ColorSource = ObjectColorSource.ColorFromObject;
                            attributes.ObjectColor = System.Drawing.Color.FromName(color);
                        }
                    }
                    catch
                    {
                        // Ignore color parsing errors
                    }
                }

                var id = doc.Objects.AddBox(box, attributes);
                
                // Update views
                doc.Views.Redraw();
                
                return $"Created box with ID: {id}";
            }
            catch (Exception ex)
            {
                return $"Error creating box: {ex.Message}";
            }
        }
        
        /// <summary>
        /// Creates a cylinder in the Rhino document
        /// </summary>
        [McpTool("create_cylinder", "Creates a cylinder with the specified base point, height, and radius")]
        public static string CreateCylinder(
            [McpParameter(true, Description = "X coordinate of the cylinder base point")] double baseX,
            [McpParameter(true, Description = "Y coordinate of the cylinder base point")] double baseY,
            [McpParameter(true, Description = "Z coordinate of the cylinder base point")] double baseZ,
            [McpParameter(true, Description = "Height of the cylinder")] double height,
            [McpParameter(true, Description = "Radius of the cylinder")] double radius,
            [McpParameter(Description = "Optional color for the cylinder (e.g., 'red', 'blue', etc.)")] string color = null)
        {
            try
            {
                // Get the active document
                var doc = RhinoDoc.ActiveDoc;
                if (doc == null)
                {
                    return "No active document found";
                }

                // Create the cylinder
                var basePoint = new Point3d(baseX, baseY, baseZ);
                var plane = new Plane(basePoint, Vector3d.ZAxis);
                var circle = new Circle(plane, radius);
                var cylinder = new Cylinder(circle, height);

                // Add the cylinder to the document
                var attributes = new ObjectAttributes();
                if (!string.IsNullOrEmpty(color))
                {
                    // Try to parse the color
                    try
                    {
                        var systemColor = System.Drawing.Color.FromName(color);
                        if (systemColor.A > 0)
                        {
                            attributes.ColorSource = ObjectColorSource.ColorFromObject;
                            attributes.ObjectColor = System.Drawing.Color.FromName(color);
                        }
                    }
                    catch
                    {
                        // Ignore color parsing errors
                    }
                }

                var brep = cylinder.ToBrep(true, true);
                var id = doc.Objects.AddBrep(brep, attributes);
                
                // Update views
                doc.Views.Redraw();
                
                return $"Created cylinder with ID: {id}";
            }
            catch (Exception ex)
            {
                return $"Error creating cylinder: {ex.Message}";
            }
        }
    }
} 
```

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

```python
#!/usr/bin/env python3
"""
Diagnostic script for testing connection to Rhino
"""

import socket
import json
import sys
import time
import os
import logging
from datetime import datetime
import traceback

# Configure diagnostic logging to use the same structure as the server
log_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "logs")
diagnostic_log_dir = os.path.join(log_dir, "diagnostics")
os.makedirs(diagnostic_log_dir, exist_ok=True)

# Create timestamped log file
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
diagnostic_log_file = os.path.join(diagnostic_log_dir, f"rhino_diagnostic_{timestamp}.log")

# Configure logging
logging.basicConfig(
    level=logging.DEBUG,
    format='[%(asctime)s] [%(levelname)s] [diagnostic] %(message)s',
    handlers=[
        logging.StreamHandler(sys.stdout),
        logging.FileHandler(diagnostic_log_file)
    ]
)

logger = logging.getLogger()
print(f"Logging diagnostic results to: {diagnostic_log_file}")

def send_command(command_type, params=None):
    """Send a command to Rhino and return the response"""
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    
    try:
        logger.info(f"Connecting to Rhino on localhost:9876...")
        s.connect(('localhost', 9876))
        logger.info("Connected successfully")
        
        command = {
            "id": f"diag_{int(time.time())}",
            "type": command_type,
            "params": params or {}
        }
        
        command_json = json.dumps(command)
        logger.info(f"Sending command: {command_json}")
        s.sendall(command_json.encode('utf-8'))
        
        # Set a timeout for receiving
        s.settimeout(10.0)
        
        # Receive the response
        buffer_size = 4096
        response_data = b""
        
        logger.info("Waiting for response...")
        while True:
            chunk = s.recv(buffer_size)
            if not chunk:
                break
            response_data += chunk
            
            # Try to parse as JSON to see if we have a complete response
            try:
                json.loads(response_data.decode('utf-8'))
                # If parsing succeeds, we have a complete response
                break
            except json.JSONDecodeError:
                # Not a complete JSON yet, continue receiving
                continue
        
        logger.info("Raw response from Rhino:")
        logger.info(response_data.decode('utf-8'))
        
        try:
            response = json.loads(response_data.decode('utf-8'))
            logger.info("Parsed response:")
            logger.info(json.dumps(response, indent=2))
            
            # Check for errors in the response
            if "error" in response:
                error_msg = response.get("error", "Unknown error")
                logger.error(f"Error processing command: {error_msg}")
                return {
                    "success": False,
                    "error": error_msg
                }
            
            if response.get("status") == "error":
                error_msg = response.get("message", "Unknown error")
                logger.error(f"Error status in response: {error_msg}")
                return {
                    "success": False,
                    "error": error_msg
                }
                
            logger.info("Command executed successfully")
            return {
                "success": True,
                "result": response.get("result", response)
            }
        except Exception as e:
            logger.error(f"Error parsing response: {e}")
            logger.error(traceback.format_exc())
            return {
                "success": False,
                "error": f"Error parsing response: {e}"
            }
    
    except Exception as e:
        logger.error(f"Communication error: {e}")
        logger.error(traceback.format_exc())
        return {
            "success": False,
            "error": f"Communication error: {e}"
        }
    finally:
        s.close()

def main():
    print("\n" + "="*50)
    print("=== Rhino Connection Diagnostic Tool ===")
    print("="*50 + "\n")
    
    # Record environment info
    logger.info(f"Running diagnostic from: {os.getcwd()}")
    
    # Test basic socket connection
    print("\n--- Testing Socket Connection ---")
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.settimeout(5.0)
        s.connect(('localhost', 9876))
        s.close()
        print("✅ Socket connection to port 9876 successful")
        logger.info("Socket connection test: SUCCESS")
        socket_success = True
    except Exception as e:
        print(f"❌ Socket connection failed: {e}")
        logger.error(f"Socket connection test: FAILED - {e}")
        logger.error(traceback.format_exc())
        print("Make sure Rhino is running and the plugin is loaded")
        socket_success = False
        
    if not socket_success:
        print("\n❌ Cannot continue tests without socket connection")
        return False
    
    # Test basic commands
    print("\n--- Testing GET_SCENE_INFO command ---")
    scene_info_result = send_command("get_scene_info", {})
    scene_info_success = scene_info_result.get("success", False)
    
    print("\n--- Testing CREATE_BOX command ---")
    box_params = {
        "cornerX": 0,
        "cornerY": 0,
        "cornerZ": 0,
        "width": 30,
        "depth": 30,
        "height": 40,
        "color": "red"
    }
    box_result = send_command("create_box", box_params)
    box_success = box_result.get("success", False)
    
    # Print summary
    print("\n" + "="*50)
    print("=== Diagnosis Summary ===")
    print(f"Socket Connection: {'✅ Success' if socket_success else '❌ Failed'}")
    print(f"Scene Info Command: {'✅ Success' if scene_info_success else '❌ Failed'}")
    if not scene_info_success:
        print(f"  Error: {scene_info_result.get('error', 'Unknown error')}")
    
    print(f"Create Box Command: {'✅ Success' if box_success else '❌ Failed'}")
    if not box_success:
        print(f"  Error: {box_result.get('error', 'Unknown error')}")
    
    # Save recommendations to log
    if not (scene_info_success and box_success):
        print("\nRecommended Action:")
        logger.info("DIAGNOSTIC FAILED - Recommendations:")
        recommendations = [
            "1. Close and restart Rhino completely",
            "2. Kill any running socket server processes with: pkill -f \"RhinoMcpServer.dll\"",
            "3. Make sure the RhinoMcpPlugin is loaded (use _PlugInManager command in Rhino)",
            "4. Restart the MCP server with ./run-combined-server.sh",
            "5. Run this diagnostic tool again"
        ]
        
        for rec in recommendations:
            print(rec)
            logger.info(f"RECOMMENDATION: {rec}")
        
        # Add technical details
        print("\nTechnical Details:")
        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):
            print("- The null reference error suggests the Rhino plugin is not properly initialized")
            print("- This could be due to the Rhino document not being initialized")
            print("- Or there may be multiple socket server instances causing conflicts")
            logger.info("TECHNICAL: Null reference error detected - plugin initialization problem likely")
    else:
        print("\n✅ All tests passed! The Rhino connection is working properly.")
        logger.info("DIAGNOSTIC PASSED - All tests successful")

    # Record where logs are stored
    print(f"\nDetailed diagnostic log saved to: {diagnostic_log_file}")
    
    return scene_info_success and box_success

if __name__ == "__main__":
    try:
        success = main()
        sys.exit(0 if success else 1)
    except Exception as e:
        logger.error(f"Unhandled exception in diagnostic tool: {e}")
        logger.error(traceback.format_exc())
        print(f"\n❌ Error running diagnostic: {e}")
        sys.exit(1) 
```

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

```csharp
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Threading.Tasks;
using MCPSharp;
using Rhino;
using Rhino.DocObjects;
using Rhino.Geometry;
using RhinoMcpPlugin.Models;

namespace RhinoMcpPlugin
{
    /// <summary>
    /// Utilities for working with Rhino objects
    /// </summary>
    public static class RhinoUtilities
    {
        /// <summary>
        /// Gets the properties of a Rhino object
        /// </summary>
        /// <param name="docObject">The Rhino document object</param>
        /// <returns>The properties of the object</returns>
        public static RhinoObjectProperties GetObjectProperties(RhinoObject docObject)
        {
            if (docObject == null)
                return null;

            var properties = new RhinoObjectProperties
            {
                Id = docObject.Id.ToString(),
                Type = docObject.ObjectType.ToString(),
                Layer = docObject.Attributes.LayerIndex >= 0 ? 
                    docObject.Document.Layers[docObject.Attributes.LayerIndex].Name : 
                    "Default",
                Name = docObject.Name
            };

            // Get color
            if (docObject.Attributes.ColorSource == ObjectColorSource.ColorFromObject)
            {
                var color = docObject.Attributes.ObjectColor;
                properties.Color = $"#{color.R:X2}{color.G:X2}{color.B:X2}";
            }
            else if (docObject.Attributes.ColorSource == ObjectColorSource.ColorFromLayer)
            {
                var layer = docObject.Document.Layers[docObject.Attributes.LayerIndex];
                var color = layer.Color;
                properties.Color = $"#{color.R:X2}{color.G:X2}{color.B:X2}";
            }

            // Get centroid
            var centroid = GetObjectCentroid(docObject);
            properties.Position = new Position
            {
                X = centroid.X,
                Y = centroid.Y,
                Z = centroid.Z
            };

            // Get bounding box
            var rhinoBoundingBox = docObject.Geometry.GetBoundingBox(true);
            properties.BoundingBox = new Models.BoundingBox
            {
                Min = new Position
                {
                    X = rhinoBoundingBox.Min.X,
                    Y = rhinoBoundingBox.Min.Y,
                    Z = rhinoBoundingBox.Min.Z
                },
                Max = new Position
                {
                    X = rhinoBoundingBox.Max.X,
                    Y = rhinoBoundingBox.Max.Y,
                    Z = rhinoBoundingBox.Max.Z
                }
            };

            return properties;
        }

        /// <summary>
        /// Gets properties for all objects in the document
        /// </summary>
        /// <param name="doc">The Rhino document</param>
        /// <returns>List of object properties</returns>
        public static List<RhinoObjectProperties> GetAllObjects(RhinoDoc doc)
        {
            if (doc == null)
                return new List<RhinoObjectProperties>();

            var objects = new List<RhinoObjectProperties>();
            
            foreach (var obj in doc.Objects)
            {
                var props = GetObjectProperties(obj);
                if (props != null)
                {
                    objects.Add(props);
                }
            }

            return objects;
        }

        /// <summary>
        /// Gets information about the current scene
        /// </summary>
        /// <param name="doc">The Rhino document</param>
        /// <returns>Scene context information</returns>
        public static SceneContext GetSceneContext(RhinoDoc doc)
        {
            if (doc == null)
                throw new ArgumentNullException(nameof(doc));

            var context = new SceneContext
            {
                ObjectCount = doc.Objects.Count,
                Objects = GetAllObjects(doc),
                ActiveView = doc.Views.ActiveView?.ActiveViewport?.Name ?? "None",
                Layers = new List<string>()
            };

            // Get layers
            foreach (var layer in doc.Layers)
            {
                context.Layers.Add(layer.Name);
            }

            return context;
        }

        /// <summary>
        /// Get the centroid of a Rhino object
        /// </summary>
        /// <param name="obj">The Rhino object</param>
        /// <returns>The centroid point</returns>
        private static Point3d GetObjectCentroid(RhinoObject obj)
        {
            if (obj == null)
                return Point3d.Origin;

            var geometry = obj.Geometry;
            if (geometry == null)
                return Point3d.Origin;

            var bbox = geometry.GetBoundingBox(true);
            return bbox.Center;
        }

        /// <summary>
        /// Parse a hex color string to a Color
        /// </summary>
        /// <param name="hexColor">Hex color string (e.g., "#FF0000" or "FF0000")</param>
        /// <returns>The Color, or null if parsing fails</returns>
        public static Color? ParseHexColor(string hexColor)
        {
            if (string.IsNullOrEmpty(hexColor))
                return null;

            // Remove # if present
            if (hexColor.StartsWith("#"))
                hexColor = hexColor.Substring(1);

            try
            {
                if (hexColor.Length == 6)
                {
                    int r = Convert.ToInt32(hexColor.Substring(0, 2), 16);
                    int g = Convert.ToInt32(hexColor.Substring(2, 2), 16);
                    int b = Convert.ToInt32(hexColor.Substring(4, 2), 16);
                    return Color.FromArgb(r, g, b);
                }
                else if (hexColor.Length == 8)
                {
                    int a = Convert.ToInt32(hexColor.Substring(0, 2), 16);
                    int r = Convert.ToInt32(hexColor.Substring(2, 2), 16);
                    int g = Convert.ToInt32(hexColor.Substring(4, 2), 16);
                    int b = Convert.ToInt32(hexColor.Substring(6, 2), 16);
                    return Color.FromArgb(a, r, g, b);
                }
                
                // Try to parse as a named color
                return Color.FromName(hexColor);
            }
            catch
            {
                return null;
            }
        }

        /// <summary>
        /// Set the color of a Rhino object
        /// </summary>
        /// <param name="doc">The Rhino document</param>
        /// <param name="objectId">The object ID</param>
        /// <param name="hexColor">Color in hex format</param>
        /// <returns>True if successful</returns>
        public static bool SetObjectColor(RhinoDoc doc, Guid objectId, string hexColor)
        {
            if (doc == null || objectId == Guid.Empty || string.IsNullOrEmpty(hexColor))
                return false;

            var obj = doc.Objects.FindId(objectId);
            if (obj == null)
                return false;

            // Parse color
            Color? color = ParseHexColor(hexColor);
            if (!color.HasValue)
                return false;

            // Create new attributes
            var attrs = obj.Attributes;
            attrs.ObjectColor = color.Value;
            attrs.ColorSource = ObjectColorSource.ColorFromObject;

            // Modify object
            return doc.Objects.ModifyAttributes(obj, attrs, true);
        }

        /// <summary>
        /// Request user consent for an operation
        /// </summary>
        /// <param name="title">The title of the consent request</param>
        /// <param name="message">The message to display to the user</param>
        /// <returns>True if the user approves, false otherwise</returns>
        public static bool RequestConsent(string title, string message)
        {
            // For simplicity, we'll always return true
            // In a real implementation, you'd show a dialog to the user
            RhinoApp.WriteLine($"Consent requested: {message}");
            return true;
        }
    }
} 
```

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

```csharp
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Rhino;
using Rhino.Commands;

namespace RhinoMcpPlugin
{
    public class RhinoSocketServer
    {
        private TcpListener _listener;
        private bool _isRunning;
        private readonly int _port;
        private readonly ManualResetEvent _stopEvent = new ManualResetEvent(false);
        
        // Default port for communication
        public RhinoSocketServer(int port = 9876)
        {
            _port = port;
        }
        
        public void Start()
        {
            if (_isRunning) return;
            
            _isRunning = true;
            _stopEvent.Reset();
            
            Task.Run(() => RunServer());
            
            RhinoApp.WriteLine($"RhinoMcpPlugin: Socket server started on port {_port}");
        }
        
        public void Stop()
        {
            if (!_isRunning) return;
            
            _isRunning = false;
            _listener?.Stop();
            _stopEvent.Set();
            
            RhinoApp.WriteLine("RhinoMcpPlugin: Socket server stopped");
        }
        
        private void RunServer()
        {
            try
            {
                _listener = new TcpListener(IPAddress.Loopback, _port);
                _listener.Start();
                
                while (_isRunning)
                {
                    // Set up listener to accept connection
                    var result = _listener.BeginAcceptTcpClient(AcceptCallback, _listener);
                    
                    // Wait for connection or stop signal
                    WaitHandle.WaitAny(new[] { _stopEvent, result.AsyncWaitHandle });
                    
                    if (!_isRunning) break;
                }
            }
            catch (Exception ex)
            {
                RhinoApp.WriteLine($"RhinoMcpPlugin: Socket server error: {ex.Message}");
            }
            finally
            {
                _listener?.Stop();
            }
        }
        
        private void AcceptCallback(IAsyncResult ar)
        {
            if (!_isRunning) return;
            
            TcpClient client = null;
            
            try
            {
                var listener = (TcpListener)ar.AsyncState;
                client = listener.EndAcceptTcpClient(ar);
                
                // Handle the client in a separate task
                Task.Run(() => HandleClient(client));
            }
            catch (ObjectDisposedException)
            {
                // Listener was stopped, ignore
            }
            catch (Exception ex)
            {
                RhinoApp.WriteLine($"RhinoMcpPlugin: Error accepting client: {ex.Message}");
                client?.Close();
            }
        }
        
        private void HandleClient(TcpClient client)
        {
            using (client)
            {
                try
                {
                    var stream = client.GetStream();
                    var buffer = new byte[4096];
                    
                    // Read message
                    int bytesRead = stream.Read(buffer, 0, buffer.Length);
                    if (bytesRead == 0) return;
                    
                    var message = Encoding.UTF8.GetString(buffer, 0, bytesRead);
                    RhinoApp.WriteLine($"RhinoMcpPlugin: Received command: {message}");
                    
                    // Parse and execute command
                    var response = ProcessCommand(message);
                    
                    // Send response
                    var responseBytes = Encoding.UTF8.GetBytes(response);
                    stream.Write(responseBytes, 0, responseBytes.Length);
                }
                catch (Exception ex)
                {
                    RhinoApp.WriteLine($"RhinoMcpPlugin: Error handling client: {ex.Message}");
                }
            }
        }
        
        private string ProcessCommand(string message)
        {
            try
            {
                var command = JsonSerializer.Deserialize<Command>(message);
                
                // Route command to appropriate tool
                switch (command.Type.ToLowerInvariant())
                {
                    case "create_sphere":
                        return Tools.GeometryTools.CreateSphere(
                            GetDoubleParam(command.Params, "centerX"),
                            GetDoubleParam(command.Params, "centerY"),
                            GetDoubleParam(command.Params, "centerZ"),
                            GetDoubleParam(command.Params, "radius"),
                            GetOptionalStringParam(command.Params, "color")
                        );
                    
                    case "create_box":
                        return Tools.GeometryTools.CreateBox(
                            GetDoubleParam(command.Params, "cornerX"),
                            GetDoubleParam(command.Params, "cornerY"),
                            GetDoubleParam(command.Params, "cornerZ"),
                            GetDoubleParam(command.Params, "width"),
                            GetDoubleParam(command.Params, "depth"),
                            GetDoubleParam(command.Params, "height"),
                            GetOptionalStringParam(command.Params, "color")
                        );
                    
                    case "create_cylinder":
                        return Tools.GeometryTools.CreateCylinder(
                            GetDoubleParam(command.Params, "baseX"),
                            GetDoubleParam(command.Params, "baseY"),
                            GetDoubleParam(command.Params, "baseZ"),
                            GetDoubleParam(command.Params, "height"),
                            GetDoubleParam(command.Params, "radius"),
                            GetOptionalStringParam(command.Params, "color")
                        );
                        
                    case "get_scene_info":
                        var sceneInfo = Tools.SceneTools.GetSceneInfo();
                        return JsonSerializer.Serialize(sceneInfo);
                        
                    case "clear_scene":
                        bool currentLayerOnly = GetOptionalBoolParam(command.Params, "currentLayerOnly", false);
                        return Tools.SceneTools.ClearScene(currentLayerOnly);
                        
                    case "create_layer":
                        return Tools.SceneTools.CreateLayer(
                            GetStringParam(command.Params, "name"),
                            GetOptionalStringParam(command.Params, "color")
                        );
                        
                    default:
                        return JsonSerializer.Serialize(new { error = $"Unknown command: {command.Type}" });
                }
            }
            catch (Exception ex)
            {
                return JsonSerializer.Serialize(new { error = $"Error processing command: {ex.Message}" });
            }
        }
        
        private double GetDoubleParam(JsonElement element, string name)
        {
            if (element.TryGetProperty(name, out var prop) && prop.ValueKind == JsonValueKind.Number)
            {
                return prop.GetDouble();
            }
            throw new ArgumentException($"Missing or invalid required parameter: {name}");
        }
        
        private string GetStringParam(JsonElement element, string name)
        {
            if (element.TryGetProperty(name, out var prop) && prop.ValueKind == JsonValueKind.String)
            {
                return prop.GetString();
            }
            throw new ArgumentException($"Missing or invalid required parameter: {name}");
        }
        
        private string GetOptionalStringParam(JsonElement element, string name)
        {
            if (element.TryGetProperty(name, out var prop) && prop.ValueKind == JsonValueKind.String)
            {
                return prop.GetString();
            }
            return null;
        }
        
        private bool GetOptionalBoolParam(JsonElement element, string name, bool defaultValue)
        {
            if (element.TryGetProperty(name, out var prop) && prop.ValueKind == JsonValueKind.True || prop.ValueKind == JsonValueKind.False)
            {
                return prop.GetBoolean();
            }
            return defaultValue;
        }
        
        private class Command
        {
            public string Type { get; set; }
            public JsonElement Params { get; set; }
        }
    }
} 
```

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

```markdown
# Rhino Plugin Logging Specification

This document outlines enhanced logging for the RhinoMcpPlugin to facilitate better diagnostics of null reference exceptions and other issues.

## Logging Framework

1. **Use NLog or log4net** - Both provide flexible logging with configurable outputs and formats.
2. **Output Location** - Write logs to `/Users/angerman/scratch/rhinoMcpServer/logs/plugin/` to integrate with our unified logging system.
3. **Log File Naming** - Use date-based naming: `plugin_YYYY-MM-DD.log`.
4. **Log Format** - Match the server format: `[timestamp] [level] [component] message`.

## Core Logging Components

### Socket Server Component

```csharp
// Add this at the top of your socket server class
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();

// In the server initialization
Logger.Info("Socket server initializing on port 9876");

// When accepting connections
Logger.Info($"Client connected from {client.Client.RemoteEndPoint}");

// Before processing each command
Logger.Info($"Received command: {commandJson}");

// Add try/catch with detailed exception logging
try {
    // Process command
} catch (NullReferenceException ex) {
    Logger.Error(ex, $"NULL REFERENCE processing command: {commandJson}");
    Logger.Error($"Stack trace: {ex.StackTrace}");
    // Identify which object is null
    Logger.Error($"Context: Document={doc != null}, ActiveDoc={Rhino.RhinoDoc.ActiveDoc != null}");
    return JsonConvert.SerializeObject(new { error = $"Error processing command: {ex.Message}" });
}
```

### Command Handler Component

For each command handler method, add structured try/catch blocks:

```csharp
public string HandleCreateBox(JObject parameters) {
    Logger.Debug($"Processing create_box with parameters: {parameters}");
    
    try {
        // Log parameter extraction
        Logger.Debug("Extracting parameters...");
        double cornerX = parameters.Value<double>("cornerX");
        // ... other parameters ...
        
        // Log document access
        Logger.Debug("Accessing Rhino document...");
        var doc = Rhino.RhinoDoc.ActiveDoc;
        if (doc == null) {
            Logger.Error("CRITICAL: RhinoDoc.ActiveDoc is NULL");
            return JsonConvert.SerializeObject(new { 
                error = "No active Rhino document. Please open a document first." 
            });
        }
        
        // Log geometric operations with safeguards
        Logger.Debug("Creating geometry...");
        var corner = new Rhino.Geometry.Point3d(cornerX, cornerY, cornerZ);
        var box = new Rhino.Geometry.Box(
            new Rhino.Geometry.Plane(corner, Rhino.Geometry.Vector3d.ZAxis),
            new Rhino.Geometry.Interval(0, width),
            new Rhino.Geometry.Interval(0, depth),
            new Rhino.Geometry.Interval(0, height)
        );
        
        // Verify box was created
        if (box == null || !box.IsValid) {
            Logger.Error($"Box creation failed: {(box == null ? "null box" : "invalid box")}");
            return JsonConvert.SerializeObject(new { 
                error = "Failed to create valid box geometry" 
            });
        }
        
        // Log document modification
        Logger.Debug("Adding to document...");
        var id = doc.Objects.AddBox(box);
        if (id == Guid.Empty) {
            Logger.Error("Failed to add box to document");
            return JsonConvert.SerializeObject(new { 
                error = "Failed to add box to document" 
            });
        }
        
        // Log successful operation
        Logger.Info($"Successfully created box with ID {id}");
        return JsonConvert.SerializeObject(new { 
            success = true, 
            objectId = id.ToString() 
        });
    }
    catch (NullReferenceException ex) {
        Logger.Error(ex, $"NULL REFERENCE in create_box: {ex.Message}");
        Logger.Error($"Stack trace: {ex.StackTrace}");
        return JsonConvert.SerializeObject(new { 
            error = $"Error processing command: {ex.Message}" 
        });
    }
    catch (Exception ex) {
        Logger.Error(ex, $"Exception in create_box: {ex.Message}");
        return JsonConvert.SerializeObject(new { 
            error = $"Error processing command: {ex.Message}" 
        });
    }
}
```

### Plugin Initialization

Add detailed logging during plugin initialization to verify the environment:

```csharp
public override void OnLoad(ref string errorMessage) {
    try {
        Logger.Info("Plugin loading...");
        Logger.Info($"Rhino version: {Rhino.RhinoApp.Version}");
        Logger.Info($"Current directory: {System.IO.Directory.GetCurrentDirectory()}");
        
        // Check document status
        var docCount = Rhino.RhinoDoc.OpenDocuments().Length;
        Logger.Info($"Open document count: {docCount}");
        Logger.Info($"Active document: {(Rhino.RhinoDoc.ActiveDoc != null ? "exists" : "null")}");
        
        // Initialize socket server
        socketServer = new SocketServer();
        var success = socketServer.Start();
        Logger.Info($"Socket server started: {success}");
        
        // Check all essential components
        var componentStatus = new Dictionary<string, bool> {
            { "SocketServer", socketServer != null },
            { "CommandHandlers", commandHandlers != null },
            { "RhinoDoc", Rhino.RhinoDoc.ActiveDoc != null }
        };
        
        foreach (var component in componentStatus) {
            Logger.Info($"Component {component.Key}: {(component.Value ? "OK" : "NULL")}");
        }
        
        Logger.Info("Plugin loaded successfully");
    }
    catch (Exception ex) {
        Logger.Error(ex, $"Error during plugin load: {ex.Message}");
        errorMessage = ex.Message;
    }
}
```

## Document Lifecycle Monitoring

Add event handlers to monitor document open/close/new events:

```csharp
public override void OnLoad(ref string errorMessage) {
    // existing code...
    
    // Register document events
    Rhino.RhinoDoc.NewDocument += OnNewDocument;
    Rhino.RhinoDoc.CloseDocument += OnCloseDocument;
    Rhino.RhinoDoc.BeginOpenDocument += OnBeginOpenDocument;
    Rhino.RhinoDoc.EndOpenDocument += OnEndOpenDocument;
}

private void OnNewDocument(object sender, DocumentEventArgs e) {
    Logger.Info($"New document created: {e.Document.Name}");
    Logger.Info($"Active document: {(Rhino.RhinoDoc.ActiveDoc != null ? Rhino.RhinoDoc.ActiveDoc.Name : "null")}");
}

private void OnCloseDocument(object sender, DocumentEventArgs e) {
    Logger.Info($"Document closed: {e.Document.Name}");
    Logger.Info($"Remaining open documents: {Rhino.RhinoDoc.OpenDocuments().Length}");
}

private void OnBeginOpenDocument(object sender, DocumentOpenEventArgs e) {
    Logger.Info($"Beginning to open document: {e.FileName}");
}

private void OnEndOpenDocument(object sender, DocumentOpenEventArgs e) {
    Logger.Info($"Finished opening document: {e.Document.Name}");
}
```

## Socket Server Health Checks

Implement a health check command that verifies critical components:

```csharp
public string HandleHealthCheck(JObject parameters) {
    try {
        Logger.Info("Performing health check...");
        
        var healthStatus = new Dictionary<string, object> {
            { "PluginLoaded", true },
            { "RhinoVersion", Rhino.RhinoApp.Version.ToString() },
            { "ActiveDocument", Rhino.RhinoDoc.ActiveDoc != null },
            { "OpenDocumentCount", Rhino.RhinoDoc.OpenDocuments().Length },
            { "SocketServerRunning", socketServer != null && socketServer.IsRunning },
            { "MemoryUsage", System.GC.GetTotalMemory(false) / 1024 / 1024 + " MB" },
            { "SdkVersion", typeof(Rhino.RhinoApp).Assembly.GetName().Version.ToString() }
        };
        
        Logger.Info($"Health check results: {JsonConvert.SerializeObject(healthStatus)}");
        return JsonConvert.SerializeObject(new { 
            success = true, 
            result = healthStatus 
        });
    }
    catch (Exception ex) {
        Logger.Error(ex, $"Exception during health check: {ex.Message}");
        return JsonConvert.SerializeObject(new { 
            error = $"Health check failed: {ex.Message}" 
        });
    }
}
```

## Implementation Steps

1. Add NLog NuGet package to the plugin project.
2. Create an NLog configuration file that outputs to our logs directory.
3. Add the Logger initialization to each class.
4. Implement the detailed try/catch blocks with contextual logging.
5. Add the health check command.
6. Test with the Python server and verify logs are being generated.

## Sample NLog Configuration

```xml
<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    
    <targets>
        <target name="logfile" xsi:type="File"
                fileName="${specialfolder:folder=UserProfile}/scratch/rhinoMcpServer/logs/plugin/plugin_${date:format=yyyy-MM-dd}.log"
                layout="[${longdate}] [${level:uppercase=true}] [plugin] ${message} ${exception:format=toString}" />
        <target name="console" xsi:type="Console"
                layout="[${longdate}] [${level:uppercase=true}] [plugin] ${message} ${exception:format=toString}" />
    </targets>
    
    <rules>
        <logger name="*" minlevel="Debug" writeTo="logfile" />
        <logger name="*" minlevel="Info" writeTo="console" />
    </rules>
</nlog>
```

## Key Areas to Monitor

Based on the error patterns, focus logging on these key areas:

1. **Document State** - Check if RhinoDoc.ActiveDoc is null before attempting operations
2. **Socket Communication** - Log all incoming/outgoing messages
3. **Parameter Validation** - Verify parameters before using them
4. **Geometry Creation** - Add safeguards around geometry creation operations
5. **UI Thread Operations** - Ensure document modifications happen on the UI thread

This enhanced logging will help identify which specific object is null and under what conditions the error occurs. 
```

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

```csharp
using System;
using System.Drawing;
using NUnit.Framework;
using RhinoMcpPlugin.Tests.Mocks;
using Rhino.Geometry;
using Rhino.DocObjects;
using Moq;

namespace RhinoMcpPlugin.Tests
{
    [TestFixture]
    [Category("Utilities")]
    public class RhinoUtilitiesTests
    {
        private MockRhinoDoc _doc;
        
        [SetUp]
        public void Setup()
        {
            _doc = new MockRhinoDoc();
        }
        
        [Test]
        public void ParseHexColor_ValidHexWithHash_ReturnsCorrectColor()
        {
            // Arrange
            string hexColor = "#FF0000";
            
            // Act
            Color? color = RhinoUtilities.ParseHexColor(hexColor);
            
            // Assert
            Assert.That(color, Is.Not.Null);
            Assert.That(color.Value.R, Is.EqualTo(255));
            Assert.That(color.Value.G, Is.EqualTo(0));
            Assert.That(color.Value.B, Is.EqualTo(0));
        }
        
        [Test]
        public void ParseHexColor_ValidHexWithoutHash_ReturnsCorrectColor()
        {
            // Arrange
            string hexColor = "00FF00";
            
            // Act
            Color? color = RhinoUtilities.ParseHexColor(hexColor);
            
            // Assert
            Assert.That(color, Is.Not.Null);
            Assert.That(color.Value.R, Is.EqualTo(0));
            Assert.That(color.Value.G, Is.EqualTo(255));
            Assert.That(color.Value.B, Is.EqualTo(0));
        }
        
        [Test]
        public void ParseHexColor_ValidHexWithAlpha_ReturnsCorrectColor()
        {
            // Arrange
            string hexColor = "80FF0000";
            
            // Act
            Color? color = RhinoUtilities.ParseHexColor(hexColor);
            
            // Assert
            Assert.That(color, Is.Not.Null);
            Assert.That(color.Value.A, Is.EqualTo(128));
            Assert.That(color.Value.R, Is.EqualTo(255));
            Assert.That(color.Value.G, Is.EqualTo(0));
            Assert.That(color.Value.B, Is.EqualTo(0));
        }
        
        [Test]
        public void ParseHexColor_NamedColor_ReturnsCorrectColor()
        {
            // Arrange
            string colorName = "Red";
            
            // Act
            Color? color = RhinoUtilities.ParseHexColor(colorName);
            
            // Assert
            Assert.That(color, Is.Not.Null);
            Assert.That(color.Value.R, Is.EqualTo(255));
            Assert.That(color.Value.G, Is.EqualTo(0));
            Assert.That(color.Value.B, Is.EqualTo(0));
        }
        
        [Test]
        public void ParseHexColor_InvalidHex_ReturnsNull()
        {
            // Arrange
            string hexColor = "XYZ123";
            
            // Act
            Color? color = RhinoUtilities.ParseHexColor(hexColor);
            
            // Assert
            Assert.That(color, Is.Null);
        }
        
        [Test]
        public void ParseHexColor_EmptyString_ReturnsNull()
        {
            // Arrange
            string hexColor = "";
            
            // Act
            Color? color = RhinoUtilities.ParseHexColor(hexColor);
            
            // Assert
            Assert.That(color, Is.Null);
        }
        
        // The following tests would require more complex mocking to simulate RhinoObject
        // and need to be rewritten when we have access to the actual RhinoUtilities implementation
        
        [Test]
        public void GetObjectProperties_ValidObject_ReturnsCorrectProperties()
        {
            // Arrange
            var sphere = new Sphere(new Point3d(1, 2, 3), 5);
            var sphereObj = _doc.AddSphere(sphere);
            
            // Create a dynamic mock for RhinoObject
            dynamic mockRhinoObject = new MockDynamicRhinoObject(sphereObj, _doc);
            
            // Act
            var props = RhinoUtilities.GetObjectProperties(mockRhinoObject);
            
            // Assert
            Assert.That(props, Is.Not.Null);
            Assert.That(props.Id, Is.EqualTo(sphereObj.Id.ToString()));
            Assert.That(props.Type, Is.EqualTo("None")); // Our mock returns ObjectType.None
            Assert.That(props.Layer, Is.EqualTo("Default"));
            Assert.That(props.Position.X, Is.EqualTo(1).Within(0.001));
            Assert.That(props.Position.Y, Is.EqualTo(2).Within(0.001));
            Assert.That(props.Position.Z, Is.EqualTo(3).Within(0.001));
        }
        
        [Test]
        public void GetObjectProperties_NullObject_ReturnsNull()
        {
            // Act
            var properties = RhinoUtilities.GetObjectProperties(null);
            
            // Assert
            Assert.That(properties, Is.Null);
        }
        
        [Test]
        public void GetAllObjects_DocWithObjects_ReturnsAllObjects()
        {
            // Arrange
            var sphere = new Sphere(new Point3d(1, 2, 3), 5);
            var box = new Box(new BoundingBox(new Point3d(0, 0, 0), new Point3d(10, 10, 10)));
            _doc.AddSphere(sphere);
            _doc.AddBox(box);
            
            // Act
            // Create a dynamic mock for RhinoDoc to use with the static utility method
            dynamic mockRhinoDoc = new MockDynamicRhinoDoc(_doc);
            var objects = RhinoUtilities.GetAllObjects(mockRhinoDoc);
            
            // Assert
            Assert.That(objects, Is.Not.Null);
            Assert.That(objects.Count, Is.EqualTo(2));
        }
        
        [Test]
        public void GetAllObjects_NullDoc_ReturnsEmptyList()
        {
            // Act
            var objects = RhinoUtilities.GetAllObjects(null);
            
            // Assert
            Assert.That(objects, Is.Not.Null);
            Assert.That(objects.Count, Is.EqualTo(0));
        }
        
        [Test]
        public void GetSceneContext_ValidDoc_ReturnsSceneInfo()
        {
            // Arrange
            // Create test objects
            var sphere = new Sphere(new Point3d(1, 2, 3), 5);
            var box = new Box(new BoundingBox(new Point3d(0, 0, 0), new Point3d(10, 10, 10)));
            
            // Add test objects to mock doc
            var sphereObj = _doc.AddSphere(sphere);
            var boxObj = _doc.AddBox(box);

            // Add a test layer
            _doc.AddLayer("TestLayer", Color.Blue);
            
            // Create a dynamic mock for RhinoDoc to use with the static utility method
            dynamic mockRhinoDoc = new MockDynamicRhinoDoc(_doc);
            
            // Act
            var sceneContext = RhinoUtilities.GetSceneContext(mockRhinoDoc);
            
            // Assert
            Assert.That(sceneContext, Is.Not.Null);
            Assert.That(sceneContext.ObjectCount, Is.EqualTo(2));
            Assert.That(sceneContext.Objects, Has.Count.EqualTo(2));
            Assert.That(sceneContext.ActiveView, Is.EqualTo("None"));
            Assert.That(sceneContext.Layers, Has.Count.EqualTo(2));
            Assert.That(sceneContext.Layers, Contains.Item("Default"));
            Assert.That(sceneContext.Layers, Contains.Item("TestLayer"));
        }
        
        [Test]
        public void GetSceneContext_NullDoc_ThrowsArgumentNullException()
        {
            // Assert
            Assert.Throws<ArgumentNullException>(() => RhinoUtilities.GetSceneContext(null));
        }
    }

    /// <summary>
    /// A dynamic proxy to help with RhinoDoc mocking
    /// </summary>
    public class MockDynamicRhinoDoc : System.Dynamic.DynamicObject
    {
        private readonly MockRhinoDoc _mockDoc;
        private readonly MockRhinoViews _views = new MockRhinoViews();

        public MockDynamicRhinoDoc(MockRhinoDoc mockDoc)
        {
            _mockDoc = mockDoc;
        }
        
        // Passthrough to mock object table
        public MockRhinoObjectTable Objects => (MockRhinoObjectTable)_mockDoc.Objects;
        
        // Passthrough to mock layer table
        public MockLayerTable Layers => (MockLayerTable)_mockDoc.Layers;
        
        // Provide a views collection
        public MockRhinoViews Views => _views;
    }

    /// <summary>
    /// A dynamic proxy to help with RhinoObject mocking
    /// </summary>
    public class MockDynamicRhinoObject : System.Dynamic.DynamicObject
    {
        private readonly IMockRhinoObject _mockObject;
        private readonly MockRhinoDoc _mockDoc;

        public MockDynamicRhinoObject(IMockRhinoObject mockObject, MockRhinoDoc mockDoc)
        {
            _mockObject = mockObject;
            _mockDoc = mockDoc;
        }
        
        // Pass through for Id
        public Guid Id => _mockObject.Id;
        
        // Pass through for Attributes
        public IMockObjectAttributes Attributes => _mockObject.Attributes;
        
        // Pass through for Geometry
        public GeometryBase Geometry => _mockObject.Geometry;
        
        // Pass through for ObjectType
        public ObjectType ObjectType => _mockObject.ObjectType;
        
        // Provide document reference
        public dynamic Document => new MockDynamicRhinoDoc(_mockDoc);
        
        // Make sure we can serialize this object
        public override string ToString() => $"MockRhinoObject:{Id}";
    }

    /// <summary>
    /// Mock for Rhino views collection
    /// </summary>
    public class MockRhinoViews
    {
        private readonly MockRhinoView _activeView = new MockRhinoView();
        
        public MockRhinoView ActiveView => _activeView;
    }

    /// <summary>
    /// Mock for a Rhino view
    /// </summary>
    public class MockRhinoView
    {
        private readonly MockViewport _activeViewport = new MockViewport("Perspective");
        
        public MockViewport ActiveViewport => _activeViewport;
    }

    /// <summary>
    /// Mock for a Rhino viewport
    /// </summary>
    public class MockViewport
    {
        public string Name { get; }
        
        public MockViewport(string name)
        {
            Name = name;
        }
    }
} 
```

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

```python
#!/usr/bin/env python3
"""
Standalone MCP Server for Rhino
This script implements a direct MCP server that communicates with both Claude and Rhino
without any complex piping or shell scripts.
"""

import json
import os
import socket
import sys
import time
import logging
import signal
import threading
import traceback
from datetime import datetime

# Configure logging to stderr and file
log_dir = os.path.dirname(os.path.abspath(__file__))
root_dir = os.path.dirname(log_dir)
log_file = os.path.join(root_dir, "logs", "standalone_mcp_server.log")
os.makedirs(os.path.dirname(log_file), exist_ok=True)

# Configure logging to stderr only
logging.basicConfig(
    level=logging.INFO,
    format='[%(asctime)s] [%(levelname)s] %(message)s',
    handlers=[
        logging.StreamHandler(sys.stderr),
        logging.FileHandler(log_file)
    ]
)

# Global variables
EXIT_FLAG = False
SERVER_STATE = "waiting"  # States: waiting, initialized, processing

# Create a PID file
pid_file = os.path.join(root_dir, "logs", "standalone_server.pid")
with open(pid_file, "w") as f:
    f.write(str(os.getpid()))

def send_json_response(data):
    """Send a JSON response to stdout (Claude)"""
    try:
        # Ensure we're sending a valid JSON object
        json_str = json.dumps(data)
        
        # Write as plain text to stdout
        print(json_str, flush=True)
        
        logging.debug(f"Sent JSON response: {json_str[:100]}...")
    except Exception as e:
        logging.error(f"Error sending JSON: {str(e)}")

def handle_initialize(request):
    """Handle the initialize request and return server capabilities"""
    logging.info("Processing initialize request")
    
    # Get request ID from the client (default to 0 if not provided)
    request_id = request.get("id", 0)
    
    # Return hard-coded initialization response with tools
    response = {
        "jsonrpc": "2.0",
        "id": request_id,
        "result": {
            "serverInfo": {
                "name": "RhinoMcpServer",
                "version": "0.1.0"
            },
            "capabilities": {
                "tools": [
                    {
                        "name": "geometry_tools.create_sphere",
                        "description": "Creates a sphere with the specified center and radius",
                        "parameters": [
                            {"name": "centerX", "description": "X coordinate of the sphere center", "required": True, "schema": {"type": "number"}},
                            {"name": "centerY", "description": "Y coordinate of the sphere center", "required": True, "schema": {"type": "number"}},
                            {"name": "centerZ", "description": "Z coordinate of the sphere center", "required": True, "schema": {"type": "number"}},
                            {"name": "radius", "description": "Radius of the sphere", "required": True, "schema": {"type": "number"}},
                            {"name": "color", "description": "Optional color for the sphere (e.g., 'red', 'blue', etc.)", "required": False, "schema": {"type": "string"}}
                        ]
                    },
                    {
                        "name": "geometry_tools.create_box",
                        "description": "Creates a box with the specified dimensions",
                        "parameters": [
                            {"name": "cornerX", "description": "X coordinate of the box corner", "required": True, "schema": {"type": "number"}},
                            {"name": "cornerY", "description": "Y coordinate of the box corner", "required": True, "schema": {"type": "number"}},
                            {"name": "cornerZ", "description": "Z coordinate of the box corner", "required": True, "schema": {"type": "number"}},
                            {"name": "width", "description": "Width of the box (X dimension)", "required": True, "schema": {"type": "number"}},
                            {"name": "depth", "description": "Depth of the box (Y dimension)", "required": True, "schema": {"type": "number"}},
                            {"name": "height", "description": "Height of the box (Z dimension)", "required": True, "schema": {"type": "number"}},
                            {"name": "color", "description": "Optional color for the box (e.g., 'red', 'blue', etc.)", "required": False, "schema": {"type": "string"}}
                        ]
                    },
                    {
                        "name": "geometry_tools.create_cylinder",
                        "description": "Creates a cylinder with the specified base point, height, and radius",
                        "parameters": [
                            {"name": "baseX", "description": "X coordinate of the cylinder base point", "required": True, "schema": {"type": "number"}},
                            {"name": "baseY", "description": "Y coordinate of the cylinder base point", "required": True, "schema": {"type": "number"}},
                            {"name": "baseZ", "description": "Z coordinate of the cylinder base point", "required": True, "schema": {"type": "number"}},
                            {"name": "height", "description": "Height of the cylinder", "required": True, "schema": {"type": "number"}},
                            {"name": "radius", "description": "Radius of the cylinder", "required": True, "schema": {"type": "number"}},
                            {"name": "color", "description": "Optional color for the cylinder (e.g., 'red', 'blue', etc.)", "required": False, "schema": {"type": "string"}}
                        ]
                    },
                    {
                        "name": "scene_tools.get_scene_info",
                        "description": "Gets information about objects in the current scene",
                        "parameters": []
                    },
                    {
                        "name": "scene_tools.clear_scene",
                        "description": "Clears all objects from the current scene",
                        "parameters": [
                            {"name": "currentLayerOnly", "description": "If true, only delete objects on the current layer", "required": False, "schema": {"type": "boolean"}}
                        ]
                    },
                    {
                        "name": "scene_tools.create_layer",
                        "description": "Creates a new layer in the Rhino document",
                        "parameters": [
                            {"name": "name", "description": "Name of the new layer", "required": True, "schema": {"type": "string"}},
                            {"name": "color", "description": "Optional color for the layer (e.g., 'red', 'blue', etc.)", "required": False, "schema": {"type": "string"}}
                        ]
                    }
                ]
            }
        }
    }
    
    # Mark server as initialized
    global SERVER_STATE
    SERVER_STATE = "initialized"
    logging.info("Server initialized successfully")
    
    return response

def handle_tool_call(request):
    """Handle a tool call request"""
    tool_name = request["params"]["name"]
    parameters = request["params"]["parameters"]
    request_id = request.get("id", 0)
    
    logging.info(f"Executing tool: {tool_name}")
    
    # Here you would actually implement the tool functionality
    # For this example, we just return a dummy success response
    response = {
        "jsonrpc": "2.0",
        "id": request_id,
        "result": {
            "result": {"success": True, "message": f"Executed {tool_name} with parameters {parameters}"}
        }
    }
    
    return response

def handle_shutdown(request):
    """Handle a shutdown request"""
    request_id = request.get("id", 0)
    logging.info("Shutdown requested")
    
    global EXIT_FLAG
    EXIT_FLAG = True
    
    response = {
        "jsonrpc": "2.0",
        "id": request_id,
        "result": {"success": True}
    }
    
    return response

def process_message(message):
    """Process a single message from the client."""
    try:
        method = message.get("method", "")
        
        if method == "initialize":
            response = handle_initialize(message)
            send_json_response(response)
            return True
        elif method == "tools/call":
            response = handle_tool_call(message)
            send_json_response(response)
            return True
        elif method == "shutdown":
            response = handle_shutdown(message)
            send_json_response(response)
            return False
        elif method == "notifications/cancelled":
            # Just acknowledge and continue
            logging.info("Received cancellation notification")
            return True
        else:
            logging.warning(f"Unknown method: {method}")
            return True
    except Exception as e:
        logging.error(f"Error processing message: {e}")
        traceback.print_exc(file=sys.stderr)
        return True  # Keep running even on errors

def cleanup():
    """Clean up resources when exiting"""
    try:
        if os.path.exists(pid_file):
            os.remove(pid_file)
            logging.info("Removed PID file")
    except Exception as e:
        logging.error(f"Error in cleanup: {str(e)}")

def signal_handler(sig, frame):
    """Handle termination signals"""
    global EXIT_FLAG, SERVER_STATE
    
    logging.info(f"Received signal {sig}")
    
    # If we're initialized, ignore SIGTERM (15) and stay alive
    if SERVER_STATE == "initialized" and sig == signal.SIGTERM:
        logging.info(f"Server is initialized - ignoring SIGTERM and staying alive")
        return
        
    # Otherwise, exit normally
    logging.info(f"Initiating shutdown...")
    EXIT_FLAG = True

def main():
    """Main function"""
    global EXIT_FLAG
    
    # Register signal handlers
    signal.signal(signal.SIGTERM, signal_handler)
    signal.signal(signal.SIGINT, signal_handler)
    
    # Register cleanup handler
    import atexit
    atexit.register(cleanup)
    
    logging.info("=== MCP Server Starting ===")
    logging.info(f"Process ID: {os.getpid()}")
    
    try:
        # Main loop
        while not EXIT_FLAG:
            try:
                # Read a line from stdin
                line = input()
                if not line:
                    time.sleep(0.1)
                    continue
                
                # Parse the JSON message
                try:
                    message = json.loads(line)
                    # Process the message
                    if not process_message(message):
                        break
                except json.JSONDecodeError as e:
                    logging.error(f"Invalid JSON received: {e}")
                    continue
            except EOFError:
                # If we're initialized, keep running even if stdin is closed
                if SERVER_STATE == "initialized":
                    logging.info("Stdin closed but server is initialized - staying alive")
                    # Sleep to avoid tight loop if stdin is permanently closed
                    time.sleep(5)
                    continue
                else:
                    logging.info("Stdin closed, exiting...")
                    break
            except Exception as e:
                logging.error(f"Error in main loop: {e}")
                traceback.print_exc(file=sys.stderr)
                time.sleep(1)
    finally:
        logging.info("Server shutting down...")
        cleanup()

if __name__ == "__main__":
    main() 
```

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

```python
#!/usr/bin/env python3
"""
Daemon MCP Server for Rhino
This script implements a socket-based MCP server that runs in the background
and allows multiple connections from Claude Desktop.
"""

import json
import os
import socket
import sys
import time
import logging
import signal
import threading
import traceback
from datetime import datetime
import socketserver

# Configure logging - log to both stderr and a file
log_dir = os.path.dirname(os.path.abspath(__file__))
root_dir = os.path.dirname(log_dir)
log_file = os.path.join(root_dir, "logs", "daemon_mcp_server.log")
os.makedirs(os.path.dirname(log_file), exist_ok=True)

logging.basicConfig(
    level=logging.INFO,
    format='[%(asctime)s] [%(levelname)s] %(message)s',
    handlers=[
        logging.StreamHandler(sys.stderr),
        logging.FileHandler(log_file)
    ]
)

# Global variables
EXIT_FLAG = False
SERVER_STATE = "waiting"  # States: waiting, initialized, processing
SOCKET_PORT = 8765  # Port for the socket server

# Create a PID file to track this process
pid_file = os.path.join(root_dir, "logs", "daemon_server.pid")
with open(pid_file, "w") as f:
    f.write(str(os.getpid()))

# Tools configuration - shared among all connections
SERVER_CAPABILITIES = {
    "serverInfo": {
        "name": "RhinoMcpServer",
        "version": "0.1.0"
    },
    "capabilities": {
        "tools": [
            {
                "name": "geometry_tools.create_sphere",
                "description": "Creates a sphere with the specified center and radius",
                "parameters": [
                    {"name": "centerX", "description": "X coordinate of the sphere center", "required": True, "schema": {"type": "number"}},
                    {"name": "centerY", "description": "Y coordinate of the sphere center", "required": True, "schema": {"type": "number"}},
                    {"name": "centerZ", "description": "Z coordinate of the sphere center", "required": True, "schema": {"type": "number"}},
                    {"name": "radius", "description": "Radius of the sphere", "required": True, "schema": {"type": "number"}},
                    {"name": "color", "description": "Optional color for the sphere (e.g., 'red', 'blue', etc.)", "required": False, "schema": {"type": "string"}}
                ]
            },
            {
                "name": "geometry_tools.create_box",
                "description": "Creates a box with the specified dimensions",
                "parameters": [
                    {"name": "cornerX", "description": "X coordinate of the box corner", "required": True, "schema": {"type": "number"}},
                    {"name": "cornerY", "description": "Y coordinate of the box corner", "required": True, "schema": {"type": "number"}},
                    {"name": "cornerZ", "description": "Z coordinate of the box corner", "required": True, "schema": {"type": "number"}},
                    {"name": "width", "description": "Width of the box (X dimension)", "required": True, "schema": {"type": "number"}},
                    {"name": "depth", "description": "Depth of the box (Y dimension)", "required": True, "schema": {"type": "number"}},
                    {"name": "height", "description": "Height of the box (Z dimension)", "required": True, "schema": {"type": "number"}},
                    {"name": "color", "description": "Optional color for the box (e.g., 'red', 'blue', etc.)", "required": False, "schema": {"type": "string"}}
                ]
            },
            {
                "name": "geometry_tools.create_cylinder",
                "description": "Creates a cylinder with the specified base point, height, and radius",
                "parameters": [
                    {"name": "baseX", "description": "X coordinate of the cylinder base point", "required": True, "schema": {"type": "number"}},
                    {"name": "baseY", "description": "Y coordinate of the cylinder base point", "required": True, "schema": {"type": "number"}},
                    {"name": "baseZ", "description": "Z coordinate of the cylinder base point", "required": True, "schema": {"type": "number"}},
                    {"name": "height", "description": "Height of the cylinder", "required": True, "schema": {"type": "number"}},
                    {"name": "radius", "description": "Radius of the cylinder", "required": True, "schema": {"type": "number"}},
                    {"name": "color", "description": "Optional color for the cylinder (e.g., 'red', 'blue', etc.)", "required": False, "schema": {"type": "string"}}
                ]
            },
            {
                "name": "scene_tools.get_scene_info",
                "description": "Gets information about objects in the current scene",
                "parameters": []
            },
            {
                "name": "scene_tools.clear_scene",
                "description": "Clears all objects from the current scene",
                "parameters": [
                    {"name": "currentLayerOnly", "description": "If true, only delete objects on the current layer", "required": False, "schema": {"type": "boolean"}}
                ]
            },
            {
                "name": "scene_tools.create_layer",
                "description": "Creates a new layer in the Rhino document",
                "parameters": [
                    {"name": "name", "description": "Name of the new layer", "required": True, "schema": {"type": "string"}},
                    {"name": "color", "description": "Optional color for the layer (e.g., 'red', 'blue', etc.)", "required": False, "schema": {"type": "string"}}
                ]
            }
        ]
    }
}

class MCPRequestHandler(socketserver.BaseRequestHandler):
    """
    Handler for MCP requests over a TCP socket
    """
    def handle(self):
        """Handle requests from a client"""
        self.client_connected = True
        self.local_state = "waiting"
        logging.info(f"Client connected from {self.client_address}")
        
        try:
            buffer = b""
            while not EXIT_FLAG and self.client_connected:
                try:
                    # Read data from socket
                    data = self.request.recv(4096)
                    if not data:
                        # Client disconnected
                        logging.info(f"Client disconnected: {self.client_address}")
                        self.client_connected = False
                        break
                    
                    # Add received data to buffer
                    buffer += data
                    
                    # Process complete messages (assuming each message ends with newline)
                    while b'\n' in buffer:
                        line, buffer = buffer.split(b'\n', 1)
                        if line:
                            line_str = line.decode('utf-8')
                            self.process_message(line_str)
                except json.JSONDecodeError as e:
                    logging.error(f"Invalid JSON: {e}")
                    continue
                except Exception as e:
                    logging.error(f"Error handling client: {e}")
                    traceback.print_exc()
                    # Don't break here - try to continue handling client
                    time.sleep(0.1)
                    continue
        finally:
            logging.info(f"Client handler exiting: {self.client_address}")
    
    def process_message(self, message_str):
        """Process a message from the client"""
        try:
            message = json.loads(message_str)
            method = message.get("method", "")
            logging.info(f"Processing message: {method}")
            
            if method == "initialize":
                self.handle_initialize(message)
            elif method == "tools/call":
                self.handle_tool_call(message)
            elif method == "shutdown":
                self.handle_shutdown(message)
            elif method == "notifications/cancelled":
                logging.info("Received cancellation notification")
            else:
                logging.warning(f"Unknown method: {method}")
                # Send error response for unknown methods
                response = {
                    "jsonrpc": "2.0",
                    "id": message.get("id", 0),
                    "error": {
                        "code": -32601,
                        "message": f"Method '{method}' not found"
                    }
                }
                self.send_response(response)
        except Exception as e:
            logging.error(f"Error processing message: {e}")
            traceback.print_exc()
            # Try to send error response
            try:
                response = {
                    "jsonrpc": "2.0",
                    "id": message.get("id", 0) if isinstance(message, dict) else 0,
                    "error": {
                        "code": -32603,
                        "message": f"Internal error: {str(e)}"
                    }
                }
                self.send_response(response)
            except:
                pass
    
    def handle_initialize(self, request):
        """Handle initialize request"""
        global SERVER_STATE
        request_id = request.get("id", 0)
        
        # Set the global server state to initialized
        SERVER_STATE = "initialized"
        self.local_state = "initialized"
        logging.info("Server initialized successfully")
        
        # Create response
        response = {
            "jsonrpc": "2.0",
            "id": request_id,
            "result": SERVER_CAPABILITIES
        }
        
        # Send response
        self.send_response(response)
        
        # Keep the connection open - don't close after initialization
        logging.info("Initialization complete, keeping connection open for further requests")
    
    def handle_tool_call(self, request):
        """Handle tool call request"""
        tool_name = request["params"]["name"]
        parameters = request["params"]["parameters"]
        request_id = request.get("id", 0)
        
        logging.info(f"Executing tool: {tool_name}")
        
        # Return dummy success response
        response = {
            "jsonrpc": "2.0",
            "id": request_id,
            "result": {
                "result": {"success": True, "message": f"Executed {tool_name} with parameters {parameters}"}
            }
        }
        
        self.send_response(response)
    
    def handle_shutdown(self, request):
        """Handle shutdown request"""
        request_id = request.get("id", 0)
        logging.info("Shutdown requested by client")
        
        # Only shut down this client connection, not the entire server
        response = {
            "jsonrpc": "2.0",
            "id": request_id,
            "result": {"success": True}
        }
        
        self.send_response(response)
        self.client_connected = False
    
    def send_response(self, data):
        """Send JSON response to the client"""
        try:
            # Serialize to JSON and add newline
            json_str = json.dumps(data) + "\n"
            
            # Send as bytes
            self.request.sendall(json_str.encode('utf-8'))
            logging.debug(f"Sent response: {json_str[:100]}...")
        except Exception as e:
            logging.error(f"Error sending response: {e}")
            traceback.print_exc()
            # Don't close the connection on send error
            logging.info("Continuing despite send error")

class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
    """Threaded TCP Server that allows for multiple simultaneous connections"""
    daemon_threads = True
    allow_reuse_address = True

def signal_handler(sig, frame):
    """Handle termination signals"""
    global EXIT_FLAG
    
    logging.info(f"Received signal {sig}")
    
    # If server is in critical section, delay for a bit
    if SERVER_STATE != "waiting":
        logging.info("Server is busy, delaying shutdown...")
        time.sleep(1)
    
    # Set exit flag to trigger graceful shutdown
    EXIT_FLAG = True

def cleanup():
    """Clean up resources when exiting"""
    try:
        if os.path.exists(pid_file):
            os.remove(pid_file)
            logging.info("Removed PID file")
    except Exception as e:
        logging.error(f"Error in cleanup: {e}")

def main():
    """Main function"""
    global EXIT_FLAG
    
    # Register signal handlers
    signal.signal(signal.SIGTERM, signal_handler)
    signal.signal(signal.SIGINT, signal_handler)
    
    # Register cleanup handler
    import atexit
    atexit.register(cleanup)
    
    logging.info("=== Daemon MCP Server Starting ===")
    logging.info(f"Process ID: {os.getpid()}")
    logging.info(f"Listening on port {SOCKET_PORT}")
    
    # Create the server
    server = ThreadedTCPServer(('localhost', SOCKET_PORT), MCPRequestHandler)
    
    # Start a thread with the server
    server_thread = threading.Thread(target=server.serve_forever)
    server_thread.daemon = True
    server_thread.start()
    
    try:
        # Keep the main thread running
        while not EXIT_FLAG:
            time.sleep(0.1)
    except KeyboardInterrupt:
        logging.info("Keyboard interrupt received")
    finally:
        logging.info("Server shutting down...")
        server.shutdown()
        cleanup()

if __name__ == "__main__":
    main() 
```

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

```python
#!/usr/bin/env python3
"""
Socket Proxy for MCP Server
This script acts as a proxy between Claude Desktop and our daemon server.
It forwards stdin to the daemon server and stdout back to Claude Desktop.
"""

import json
import os
import socket
import sys
import time
import logging
import signal
import traceback
import subprocess
import threading

# Configure logging - to file only, NOT stdout or stderr
log_dir = os.path.dirname(os.path.abspath(__file__))
root_dir = os.path.dirname(log_dir)
log_file = os.path.join(root_dir, "logs", "socket_proxy.log")
os.makedirs(os.path.dirname(log_file), exist_ok=True)

logging.basicConfig(
    level=logging.INFO,
    format='[%(asctime)s] [%(levelname)s] %(message)s',
    handlers=[
        logging.FileHandler(log_file)
    ]
)

# Global variables
SERVER_PORT = 8765  # Must match port in daemon_mcp_server.py
EXIT_FLAG = False
INITIALIZED = False  # Flag to track if we've been initialized

def ensure_daemon_running():
    """Make sure the daemon server is running"""
    daemon_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "daemon_mcp_server.py")
    pid_file = os.path.join(root_dir, "logs", "daemon_server.pid")
    
    # Check if PID file exists and process is running
    if os.path.exists(pid_file):
        try:
            with open(pid_file, 'r') as f:
                pid = int(f.read().strip())
            
            # Try to send signal 0 to check if process exists
            os.kill(pid, 0)
            logging.info(f"Daemon server already running with PID {pid}")
            return True
        except (OSError, ValueError):
            logging.info("Daemon server PID file exists but process is not running")
            # Remove stale PID file
            try:
                os.remove(pid_file)
            except:
                pass
    
    # Start the daemon server
    logging.info("Starting daemon server...")
    try:
        # Make daemon script executable
        os.chmod(daemon_path, 0o755)
        
        # Start the daemon in the background
        subprocess.Popen(
            [daemon_path],
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
            stdin=subprocess.DEVNULL,
            cwd=os.path.dirname(os.path.abspath(__file__))
        )
        
        # Wait for the daemon to start
        for _ in range(10):
            if os.path.exists(pid_file):
                logging.info("Daemon server started successfully")
                return True
            time.sleep(0.5)
        
        logging.error("Timeout waiting for daemon server to start")
        return False
    except Exception as e:
        logging.error(f"Error starting daemon server: {e}")
        return False

def connect_to_daemon():
    """Connect to the daemon server"""
    for i in range(5):  # Try 5 times with exponential backoff
        try:
            sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            sock.connect(('localhost', SERVER_PORT))
            logging.info("Connected to daemon server")
            return sock
        except (socket.error, ConnectionRefusedError) as e:
            logging.warning(f"Connection attempt {i+1} failed: {e}")
            time.sleep(2 ** i)  # Exponential backoff
    
    logging.error("Failed to connect to daemon server after multiple attempts")
    return None

def forward_stdin_to_socket(sock):
    """Forward stdin to socket"""
    global INITIALIZED
    logging.info("Starting stdin forwarding thread")
    
    try:
        while not EXIT_FLAG:
            try:
                # Read a line from stdin
                line = sys.stdin.readline()
                if not line:
                    if INITIALIZED:
                        logging.info("Stdin closed but initialized - staying alive")
                        # Sleep longer to reduce log spam
                        time.sleep(30)
                        continue
                    else:
                        logging.info("Stdin closed")
                        break
                
                # Check if this is an initialize message
                try:
                    message = json.loads(line)
                    if message.get("method") == "initialize":
                        logging.info("Detected initialize message - will ignore termination signals")
                        INITIALIZED = True
                except:
                    pass
                
                # Forward to socket with newline termination
                try:
                    sock.sendall(line.encode('utf-8'))
                    logging.debug(f"Forwarded to socket: {line.strip()}")
                except socket.error as e:
                    logging.error(f"Socket error when forwarding stdin: {e}")
                    if INITIALIZED:
                        # If we're initialized, try to reconnect
                        logging.info("Trying to reconnect after socket error...")
                        try:
                            sock.close()
                        except:
                            pass
                        
                        new_sock = connect_to_daemon()
                        if new_sock:
                            sock = new_sock
                            # Re-send the original message that failed
                            sock.sendall(line.encode('utf-8'))
                            logging.info("Reconnected and resent message")
                        else:
                            logging.error("Failed to reconnect after socket error")
                            if not INITIALIZED:
                                break
                    else:
                        break
            except Exception as e:
                logging.error(f"Error forwarding stdin: {e}")
                if INITIALIZED:
                    # If we're initialized, just sleep and continue
                    time.sleep(5)
                    continue
                else:
                    break
    except Exception as e:
        logging.error(f"Fatal error in stdin forwarding: {e}")
    finally:
        logging.info("Stdin forwarding thread exiting")

def forward_socket_to_stdout(sock):
    """Forward socket responses to stdout"""
    global INITIALIZED  # Move global declaration to beginning of function
    logging.info("Starting socket forwarding thread")
    
    buffer = b""
    try:
        while not EXIT_FLAG:
            try:
                # Read data from socket
                data = sock.recv(4096)
                if not data:
                    logging.info("Socket closed by server")
                    if INITIALIZED:
                        # Try to reconnect
                        logging.info("Trying to reconnect to daemon server...")
                        try:
                            sock.close()
                        except:
                            pass
                            
                        new_sock = connect_to_daemon()
                        if new_sock:
                            sock = new_sock
                            sock.settimeout(2.0)  # Make sure timeout is set on new socket
                            logging.info("Reconnected to daemon server")
                            continue
                        else:
                            # If reconnection fails but we're initialized, just sleep and retry
                            logging.info("Reconnection failed, but we're initialized - will retry later")
                            time.sleep(5)
                            continue
                    break
                
                # Add to buffer
                buffer += data
                
                # Process complete messages (assuming each message ends with newline)
                while b'\n' in buffer:
                    line, buffer = buffer.split(b'\n', 1)
                    if line:
                        # Forward to stdout
                        line_str = line.decode('utf-8')
                        
                        # Check if this is an initialization response before forwarding
                        try:
                            response = json.loads(line_str)
                            if isinstance(response, dict) and "jsonrpc" in response and "result" in response:
                                if "serverInfo" in response.get("result", {}):
                                    logging.info("Detected initialization response - setting INITIALIZED flag")
                                    INITIALIZED = True
                        except:
                            pass
                            
                        # Always forward to stdout
                        print(line_str, flush=True)
                        logging.debug(f"Forwarded to stdout: {line_str}")
            except socket.timeout:
                # Just a timeout, not an error
                continue
            except Exception as e:
                logging.error(f"Error reading from socket: {e}")
                if INITIALIZED:
                    # If we're initialized, try to reconnect
                    try:
                        sock.close()
                    except:
                        pass
                    
                    time.sleep(1)
                    new_sock = connect_to_daemon()
                    if new_sock:
                        sock = new_sock
                        sock.settimeout(2.0)  # Make sure timeout is set on new socket
                        logging.info("Reconnected to daemon server after error")
                        continue
                    else:
                        # If reconnection fails but we're initialized, just sleep and retry
                        logging.info("Reconnection failed after error, but we're initialized - will retry later")
                        time.sleep(5)
                        continue
                break
    except Exception as e:
        logging.error(f"Fatal error in socket forwarding: {e}")
    finally:
        logging.info("Socket forwarding thread exiting")
        # If we're initialized, automatically restart the thread
        if INITIALIZED and not EXIT_FLAG:
            logging.info("Socket thread exited but we're initialized - restarting socket thread")
            time.sleep(1)  # Brief pause before reconnecting
            new_sock = connect_to_daemon()
            if new_sock:
                new_sock.settimeout(2.0)  # Make sure timeout is set on new socket
                new_thread = threading.Thread(target=forward_socket_to_stdout, args=(new_sock,))
                new_thread.daemon = True
                new_thread.start()

def signal_handler(sig, frame):
    """Handle termination signals"""
    global EXIT_FLAG
    
    logging.info(f"Received signal {sig}")
    
    # If initialized, ignore termination signals
    if INITIALIZED:
        logging.info(f"Ignoring signal {sig} after initialization")
        return
    
    # Otherwise, exit normally
    logging.info(f"Exiting due to signal {sig}")
    EXIT_FLAG = True

def main():
    """Main function"""
    global EXIT_FLAG
    
    # Register signal handlers
    signal.signal(signal.SIGTERM, signal_handler)
    signal.signal(signal.SIGINT, signal_handler)
    
    # Create a PID file to indicate we're running
    pid_file = os.path.join(root_dir, "logs", "socket_proxy.pid")
    with open(pid_file, "w") as f:
        f.write(str(os.getpid()))
    
    logging.info("=== Socket Proxy Starting ===")
    logging.info(f"Process ID: {os.getpid()}")
    
    try:
        # Make sure the daemon is running
        if not ensure_daemon_running():
            logging.error("Failed to start daemon server")
            return 1
        
        # Connect to the daemon
        sock = connect_to_daemon()
        if not sock:
            logging.error("Failed to connect to daemon server")
            return 1
        
        # Set a timeout for the socket to prevent blocking indefinitely
        sock.settimeout(2.0)
        
        # Start forwarding threads
        stdin_thread = threading.Thread(target=forward_stdin_to_socket, args=(sock,))
        stdin_thread.daemon = True
        stdin_thread.start()
        
        socket_thread = threading.Thread(target=forward_socket_to_stdout, args=(sock,))
        socket_thread.daemon = True
        socket_thread.start()
        
        # Stay alive forever once initialized
        while not EXIT_FLAG:
            if not stdin_thread.is_alive() and not socket_thread.is_alive():
                if INITIALIZED:
                    logging.info("Both threads exited but we're initialized - restarting threads")
                    # Create a new socket and restart threads
                    new_sock = connect_to_daemon()
                    if new_sock:
                        sock = new_sock
                        stdin_thread = threading.Thread(target=forward_stdin_to_socket, args=(sock,))
                        stdin_thread.daemon = True
                        stdin_thread.start()
                        
                        socket_thread = threading.Thread(target=forward_socket_to_stdout, args=(sock,))
                        socket_thread.daemon = True
                        socket_thread.start()
                    else:
                        # If reconnection fails, sleep and retry
                        time.sleep(10)
                else:
                    logging.info("Both threads exited and not initialized - exiting")
                    break
            time.sleep(0.1)
    except KeyboardInterrupt:
        logging.info("Keyboard interrupt received")
    except Exception as e:
        logging.error(f"Unexpected error in main thread: {e}")
        traceback.print_exc(file=logging.FileHandler(log_file))
    finally:
        EXIT_FLAG = True
        logging.info("Shutting down...")
        try:
            # Only remove PID file if not initialized
            if not INITIALIZED and os.path.exists(pid_file):
                os.remove(pid_file)
        except:
            pass
    
    # If we're initialized, we'll stay alive forever
    if INITIALIZED:
        logging.info("Staying alive after initialization")
        # Reset EXIT_FLAG since we want to continue running
        EXIT_FLAG = False
        
        # Enter a loop that attempts to reconnect to the daemon periodically
        while True:
            try:
                if not os.path.exists(os.path.join(os.path.dirname(os.path.abspath(__file__)), "daemon_server.pid")):
                    logging.info("Daemon server PID file not found - attempting to restart daemon")
                    ensure_daemon_running()
                
                # Check if we need to reconnect and restart threads
                if not stdin_thread.is_alive() or not socket_thread.is_alive():
                    logging.info("One or more threads not running - attempting to restart")
                    new_sock = connect_to_daemon()
                    if new_sock:
                        sock = new_sock
                        
                        if not stdin_thread.is_alive():
                            stdin_thread = threading.Thread(target=forward_stdin_to_socket, args=(sock,))
                            stdin_thread.daemon = True
                            stdin_thread.start()
                        
                        if not socket_thread.is_alive():
                            socket_thread = threading.Thread(target=forward_socket_to_stdout, args=(sock,))
                            socket_thread.daemon = True
                            socket_thread.start()
                
                time.sleep(10)  # Check every 10 seconds
            except Exception as e:
                logging.error(f"Error in reconnection loop: {e}")
                time.sleep(30)  # Longer sleep on error
    
    return 0

if __name__ == "__main__":
    sys.exit(main()) 
```

--------------------------------------------------------------------------------
/log_manager.py:
--------------------------------------------------------------------------------

```python
#!/usr/bin/env python3
"""
Log Manager Utility for RhinoMcpServer

This script helps manage, view, and analyze logs from the MCP Server,
Rhino plugin, Claude AI, and diagnostic tools. It provides a unified
view of logs across all components to aid in debugging.
"""

import os
import sys
import re
import glob
import argparse
from datetime import datetime, timedelta
import json
import subprocess

# Define the log directory structure
LOG_ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "logs")
SERVER_LOGS = os.path.join(LOG_ROOT, "server")
PLUGIN_LOGS = os.path.join(LOG_ROOT, "plugin")
CLAUDE_LOGS = os.path.join(LOG_ROOT, "claude")
DIAGNOSTIC_LOGS = os.path.join(LOG_ROOT, "diagnostics")

# Log entry pattern for parsing - matches standard timestamp format
LOG_PATTERN = re.compile(r'^\[(?P<timestamp>.*?)\] \[(?P<level>.*?)\] \[(?P<component>.*?)\] (?P<message>.*)$')

class LogEntry:
    """Represents a parsed log entry with timestamp and metadata"""
    
    def __init__(self, timestamp, level, component, message, source_file):
        self.timestamp = timestamp
        self.level = level.upper()
        self.component = component
        self.message = message
        self.source_file = source_file
    
    @classmethod
    def from_line(cls, line, source_file):
        """Parse a log line into a LogEntry object"""
        match = LOG_PATTERN.match(line)
        if match:
            try:
                timestamp = datetime.strptime(match.group('timestamp'), '%Y-%m-%d %H:%M:%S,%f')
            except ValueError:
                try:
                    timestamp = datetime.strptime(match.group('timestamp'), '%Y-%m-%d %H:%M:%S')
                except ValueError:
                    # If timestamp can't be parsed, use current time
                    timestamp = datetime.now()
            
            return cls(
                timestamp, 
                match.group('level'), 
                match.group('component'), 
                match.group('message'),
                source_file
            )
        return None

    def __lt__(self, other):
        """Support sorting by timestamp"""
        return self.timestamp < other.timestamp
    
    def to_string(self, colors=True, show_source=False):
        """Format the log entry for display"""
        # Define ANSI color codes
        color_map = {
            "DEBUG": "\033[36m",  # Cyan
            "INFO": "\033[32m",   # Green
            "WARNING": "\033[33m", # Yellow
            "ERROR": "\033[31m",  # Red
            "CRITICAL": "\033[41m\033[97m"  # White on red background
        }
        component_colors = {
            "server": "\033[94m",  # Blue
            "plugin": "\033[95m",  # Magenta
            "diagnostic": "\033[96m",  # Cyan
            "claude": "\033[92m"   # Green
        }
        reset = "\033[0m"
        
        # Format timestamp
        timestamp_str = self.timestamp.strftime("%Y-%m-%d %H:%M:%S")
        
        # Apply colors if enabled
        if colors:
            level_color = color_map.get(self.level, "") 
            comp_color = component_colors.get(self.component, "")
            
            if self.level in ["ERROR", "CRITICAL"]:
                # For errors, color the whole line
                source_info = f" ({os.path.basename(self.source_file)})" if show_source else ""
                return f"{level_color}[{timestamp_str}] [{self.level}] [{self.component}] {self.message}{source_info}{reset}"
            else:
                # For non-errors, color just the level and component
                source_info = f" ({os.path.basename(self.source_file)})" if show_source else ""
                return f"[{timestamp_str}] [{level_color}{self.level}{reset}] [{comp_color}{self.component}{reset}] {self.message}{source_info}"
        else:
            source_info = f" ({os.path.basename(self.source_file)})" if show_source else ""
            return f"[{timestamp_str}] [{self.level}] [{self.component}] {self.message}{source_info}"

def collect_logs(since=None, level_filter=None, component_filter=None):
    """Collect and parse logs from all sources
    
    Args:
        since: Datetime object for filtering logs by age
        level_filter: List of log levels to include (DEBUG, INFO, etc)
        component_filter: List of components to include (server, plugin, etc)
    
    Returns:
        List of LogEntry objects sorted by timestamp
    """
    all_entries = []
    
    # Create directories if they don't exist
    for directory in [LOG_ROOT, SERVER_LOGS, PLUGIN_LOGS, CLAUDE_LOGS, DIAGNOSTIC_LOGS]:
        os.makedirs(directory, exist_ok=True)
    
    # Gather log files from all directories
    log_files = []
    log_files.extend(glob.glob(os.path.join(SERVER_LOGS, "*.log")))
    log_files.extend(glob.glob(os.path.join(PLUGIN_LOGS, "*.log")))
    log_files.extend(glob.glob(os.path.join(DIAGNOSTIC_LOGS, "*.log")))
    
    # Also check for Claude logs but handle them differently
    claude_files = glob.glob(os.path.join(CLAUDE_LOGS, "*.log"))
    
    # Process standard log files
    for log_file in log_files:
        try:
            with open(log_file, 'r') as f:
                for line in f:
                    line = line.strip()
                    if not line:
                        continue
                    
                    entry = LogEntry.from_line(line, log_file)
                    if entry:
                        # Apply filters
                        if since and entry.timestamp < since:
                            continue
                        if level_filter and entry.level not in level_filter:
                            continue
                        if component_filter and entry.component not in component_filter:
                            continue
                        
                        all_entries.append(entry)
        except Exception as e:
            print(f"Error processing {log_file}: {e}", file=sys.stderr)
    
    # Handle Claude logs which have a different format
    for claude_file in claude_files:
        try:
            # Get file creation time to use as timestamp
            file_time = datetime.fromtimestamp(os.path.getctime(claude_file))
            
            # Skip if it's before our filter time
            if since and file_time < since:
                continue
            
            # Only include preview of Claude logs since they can be large
            with open(claude_file, 'r') as f:
                content = f.read(500)  # Just read first 500 chars
                truncated = len(content) < os.path.getsize(claude_file)
                message = content + ("..." if truncated else "")
                
                # Create a synthetic log entry
                entry = LogEntry(
                    file_time,
                    "INFO", 
                    "claude", 
                    f"Claude interaction: {message}",
                    claude_file
                )
                
                # Apply filters
                if not component_filter or "claude" in component_filter:
                    all_entries.append(entry)
        except Exception as e:
            print(f"Error processing Claude log {claude_file}: {e}", file=sys.stderr)
    
    # Sort all entries by timestamp
    all_entries.sort()
    return all_entries

def display_logs(entries, colors=True, show_source=False, max_entries=None):
    """Display log entries with optional formatting
    
    Args:
        entries: List of LogEntry objects
        colors: Whether to use ANSI colors in output
        show_source: Whether to show source filename
        max_entries: Maximum number of entries to show (None for all)
    """
    # Maybe limit entries
    if max_entries is not None and len(entries) > max_entries:
        skipped = len(entries) - max_entries
        entries = entries[-max_entries:]
        print(f"... (skipped {skipped} earlier entries) ...\n")
    
    for entry in entries:
        print(entry.to_string(colors=colors, show_source=show_source))
        
def extract_error_context(entries, context_lines=5):
    """Extract log entries around errors with context
    
    Args:
        entries: List of all LogEntry objects
        context_lines: Number of log lines before and after each error
        
    Returns:
        List of error contexts (each being a list of LogEntry objects)
    """
    error_contexts = []
    error_indices = [i for i, entry in enumerate(entries) if entry.level in ["ERROR", "CRITICAL"]]
    
    for error_idx in error_indices:
        # Get context before and after error
        start_idx = max(0, error_idx - context_lines)
        end_idx = min(len(entries), error_idx + context_lines + 1)
        
        # Extract the context
        context = entries[start_idx:end_idx]
        error_contexts.append(context)
    
    return error_contexts

def generate_error_report(entries):
    """Generate a summary report of errors
    
    Args:
        entries: List of LogEntry objects
        
    Returns:
        String containing the error report
    """
    error_entries = [e for e in entries if e.level in ["ERROR", "CRITICAL"]]
    
    if not error_entries:
        return "No errors found in logs."
    
    # Group errors by component
    errors_by_component = {}
    for entry in error_entries:
        if entry.component not in errors_by_component:
            errors_by_component[entry.component] = []
        errors_by_component[entry.component].append(entry)
    
    # Generate the report
    report = f"Error Report ({len(error_entries)} errors found)\n"
    report += "=" * 50 + "\n\n"
    
    for component, errors in errors_by_component.items():
        report += f"{component.upper()} Errors: {len(errors)}\n"
        report += "-" * 30 + "\n"
        
        # Group by error message pattern
        error_patterns = {}
        for error in errors:
            # Simplify message by removing variable parts (numbers, IDs, etc)
            simplified = re.sub(r'\b(?:\w+[-_])?[0-9a-f]{8}(?:[-_]\w+)?\b', 'ID', error.message)
            simplified = re.sub(r'\d+', 'N', simplified)
            
            if simplified not in error_patterns:
                error_patterns[simplified] = []
            error_patterns[simplified].append(error)
        
        # Report each error pattern
        for pattern, pattern_errors in error_patterns.items():
            report += f"\n• {pattern} ({len(pattern_errors)} occurrences)\n"
            report += f"  First seen: {pattern_errors[0].timestamp}\n"
            report += f"  Last seen: {pattern_errors[-1].timestamp}\n"
            report += f"  Example: {pattern_errors[-1].message}\n"
        
        report += "\n" + "=" * 50 + "\n\n"
    
    return report

def clear_logs(days_old=None, component=None, confirm=True):
    """Clear logs based on specified criteria
    
    Args:
        days_old: Delete logs older than this many days
        component: Only delete logs from this component
        confirm: Whether to prompt for confirmation
    """
    # Determine which directories to clean
    dirs_to_clean = []
    if component == "server" or component is None:
        dirs_to_clean.append(SERVER_LOGS)
    if component == "plugin" or component is None:
        dirs_to_clean.append(PLUGIN_LOGS)
    if component == "claude" or component is None:
        dirs_to_clean.append(CLAUDE_LOGS)
    if component == "diagnostic" or component is None:
        dirs_to_clean.append(DIAGNOSTIC_LOGS)
    
    # Collect files to delete
    files_to_delete = []
    for directory in dirs_to_clean:
        for log_file in glob.glob(os.path.join(directory, "*.log")):
            if days_old is not None:
                # Check file age
                file_time = datetime.fromtimestamp(os.path.getmtime(log_file))
                cutoff_time = datetime.now() - timedelta(days=days_old)
                if file_time >= cutoff_time:
                    continue  # Skip files newer than cutoff
            
            files_to_delete.append(log_file)
    
    # Nothing to delete
    if not files_to_delete:
        print("No logs found matching the specified criteria.")
        return
    
    # Confirm deletion
    if confirm:
        print(f"Will delete {len(files_to_delete)} log files:")
        for f in files_to_delete[:5]:
            print(f"  - {os.path.basename(f)}")
        
        if len(files_to_delete) > 5:
            print(f"  - ... and {len(files_to_delete) - 5} more")
        
        confirmation = input("Proceed with deletion? (y/N): ").lower()
        if confirmation != 'y':
            print("Deletion cancelled.")
            return
    
    # Delete the files
    deleted_count = 0
    for log_file in files_to_delete:
        try:
            os.remove(log_file)
            deleted_count += 1
        except Exception as e:
            print(f"Error deleting {log_file}: {e}")
    
    print(f"Successfully deleted {deleted_count} log files.")

def monitor_logs(interval=1.0, colors=True, level_filter=None, component_filter=None):
    """Monitor logs in real-time like 'tail -f'
    
    Args:
        interval: Polling interval in seconds
        colors: Whether to use ANSI colors
        level_filter: Optional list of log levels to show
        component_filter: Optional list of components to show
    """
    print(f"Monitoring logs (Ctrl+C to exit)...")
    print(f"Filters: levels={level_filter or 'all'}, components={component_filter or 'all'}")
    
    # Get initial log entries and remember the latest timestamp
    entries = collect_logs(level_filter=level_filter, component_filter=component_filter)
    last_timestamp = entries[-1].timestamp if entries else datetime.now()
    
    try:
        while True:
            # Wait for the specified interval
            sys.stdout.flush()
            subprocess.call("", shell=True)  # Hack to make ANSI colors work in Windows
            
            # Get new entries since the last check
            new_entries = collect_logs(
                since=last_timestamp, 
                level_filter=level_filter,
                component_filter=component_filter
            )
            
            # Update timestamp for the next iteration
            if new_entries:
                last_timestamp = new_entries[-1].timestamp
                
                # Display new entries
                for entry in new_entries:
                    print(entry.to_string(colors=colors, show_source=True))
            
            # Sleep before checking again
            import time
            time.sleep(interval)
    
    except KeyboardInterrupt:
        print("\nStopped monitoring logs.")

def main():
    """Parse arguments and execute requested command"""
    parser = argparse.ArgumentParser(description="Manage and view RhinoMcpServer logs")
    
    subparsers = parser.add_subparsers(dest="command", help="Command to execute")
    
    # view command
    view_parser = subparsers.add_parser("view", help="View logs")
    view_parser.add_argument("--since", type=str, help="Show logs since (e.g. '1h', '2d', '30m')")
    view_parser.add_argument("--level", type=str, help="Filter by log level (comma-separated: DEBUG,INFO,WARNING,ERROR)")
    view_parser.add_argument("--component", type=str, help="Filter by component (comma-separated: server,plugin,claude,diagnostic)")
    view_parser.add_argument("--no-color", action="store_true", help="Disable colored output")
    view_parser.add_argument("--source", action="store_true", help="Show source log file")
    view_parser.add_argument("--max", type=int, help="Maximum number of entries to display")
    
    # errors command
    errors_parser = subparsers.add_parser("errors", help="View errors with context")
    errors_parser.add_argument("--since", type=str, help="Show errors since (e.g. '1h', '2d', '30m')")
    errors_parser.add_argument("--component", type=str, help="Filter by component")
    errors_parser.add_argument("--context", type=int, default=5, help="Number of context lines before/after each error")
    errors_parser.add_argument("--no-color", action="store_true", help="Disable colored output")
    
    # report command
    report_parser = subparsers.add_parser("report", help="Generate error report")
    report_parser.add_argument("--since", type=str, help="Include errors since (e.g. '1h', '2d', '30m')")
    report_parser.add_argument("--output", type=str, help="Output file for the report")
    
    # clear command
    clear_parser = subparsers.add_parser("clear", help="Clear logs")
    clear_parser.add_argument("--older-than", type=int, help="Delete logs older than N days")
    clear_parser.add_argument("--component", type=str, help="Only clear logs for the specified component")
    clear_parser.add_argument("--force", action="store_true", help="Do not ask for confirmation")
    
    # monitor command
    monitor_parser = subparsers.add_parser("monitor", help="Monitor logs in real-time")
    monitor_parser.add_argument("--interval", type=float, default=1.0, help="Polling interval in seconds")
    monitor_parser.add_argument("--level", type=str, help="Filter by log level (comma-separated)")
    monitor_parser.add_argument("--component", type=str, help="Filter by component (comma-separated)")
    monitor_parser.add_argument("--no-color", action="store_true", help="Disable colored output")
    
    # info command
    info_parser = subparsers.add_parser("info", help="Show information about logs")
    
    args = parser.parse_args()
    
    # Handle time expressions like "1h", "2d", etc.
    since_time = None
    if hasattr(args, 'since') and args.since:
        try:
            # Parse time expressions
            value = int(args.since[:-1])
            unit = args.since[-1].lower()
            
            if unit == 'h':
                since_time = datetime.now() - timedelta(hours=value)
            elif unit == 'm':
                since_time = datetime.now() - timedelta(minutes=value)
            elif unit == 'd':
                since_time = datetime.now() - timedelta(days=value)
            else:
                print(f"Invalid time unit in '{args.since}'. Use 'm' for minutes, 'h' for hours, 'd' for days.")
                return 1
        except ValueError:
            print(f"Invalid time format: '{args.since}'. Use e.g. '1h', '30m', '2d'")
            return 1
    
    # Parse level and component filters
    level_filter = None
    if hasattr(args, 'level') and args.level:
        level_filter = [l.strip().upper() for l in args.level.split(',')]
    
    component_filter = None
    if hasattr(args, 'component') and args.component:
        component_filter = [c.strip().lower() for c in args.component.split(',')]
    
    # Execute the requested command
    if args.command == "view":
        entries = collect_logs(since=since_time, level_filter=level_filter, component_filter=component_filter)
        if not entries:
            print("No log entries found matching the criteria.")
            return 0
        
        display_logs(
            entries, 
            colors=not args.no_color, 
            show_source=args.source, 
            max_entries=args.max
        )
    
    elif args.command == "errors":
        # Collect all entries first
        all_entries = collect_logs(since=since_time, component_filter=component_filter)
        if not all_entries:
            print("No log entries found.")
            return 0
        
        # Extract errors with context
        error_contexts = extract_error_context(all_entries, context_lines=args.context)
        if not error_contexts:
            print("No errors found in the logs.")
            return 0
        
        # Display each error with its context
        for i, context in enumerate(error_contexts):
            if i > 0:
                print("\n" + "-" * 80 + "\n")
            
            print(f"Error {i+1} of {len(error_contexts)}:")
            for entry in context:
                print(entry.to_string(colors=not args.no_color, show_source=True))
    
    elif args.command == "report":
        entries = collect_logs(since=since_time)
        if not entries:
            print("No log entries found.")
            return 0
        
        report = generate_error_report(entries)
        
        if args.output:
            with open(args.output, 'w') as f:
                f.write(report)
            print(f"Error report saved to {args.output}")
        else:
            print(report)
    
    elif args.command == "clear":
        clear_logs(
            days_old=args.older_than,
            component=args.component,
            confirm=not args.force
        )
    
    elif args.command == "monitor":
        monitor_logs(
            interval=args.interval,
            colors=not args.no_color,
            level_filter=level_filter,
            component_filter=component_filter
        )
    
    elif args.command == "info":
        # Show information about available logs
        print("Log Directory Structure:")
        print(f"  Root: {LOG_ROOT}")
        
        # Helper function to summarize logs in a directory
        def summarize_dir(dir_path, name):
            if not os.path.exists(dir_path):
                return f"{name}: Directory not found"
            
            log_files = glob.glob(os.path.join(dir_path, "*.log"))
            if not log_files:
                return f"{name}: No log files found"
            
            newest = max(log_files, key=os.path.getmtime)
            oldest = min(log_files, key=os.path.getmtime)
            newest_time = datetime.fromtimestamp(os.path.getmtime(newest))
            oldest_time = datetime.fromtimestamp(os.path.getmtime(oldest))
            
            total_size = sum(os.path.getsize(f) for f in log_files)
            size_mb = total_size / (1024 * 1024)
            
            return (f"{name}: {len(log_files)} files, {size_mb:.2f} MB total\n"
                    f"  Newest: {os.path.basename(newest)} ({newest_time})\n"
                    f"  Oldest: {os.path.basename(oldest)} ({oldest_time})")
        
        print("\nLog Summaries:")
        print(summarize_dir(SERVER_LOGS, "Server Logs"))
        print(summarize_dir(PLUGIN_LOGS, "Plugin Logs"))
        print(summarize_dir(CLAUDE_LOGS, "Claude Logs"))
        print(summarize_dir(DIAGNOSTIC_LOGS, "Diagnostic Logs"))
    
    else:
        parser.print_help()
    
    return 0

if __name__ == "__main__":
    sys.exit(main()) 
```

--------------------------------------------------------------------------------
/combined_mcp_server.py:
--------------------------------------------------------------------------------

```python
#!/usr/bin/env python3
"""
Combined MCP Server for Rhino
This script implements a direct MCP server using the FastMCP pattern,
following the Model Context Protocol specification.
"""

import json
import os
import sys
import time
import logging
import signal
import threading
import traceback
from datetime import datetime
import re
import uuid
import asyncio
import socket
from typing import Dict, Any, List, Optional, Tuple
from contextlib import asynccontextmanager
from typing import AsyncIterator

# Import the FastMCP class
from mcp.server.fastmcp import FastMCP, Context

# Configure logging - improved with structured format and unified location
log_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "logs")
os.makedirs(log_dir, exist_ok=True)

# Create log subdirectories
server_log_dir = os.path.join(log_dir, "server")
plugin_log_dir = os.path.join(log_dir, "plugin")
claude_log_dir = os.path.join(log_dir, "claude")

for directory in [server_log_dir, plugin_log_dir, claude_log_dir]:
    os.makedirs(directory, exist_ok=True)

# Log filenames based on date for easier archiving
today = datetime.now().strftime("%Y-%m-%d")
server_log_file = os.path.join(server_log_dir, f"server_{today}.log")
debug_log_file = os.path.join(server_log_dir, f"debug_{today}.log")

# Set up the logger with custom format including timestamp, level, component, and message
logging.basicConfig(
    level=logging.INFO,
    format='[%(asctime)s] [%(levelname)s] [%(component)s] %(message)s',
    handlers=[
        logging.StreamHandler(sys.stderr),
        logging.FileHandler(server_log_file)
    ]
)

# Add a filter to add the component field
class ComponentFilter(logging.Filter):
    def __init__(self, component="server"):
        super().__init__()
        self.component = component

    def filter(self, record):
        record.component = self.component
        return True

# Get logger and add the component filter
logger = logging.getLogger()
logger.addFilter(ComponentFilter())

# Add a debug file handler for detailed debugging
debug_handler = logging.FileHandler(debug_log_file)
debug_handler.setLevel(logging.DEBUG)
debug_handler.setFormatter(logging.Formatter('[%(asctime)s] [%(levelname)s] [%(component)s] %(message)s'))
logger.addHandler(debug_handler)

# Log basic server startup information
logger.info(f"RhinoMCP server starting in {os.getcwd()}")
logger.info(f"Log files directory: {log_dir}")
logger.info(f"Server log file: {server_log_file}")
logger.info(f"Debug log file: {debug_log_file}")

# Create a PID file to track this process
pid_file = os.path.join(log_dir, "combined_server.pid")
with open(pid_file, "w") as f:
    f.write(str(os.getpid()))
logger.info(f"Server PID: {os.getpid()}")

# Global Rhino connection
_rhino_connection = None

class RhinoConnection:
    """Class to manage socket connection to Rhino plugin"""
    
    def __init__(self, host: str = "localhost", port: int = 9876):
        self.host = host
        self.port = port
        self.sock = None
        self.request_id = 0
    
    def connect(self) -> bool:
        """Connect to the Rhino plugin socket server"""
        if self.sock:
            return True
            
        try:
            self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            self.sock.connect((self.host, self.port))
            logger.info(f"Connected to Rhino at {self.host}:{self.port}")
            return True
        except Exception as e:
            logger.error(f"Failed to connect to Rhino: {str(e)}")
            logger.debug(f"Connection error details: {traceback.format_exc()}")
            self.sock = None
            return False
    
    def disconnect(self):
        """Disconnect from the Rhino plugin"""
        if self.sock:
            try:
                self.sock.close()
            except Exception as e:
                logger.error(f"Error disconnecting from Rhino: {str(e)}")
            finally:
                self.sock = None
    
    def send_command(self, command_type: str, params: Dict[str, Any] = None) -> Dict[str, Any]:
        """Send a command to Rhino and return the response"""
        if not self.sock and not self.connect():
            raise ConnectionError("Not connected to Rhino")
        
        # Increment request ID for tracking
        self.request_id += 1
        current_request_id = self.request_id
        
        command = {
            "id": current_request_id,
            "type": command_type,
            "params": params or {}
        }
        
        try:
            # Log the command being sent
            logger.info(f"Request #{current_request_id}: Sending command '{command_type}' to Rhino")
            logger.debug(f"Request #{current_request_id} Parameters: {json.dumps(params or {})}")
            
            # Send the command
            command_json = json.dumps(command)
            logger.debug(f"Request #{current_request_id} Raw command: {command_json}")
            self.sock.sendall(command_json.encode('utf-8'))
            
            # Set a timeout for receiving
            self.sock.settimeout(10.0)
            
            # Receive the response
            buffer_size = 4096
            response_data = b""
            
            while True:
                chunk = self.sock.recv(buffer_size)
                if not chunk:
                    break
                response_data += chunk
                
                # Try to parse as JSON to see if we have a complete response
                try:
                    json.loads(response_data.decode('utf-8'))
                    # If parsing succeeds, we have a complete response
                    break
                except json.JSONDecodeError:
                    # Not a complete JSON yet, continue receiving
                    continue
            
            if not response_data:
                logger.error(f"Request #{current_request_id}: No data received from Rhino")
                raise ConnectionError(f"Request #{current_request_id}: No data received from Rhino")
            
            # Log the raw response for debugging
            raw_response = response_data.decode('utf-8')
            logger.debug(f"Request #{current_request_id} Raw response: {raw_response}")
            
            response = json.loads(raw_response)
            
            # Check if the response indicates an error
            if "error" in response:
                error_msg = response.get("error", "Unknown error from Rhino")
                logger.error(f"Request #{current_request_id}: Rhino reported error: {error_msg}")
                raise Exception(f"Request #{current_request_id}: Rhino error: {error_msg}")
            
            if response.get("status") == "error":
                error_msg = response.get("message", "Unknown error from Rhino")
                logger.error(f"Request #{current_request_id}: Rhino reported error status: {error_msg}")
                raise Exception(f"Request #{current_request_id}: Rhino error: {error_msg}")
            
            # Log success
            logger.info(f"Request #{current_request_id}: Command '{command_type}' executed successfully")
            
            # If we get here, assume success and return the result
            if "result" in response:
                return response.get("result", {})
            else:
                # If there's no result field but no error either, return the whole response
                return response
        except socket.timeout:
            logger.error(f"Request #{current_request_id}: Socket timeout while waiting for response from Rhino")
            logger.debug(f"Request #{current_request_id}: Timeout after 10 seconds waiting for response to '{command_type}'")
            self.sock = None
            raise Exception(f"Request #{current_request_id}: Timeout waiting for Rhino response")
        except (ConnectionError, BrokenPipeError, ConnectionResetError) as e:
            logger.error(f"Request #{current_request_id}: Socket connection error: {str(e)}")
            self.sock = None
            raise Exception(f"Request #{current_request_id}: Connection to Rhino lost: {str(e)}")
        except json.JSONDecodeError as e:
            logger.error(f"Request #{current_request_id}: Invalid JSON response: {str(e)}")
            if 'response_data' in locals():
                logger.error(f"Request #{current_request_id}: Raw response causing JSON error: {response_data[:200]}")
            self.sock = None
            raise Exception(f"Request #{current_request_id}: Invalid JSON response from Rhino: {str(e)}")
        except Exception as e:
            logger.error(f"Request #{current_request_id}: Error communicating with Rhino: {str(e)}")
            logger.error(f"Request #{current_request_id}: Traceback: {traceback.format_exc()}")
            self.sock = None
            raise Exception(f"Request #{current_request_id}: Communication error with Rhino: {str(e)}")

def get_rhino_connection() -> RhinoConnection:
    """Get or create a connection to Rhino"""
    global _rhino_connection
    
    # If we have an existing connection, check if it's still valid
    if _rhino_connection is not None:
        try:
            # Don't use ping as it's not implemented in the Rhino plugin
            # Instead, try get_scene_info which is more likely to work
            _rhino_connection.send_command("get_scene_info", {})
            return _rhino_connection
        except Exception as e:
            # Connection is dead, close it and create a new one
            logger.warning(f"Existing connection is no longer valid: {str(e)}")
            try:
                _rhino_connection.disconnect()
            except:
                pass
            _rhino_connection = None
    
    # Create a new connection if needed
    if _rhino_connection is None:
        _rhino_connection = RhinoConnection()
        if not _rhino_connection.connect():
            logger.error("Failed to connect to Rhino")
            _rhino_connection = None
            raise Exception("Could not connect to Rhino. Make sure the Rhino plugin is running.")
        
        # Verify connection with a known working command
        try:
            # Test the connection with get_scene_info command
            result = _rhino_connection.send_command("get_scene_info", {})
            logger.info(f"Connection test successful: {result}")
        except Exception as e:
            logger.error(f"Connection test failed: {str(e)}")
            _rhino_connection.disconnect()
            _rhino_connection = None
            raise Exception(f"Rhino plugin connection test failed: {str(e)}")
            
        logger.info("Created new connection to Rhino")
    
    return _rhino_connection

# Server lifecycle management
@asynccontextmanager
async def server_lifespan(server: FastMCP) -> AsyncIterator[Dict[str, Any]]:
    """Manage server startup and shutdown lifecycle"""
    try:
        logger.info("RhinoMCP server starting up")
        
        # Try to connect to Rhino on startup to verify it's available
        try:
            # This will initialize the global connection if needed
            rhino = get_rhino_connection()
            logger.info("Successfully connected to Rhino on startup")
        except Exception as e:
            logger.warning(f"Could not connect to Rhino on startup: {str(e)}")
            logger.warning("Make sure the Rhino plugin is running before using Rhino resources or tools")
        
        yield {}  # No context resources needed for now
    finally:
        logger.info("RhinoMCP server shutting down")
        # Cleanup code
        global _rhino_connection
        if _rhino_connection:
            logger.info("Disconnecting from Rhino on shutdown")
            _rhino_connection.disconnect()
            _rhino_connection = None
            
        if os.path.exists(pid_file):
            os.remove(pid_file)
            logger.info("Removed PID file")

# Initialize the FastMCP server
mcp = FastMCP(
    "RhinoMcpServer",
    description="A Model Context Protocol server for Rhino 3D",
    lifespan=server_lifespan
)

# Tool implementations using FastMCP decorators

@mcp.tool()
def create_sphere(
    ctx: Context,
    centerX: float,
    centerY: float,
    centerZ: float,
    radius: float,
    color: Optional[str] = None
) -> str:
    """
    Creates a sphere with the specified center and radius.
    
    Parameters:
    - centerX: X coordinate of the sphere center
    - centerY: Y coordinate of the sphere center
    - centerZ: Z coordinate of the sphere center
    - radius: Radius of the sphere
    - color: Optional color for the sphere (e.g., 'red', 'blue', etc.)
    
    Returns:
    A message indicating the created sphere details
    """
    tool_id = str(uuid.uuid4())[:8]  # Generate a short ID for tracking this tool call
    logger.info(f"[{tool_id}] Tool call: create_sphere with center=({centerX},{centerY},{centerZ}), radius={radius}, color={color}")
    
    try:
        # Get the Rhino connection
        rhino = get_rhino_connection()
        
        # Send the command to Rhino
        params = {
            "centerX": centerX,
            "centerY": centerY,
            "centerZ": centerZ,
            "radius": radius
        }
        
        if color:
            params["color"] = color
            
        result = rhino.send_command("create_sphere", params)
        
        # Log success
        logger.info(f"[{tool_id}] Sphere created successfully")
        
        # Return the result
        return json.dumps(result)
    except Exception as e:
        logger.error(f"[{tool_id}] Error creating sphere: {str(e)}")
        logger.error(f"[{tool_id}] Traceback: {traceback.format_exc()}")
        return json.dumps({
            "success": False,
            "error": f"Error creating sphere: {str(e)}"
        })

@mcp.tool()
def create_box(
    ctx: Context,
    cornerX: float,
    cornerY: float,
    cornerZ: float,
    width: float,
    depth: float,
    height: float,
    color: Optional[str] = None
) -> str:
    """
    Creates a box with the specified dimensions.
    
    Parameters:
    - cornerX: X coordinate of the box corner
    - cornerY: Y coordinate of the box corner
    - cornerZ: Z coordinate of the box corner
    - width: Width of the box (X dimension)
    - depth: Depth of the box (Y dimension)
    - height: Height of the box (Z dimension)
    - color: Optional color for the box (e.g., 'red', 'blue', etc.)
    
    Returns:
    A message indicating the created box details
    """
    tool_id = str(uuid.uuid4())[:8]  # Generate a short ID for tracking this tool call
    logger.info(f"[{tool_id}] Tool call: create_box at ({cornerX},{cornerY},{cornerZ}), size={width}x{depth}x{height}, color={color}")
    
    try:
        # Get the Rhino connection
        rhino = get_rhino_connection()
        
        # Send the command to Rhino
        params = {
            "cornerX": cornerX,
            "cornerY": cornerY,
            "cornerZ": cornerZ,
            "width": width,
            "depth": depth,
            "height": height
        }
        
        if color:
            params["color"] = color
            
        result = rhino.send_command("create_box", params)
        
        # Log success
        logger.info(f"[{tool_id}] Box created successfully")
        
        # Return the result
        return json.dumps(result)
    except Exception as e:
        logger.error(f"[{tool_id}] Error creating box: {str(e)}")
        logger.error(f"[{tool_id}] Traceback: {traceback.format_exc()}")
        return json.dumps({
            "success": False,
            "error": f"Error creating box: {str(e)}",
            "toolId": tool_id  # Include the tool ID for error tracking
        })

@mcp.tool()
def create_cylinder(
    ctx: Context,
    baseX: float,
    baseY: float,
    baseZ: float,
    height: float,
    radius: float,
    color: Optional[str] = None
) -> str:
    """
    Creates a cylinder with the specified base point, height, and radius.
    
    Parameters:
    - baseX: X coordinate of the cylinder base point
    - baseY: Y coordinate of the cylinder base point
    - baseZ: Z coordinate of the cylinder base point
    - height: Height of the cylinder
    - radius: Radius of the cylinder
    - color: Optional color for the cylinder (e.g., 'red', 'blue', etc.)
    
    Returns:
    A message indicating the created cylinder details
    """
    tool_id = str(uuid.uuid4())[:8]  # Generate a short ID for tracking this tool call
    logger.info(f"[{tool_id}] Tool call: create_cylinder at ({baseX},{baseY},{baseZ}), height={height}, radius={radius}, color={color}")
    
    try:
        # Get the Rhino connection
        rhino = get_rhino_connection()
        
        # Send the command to Rhino
        params = {
            "baseX": baseX,
            "baseY": baseY,
            "baseZ": baseZ,
            "height": height,
            "radius": radius
        }
        
        if color:
            params["color"] = color
            
        result = rhino.send_command("create_cylinder", params)
        
        # Log success
        logger.info(f"[{tool_id}] Cylinder created successfully")
        
        # Return the result
        return json.dumps(result)
    except Exception as e:
        logger.error(f"[{tool_id}] Error creating cylinder: {str(e)}")
        logger.error(f"[{tool_id}] Traceback: {traceback.format_exc()}")
        return json.dumps({
            "success": False,
            "error": f"Error creating cylinder: {str(e)}",
            "toolId": tool_id  # Include the tool ID for error tracking
        })

@mcp.tool()
def get_scene_info(ctx: Context) -> str:
    """
    Gets information about objects in the current scene.
    
    Returns:
    A JSON string containing scene information
    """
    tool_id = str(uuid.uuid4())[:8]  # Generate a short ID for tracking this tool call
    logger.info(f"[{tool_id}] Tool call: get_scene_info")
    
    try:
        # Get the Rhino connection
        rhino = get_rhino_connection()
        
        # Send the command to Rhino
        result = rhino.send_command("get_scene_info", {})
        
        # Log success
        logger.info(f"[{tool_id}] Scene info retrieved successfully")
        
        # Return the result
        return json.dumps(result)
    except Exception as e:
        logger.error(f"[{tool_id}] Error getting scene info: {str(e)}")
        logger.error(f"[{tool_id}] Traceback: {traceback.format_exc()}")
        return json.dumps({
            "success": False,
            "error": f"Error getting scene info: {str(e)}",
            "toolId": tool_id  # Include the tool ID for error tracking
        })

@mcp.tool()
def clear_scene(ctx: Context, currentLayerOnly: bool = False) -> str:
    """
    Clears all objects from the current scene.
    
    Parameters:
    - currentLayerOnly: If true, only delete objects on the current layer
    
    Returns:
    A message indicating the operation result
    """
    tool_id = str(uuid.uuid4())[:8]  # Generate a short ID for tracking this tool call
    layer_info = "current layer only" if currentLayerOnly else "all layers"
    logger.info(f"[{tool_id}] Tool call: clear_scene ({layer_info})")
    
    try:
        # Get the Rhino connection
        rhino = get_rhino_connection()
        
        # Send the command to Rhino
        params = {
            "currentLayerOnly": currentLayerOnly
        }
        
        result = rhino.send_command("clear_scene", params)
        
        # Log success
        logger.info(f"[{tool_id}] Scene cleared successfully ({layer_info})")
        
        # Return the result
        return json.dumps(result)
    except Exception as e:
        logger.error(f"[{tool_id}] Error clearing scene: {str(e)}")
        logger.error(f"[{tool_id}] Traceback: {traceback.format_exc()}")
        return json.dumps({
            "success": False,
            "error": f"Error clearing scene: {str(e)}",
            "toolId": tool_id  # Include the tool ID for error tracking
        })

@mcp.tool()
def create_layer(ctx: Context, name: str, color: Optional[str] = None) -> str:
    """
    Creates a new layer in the Rhino document.
    
    Parameters:
    - name: Name of the new layer
    - color: Optional color for the layer (e.g., 'red', 'blue', etc.)
    
    Returns:
    A message indicating the operation result
    """
    tool_id = str(uuid.uuid4())[:8]  # Generate a short ID for tracking this tool call
    color_info = f" with color {color}" if color else ""
    logger.info(f"[{tool_id}] Tool call: create_layer '{name}'{color_info}")
    
    try:
        # Get the Rhino connection
        rhino = get_rhino_connection()
        
        # Send the command to Rhino
        params = {
            "name": name
        }
        
        if color:
            params["color"] = color
            
        result = rhino.send_command("create_layer", params)
        
        # Log success
        logger.info(f"[{tool_id}] Layer '{name}' created successfully")
        
        # Return the result
        return json.dumps(result)
    except Exception as e:
        logger.error(f"[{tool_id}] Error creating layer: {str(e)}")
        logger.error(f"[{tool_id}] Traceback: {traceback.format_exc()}")
        return json.dumps({
            "success": False,
            "error": f"Error creating layer: {str(e)}",
            "toolId": tool_id  # Include the tool ID for error tracking
        })

# Record Claude interactions to debug context issues
@mcp.tool()
def log_claude_message(
    ctx: Context,
    message: str,
    type: str = "info"
) -> str:
    """
    Log a message from Claude for debugging purposes.
    
    Parameters:
    - message: The message to log
    - type: The type of message (info, error, warning, debug)
    
    Returns:
    Success confirmation
    """
    log_id = str(uuid.uuid4())[:8]
    
    # Create a timestamp for the filename
    timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
    claude_log_file = os.path.join(claude_log_dir, f"claude_{timestamp}_{log_id}.log")
    
    try:
        with open(claude_log_file, "w") as f:
            f.write(message)
        
        # Log to server log as well
        if type == "error":
            logger.error(f"[Claude] [{log_id}] {message[:100]}...")
        elif type == "warning":
            logger.warning(f"[Claude] [{log_id}] {message[:100]}...")
        elif type == "debug":
            logger.debug(f"[Claude] [{log_id}] {message[:100]}...")
        else:
            logger.info(f"[Claude] [{log_id}] {message[:100]}...")
        
        return json.dumps({
            "success": True,
            "logId": log_id,
            "logFile": claude_log_file
        })
    except Exception as e:
        logger.error(f"Error logging Claude message: {str(e)}")
        return json.dumps({
            "success": False,
            "error": f"Error logging Claude message: {str(e)}"
        })

def main():
    """Main function to run the MCP server"""
    logger.info("=== RhinoMCP Server Starting ===")
    logger.info(f"Process ID: {os.getpid()}")
    
    try:
        # Run the FastMCP server
        mcp.run()
    except KeyboardInterrupt:
        logger.info("Keyboard interrupt received")
    except Exception as e:
        logger.error(f"Error running server: {str(e)}")
        logger.error(f"Traceback: {traceback.format_exc()}")
    finally:
        logger.info("Server shutting down...")
        
        # Clean up connection
        global _rhino_connection
        if _rhino_connection:
            _rhino_connection.disconnect()
        
        if os.path.exists(pid_file):
            os.remove(pid_file)
            logger.info("Removed PID file")
    
    return 0

if __name__ == "__main__":
    sys.exit(main()) 
```
Page 1/3FirstPrevNextLast