code refactoring: removed create, delete, modify functions

This commit is contained in:
ahujasid 2025-04-03 23:01:20 +02:00
parent 9b3b327853
commit e0e8095892
5 changed files with 13 additions and 451 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

267
addon.py
View File

@ -1,3 +1,5 @@
# Code created by Siddharth Ahuja: www.github.com/ahujasid © 2025
import bpy import bpy
import mathutils import mathutils
import json import json
@ -14,7 +16,7 @@ from bpy.props import StringProperty, IntProperty, BoolProperty, EnumProperty
bl_info = { bl_info = {
"name": "Blender MCP", "name": "Blender MCP",
"author": "BlenderMCP", "author": "BlenderMCP",
"version": (0, 2), "version": (1, 2),
"blender": (3, 0, 0), "blender": (3, 0, 0),
"location": "View3D > Sidebar > BlenderMCP", "location": "View3D > Sidebar > BlenderMCP",
"description": "Connect Blender to Claude via MCP", "description": "Connect Blender to Claude via MCP",
@ -172,18 +174,8 @@ class BlenderMCPServer:
def execute_command(self, command): def execute_command(self, command):
"""Execute a command in the main Blender thread""" """Execute a command in the main Blender thread"""
try: try:
cmd_type = command.get("type") return self._execute_command_internal(command)
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: except Exception as e:
print(f"Error executing command: {str(e)}") print(f"Error executing command: {str(e)}")
@ -202,12 +194,8 @@ class BlenderMCPServer:
# Base handlers that are always available # Base handlers that are always available
handlers = { handlers = {
"get_scene_info": self.get_scene_info, "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, "get_object_info": self.get_object_info,
"execute_code": self.execute_code, "execute_code": self.execute_code,
"set_material": self.set_material,
"get_polyhaven_status": self.get_polyhaven_status, "get_polyhaven_status": self.get_polyhaven_status,
"get_hyper3d_status": self.get_hyper3d_status, "get_hyper3d_status": self.get_hyper3d_status,
} }
@ -246,13 +234,6 @@ class BlenderMCPServer:
return {"status": "error", "message": f"Unknown command type: {cmd_type}"} 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): def get_scene_info(self):
"""Get information about the current Blender scene""" """Get information about the current Blender scene"""
@ -308,140 +289,7 @@ class BlenderMCPServer:
[*min_corner], [*max_corner] [*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): def get_object_info(self, name):
"""Get detailed information about a specific object""" """Get detailed information about a specific object"""
@ -491,108 +339,7 @@ class BlenderMCPServer:
except Exception as e: except Exception as e:
raise Exception(f"Code execution error: {str(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): def get_polyhaven_categories(self, asset_type):
"""Get categories for a specific asset type from Polyhaven""" """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") layout.operator("blendermcp.set_hyper3d_free_trial_api_key", text="Set Free Trial API Key")
if not scene.blendermcp_server_running: 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: 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}") layout.label(text=f"Running on port {scene.blendermcp_port}")
# Operator to set Hyper3D API Key # Operator to set Hyper3D API Key

View File

@ -1,6 +1,6 @@
[project] [project]
name = "blender-mcp" name = "blender-mcp"
version = "1.1.1" version = "1.1.3"
description = "Blender integration through the Model Context Protocol" description = "Blender integration through the Model Context Protocol"
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.10"

View File

@ -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() @mcp.tool()
def execute_blender_code(ctx: Context, code: str) -> str: 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: Parameters:
- code: The Python code to execute - 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. Hyper3D Rodin is good at generating 3D models for single item.
So don't try to: So don't try to:
1. Generate the whole scene with one shot 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 3. Generate parts of the items separately and put them together afterwards
Use get_hyper3d_status() to verify its status 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. 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: 3. Always check the world_bounding_box for each item so that:
- 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:
- Ensure that all objects that should not be clipping are not clipping. - Ensure that all objects that should not be clipping are not clipping.
- Items have right spatial relationship. - 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 - PolyHaven and Hyper3D are disabled
- A simple primitive is explicitly requested - A simple primitive is explicitly requested
- No suitable PolyHaven asset exists - No suitable PolyHaven asset exists

2
uv.lock generated
View File

@ -28,7 +28,7 @@ wheels = [
[[package]] [[package]]
name = "blender-mcp" name = "blender-mcp"
version = "1.1.1" version = "1.1.2"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "mcp", extra = ["cli"] }, { name = "mcp", extra = ["cli"] },