Skip to content

Development Guide

This guide covers the development of blender-remote, including architecture, adding new features, testing, and contributing to the project.

Architecture Overview

System Components

System Architecture

Data Flow

  1. LLM Request: IDE sends MCP request via stdio
  2. MCP Processing: FastMCP server processes request
  3. Tool Execution: Appropriate tool handler is called
  4. Blender Communication: JSON-TCP request to BLD_Remote_MCP
  5. Blender Execution: Blender addon executes operation
  6. Response Chain: Results flow back through the layers

Project Structure

blender-remote/
├── src/blender_remote/              # Python package
│   ├── __init__.py                  # Package initialization
│   ├── mcp_server.py               # FastMCP server implementation
│   └── cli/                        # CLI tools (blender-remote-cli)
│       ├── __init__.py             # Re-exports cli + BlenderRemoteConfig
│       ├── __main__.py             # Enables `python -m blender_remote.cli`
│       ├── app.py                  # Click app + command registration
│       ├── commands/               # One module per subcommand
│       ├── config.py               # BlenderRemoteConfig (OmegaConf I/O)
│       ├── constants.py            # Config paths + shared constants
│       ├── detection.py            # Blender discovery + introspection
│       ├── addon.py                # Addon zipping + install-script builder
│       ├── transport.py            # TCP JSON transport to Blender addon
│       └── scripts.py              # Embedded Blender startup scripts
├── blender_addon/                  # Blender addon
│   └── bld_remote_mcp/            # BLD_Remote_MCP service
│       ├── __init__.py            # Main addon logic
│       ├── config.py              # Configuration management
│       ├── utils.py               # Utility functions
│       └── async_loop.py          # Async event loop
├── tests/                         # Test suite
│   ├── mcp-server/               # MCP server tests
│   ├── integration/              # Integration tests
│   └── others/                   # Development scripts
├── docs/                         # Documentation
├── examples/                     # Usage examples
└── context/                      # Development context

CLI Architecture

The blender-remote-cli entrypoint is backed by the blender_remote.cli package in src/blender_remote/cli/.

  • Top-level app: src/blender_remote/cli/app.py defines the Click group (cli) and registers subcommands.
  • Commands: Each CLI subcommand lives in src/blender_remote/cli/commands/ and exposes a Click command/group (imported and added in app.py).
  • Shared helpers: Common functionality (config I/O, Blender discovery, addon utilities, TCP transport, embedded scripts) lives next to app.py.
  • Running locally: pixi run python -m blender_remote.cli --help (or pixi run python -m blender_remote.cli start ...).

Development Environment Setup

Prerequisites

  • Python 3.10+
  • Blender 4.4.3+
  • pixi (recommended) or pip
  • Git

Setup Steps

  1. Clone Repository

    git clone https://github.com/igamenovoer/blender-remote.git
    cd blender-remote
    

  2. Install Dependencies

    # Option 1: Using pixi (recommended)
    pixi install
    
    # Option 2: Using pip
    pip install -e .
    

  3. Install Blender Addon

    # Copy addon to Blender directory
    cp -r blender_addon/bld_remote_mcp/ ~/.config/blender/4.4/scripts/addons/
    

  4. Configure Environment

    export BLD_REMOTE_MCP_PORT=6688
    export BLD_REMOTE_MCP_START_NOW=1
    

  5. Start Blender

    blender &
    

  6. Verify Setup

    pixi run pytest tests/mcp-server/test_fastmcp_server.py::test_connection
    

MCP Server Architecture

Core Components

1. FastMCP Server (src/blender_remote/mcp_server.py)

from fastmcp import FastMCP

# Create MCP server instance
mcp = FastMCP("Blender Remote MCP")

# Connection management
class ConnectionManager:
    def __init__(self):
        self.socket = None
        self.connected = False

    def connect(self):
        # Handle connection to BLD_Remote_MCP
        pass

    def send_command(self, command):
        # Send JSON-TCP command
        pass

2. Tool Handlers

Each MCP tool is implemented as a handler function:

@mcp.tool()
async def get_scene_info(ctx: Context) -> dict:
    """Get information about the current Blender scene."""
    try:
        # Connect to BLD_Remote_MCP
        conn = await get_connection()

        # Send command
        command = {"type": "get_scene_info", "params": {}}
        result = await conn.send_command(command)

        # Process response
        return result
    except Exception as e:
        await ctx.error(f"Failed to get scene info: {e}")
        return {"status": "error", "message": str(e)}

3. Python Client API (src/blender_remote/client.py)

Direct Python API for interacting with Blender without going through MCP:

