feat(skills): enhance blender-mcp with comprehensive references and recipes

- Rewrite SKILL.md with full setup guide, MCP config, typed commands, object types table
- Add references/bpy-api.md: scene, transforms, bmesh, materials, modifiers, camera, rendering, animation
- Add references/pitfalls.md: 19 real-session pitfalls (connection, Python exec, rendering, version quirks)
- Add references/recipes.md: 5 copy-paste recipes (landscape, glass sphere, donut, turntable, render)
- Preserve original blender-mcp name and credit to alireza78a
- Tested with Blender 5.1 on macOS
This commit is contained in:
kshitijk4poor 2026-04-16 13:58:41 +05:30 committed by Teknium
parent bd7e480236
commit 5601f24449
3 changed files with 593 additions and 0 deletions

View file

@ -0,0 +1,264 @@
# Essential bpy API Reference
## Scene & Objects
```python
# List all objects
[obj.name for obj in bpy.data.objects]
# Get active object
obj = bpy.context.active_object
# Select object by name
bpy.data.objects['Cube'].select_set(True)
# Delete object by name (no context needed)
bpy.data.objects.remove(bpy.data.objects['Cube'], do_unlink=True)
# Delete all objects
for obj in list(bpy.data.objects):
bpy.data.objects.remove(obj, do_unlink=True)
# Duplicate object
import bpy
src = bpy.data.objects['Cube']
new_obj = src.copy()
new_obj.data = src.data.copy()
new_obj.name = 'Cube_Copy'
bpy.context.collection.objects.link(new_obj)
# Parent objects
child = bpy.data.objects['Child']
parent = bpy.data.objects['Parent']
child.parent = parent
```
## Transforms
```python
import math, mathutils
obj = bpy.data.objects['Cube']
# Location (world coordinates)
obj.location = (1.0, 2.0, 3.0)
obj.location.x += 0.5
# Rotation (radians)
obj.rotation_euler = (math.radians(45), 0, 0)
# Scale
obj.scale = (2.0, 2.0, 2.0)
# Apply transforms (bake into mesh)
bpy.context.view_layer.objects.active = obj
bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
```
## Mesh Creation (bmesh)
```python
import bmesh
# Create mesh from scratch
mesh = bpy.data.meshes.new('CustomMesh')
obj = bpy.data.objects.new('CustomObject', mesh)
bpy.context.collection.objects.link(obj)
bm = bmesh.new()
# Add vertices
v1 = bm.verts.new((0, 0, 0))
v2 = bm.verts.new((1, 0, 0))
v3 = bm.verts.new((0.5, 1, 0))
bm.faces.new((v1, v2, v3))
bm.to_mesh(mesh)
bm.free()
```
## Materials (Principled BSDF)
```python
# Create material
mat = bpy.data.materials.new('MyMat')
mat.use_nodes = True
nodes = mat.node_tree.nodes
links = mat.node_tree.links
principled = nodes['Principled BSDF']
principled.inputs['Base Color'].default_value = (0.8, 0.1, 0.1, 1.0)
principled.inputs['Metallic'].default_value = 0.9
principled.inputs['Roughness'].default_value = 0.1
# Assign to object
obj = bpy.data.objects['Cube']
obj.data.materials.append(mat)
# Emission material
mat = bpy.data.materials.new('GlowMat')
mat.use_nodes = True
nodes = mat.node_tree.nodes
links = mat.node_tree.links
nodes.clear()
output = nodes.new('ShaderNodeOutputMaterial')
emission = nodes.new('ShaderNodeEmission')
emission.inputs['Color'].default_value = (0.0, 1.0, 0.5, 1.0)
emission.inputs['Strength'].default_value = 5.0
links.new(emission.outputs[0], output.inputs[0])
obj.data.materials.append(mat)
# Glass material
mat = bpy.data.materials.new('GlassMat')
mat.use_nodes = True
principled = mat.node_tree.nodes['Principled BSDF']
principled.inputs['Transmission Weight'].default_value = 1.0 # Blender 4.x
principled.inputs['IOR'].default_value = 1.45
principled.inputs['Roughness'].default_value = 0.0
```
## Modifiers
```python
obj = bpy.data.objects['Cube']
# Subdivision Surface
mod = obj.modifiers.new('Subdiv', 'SUBSURF')
mod.levels = 2
mod.render_levels = 3
# Solidify
mod = obj.modifiers.new('Solidify', 'SOLIDIFY')
mod.thickness = 0.05
# Boolean
mod = obj.modifiers.new('Bool', 'BOOLEAN')
mod.operation = 'DIFFERENCE'
mod.object = bpy.data.objects['BoolCutter']
# Array
mod = obj.modifiers.new('Array', 'ARRAY')
mod.count = 5
mod.relative_offset_displace = (1.2, 0, 0)
# Apply modifier
bpy.context.view_layer.objects.active = obj
bpy.ops.object.modifier_apply(modifier='Subdiv')
```
## Camera & Lighting
```python
# Create camera
bpy.ops.object.camera_add(location=(7, -7, 5))
cam = bpy.context.active_object
cam.rotation_euler = (math.radians(63), 0, math.radians(45))
bpy.context.scene.camera = cam
# Camera settings
cam.data.lens = 50 # focal length mm
cam.data.clip_start = 0.1
cam.data.clip_end = 1000
# Point light
bpy.ops.object.light_add(type='POINT', location=(3, 3, 5))
light = bpy.context.active_object
light.data.energy = 1000 # watts
light.data.color = (1.0, 0.9, 0.8)
# Sun light
bpy.ops.object.light_add(type='SUN', rotation=(math.radians(45), 0, 0))
sun = bpy.context.active_object
sun.data.energy = 3
# HDRI world lighting
world = bpy.context.scene.world
if not world:
world = bpy.data.worlds.new('World')
bpy.context.scene.world = world
world.use_nodes = True
nodes = world.node_tree.nodes
links = world.node_tree.links
nodes.clear()
output = nodes.new('ShaderNodeOutputWorld')
bg = nodes.new('ShaderNodeBackground')
env = nodes.new('ShaderNodeTexEnvironment')
env.image = bpy.data.images.load('/path/to/hdri.hdr')
links.new(env.outputs[0], bg.inputs[0])
links.new(bg.outputs[0], output.inputs[0])
```
## Rendering
```python
scene = bpy.context.scene
# Resolution
scene.render.resolution_x = 1920
scene.render.resolution_y = 1080
scene.render.resolution_percentage = 100
# Output
scene.render.filepath = '/tmp/render.png'
scene.render.image_settings.file_format = 'PNG' # PNG, JPEG, OPEN_EXR
# Engine
scene.render.engine = 'CYCLES' # or 'BLENDER_EEVEE' (5.x) / 'BLENDER_EEVEE_NEXT' (4.x)
# Cycles settings
scene.cycles.samples = 128
scene.cycles.use_denoising = True
# Render
bpy.ops.render.render(write_still=True)
# Animation render
scene.frame_start = 1
scene.frame_end = 250
scene.render.filepath = '/tmp/anim_'
scene.render.image_settings.file_format = 'PNG'
bpy.ops.render.render(animation=True)
```
## Animation (Keyframes)
```python
obj = bpy.data.objects['Cube']
scene = bpy.context.scene
# Set keyframe at frame 1
scene.frame_set(1)
obj.location = (0, 0, 0)
obj.keyframe_insert(data_path='location', frame=1)
# Set keyframe at frame 60
scene.frame_set(60)
obj.location = (5, 0, 3)
obj.keyframe_insert(data_path='location', frame=60)
# Rotation keyframe
obj.rotation_euler = (0, 0, math.radians(360))
obj.keyframe_insert(data_path='rotation_euler', frame=60)
# Material keyframe
mat = obj.data.materials[0]
principled = mat.node_tree.nodes['Principled BSDF']
principled.inputs['Base Color'].default_value = (1, 0, 0, 1)
principled.inputs['Base Color'].keyframe_insert(data_path='default_value', frame=1)
principled.inputs['Base Color'].default_value = (0, 0, 1, 1)
principled.inputs['Base Color'].keyframe_insert(data_path='default_value', frame=60)
```
## Collections
```python
# Create collection
col = bpy.data.collections.new('MyCollection')
bpy.context.scene.collection.children.link(col)
# Move object to collection
col.objects.link(obj)
bpy.context.scene.collection.objects.unlink(obj)
# Hide collection
col.hide_viewport = True
col.hide_render = True
```

