
Blender Mcp
- 1.5k installs
- 7 repo stars
- Updated July 30, 2026
- vladmdgolam/agent-skills
blender-mcp is an agent skill for control blender via mcp for scene inspection, python scripting, gltf export, and materials.
About
The blender-mcp skill is designed for control Blender via MCP for scene inspection, Python scripting, GLTF export, and materials. Blender MCP Tool Selection Use structured MCP tools (get_scene_info, screenshot) for quick inspection. Use execute_python for anything non-trivial: hierarchy traversal, material extraction, animation baking, bulk operations. Invoke when the user uses Blender MCP tools like get_scene_info, execute_python, or screenshot.
- Array modifiers (will balloon file size if baked — must replicate at runtime).
- Objects with many vertices (risk of slow export or large GLB).
- Hidden objects you may or may not want to export.
- Missing materials (empty material_slots).
- export_apply=False — do not bake modifiers (Array modifier turns 1 MB into 56 MB).
Blender Mcp by the numbers
- 1,520 all-time installs (skills.sh)
- +61 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #795 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
blender-mcp capabilities & compatibility
- Capabilities
- array modifiers (will balloon file size if baked · objects with many vertices (risk of slow export · hidden objects you may or may not want to export · missing materials (empty material_slots)
What blender-mcp says it does
Blender MCP expert for scene inspection, Python scripting, GLTF export, and material/animation extraction. Activate when: (1) using Blender MCP tools (get_scene_info, execute_pytho
Blender MCP expert for scene inspection, Python scripting, GLTF export, and material/animation extraction. Activate when: (1) using Blender MCP tools (get_scene
npx skills add https://github.com/vladmdgolam/agent-skills --skill blender-mcpAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1.5k |
|---|---|
| repo stars | ★ 7 |
| Security audit | 2 / 3 scanners passed |
| Last updated | July 30, 2026 |
| Repository | vladmdgolam/agent-skills ↗ |
How do I control blender via mcp for scene inspection, python scripting, gltf export, and materials?
Control Blender via MCP for scene inspection, Python scripting, GLTF export, and materials.
Who is it for?
3D artists automating Blender scenes through MCP tools and Python execution.
Skip if: Skip for Maya or Unreal workflows without Blender MCP tooling.
When should I use this skill?
User uses Blender MCP tools like get_scene_info, execute_python, or screenshot.
What you get
Completed blender-mcp workflow with documented commands, files, and expected deliverables.
- Imported HDRIs
- PBR node graphs
- Scene-ready 3D models
Files
Blender MCP
Tool Selection
Use structured MCP tools (get_scene_info, screenshot) for quick inspection.
Use `execute_python` for anything non-trivial: hierarchy traversal, material extraction, animation baking, bulk operations. It gives full bpy API access and avoids tool schema limitations.
Use headless CLI for GLTF exports — the MCP server times out on export operations.
Health Check (Always First)
1. get_scene_info — verify connection (default port 9876) 2. execute_python with print("ok") — verify Python works 3. screenshot — verify viewport capture works
If MCP is unresponsive, check that the Blender MCP addon is enabled and the socket server is running.
Complete Export Workflow
This is the end-to-end linear narrative. Follow these steps in order. Do not skip steps.
Step 1: Health Check
Confirm MCP is alive before touching anything else:
# In MCP tool call:
get_scene_info
execute_python: print("ok")
screenshotIf any step fails, stop and fix MCP connectivity first. See Known Errors.
Step 2: Inspect Scene
Run the full hierarchy extraction to understand what you're working with:
import bpy, json
def extract_hierarchy(obj, depth=0):
data = {
"name": obj.name,
"type": obj.type,
"location": list(obj.location),
"rotation": list(obj.rotation_euler),
"scale": list(obj.scale),
"visible": not obj.hide_viewport,
"children": [],
}
if obj.type == 'MESH' and obj.data:
data["vertices"] = len(obj.data.vertices)
data["faces"] = len(obj.data.polygons)
data["materials"] = [slot.material.name for slot in obj.material_slots if slot.material]
if obj.type == 'LIGHT':
data["light_type"] = obj.data.type
data["energy"] = obj.data.energy
data["color"] = list(obj.data.color)
for mod in obj.modifiers:
if mod.type == 'ARRAY':
data.setdefault("modifiers", []).append({
"type": "ARRAY",
"count": mod.count,
"offset_object": mod.offset_object.name if mod.offset_object else None,
})
for child in obj.children:
data["children"].append(extract_hierarchy(child, depth + 1))
return data
scene_data = {
"name": bpy.context.scene.name,
"fps": bpy.context.scene.render.fps,
"frame_start": bpy.context.scene.frame_start,
"frame_end": bpy.context.scene.frame_end,
"objects": [],
}
for obj in bpy.context.scene.objects:
if obj.parent is None:
scene_data["objects"].append(extract_hierarchy(obj))
print(json.dumps(scene_data, indent=2))Look for:
- Array modifiers (will balloon file size if baked — must replicate at runtime)
- Objects with many vertices (risk of slow export or large GLB)
- Hidden objects you may or may not want to export
- Missing materials (empty
material_slots)
Step 3: Verify Materials
Run the material extraction to catch export-lossy setups before committing to an export:
import bpy, json
def extract_materials():
materials = []
for mat in bpy.data.materials:
if not mat.use_nodes:
continue
info = {"name": mat.name, "nodes": [], "warnings": []}
has_principled = False
for node in mat.node_tree.nodes:
node_data = {"type": node.type, "name": node.name}
if node.type == 'BSDF_PRINCIPLED':
has_principled = True
for inp in node.inputs:
if inp.is_linked:
node_data[inp.name] = "linked"
elif hasattr(inp, 'default_value'):
val = inp.default_value
try:
node_data[inp.name] = list(val)
except TypeError:
node_data[inp.name] = float(val)
if node.type == 'TEX_IMAGE' and node.image:
node_data["image"] = node.image.filepath
node_data["size"] = [node.image.size[0], node.image.size[1]]
if node.image.size[0] > 2048:
info["warnings"].append(f"Large texture: {node.image.filepath} ({node.image.size[0]}x{node.image.size[1]})")
if node.type in ('TEX_NOISE', 'TEX_VORONOI', 'TEX_WAVE', 'TEX_MUSGRAVE'):
info["warnings"].append(f"Procedural texture node '{node.name}' ({node.type}) will be LOST on GLTF export")
if node.type == 'VALTORGB': # Color Ramp
info["warnings"].append(f"Color Ramp '{node.name}' remapping will be LOST on GLTF export")
if not has_principled:
info["warnings"].append("No Principled BSDF found — export result unpredictable")
info["nodes"].append(node_data)
materials.append(info)
return materials
result = extract_materials()
for mat in result:
if mat["warnings"]:
print(f"WARN [{mat['name']}]: {'; '.join(mat['warnings'])}")
print(json.dumps(result, indent=2))Review all warnings before proceeding. Decide: bake procedural textures now, or patch materials at runtime after export.
Step 4: Export via Headless CLI
The MCP server cannot handle GLTF exports (timeout). Always use headless CLI:
# Use 'blender' if it's on PATH, otherwise use the platform-specific path:
# macOS: /Applications/Blender.app/Contents/MacOS/Blender
# Windows: "C:\Program Files\Blender Foundation\Blender 4.x\blender.exe"
# Linux: /usr/bin/blender
blender \
--background "/path/to/scene.blend" \
--python-expr "
import bpy, os
export_path = '/path/to/output.glb'
os.makedirs(os.path.dirname(os.path.abspath(export_path)), exist_ok=True)
bpy.ops.export_scene.gltf(
filepath=export_path,
export_format='GLB',
export_apply=False,
export_animations=True,
export_nla_strips=True,
export_cameras=True,
export_lights=False,
export_draco_mesh_compression_enable=False,
)
size_mb = os.path.getsize(export_path) / 1024 / 1024
print(f'Export complete: {export_path} ({size_mb:.1f} MB)')
"Critical flags:
export_apply=False— do not bake modifiers (Array modifier turns 1 MB into 56 MB)export_draco_mesh_compression_enable=False— apply Draco later via gltf-transform- Quote all paths that may contain spaces
Step 5: Optimize with gltf-transform
Run after a successful export. Always use individual steps, never optimize:
# 1. Inspect raw export first
npx @gltf-transform/cli inspect output.glb
# 2. Resize textures (max 1K for web/mobile)
npx @gltf-transform/cli resize output.glb resized.glb --width 1024 --height 1024
# 3. WebP compression (quality 90 preserves detail)
npx @gltf-transform/cli webp resized.glb webp.glb --quality 90
# 4. Draco mesh compression (LAST — irreversible)
npx @gltf-transform/cli draco webp.glb final.glb
# 5. Inspect final result
npx @gltf-transform/cli inspect final.glbExpected size reduction: ~22 MB raw → ~3.7 MB (WebP) → ~1 MB (Draco). See references/texture-optimization.md for detailed metrics.
Step 6: Validate
Run the full Post-Export Validation checklist below before shipping.
Post-Export Validation Checklist
After every export, verify the following before handing off the GLB for integration:
- [ ] File size is reasonable — raw GLB under 30 MB, optimized GLB under 5 MB for typical web scenes. Flag anything above these thresholds.
- [ ] Inspect with gltf-transform CLI — run
npx @gltf-transform/cli inspect final.glband check: mesh count, texture count, texture sizes, animation count, accessor sizes. No unexpected duplication. - [ ] Visual test in Babylon.js Sandbox — drag-and-drop the GLB at sandbox.babylonjs.com. Verify: mesh renders correctly, textures appear, animations play, no black/pink materials.
- [ ] No Three.js console errors — load in a minimal Three.js GLTFLoader test page and check browser console. Common errors:
THREE.GLTFLoader: Unknown extension, missing texture files, unsupported Draco version. - [ ] Materials spot-check — pick 3–5 materials and visually confirm roughness, metalness, and base color look correct. Compare against Blender viewport render. Flag any that look flat or overly shiny.
- [ ] Animation spot-check — if the scene has animations, verify at least one plays correctly in Babylon.js Sandbox or Three.js. Check frame count matches expected.
- [ ] Name mapping verified — if runtime code references mesh names, confirm the names match after GLTF export transformation (spaces → underscores, dots removed). See Critical Rule 5.
- [ ] No missing textures — check Babylon.js Sandbox network tab. No 404s for texture files. All textures should be packed inside the GLB.
Examples
Example 1: Export Character Rig with Animations
Scenario: You have a humanoid character with armature, 3 NLA actions (idle, walk, run), PBR texture set, and a weapon attached via parenting. You need a web-ready GLB for a Three.js scene.
Step 1: Health check and scene inspection
# MCP tool calls
get_scene_info
execute_python: print("ok")Step 2: Inspect the rig
import bpy, json
# Check armature and NLA strips
for obj in bpy.data.objects:
if obj.type == 'ARMATURE':
print(f"Armature: {obj.name}")
if obj.animation_data:
print(f" Active action: {obj.animation_data.action.name if obj.animation_data.action else 'None'}")
for track in obj.animation_data.nla_tracks:
print(f" NLA track: {track.name}")
for strip in track.strips:
print(f" Strip: {strip.name}, frames {strip.frame_start}-{strip.frame_end}")Step 3: Check materials for export losses
Run the material extraction above. For a character, watch for:
- Procedural skin texture nodes (Noise → color variation) — these will be lost
- Color Ramp on roughness for fabric — will be lost, roughness will look flat
- Decision: bake procedural variations to image textures, or patch roughness values at runtime
Step 4: Export
blender \
--background "/path/to/character.blend" \
--python-expr "
import bpy, os, tempfile
export_dir = tempfile.gettempdir()
bpy.ops.export_scene.gltf(
filepath=os.path.join(export_dir, 'character.glb'),
export_format='GLB',
export_apply=False,
export_animations=True,
export_nla_strips=True,
export_cameras=False,
export_lights=False,
export_draco_mesh_compression_enable=False,
export_skins=True,
export_morph=True,
)
print('done:', os.path.getsize(os.path.join(export_dir, 'character.glb')) / 1024 / 1024, 'MB')
"Step 5: Verify animations exported
npx @gltf-transform/cli inspect character.glb | grep -i animExpected output: 3 animations (Idle, Walk, Run). If 0, check that NLA strips are muted or the tracks are set to solo.
Step 6: Optimize
npx @gltf-transform/cli resize character.glb char_resized.glb --width 1024 --height 1024
npx @gltf-transform/cli webp char_resized.glb char_webp.glb --quality 90
npx @gltf-transform/cli draco char_webp.glb character_final.glbStep 7: Runtime animation setup (Three.js)
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
import * as THREE from 'three';
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath('/draco/');
const loader = new GLTFLoader();
loader.setDRACOLoader(dracoLoader);
loader.load('/character_final.glb', (gltf) => {
const mixer = new THREE.AnimationMixer(gltf.scene);
const clips = gltf.animations; // [Idle, Walk, Run]
const idleAction = mixer.clipAction(clips.find(c => c.name === 'Idle'));
idleAction.play();
// Animate mixer in render loop: mixer.update(delta)
});---
Example 2: Debug Material Export Loss (Roughness Looks Flat)
Scenario: After export, a metal panel material looks uniformly flat and shiny in Three.js. In Blender it had interesting roughness variation from a Noise Texture → Color Ramp → roughness input.
Step 1: Confirm the problem in Blender
import bpy, json
mat = bpy.data.materials.get("MetalPanel")
if mat and mat.use_nodes:
for node in mat.node_tree.nodes:
print(f"Node: {node.type} - {node.name}")
for inp in node.inputs:
if inp.is_linked:
print(f" Input '{inp.name}': linked to something")Expected output reveals:
Node: BSDF_PRINCIPLED - Principled BSDF
Input 'Roughness': linked to something
Node: VALTORGB - Color Ramp <-- this will NOT export
Node: TEX_NOISE - Noise Texture <-- this will NOT exportStep 2: Understand what GLTF received
The export exports the Principled BSDF's roughness input. When linked to a Color Ramp, GLTF exporter takes the default_value of the input socket (fallback), which is typically 0.5 — perfectly flat.
Step 3A: Fix by baking in Blender (best quality)
import bpy
# Select the object
obj = bpy.data.objects["MetalPanelMesh"]
bpy.context.view_layer.objects.active = obj
bpy.ops.object.select_all(action='DESELECT')
obj.select_set(True)
# Create a new image to bake into
bake_img = bpy.data.images.new("MetalPanel_roughness_baked", width=1024, height=1024)
bake_img.colorspace_settings.name = 'Non-Color'
# Add image texture node to material
mat = obj.active_material
nodes = mat.node_tree.nodes
img_node = nodes.new('ShaderNodeTexImage')
img_node.image = bake_img
nodes.active = img_node
# Bake roughness (use ROUGHNESS pass or EMIT trick)
bpy.context.scene.cycles.bake_type = 'ROUGHNESS'
bpy.ops.object.bake(type='ROUGHNESS', save_mode='INTERNAL')
# Save baked image
import tempfile, os
bake_path = os.path.join(tempfile.gettempdir(), 'MetalPanel_roughness_baked.png')
bake_img.filepath_raw = bake_path
bake_img.file_format = 'PNG'
bake_img.save()
print(f"Baked roughness to {bake_path}")Then connect the new image texture node to the Roughness input and re-export.
Step 3B: Fix at runtime in Three.js (quick patch)
If you cannot bake, override the material roughness after load:
loader.load('/metal_panel.glb', (gltf) => {
gltf.scene.traverse((child) => {
if (child.isMesh && child.material) {
const mats = Array.isArray(child.material) ? child.material : [child.material];
mats.forEach(mat => {
if (mat.name === 'MetalPanel') {
// Instead of flat 0.5, set a textured roughness or varied value
mat.roughness = 0.3; // adjust to match intended look
mat.metalness = 0.9;
mat.needsUpdate = true;
}
});
}
});
});Step 4: Verify fix
Re-export and run validation checklist. In Babylon.js Sandbox, compare the metal panel material against a Blender viewport screenshot to confirm roughness variation is preserved.
Critical Rules
1. MCP Server Times Out on Exports
The Blender MCP server cannot handle GLTF exports — they exceed the timeout. Always use headless CLI:
blender --background "scene.blend" --python-expr "
import bpy, os
export_path = 'output.glb'
os.makedirs(os.path.dirname(export_path), exist_ok=True)
bpy.ops.export_scene.gltf(
filepath=export_path,
export_format='GLB',
export_apply=False,
export_animations=True,
export_nla_strips=True,
export_cameras=True,
export_lights=False,
export_draco_mesh_compression_enable=False,
)
print(f'Size: {os.path.getsize(export_path)/1024/1024:.1f} MB')
"2. Do NOT Apply Modifiers on Export
Set export_apply=False. Array modifiers (circular patterns, linear repeats) balloon file size when baked. Replicate them at runtime instead.
Example: 16 roller instances via Array modifier = ~1 MB GLB. Baked = ~56 MB GLB.
3. Export WITHOUT Draco First
If you plan to optimize with gltf-transform, export without Draco compression. Re-encoding existing Draco corrupts meshes. Apply Draco as the final step.
4. Procedural Textures Don't Export to GLTF
These Blender node setups are lost on export:
| Node Setup | What's Lost | Workaround |
|---|---|---|
| Noise Texture → roughness | Entire procedural chain | Bake to texture, or shader patch at runtime |
| Color Ramp on roughness texture | Value remapping range | Manual roughness values, or runtime remap |
| Procedural bump (Noise → Bump) | Bump detail | Bake normal map in Blender |
| Mix Shader with complex factor | Blend logic | Simplify to single BSDF before export |
What DOES export: flat roughness/metallic values, image textures (without Color Ramp remapping), baked normal maps, PBR texture sets (baseColor, metallicRoughness, normal).
5. GLTF Name Mapping
Blender names are transformed in GLTF:
- Spaces → underscores
- Dots → removed
- Trailing spaces → trailing underscore
| Blender | GLTF |
|---|---|
RINGS ball L | RINGS_ball_L |
Sphere.003 | Sphere003 |
RINGS L.001 | RINGS_L001 |
RINGS S (trailing space) | RINGS_S_ |
Always check names in the exported GLB, not Blender, when referencing meshes in code.
6. Never Use gltf-transform optimize
The optimize command includes simplify which destroys mesh geometry. Use individual steps instead:
# Resize textures (max 1024x1024)
npx @gltf-transform/cli resize input.glb resized.glb --width 1024 --height 1024
# WebP texture compression
npx @gltf-transform/cli webp resized.glb webp.glb --quality 90
# Draco mesh compression (LAST step)
npx @gltf-transform/cli draco webp.glb output.glb7. Quote Paths with Spaces
Blender project paths often contain spaces. Always double-quote:
blender --background "$HOME/Downloads/blend 3/scene.blend" ...Scene Extraction Pattern
Full hierarchy with materials, transforms, and modifiers:
import bpy, json
def extract_hierarchy(obj, depth=0):
data = {
"name": obj.name,
"type": obj.type,
"location": list(obj.location),
"rotation": list(obj.rotation_euler),
"scale": list(obj.scale),
"visible": not obj.hide_viewport,
"children": [],
}
if obj.type == 'MESH' and obj.data:
data["vertices"] = len(obj.data.vertices)
data["faces"] = len(obj.data.polygons)
data["materials"] = [slot.material.name for slot in obj.material_slots if slot.material]
if obj.type == 'LIGHT':
data["light_type"] = obj.data.type
data["energy"] = obj.data.energy
data["color"] = list(obj.data.color)
if obj.data.type == 'AREA':
data["size"] = obj.data.size
data["size_y"] = obj.data.size_y
# Array modifiers (important for runtime replication)
for mod in obj.modifiers:
if mod.type == 'ARRAY':
data.setdefault("modifiers", []).append({
"type": "ARRAY",
"count": mod.count,
"offset_object": mod.offset_object.name if mod.offset_object else None,
})
for child in obj.children:
data["children"].append(extract_hierarchy(child, depth + 1))
return data
scene_data = {
"name": bpy.context.scene.name,
"fps": bpy.context.scene.render.fps,
"frame_start": bpy.context.scene.frame_start,
"frame_end": bpy.context.scene.frame_end,
"objects": [],
}
for obj in bpy.context.scene.objects:
if obj.parent is None:
scene_data["objects"].append(extract_hierarchy(obj))
print(json.dumps(scene_data, indent=2))Material Extraction Pattern
import bpy, json
def extract_materials():
materials = []
for mat in bpy.data.materials:
if not mat.use_nodes:
continue
info = {"name": mat.name, "nodes": []}
for node in mat.node_tree.nodes:
node_data = {"type": node.type, "name": node.name}
if node.type == 'BSDF_PRINCIPLED':
for inp in node.inputs:
if inp.is_linked:
node_data[inp.name] = "linked"
elif hasattr(inp, 'default_value'):
val = inp.default_value
try:
node_data[inp.name] = list(val)
except TypeError:
node_data[inp.name] = float(val)
if node.type == 'TEX_IMAGE' and node.image:
node_data["image"] = node.image.filepath
node_data["size"] = [node.image.size[0], node.image.size[1]]
info["nodes"].append(node_data)
materials.append(info)
return materials
print(json.dumps(extract_materials(), indent=2))Animation Keyframe Extraction
import bpy, json
def extract_animation(obj):
if not obj.animation_data or not obj.animation_data.action:
return None
tracks = []
for fc in obj.animation_data.action.fcurves:
keyframes = []
for kp in fc.keyframe_points:
keyframes.append({
"frame": int(kp.co[0]),
"value": float(kp.co[1]),
"interpolation": kp.interpolation,
})
tracks.append({
"data_path": fc.data_path,
"index": fc.array_index,
"keyframes": keyframes,
})
return {"object": obj.name, "tracks": tracks}
animations = []
for obj in bpy.data.objects:
anim = extract_animation(obj)
if anim:
animations.append(anim)
print(json.dumps(animations, indent=2))GLTF Export Settings Reference
| Setting | Value | Why |
|---|---|---|
export_format | 'GLB' | Single binary file |
export_apply | False | Don't bake modifiers (Array, etc.) |
export_animations | True | Include animation data |
export_nla_strips | True | Bake NLA strips into actions |
export_cameras | True | Include camera rigs |
export_lights | False | Handle lights in runtime (Three.js/R3F) |
export_draco_mesh_compression_enable | False | Apply Draco later via gltf-transform |
Texture Optimization Pipeline
Target: smallest GLB with acceptable visual quality.
Blender export (no Draco) → resize (1K max) → WebP (q90) → Draco
~22 MB ~3.7 MB ~3.7 MB ~1 MBKey insights:
- 4K textures (4096x4096) = ~89 MB GPU memory per texture. 1K = ~5.6 MB. 16x reduction.
- PNG metallicRoughness textures compress well to WebP at quality 85-90.
- Mobile GPUs (Adreno, Mali) benefit most from texture downscaling.
- Inspect with:
npx @gltf-transform/cli inspect model.glb
See references/texture-optimization.md for concrete commands and quality metrics.
Asset Integrations
Available through Blender MCP when configured:
| Integration | Capabilities |
|---|---|
| PolyHaven | Search, download, import free HDRIs, textures, and 3D models with auto material setup |
| Sketchfab | Search and download models (requires access token) |
| Hyper3D Rodin | Generate 3D models from text descriptions or reference images |
| Hunyuan3D | Create 3D assets from text prompts, images, or both |
See references/asset-integrations.md for usage examples and workflow patterns.
Known Errors & Workarounds
See references/errors.md for complete error tables.
Data Output
print()+json.dumps()for small results (scene info, single object)- Use
tempfile.gettempdir()for large extraction results (full hierarchy, animation data, material reports) - Always include metadata: scene name, fps, frame range, Blender version
Blender MCP Asset Integrations
The Blender MCP server supports several external asset services when configured with appropriate API keys or local installations. Each integration is accessible via MCP tool calls or execute_python.
---
PolyHaven
What it is: A free, CC0 library of HDRIs, PBR texture sets, and 3D models. No license restrictions. Best source for environment maps and surface materials.
Capabilities via Blender MCP:
- Search the library by keyword, category, or asset type
- Download and auto-import HDRIs as world lighting
- Download and auto-import PBR texture sets with full node setup (baseColor, normal, roughness, metallic, displacement)
- Download 3D models with materials pre-applied
Typical usage patterns:
Import an HDRI for environment lighting
import bpy
# After MCP downloads the HDRI to a local path:
hdri_path = "/path/to/studio_small_09_4k.exr"
world = bpy.data.worlds["World"]
world.use_nodes = True
nodes = world.node_tree.nodes
links = world.node_tree.links
# Clear existing nodes
nodes.clear()
# Add environment texture
bg_node = nodes.new("ShaderNodeBackground")
env_node = nodes.new("ShaderNodeTexEnvironment")
out_node = nodes.new("ShaderNodeOutputWorld")
map_node = nodes.new("ShaderNodeMapping")
coord_node = nodes.new("ShaderNodeTexCoord")
env_node.image = bpy.data.images.load(hdri_path)
links.new(coord_node.outputs["Generated"], map_node.inputs["Vector"])
links.new(map_node.outputs["Vector"], env_node.inputs["Vector"])
links.new(env_node.outputs["Color"], bg_node.inputs["Color"])
links.new(bg_node.outputs["Background"], out_node.inputs["Surface"])
bg_node.inputs["Strength"].default_value = 1.0
print("HDRI loaded:", hdri_path)Import a PBR texture set onto a material
import bpy, os
# After MCP downloads the texture set to a local directory:
tex_dir = "/path/to/concrete_layers_02_1k/"
mat = bpy.data.materials.new(name="ConcreteLayers")
mat.use_nodes = True
nodes = mat.node_tree.nodes
links = mat.node_tree.links
nodes.clear()
bsdf = nodes.new("ShaderNodeBsdfPrincipled")
output = nodes.new("ShaderNodeOutputMaterial")
links.new(bsdf.outputs["BSDF"], output.inputs["Surface"])
def load_tex(fname, colorspace="sRGB"):
path = os.path.join(tex_dir, fname)
if not os.path.exists(path):
return None
img = bpy.data.images.load(path)
img.colorspace_settings.name = colorspace
node = nodes.new("ShaderNodeTexImage")
node.image = img
return node
base_color = load_tex("concrete_layers_02_diff_1k.jpg", "sRGB")
roughness = load_tex("concrete_layers_02_rough_1k.jpg", "Non-Color")
normal_img = load_tex("concrete_layers_02_nor_gl_1k.jpg", "Non-Color")
disp_img = load_tex("concrete_layers_02_disp_1k.jpg", "Non-Color")
if base_color:
links.new(base_color.outputs["Color"], bsdf.inputs["Base Color"])
if roughness:
links.new(roughness.outputs["Color"], bsdf.inputs["Roughness"])
if normal_img:
normal_map = nodes.new("ShaderNodeNormalMap")
links.new(normal_img.outputs["Color"], normal_map.inputs["Color"])
links.new(normal_map.outputs["Normal"], bsdf.inputs["Normal"])
print("Material setup complete:", mat.name)Notes:
- PolyHaven textures are CC0 — safe to include in commercial projects
- Always choose 1K or 2K resolution downloads; 4K+ will need resizing before GLB export
- Displacement maps are not supported in GLTF — skip or bake to normal map
---
Sketchfab
What it is: A marketplace and community for 3D models. Free and paid assets. Requires a Sketchfab access token for download.
Capabilities via Blender MCP:
- Search the library by keyword
- Download licensed models (free and purchased) directly into Blender
- Auto-import with materials
Configuration: Set your Sketchfab API token in the Blender MCP addon settings before use.
Typical usage pattern:
Search and import a model
# MCP handles search and download internally.
# After import, verify the object landed correctly:
import bpy, json
imported = []
for obj in bpy.context.scene.objects:
imported.append({
"name": obj.name,
"type": obj.type,
"materials": [s.material.name for s in obj.material_slots if s.material],
})
print(json.dumps(imported, indent=2))Post-import checklist:
- Check for missing textures:
File → External Data → Report Missing Files - Verify scale (Sketchfab models often come in at wrong scale — check bounding box)
- Check material nodes for non-Principled BSDF shaders (some imports use Diffuse BSDF, which exports poorly)
Convert imported Diffuse BSDF to Principled for better export:
import bpy
for mat in bpy.data.materials:
if not mat.use_nodes:
continue
nodes = mat.node_tree.nodes
links = mat.node_tree.links
diffuse_nodes = [n for n in nodes if n.type == 'BSDF_DIFFUSE']
for diff_node in diffuse_nodes:
# Create Principled BSDF in its place
princ = nodes.new('ShaderNodeBsdfPrincipled')
princ.location = diff_node.location
# Re-wire inputs
for link in mat.node_tree.links:
if link.to_node == diff_node:
if link.to_socket.name == 'Color':
links.new(link.from_socket, princ.inputs['Base Color'])
elif link.to_socket.name == 'Normal':
links.new(link.from_socket, princ.inputs['Normal'])
if link.from_node == diff_node:
links.new(princ.outputs['BSDF'], link.to_socket)
nodes.remove(diff_node)
print(f"Converted Diffuse BSDF in {mat.name}")---
Hyper3D Rodin
What it is: AI-powered 3D model generation from text descriptions or reference images. Produces watertight meshes with PBR textures.
Capabilities via Blender MCP:
- Generate a 3D model from a text prompt
- Generate a 3D model from one or more reference images
- Import generated model directly into the current Blender scene
Typical usage patterns:
Generate from text prompt
# MCP tool call (handled internally by the integration):
# Prompt: "A medieval iron lantern with glass panels, hanging chain, aged patina"
# Format: GLB
# Resolution: Medium (512 texture)After import, verify and adjust:
import bpy, json
# Find the newly imported object (usually the most recently added)
latest = sorted(bpy.data.objects, key=lambda o: o.name)[-1]
print("Imported:", latest.name)
print("Vertices:", len(latest.data.vertices) if latest.data else "N/A")
print("Materials:", [s.material.name for s in latest.material_slots if s.material])Generate from reference image
# MCP tool call:
# Image: /path/to/reference_photo.jpg
# Prompt: "Stylized low-poly tree with autumn leaves"
# Format: GLBPost-generation workflow:
import bpy
# Rodin models often come in with Y-up orientation; correct for Blender Z-up:
obj = bpy.context.active_object
if obj:
obj.rotation_euler[0] = 0 # reset X rotation if double-applied
bpy.ops.object.transform_apply(rotation=True)
# Check poly count — Rodin tends to produce dense meshes
for obj in bpy.context.selected_objects:
if obj.type == 'MESH':
print(f"{obj.name}: {len(obj.data.polygons)} faces")Notes:
- Rodin outputs are good starting points but often need decimation for web use
- PBR textures from Rodin export cleanly to GLTF (no procedural nodes)
- Generation time varies: 30 seconds to several minutes depending on complexity
Decimate after Rodin import:
import bpy
obj = bpy.context.active_object
mod = obj.modifiers.new("Decimate", "DECIMATE")
mod.ratio = 0.3 # reduce to 30% of original face count
# Do NOT apply modifier before export — set export_apply=False
print(f"Decimation modifier added. Effective faces: ~{len(obj.data.polygons) * mod.ratio:.0f}")---
Hunyuan3D
What it is: Tencent's open-source 3D generation model. Runs locally. Accepts text prompts, images, or both. Produces detailed meshes with high-quality texture generation.
Capabilities via Blender MCP:
- Generate 3D assets from text prompts
- Generate from single or multiple reference images
- Combined text + image conditioning for style control
- Import directly into Blender scene
Typical usage patterns:
Text + image generation (best quality)
# MCP tool call:
# Image: /path/to/concept_art.png
# Prompt: "Game-ready sci-fi crate, metallic, worn edges, sticker decals"
# Steps: 50 (more steps = more detail, slower)Post-import material inspection
import bpy, json
# Hunyuan3D typically outputs a single mesh with baked texture atlas
result = []
for obj in bpy.data.objects:
if obj.type != 'MESH':
continue
mats = []
for slot in obj.material_slots:
if not slot.material or not slot.material.use_nodes:
continue
mat_info = {"name": slot.material.name, "textures": []}
for node in slot.material.node_tree.nodes:
if node.type == 'TEX_IMAGE' and node.image:
mat_info["textures"].append({
"image": node.image.name,
"size": list(node.image.size),
"filepath": node.image.filepath,
})
mats.append(mat_info)
result.append({"object": obj.name, "materials": mats})
print(json.dumps(result, indent=2))Notes:
- Hunyuan3D runs locally — requires significant VRAM (16 GB+ recommended for full quality)
- Outputs often use a single UV-unwrapped texture atlas — ideal for GLTF (no procedural nodes)
- Texture baking is already done; export to GLTF proceeds without baking steps
- Local generation avoids API rate limits and keeps assets private
Quality comparison (approximate):
| Scenario | Hyper3D Rodin | Hunyuan3D |
|---|---|---|
| Speed | Cloud, ~1-3 min | Local, ~2-10 min |
| Texture quality | Good | Very good |
| Geometry quality | Clean, watertight | Dense, may need decimation |
| Privacy | Cloud API | Fully local |
| Cost | API credits | Hardware cost only |
| Best for | Quick iteration | Final assets |
---
Integration Comparison
| Integration | Asset type | Free? | Requires auth? | Best use case |
|---|---|---|---|---|
| PolyHaven | HDRIs, textures, models | Yes (CC0) | No | Environment setup, surface materials |
| Sketchfab | Models (any) | Free tier + paid | Yes (API token) | Finding specific objects |
| Hyper3D Rodin | AI-generated models | Credits | Yes (API key) | Concept-to-3D, fast iteration |
| Hunyuan3D | AI-generated models | Free (local) | No | High-quality local generation |
Blender MCP Error Reference
MCP Server Errors
| Error | Cause | Fix |
|---|---|---|
| Export timeout | GLTF export exceeds MCP timeout (~15-30s) | Use headless CLI: blender --background file.blend --python-expr "..." |
| Connection refused on port 9876 | MCP addon not running | Enable Blender MCP addon in Preferences → Add-ons, restart Blender |
| Socket timeout | Blender is busy (rendering, heavy computation) | Wait for current operation to finish, retry |
| Script execution timeout | Python script too slow | Split into smaller scripts, reduce iteration count |
GLTF Export Errors
| Error | Cause | Fix |
|---|---|---|
| Corrupt mesh after Draco re-encode | Applied Draco twice (export + gltf-transform) | Export WITHOUT Draco, apply Draco as final gltf-transform step |
Mesh destroyed by optimize | gltf-transform optimize includes simplify | Use individual steps: resize → webp → draco |
| Missing textures in GLB | Broken texture paths (e.g., textures/textures/ double nesting) | Check File → External Data → Report Missing Files in Blender |
| Materials look flat | Color Ramp remapping lost on export | Apply manual roughness/metalness values at runtime |
| Procedural roughness missing | Noise Texture nodes don't export | Bake to texture in Blender, or use runtime shader patch |
| Giant file size | Array modifiers baked on export | Set export_apply=False, replicate arrays at runtime |
Blender Python API Errors
| Error | Cause | Fix |
|---|---|---|
RuntimeError: Operator bpy.ops.export_scene.gltf.poll() failed | No active scene or context override needed | Run in correct context or use --background mode |
AttributeError: 'NoneType' has no attribute 'nodes' | Material has use_nodes = False | Check mat.use_nodes before accessing mat.node_tree.nodes |
KeyError: 'Principled BSDF' | Material uses non-standard shader | Iterate node_tree.nodes and filter by node.type == 'BSDF_PRINCIPLED' |
RecursionError in hierarchy traversal | Deep nesting hits Python limit | Use iterative (stack-based) traversal instead of recursion |
bpy.context.scene is None in background mode | Context not fully initialized | Use bpy.data.scenes[0] or bpy.context.window.scene |
Texture Path Issues
| Symptom | Cause | Fix |
|---|---|---|
| Textures not packed in GLB | External file references with relative paths | File → External Data → Pack Resources before export |
Double-nested path (textures/textures/) | Incorrect relative path in Blender | Fix path in Image Editor or via bpy.data.images["name"].filepath |
| 4K texture causing mobile GPU OOM | Texture too large for mobile VRAM | Resize to 1024x1024 via gltf-transform |
GLTF Name Mapping Gotchas
| Issue | Example | Notes |
|---|---|---|
| Spaces → underscores | RINGS ball L → RINGS_ball_L | Always check GLB names, not Blender names |
| Dots removed | Sphere.003 → Sphere003 | Dot-number suffixes collapse |
| Trailing spaces → underscore | RINGS S → RINGS_S_ | Easy to miss in Blender UI |
| Duplicate names | Two objects named Cube | GLTF appends _1, _2 — unpredictable |
Material Export Survival Matrix
| Blender Feature | Exports to GLTF? | Notes |
|---|---|---|
| Flat roughness/metallic values | Yes | Direct mapping |
| Image textures (baseColor, normal) | Yes | Packed or referenced |
| Image roughness texture | Partially | Texture exports, Color Ramp remapping lost |
| Procedural Noise Texture | No | Must bake or patch at runtime |
| Color Ramp value remapping | No | Range compression lost |
| Bump from Noise node | No | Bake to normal map |
| Baked normal maps | Yes | Standard GLTF feature |
| Alpha from texture | Yes | Via alphaMode |
| Emission | Yes | Via emissiveFactor / emissiveTexture |
| Separate metallic + roughness channels | Yes | Combined into metallicRoughness texture |
Texture Optimization Pipeline
Target: smallest GLB with acceptable visual quality for web delivery.
---
Pipeline Overview
Blender export (no Draco)
|
v
inspect (gltf-transform)
|
v
resize to 1K max
|
v
WebP compression (q85-90)
|
v
Draco mesh compression (LAST)
|
v
final inspect + validateTypical size reduction:
| Stage | Size | Notes |
|---|---|---|
| Raw Blender export | ~22 MB | 4K PNG textures, no mesh compression |
| After resize 1024x1024 | ~3.7 MB | 16x GPU memory reduction |
| After WebP q90 | ~3.7 MB | File size drops, GPU memory same |
| After Draco | ~1.0 MB | ~75% mesh data reduction |
---
Step-by-Step Commands
Step 0: Install gltf-transform
npm install -g @gltf-transform/cli
# or use via npx without installing:
npx @gltf-transform/cli --helpStep 1: Inspect Before Optimizing
Always inspect before touching anything. You need a baseline.
npx @gltf-transform/cli inspect input.glbExample output to read:
SCENE Spot Lighting
MESH 16 meshes, 12,847 primitives
SKIN 0 skins
ANIM 3 animations (45.0s / 1350 frames)
TEXTURE 8 textures
image/png 3 textures, 12.4 MB
image/jpeg 5 textures, 4.2 MB
GPU EST ~89.3 MB VRAMKey metrics to note:
GPU EST— estimated VRAM usage. Over 50 MB is heavy for mobile. Over 100 MB will OOM on low-end devices.- Texture count and format — PNGs are larger, JPEGs are lossy, WebP is smaller with same quality
- Individual texture sizes — find which textures dominate total size
Step 2: Resize Textures
# Resize all textures to max 1024x1024
npx @gltf-transform/cli resize input.glb resized.glb --width 1024 --height 1024
# For higher-fidelity desktop targets (still reasonable):
npx @gltf-transform/cli resize input.glb resized.glb --width 2048 --height 2048
# For very lightweight mobile targets:
npx @gltf-transform/cli resize input.glb resized.glb --width 512 --height 512GPU memory impact per texture at different sizes:
| Resolution | GPU Memory (RGBA) | Notes |
|---|---|---|
| 4096x4096 | ~89 MB | Desktop only, very heavy |
| 2048x2048 | ~22 MB | Acceptable for desktop |
| 1024x1024 | ~5.6 MB | Good web/mobile target |
| 512x512 | ~1.4 MB | Mobile minimum quality |
With 8 textures at 4K: ~712 MB VRAM total — crashes most mobile browsers. With 8 textures at 1K: ~45 MB VRAM total — workable on mid-range mobile.
Step 3: WebP Compression
# Quality 90 — high fidelity, good for hero assets
npx @gltf-transform/cli webp resized.glb webp.glb --quality 90
# Quality 85 — recommended default (smaller file, imperceptible quality loss)
npx @gltf-transform/cli webp resized.glb webp.glb --quality 85
# Quality 75 — aggressive compression, acceptable for background/prop assets
npx @gltf-transform/cli webp resized.glb webp.glb --quality 75Quality setting guide:
| Quality | Use case | File size vs q90 |
|---|---|---|
| 90 | Hero characters, close-up assets | 1x (baseline) |
| 85 | Standard assets | ~0.7x |
| 75 | Background props, distant objects | ~0.5x |
| 60 | Thumbnails, preview LODs | ~0.35x |
Important: WebP reduces file download size but does NOT reduce GPU memory. The GPU still decompresses to the full bitmap. Only resizing reduces GPU memory.
Texture type quality recommendations:
| Texture type | Recommended quality | Reason |
|---|---|---|
| Base color (diffuse) | 85-90 | Color accuracy visible to eye |
| Normal map | 90 | Low-quality normals cause visible banding |
| MetallicRoughness | 85 | Channel precision less perceptible |
| Emissive | 85 | Glow artifacts masked by bloom |
| Occlusion | 80 | Subtle, low-frequency data |
Step 4: Draco Mesh Compression
# Default settings (good for most cases)
npx @gltf-transform/cli draco webp.glb final.glb
# Higher compression (smaller file, more decode time)
npx @gltf-transform/cli draco webp.glb final.glb \
--quantize-position 14 \
--quantize-normal 10 \
--quantize-texcoord 12
# Lower compression (faster decode, good for interactive scenes with many objects)
npx @gltf-transform/cli draco webp.glb final.glb \
--quantize-position 11 \
--quantize-normal 8 \
--quantize-texcoord 10Draco quantization bits guide:
| Parameter | Default | Range | Trade-off |
|---|---|---|---|
--quantize-position | 14 | 8-16 | Higher = more accurate vertex positions |
--quantize-normal | 10 | 6-12 | Higher = smoother curved surfaces |
--quantize-texcoord | 12 | 8-14 | Higher = less UV seam artifacts |
WARNING: Draco is irreversible. Always keep an uncompressed version as source.
WARNING: Do NOT apply Draco if you exported with Draco from Blender. Re-encoding corrupts meshes. The pipeline must be: Blender (no Draco) → gltf-transform Draco.
Step 5: Final Inspect
npx @gltf-transform/cli inspect final.glbCompare against baseline inspection from Step 1. Verify:
- GPU EST is below target (50 MB for mobile, 200 MB for desktop)
- Texture formats show
image/webp - Mesh compression shows
draco - Animation count matches original
---
Complete Pipeline Script
For repeatability, use a shell script:
#!/bin/bash
# optimize-glb.sh — full optimization pipeline
# Usage: ./optimize-glb.sh input.glb output_dir/
set -e
INPUT="$1"
OUTPUT_DIR="${2:-.}"
BASENAME=$(basename "$INPUT" .glb)
echo "=== Inspecting source: $INPUT ==="
npx @gltf-transform/cli inspect "$INPUT"
echo ""
echo "=== Resizing textures to 1024x1024 ==="
npx @gltf-transform/cli resize "$INPUT" "$OUTPUT_DIR/${BASENAME}_resized.glb" \
--width 1024 --height 1024
echo ""
echo "=== WebP compression (quality 85) ==="
npx @gltf-transform/cli webp \
"$OUTPUT_DIR/${BASENAME}_resized.glb" \
"$OUTPUT_DIR/${BASENAME}_webp.glb" \
--quality 85
echo ""
echo "=== Draco mesh compression ==="
npx @gltf-transform/cli draco \
"$OUTPUT_DIR/${BASENAME}_webp.glb" \
"$OUTPUT_DIR/${BASENAME}_final.glb"
echo ""
echo "=== Final inspection ==="
npx @gltf-transform/cli inspect "$OUTPUT_DIR/${BASENAME}_final.glb"
echo ""
echo "=== Size comparison ==="
echo "Source: $(du -sh "$INPUT" | cut -f1)"
echo "Final: $(du -sh "$OUTPUT_DIR/${BASENAME}_final.glb" | cut -f1)"---
Advanced: Selective Texture Operations
Resize only specific textures
# List texture names first
npx @gltf-transform/cli inspect input.glb --format table
# Use Node.js API for selective operations:
node -e "
const { NodeIO } = require('@gltf-transform/core');
const { resize } = require('@gltf-transform/functions');
async function run() {
const io = new NodeIO();
const doc = await io.read('input.glb');
for (const tex of doc.getRoot().listTextures()) {
const name = tex.getName();
if (name.includes('background') || name.includes('skybox')) {
// Skip background textures — they are meant to be large
continue;
}
// Apply resize to all other textures
const img = tex.getImage();
if (img) {
console.log('Resizing:', name);
}
}
await io.write('output.glb', doc);
}
run().catch(console.error);
"Convert only PNG textures to WebP (preserve existing WebP/JPEG)
# gltf-transform webp converts all by default
# To be selective, inspect first and decide per texture
npx @gltf-transform/cli inspect input.glb---
Quality Metrics Reference
Use these metrics to evaluate output quality before shipping:
File size targets (web delivery)
| Asset type | Raw export | Acceptable final | Excellent final |
|---|---|---|---|
| Character model + animations | 15-30 MB | < 5 MB | < 2 MB |
| Environment scene | 20-50 MB | < 8 MB | < 4 MB |
| Prop/object | 1-5 MB | < 1 MB | < 300 KB |
| Icon/small asset | < 1 MB | < 100 KB | < 50 KB |
GPU VRAM targets
| Platform | Target VRAM per scene | Notes |
|---|---|---|
| High-end desktop | < 500 MB | RTX 3080+, integrated scenes |
| Mid desktop / console | < 200 MB | RTX 2060, PS5 |
| High-end mobile | < 100 MB | iPhone 14+, Snapdragon 8 Gen 2 |
| Mid-range mobile | < 50 MB | Snapdragon 778G, Helio G99 |
| Low-end mobile | < 20 MB | Budget Android |
Visual quality checklist
- Normal maps: no visible faceting at expected viewing distance
- Base color: no visible compression artifacts on flat color surfaces
- Roughness: no banding on smooth gradient surfaces
- Metalness: specular highlights look physically correct
- Animations: no vertex snapping (visible with low
--quantize-position)
---
Common Mistakes
| Mistake | Symptom | Fix |
|---|---|---|
Used gltf-transform optimize | Mesh geometry destroyed | Always use individual steps: resize → webp → draco |
| Applied Draco in Blender then again in gltf-transform | Corrupted/missing mesh | Export from Blender with export_draco_mesh_compression_enable=False |
| Skipped resize, only ran WebP | Still high GPU VRAM | Run resize first — WebP doesn't reduce VRAM |
| Quantize-position too low | Vertex positions snap/jump | Use 12+ bits for position quantization |
| Optimized without backup | Cannot recover quality | Always keep uncompressed intermediate files |
| Ran optimization on already-Draco GLB | No further compression, possible errors | Start from the raw Blender export |
Related skills
How it compares
Pick blender-mcp over manual Blender import workflows when MCP-driven search, download, and node setup must happen inside an agent session.
FAQ
What does blender-mcp do?
Control Blender via MCP for scene inspection, Python scripting, GLTF export, and materials.
When should I use blender-mcp?
User uses Blender MCP tools like get_scene_info, execute_python, or screenshot.
Is blender-mcp safe to install?
Review the Security Audits panel on this page before installing in production.