from blender_remote import BlenderClientAPI

# Create client instance
client = BlenderClientAPI()

# Connect to Blender
client.connect()

# Execute operations directly
result = client.send_command({
    "type": "execute_code",
    "params": {"code": "bpy.ops.mesh.primitive_cube_add()"}
})

4. Scene Manager (src/blender_remote/scene_manager.py)

High-level scene management API:

from blender_remote import SceneManager

# Create scene manager
scene = SceneManager(client)

# Scene operations
scene.create_object("Cube", "mesh")
scene.set_object_transform("Cube", location=(1, 2, 3))
scene.get_scene_info()

5. Asset Manager (src/blender_remote/asset_manager.py)

Asset management functionality:

from blender_remote import AssetManager

# Create asset manager
assets = AssetManager(client)

# Asset operations
assets.import_file("/path/to/model.obj")
assets.export_scene("/path/to/output.fbx")

6. Connection Management

class ConnectionManager:
    def __init__(self):
        self.host = "127.0.0.1"
        self.port = int(os.getenv("BLD_REMOTE_MCP_PORT", "6688"))
        self.socket = None
        self.connected = False

    async def ensure_connection(self):
        """Ensure connection is established."""
        if not self.connected:
            await self.connect()

    async def connect(self):
        """Connect to BLD_Remote_MCP service."""
        try:
            self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            self.socket.connect((self.host, self.port))
            self.connected = True
        except Exception as e:
            raise ConnectionError(f"Failed to connect: {e}")

    async def send_command(self, command):
        """Send command and get response."""
        await self.ensure_connection()

        # Send command
        data = json.dumps(command).encode('utf-8')
        self.socket.sendall(data)

        # Receive response
        response = self.socket.recv(4096).decode('utf-8')
        return json.loads(response)

Adding New MCP Tools

Step 1: Define Tool Function

@mcp.tool()
async def my_new_tool(ctx: Context, param1: str, param2: int = 10) -> dict:
    """
    Brief description of what this tool does.

    Args:
        param1: Description of parameter 1
        param2: Description of parameter 2 (optional)

    Returns:
        Dictionary with result data
    """
    try:
        # Validate parameters
        if not param1:
            await ctx.error("param1 is required")
            return {"status": "error", "message": "param1 is required"}

        # Connect to BLD_Remote_MCP
        conn = await get_connection()

        # Prepare command
        command = {
            "type": "my_new_command",
            "params": {
                "param1": param1,
                "param2": param2
            }
        }

        # Send command
        result = await conn.send_command(command)

        # Log success
        await ctx.info(f"Tool executed successfully: {result}")

        return result

    except Exception as e:
        await ctx.error(f"Tool failed: {e}")
        return {"status": "error", "message": str(e)}

Step 2: Add Blender Handler

In blender_addon/bld_remote_mcp/__init__.py:

def handle_my_new_command(params):
    """Handle my_new_command in Blender."""
    try:
        param1 = params.get("param1")
        param2 = params.get("param2", 10)

        # Perform Blender operations
        result = perform_blender_operation(param1, param2)

        return {
            "status": "success",
            "result": result
        }

    except Exception as e:
        return {
            "status": "error",
            "message": str(e)
        }

# Register handler
COMMAND_HANDLERS["my_new_command"] = handle_my_new_command

Step 3: Add Tests

Create tests/mcp-server/test_my_new_tool.py:

import pytest
from blender_remote.mcp_server import my_new_tool

@pytest.mark.asyncio
async def test_my_new_tool():
    """Test my_new_tool functionality."""
    # Mock context
    class MockContext:
        def __init__(self):
            self.messages = []

        async def info(self, msg):
            self.messages.append(("info", msg))

        async def error(self, msg):
            self.messages.append(("error", msg))

    ctx = MockContext()

    # Test successful execution
    result = await my_new_tool(ctx, "test_param", 20)
    assert result["status"] == "success"

    # Test error handling
    result = await my_new_tool(ctx, "", 20)
    assert result["status"] == "error"

Step 4: Update Documentation

Add to docs/api/mcp-server-api.md:

## my_new_tool

Description of what the tool does.

### Parameters

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `param1` | `string` | Yes | Description of param1 |
| `param2` | `number` | No | Description of param2 (default: 10) |

### Returns