View file

@ -0,0 +1,106 @@
# Blender MCP — Pitfalls & Lessons Learned
## Setup & Connection
### 1. Addon must be started BEFORE connecting
The Blender MCP addon creates the socket server only when you click "Start MCP Server" in the BlenderMCP sidebar tab. If the agent tries to connect before this, you get ConnectionRefusedError on port 9876.
**Verify with:** `lsof -i :9876 -P -n | grep LISTEN`
### 2. Port 9876 is the default — check for conflicts
Other services may use 9876. If connection fails but Blender is running with the addon started, check with lsof. Change the port in the BlenderMCP addon UI panel if needed, and update blender_exec() accordingly.
### 3. The MCP server (uvx blender-mcp) is OPTIONAL for Hermes
The uvx blender-mcp MCP server is a subprocess bridge designed for Claude Desktop. Hermes can talk directly to the addon's socket using blender_exec() — no MCP subprocess needed. The MCP config is optional and adds a layer of indirection.
### 4. Addon installation requires user interaction
Unlike TouchDesigner (where we can paste a script into Textport), Blender addon installation requires the GUI: Edit > Preferences > Add-ons > Install. The agent cannot automate this. Provide the file path and let the user install it.
## Python Execution
### 5. Only bpy and math are in the namespace
The execute_code command provides a namespace with only bpy and math. If you need os, json, bmesh, mathutils, etc., import them inside the code string:
```python
blender_exec("import bmesh; bm = bmesh.new(); ...")
```
### 6. execute_code result is always empty in Blender 5.x addon
The blender-mcp addon's execute_code returns `{"result": {"executed": true, "result": ""}}` for ALL code — both eval and exec. The eval result is not captured in Blender 5.x. To get values:
- Use `get_scene_info` or `get_object_info` for queries
- Write results to a temp file and read back: `blender_exec("import json; open('/tmp/result.json','w').write(json.dumps([o.name for o in bpy.data.objects]))")`
### 7. Errors in execute_code return {"error": "..."} — always check
Always check for the error key in the response. The addon catches exceptions and returns them as error strings rather than crashing.
### 8. bpy.ops require correct context
Many bpy.ops functions require the right context. When executing via the socket, context may differ from the interactive UI. Prefer direct data manipulation:
```python
# Prefer data API over ops
blender_exec("bpy.data.objects.remove(bpy.data.objects['Cube'], do_unlink=True)")
```
## Objects & Scene
### 9. Default scene has Cube, Light, Camera
New Blender files start with a Cube at (0,0,0), a Light, and a Camera. Clear them before building.
### 10. Object names are unique — Blender auto-renames duplicates
Creating an object with name "Cube" when one already exists results in "Cube.001". Always check the returned name.
### 11. Rotation in create_object is in DEGREES, not radians
The addon's create_object command converts degrees to radians internally. But in execute_code, bpy uses radians. Be careful about the distinction.
## Materials
### 12. Principled BSDF is the default shader
All materials created by the addon use Principled BSDF. For other shader types, use execute_code to build the node tree manually.
### 13. Color is RGBA 0-1, not RGB 0-255
Material colors use floating-point RGBA in 0.0-1.0 range.
## Rendering
### 14. Render blocks the connection — set timeout high
Rendering is synchronous. For the agent, the command will take longer to respond. Set timeout to 120s+ for render operations.
### 15. Engine name varies by Blender version
In Blender 5.x, EEVEE is `'BLENDER_EEVEE'` (not `'BLENDER_EEVEE_NEXT'` which was Blender 4.x). Always discover available engines:
```python
blender_exec("import json; open('/tmp/engines.json','w').write(json.dumps(list(bpy.types.RenderSettings.bl_rna.properties['engine'].enum_items.keys())))")
```
Known engine names: `BLENDER_EEVEE`, `BLENDER_WORKBENCH`, `CYCLES`
### 16. GPU rendering requires explicit setup on macOS
Use METAL compute device type on Apple Silicon. Use CUDA or OPTIX on NVIDIA.
## Connection Reliability
### 17. Each command creates a new TCP connection
blender_exec() opens and closes a TCP connection per call. All state lives in Blender's scene data, not the socket.
### 18. Large responses may need multiple recv() calls
The blender_exec() helper loops on recv(65536) until valid JSON is parsed. The 30s timeout handles edge cases.
### 19. Blender crash loses the socket server
If Blender crashes, relaunch it, re-enable the addon, click "Start MCP Server" again. Save frequently.

