#
tokens: 29165/50000 23/23 files
lines: off (toggle) GitHub
raw markdown copy
# Directory Structure

```
├── .gitignore
├── .local-index
│   └── sketchup_mcp-0.1.0-py3-none-any.whl
├── .python-version
├── examples
│   ├── arts_and_crafts_cabinet.py
│   ├── behavior_tester.py
│   ├── README.md
│   ├── ruby_tester.py
│   ├── simple_ruby_eval.py
│   └── simple_test.py
├── plan.md
├── pyproject.toml
├── README.md
├── requirements.txt
├── sketchup.json
├── src
│   └── sketchup_mcp
│       ├── __init__.py
│       ├── __main__.py
│       └── server.py
├── su_mcp
│   ├── extension.json
│   ├── package.rb
│   ├── su_mcp
│   │   └── main.rb
│   └── su_mcp.rb
├── su_mcp.rb
├── test_eval_ruby.py
└── update_and_restart.sh
```

# Files

--------------------------------------------------------------------------------
/.python-version:
--------------------------------------------------------------------------------

```
3.10 
```

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

```
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
dist/
*.egg-info/
.env
.venv
env/
venv/
ENV/

# Ruby
*.gem
*.rbc
/.config
/coverage/
/pkg/
/tmp/
.ruby-version
.ruby-gemset
.rvmrc

# Sketchup specific
*.rbz

# OS specific
.DS_Store
.DS_Store?
._*
Thumbs.db
*.tar.gz

```

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

```markdown
# SketchUp MCP Examples

This directory contains example scripts demonstrating how to use the SketchUp MCP (Model Context Protocol) integration.

## Ruby Code Evaluation

The SketchUp MCP now supports evaluating arbitrary Ruby code directly in SketchUp. This powerful feature allows you to create complex models and perform advanced operations that might not be directly exposed through the MCP API.

### Requirements

- SketchUp with the MCP extension installed (version 1.6.0 or later)
- Python 3.10 or later
- sketchup-mcp Python package (version 0.1.17 or later)

### Examples

#### Simple Ruby Eval Example

The `simple_ruby_eval.py` script demonstrates basic usage of the `eval_ruby` feature with several simple examples:

- Creating a line
- Creating a cube
- Getting model information

To run the example:

```bash
python examples/simple_ruby_eval.py
```

#### Arts and Crafts Cabinet Example

The `arts_and_crafts_cabinet.py` script demonstrates a more complex example, creating a detailed arts and crafts style cabinet with working doors using Ruby code.

To run the example:

```bash
python examples/arts_and_crafts_cabinet.py
```

### Using the eval_ruby Feature in Your Own Code

To use the `eval_ruby` feature in your own code:

```python
from mcp.client import Client
import json

# Connect to the SketchUp MCP server
client = Client("sketchup")

# Define your Ruby code
ruby_code = """
    model = Sketchup.active_model
    entities = model.active_entities
    line = entities.add_line([0,0,0], [100,100,100])
    line.entityID
"""

# Evaluate the Ruby code
response = client.eval_ruby(code=ruby_code)

# Parse the response
result = json.loads(response)
if result.get("success"):
    print(f"Success! Result: {result.get('result')}")
else:
    print(f"Error: {result.get('error')}")
```

### Tips for Using eval_ruby

1. **Return Values**: The last expression in your Ruby code will be returned as the result. Make sure to return something meaningful, like an entity ID or a JSON string.

2. **Error Handling**: Ruby errors will be caught and returned in the response. Check the `success` field to determine if the code executed successfully.

3. **Model Operations**: For operations that modify the model, consider wrapping them in `model.start_operation` and `model.commit_operation` to make them undoable.

4. **Performance**: For complex operations, it's more efficient to send a single large Ruby script than many small ones.

5. **Security**: Be careful when evaluating user-provided Ruby code, as it has full access to the SketchUp API. 
```

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

```markdown
# SketchupMCP - Sketchup Model Context Protocol Integration

SketchupMCP connects Sketchup to Claude AI through the Model Context Protocol (MCP), allowing Claude to directly interact with and control Sketchup. This integration enables prompt-assisted 3D modeling, scene creation, and manipulation in Sketchup.

Big Shoutout to [Blender MCP](https://github.com/ahujasid/blender-mcp) for the inspiration and structure.

## Features

* **Two-way communication**: Connect Claude AI to Sketchup through a TCP socket connection
* **Component manipulation**: Create, modify, delete, and transform components in Sketchup
* **Material control**: Apply and modify materials and colors
* **Scene inspection**: Get detailed information about the current Sketchup scene
* **Selection handling**: Get and manipulate selected components
* **Ruby code evaluation**: Execute arbitrary Ruby code directly in SketchUp for advanced operations

## Components

The system consists of two main components:

1. **Sketchup Extension**: A Sketchup extension that creates a TCP server within Sketchup to receive and execute commands
2. **MCP Server (`sketchup_mcp/server.py`)**: A Python server that implements the Model Context Protocol and connects to the Sketchup extension

## Installation

### Python Packaging

We're using uv so you'll need to ```brew install uv```

### Sketchup Extension

1. Download or build the latest `.rbz` file
2. In Sketchup, go to Window > Extension Manager
3. Click "Install Extension" and select the downloaded `.rbz` file
4. Restart Sketchup

## Usage

### Starting the Connection

1. In Sketchup, go to Extensions > SketchupMCP > Start Server
2. The server will start on the default port (9876)
3. Make sure the MCP server is running in your terminal

### Using with Claude

Configure Claude to use the MCP server by adding the following to your Claude configuration:

```json
    "mcpServers": {
        "sketchup": {
            "command": "uvx",
            "args": [
                "sketchup-mcp"
            ]
        }
    }
```

This will pull the [latest from PyPI](https://pypi.org/project/sketchup-mcp/)

Once connected, Claude can interact with Sketchup using the following capabilities:

#### Tools

* `get_scene_info` - Gets information about the current Sketchup scene
* `get_selected_components` - Gets information about currently selected components
* `create_component` - Create a new component with specified parameters
* `delete_component` - Remove a component from the scene
* `transform_component` - Move, rotate, or scale a component
* `set_material` - Apply materials to components
* `export_scene` - Export the current scene to various formats
* `eval_ruby` - Execute arbitrary Ruby code in SketchUp for advanced operations

### Example Commands

Here are some examples of what you can ask Claude to do:

* "Create a simple house model with a roof and windows"
* "Select all components and get their information"
* "Make the selected component red"
* "Move the selected component 10 units up"
* "Export the current scene as a 3D model"
* "Create a complex arts and crafts cabinet using Ruby code"

## Troubleshooting

* **Connection issues**: Make sure both the Sketchup extension server and the MCP server are running
* **Command failures**: Check the Ruby Console in Sketchup for error messages
* **Timeout errors**: Try simplifying your requests or breaking them into smaller steps

## Technical Details

### Communication Protocol

The system uses a simple JSON-based protocol over TCP sockets:

* **Commands** are sent as JSON objects with a `type` and optional `params`
* **Responses** are JSON objects with a `status` and `result` or `message`

## Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

## License

MIT 
```

--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------

```
mcp[cli]>=1.3.0
websockets>=12.0
aiohttp>=3.9.0 
```

--------------------------------------------------------------------------------
/src/sketchup_mcp/__main__.py:
--------------------------------------------------------------------------------

```python
from .server import main

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

--------------------------------------------------------------------------------
/src/sketchup_mcp/__init__.py:
--------------------------------------------------------------------------------

```python
"""Sketchup integration through Model Context Protocol"""

__version__ = "0.1.17"

# Expose key classes and functions for easier imports
from .server import mcp 
```

--------------------------------------------------------------------------------
/su_mcp/extension.json:
--------------------------------------------------------------------------------

```json
{
  "name": "Sketchup MCP Server",
  "description": "Model Context Protocol server for Sketchup",
  "creator": "MCP Team",
  "copyright": "2024",
  "license": "MIT",
  "product_id": "SU_MCP_SERVER",
  "version": "1.6.0",
  "build": "1"
} 
```

--------------------------------------------------------------------------------
/update_and_restart.sh:
--------------------------------------------------------------------------------

```bash
#!/bin/bash

# Kill any existing sketchup-mcp processes
pkill -f "python -m sketchup_mcp"

# Update the package
pip install sketchup-mcp==0.1.15

# Start the server in the background
python -m sketchup_mcp &

# Wait a moment for the server to start
sleep 1

echo "Updated to sketchup-mcp 0.1.15 and restarted the server" 
```

--------------------------------------------------------------------------------
/su_mcp/su_mcp.rb:
--------------------------------------------------------------------------------

```ruby
require 'sketchup'
require 'extensions'

module SU_MCP
  unless file_loaded?(__FILE__)
    ext = SketchupExtension.new('Sketchup MCP Server', 'su_mcp/main')
    ext.description = 'Model Context Protocol server for Sketchup'
    ext.version     = '1.5.0'
    ext.copyright   = '2024'
    ext.creator     = 'MCP Team'
    
    Sketchup.register_extension(ext, true)
    
    file_loaded(__FILE__)
  end
end 
```

--------------------------------------------------------------------------------
/su_mcp.rb:
--------------------------------------------------------------------------------

```ruby
require 'sketchup.rb'
require 'extensions.rb'
require 'json'
require 'socket'

module SU_MCP
  unless file_loaded?(__FILE__)
    ex = SketchupExtension.new('Sketchup MCP', 'su_mcp/main')
    ex.description = 'MCP server for Sketchup that allows AI agents to control and manipulate scenes'
    ex.version     = '0.1.0'
    ex.copyright   = '2024'
    Sketchup.register_extension(ex, true)
    file_loaded(__FILE__)
  end
end 
```

--------------------------------------------------------------------------------
/test_eval_ruby.py:
--------------------------------------------------------------------------------

```python
import json
from dataclasses import dataclass

@dataclass
class MockContext:
    request_id: int = 1

# Import the function we want to test
from sketchup_mcp.server import eval_ruby

# Test with a simple Ruby script
test_code = '''
model = Sketchup.active_model
entities = model.active_entities
line = entities.add_line([0,0,0], [100,100,100])
puts "Created line with ID: #{line.entityID}"
line.entityID
'''

# Call the function
result = eval_ruby(MockContext(), test_code)
print(f"Result: {result}")

# Parse the result
parsed = json.loads(result)
print(f"Parsed: {json.dumps(parsed, indent=2)}") 
```

--------------------------------------------------------------------------------
/su_mcp/package.rb:
--------------------------------------------------------------------------------

```ruby
#!/usr/bin/env ruby

require 'zip'
require 'fileutils'

# Configuration
EXTENSION_NAME = 'su_mcp'
VERSION = '1.6.0'
OUTPUT_NAME = "#{EXTENSION_NAME}_v#{VERSION}.rbz"

# Create temp directory
temp_dir = "#{EXTENSION_NAME}_temp"
FileUtils.rm_rf(temp_dir) if Dir.exist?(temp_dir)
FileUtils.mkdir_p(temp_dir)

# Copy files to temp directory
FileUtils.cp_r('su_mcp', temp_dir)
FileUtils.cp('su_mcp.rb', temp_dir)
FileUtils.cp('extension.json', temp_dir)

# Create zip file
FileUtils.rm(OUTPUT_NAME) if File.exist?(OUTPUT_NAME)

Zip::File.open(OUTPUT_NAME, create: true) do |zipfile|
  Dir["#{temp_dir}/**/**"].each do |file|
    next if File.directory?(file)
    puts "Adding: #{file}"
    zipfile.add(file.sub("#{temp_dir}/", ''), file)
  end
end

# Clean up
FileUtils.rm_rf(temp_dir)

puts "Created #{OUTPUT_NAME}" 
```

--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------

```toml
[project]
name = "sketchup-mcp"
version = "0.1.17"
description = "Sketchup integration through Model Context Protocol"
readme = "README.md"
requires-python = ">=3.10"
license = {text = "MIT"}
authors = [
    {name = "Your Name", email = "[email protected]"}
]
classifiers = [
    "Programming Language :: Python :: 3",
    "License :: OSI Approved :: MIT License",
    "Operating System :: OS Independent",
]
dependencies = [
    "mcp[cli]>=1.3.0"
]

[project.urls]
Homepage = "https://github.com/yourusername/sketchup-mcp"
Issues = "https://github.com/yourusername/sketchup-mcp/issues"

[project.scripts]
sketchup-mcp = "sketchup_mcp.server:main"

[project.entry-points.mcp]
sketchup = "sketchup_mcp.server:mcp"