```typescript
{
  status: "success" | "error",
  result: {
    // Description of return structure
  }
}

Example Usage

LLM Prompt: "Use my new tool with parameter 'test'"

CLI: blender-remote my-new-tool --param1 "test" --param2 20

## Testing Framework

### Test Structure
tests/ ├── mcp-server/ # MCP server functionality tests │ ├── test_fastmcp_server.py # FastMCP server tests │ ├── test_scene_info.py # Scene info tool tests │ ├── test_code_execution.py # Code execution tests │ └── test_screenshots.py # Screenshot tests ├── client-api/ # Client API tests │ ├── test_client.py # BlenderClientAPI tests │ ├── test_scene_manager.py # Scene manager tests │ └── test_asset_manager.py # Asset manager tests └── conftest.py # Test configuration
### Running Tests

```bash
# Run all tests
pixi run pytest tests/

# Run MCP server tests
pixi run pytest tests/mcp-server/

# Run client API tests
pixi run pytest tests/client-api/

# Run with verbose output
pixi run pytest tests/ -v

# Run specific test file
pixi run pytest tests/mcp-server/test_fastmcp_server.py

Test Categories

Unit Tests

Test individual components in isolation:

@pytest.mark.unit
async def test_connection_manager():
    """Test connection manager functionality."""
    manager = ConnectionManager()

    # Test connection
    await manager.connect()
    assert manager.connected

    # Test command sending
    result = await manager.send_command({"type": "test"})
    assert result["status"] == "success"

Integration Tests

Test complete workflows:

@pytest.mark.integration
async def test_scene_workflow():
    """Test complete scene creation workflow."""
    # Create scene
    code = "bpy.ops.mesh.primitive_cube_add()"
    result = await execute_blender_code(code)
    assert result["status"] == "success"

    # Verify scene
    scene = await get_scene_info()
    assert scene["result"]["total_objects"] > 0

    # Take screenshot
    if not is_background_mode():
        screenshot = await get_viewport_screenshot()
        assert screenshot["status"] == "success"

Performance Tests

Measure response times and resource usage:

@pytest.mark.performance
async def test_tool_performance():
    """Test tool performance."""
    import time

    start = time.time()
    result = await get_scene_info()
    duration = time.time() - start

    assert result["status"] == "success"
    assert duration < 0.1  # Should be under 100ms

Test Utilities

# tests/conftest.py
import pytest
from blender_remote.mcp_server import ConnectionManager

@pytest.fixture
async def connection():
    """Provide a connection to BLD_Remote_MCP."""
    manager = ConnectionManager()
    await manager.connect()
    yield manager
    manager.close()

@pytest.fixture
def mock_context():
    """Provide a mock MCP context."""
    class MockContext:
        def __init__(self):
            self.messages = []

        async def info(self, msg):
            self.messages.append(("info", msg))

        async def error(self, msg):
            self.messages.append(("error", msg))

    return MockContext()

Debugging

Development Mode

Run MCP server in development mode:

# Start with debug logging
export BLD_REMOTE_MCP_LOG_LEVEL=DEBUG
pixi run python -m blender_remote.mcp_server

# Use FastMCP inspector
pixi run fastmcp dev src/blender_remote/mcp_server.py

Connection Debugging

# Test direct connection
import socket
import json

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(('127.0.0.1', 6688))

command = {"type": "get_scene_info", "params": {}}
sock.sendall(json.dumps(command).encode())

response = sock.recv(4096).decode()
print(json.loads(response))
sock.close()

Common Issues

Port Already in Use

# Find process using port
lsof -i :6688

# Kill process
kill -9 <PID>

Import Errors

# Check Python path
python -c "import sys; print(sys.path)"

# Install in development mode
pip install -e .

Blender Addon Issues

# Check addon directory
ls ~/.config/blender/4.4/scripts/addons/bld_remote_mcp/

# Check Blender console for errors
# Window → Toggle System Console (Windows)
# Or run from terminal (Linux/Mac)

Code Quality

Style Guide

Follow PEP 8 with these additions:

# Function naming
async def get_scene_info():  # snake_case for functions
    pass

# Class naming
class ConnectionManager:  # PascalCase for classes
    pass

# Constants
DEFAULT_PORT = 6688  # UPPER_CASE for constants

# Type hints
async def my_tool(ctx: Context, param: str) -> dict:
    pass

Linting

# Run linting
pixi run lint

# Format code
pixi run format

# Type checking
pixi run mypy src/

Pre-commit Hooks

# Install pre-commit hooks
pip install pre-commit
pre-commit install

# Run manually
pre-commit run --all-files

Performance Optimization

Connection Pooling

class ConnectionPool:
    def __init__(self, max_connections=5):
        self.pool = []
        self.max_connections = max_connections

    async def get_connection(self):
        if self.pool:
            return self.pool.pop()
        return await self.create_connection()

    async def return_connection(self, conn):
        if len(self.pool) < self.max_connections:
            self.pool.append(conn)
        else:
            conn.close()