View file

@ -0,0 +1,223 @@
# Blender Recipes
Common workflows assembled from the bpy API building blocks.
## Recipe 1: Low-Poly Landscape
```python
import bpy, bmesh, math, random
# Clear scene
for obj in list(bpy.data.objects): bpy.data.objects.remove(obj, do_unlink=True)
# Create subdivided plane
bpy.ops.mesh.primitive_plane_add(size=20)
plane = bpy.context.active_object
plane.name = 'Terrain'
# Subdivide
mod = plane.modifiers.new('Subdiv', 'SUBSURF')
mod.levels = 5
mod.subdivision_type = 'SIMPLE'
bpy.context.view_layer.objects.active = plane
bpy.ops.object.modifier_apply(modifier='Subdiv')
# Displace vertices for terrain
bpy.ops.object.mode_set(mode='EDIT')
bm = bmesh.from_edit_mesh(plane.data)
random.seed(42)
for v in bm.verts:
v.co.z = random.gauss(0, 0.5) * (1 - abs(v.co.x)/10) * (1 - abs(v.co.y)/10)
bmesh.update_edit_mesh(plane.data)
bpy.ops.object.mode_set(mode='OBJECT')
# Green material
mat = bpy.data.materials.new('Grass')
mat.use_nodes = True
mat.node_tree.nodes['Principled BSDF'].inputs['Base Color'].default_value = (0.15, 0.4, 0.1, 1)
mat.node_tree.nodes['Principled BSDF'].inputs['Roughness'].default_value = 0.9
plane.data.materials.append(mat)
# Sun light
bpy.ops.object.light_add(type='SUN', rotation=(math.radians(45), 0, math.radians(30)))
bpy.context.active_object.data.energy = 3
# Camera
bpy.ops.object.camera_add(location=(12, -12, 8))
cam = bpy.context.active_object
cam.rotation_euler = (math.radians(55), 0, math.radians(45))
bpy.context.scene.camera = cam
```
## Recipe 2: Glass Sphere on Reflective Plane
```python
import bpy, math
for obj in list(bpy.data.objects): bpy.data.objects.remove(obj, do_unlink=True)
# Reflective floor
bpy.ops.mesh.primitive_plane_add(size=20)
floor = bpy.context.active_object
mat_floor = bpy.data.materials.new('Floor')
mat_floor.use_nodes = True
p = mat_floor.node_tree.nodes['Principled BSDF']
p.inputs['Base Color'].default_value = (0.02, 0.02, 0.02, 1)
p.inputs['Metallic'].default_value = 1.0
p.inputs['Roughness'].default_value = 0.05
floor.data.materials.append(mat_floor)
# Glass sphere
bpy.ops.mesh.primitive_uv_sphere_add(radius=1.5, location=(0, 0, 1.5), segments=64, ring_count=32)
sphere = bpy.context.active_object
mod = sphere.modifiers.new('Smooth', 'SUBSURF')
mod.levels = 2
mat_glass = bpy.data.materials.new('Glass')
mat_glass.use_nodes = True
p = mat_glass.node_tree.nodes['Principled BSDF']
p.inputs['Transmission Weight'].default_value = 1.0
p.inputs['IOR'].default_value = 1.45
p.inputs['Roughness'].default_value = 0.0
sphere.data.materials.append(mat_glass)
# Three-point lighting
bpy.ops.object.light_add(type='AREA', location=(4, -3, 5))
bpy.context.active_object.data.energy = 500
bpy.context.active_object.data.size = 3
bpy.ops.object.light_add(type='AREA', location=(-4, -2, 3))
bpy.context.active_object.data.energy = 200
bpy.context.active_object.data.size = 2
bpy.ops.object.light_add(type='AREA', location=(0, 4, 2))
bpy.context.active_object.data.energy = 100
bpy.context.active_object.data.size = 4
# Camera
bpy.ops.object.camera_add(location=(5, -5, 3))
cam = bpy.context.active_object
cam.rotation_euler = (math.radians(70), 0, math.radians(45))
bpy.context.scene.camera = cam
# Cycles for glass
bpy.context.scene.render.engine = 'CYCLES'
bpy.context.scene.cycles.samples = 256
```
## Recipe 3: Procedural Donut (Simplified)
```python
import bpy, math
for obj in list(bpy.data.objects): bpy.data.objects.remove(obj, do_unlink=True)
# Torus (donut body)
bpy.ops.mesh.primitive_torus_add(
major_radius=1.0, minor_radius=0.4,
major_segments=48, minor_segments=24
)
donut = bpy.context.active_object
donut.name = 'Donut'
# Subdivision for smoothness
mod = donut.modifiers.new('Subdiv', 'SUBSURF')
mod.levels = 2
# Donut material (warm brown)
mat = bpy.data.materials.new('DonutMat')
mat.use_nodes = True
p = mat.node_tree.nodes['Principled BSDF']
p.inputs['Base Color'].default_value = (0.45, 0.22, 0.08, 1.0)
p.inputs['Roughness'].default_value = 0.7
p.inputs['Subsurface Weight'].default_value = 0.3
donut.data.materials.append(mat)
# Icing (duplicate top half, scale up slightly)
bpy.ops.mesh.primitive_torus_add(
major_radius=1.02, minor_radius=0.42,
major_segments=48, minor_segments=24
)
icing = bpy.context.active_object
icing.name = 'Icing'
# Pink icing material
mat_icing = bpy.data.materials.new('IcingMat')
mat_icing.use_nodes = True
p = mat_icing.node_tree.nodes['Principled BSDF']
p.inputs['Base Color'].default_value = (0.9, 0.4, 0.5, 1.0)
p.inputs['Roughness'].default_value = 0.3
p.inputs['Coat Weight'].default_value = 0.5
icing.data.materials.append(mat_icing)
```
## Recipe 4: Turntable Animation
```python
import bpy, math
# Assume scene already has objects
# Create empty as rotation center
bpy.ops.object.empty_add(location=(0, 0, 0))
pivot = bpy.context.active_object
pivot.name = 'TurntablePivot'
# Parent camera to pivot
cam = bpy.data.objects.get('Camera')
if cam:
cam.parent = pivot
cam.location = (7, 0, 3)
cam.rotation_euler = (math.radians(75), 0, math.radians(90))
# Animate rotation
scene = bpy.context.scene
scene.frame_start = 1
scene.frame_end = 120 # 5 seconds at 24fps
pivot.rotation_euler = (0, 0, 0)
pivot.keyframe_insert(data_path='rotation_euler', frame=1)
pivot.rotation_euler = (0, 0, math.radians(360))
pivot.keyframe_insert(data_path='rotation_euler', frame=121)
# Make rotation linear (not eased)
for fc in pivot.animation_data.action.fcurves:
for kp in fc.keyframe_points:
kp.interpolation = 'LINEAR'
# Render settings
scene.render.filepath = '/tmp/turntable_'
scene.render.image_settings.file_format = 'PNG'
scene.render.resolution_x = 1080
scene.render.resolution_y = 1080
```
## Recipe 5: Render to File and Verify
```python
import bpy, os
scene = bpy.context.scene
scene.render.resolution_x = 1920
scene.render.resolution_y = 1080
scene.render.filepath = '/tmp/blender_render.png'
scene.render.image_settings.file_format = 'PNG'
# Use Cycles for quality
scene.render.engine = 'CYCLES'
scene.cycles.samples = 128
scene.cycles.use_denoising = True
# Render
bpy.ops.render.render(write_still=True)
# Verify
result = os.path.exists('/tmp/blender_render.png')
```
Then from the agent, view the render:
```python
# After blender_exec returns, verify and view
from hermes_tools import terminal
terminal("ls -la /tmp/blender_render.png")
# Use vision_analyze to inspect the render
```