Skip to main content
Glama
server.py13.6 kB
from mcp.server.fastmcp import FastMCP import socket import json import sys import logging # Force UTF-8 encoding for stdout/stdin sys.stdout.reconfigure(encoding='utf-8') sys.stdin.reconfigure(encoding='utf-8') # Setup logging to stderr (won't interfere with MCP stdio) logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', stream=sys.stderr ) logger = logging.getLogger("blender-mcp") # Initialize FastMCP server mcp = FastMCP("Blender MCP") import os # Configuration BLENDER_HOST = os.getenv('BLENDER_HOST', '127.0.0.1') BLENDER_PORT = int(os.getenv('BLENDER_PORT', '9876')) def send_to_blender(command_dict): """Helper to send JSON command to Blender via socket""" try: logger.info(f"Attempting to connect to Blender at {BLENDER_HOST}:{BLENDER_PORT}") with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.settimeout(5.0) # 5 second timeout s.connect((BLENDER_HOST, BLENDER_PORT)) s.sendall(json.dumps(command_dict).encode('utf-8')) data = s.recv(4096) resp = json.loads(data.decode('utf-8')) # Truncate output if too long to save tokens if "output" in resp and isinstance(resp["output"], str) and len(resp["output"]) > 1000: resp["output"] = resp["output"][:1000] + "... [TRUNCATED]" logger.info(f"Successfully executed command: {command_dict.get('type', 'unknown')}") return resp except ConnectionRefusedError: logger.error(f"Connection refused to Blender at {BLENDER_HOST}:{BLENDER_PORT}") return {"status": "error", "message": "Could not connect to Blender. Is the Addon installed and Blender running?"} except socket.timeout: logger.error("Socket timeout while connecting to Blender") return {"status": "error", "message": "Timeout connecting to Blender. Make sure Blender is running with the addon enabled."} except Exception as e: logger.error(f"Error communicating with Blender: {str(e)}") return {"status": "error", "message": str(e)} @mcp.tool() def run_blender_script(script_code: str) -> str: """ Executes a Python script inside the running Blender instance. Use this to create objects, modify scenes, or query data using the 'bpy' library. Example: import bpy bpy.ops.mesh.primitive_cube_add(size=2) """ command = { "type": "run_script", "script": script_code } response = send_to_blender(command) # Compact JSON to save tokens return json.dumps(response, separators=(',', ':')) @mcp.tool() def get_blender_version() -> str: """Checks the connection and returns the Blender version.""" command = { "type": "get_version" } response = send_to_blender(command) return json.dumps(response, separators=(',', ':')) @mcp.tool() def apply_texture(object_name: str, texture_path: str) -> str: """Apply an image texture to an object. Auto-joins selected objects if multiple are active/selected to ensure single mesh texturing. Auto-generates UVs if missing. Args: object_name: Name of the main object to texture. texture_path: Absolute path to the image file. """ command = { "type": "apply_texture", "object_name": object_name, "texture_path": texture_path } return json.dumps(send_to_blender(command), separators=(',', ':')) @mcp.tool() def create_primitive(type: str, name: str = "", size: float = 1.0, location: tuple[float, float, float] = (0.0, 0.0, 0.0), rotation: tuple[float, float, float] = (0.0, 0.0, 0.0)) -> str: """Create a basic 3D primitive object. Args: type: One of 'CUBE', 'SPHERE', 'ICO_SPHERE', 'CYLINDER', 'CONE', 'PLANE', 'MONKEY'. name: Optional name for the new object. size: Size/Radius of the object. location: (x, y, z) location. rotation: (x, y, z) rotation in degrees. """ script = f""" import bpy import math def make_prim(): loc = {location} rot = [math.radians(a) for a in {rotation}] sz = {size} if '{type}' == 'CUBE': bpy.ops.mesh.primitive_cube_add(size=sz, location=loc, rotation=rot) elif '{type}' == 'SPHERE': bpy.ops.mesh.primitive_uv_sphere_add(radius=sz/2, location=loc, rotation=rot) elif '{type}' == 'ICO_SPHERE': bpy.ops.mesh.primitive_ico_sphere_add(radius=sz/2, location=loc, rotation=rot) elif '{type}' == 'CYLINDER': bpy.ops.mesh.primitive_cylinder_add(radius=sz/2, depth=sz, location=loc, rotation=rot) elif '{type}' == 'CONE': bpy.ops.mesh.primitive_cone_add(radius1=sz/2, depth=sz, location=loc, rotation=rot) elif '{type}' == 'PLANE': bpy.ops.mesh.primitive_plane_add(size=sz, location=loc, rotation=rot) elif '{type}' == 'MONKEY': bpy.ops.mesh.primitive_monkey_add(size=sz, location=loc, rotation=rot) else: return "Error: Unknown primitive type '{type}'" obj = bpy.context.active_object if "{name}": obj.name = "{name}" return f"Created {{obj.name}} ({type})" make_prim() """ return json.dumps(send_to_blender({"type": "run_script", "script": script}), separators=(',', ':')) @mcp.tool() def transform_object(name: str, location: tuple[float, float, float] = None, rotation: tuple[float, float, float] = None, scale: tuple[float, float, float] = None) -> str: """Transform (move, rotate, scale) an existing object. Args: name: Name of the object to transform. location: New (x, y, z) location. None to keep current. rotation: New (x, y, z) rotation in degrees. None to keep current. scale: New (x, y, z) scale. None to keep current. """ script = f""" import bpy import math def transform(): name = "{name}" if name not in bpy.data.objects: return f"Error: Object '{{name}}' not found" obj = bpy.data.objects[name] if {location} is not None: obj.location = {location} if {rotation} is not None: obj.rotation_euler = [math.radians(a) for a in {rotation}] if {scale} is not None: obj.scale = {scale} return f"Transformed {{name}}" transform() """ return json.dumps(send_to_blender({"type": "run_script", "script": script}), separators=(',', ':')) @mcp.tool() def apply_material_preset(object_name: str, preset_name: str, color: tuple[float, float, float] = (0.8, 0.8, 0.8)) -> str: """Apply a material preset to an object. Args: object_name: Name of the object. preset_name: 'GOLD', 'SILVER', 'GLASS', 'PLASTIC', 'RUBBER', 'EMISSION', 'BASIC'. color: RGB color tuple (0-1) for the material (if applicable). """ script = f""" import bpy def apply_preset(): obj_name = "{object_name}" preset = "{preset_name}" col = {color} + (1.0,) # Add alpha if obj_name not in bpy.data.objects: return f"Error: Object '{{obj_name}}' not found" obj = bpy.data.objects[obj_name] mat_name = f"Preset_{{preset}}" if mat_name in bpy.data.materials: mat = bpy.data.materials[mat_name] else: mat = bpy.data.materials.new(name=mat_name) mat.use_nodes = True nodes = mat.node_tree.nodes nodes.clear() output = nodes.new(type='ShaderNodeOutputMaterial') output.location = (300, 0) bsdf = nodes.new(type='ShaderNodeBsdfPrincipled') bsdf.location = (0, 0) mat.node_tree.links.new(bsdf.outputs['BSDF'], output.inputs['Surface']) # Presets logic if preset == 'GOLD': bsdf.inputs['Base Color'].default_value = (1.0, 0.76, 0.33, 1.0) bsdf.inputs['Metallic'].default_value = 1.0 bsdf.inputs['Roughness'].default_value = 0.1 elif preset == 'SILVER': bsdf.inputs['Base Color'].default_value = (0.97, 0.97, 0.97, 1.0) bsdf.inputs['Metallic'].default_value = 1.0 bsdf.inputs['Roughness'].default_value = 0.1 elif preset == 'GLASS': bsdf.inputs['Base Color'].default_value = col bsdf.inputs['Transmission Weight'].default_value = 1.0 bsdf.inputs['Roughness'].default_value = 0.0 bsdf.inputs['IOR'].default_value = 1.45 elif preset == 'PLASTIC': bsdf.inputs['Base Color'].default_value = col bsdf.inputs['Roughness'].default_value = 0.2 bsdf.inputs['Specular IOR Level'].default_value = 0.5 elif preset == 'RUBBER': bsdf.inputs['Base Color'].default_value = col bsdf.inputs['Roughness'].default_value = 0.8 bsdf.inputs['Specular IOR Level'].default_value = 0.2 elif preset == 'EMISSION': nodes.remove(bsdf) emit = nodes.new(type='ShaderNodeEmission') emit.location = (0,0) emit.inputs['Color'].default_value = col emit.inputs['Strength'].default_value = 5.0 mat.node_tree.links.new(emit.outputs['Emission'], output.inputs['Surface']) else: # BASIC bsdf.inputs['Base Color'].default_value = col # Assign if obj.data.materials: obj.data.materials[0] = mat else: obj.data.materials.append(mat) return f"Applied {{preset}} to {{obj_name}}" apply_preset() """ return json.dumps(send_to_blender({"type": "run_script", "script": script}), separators=(',', ':')) @mcp.tool() def scatter_objects(source_obj_name: str, count: int, radius: float, center: tuple[float, float, float] = (0.0, 0.0, 0.0)) -> str: """Scatter copies of an object randomly within a radius. Args: source_obj_name: Name of the object to duplicate. count: Number of copies. radius: Radius of the scatter area. center: Center point (x, y, z). """ script = f""" import bpy import random import math def scatter(): name = "{source_obj_name}" if name not in bpy.data.objects: return f"Error: Object '{{name}}' not found" src_obj = bpy.data.objects[name] created = [] for i in range({count}): # Duplicate new_obj = src_obj.copy() new_obj.data = src_obj.data.copy() new_obj.animation_data_clear() # Random loc r = {radius} * math.sqrt(random.random()) theta = random.random() * 2 * math.pi x = {center}[0] + r * math.cos(theta) y = {center}[1] + r * math.sin(theta) z = {center}[2] # Flat scatter for now new_obj.location = (x, y, z) # Random rotation Z new_obj.rotation_euler[2] = random.random() * 2 * math.pi bpy.context.collection.objects.link(new_obj) created.append(new_obj.name) return f"Scattered {count} copies of {{name}}" scatter() """ return json.dumps(send_to_blender({"type": "run_script", "script": script}), separators=(',', ':')) @mcp.tool() def get_scene_info(limit: int = 50, name_filter: str = "") -> str: """Get a list of objects in the scene. Args: limit: Max number of objects to return (default 50). name_filter: Filter objects by name (contains string). """ script = f""" import bpy import json import math def get_info(): data = [] count = 0 limit = {limit} filter_txt = "{name_filter}".lower() for obj in bpy.data.objects: if count >= limit: break if filter_txt and filter_txt not in obj.name.lower(): continue info = {{ "name": obj.name, "type": obj.type, "location": [round(v, 2) for v in obj.location], "rotation": [round(math.degrees(v), 2) for v in obj.rotation_euler], "scale": [round(v, 2) for v in obj.scale], "visible": not obj.hide_viewport }} data.append(info) count += 1 return json.dumps(data, separators=(',', ':')) get_info() """ return json.dumps(send_to_blender({"type": "run_script", "script": script}), separators=(',', ':')) @mcp.tool() def delete_object(name: str) -> str: """Delete an object by name.""" script = f""" import bpy if "{name}" in bpy.data.objects: bpy.data.objects.remove(bpy.data.objects["{name}"], do_unlink=True) print("Deleted {name}") else: print("Object {name} not found") """ return json.dumps(send_to_blender({"type": "run_script", "script": script}), separators=(',', ':')) @mcp.tool() def clear_scene() -> str: """Delete ALL objects in the scene (Use with caution).""" script = """ import bpy bpy.ops.object.select_all(action='SELECT') bpy.ops.object.delete() """ return json.dumps(send_to_blender({"type": "run_script", "script": script}), separators=(',', ':')) if __name__ == "__main__": try: logger.info("=" * 60) logger.info("Starting Blender MCP Server") logger.info(f"Blender connection: {BLENDER_HOST}:{BLENDER_PORT}") logger.info("=" * 60) mcp.run(transport='stdio') except KeyboardInterrupt: logger.info("Server stopped by user") sys.exit(0) except Exception as e: logger.error(f"Fatal error in MCP server: {e}", exc_info=True) sys.exit(1)

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/mezallastudio/blender-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server