diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..e468bfd Binary files /dev/null and b/.DS_Store differ diff --git a/blender_mcp_addon.py b/blender_mcp_addon.py index 2e36a08..8336384 100644 --- a/blender_mcp_addon.py +++ b/blender_mcp_addon.py @@ -22,104 +22,124 @@ class BlenderMCPServer: self.running = False self.socket = None self.client = None - self.server_thread = None + self.command_queue = [] + self.buffer = b'' # Add buffer for incomplete data def start(self): self.running = True - self.server_thread = threading.Thread(target=self._run_server) - self.server_thread.daemon = True - self.server_thread.start() - print(f"BlenderMCP server started on {self.host}:{self.port}") - - def stop(self): - self.running = False - if self.socket: - self.socket.close() - if self.client: - self.client.close() - print("BlenderMCP server stopped") - - def _run_server(self): self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) try: self.socket.bind((self.host, self.port)) self.socket.listen(1) - self.socket.settimeout(1.0) # Add a timeout for accept + self.socket.setblocking(False) + # Register the timer + bpy.app.timers.register(self._process_server, persistent=True) + print(f"BlenderMCP server started on {self.host}:{self.port}") + except Exception as e: + print(f"Failed to start server: {str(e)}") + self.stop() - while self.running: + def stop(self): + self.running = False + if hasattr(bpy.app.timers, "unregister"): + if bpy.app.timers.is_registered(self._process_server): + bpy.app.timers.unregister(self._process_server) + if self.socket: + self.socket.close() + if self.client: + self.client.close() + self.socket = None + self.client = None + print("BlenderMCP server stopped") + + def _process_server(self): + """Timer callback to process server operations""" + if not self.running: + return None # Unregister timer + + try: + # Accept new connections + if not self.client and self.socket: try: self.client, address = self.socket.accept() + self.client.setblocking(False) print(f"Connected to client: {address}") - - while self.running: - try: - # Set a timeout for receiving data - self.client.settimeout(15.0) - data = self.client.recv(8192) # Increased buffer size - if not data: - print("Empty data received, client may have disconnected") - break - - try: - print(f"Received data: {data.decode('utf-8')}") - command = json.loads(data.decode('utf-8')) - - # Process the command - print(f"Processing command: {command.get('type')}") - response = self.execute_command(command) - - # Send the response - handle large responses by chunking if needed - response_json = json.dumps(response) - print(f"Sending response: {response_json[:100]}...") # Truncate long responses in log - - # Send in one go - most responses should be small enough - self.client.sendall(response_json.encode('utf-8')) - print("Response sent successfully") - - except json.JSONDecodeError: - print(f"Invalid JSON received: {data.decode('utf-8')}") - self.client.sendall(json.dumps({ - "status": "error", - "message": "Invalid JSON format" - }).encode('utf-8')) - except Exception as e: - print(f"Error executing command: {str(e)}") - import traceback - traceback.print_exc() - self.client.sendall(json.dumps({ - "status": "error", - "message": str(e) - }).encode('utf-8')) - except socket.timeout: - print("Socket timeout while waiting for data") - continue - except Exception as e: - print(f"Error receiving data: {str(e)}") - break - - if self.client: - self.client.close() - self.client = None - except socket.timeout: - # This is normal - just continue the loop - continue + except BlockingIOError: + pass # No connection waiting except Exception as e: - print(f"Connection error: {str(e)}") + print(f"Error accepting connection: {str(e)}") + + # Process existing connection + if self.client: + try: + # Try to receive data + try: + data = self.client.recv(8192) + if data: + self.buffer += data + # Try to process complete messages + try: + # Attempt to parse the buffer as JSON + command = json.loads(self.buffer.decode('utf-8')) + # If successful, clear the buffer and process command + self.buffer = b'' + response = self.execute_command(command) + response_json = json.dumps(response) + self.client.sendall(response_json.encode('utf-8')) + except json.JSONDecodeError: + # Incomplete data, keep in buffer + pass + else: + # Connection closed by client + print("Client disconnected") + self.client.close() + self.client = None + self.buffer = b'' + except BlockingIOError: + pass # No data available + except Exception as e: + print(f"Error receiving data: {str(e)}") + self.client.close() + self.client = None + self.buffer = b'' + + except Exception as e: + print(f"Error with client: {str(e)}") if self.client: self.client.close() self.client = None - time.sleep(1) # Prevent busy waiting - + self.buffer = b'' + except Exception as e: print(f"Server error: {str(e)}") - finally: - if self.socket: - self.socket.close() - + + return 0.1 # Continue timer with 0.1 second interval + def execute_command(self, command): - """Execute a Blender command received from the MCP server""" + """Execute a command in the main Blender thread""" + try: + cmd_type = command.get("type") + params = command.get("params", {}) + + # Ensure we're in the right context + if cmd_type in ["create_object", "modify_object", "delete_object"]: + override = bpy.context.copy() + override['area'] = [area for area in bpy.context.screen.areas if area.type == 'VIEW_3D'][0] + with bpy.context.temp_override(**override): + return self._execute_command_internal(command) + else: + return self._execute_command_internal(command) + + except Exception as e: + print(f"Error executing command: {str(e)}") + import traceback + traceback.print_exc() + return {"status": "error", "message": str(e)} + + def _execute_command_internal(self, command): + """Internal command execution with proper context""" cmd_type = command.get("type") params = command.get("params", {}) diff --git a/blender_mcp_server.py b/blender_mcp_server.py index 3243644..730413f 100644 --- a/blender_mcp_server.py +++ b/blender_mcp_server.py @@ -279,6 +279,66 @@ def get_object_info(object_name: str) -> str: # Tool endpoints +@mcp.tool() +def create_primitive( + ctx: Context, + type: str = "CUBE", + location: List[float] = None, + color: List[float] = None +) -> str: + """ + Create a basic primitive object in Blender. + + Parameters: + - type: Object type (CUBE, SPHERE, CYLINDER, PLANE) + - location: Optional [x, y, z] location coordinates + - color: Optional [R, G, B] color values (0.0-1.0) + """ + try: + blender = get_blender_connection() + loc = location or [0, 0, 0] + + # First create the object + params = { + "type": type, + "location": loc + } + result = blender.send_command("create_object", params) + + # If color specified, set the material + if color: + blender.send_command("set_material", { + "object_name": result["name"], + "color": color + }) + + return f"Created {type} at location {loc}" + except Exception as e: + return f"Error creating primitive: {str(e)}" + +@mcp.tool() +def set_object_property( + ctx: Context, + name: str, + property: str, + value: Any +) -> str: + """ + Set a single property of an object. + + Parameters: + - name: Name of the object + - property: Property to set (location, rotation, scale, color, visible) + - value: New value for the property + """ + try: + blender = get_blender_connection() + params = {"name": name, property: value} + result = blender.send_command("modify_object", params) + return f"Set {property} of {name} to {value}" + except Exception as e: + return f"Error setting property: {str(e)}" + @mcp.tool() def create_object( ctx: Context, @@ -469,130 +529,15 @@ def execute_blender_code(ctx: Context, code: str) -> str: logger.error(f"Error executing code: {str(e)}") return f"Error executing code: {str(e)}" -@mcp.tool() -def create_3d_scene(ctx: Context, description: str) -> str: - """ - Create a 3D scene based on a text description. - - This is a higher-level tool that will interpret the description and create - appropriate objects, materials, and lighting. - - Parameters: - - description: Text description of the scene to create - """ - try: - # Get the global connection - blender = get_blender_connection() - - # Parse the description and create a scene - # This is a simplified implementation - in a real tool, you would use more - # sophisticated parsing and scene generation logic - - # For now, we'll just create a simple scene with a few objects - - # Clear existing objects (optional) - try: - blender.send_command("execute_code", { - "code": """ -import bpy -# Delete all objects except camera and light -for obj in bpy.data.objects: - if obj.type not in ['CAMERA', 'LIGHT']: - bpy.data.objects.remove(obj) -""" - }) - except Exception as e: - logger.warning(f"Error clearing scene: {str(e)}") - - # Create a simple scene based on the description - objects_created = [] - - # Add a ground plane - try: - result = blender.send_command("create_object", { - "type": "PLANE", - "name": "Ground", - "location": [0, 0, 0], - "scale": [5, 5, 1] - }) - objects_created.append(result["name"]) - - # Set a material for the ground - blender.send_command("set_material", { - "object_name": "Ground", - "material_name": "GroundMaterial", - "color": [0.8, 0.8, 0.8] - }) - except Exception as e: - logger.warning(f"Error creating ground: {str(e)}") - - # Simple keyword-based object creation - if "cube" in description.lower(): - try: - result = blender.send_command("create_object", { - "type": "CUBE", - "name": "Cube", - "location": [0, 0, 1] - }) - objects_created.append(result["name"]) - except Exception as e: - logger.warning(f"Error creating cube: {str(e)}") - - if "sphere" in description.lower(): - try: - result = blender.send_command("create_object", { - "type": "SPHERE", - "name": "Sphere", - "location": [2, 0, 1] - }) - objects_created.append(result["name"]) - except Exception as e: - logger.warning(f"Error creating sphere: {str(e)}") - - if "cylinder" in description.lower(): - try: - result = blender.send_command("create_object", { - "type": "CYLINDER", - "name": "Cylinder", - "location": [-2, 0, 1] - }) - objects_created.append(result["name"]) - except Exception as e: - logger.warning(f"Error creating cylinder: {str(e)}") - - return f"Created scene with objects: {', '.join(objects_created)}" - except Exception as e: - logger.error(f"Error creating scene: {str(e)}") - return f"Error creating scene: {str(e)}" - -# Prompts to help users interact with Blender +@mcp.prompt() +def create_basic_object() -> str: + """Create a single object with basic properties""" + return """Create a blue cube at position [0, 1, 0]""" @mcp.prompt() -def create_simple_scene() -> str: - """Create a simple Blender scene with basic objects""" - return """ -I'd like to create a simple scene in Blender. Please create: -1. A ground plane -2. A cube above the ground -3. A sphere to the side -4. Make sure there's a camera and light -5. Set different colors for the objects -""" - -@mcp.prompt() -def animate_object() -> str: - """Create keyframe animation for an object""" - return """ -I want to animate a cube moving from point A to point B over 30 frames. -Can you help me create this animation? -""" - -@mcp.prompt() -def add_material() -> str: - """Add a material to an object""" - return """ -I have a cube in my scene. Can you create a blue metallic material and apply it to the cube? -""" +def modify_basic_object() -> str: + """Modify a single property of an object""" + return """Make the cube red""" # Main execution