[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"

[tool.setuptools]
package-dir = {"" = "src"} 
```

--------------------------------------------------------------------------------
/examples/simple_test.py:
--------------------------------------------------------------------------------

```python
#!/usr/bin/env python3
"""
Simple Test for eval_ruby

This is a minimal test to verify that the eval_ruby feature works correctly.
"""

import json
import logging
from mcp.client import Client

# Configure logging
logging.basicConfig(level=logging.INFO, 
                   format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger("SimpleRubyTest")

# Simple Ruby code to create a cube
CUBE_CODE = """
model = Sketchup.active_model
entities = model.active_entities

# Start an operation for undo
model.start_operation("Create Test Cube", true)

# Create a group for the cube
group = entities.add_group

# Create the bottom face
face = group.entities.add_face(
  [0, 0, 0],
  [10, 0, 0],
  [10, 10, 0],
  [0, 10, 0]
)

# Push/pull to create the cube
face.pushpull(10)

# End the operation
model.commit_operation

# Return the group ID
group.entityID.to_s
"""

def main():
    """Main function to test the eval_ruby feature."""
    # Connect to the MCP server
    client = Client("sketchup")
    
    # Check if the connection is successful
    if not client.is_connected:
        logger.error("Failed to connect to the SketchUp MCP server.")
        return
    
    logger.info("Connected to SketchUp MCP server.")
    
    # Evaluate the Ruby code
    logger.info("Creating a simple cube...")
    response = client.eval_ruby(code=CUBE_CODE)
    
    # Parse the response
    try:
        result = json.loads(response)
        if result.get("success"):
            logger.info(f"Cube created successfully! Group ID: {result.get('result')}")
        else:
            logger.error(f"Failed to create cube: {result.get('error')}")
    except json.JSONDecodeError:
        logger.error(f"Failed to parse response: {response}")
    
    logger.info("Test completed.")

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

--------------------------------------------------------------------------------
/examples/simple_ruby_eval.py:
--------------------------------------------------------------------------------

```python
#!/usr/bin/env python3
"""
Simple Ruby Eval Example

This example demonstrates the basic usage of the eval_ruby feature
to execute Ruby code in SketchUp.
"""

import json
import logging
from mcp.client import Client

# Configure logging
logging.basicConfig(level=logging.INFO, 
                   format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger("SimpleRubyEvalExample")

# Simple Ruby code examples
EXAMPLES = [
    {
        "name": "Create a line",
        "code": """
            model = Sketchup.active_model
            entities = model.active_entities
            line = entities.add_line([0,0,0], [100,100,100])
            line.entityID
        """
    },
    {
        "name": "Create a cube",
        "code": """
            model = Sketchup.active_model
            entities = model.active_entities
            group = entities.add_group
            face = group.entities.add_face(
                [0, 0, 0],
                [10, 0, 0],
                [10, 10, 0],
                [0, 10, 0]
            )
            face.pushpull(10)
            group.entityID
        """
    },
    {
        "name": "Get model information",
        "code": """
            model = Sketchup.active_model
            info = {
                "filename": model.path,
                "title": model.title,
                "description": model.description,
                "entity_count": model.entities.size,
                "selection_count": model.selection.size
            }
            info.to_json
        """
    }
]

def main():
    """Main function to demonstrate the eval_ruby feature."""
    # Connect to the MCP server
    client = Client("sketchup")
    
    # Check if the connection is successful
    if not client.is_connected:
        logger.error("Failed to connect to the SketchUp MCP server.")
        return
    
    logger.info("Connected to SketchUp MCP server.")
    
    # Run each example
    for example in EXAMPLES:
        logger.info(f"Running example: {example['name']}")
        
        # Evaluate the Ruby code
        response = client.eval_ruby(code=example["code"])
        
        # Parse the response
        try:
            result = json.loads(response)
            if result.get("success"):
                logger.info(f"Result: {result.get('result')}")
            else:
                logger.error(f"Error: {result.get('error')}")
        except json.JSONDecodeError:
            logger.error(f"Failed to parse response: {response}")
        
        logger.info("-" * 40)
    
    logger.info("All examples completed.")

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

--------------------------------------------------------------------------------
/sketchup.json:
--------------------------------------------------------------------------------

```json
{
  "name": "sketchup",
  "description": "Sketchup integration through Model Context Protocol",
  "package": "sketchup-mcp",
  "module": "sketchup_mcp.server",
  "object": "mcp",
  "tools": [
    {
      "name": "create_component",
      "description": "Create a new component in Sketchup",
      "parameters": {
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "description": "Type of component to create",
            "default": "cube"
          },
          "position": {
            "type": "array",
            "items": {
              "type": "number"
            },
            "description": "Position [x,y,z] of the component",
            "default": [0,0,0]
          },
          "dimensions": {
            "type": "array",
            "items": {
              "type": "number"
            },
            "description": "Dimensions [width,height,depth] of the component",
            "default": [1,1,1]
          }
        }
      }
    },
    {
      "name": "delete_component",
      "description": "Delete a component by ID",
      "parameters": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "ID of the component to delete"
          }
        },
        "required": ["id"]
      }
    },
    {
      "name": "transform_component",
      "description": "Transform a component's position, rotation, or scale",
      "parameters": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "ID of the component to transform"
          },
          "position": {
            "type": "array",
            "items": {
              "type": "number"
            },
            "description": "New position [x,y,z]"
          },
          "rotation": {
            "type": "array",
            "items": {
              "type": "number"
            },
            "description": "New rotation [x,y,z] in degrees"
          },
          "scale": {
            "type": "array",
            "items": {
              "type": "number"
            },
            "description": "New scale [x,y,z]"
          }
        },
        "required": ["id"]
      }
    },
    {
      "name": "get_selection",
      "description": "Get currently selected components",
      "parameters": {
        "type": "object",
        "properties": {}
      }
    },
    {
      "name": "set_material",
      "description": "Set material for a component",
      "parameters": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "ID of the component"
          },
          "material": {
            "type": "string",
            "description": "Name of the material to apply"
          }
        },
        "required": ["id", "material"]
      }
    },
    {
      "name": "export_scene",
      "description": "Export the current scene",
      "parameters": {
        "type": "object",
        "properties": {
          "format": {
            "type": "string",
            "description": "Export format (e.g. skp, obj, etc)",
            "default": "skp"
          }
        }
      }
    }
  ],
  "mcpServers": {
    "sketchup": {
      "command": "uvx",
      "args": ["sketchup_mcp"]
    }
  }
} 
```

--------------------------------------------------------------------------------
/examples/ruby_tester.py:
--------------------------------------------------------------------------------

```python
#!/usr/bin/env python3
"""
Ruby Code Tester

This script tests Ruby code in smaller chunks to identify compatibility issues with SketchUp.
"""

import json
import logging
from mcp.client import Client

