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.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", {})
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user