at least it isnt crashing now? somewhat works!
This commit is contained in:
parent
2efcd9c7f6
commit
31bce0dfbd
@ -22,104 +22,124 @@ class BlenderMCPServer:
|
|||||||
self.running = False
|
self.running = False
|
||||||
self.socket = None
|
self.socket = None
|
||||||
self.client = None
|
self.client = None
|
||||||
self.server_thread = None
|
self.command_queue = []
|
||||||
|
self.buffer = b'' # Add buffer for incomplete data
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
self.running = True
|
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 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.socket.bind((self.host, self.port))
|
self.socket.bind((self.host, self.port))
|
||||||
self.socket.listen(1)
|
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:
|
try:
|
||||||
self.client, address = self.socket.accept()
|
self.client, address = self.socket.accept()
|
||||||
|
self.client.setblocking(False)
|
||||||
print(f"Connected to client: {address}")
|
print(f"Connected to client: {address}")
|
||||||
|
except BlockingIOError:
|
||||||
|
pass # No connection waiting
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error accepting connection: {str(e)}")
|
||||||
|
|
||||||
while self.running:
|
# Process existing connection
|
||||||
|
if self.client:
|
||||||
try:
|
try:
|
||||||
# Set a timeout for receiving data
|
# Try to receive 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:
|
try:
|
||||||
print(f"Received data: {data.decode('utf-8')}")
|
data = self.client.recv(8192)
|
||||||
command = json.loads(data.decode('utf-8'))
|
if data:
|
||||||
|
self.buffer += data
|
||||||
# Process the command
|
# Try to process complete messages
|
||||||
print(f"Processing command: {command.get('type')}")
|
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 = self.execute_command(command)
|
||||||
|
|
||||||
# Send the response - handle large responses by chunking if needed
|
|
||||||
response_json = json.dumps(response)
|
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'))
|
self.client.sendall(response_json.encode('utf-8'))
|
||||||
print("Response sent successfully")
|
|
||||||
|
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
print(f"Invalid JSON received: {data.decode('utf-8')}")
|
# Incomplete data, keep in buffer
|
||||||
self.client.sendall(json.dumps({
|
pass
|
||||||
"status": "error",
|
else:
|
||||||
"message": "Invalid JSON format"
|
# Connection closed by client
|
||||||
}).encode('utf-8'))
|
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
|
||||||
|
self.buffer = b''
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Server error: {str(e)}")
|
||||||
|
|
||||||
|
return 0.1 # Continue timer with 0.1 second interval
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error executing command: {str(e)}")
|
print(f"Error executing command: {str(e)}")
|
||||||
import traceback
|
import traceback
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
self.client.sendall(json.dumps({
|
return {"status": "error", "message": str(e)}
|
||||||
"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:
|
def _execute_command_internal(self, command):
|
||||||
self.client.close()
|
"""Internal command execution with proper context"""
|
||||||
self.client = None
|
|
||||||
except socket.timeout:
|
|
||||||
# This is normal - just continue the loop
|
|
||||||
continue
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Connection error: {str(e)}")
|
|
||||||
if self.client:
|
|
||||||
self.client.close()
|
|
||||||
self.client = None
|
|
||||||
time.sleep(1) # Prevent busy waiting
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Server error: {str(e)}")
|
|
||||||
finally:
|
|
||||||
if self.socket:
|
|
||||||
self.socket.close()
|
|
||||||
|
|
||||||
def execute_command(self, command):
|
|
||||||
"""Execute a Blender command received from the MCP server"""
|
|
||||||
cmd_type = command.get("type")
|
cmd_type = command.get("type")
|
||||||
params = command.get("params", {})
|
params = command.get("params", {})
|
||||||
|
|
||||||
|
|||||||
@ -279,6 +279,66 @@ def get_object_info(object_name: str) -> str:
|
|||||||
|
|
||||||
# Tool endpoints
|
# 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()
|
@mcp.tool()
|
||||||
def create_object(
|
def create_object(
|
||||||
ctx: Context,
|
ctx: Context,
|
||||||
@ -469,130 +529,15 @@ def execute_blender_code(ctx: Context, code: str) -> str:
|
|||||||
logger.error(f"Error executing code: {str(e)}")
|
logger.error(f"Error executing code: {str(e)}")
|
||||||
return f"Error executing code: {str(e)}"
|
return f"Error executing code: {str(e)}"
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.prompt()
|
||||||
def create_3d_scene(ctx: Context, description: str) -> str:
|
def create_basic_object() -> str:
|
||||||
"""
|
"""Create a single object with basic properties"""
|
||||||
Create a 3D scene based on a text description.
|
return """Create a blue cube at position [0, 1, 0]"""
|
||||||
|
|
||||||
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()
|
@mcp.prompt()
|
||||||
def create_simple_scene() -> str:
|
def modify_basic_object() -> str:
|
||||||
"""Create a simple Blender scene with basic objects"""
|
"""Modify a single property of an object"""
|
||||||
return """
|
return """Make the cube red"""
|
||||||
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?
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Main execution
|
# Main execution
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user