# Configure logging
logging.basicConfig(level=logging.INFO, 
                   format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger("RubyTester")

# Test cases - each with a name and Ruby code to test
TEST_CASES = [
    {
        "name": "Basic Model Access",
        "code": """
            model = Sketchup.active_model
            entities = model.active_entities
            "Success: Basic model access works"
        """
    },
    {
        "name": "Create Group",
        "code": """
            model = Sketchup.active_model
            entities = model.active_entities
            group = entities.add_group
            group.entityID.to_s
        """
    },
    {
        "name": "Create Face and Pushpull",
        "code": """
            model = Sketchup.active_model
            entities = model.active_entities
            group = entities.add_group
            face = group.entities.add_face(
                [0, 0, 0],
                [10, 0, 0],
                [10, 10, 0],
                [0, 10, 0]
            )
            face.pushpull(10)
            "Success: Created face and pushpull"
        """
    },
    {
        "name": "Component Definition",
        "code": """
            model = Sketchup.active_model
            definition = model.definitions.add("Test Component")
            definition.name
        """
    },
    {
        "name": "Component Behavior",
        "code": """
            model = Sketchup.active_model
            definition = model.definitions.add("Test Component")
            # Get behavior properties
            behavior = definition.behavior
            
            # Test available methods
            methods = behavior.methods - Object.methods
            
            # Return the available methods
            methods.sort.join(", ")
        """
    },
    {
        "name": "Component Instance",
        "code": """
            model = Sketchup.active_model
            entities = model.active_entities
            definition = model.definitions.add("Test Component")
            
            # Create a point and transformation
            point = Geom::Point3d.new(0, 0, 0)
            transform = Geom::Transformation.new(point)
            
            # Add instance
            instance = entities.add_instance(definition, transform)
            
            # Set behavior properties
            behavior = instance.definition.behavior
            behavior.snapto = 0
            
            "Success: Component instance created with behavior set"
        """
    }
]

def test_ruby_code(client, test_case):
    """Test a single Ruby code snippet."""
    logger.info(f"Testing: {test_case['name']}")
    
    response = client.eval_ruby(code=test_case["code"])
    
    try:
        result = json.loads(response)
        if result.get("success"):
            logger.info(f"✅ SUCCESS: {result.get('result')}")
            return True
        else:
            logger.error(f"❌ ERROR: {result.get('error')}")
            return False
    except json.JSONDecodeError:
        logger.error(f"Failed to parse response: {response}")
        return False

def main():
    """Main function to test Ruby code snippets."""
    # Connect to the MCP server
    client = Client("sketchup")
    
    # Check if the connection is successful
    if not client.is_connected:
        logger.error("Failed to connect to the SketchUp MCP server.")
        return
    
    logger.info("Connected to SketchUp MCP server.")
    logger.info("=" * 50)
    
    # Run each test case
    success_count = 0
    for test_case in TEST_CASES:
        if test_ruby_code(client, test_case):
            success_count += 1
        logger.info("-" * 50)
    
    # Summary
    logger.info(f"Testing complete: {success_count}/{len(TEST_CASES)} tests passed")

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

--------------------------------------------------------------------------------
/examples/behavior_tester.py:
--------------------------------------------------------------------------------

```python
#!/usr/bin/env python3
"""
Component Behavior Tester

This script specifically tests the component behavior methods in SketchUp 25.0.574.
"""

import json
import logging
from mcp.client import Client

# Configure logging
logging.basicConfig(level=logging.INFO, 
                   format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger("BehaviorTester")

# Ruby code to test component behavior methods
BEHAVIOR_TEST_CODE = """
# Create a new model context
model = Sketchup.active_model
model.start_operation("Test Component Behavior", true)

# Create a new component definition
definition = model.definitions.add("Test Component")

# Get the behavior object
behavior = definition.behavior

# Get all methods available on the behavior object
all_methods = behavior.methods - Object.methods

# Test setting various behavior properties
results = {}

# Test common behavior properties
properties_to_test = [
  "snapto",
  "cuts_opening",
  "always_face_camera",
  "no_scale_tool",
  "shadows_face_sun",
  "is_component",
  "component?"
]

# Test each property
property_results = {}
for prop in properties_to_test
  begin
    # Try to get the property
    if behavior.respond_to?(prop)
      property_results[prop] = {
        "exists": true,
        "readable": true
      }
      
      # Try to set the property (for boolean properties, try setting to true)
      setter_method = prop + "="
      if behavior.respond_to?(setter_method)
        if prop == "snapto"
          behavior.send(setter_method, 0)
        else
          behavior.send(setter_method, true)
        end
        property_results[prop]["writable"] = true
      else
        property_results[prop]["writable"] = false
      end
    else
      property_results[prop] = {
        "exists": false
      }
    end
  rescue => e
    property_results[prop] = {
      "exists": true,
      "error": e.message
    }
  end
end

# End the operation
model.commit_operation

# Return the results
{
  "all_methods": all_methods.sort,
  "property_results": property_results
}.to_json
"""

def main():
    """Main function to test component behavior methods."""
    # Connect to the MCP server
    client = Client("sketchup")
    
    # Check if the connection is successful
    if not client.is_connected:
        logger.error("Failed to connect to the SketchUp MCP server.")
        return
    
    logger.info("Connected to SketchUp MCP server.")
    
    # Run the behavior test
    logger.info("Testing component behavior methods...")
    response = client.eval_ruby(code=BEHAVIOR_TEST_CODE)
    
    # Parse the response
    try:
        result = json.loads(response)
        if result.get("success"):
            # Parse the JSON result
            behavior_data = json.loads(result.get("result"))
            
            # Display all available methods
            logger.info("Available methods on Behavior object:")
            for method in behavior_data["all_methods"]:
                logger.info(f"  - {method}")
            
            # Display property test results
            logger.info("\nProperty test results:")
            for prop, prop_result in behavior_data["property_results"].items():
                if prop_result.get("exists"):
                    readable = prop_result.get("readable", False)
                    writable = prop_result.get("writable", False)
                    error = prop_result.get("error")
                    
                    status = []
                    if readable:
                        status.append("readable")
                    if writable:
                        status.append("writable")
                    
                    if error:
                        logger.info(f"  - {prop}: EXISTS but ERROR: {error}")
                    else:
                        logger.info(f"  - {prop}: EXISTS ({', '.join(status)})")
                else:
                    logger.info(f"  - {prop}: DOES NOT EXIST")
        else:
            logger.error(f"Error: {result.get('error')}")
    except json.JSONDecodeError as e:
        logger.error(f"Failed to parse response: {e}")
    
    logger.info("Testing completed.")

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

--------------------------------------------------------------------------------
/plan.md:
--------------------------------------------------------------------------------

```markdown
Comprehensive Woodworking API Plan
Now, let's outline a comprehensive plan for a fully-featured woodworking API based on the SketchUp Ruby API. This will serve as a roadmap for future development.
1. Core Geometry Operations
1.1 Advanced Shape Creation
Implement:
Rectangular Prism: Already implemented as "cube"
Cylinder: Already implemented
Cone: Already implemented
Sphere: Already implemented
Torus/Donut: For creating rings and circular moldings
Wedge: For angled cuts and joinery
Pyramid: For decorative elements
Custom Polygon Extrusion: For arbitrary base shapes
1.2 Boolean Operations
Implement:
Union: Combine multiple shapes
Difference: Cut one shape from another (crucial for joinery)
Intersection: Keep only the overlapping portion of shapes
Split: Divide a shape along a plane
1.3 Modification Operations
Implement:
Chamfer: Create beveled edges
Fillet: Create rounded edges
Shell: Hollow out a solid with a specified wall thickness
Offset: Create parallel faces at a specified distance
Taper: Create a gradual narrowing
Twist: Rotate progressively along an axis
2. Woodworking-Specific Features
2.1 Joinery
Implement:
Mortise and Tenon: Create matching mortise and tenon joints
Dovetail: Create interlocking dovetail joints
Finger Joint: Create interlocking box joints
Lap Joint: Create overlapping joints
Miter Joint: Create angled joints
Dowel Joint: Create holes for dowels
Pocket Hole: Create angled holes for pocket screws
2.2 Wood-Specific Operations
Implement:
Grain Direction: Specify and visualize wood grain
Wood Species: Library of common wood species with appropriate textures and colors
Board Dimensioning: Convert between nominal and actual lumber dimensions
Plywood Sheet Optimization: Calculate optimal cutting patterns
2.3 Hardware
Implement:
Screws: Add various types of screws
Nails: Add various types of nails
Hinges: Add hinges with proper movement constraints
Drawer Slides: Add drawer slides with proper movement
Handles/Knobs: Add decorative hardware
3. Advanced Geometry Manipulation
3.1 Curves and Surfaces
Implement:
Bezier Curves: Create smooth curves
Splines: Create complex curves through multiple points
Loft: Create a surface between multiple profiles
Sweep: Create a surface by moving a profile along a path
Revolve: Create a surface by rotating a profile around an axis
3.2 Pattern Operations
Implement:
Linear Pattern: Create multiple copies along a line
Circular Pattern: Create multiple copies around a center
Mirror: Create a mirrored copy
Symmetry: Enforce symmetry constraints
4. Material and Appearance
4.1 Materials
Implement:
Basic Colors: Already implemented
Wood Textures: Add realistic wood grain textures
Finish Types: Stain, paint, varnish, etc.
Material Properties: Reflectivity, transparency, etc.
4.2 Rendering
Implement:
Realistic Rendering: High-quality visualization
Exploded Views: Show assembly steps
Section Views: Show internal details
5. Measurement and Analysis
5.1 Dimensioning
Implement:
Linear Dimensions: Measure distances
Angular Dimensions: Measure angles
Radius/Diameter Dimensions: Measure curves
Automatic Dimensioning: Add dimensions to all features
5.2 Analysis
Implement:
Volume Calculation: Calculate wood volume
Cost Estimation: Calculate material costs
Weight Calculation: Estimate weight based on wood species
Structural Analysis: Basic strength calculations
6. Project Management
6.1 Organization
Implement:
Component Hierarchy: Organize parts into assemblies
Layers: Organize by function or stage
Tags: Add metadata to components
6.2 Documentation
Implement:
Cut Lists: Generate cutting diagrams
Assembly Instructions: Generate step-by-step guides
Bill of Materials: List all required parts and hardware
Implementation Plan
Phase 1: Core Functionality (Current)
✅ Basic shapes (cube, cylinder, sphere, cone)
✅ Basic transformations (move, rotate, scale)
✅ Basic materials
✅ Export functionality
Phase 2: Advanced Geometry (Next)
Boolean operations (union, difference, intersection)
Additional shapes (torus, wedge, pyramid)
Chamfer and fillet operations
Curve creation and manipulation
Phase 3: Woodworking Specifics
Joinery tools (mortise and tenon, dovetail, etc.)
Wood species and grain direction
Hardware components
Phase 4: Project Management
Component organization
Dimensioning and measurement
Cut lists and bill of materials
Phase 5: Advanced Visualization
Realistic materials and textures
Enhanced rendering
Animation and assembly visualization
```

--------------------------------------------------------------------------------
/examples/arts_and_crafts_cabinet.py:
--------------------------------------------------------------------------------

```python
#!/usr/bin/env python3
"""
Arts and Crafts Cabinet Example

This example demonstrates how to use the eval_ruby feature to create
a complex arts and crafts style cabinet in SketchUp using Ruby code.
"""

import json
import logging
from mcp.client import Client

# Configure logging
logging.basicConfig(level=logging.INFO, 
                   format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger("ArtsAndCraftsCabinetExample")

# Ruby code to create an arts and crafts cabinet
CABINET_RUBY_CODE = """
# Arts and Crafts Cabinet with Working Doors
# This script creates a stylish arts and crafts style cabinet with working doors
# that can be opened and closed using SketchUp's component functionality

def create_arts_and_crafts_cabinet
  # Get the active model and start an operation for undo purposes
  model = Sketchup.active_model
  model.start_operation("Create Arts and Crafts Cabinet", true)
  
  # Define cabinet dimensions (in inches)
  width = 36
  depth = 18
  height = 72
  thickness = 0.75
  
  # Create a new component definition for the cabinet
  cabinet_def = model.definitions.add("Arts and Crafts Cabinet")
  entities = cabinet_def.entities
  
  # Create the main cabinet box
  create_cabinet_box(entities, width, depth, height, thickness)
  
  # Add shelves
  shelf_positions = [height/3, 2*height/3]
  create_shelves(entities, width, depth, thickness, shelf_positions)
  
  # Create doors (as nested components that can swing open)
  create_doors(entities, width, depth, height, thickness)
  
  # Add decorative elements typical of arts and crafts style
  add_decorative_elements(entities, width, depth, height, thickness)
  
  # Place the component in the model
  point = Geom::Point3d.new(0, 0, 0)
  transform = Geom::Transformation.new(point)
  instance = model.active_entities.add_instance(cabinet_def, transform)
  
  # End the operation
  model.commit_operation
  
  # Return the component instance ID
  return instance.entityID
end

def create_cabinet_box(entities, width, depth, height, thickness)
  # Bottom
  bottom_points = [
    [0, 0, 0], 
    [width, 0, 0], 
    [width, depth, 0], 
    [0, depth, 0]
  ]
  bottom_face = entities.add_face(bottom_points)
  bottom_face.pushpull(-thickness)
  
  # Back
  back_points = [
    [0, depth, 0], 
    [width, depth, 0], 
    [width, depth, height], 
    [0, depth, height]
  ]
  back_face = entities.add_face(back_points)
  back_face.pushpull(-thickness)
  
  # Left side
  left_points = [
    [0, 0, 0], 
    [0, depth, 0], 
    [0, depth, height], 
    [0, 0, height]
  ]
  left_face = entities.add_face(left_points)
  left_face.pushpull(-thickness)
  
  # Right side
  right_points = [
    [width, 0, 0], 
    [width, depth, 0], 
    [width, depth, height], 
    [width, 0, height]
  ]
  right_face = entities.add_face(right_points)
  right_face.pushpull(thickness)
  
  # Top
  top_points = [
    [0, 0, height], 
    [width, 0, height], 
    [width, depth, height], 
    [0, depth, height]
  ]
  top_face = entities.add_face(top_points)
  top_face.pushpull(thickness)
end

def create_shelves(entities, width, depth, thickness, positions)
  positions.each do |z_pos|
    shelf_points = [
      [thickness, thickness, z_pos], 
      [width - thickness, thickness, z_pos], 
      [width - thickness, depth - thickness, z_pos], 
      [thickness, depth - thickness, z_pos]
    ]
    shelf_face = entities.add_face(shelf_points)
    shelf_face.pushpull(-thickness)
  end
end

def create_doors(entities, width, depth, height, thickness)
  # Define door dimensions
  door_width = (width - thickness) / 2
  door_height = height - 2 * thickness
  
  # Create left door as a component (so it can be animated)
  left_door_def = Sketchup.active_model.definitions.add("Left Cabinet Door")
  
  # Create the door geometry in the component
  door_entities = left_door_def.entities
  left_door_points = [
    [0, 0, 0], 
    [door_width, 0, 0], 
    [door_width, thickness, 0], 
    [0, thickness, 0]
  ]
  left_door_face = door_entities.add_face(left_door_points)
  left_door_face.pushpull(door_height)
  
  # Add door details
  add_door_details(door_entities, door_width, thickness, door_height)
  
  # Place the left door component
  left_hinge_point = Geom::Point3d.new(thickness, thickness, thickness)
  left_transform = Geom::Transformation.new(left_hinge_point)
  left_door_instance = entities.add_instance(left_door_def, left_transform)
  
  # Set the hinge axis for animation - using correct method for SketchUp 2025
  # The component behavior is already set by default
  left_door_instance.definition.behavior.snapto = 0 # No automatic snapping
  
  # Create right door (similar process)
  right_door_def = Sketchup.active_model.definitions.add("Right Cabinet Door")
  
  door_entities = right_door_def.entities
  right_door_points = [
    [0, 0, 0], 
    [door_width, 0, 0], 
    [door_width, thickness, 0], 
    [0, thickness, 0]
  ]
  right_door_face = door_entities.add_face(right_door_points)
  right_door_face.pushpull(door_height)
  
  # Add door details
  add_door_details(door_entities, door_width, thickness, door_height)
  
  # Place the right door component
  right_hinge_point = Geom::Point3d.new(width - thickness, thickness, thickness)
  right_transform = Geom::Transformation.new(right_hinge_point)
  right_door_instance = entities.add_instance(right_door_def, right_transform)
  
  # Set the hinge axis for animation (flipped compared to left door)
  # The component behavior is already set by default
  right_door_instance.definition.behavior.snapto = 0
end

def add_door_details(entities, width, thickness, height)
  # Add a decorative panel that's inset
  inset = thickness / 2
  panel_points = [
    [inset, -thickness/2, inset], 
    [width - inset, -thickness/2, inset], 
    [width - inset, -thickness/2, height - inset], 
    [inset, -thickness/2, height - inset]
  ]
  panel = entities.add_face(panel_points)
  panel.pushpull(-thickness/4)
  
  # Add a small handle
  handle_position = [width - 2 * inset, -thickness * 1.5, height / 2]
  handle_size = height / 20
  
  # Create a cylinder for the handle
  handle_circle = entities.add_circle(handle_position, [0, 1, 0], handle_size, 12)
  handle_face = entities.add_face(handle_circle)
  handle_face.pushpull(-thickness)
end

def add_decorative_elements(entities, width, depth, height, thickness)
  # Add characteristic arts and crafts style base
  base_height = 4
  
  # Create a slightly wider base
  base_extension = 1
  base_points = [
    [-base_extension, -base_extension, 0], 
    [width + base_extension, -base_extension, 0], 
    [width + base_extension, depth + base_extension, 0], 
    [-base_extension, depth + base_extension, 0]
  ]
  base_face = entities.add_face(base_points)
  base_face.pushpull(-base_height)
  
  # Add corbels in the arts and crafts style
  add_corbels(entities, width, depth, height, thickness)
  
  # Add crown detail at the top
  add_crown(entities, width, depth, height, thickness)
end

def add_corbels(entities, width, depth, height, thickness)
  # Add decorative corbels under the top
  corbel_height = 3
  corbel_depth = 2
  
  # Left front corbel
  left_corbel_points = [
    [thickness * 2, thickness, height - thickness - corbel_height],
    [thickness * 2 + corbel_depth, thickness, height - thickness - corbel_height],
    [thickness * 2 + corbel_depth, thickness, height - thickness],
    [thickness * 2, thickness, height - thickness]
  ]
  left_corbel = entities.add_face(left_corbel_points)
  left_corbel.pushpull(-thickness)
  
  # Right front corbel
  right_corbel_points = [
    [width - thickness * 2 - corbel_depth, thickness, height - thickness - corbel_height],
    [width - thickness * 2, thickness, height - thickness - corbel_height],
    [width - thickness * 2, thickness, height - thickness],
    [width - thickness * 2 - corbel_depth, thickness, height - thickness]
  ]
  right_corbel = entities.add_face(right_corbel_points)
  right_corbel.pushpull(-thickness)
end

def add_crown(entities, width, depth, height, thickness)
  # Add a simple crown molding at the top
  crown_height = 2
  crown_extension = 1.5
  
  crown_points = [
    [-crown_extension, -crown_extension, height + thickness],
    [width + crown_extension, -crown_extension, height + thickness],
    [width + crown_extension, depth + crown_extension, height + thickness],
    [-crown_extension, depth + crown_extension, height + thickness]
  ]
  crown_face = entities.add_face(crown_points)
  crown_face.pushpull(crown_height)
  
  # Add a slight taper to the crown
  taper_points = [
    [-crown_extension/2, -crown_extension/2, height + thickness + crown_height],
    [width + crown_extension/2, -crown_extension/2, height + thickness + crown_height],
    [width + crown_extension/2, depth + crown_extension/2, height + thickness + crown_height],
    [-crown_extension/2, depth + crown_extension/2, height + thickness + crown_height]
  ]
  taper_face = entities.add_face(taper_points)
  taper_face.pushpull(crown_height/2)
end

# Execute the function to create the cabinet
create_arts_and_crafts_cabinet
"""

def main():
    """Main function to create the arts and crafts cabinet in SketchUp."""
    # Connect to the MCP server
    client = Client("sketchup")
    
    # Check if the connection is successful
    if not client.is_connected:
        logger.error("Failed to connect to the SketchUp MCP server.")
        return
    
    logger.info("Connected to SketchUp MCP server.")
    
    # Evaluate the Ruby code to create the cabinet
    logger.info("Creating arts and crafts cabinet...")
    response = client.eval_ruby(code=CABINET_RUBY_CODE)
    
    # Parse the response
    try:
        result = json.loads(response)
        if result.get("success"):
            logger.info(f"Cabinet created successfully! Result: {result.get('result')}")
        else:
            logger.error(f"Failed to create cabinet: {result.get('error')}")
    except json.JSONDecodeError:
        logger.error(f"Failed to parse response: {response}")
    
    logger.info("Example completed.")

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

--------------------------------------------------------------------------------
/src/sketchup_mcp/server.py:
--------------------------------------------------------------------------------

```python
from mcp.server.fastmcp import FastMCP, Context
import socket
import json
import asyncio
import logging
from dataclasses import dataclass
from contextlib import asynccontextmanager
from typing import AsyncIterator, Dict, Any, List

# Configure logging
logging.basicConfig(level=logging.INFO, 
                   format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger("SketchupMCPServer")

# Define version directly to avoid pkg_resources dependency
__version__ = "0.1.17"
logger.info(f"SketchupMCP Server version {__version__} starting up")

@dataclass
class SketchupConnection:
    host: str
    port: int
    sock: socket.socket = None
    
    def connect(self) -> bool:
        """Connect to the Sketchup extension socket server"""
        if self.sock:
            try:
                # Test if connection is still alive
                self.sock.settimeout(0.1)
                self.sock.send(b'')
                return True
            except (socket.error, BrokenPipeError, ConnectionResetError):
                # Connection is dead, close it and reconnect
                logger.info("Connection test failed, reconnecting...")
                self.disconnect()
            
        try:
            self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            self.sock.connect((self.host, self.port))
            logger.info(f"Connected to Sketchup at {self.host}:{self.port}")
            return True
        except Exception as e:
            logger.error(f"Failed to connect to Sketchup: {str(e)}")
            self.sock = None
            return False
    
    def disconnect(self):
        """Disconnect from the Sketchup extension"""
        if self.sock:
            try:
                self.sock.close()
            except Exception as e:
                logger.error(f"Error disconnecting from Sketchup: {str(e)}")
            finally:
                self.sock = None

    def receive_full_response(self, sock, buffer_size=8192):
        """Receive the complete response, potentially in multiple chunks"""
        chunks = []
        sock.settimeout(15.0)
        
        try:
            while True:
                try:
                    chunk = sock.recv(buffer_size)
                    if not chunk:
                        if not chunks:
                            raise Exception("Connection closed before receiving any data")
                        break
                    
                    chunks.append(chunk)
                    
                    try:
                        data = b''.join(chunks)
                        json.loads(data.decode('utf-8'))
                        logger.info(f"Received complete response ({len(data)} bytes)")
                        return data
                    except json.JSONDecodeError:
                        continue
                except socket.timeout:
                    logger.warning("Socket timeout during chunked receive")
                    break
                except (ConnectionError, BrokenPipeError, ConnectionResetError) as e:
                    logger.error(f"Socket connection error during receive: {str(e)}")
                    raise
        except socket.timeout:
            logger.warning("Socket timeout during chunked receive")
        except Exception as e:
            logger.error(f"Error during receive: {str(e)}")
            raise
            
        if chunks:
            data = b''.join(chunks)
            logger.info(f"Returning data after receive completion ({len(data)} bytes)")
            try:
                json.loads(data.decode('utf-8'))
                return data
            except json.JSONDecodeError:
                raise Exception("Incomplete JSON response received")
        else:
            raise Exception("No data received")

    def send_command(self, method: str, params: Dict[str, Any] = None, request_id: Any = None) -> Dict[str, Any]:
        """Send a JSON-RPC request to Sketchup and return the response"""
        # Try to connect if not connected
        if not self.connect():
            raise ConnectionError("Not connected to Sketchup")
        
        # Ensure we're sending a proper JSON-RPC request
        if method == "tools/call" and params and "name" in params and "arguments" in params:
            # This is already in the correct format
            request = {
                "jsonrpc": "2.0",
                "method": method,
                "params": params,
                "id": request_id
            }
        else:
            # This is a direct command - convert to JSON-RPC
            command_name = method
            command_params = params or {}
            
            # Log the conversion
            logger.info(f"Converting direct command '{command_name}' to JSON-RPC format")
            
            request = {
                "jsonrpc": "2.0",
                "method": "tools/call",
                "params": {
                    "name": command_name,
                    "arguments": command_params
                },
                "id": request_id
            }
        
        # Maximum number of retries
        max_retries = 2
        retry_count = 0
        
        while retry_count <= max_retries:
            try:
                logger.info(f"Sending JSON-RPC request: {request}")
                
                # Log the exact bytes being sent
                request_bytes = json.dumps(request).encode('utf-8') + b'\n'
                logger.info(f"Raw bytes being sent: {request_bytes}")
                
                self.sock.sendall(request_bytes)
                logger.info(f"Request sent, waiting for response...")
                
                self.sock.settimeout(15.0)
                
                response_data = self.receive_full_response(self.sock)
                logger.info(f"Received {len(response_data)} bytes of data")
                
                response = json.loads(response_data.decode('utf-8'))
                logger.info(f"Response parsed: {response}")
                
                if "error" in response:
                    logger.error(f"Sketchup error: {response['error']}")
                    raise Exception(response["error"].get("message", "Unknown error from Sketchup"))
                
                return response.get("result", {})
                
            except (socket.timeout, ConnectionError, BrokenPipeError, ConnectionResetError) as e:
                logger.warning(f"Connection error (attempt {retry_count+1}/{max_retries+1}): {str(e)}")
                retry_count += 1
                
                if retry_count <= max_retries:
                    logger.info(f"Retrying connection...")
                    self.disconnect()
                    if not self.connect():
                        logger.error("Failed to reconnect")
                        break
                else:
                    logger.error(f"Max retries reached, giving up")
                    self.sock = None
                    raise Exception(f"Connection to Sketchup lost after {max_retries+1} attempts: {str(e)}")
            
            except json.JSONDecodeError as e:
                logger.error(f"Invalid JSON response from Sketchup: {str(e)}")
                if 'response_data' in locals() and response_data:
                    logger.error(f"Raw response (first 200 bytes): {response_data[:200]}")
                raise Exception(f"Invalid response from Sketchup: {str(e)}")
            
            except Exception as e:
                logger.error(f"Error communicating with Sketchup: {str(e)}")
                self.sock = None
                raise Exception(f"Communication error with Sketchup: {str(e)}")

# Global connection management
_sketchup_connection = None

def get_sketchup_connection():
    """Get or create a persistent Sketchup connection"""
    global _sketchup_connection
    
    if _sketchup_connection is not None:
        try:
            # Test connection with a ping command
            ping_request = {
                "jsonrpc": "2.0",
                "method": "ping",
                "params": {},
                "id": 0
            }
            _sketchup_connection.sock.sendall(json.dumps(ping_request).encode('utf-8') + b'\n')
            return _sketchup_connection
        except Exception as e:
            logger.warning(f"Existing connection is no longer valid: {str(e)}")
            try:
                _sketchup_connection.disconnect()
            except:
                pass
            _sketchup_connection = None
    
    if _sketchup_connection is None:
        _sketchup_connection = SketchupConnection(host="localhost", port=9876)
        if not _sketchup_connection.connect():
            logger.error("Failed to connect to Sketchup")
            _sketchup_connection = None
            raise Exception("Could not connect to Sketchup. Make sure the Sketchup extension is running.")
        logger.info("Created new persistent connection to Sketchup")
    
    return _sketchup_connection

@asynccontextmanager
async def server_lifespan(server: FastMCP) -> AsyncIterator[Dict[str, Any]]:
    """Manage server startup and shutdown lifecycle"""
    try:
        logger.info("SketchupMCP server starting up")
        try:
            sketchup = get_sketchup_connection()
            logger.info("Successfully connected to Sketchup on startup")
        except Exception as e:
            logger.warning(f"Could not connect to Sketchup on startup: {str(e)}")
            logger.warning("Make sure the Sketchup extension is running")
        yield {}
    finally:
        global _sketchup_connection
        if _sketchup_connection:
            logger.info("Disconnecting from Sketchup")
            _sketchup_connection.disconnect()
            _sketchup_connection = None
        logger.info("SketchupMCP server shut down")

# Create MCP server with lifespan support
mcp = FastMCP(
    "SketchupMCP",
    description="Sketchup integration through the Model Context Protocol",
    lifespan=server_lifespan
)

# Tool endpoints
@mcp.tool()
def create_component(
    ctx: Context,
    type: str = "cube",
    position: List[float] = None,
    dimensions: List[float] = None
) -> str:
    """Create a new component in Sketchup"""
    try:
        logger.info(f"create_component called with type={type}, position={position}, dimensions={dimensions}, request_id={ctx.request_id}")
        
        sketchup = get_sketchup_connection()
        
        params = {
            "name": "create_component",
            "arguments": {
                "type": type,
                "position": position or [0,0,0],
                "dimensions": dimensions or [1,1,1]
            }
        }
        
        logger.info(f"Calling send_command with method='tools/call', params={params}, request_id={ctx.request_id}")
        
        result = sketchup.send_command(
            method="tools/call",
            params=params,
            request_id=ctx.request_id
        )
        
        logger.info(f"create_component result: {result}")
        return json.dumps(result)
    except Exception as e:
        logger.error(f"Error in create_component: {str(e)}")
        return f"Error creating component: {str(e)}"

@mcp.tool()
def delete_component(
    ctx: Context,
    id: str
) -> str:
    """Delete a component by ID"""
    try:
        sketchup = get_sketchup_connection()
        result = sketchup.send_command(
            method="tools/call",
            params={
                "name": "delete_component",
                "arguments": {"id": id}
            },
            request_id=ctx.request_id
        )
        return json.dumps(result)
    except Exception as e:
        return f"Error deleting component: {str(e)}"

@mcp.tool()
def transform_component(
    ctx: Context,
    id: str,
    position: List[float] = None,
    rotation: List[float] = None,
    scale: List[float] = None
) -> str:
    """Transform a component's position, rotation, or scale"""
    try:
        sketchup = get_sketchup_connection()
        arguments = {"id": id}
        if position is not None:
            arguments["position"] = position
        if rotation is not None:
            arguments["rotation"] = rotation
        if scale is not None:
            arguments["scale"] = scale
            
        result = sketchup.send_command(
            method="tools/call",
            params={
                "name": "transform_component",
                "arguments": arguments
            },
            request_id=ctx.request_id
        )
        return json.dumps(result)
    except Exception as e:
        return f"Error transforming component: {str(e)}"

@mcp.tool()
def get_selection(ctx: Context) -> str:
    """Get currently selected components"""
    try:
        sketchup = get_sketchup_connection()
        result = sketchup.send_command(
            method="tools/call",
            params={
                "name": "get_selection",
                "arguments": {}
            },
            request_id=ctx.request_id
        )
        return json.dumps(result)
    except Exception as e:
        return f"Error getting selection: {str(e)}"

@mcp.tool()
def set_material(
    ctx: Context,
    id: str,
    material: str
) -> str:
    """Set material for a component"""
    try:
        sketchup = get_sketchup_connection()
        result = sketchup.send_command(
            method="tools/call",
            params={
                "name": "set_material",
                "arguments": {
                    "id": id,
                    "material": material
                }
            },
            request_id=ctx.request_id
        )
        return json.dumps(result)
    except Exception as e:
        return f"Error setting material: {str(e)}"

@mcp.tool()
def export_scene(
    ctx: Context,
    format: str = "skp"
) -> str:
    """Export the current scene"""
    try:
        sketchup = get_sketchup_connection()
        result = sketchup.send_command(
            method="tools/call",
            params={
                "name": "export",
                "arguments": {
                    "format": format
                }
            },
            request_id=ctx.request_id
        )
        return json.dumps(result)
    except Exception as e:
        return f"Error exporting scene: {str(e)}"

@mcp.tool()
def create_mortise_tenon(
    ctx: Context,
    mortise_id: str,
    tenon_id: str,
    width: float = 1.0,
    height: float = 1.0,
    depth: float = 1.0,
    offset_x: float = 0.0,
    offset_y: float = 0.0,
    offset_z: float = 0.0
) -> str:
    """Create a mortise and tenon joint between two components"""
    try:
        logger.info(f"create_mortise_tenon called with mortise_id={mortise_id}, tenon_id={tenon_id}, width={width}, height={height}, depth={depth}, offsets=({offset_x}, {offset_y}, {offset_z})")
        
        sketchup = get_sketchup_connection()
        
        result = sketchup.send_command(
            method="tools/call",
            params={
                "name": "create_mortise_tenon",
                "arguments": {
                    "mortise_id": mortise_id,
                    "tenon_id": tenon_id,
                    "width": width,
                    "height": height,
                    "depth": depth,
                    "offset_x": offset_x,
                    "offset_y": offset_y,
                    "offset_z": offset_z
                }
            },
            request_id=ctx.request_id
        )
        
        logger.info(f"create_mortise_tenon result: {result}")
        return json.dumps(result)
    except Exception as e:
        logger.error(f"Error in create_mortise_tenon: {str(e)}")
        return f"Error creating mortise and tenon joint: {str(e)}"

@mcp.tool()
def create_dovetail(
    ctx: Context,
    tail_id: str,
    pin_id: str,
    width: float = 1.0,
    height: float = 1.0,
    depth: float = 1.0,
    angle: float = 15.0,
    num_tails: int = 3,
    offset_x: float = 0.0,
    offset_y: float = 0.0,
    offset_z: float = 0.0
) -> str:
    """Create a dovetail joint between two components"""
    try:
        logger.info(f"create_dovetail called with tail_id={tail_id}, pin_id={pin_id}, width={width}, height={height}, depth={depth}, angle={angle}, num_tails={num_tails}")
        
        sketchup = get_sketchup_connection()
        
        result = sketchup.send_command(
            method="tools/call",
            params={
                "name": "create_dovetail",
                "arguments": {
                    "tail_id": tail_id,
                    "pin_id": pin_id,
                    "width": width,
                    "height": height,
                    "depth": depth,
                    "angle": angle,
                    "num_tails": num_tails,
                    "offset_x": offset_x,
                    "offset_y": offset_y,
                    "offset_z": offset_z
                }
            },
            request_id=ctx.request_id
        )
        
        logger.info(f"create_dovetail result: {result}")
        return json.dumps(result)
    except Exception as e:
        logger.error(f"Error in create_dovetail: {str(e)}")
        return f"Error creating dovetail joint: {str(e)}"

@mcp.tool()
def create_finger_joint(
    ctx: Context,
    board1_id: str,
    board2_id: str,
    width: float = 1.0,
    height: float = 1.0,
    depth: float = 1.0,
    num_fingers: int = 5,
    offset_x: float = 0.0,
    offset_y: float = 0.0,
    offset_z: float = 0.0
) -> str:
    """Create a finger joint (box joint) between two components"""
    try:
        logger.info(f"create_finger_joint called with board1_id={board1_id}, board2_id={board2_id}, width={width}, height={height}, depth={depth}, num_fingers={num_fingers}")
        
        sketchup = get_sketchup_connection()
        
        result = sketchup.send_command(
            method="tools/call",
            params={
                "name": "create_finger_joint",
                "arguments": {
                    "board1_id": board1_id,
                    "board2_id": board2_id,
                    "width": width,
                    "height": height,
                    "depth": depth,
                    "num_fingers": num_fingers,
                    "offset_x": offset_x,
                    "offset_y": offset_y,
                    "offset_z": offset_z
                }
            },
            request_id=ctx.request_id
        )
        
        logger.info(f"create_finger_joint result: {result}")
        return json.dumps(result)
    except Exception as e:
        logger.error(f"Error in create_finger_joint: {str(e)}")
        return f"Error creating finger joint: {str(e)}"

@mcp.tool()
def eval_ruby(
    ctx: Context,
    code: str
) -> str:
    """Evaluate arbitrary Ruby code in Sketchup"""
    try:
        logger.info(f"eval_ruby called with code length: {len(code)}")
        
        sketchup = get_sketchup_connection()
        
        result = sketchup.send_command(
            method="tools/call",
            params={
                "name": "eval_ruby",
                "arguments": {
                    "code": code
                }
            },
            request_id=ctx.request_id
        )
        
        logger.info(f"eval_ruby result: {result}")
        
        # Format the response to include the result
        response = {
            "success": True,
            "result": result.get("content", [{"text": "Success"}])[0].get("text", "Success") if isinstance(result.get("content"), list) and len(result.get("content", [])) > 0 else "Success"
        }
        
        return json.dumps(response)
    except Exception as e:
        logger.error(f"Error in eval_ruby: {str(e)}")
        return json.dumps({
            "success": False,
            "error": str(e)
        })

def main():
    mcp.run()

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

--------------------------------------------------------------------------------
/su_mcp/su_mcp/main.rb:
--------------------------------------------------------------------------------

```ruby
require 'sketchup'
require 'json'
require 'socket'
require 'fileutils'

puts "MCP Extension loading..."
SKETCHUP_CONSOLE.show rescue nil

module SU_MCP
  class Server
    def initialize
      @port = 9876
      @server = nil
      @running = false
      @timer_id = nil
      
      # Try multiple ways to show console
      begin
        SKETCHUP_CONSOLE.show
      rescue
        begin
          Sketchup.send_action("showRubyPanel:")
        rescue
          UI.start_timer(0) { SKETCHUP_CONSOLE.show }
        end
      end
    end

    def log(msg)
      begin
        SKETCHUP_CONSOLE.write("MCP: #{msg}\n")
      rescue
        puts "MCP: #{msg}"
      end
      STDOUT.flush
    end

    def start
      return if @running
      
      begin
        log "Starting server on localhost:#{@port}..."
        
        @server = TCPServer.new('127.0.0.1', @port)
        log "Server created on port #{@port}"
        
        @running = true
        
        @timer_id = UI.start_timer(0.1, true) {
          begin
            if @running
              # Check for connection
              ready = IO.select([@server], nil, nil, 0)
              if ready
                log "Connection waiting..."
                client = @server.accept_nonblock
                log "Client accepted"
                
                data = client.gets
                log "Raw data: #{data.inspect}"
                
                if data
                  begin
                    # Parse the raw JSON first to check format
                    raw_request = JSON.parse(data)
                    log "Raw parsed request: #{raw_request.inspect}"
                    
                    # Extract the original request ID if it exists in the raw data
                    original_id = nil
                    if data =~ /"id":\s*(\d+)/
                      original_id = $1.to_i
                      log "Found original request ID: #{original_id}"
                    end
                    
                    # Use the raw request directly without transforming it
                    # Just ensure the ID is preserved if it exists
                    request = raw_request
                    if !request["id"] && original_id
                      request["id"] = original_id
                      log "Added missing ID: #{original_id}"
                    end
                    
                    log "Processed request: #{request.inspect}"
                    response = handle_jsonrpc_request(request)
                    response_json = response.to_json + "\n"
                    
                    log "Sending response: #{response_json.strip}"
                    client.write(response_json)
                    client.flush
                    log "Response sent"
                  rescue JSON::ParserError => e
                    log "JSON parse error: #{e.message}"
                    error_response = {
                      jsonrpc: "2.0",
                      error: { code: -32700, message: "Parse error" },
                      id: original_id
                    }.to_json + "\n"
                    client.write(error_response)
                    client.flush
                  rescue StandardError => e
                    log "Request error: #{e.message}"
                    error_response = {
                      jsonrpc: "2.0",
                      error: { code: -32603, message: e.message },
                      id: request ? request["id"] : original_id
                    }.to_json + "\n"
                    client.write(error_response)
                    client.flush
                  end
                end
                
                client.close
                log "Client closed"
              end
            end
          rescue IO::WaitReadable
            # Normal for accept_nonblock
          rescue StandardError => e
            log "Timer error: #{e.message}"
            log e.backtrace.join("\n")
          end
        }
        
        log "Server started and listening"
        
      rescue StandardError => e
        log "Error: #{e.message}"
        log e.backtrace.join("\n")
        stop
      end
    end

    def stop
      log "Stopping server..."
      @running = false
      
      if @timer_id
        UI.stop_timer(@timer_id)
        @timer_id = nil
      end
      
      @server.close if @server
      @server = nil
      log "Server stopped"
    end

    private

    def handle_jsonrpc_request(request)
      log "Handling JSONRPC request: #{request.inspect}"
      
      # Handle direct command format (for backward compatibility)
      if request["command"]
        tool_request = {
          "method" => "tools/call",
          "params" => {
            "name" => request["command"],
            "arguments" => request["parameters"]
          },
          "jsonrpc" => request["jsonrpc"] || "2.0",
          "id" => request["id"]
        }
        log "Converting to tool request: #{tool_request.inspect}"
        return handle_tool_call(tool_request)
      end

      # Handle jsonrpc format
      case request["method"]
      when "tools/call"
        handle_tool_call(request)
      when "resources/list"
        {
          jsonrpc: request["jsonrpc"] || "2.0",
          result: { 
            resources: list_resources,
            success: true
          },
          id: request["id"]
        }
      when "prompts/list"
        {
          jsonrpc: request["jsonrpc"] || "2.0",
          result: { 
            prompts: [],
            success: true
          },
          id: request["id"]
        }
      else
        {
          jsonrpc: request["jsonrpc"] || "2.0",
          error: { 
            code: -32601, 
            message: "Method not found",
            data: { success: false }
          },
          id: request["id"]
        }
      end
    end

    def list_resources
      model = Sketchup.active_model
      return [] unless model
      
      model.entities.map do |entity|
        {
          id: entity.entityID,
          type: entity.typename.downcase
        }
      end
    end

    def handle_tool_call(request)
      log "Handling tool call: #{request.inspect}"
      tool_name = request["params"]["name"]
      args = request["params"]["arguments"]

      begin
        result = case tool_name
        when "create_component"
          create_component(args)
        when "delete_component"
          delete_component(args)
        when "transform_component"
          transform_component(args)
        when "get_selection"
          get_selection
        when "export", "export_scene"
          export_scene(args)
        when "set_material"
          set_material(args)
        when "boolean_operation"
          boolean_operation(args)
        when "chamfer_edges"
          chamfer_edges(args)
        when "fillet_edges"
          fillet_edges(args)
        when "create_mortise_tenon"
          create_mortise_tenon(args)
        when "create_dovetail"
          create_dovetail(args)
        when "create_finger_joint"
          create_finger_joint(args)
        when "eval_ruby"
          eval_ruby(args)
        else
          raise "Unknown tool: #{tool_name}"
        end

        log "Tool call result: #{result.inspect}"
        if result[:success]
          response = {
            jsonrpc: request["jsonrpc"] || "2.0",
            result: {
              content: [{ type: "text", text: result[:result] || "Success" }],
              isError: false,
              success: true,
              resourceId: result[:id]
            },
            id: request["id"]
          }
          log "Sending success response: #{response.inspect}"
          response
        else
          response = {
            jsonrpc: request["jsonrpc"] || "2.0",
            error: { 
              code: -32603, 
              message: "Operation failed",
              data: { success: false }
            },
            id: request["id"]
          }
          log "Sending error response: #{response.inspect}"
          response
        end
      rescue StandardError => e
        log "Tool call error: #{e.message}"
        response = {
          jsonrpc: request["jsonrpc"] || "2.0",
          error: { 
            code: -32603, 
            message: e.message,
            data: { success: false }
          },
          id: request["id"]
        }
        log "Sending error response: #{response.inspect}"
        response
      end
    end

    def create_component(params)
      log "Creating component with params: #{params.inspect}"
      model = Sketchup.active_model
      log "Got active model: #{model.inspect}"
      entities = model.active_entities
      log "Got active entities: #{entities.inspect}"
      
      pos = params["position"] || [0,0,0]
      dims = params["dimensions"] || [1,1,1]
      
      case params["type"]
      when "cube"
        log "Creating cube at position #{pos.inspect} with dimensions #{dims.inspect}"
        
        begin
          group = entities.add_group
          log "Created group: #{group.inspect}"
          
          face = group.entities.add_face(
            [pos[0], pos[1], pos[2]],
            [pos[0] + dims[0], pos[1], pos[2]],
            [pos[0] + dims[0], pos[1] + dims[1], pos[2]],
            [pos[0], pos[1] + dims[1], pos[2]]
          )
          log "Created face: #{face.inspect}"
          
          face.pushpull(dims[2])
          log "Pushed/pulled face by #{dims[2]}"
          
          result = { 
            id: group.entityID,
            success: true
          }
          log "Returning result: #{result.inspect}"
          result
        rescue StandardError => e
          log "Error in create_component: #{e.message}"
          log e.backtrace.join("\n")
          raise
        end
      when "cylinder"
        log "Creating cylinder at position #{pos.inspect} with dimensions #{dims.inspect}"
        
        begin
          # Create a group to contain the cylinder
          group = entities.add_group
          
          # Extract dimensions
          radius = dims[0] / 2.0
          height = dims[2]
          
          # Create a circle at the base
          center = [pos[0] + radius, pos[1] + radius, pos[2]]
          
          # Create points for a circle
          num_segments = 24  # Number of segments for the circle
          circle_points = []
          
          num_segments.times do |i|
            angle = Math::PI * 2 * i / num_segments
            x = center[0] + radius * Math.cos(angle)
            y = center[1] + radius * Math.sin(angle)
            z = center[2]
            circle_points << [x, y, z]
          end
          
          # Create the circular face
          face = group.entities.add_face(circle_points)
          
          # Extrude the face to create the cylinder
          face.pushpull(height)
          
          result = { 
            id: group.entityID,
            success: true
          }
          log "Created cylinder, returning result: #{result.inspect}"
          result
        rescue StandardError => e
          log "Error creating cylinder: #{e.message}"
          log e.backtrace.join("\n")
          raise
        end
      when "sphere"
        log "Creating sphere at position #{pos.inspect} with dimensions #{dims.inspect}"
        
        begin
          # Create a group to contain the sphere
          group = entities.add_group
          
          # Extract dimensions
          radius = dims[0] / 2.0
          center = [pos[0] + radius, pos[1] + radius, pos[2] + radius]
          
          # Use SketchUp's built-in sphere method if available
          if Sketchup::Tools.respond_to?(:create_sphere)
            Sketchup::Tools.create_sphere(center, radius, 24, group.entities)
          else
            # Fallback implementation using polygons
            # Create a UV sphere with latitude and longitude segments
            segments = 16
            
            # Create points for the sphere
            points = []
            for lat_i in 0..segments
              lat = Math::PI * lat_i / segments
              for lon_i in 0..segments
                lon = 2 * Math::PI * lon_i / segments
                x = center[0] + radius * Math.sin(lat) * Math.cos(lon)
                y = center[1] + radius * Math.sin(lat) * Math.sin(lon)
                z = center[2] + radius * Math.cos(lat)
                points << [x, y, z]
              end
            end
            
            # Create faces for the sphere (simplified approach)
            for lat_i in 0...segments
              for lon_i in 0...segments
                i1 = lat_i * (segments + 1) + lon_i
                i2 = i1 + 1
                i3 = i1 + segments + 1
                i4 = i3 + 1
                
                # Create a quad face
                begin
                  group.entities.add_face(points[i1], points[i2], points[i4], points[i3])
                rescue StandardError => e
                  # Skip faces that can't be created (may happen at poles)
                  log "Skipping face: #{e.message}"
                end
              end
            end
          end
          
          result = { 
            id: group.entityID,
            success: true
          }
          log "Created sphere, returning result: #{result.inspect}"
          result
        rescue StandardError => e
          log "Error creating sphere: #{e.message}"
          log e.backtrace.join("\n")
          raise
        end
      when "cone"
        log "Creating cone at position #{pos.inspect} with dimensions #{dims.inspect}"
        
        begin
          # Create a group to contain the cone
          group = entities.add_group
          
          # Extract dimensions
          radius = dims[0] / 2.0
          height = dims[2]
          
          # Create a circle at the base
          center = [pos[0] + radius, pos[1] + radius, pos[2]]
          apex = [center[0], center[1], center[2] + height]
          
          # Create points for a circle
          num_segments = 24  # Number of segments for the circle
          circle_points = []
          
          num_segments.times do |i|
            angle = Math::PI * 2 * i / num_segments
            x = center[0] + radius * Math.cos(angle)
            y = center[1] + radius * Math.sin(angle)
            z = center[2]
            circle_points << [x, y, z]
          end
          
          # Create the circular face for the base
          base = group.entities.add_face(circle_points)
          
          # Create the cone sides
          (0...num_segments).each do |i|
            j = (i + 1) % num_segments
            # Create a triangular face from two adjacent points on the circle to the apex
            group.entities.add_face(circle_points[i], circle_points[j], apex)
          end
          
          result = { 
            id: group.entityID,
            success: true
          }
          log "Created cone, returning result: #{result.inspect}"
          result
        rescue StandardError => e
          log "Error creating cone: #{e.message}"
          log e.backtrace.join("\n")
          raise
        end
      else
        raise "Unknown component type: #{params["type"]}"
      end
    end

    def delete_component(params)
      model = Sketchup.active_model
      
      # Handle ID format - strip quotes if present
      id_str = params["id"].to_s.gsub('"', '')
      log "Looking for entity with ID: #{id_str}"
      
      entity = model.find_entity_by_id(id_str.to_i)
      
      if entity
        log "Found entity: #{entity.inspect}"
        entity.erase!
        { success: true }
      else
        raise "Entity not found"
      end
    end

    def transform_component(params)
      model = Sketchup.active_model
      
      # Handle ID format - strip quotes if present
      id_str = params["id"].to_s.gsub('"', '')
      log "Looking for entity with ID: #{id_str}"
      
      entity = model.find_entity_by_id(id_str.to_i)
      
      if entity
        log "Found entity: #{entity.inspect}"
        
        # Handle position
        if params["position"]
          pos = params["position"]
          log "Transforming position to #{pos.inspect}"
          
          # Create a transformation to move the entity
          translation = Geom::Transformation.translation(Geom::Point3d.new(pos[0], pos[1], pos[2]))
          entity.transform!(translation)
        end
        
        # Handle rotation (in degrees)
        if params["rotation"]
          rot = params["rotation"]
          log "Rotating by #{rot.inspect} degrees"
          
          # Convert to radians
          x_rot = rot[0] * Math::PI / 180
          y_rot = rot[1] * Math::PI / 180
          z_rot = rot[2] * Math::PI / 180
          
          # Apply rotations
          if rot[0] != 0
            rotation = Geom::Transformation.rotation(entity.bounds.center, Geom::Vector3d.new(1, 0, 0), x_rot)
            entity.transform!(rotation)
          end
          
          if rot[1] != 0
            rotation = Geom::Transformation.rotation(entity.bounds.center, Geom::Vector3d.new(0, 1, 0), y_rot)
            entity.transform!(rotation)
          end
          
          if rot[2] != 0
            rotation = Geom::Transformation.rotation(entity.bounds.center, Geom::Vector3d.new(0, 0, 1), z_rot)
            entity.transform!(rotation)
          end
        end
        
        # Handle scale
        if params["scale"]
          scale = params["scale"]
          log "Scaling by #{scale.inspect}"
          
          # Create a transformation to scale the entity
          center = entity.bounds.center
          scaling = Geom::Transformation.scaling(center, scale[0], scale[1], scale[2])
          entity.transform!(scaling)
        end
        
        { success: true, id: entity.entityID }
      else
        raise "Entity not found"
      end
    end

    def get_selection
      model = Sketchup.active_model
      selection = model.selection
      
      log "Getting selection, count: #{selection.length}"
      
      selected_entities = selection.map do |entity|
        {
          id: entity.entityID,
          type: entity.typename.downcase
        }
      end
      
      { success: true, entities: selected_entities }
    end
    
    def export_scene(params)
      log "Exporting scene with params: #{params.inspect}"
      model = Sketchup.active_model
      
      format = params["format"] || "skp"
      
      begin
        # Create a temporary directory for exports
        temp_dir = File.join(ENV['TEMP'] || ENV['TMP'] || Dir.tmpdir, "sketchup_exports")
        FileUtils.mkdir_p(temp_dir) unless Dir.exist?(temp_dir)
        
        # Generate a unique filename
        timestamp = Time.now.strftime("%Y%m%d_%H%M%S")
        filename = "sketchup_export_#{timestamp}"
        
        case format.downcase
        when "skp"
          # Export as SketchUp file
          export_path = File.join(temp_dir, "#{filename}.skp")
          log "Exporting to SketchUp file: #{export_path}"
          model.save(export_path)
          
        when "obj"
          # Export as OBJ file
          export_path = File.join(temp_dir, "#{filename}.obj")
          log "Exporting to OBJ file: #{export_path}"
          
          # Check if OBJ exporter is available
          if Sketchup.require("sketchup.rb")
            options = {
              :triangulated_faces => true,
              :double_sided_faces => true,
              :edges => false,
              :texture_maps => true
            }
            model.export(export_path, options)
          else
            raise "OBJ exporter not available"
          end
          
        when "dae"
          # Export as COLLADA file
          export_path = File.join(temp_dir, "#{filename}.dae")
          log "Exporting to COLLADA file: #{export_path}"
          
          # Check if COLLADA exporter is available
          if Sketchup.require("sketchup.rb")
            options = { :triangulated_faces => true }
            model.export(export_path, options)
          else
            raise "COLLADA exporter not available"
          end
          
        when "stl"
          # Export as STL file
          export_path = File.join(temp_dir, "#{filename}.stl")
          log "Exporting to STL file: #{export_path}"
          
          # Check if STL exporter is available
          if Sketchup.require("sketchup.rb")
            options = { :units => "model" }
            model.export(export_path, options)
          else
            raise "STL exporter not available"
          end
          
        when "png", "jpg", "jpeg"
          # Export as image
          ext = format.downcase == "jpg" ? "jpeg" : format.downcase
          export_path = File.join(temp_dir, "#{filename}.#{ext}")
          log "Exporting to image file: #{export_path}"
          
          # Get the current view
          view = model.active_view
          
          # Set up options for the export
          options = {
            :filename => export_path,
            :width => params["width"] || 1920,
            :height => params["height"] || 1080,
            :antialias => true,
            :transparent => (ext == "png")
          }
          
          # Export the image
          view.write_image(options)
          
        else
          raise "Unsupported export format: #{format}"
        end
        
        log "Export completed successfully to: #{export_path}"
        
        { 
          success: true, 
          path: export_path,
          format: format
        }
      rescue StandardError => e
        log "Error in export_scene: #{e.message}"
        log e.backtrace.join("\n")
        raise
      end
    end
    
    def set_material(params)
      log "Setting material with params: #{params.inspect}"
      model = Sketchup.active_model
      
      # Handle ID format - strip quotes if present
      id_str = params["id"].to_s.gsub('"', '')
      log "Looking for entity with ID: #{id_str}"
      
      entity = model.find_entity_by_id(id_str.to_i)
      
      if entity
        log "Found entity: #{entity.inspect}"
        
        material_name = params["material"]
        log "Setting material to: #{material_name}"
        
        # Get or create the material
        material = model.materials[material_name]
        if !material
          # Create a new material if it doesn't exist
          material = model.materials.add(material_name)
          
          # Handle color specification
          case material_name.downcase
          when "red"
            material.color = Sketchup::Color.new(255, 0, 0)
          when "green"
            material.color = Sketchup::Color.new(0, 255, 0)
          when "blue"
            material.color = Sketchup::Color.new(0, 0, 255)
          when "yellow"
            material.color = Sketchup::Color.new(255, 255, 0)
          when "cyan", "turquoise"
            material.color = Sketchup::Color.new(0, 255, 255)
          when "magenta", "purple"
            material.color = Sketchup::Color.new(255, 0, 255)
          when "white"
            material.color = Sketchup::Color.new(255, 255, 255)
          when "black"
            material.color = Sketchup::Color.new(0, 0, 0)
          when "brown"
            material.color = Sketchup::Color.new(139, 69, 19)
          when "orange"
            material.color = Sketchup::Color.new(255, 165, 0)
          when "gray", "grey"
            material.color = Sketchup::Color.new(128, 128, 128)
          else
            # If it's a hex color code like "#FF0000"
            if material_name.start_with?("#") && material_name.length == 7
              begin
                r = material_name[1..2].to_i(16)
                g = material_name[3..4].to_i(16)
                b = material_name[5..6].to_i(16)
                material.color = Sketchup::Color.new(r, g, b)
              rescue
                # Default to a wood color if parsing fails
                material.color = Sketchup::Color.new(184, 134, 72)
              end
            else
              # Default to a wood color
              material.color = Sketchup::Color.new(184, 134, 72)
            end
          end
        end
        
        # Apply the material to the entity
        if entity.respond_to?(:material=)
          entity.material = material
        elsif entity.is_a?(Sketchup::Group) || entity.is_a?(Sketchup::ComponentInstance)
          # For groups and components, we need to apply to all faces
          entities = entity.is_a?(Sketchup::Group) ? entity.entities : entity.definition.entities
          entities.grep(Sketchup::Face).each { |face| face.material = material }
        end
        
        { success: true, id: entity.entityID }
      else
        raise "Entity not found"
      end
    end
    
    def boolean_operation(params)
      log "Performing boolean operation with params: #{params.inspect}"
      model = Sketchup.active_model
      
      # Get operation type
      operation_type = params["operation"]
      unless ["union", "difference", "intersection"].include?(operation_type)
        raise "Invalid boolean operation: #{operation_type}. Must be 'union', 'difference', or 'intersection'."
      end
      
      # Get target and tool entities
      target_id = params["target_id"].to_s.gsub('"', '')
      tool_id = params["tool_id"].to_s.gsub('"', '')
      
      log "Looking for target entity with ID: #{target_id}"
      target_entity = model.find_entity_by_id(target_id.to_i)
      
      log "Looking for tool entity with ID: #{tool_id}"
      tool_entity = model.find_entity_by_id(tool_id.to_i)
      
      unless target_entity && tool_entity
        missing = []
        missing << "target" unless target_entity
        missing << "tool" unless tool_entity
        raise "Entity not found: #{missing.join(', ')}"
      end
      
      # Ensure both entities are groups or component instances
      unless (target_entity.is_a?(Sketchup::Group) || target_entity.is_a?(Sketchup::ComponentInstance)) &&
             (tool_entity.is_a?(Sketchup::Group) || tool_entity.is_a?(Sketchup::ComponentInstance))
        raise "Boolean operations require groups or component instances"
      end
      
      # Create a new group to hold the result
      result_group = model.active_entities.add_group
      
      # Perform the boolean operation
      case operation_type
      when "union"
        log "Performing union operation"
        perform_union(target_entity, tool_entity, result_group)
      when "difference"
        log "Performing difference operation"
        perform_difference(target_entity, tool_entity, result_group)
      when "intersection"
        log "Performing intersection operation"
        perform_intersection(target_entity, tool_entity, result_group)
      end
      
      # Clean up original entities if requested
      if params["delete_originals"]
        target_entity.erase! if target_entity.valid?
        tool_entity.erase! if tool_entity.valid?
      end
      
      # Return the result
      { 
        success: true, 
        id: result_group.entityID
      }
    end
    
    def perform_union(target, tool, result_group)
      model = Sketchup.active_model
      
      # Create temporary copies of the target and tool
      target_copy = target.copy
      tool_copy = tool.copy
      
      # Get the transformation of each entity
      target_transform = target.transformation
      tool_transform = tool.transformation
      
      # Apply the transformations to the copies
      target_copy.transform!(target_transform)
      tool_copy.transform!(tool_transform)
      
      # Get the entities from the copies
      target_entities = target_copy.is_a?(Sketchup::Group) ? target_copy.entities : target_copy.definition.entities
      tool_entities = tool_copy.is_a?(Sketchup::Group) ? tool_copy.entities : tool_copy.definition.entities
      
      # Copy all entities from target to result
      target_entities.each do |entity|
        entity.copy(result_group.entities)
      end
      
      # Copy all entities from tool to result
      tool_entities.each do |entity|
        entity.copy(result_group.entities)
      end
      
      # Clean up temporary copies
      target_copy.erase!
      tool_copy.erase!
      
      # Outer shell - this will merge overlapping geometry
      result_group.entities.outer_shell
    end
    
    def perform_difference(target, tool, result_group)
      model = Sketchup.active_model
      
      # Create temporary copies of the target and tool
      target_copy = target.copy
      tool_copy = tool.copy
      
      # Get the transformation of each entity
      target_transform = target.transformation
      tool_transform = tool.transformation
      
      # Apply the transformations to the copies
      target_copy.transform!(target_transform)
      tool_copy.transform!(tool_transform)
      
      # Get the entities from the copies
      target_entities = target_copy.is_a?(Sketchup::Group) ? target_copy.entities : target_copy.definition.entities
      tool_entities = tool_copy.is_a?(Sketchup::Group) ? tool_copy.entities : tool_copy.definition.entities
      
      # Copy all entities from target to result
      target_entities.each do |entity|
        entity.copy(result_group.entities)
      end
      
      # Create a temporary group for the tool
      temp_tool_group = model.active_entities.add_group
      
      # Copy all entities from tool to temp group
      tool_entities.each do |entity|
        entity.copy(temp_tool_group.entities)
      end
      
      # Subtract the tool from the result
      result_group.entities.subtract(temp_tool_group.entities)
      
      # Clean up temporary copies and groups
      target_copy.erase!
      tool_copy.erase!
      temp_tool_group.erase!
    end
    
    def perform_intersection(target, tool, result_group)
      model = Sketchup.active_model
      
      # Create temporary copies of the target and tool
      target_copy = target.copy
      tool_copy = tool.copy
      
      # Get the transformation of each entity
      target_transform = target.transformation
      tool_transform = tool.transformation
      
      # Apply the transformations to the copies
      target_copy.transform!(target_transform)
      tool_copy.transform!(tool_transform)
      
      # Get the entities from the copies
      target_entities = target_copy.is_a?(Sketchup::Group) ? target_copy.entities : target_copy.definition.entities
      tool_entities = tool_copy.is_a?(Sketchup::Group) ? tool_copy.entities : tool_copy.definition.entities
      
      # Create temporary groups for target and tool
      temp_target_group = model.active_entities.add_group
      temp_tool_group = model.active_entities.add_group
      
      # Copy all entities from target and tool to temp groups
      target_entities.each do |entity|
        entity.copy(temp_target_group.entities)
      end
      
      tool_entities.each do |entity|
        entity.copy(temp_tool_group.entities)
      end
      
      # Perform the intersection
      result_group.entities.intersect_with(temp_target_group.entities, temp_tool_group.entities)
      
      # Clean up temporary copies and groups
      target_copy.erase!
      tool_copy.erase!
      temp_target_group.erase!
      temp_tool_group.erase!
    end
    
    def chamfer_edges(params)
      log "Chamfering edges with params: #{params.inspect}"
      model = Sketchup.active_model
      
      # Get entity ID
      entity_id = params["entity_id"].to_s.gsub('"', '')
      log "Looking for entity with ID: #{entity_id}"
      
      entity = model.find_entity_by_id(entity_id.to_i)
      unless entity
        raise "Entity not found: #{entity_id}"
      end
      
      # Ensure entity is a group or component instance
      unless entity.is_a?(Sketchup::Group) || entity.is_a?(Sketchup::ComponentInstance)
        raise "Chamfer operation requires a group or component instance"
      end
      
      # Get the distance parameter
      distance = params["distance"] || 0.5
      
      # Get the entities collection
      entities = entity.is_a?(Sketchup::Group) ? entity.entities : entity.definition.entities
      
      # Find all edges in the entity
      edges = entities.grep(Sketchup::Edge)
      
      # If specific edges are provided, filter the edges
      if params["edge_indices"] && params["edge_indices"].is_a?(Array)
        edge_indices = params["edge_indices"]
        edges = edges.select.with_index { |_, i| edge_indices.include?(i) }
      end
      
      # Create a new group to hold the result
      result_group = model.active_entities.add_group
      
      # Copy all entities from the original to the result
      entities.each do |e|
        e.copy(result_group.entities)
      end
      
      # Get the edges in the result group
      result_edges = result_group.entities.grep(Sketchup::Edge)
      
      # If specific edges were provided, filter the result edges
      if params["edge_indices"] && params["edge_indices"].is_a?(Array)
        edge_indices = params["edge_indices"]
        result_edges = result_edges.select.with_index { |_, i| edge_indices.include?(i) }
      end
      
      # Perform the chamfer operation
      begin
        # Create a transformation for the chamfer
        chamfer_transform = Geom::Transformation.scaling(1.0 - distance)
        
        # For each edge, create a chamfer
        result_edges.each do |edge|
          # Get the faces connected to this edge
          faces = edge.faces
          next if faces.length < 2
          
          # Get the start and end points of the edge
          start_point = edge.start.position
          end_point = edge.end.position
          
          # Calculate the midpoint of the edge
          midpoint = Geom::Point3d.new(
            (start_point.x + end_point.x) / 2.0,
            (start_point.y + end_point.y) / 2.0,
            (start_point.z + end_point.z) / 2.0
          )
          
          # Create a chamfer by creating a new face
          # This is a simplified approach - in a real implementation,
          # you would need to handle various edge cases
          new_points = []
          
          # For each vertex of the edge
          [edge.start, edge.end].each do |vertex|
            # Get all edges connected to this vertex
            connected_edges = vertex.edges - [edge]
            
            # For each connected edge
            connected_edges.each do |connected_edge|
              # Get the other vertex of the connected edge
              other_vertex = (connected_edge.vertices - [vertex])[0]
              
              # Calculate a point along the connected edge
              direction = other_vertex.position - vertex.position
              new_point = vertex.position.offset(direction, distance)
              
              new_points << new_point
            end
          end
          
          # Create a new face using the new points
          if new_points.length >= 3
            result_group.entities.add_face(new_points)
          end
        end
        
        # Clean up the original entity if requested
        if params["delete_original"]
          entity.erase! if entity.valid?
        end
        
        # Return the result
        { 
          success: true, 
          id: result_group.entityID
        }
      rescue StandardError => e
        log "Error in chamfer_edges: #{e.message}"
        log e.backtrace.join("\n")
        
        # Clean up the result group if there was an error
        result_group.erase! if result_group.valid?
        
        raise
      end
    end
    
    def fillet_edges(params)
      log "Filleting edges with params: #{params.inspect}"
      model = Sketchup.active_model
      
      # Get entity ID
      entity_id = params["entity_id"].to_s.gsub('"', '')
      log "Looking for entity with ID: #{entity_id}"
      
      entity = model.find_entity_by_id(entity_id.to_i)
      unless entity
        raise "Entity not found: #{entity_id}"
      end
      
      # Ensure entity is a group or component instance
      unless entity.is_a?(Sketchup::Group) || entity.is_a?(Sketchup::ComponentInstance)
        raise "Fillet operation requires a group or component instance"
      end
      
      # Get the radius parameter
      radius = params["radius"] || 0.5
      
      # Get the number of segments for the fillet
      segments = params["segments"] || 8
      
      # Get the entities collection
      entities = entity.is_a?(Sketchup::Group) ? entity.entities : entity.definition.entities
      
      # Find all edges in the entity
      edges = entities.grep(Sketchup::Edge)
      
      # If specific edges are provided, filter the edges
      if params["edge_indices"] && params["edge_indices"].is_a?(Array)
        edge_indices = params["edge_indices"]
        edges = edges.select.with_index { |_, i| edge_indices.include?(i) }
      end
      
      # Create a new group to hold the result
      result_group = model.active_entities.add_group
      
      # Copy all entities from the original to the result
      entities.each do |e|
        e.copy(result_group.entities)
      end
      
      # Get the edges in the result group
      result_edges = result_group.entities.grep(Sketchup::Edge)
      
      # If specific edges were provided, filter the result edges
      if params["edge_indices"] && params["edge_indices"].is_a?(Array)
        edge_indices = params["edge_indices"]
        result_edges = result_edges.select.with_index { |_, i| edge_indices.include?(i) }
      end
      
      # Perform the fillet operation
      begin
        # For each edge, create a fillet
        result_edges.each do |edge|
          # Get the faces connected to this edge
          faces = edge.faces
          next if faces.length < 2
          
          # Get the start and end points of the edge
          start_point = edge.start.position
          end_point = edge.end.position
          
          # Calculate the midpoint of the edge
          midpoint = Geom::Point3d.new(
            (start_point.x + end_point.x) / 2.0,
            (start_point.y + end_point.y) / 2.0,
            (start_point.z + end_point.z) / 2.0
          )
          
          # Calculate the edge vector
          edge_vector = end_point - start_point
          edge_length = edge_vector.length
          
          # Create points for the fillet curve
          fillet_points = []
          
          # Create a series of points along a circular arc
          (0..segments).each do |i|
            angle = Math::PI * i / segments
            
            # Calculate the point on the arc
            x = midpoint.x + radius * Math.cos(angle)
            y = midpoint.y + radius * Math.sin(angle)
            z = midpoint.z
            
            fillet_points << Geom::Point3d.new(x, y, z)
          end
          
          # Create edges connecting the fillet points
          (0...fillet_points.length - 1).each do |i|
            result_group.entities.add_line(fillet_points[i], fillet_points[i+1])
          end
          
          # Create a face from the fillet points
          if fillet_points.length >= 3
            result_group.entities.add_face(fillet_points)
          end
        end
        
        # Clean up the original entity if requested
        if params["delete_original"]
          entity.erase! if entity.valid?
        end
        
        # Return the result
        { 
          success: true, 
          id: result_group.entityID
        }
      rescue StandardError => e
        log "Error in fillet_edges: #{e.message}"
        log e.backtrace.join("\n")
        
        # Clean up the result group if there was an error
        result_group.erase! if result_group.valid?
        
        raise
      end
    end
    
    def create_mortise_tenon(params)
      log "Creating mortise and tenon joint with params: #{params.inspect}"
      model = Sketchup.active_model
      
      # Get the mortise and tenon board IDs
      mortise_id = params["mortise_id"].to_s.gsub('"', '')
      tenon_id = params["tenon_id"].to_s.gsub('"', '')
      
      log "Looking for mortise board with ID: #{mortise_id}"
      mortise_board = model.find_entity_by_id(mortise_id.to_i)
      
      log "Looking for tenon board with ID: #{tenon_id}"
      tenon_board = model.find_entity_by_id(tenon_id.to_i)
      
      unless mortise_board && tenon_board
        missing = []
        missing << "mortise board" unless mortise_board
        missing << "tenon board" unless tenon_board
        raise "Entity not found: #{missing.join(', ')}"
      end
      
      # Ensure both entities are groups or component instances
      unless (mortise_board.is_a?(Sketchup::Group) || mortise_board.is_a?(Sketchup::ComponentInstance)) &&
             (tenon_board.is_a?(Sketchup::Group) || tenon_board.is_a?(Sketchup::ComponentInstance))
        raise "Mortise and tenon operation requires groups or component instances"
      end
      
      # Get joint parameters
      width = params["width"] || 1.0
      height = params["height"] || 1.0
      depth = params["depth"] || 1.0
      offset_x = params["offset_x"] || 0.0
      offset_y = params["offset_y"] || 0.0
      offset_z = params["offset_z"] || 0.0
      
      # Get the bounds of both boards
      mortise_bounds = mortise_board.bounds
      tenon_bounds = tenon_board.bounds
      
      # Determine the face to place the joint on based on the relative positions of the boards
      mortise_center = mortise_bounds.center
      tenon_center = tenon_bounds.center
      
      # Calculate the direction vector from mortise to tenon
      direction_vector = tenon_center - mortise_center
      
      # Determine which face of the mortise board is closest to the tenon board
      mortise_face_direction = determine_closest_face(direction_vector)
      
      # Create the mortise (hole) in the mortise board
      mortise_result = create_mortise(
        mortise_board, 
        width, 
        height, 
        depth, 
        mortise_face_direction,
        mortise_bounds,
        offset_x, 
        offset_y, 
        offset_z
      )
      
      # Determine which face of the tenon board is closest to the mortise board
      tenon_face_direction = determine_closest_face(direction_vector.reverse)
      
      # Create the tenon (projection) on the tenon board
      tenon_result = create_tenon(
        tenon_board, 
        width, 
        height, 
        depth, 
        tenon_face_direction,
        tenon_bounds,
        offset_x, 
        offset_y, 
        offset_z
      )
      
      # Return the result
      { 
        success: true, 
        mortise_id: mortise_result[:id],
        tenon_id: tenon_result[:id]
      }
    end
    
    def determine_closest_face(direction_vector)
      # Normalize the direction vector
      direction_vector.normalize!
      
      # Determine which axis has the largest component
      x_abs = direction_vector.x.abs
      y_abs = direction_vector.y.abs
      z_abs = direction_vector.z.abs
      
      if x_abs >= y_abs && x_abs >= z_abs
        # X-axis is dominant
        return direction_vector.x > 0 ? :east : :west
      elsif y_abs >= x_abs && y_abs >= z_abs
        # Y-axis is dominant
        return direction_vector.y > 0 ? :north : :south
      else
        # Z-axis is dominant
        return direction_vector.z > 0 ? :top : :bottom
      end
    end
    
    def create_mortise(board, width, height, depth, face_direction, bounds, offset_x, offset_y, offset_z)
      model = Sketchup.active_model
      
      # Get the board's entities
      entities = board.is_a?(Sketchup::Group) ? board.entities : board.definition.entities
      
      # Calculate the position of the mortise based on the face direction
      mortise_position = calculate_position_on_face(face_direction, bounds, width, height, depth, offset_x, offset_y, offset_z)
      
      log "Creating mortise at position: #{mortise_position.inspect} with dimensions: #{[width, height, depth].inspect}"
      
      # Create a box for the mortise
      mortise_group = entities.add_group
      
      # Create the mortise box with the correct orientation
      case face_direction
      when :east, :west
        # Mortise on east or west face (YZ plane)
        mortise_face = mortise_group.entities.add_face(
          [mortise_position[0], mortise_position[1], mortise_position[2]],
          [mortise_position[0], mortise_position[1] + width, mortise_position[2]],
          [mortise_position[0], mortise_position[1] + width, mortise_position[2] + height],
          [mortise_position[0], mortise_position[1], mortise_position[2] + height]
        )
        mortise_face.pushpull(face_direction == :east ? -depth : depth)
      when :north, :south
        # Mortise on north or south face (XZ plane)
        mortise_face = mortise_group.entities.add_face(
          [mortise_position[0], mortise_position[1], mortise_position[2]],
          [mortise_position[0] + width, mortise_position[1], mortise_position[2]],
          [mortise_position[0] + width, mortise_position[1], mortise_position[2] + height],
          [mortise_position[0], mortise_position[1], mortise_position[2] + height]
        )
        mortise_face.pushpull(face_direction == :north ? -depth : depth)
      when :top, :bottom
        # Mortise on top or bottom face (XY plane)
        mortise_face = mortise_group.entities.add_face(
          [mortise_position[0], mortise_position[1], mortise_position[2]],
          [mortise_position[0] + width, mortise_position[1], mortise_position[2]],
          [mortise_position[0] + width, mortise_position[1] + height, mortise_position[2]],
          [mortise_position[0], mortise_position[1] + height, mortise_position[2]]
        )
        mortise_face.pushpull(face_direction == :top ? -depth : depth)
      end
      
      # Subtract the mortise from the board
      entities.subtract(mortise_group.entities)
      
      # Clean up the temporary group
      mortise_group.erase!
      
      # Return the result
      { 
        success: true, 
        id: board.entityID
      }
    end
    
    def create_tenon(board, width, height, depth, face_direction, bounds, offset_x, offset_y, offset_z)
      model = Sketchup.active_model
      
      # Get the board's entities
      entities = board.is_a?(Sketchup::Group) ? board.entities : board.definition.entities
      
      # Calculate the position of the tenon based on the face direction
      tenon_position = calculate_position_on_face(face_direction, bounds, width, height, depth, offset_x, offset_y, offset_z)
      
      log "Creating tenon at position: #{tenon_position.inspect} with dimensions: #{[width, height, depth].inspect}"
      
      # Create a box for the tenon
      tenon_group = model.active_entities.add_group
      
      # Create the tenon box with the correct orientation
      case face_direction
      when :east, :west
        # Tenon on east or west face (YZ plane)
        tenon_face = tenon_group.entities.add_face(
          [tenon_position[0], tenon_position[1], tenon_position[2]],
          [tenon_position[0], tenon_position[1] + width, tenon_position[2]],
          [tenon_position[0], tenon_position[1] + width, tenon_position[2] + height],
          [tenon_position[0], tenon_position[1], tenon_position[2] + height]
        )
        tenon_face.pushpull(face_direction == :east ? depth : -depth)
      when :north, :south
        # Tenon on north or south face (XZ plane)
        tenon_face = tenon_group.entities.add_face(
          [tenon_position[0], tenon_position[1], tenon_position[2]],
          [tenon_position[0] + width, tenon_position[1], tenon_position[2]],
          [tenon_position[0] + width, tenon_position[1], tenon_position[2] + height],
          [tenon_position[0], tenon_position[1], tenon_position[2] + height]
        )
        tenon_face.pushpull(face_direction == :north ? depth : -depth)
      when :top, :bottom
        # Tenon on top or bottom face (XY plane)
        tenon_face = tenon_group.entities.add_face(
          [tenon_position[0], tenon_position[1], tenon_position[2]],
          [tenon_position[0] + width, tenon_position[1], tenon_position[2]],
          [tenon_position[0] + width, tenon_position[1] + height, tenon_position[2]],
          [tenon_position[0], tenon_position[1] + height, tenon_position[2]]
        )
        tenon_face.pushpull(face_direction == :top ? depth : -depth)
      end
      
      # Get the transformation of the board
      board_transform = board.transformation
      
      # Apply the inverse transformation to the tenon group
      tenon_group.transform!(board_transform.inverse)
      
      # Union the tenon with the board
      board_entities = board.is_a?(Sketchup::Group) ? board.entities : board.definition.entities
      board_entities.add_instance(tenon_group.entities.parent, Geom::Transformation.new)
      
      # Clean up the temporary group
      tenon_group.erase!
      
      # Return the result
      { 
        success: true, 
        id: board.entityID
      }
    end
    
    def calculate_position_on_face(face_direction, bounds, width, height, depth, offset_x, offset_y, offset_z)
      # Calculate the position on the specified face with offsets
      case face_direction
      when :east
        # Position on the east face (max X)
        [
          bounds.max.x,
          bounds.center.y - width/2 + offset_y,
          bounds.center.z - height/2 + offset_z
        ]
      when :west
        # Position on the west face (min X)
        [
          bounds.min.x,
          bounds.center.y - width/2 + offset_y,
          bounds.center.z - height/2 + offset_z
        ]
      when :north
        # Position on the north face (max Y)
        [
          bounds.center.x - width/2 + offset_x,
          bounds.max.y,
          bounds.center.z - height/2 + offset_z
        ]
      when :south
        # Position on the south face (min Y)
        [
          bounds.center.x - width/2 + offset_x,
          bounds.min.y,
          bounds.center.z - height/2 + offset_z
        ]
      when :top
        # Position on the top face (max Z)
        [
          bounds.center.x - width/2 + offset_x,
          bounds.center.y - height/2 + offset_y,
          bounds.max.z
        ]
      when :bottom
        # Position on the bottom face (min Z)
        [
          bounds.center.x - width/2 + offset_x,
          bounds.center.y - height/2 + offset_y,
          bounds.min.z
        ]
      end
    end
    
    def create_dovetail(params)
      log "Creating dovetail joint with params: #{params.inspect}"
      model = Sketchup.active_model
      
      # Get the tail and pin board IDs
      tail_id = params["tail_id"].to_s.gsub('"', '')
      pin_id = params["pin_id"].to_s.gsub('"', '')
      
      log "Looking for tail board with ID: #{tail_id}"
      tail_board = model.find_entity_by_id(tail_id.to_i)
      
      log "Looking for pin board with ID: #{pin_id}"
      pin_board = model.find_entity_by_id(pin_id.to_i)
      
      unless tail_board && pin_board
        missing = []
        missing << "tail board" unless tail_board
        missing << "pin board" unless pin_board
        raise "Entity not found: #{missing.join(', ')}"
      end
      
      # Ensure both entities are groups or component instances
      unless (tail_board.is_a?(Sketchup::Group) || tail_board.is_a?(Sketchup::ComponentInstance)) &&
             (pin_board.is_a?(Sketchup::Group) || pin_board.is_a?(Sketchup::ComponentInstance))
        raise "Dovetail operation requires groups or component instances"
      end
      
      # Get joint parameters
      width = params["width"] || 1.0
      height = params["height"] || 2.0
      depth = params["depth"] || 1.0
      angle = params["angle"] || 15.0  # Dovetail angle in degrees
      num_tails = params["num_tails"] || 3
      offset_x = params["offset_x"] || 0.0
      offset_y = params["offset_y"] || 0.0
      offset_z = params["offset_z"] || 0.0
      
      # Create the tails on the tail board
      tail_result = create_tails(tail_board, width, height, depth, angle, num_tails, offset_x, offset_y, offset_z)
      
      # Create the pins on the pin board
      pin_result = create_pins(pin_board, width, height, depth, angle, num_tails, offset_x, offset_y, offset_z)
      
      # Return the result
      { 
        success: true, 
        tail_id: tail_result[:id],
        pin_id: pin_result[:id]
      }
    end
    
    def create_tails(board, width, height, depth, angle, num_tails, offset_x, offset_y, offset_z)
      model = Sketchup.active_model
      
      # Get the board's entities
      entities = board.is_a?(Sketchup::Group) ? board.entities : board.definition.entities
      
      # Get the board's bounds
      bounds = board.bounds
      
      # Calculate the position of the dovetail joint
      center_x = bounds.center.x + offset_x
      center_y = bounds.center.y + offset_y
      center_z = bounds.center.z + offset_z
      
      # Calculate the width of each tail and space
      total_width = width
      tail_width = total_width / (2 * num_tails - 1)
      
      # Create a group for the tails
      tails_group = entities.add_group
      
      # Create each tail
      num_tails.times do |i|
        # Calculate the position of this tail
        tail_center_x = center_x - width/2 + tail_width * (2 * i)
        
        # Calculate the dovetail shape
        angle_rad = angle * Math::PI / 180.0
        tail_top_width = tail_width
        tail_bottom_width = tail_width + 2 * depth * Math.tan(angle_rad)
        
        # Create the tail shape
        tail_points = [
          [tail_center_x - tail_top_width/2, center_y - height/2, center_z],
          [tail_center_x + tail_top_width/2, center_y - height/2, center_z],
          [tail_center_x + tail_bottom_width/2, center_y - height/2, center_z - depth],
          [tail_center_x - tail_bottom_width/2, center_y - height/2, center_z - depth]
        ]
        
        # Create the tail face
        tail_face = tails_group.entities.add_face(tail_points)
        
        # Extrude the tail
        tail_face.pushpull(height)
      end
      
      # Return the result
      { 
        success: true, 
        id: board.entityID
      }
    end
    
    def create_pins(board, width, height, depth, angle, num_tails, offset_x, offset_y, offset_z)
      model = Sketchup.active_model
      
      # Get the board's entities
      entities = board.is_a?(Sketchup::Group) ? board.entities : board.definition.entities
      
      # Get the board's bounds
      bounds = board.bounds
      
      # Calculate the position of the dovetail joint
      center_x = bounds.center.x + offset_x
      center_y = bounds.center.y + offset_y
      center_z = bounds.center.z + offset_z
      
      # Calculate the width of each tail and space
      total_width = width
      tail_width = total_width / (2 * num_tails - 1)
      
      # Create a group for the pins
      pins_group = entities.add_group
      
      # Create a box for the entire pin area
      pin_area_face = pins_group.entities.add_face(
        [center_x - width/2, center_y - height/2, center_z],
        [center_x + width/2, center_y - height/2, center_z],
        [center_x + width/2, center_y + height/2, center_z],
        [center_x - width/2, center_y + height/2, center_z]
      )
      
      # Extrude the pin area
      pin_area_face.pushpull(depth)
      
      # Create each tail cutout
      num_tails.times do |i|
        # Calculate the position of this tail
        tail_center_x = center_x - width/2 + tail_width * (2 * i)
        
        # Calculate the dovetail shape
        angle_rad = angle * Math::PI / 180.0
        tail_top_width = tail_width
        tail_bottom_width = tail_width + 2 * depth * Math.tan(angle_rad)
        
        # Create a group for the tail cutout
        tail_cutout_group = entities.add_group
        
        # Create the tail cutout shape
        tail_points = [
          [tail_center_x - tail_top_width/2, center_y - height/2, center_z],
          [tail_center_x + tail_top_width/2, center_y - height/2, center_z],
          [tail_center_x + tail_bottom_width/2, center_y - height/2, center_z - depth],
          [tail_center_x - tail_bottom_width/2, center_y - height/2, center_z - depth]
        ]
        
        # Create the tail cutout face
        tail_face = tail_cutout_group.entities.add_face(tail_points)
        
        # Extrude the tail cutout
        tail_face.pushpull(height)
        
        # Subtract the tail cutout from the pin area
        pins_group.entities.subtract(tail_cutout_group.entities)
        
        # Clean up the temporary group
        tail_cutout_group.erase!
      end
      
      # Return the result
      { 
        success: true, 
        id: board.entityID
      }
    end
    
    def create_finger_joint(params)
      log "Creating finger joint with params: #{params.inspect}"
      model = Sketchup.active_model
      
      # Get the two board IDs
      board1_id = params["board1_id"].to_s.gsub('"', '')
      board2_id = params["board2_id"].to_s.gsub('"', '')
      
      log "Looking for board 1 with ID: #{board1_id}"
      board1 = model.find_entity_by_id(board1_id.to_i)
      
      log "Looking for board 2 with ID: #{board2_id}"
      board2 = model.find_entity_by_id(board2_id.to_i)
      
      unless board1 && board2
        missing = []
        missing << "board 1" unless board1
        missing << "board 2" unless board2
        raise "Entity not found: #{missing.join(', ')}"
      end
      
      # Ensure both entities are groups or component instances
      unless (board1.is_a?(Sketchup::Group) || board1.is_a?(Sketchup::ComponentInstance)) &&
             (board2.is_a?(Sketchup::Group) || board2.is_a?(Sketchup::ComponentInstance))
        raise "Finger joint operation requires groups or component instances"
      end
      
      # Get joint parameters
      width = params["width"] || 1.0
      height = params["height"] || 2.0
      depth = params["depth"] || 1.0
      num_fingers = params["num_fingers"] || 5
      offset_x = params["offset_x"] || 0.0
      offset_y = params["offset_y"] || 0.0
      offset_z = params["offset_z"] || 0.0
      
      # Create the fingers on board 1
      board1_result = create_board1_fingers(board1, width, height, depth, num_fingers, offset_x, offset_y, offset_z)
      
      # Create the matching slots on board 2
      board2_result = create_board2_slots(board2, width, height, depth, num_fingers, offset_x, offset_y, offset_z)
      
      # Return the result
      { 
        success: true, 
        board1_id: board1_result[:id],
        board2_id: board2_result[:id]
      }
    end
    
    def create_board1_fingers(board, width, height, depth, num_fingers, offset_x, offset_y, offset_z)
      model = Sketchup.active_model
      
      # Get the board's entities
      entities = board.is_a?(Sketchup::Group) ? board.entities : board.definition.entities
      
      # Get the board's bounds
      bounds = board.bounds
      
      # Calculate the position of the joint
      center_x = bounds.center.x + offset_x
      center_y = bounds.center.y + offset_y
      center_z = bounds.center.z + offset_z
      
      # Calculate the width of each finger
      finger_width = width / num_fingers
      
      # Create a group for the fingers
      fingers_group = entities.add_group
      
      # Create a base rectangle for the joint area
      base_face = fingers_group.entities.add_face(
        [center_x - width/2, center_y - height/2, center_z],
        [center_x + width/2, center_y - height/2, center_z],
        [center_x + width/2, center_y + height/2, center_z],
        [center_x - width/2, center_y + height/2, center_z]
      )
      
      # Create cutouts for the spaces between fingers
      (num_fingers / 2).times do |i|
        # Calculate the position of this cutout
        cutout_center_x = center_x - width/2 + finger_width * (2 * i + 1)
        
        # Create a group for the cutout
        cutout_group = entities.add_group
        
        # Create the cutout shape
        cutout_face = cutout_group.entities.add_face(
          [cutout_center_x - finger_width/2, center_y - height/2, center_z],
          [cutout_center_x + finger_width/2, center_y - height/2, center_z],
          [cutout_center_x + finger_width/2, center_y + height/2, center_z],
          [cutout_center_x - finger_width/2, center_y + height/2, center_z]
        )
        
        # Extrude the cutout
        cutout_face.pushpull(depth)
        
        # Subtract the cutout from the fingers
        fingers_group.entities.subtract(cutout_group.entities)
        
        # Clean up the temporary group
        cutout_group.erase!
      end
      
      # Extrude the fingers
      base_face.pushpull(depth)
      
      # Return the result
      { 
        success: true, 
        id: board.entityID
      }
    end
    
    def create_board2_slots(board, width, height, depth, num_fingers, offset_x, offset_y, offset_z)
      model = Sketchup.active_model
      
      # Get the board's entities
      entities = board.is_a?(Sketchup::Group) ? board.entities : board.definition.entities
      
      # Get the board's bounds
      bounds = board.bounds
      
      # Calculate the position of the joint
      center_x = bounds.center.x + offset_x
      center_y = bounds.center.y + offset_y
      center_z = bounds.center.z + offset_z
      
      # Calculate the width of each finger
      finger_width = width / num_fingers
      
      # Create a group for the slots
      slots_group = entities.add_group
      
      # Create cutouts for the fingers from board 1
      (num_fingers / 2 + num_fingers % 2).times do |i|
        # Calculate the position of this cutout
        cutout_center_x = center_x - width/2 + finger_width * (2 * i)
        
        # Create a group for the cutout
        cutout_group = entities.add_group
        
        # Create the cutout shape
        cutout_face = cutout_group.entities.add_face(
          [cutout_center_x - finger_width/2, center_y - height/2, center_z],
          [cutout_center_x + finger_width/2, center_y - height/2, center_z],
          [cutout_center_x + finger_width/2, center_y + height/2, center_z],
          [cutout_center_x - finger_width/2, center_y + height/2, center_z]
        )
        
        # Extrude the cutout
        cutout_face.pushpull(depth)
        
        # Subtract the cutout from the board
        entities.subtract(cutout_group.entities)
        
        # Clean up the temporary group
        cutout_group.erase!
      end
      
      # Return the result
      { 
        success: true, 
        id: board.entityID
      }
    end
    
    def eval_ruby(params)
      log "Evaluating Ruby code with length: #{params['code'].length}"
      
      begin
        # Create a safe binding for evaluation
        binding = TOPLEVEL_BINDING.dup
        
        # Evaluate the Ruby code
        log "Starting code evaluation..."
        result = eval(params["code"], binding)
        log "Code evaluation completed with result: #{result.inspect}"
        
        # Return success with the result as a string
        { 
          success: true,
          result: result.to_s
        }
      rescue StandardError => e
        log "Error in eval_ruby: #{e.message}"
        log e.backtrace.join("\n")
        raise "Ruby evaluation error: #{e.message}"
      end
    end
  end

  unless file_loaded?(__FILE__)
    @server = Server.new
    
    menu = UI.menu("Plugins").add_submenu("MCP Server")
    menu.add_item("Start Server") { @server.start }
    menu.add_item("Stop Server") { @server.stop }
    
    file_loaded(__FILE__)
  end
end 
```