From e0e8095892c98bde96b9c09b4a83d9f1ff191e58 Mon Sep 17 00:00:00 2001 From: ahujasid Date: Thu, 3 Apr 2025 23:01:20 +0200 Subject: [PATCH] code refactoring: removed create, delete, modify functions --- .DS_Store | Bin 0 -> 6148 bytes addon.py | 267 +------------------------------------- pyproject.toml | 2 +- src/blender_mcp/server.py | 193 +-------------------------- uv.lock | 2 +- 5 files changed, 13 insertions(+), 451 deletions(-) create mode 100644 .DS_Store diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..d2299b7103e93be77befe8a72656339e30671c90 GIT binary patch literal 6148 zcmeHK%}T>S5Z<-brW7Fug&r5Y7Ob@uikA@U3mDOZN=;1BV9b^zHHT8jSzpK}@p+ut z-GIfMMeGdhe)GGV{UH0p7~}3DJYvjdj9JhSIVv@R?%L3nNk-&2Mm7&(8G!W>%uVdC z1AcphWh`Y6LGk_j<0#Af{ZGEtXm0Pctd`ZW?z|^ic)6c1GS^RU(7Kc|4l3ObuA*5n zwfD|slKW9IOI1M>&LHLXI!Z!WxN?z%nX2`4z-n9Vsoh;JN26g+3Pb|lSeoqXK zPgbk8wSRDUdNF;DUlRGI>Eyt+l3jxpyn|v^^XjEZER#p@RM}M)Au&J<5Cg=(W;0;U z1*@~!G|=ja0b-zr0o)%1G(^W>sZnhm(BbtN<4r^q(D5ySC=5CVON|f#;kp!1mvZyO z;JO_A!sIyyOO3jmaWylHV`i=%FI>$IexcGCcQjH@3=jkB3^cT9#GAOVD08;{SRS6W0@^(^6wE780ResO5&#D7BV85Lae+GIIR;COI12hz QIUrpG6d}|R1HZt)7jWE3hyVZp literal 0 HcmV?d00001 diff --git a/addon.py b/addon.py index 65648b1..95cdb9d 100644 --- a/addon.py +++ b/addon.py @@ -1,3 +1,5 @@ +# Code created by Siddharth Ahuja: www.github.com/ahujasid © 2025 + import bpy import mathutils import json @@ -14,7 +16,7 @@ from bpy.props import StringProperty, IntProperty, BoolProperty, EnumProperty bl_info = { "name": "Blender MCP", "author": "BlenderMCP", - "version": (0, 2), + "version": (1, 2), "blender": (3, 0, 0), "location": "View3D > Sidebar > BlenderMCP", "description": "Connect Blender to Claude via MCP", @@ -172,18 +174,8 @@ class BlenderMCPServer: def execute_command(self, command): """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) + try: + return self._execute_command_internal(command) except Exception as e: print(f"Error executing command: {str(e)}") @@ -202,12 +194,8 @@ class BlenderMCPServer: # Base handlers that are always available handlers = { "get_scene_info": self.get_scene_info, - "create_object": self.create_object, - "modify_object": self.modify_object, - "delete_object": self.delete_object, "get_object_info": self.get_object_info, "execute_code": self.execute_code, - "set_material": self.set_material, "get_polyhaven_status": self.get_polyhaven_status, "get_hyper3d_status": self.get_hyper3d_status, } @@ -246,13 +234,6 @@ class BlenderMCPServer: return {"status": "error", "message": f"Unknown command type: {cmd_type}"} - def get_simple_info(self): - """Get basic Blender information""" - return { - "blender_version": ".".join(str(v) for v in bpy.app.version), - "scene_name": bpy.context.scene.name, - "object_count": len(bpy.context.scene.objects) - } def get_scene_info(self): """Get information about the current Blender scene""" @@ -308,140 +289,7 @@ class BlenderMCPServer: [*min_corner], [*max_corner] ] - def create_object(self, type="CUBE", name=None, location=(0, 0, 0), rotation=(0, 0, 0), scale=(1, 1, 1), - align="WORLD", major_segments=48, minor_segments=12, mode="MAJOR_MINOR", - major_radius=1.0, minor_radius=0.25, abso_major_rad=1.25, abso_minor_rad=0.75, generate_uvs=True): - """Create a new object in the scene""" - try: - # Deselect all objects first - bpy.ops.object.select_all(action='DESELECT') - - # Create the object based on type - if type == "CUBE": - bpy.ops.mesh.primitive_cube_add(location=location, rotation=rotation, scale=scale) - elif type == "SPHERE": - bpy.ops.mesh.primitive_uv_sphere_add(location=location, rotation=rotation, scale=scale) - elif type == "CYLINDER": - bpy.ops.mesh.primitive_cylinder_add(location=location, rotation=rotation, scale=scale) - elif type == "PLANE": - bpy.ops.mesh.primitive_plane_add(location=location, rotation=rotation, scale=scale) - elif type == "CONE": - bpy.ops.mesh.primitive_cone_add(location=location, rotation=rotation, scale=scale) - elif type == "TORUS": - bpy.ops.mesh.primitive_torus_add( - align=align, - location=location, - rotation=rotation, - major_segments=major_segments, - minor_segments=minor_segments, - mode=mode, - major_radius=major_radius, - minor_radius=minor_radius, - abso_major_rad=abso_major_rad, - abso_minor_rad=abso_minor_rad, - generate_uvs=generate_uvs - ) - elif type == "EMPTY": - bpy.ops.object.empty_add(location=location, rotation=rotation, scale=scale) - elif type == "CAMERA": - bpy.ops.object.camera_add(location=location, rotation=rotation) - elif type == "LIGHT": - bpy.ops.object.light_add(type='POINT', location=location, rotation=rotation, scale=scale) - else: - raise ValueError(f"Unsupported object type: {type}") - - # Force update the view layer - bpy.context.view_layer.update() - - # Get the active object (which should be our newly created object) - obj = bpy.context.view_layer.objects.active - - # If we don't have an active object, something went wrong - if obj is None: - raise RuntimeError("Failed to create object - no active object") - - # Make sure it's selected - obj.select_set(True) - - # Rename if name is provided - if name: - obj.name = name - if obj.data: - obj.data.name = name - - # Patch for PLANE: scale don't work with bpy.ops.mesh.primitive_plane_add() - if type in {"PLANE"}: - obj.scale = scale - # Return the object info - result = { - "name": obj.name, - "type": obj.type, - "location": [obj.location.x, obj.location.y, obj.location.z], - "rotation": [obj.rotation_euler.x, obj.rotation_euler.y, obj.rotation_euler.z], - "scale": [obj.scale.x, obj.scale.y, obj.scale.z], - } - - if obj.type == "MESH": - bounding_box = self._get_aabb(obj) - result["world_bounding_box"] = bounding_box - - return result - except Exception as e: - print(f"Error in create_object: {str(e)}") - traceback.print_exc() - return {"error": str(e)} - - def modify_object(self, name, location=None, rotation=None, scale=None, visible=None): - """Modify an existing object in the scene""" - # Find the object by name - obj = bpy.data.objects.get(name) - if not obj: - raise ValueError(f"Object not found: {name}") - - # Modify properties as requested - if location is not None: - obj.location = location - - if rotation is not None: - obj.rotation_euler = rotation - - if scale is not None: - obj.scale = scale - - if visible is not None: - obj.hide_viewport = not visible - obj.hide_render = not visible - - result = { - "name": obj.name, - "type": obj.type, - "location": [obj.location.x, obj.location.y, obj.location.z], - "rotation": [obj.rotation_euler.x, obj.rotation_euler.y, obj.rotation_euler.z], - "scale": [obj.scale.x, obj.scale.y, obj.scale.z], - "visible": obj.visible_get(), - } - - if obj.type == "MESH": - bounding_box = self._get_aabb(obj) - result["world_bounding_box"] = bounding_box - - return result - - def delete_object(self, name): - """Delete an object from the scene""" - obj = bpy.data.objects.get(name) - if not obj: - raise ValueError(f"Object not found: {name}") - - # Store the name to return - obj_name = obj.name - - # Select and delete the object - if obj: - bpy.data.objects.remove(obj, do_unlink=True) - - return {"deleted": obj_name} def get_object_info(self, name): """Get detailed information about a specific object""" @@ -491,108 +339,7 @@ class BlenderMCPServer: except Exception as e: raise Exception(f"Code execution error: {str(e)}") - def set_material(self, object_name, material_name=None, create_if_missing=True, color=None): - """Set or create a material for an object""" - try: - # Get the object - obj = bpy.data.objects.get(object_name) - if not obj: - raise ValueError(f"Object not found: {object_name}") - - # Make sure object can accept materials - if not hasattr(obj, 'data') or not hasattr(obj.data, 'materials'): - raise ValueError(f"Object {object_name} cannot accept materials") - - # Create or get material - if material_name: - mat = bpy.data.materials.get(material_name) - if not mat and create_if_missing: - mat = bpy.data.materials.new(name=material_name) - print(f"Created new material: {material_name}") - else: - # Generate unique material name if none provided - mat_name = f"{object_name}_material" - mat = bpy.data.materials.get(mat_name) - if not mat: - mat = bpy.data.materials.new(name=mat_name) - material_name = mat_name - print(f"Using material: {mat_name}") - - # Set up material nodes if needed - if mat: - if not mat.use_nodes: - mat.use_nodes = True - - # Get or create Principled BSDF - principled = mat.node_tree.nodes.get('Principled BSDF') - if not principled: - principled = mat.node_tree.nodes.new('ShaderNodeBsdfPrincipled') - # Get or create Material Output - output = mat.node_tree.nodes.get('Material Output') - if not output: - output = mat.node_tree.nodes.new('ShaderNodeOutputMaterial') - # Link if not already linked - if not principled.outputs[0].links: - mat.node_tree.links.new(principled.outputs[0], output.inputs[0]) - - # Set color if provided - if color and len(color) >= 3: - principled.inputs['Base Color'].default_value = ( - color[0], - color[1], - color[2], - 1.0 if len(color) < 4 else color[3] - ) - print(f"Set material color to {color}") - - # Assign material to object if not already assigned - if mat: - if not obj.data.materials: - obj.data.materials.append(mat) - else: - # Only modify first material slot - obj.data.materials[0] = mat - - print(f"Assigned material {mat.name} to object {object_name}") - - return { - "status": "success", - "object": object_name, - "material": mat.name, - "color": color if color else None - } - else: - raise ValueError(f"Failed to create or find material: {material_name}") - - except Exception as e: - print(f"Error in set_material: {str(e)}") - traceback.print_exc() - return { - "status": "error", - "message": str(e), - "object": object_name, - "material": material_name if 'material_name' in locals() else None - } - def render_scene(self, output_path=None, resolution_x=None, resolution_y=None): - """Render the current scene""" - if resolution_x is not None: - bpy.context.scene.render.resolution_x = resolution_x - - if resolution_y is not None: - bpy.context.scene.render.resolution_y = resolution_y - - if output_path: - bpy.context.scene.render.filepath = output_path - - # Render the scene - bpy.ops.render.render(write_still=bool(output_path)) - - return { - "rendered": True, - "output_path": output_path if output_path else "[not saved]", - "resolution": [bpy.context.scene.render.resolution_x, bpy.context.scene.render.resolution_y], - } def get_polyhaven_categories(self, asset_type): """Get categories for a specific asset type from Polyhaven""" @@ -1630,9 +1377,9 @@ class BLENDERMCP_PT_Panel(bpy.types.Panel): layout.operator("blendermcp.set_hyper3d_free_trial_api_key", text="Set Free Trial API Key") if not scene.blendermcp_server_running: - layout.operator("blendermcp.start_server", text="Start MCP Server") + layout.operator("blendermcp.start_server", text="Connect to MCP server") else: - layout.operator("blendermcp.stop_server", text="Stop MCP Server") + layout.operator("blendermcp.stop_server", text="Disconnect from MCP server") layout.label(text=f"Running on port {scene.blendermcp_port}") # Operator to set Hyper3D API Key diff --git a/pyproject.toml b/pyproject.toml index bc901fb..f782330 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "blender-mcp" -version = "1.1.1" +version = "1.1.3" description = "Blender integration through the Model Context Protocol" readme = "README.md" requires-python = ">=3.10" diff --git a/src/blender_mcp/server.py b/src/blender_mcp/server.py index 09f7954..6614e10 100644 --- a/src/blender_mcp/server.py +++ b/src/blender_mcp/server.py @@ -268,186 +268,10 @@ def get_object_info(ctx: Context, object_name: str) -> str: -@mcp.tool() -def create_object( - ctx: Context, - type: str = "CUBE", - name: str = None, - location: List[float] = None, - rotation: List[float] = None, - scale: List[float] = None, - # Torus-specific parameters - align: str = "WORLD", - major_segments: int = 48, - minor_segments: int = 12, - mode: str = "MAJOR_MINOR", - major_radius: float = 1.0, - minor_radius: float = 0.25, - abso_major_rad: float = 1.25, - abso_minor_rad: float = 0.75, - generate_uvs: bool = True -) -> str: - """ - Create a new object in the Blender scene. - - Parameters: - - type: Object type (CUBE, SPHERE, CYLINDER, PLANE, CONE, TORUS, EMPTY, CAMERA, LIGHT) - - name: Optional name for the object - - location: Optional [x, y, z] location coordinates - - rotation: Optional [x, y, z] rotation in radians - - scale: Optional [x, y, z] scale factors (not used for TORUS) - - Torus-specific parameters (only used when type == "TORUS"): - - align: How to align the torus ('WORLD', 'VIEW', or 'CURSOR') - - major_segments: Number of segments for the main ring - - minor_segments: Number of segments for the cross-section - - mode: Dimension mode ('MAJOR_MINOR' or 'EXT_INT') - - major_radius: Radius from the origin to the center of the cross sections - - minor_radius: Radius of the torus' cross section - - abso_major_rad: Total exterior radius of the torus - - abso_minor_rad: Total interior radius of the torus - - generate_uvs: Whether to generate a default UV map - - Returns: - A message indicating the created object name. - """ - try: - # Get the global connection - blender = get_blender_connection() - - # Set default values for missing parameters - loc = location or [0, 0, 0] - rot = rotation or [0, 0, 0] - sc = scale or [1, 1, 1] - - params = { - "type": type, - "location": loc, - "rotation": rot, - } - - if name: - params["name"] = name - - if type == "TORUS": - # For torus, the scale is not used. - params.update({ - "align": align, - "major_segments": major_segments, - "minor_segments": minor_segments, - "mode": mode, - "major_radius": major_radius, - "minor_radius": minor_radius, - "abso_major_rad": abso_major_rad, - "abso_minor_rad": abso_minor_rad, - "generate_uvs": generate_uvs - }) - result = blender.send_command("create_object", params) - return f"Created {type} object: {result['name']}" - else: - # For non-torus objects, include scale - params["scale"] = sc - result = blender.send_command("create_object", params) - return f"Created {type} object: {result['name']}" - except Exception as e: - logger.error(f"Error creating object: {str(e)}") - return f"Error creating object: {str(e)}" - - -@mcp.tool() -def modify_object( - ctx: Context, - name: str, - location: List[float] = None, - rotation: List[float] = None, - scale: List[float] = None, - visible: bool = None -) -> str: - """ - Modify an existing object in the Blender scene. - - Parameters: - - name: Name of the object to modify - - location: Optional [x, y, z] location coordinates - - rotation: Optional [x, y, z] rotation in radians - - scale: Optional [x, y, z] scale factors - - visible: Optional boolean to set visibility - """ - try: - # Get the global connection - blender = get_blender_connection() - - params = {"name": name} - - if location is not None: - params["location"] = location - if rotation is not None: - params["rotation"] = rotation - if scale is not None: - params["scale"] = scale - if visible is not None: - params["visible"] = visible - - result = blender.send_command("modify_object", params) - return f"Modified object: {result['name']}" - except Exception as e: - logger.error(f"Error modifying object: {str(e)}") - return f"Error modifying object: {str(e)}" - -@mcp.tool() -def delete_object(ctx: Context, name: str) -> str: - """ - Delete an object from the Blender scene. - - Parameters: - - name: Name of the object to delete - """ - try: - # Get the global connection - blender = get_blender_connection() - - result = blender.send_command("delete_object", {"name": name}) - return f"Deleted object: {name}" - except Exception as e: - logger.error(f"Error deleting object: {str(e)}") - return f"Error deleting object: {str(e)}" - -@mcp.tool() -def set_material( - ctx: Context, - object_name: str, - material_name: str = None, - color: List[float] = None -) -> str: - """ - Set or create a material for an object. - - Parameters: - - object_name: Name of the object to apply the material to - - material_name: Optional name of the material to use or create - - color: Optional [R, G, B] color values (0.0-1.0) - """ - try: - # Get the global connection - blender = get_blender_connection() - - params = {"object_name": object_name} - - if material_name: - params["material_name"] = material_name - if color: - params["color"] = color - - result = blender.send_command("set_material", params) - return f"Applied material to {object_name}: {result.get('material_name', 'unknown')}" - except Exception as e: - logger.error(f"Error setting material: {str(e)}") - return f"Error setting material: {str(e)}" - @mcp.tool() def execute_blender_code(ctx: Context, code: str) -> str: """ - Execute arbitrary Python code in Blender. + Execute arbitrary Python code in Blender. Make sure to do it step-by-step by breaking it into smaller chunks. Parameters: - code: The Python code to execute @@ -885,7 +709,7 @@ def asset_creation_strategy() -> str: Hyper3D Rodin is good at generating 3D models for single item. So don't try to: 1. Generate the whole scene with one shot - 2. Generate ground using Rodin + 2. Generate ground using Hyper3D 3. Generate parts of the items separately and put them together afterwards Use get_hyper3d_status() to verify its status @@ -907,21 +731,12 @@ def asset_creation_strategy() -> str: You can reuse assets previous generated by running python code to duplicate the object, without creating another generation task. - 2. If all integrations are disabled or when falling back to basic tools: - - create_object() for basic primitives (CUBE, SPHERE, CYLINDER, etc.) - - set_material() for basic colors and materials - - 3. When including an object into scene, ALWAYS make sure that the name of the object is meanful. - - 4. Always check the world_bounding_box for each item so that: + 3. Always check the world_bounding_box for each item so that: - Ensure that all objects that should not be clipping are not clipping. - Items have right spatial relationship. - 5. After giving the tool location/scale/rotation information (via create_object() and modify_object()), - double check the related object's location, scale, rotation, and world_bounding_box using get_object_info(), - so that the object is in the desired location. - Only fall back to basic creation tools when: + Only fall back to scripting when: - PolyHaven and Hyper3D are disabled - A simple primitive is explicitly requested - No suitable PolyHaven asset exists diff --git a/uv.lock b/uv.lock index 9fda30a..7f7e6bc 100644 --- a/uv.lock +++ b/uv.lock @@ -28,7 +28,7 @@ wheels = [ [[package]] name = "blender-mcp" -version = "1.1.1" +version = "1.1.2" source = { editable = "." } dependencies = [ { name = "mcp", extra = ["cli"] },