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¶
Data Flow¶
- LLM Request: IDE sends MCP request via stdio
- MCP Processing: FastMCP server processes request
- Tool Execution: Appropriate tool handler is called
- Blender Communication: JSON-TCP request to BLD_Remote_MCP
- Blender Execution: Blender addon executes operation
- 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.pydefines 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 inapp.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(orpixi 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¶
-
Clone Repository
-
Install Dependencies
-
Install Blender Addon
-
Configure Environment
-
Start Blender
-
Verify Setup
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
### 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¶
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¶
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¶
-
Fork Repository
-
Create Feature Branch
-
Make Changes
- Write code following style guide
- Add tests for new functionality
-
Update documentation
-
Test Changes
-
Commit Changes
-
Push and Create PR
Pull Request Guidelines¶
- Description: Clear description of changes and motivation
- Tests: Include tests for new functionality
- Documentation: Update relevant documentation
- Backward Compatibility: Ensure no breaking changes
- Performance: Consider performance implications
Code Review Process¶
- Automated Checks: CI runs tests and linting
- Manual Review: Core maintainers review code
- Feedback: Address review comments
- Approval: Get approval from maintainers
- 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¶
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¶
- Documentation: Check existing docs first
- Issues: Search GitHub issues for similar problems
- Discussions: Use GitHub discussions for questions
- Code: Read existing code for patterns
Future Development¶
Planned Features¶
- Enhanced Tools
- Animation control tools
- Rendering pipeline tools
-
Asset management tools
-
Performance Improvements
- Connection pooling
- Response caching
-
Async optimizations
-
UI Improvements
- Better error messages
- Progress indicators
- Configuration GUI
Contributing Ideas¶
- New MCP Tools: Implement additional Blender functionality
- IDE Integrations: Support for more IDEs
- Documentation: Improve docs and examples
- 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!