Caching

from functools import lru_cache

@lru_cache(maxsize=128)
def get_cached_scene_info():
    """Cache scene info for performance."""
    return get_scene_info()

Async Optimization

import asyncio

async def batch_operations():
    """Execute multiple operations concurrently."""
    tasks = [
        get_scene_info(),
        get_object_info("Cube"),
        get_object_info("Camera")
    ]

    results = await asyncio.gather(*tasks)
    return results

Security Considerations

Input Validation

def validate_object_name(name: str) -> bool:
    """Validate object name input."""
    if not name or not isinstance(name, str):
        return False

    # Check for dangerous characters
    dangerous_chars = ['/', '\\', '..', ';', '|']
    if any(char in name for char in dangerous_chars):
        return False

    return True

Code Execution Safety

def sanitize_code(code: str) -> str:
    """Sanitize Python code before execution."""
    # Remove dangerous imports
    dangerous_imports = ['os', 'sys', 'subprocess', 'socket']

    for imp in dangerous_imports:
        if f'import {imp}' in code:
            raise ValueError(f"Import of {imp} not allowed")

    return code

Error Handling

async def safe_execute(func, *args, **kwargs):
    """Safely execute function with error handling."""
    try:
        return await func(*args, **kwargs)
    except Exception as e:
        # Log error without exposing sensitive information
        logger.error(f"Function {func.__name__} failed: {type(e).__name__}")
        return {"status": "error", "message": "Internal error occurred"}

Contributing

Development Workflow

  1. Fork Repository

    git clone https://github.com/yourusername/blender-remote.git
    cd blender-remote
    

  2. Create Feature Branch

    git checkout -b feature/my-new-feature
    

  3. Make Changes

  4. Write code following style guide
  5. Add tests for new functionality
  6. Update documentation

  7. Test Changes

    pixi run pytest tests/
    

  8. Commit Changes

    git add .
    git commit -m "Add new MCP tool for X functionality"
    

  9. Push and Create PR

    git push origin feature/my-new-feature
    # Create pull request on GitHub
    

Pull Request Guidelines

  1. Description: Clear description of changes and motivation
  2. Tests: Include tests for new functionality
  3. Documentation: Update relevant documentation
  4. Backward Compatibility: Ensure no breaking changes
  5. Performance: Consider performance implications

Code Review Process

  1. Automated Checks: CI runs tests and linting
  2. Manual Review: Core maintainers review code
  3. Feedback: Address review comments
  4. Approval: Get approval from maintainers
  5. Merge: Maintainers merge approved PRs

Release Process

Version Management

# Update version in pyproject.toml
version = "1.2.0"

# Create git tag
git tag -a v1.2.0 -m "Release version 1.2.0"
git push origin v1.2.0

Build Process

# Build package
pixi run build

# Test installation
pip install dist/blender-remote-1.1.0.tar.gz

# Test functionality
uvx blender-remote --help

Publishing

# Upload to PyPI
pixi run publish

# Update documentation
pixi run docs

Troubleshooting Development Issues

Common Problems

FastMCP Import Errors

# Install correct version
pip install fastmcp>=2.0.0

# Check installation
python -c "import fastmcp; print(fastmcp.__version__)"

Blender Connection Issues

# Check service status
netstat -tlnp | grep 6688

# Restart Blender
pkill -f blender
export BLD_REMOTE_MCP_START_NOW=1
blender &

Test Failures

# Run specific test
pytest tests/mcp-server/test_fastmcp_server.py -v

# Debug test
pytest tests/mcp-server/test_fastmcp_server.py --pdb

Getting Help

  1. Documentation: Check existing docs first
  2. Issues: Search GitHub issues for similar problems
  3. Discussions: Use GitHub discussions for questions
  4. Code: Read existing code for patterns

Future Development

Planned Features

  1. Enhanced Tools
  2. Animation control tools
  3. Rendering pipeline tools
  4. Asset management tools

  5. Performance Improvements

  6. Connection pooling
  7. Response caching
  8. Async optimizations

  9. UI Improvements

  10. Better error messages
  11. Progress indicators
  12. Configuration GUI

Contributing Ideas

  1. New MCP Tools: Implement additional Blender functionality
  2. IDE Integrations: Support for more IDEs
  3. Documentation: Improve docs and examples
  4. Testing: Add more comprehensive tests

Resources

Documentation

External Resources

Community


Ready to contribute to blender-remote? Start with the development setup and check out the issue tracker for good first issues!