Skip to main content
Glama
robot_model_tools.py46 kB
"""Robot model import/export tools for robotics-mcp. Handles robot 3D model formats (FBX, GLB, OBJ) for virtual robotics. Note: VRM format is for humanoid avatars only - use FBX/GLB for wheeled robots. Integrates with: - blender-mcp: For creating/editing 3D models and exporting to FBX/GLB/OBJ - gimp-mcp: For creating/editing textures and images for robot models """ from pathlib import Path from typing import Any, Dict, Literal, Optional import structlog from fastmcp import Client from ..utils.error_handler import format_error_response, format_success_response, handle_tool_error logger = structlog.get_logger(__name__) # Supported formats for robots ROBOT_MODEL_FORMATS = ["fbx", "glb", "obj", "blend"] # Note: VRM only for humanoid robots HUMANOID_ROBOT_TYPES = ["robbie", "g1"] # Robots that CAN use VRM NON_HUMANOID_ROBOT_TYPES = ["scout", "scout_e", "go2"] # Robots that should NOT use VRM class RobotModelTools: """Tools for importing, exporting, and converting robot 3D models.""" def __init__(self, mcp: Any, state_manager: Any, mounted_servers: Optional[Dict[str, Any]] = None): """Initialize robot model tools. Args: mcp: FastMCP server instance. state_manager: Robot state manager instance. mounted_servers: Dictionary of mounted MCP servers. """ self.mcp = mcp self.state_manager = state_manager self.mounted_servers = mounted_servers or {} def register(self): """Register robot model portmanteau tool with MCP server.""" @self.mcp.tool() async def robot_model( operation: Literal["create", "import", "export", "convert"], robot_type: Optional[str] = None, model_path: Optional[str] = None, output_path: Optional[str] = None, format: Literal["fbx", "glb", "obj", "vrm", "blend"] = "fbx", platform: Literal["unity", "vrchat", "resonite"] = "unity", dimensions: Optional[Dict[str, float]] = None, create_textures: bool = True, texture_style: Literal["realistic", "stylized", "simple"] = "realistic", robot_id: Optional[str] = None, include_animations: bool = True, project_path: Optional[str] = None, create_prefab: bool = True, source_path: Optional[str] = None, source_format: Optional[Literal["fbx", "glb", "obj", "blend", "vrm"]] = None, target_format: Optional[Literal["fbx", "glb", "obj", "vrm"]] = None, target_path: Optional[str] = None, ) -> Dict[str, Any]: """Robot model management portmanteau for Robotics MCP. PORTMANTEAU PATTERN RATIONALE: Instead of creating 4 separate tools (create, import, export, convert), this tool consolidates related model operations into a single interface. This design: - Prevents tool explosion (4 tools → 1 tool) while maintaining full functionality - Improves discoverability by grouping related operations together - Reduces cognitive load when working with robot models - Enables consistent model interface across all operations - Follows FastMCP 2.13+ best practices for feature-rich MCP servers SUPPORTED OPERATIONS: - create: Create robot 3D model from scratch using Blender MCP - import: Import robot 3D model into Unity/VRChat/Resonite project - export: Export robot model from Unity to file format - convert: Convert robot model between formats Args: operation: The model operation to perform. MUST be one of: - "create": Create model (requires: robot_type, output_path) - "import": Import model (requires: robot_type, model_path) - "export": Export model (requires: robot_id) - "convert": Convert model (requires: source_path, source_format, target_format) robot_type: Type of robot (required for create/import). Examples: "scout", "go2", "g1", "robbie", "custom" model_path: Path to model file (required for import). output_path: Path for output file (required for create, optional for export/convert). format: Model format (used by create/import/export). - "fbx": Industry standard (recommended for robots) - "glb": Modern glTF 2.0 format - "obj": Simple mesh format - "vrm": VRM format (ONLY for humanoid robots) platform: Target platform for import (unity/vrchat/resonite). dimensions: Custom dimensions for create (length, width, height in meters). create_textures: Create textures using gimp-mcp (for create). texture_style: Texture style for create (realistic/stylized/simple). robot_id: Virtual robot identifier (required for export). include_animations: Include animations in export. project_path: Unity project path (for import). create_prefab: Create Unity prefab after import. source_path: Source file path (required for convert). source_format: Source file format (required for convert). target_format: Target file format (required for convert). target_path: Output file path (optional for convert). Returns: Dictionary containing operation-specific results. Examples: Create Scout model: result = await robot_model( operation="create", robot_type="scout", output_path="D:/Models/scout_model.fbx", format="fbx" ) Import model to Unity: result = await robot_model( operation="import", robot_type="scout", model_path="D:/Models/scout_model.fbx", platform="unity" ) Export robot from Unity: result = await robot_model( operation="export", robot_id="vbot_scout_01", format="fbx" ) Convert FBX to GLB: result = await robot_model( operation="convert", source_path="D:/Models/scout.fbx", source_format="fbx", target_format="glb" ) """ try: if operation == "create": if not robot_type or not output_path: return format_error_response( "robot_type and output_path are required for 'create' operation", error_type="validation_error", ) return await self._handle_create( robot_type, output_path, format, dimensions, create_textures, texture_style ) elif operation == "import": if not robot_type or not model_path: return format_error_response( "robot_type and model_path are required for 'import' operation", error_type="validation_error", ) return await self._handle_import(robot_type, model_path, format, platform, project_path, create_prefab) elif operation == "export": if not robot_id: return format_error_response( "robot_id is required for 'export' operation", error_type="validation_error", ) return await self._handle_export(robot_id, format, output_path, include_animations) elif operation == "convert": if not source_path or not source_format or not target_format: return format_error_response( "source_path, source_format, and target_format are required for 'convert' operation", error_type="validation_error", ) return await self._handle_convert(source_path, source_format, target_format, target_path, robot_type) else: return format_error_response(f"Unknown operation: {operation}", error_type="validation_error") except Exception as e: return handle_tool_error("robot_model", e, operation=operation) async def _handle_create( self, robot_type: str, output_path: str, format: str, dimensions: Optional[Dict[str, float]], create_textures: bool, texture_style: str, ) -> Dict[str, Any]: """Handle create operation.""" try: if "blender" not in self.mounted_servers: return format_error_response( "blender-mcp not available - required for model creation", error_type="not_available", details={"mounted_servers": list(self.mounted_servers.keys())}, ) if not dimensions: dimensions = self._get_default_dimensions(robot_type) return await self._create_model_via_blender( robot_type, output_path, format, dimensions, create_textures, texture_style ) except Exception as e: return handle_tool_error("robot_model", e, operation="create") async def _handle_import( self, robot_type: str, model_path: str, format: str, platform: str, project_path: Optional[str], create_prefab: bool, ) -> Dict[str, Any]: """Handle import operation.""" try: if format == "vrm" and robot_type in NON_HUMANOID_ROBOT_TYPES: return format_error_response( f"VRM format is for humanoid robots only. {robot_type} is not humanoid. Use FBX/GLB instead.", error_type="validation_error", robot_type=robot_type, format=format, ) if platform == "unity": return await self._import_to_unity(robot_type, model_path, format, project_path, create_prefab) elif platform == "vrchat": return await self._import_to_vrchat(robot_type, model_path, format, project_path) elif platform == "resonite": return await self._import_to_resonite(robot_type, model_path, format) else: return format_error_response(f"Unsupported platform: {platform}", error_type="validation_error") except Exception as e: return handle_tool_error("robot_model", e, operation="import") async def _handle_export( self, robot_id: str, format: str, output_path: Optional[str], include_animations: bool ) -> Dict[str, Any]: """Handle export operation.""" try: robot = self.state_manager.get_robot(robot_id) if not robot: return format_error_response(f"Robot {robot_id} not found", error_type="not_found", robot_id=robot_id) if not robot.is_virtual: return format_error_response( f"Robot {robot_id} is not a virtual robot", error_type="validation_error", robot_id=robot_id ) if robot.platform == "unity" and "unity" in self.mounted_servers: return await self._export_from_unity(robot_id, format, output_path, include_animations) else: return format_error_response( f"Export not yet implemented for platform: {robot.platform}", error_type="not_implemented", robot_id=robot_id, ) except Exception as e: return handle_tool_error("robot_model", e, operation="export") async def _handle_convert( self, source_path: str, source_format: str, target_format: str, target_path: Optional[str], robot_type: Optional[str], ) -> Dict[str, Any]: """Handle convert operation.""" try: if target_format == "vrm" and robot_type and robot_type in NON_HUMANOID_ROBOT_TYPES: return format_error_response( f"VRM conversion only works for humanoid robots. {robot_type} is not humanoid.", error_type="validation_error", robot_type=robot_type, ) if "blender" not in self.mounted_servers: return format_error_response( "blender-mcp not available - required for model conversion", error_type="not_available", details={"mounted_servers": list(self.mounted_servers.keys())}, ) return await self._convert_via_blender(source_path, source_format, target_format, target_path) except Exception as e: return handle_tool_error("robot_model", e, operation="convert") # Legacy individual tools removed - consolidated into robot_model portmanteau above # Keeping method signatures for reference but not registering as tools: async def _legacy_robot_model_import( robot_type: str, model_path: str, format: Literal["fbx", "glb", "obj", "vrm"] = "fbx", platform: Literal["unity", "vrchat", "resonite"] = "unity", project_path: Optional[str] = None, create_prefab: bool = True, ) -> Dict[str, Any]: """Import robot 3D model into Unity/VRChat/Resonite project. Imports a robot 3D model file (FBX, GLB, OBJ, or VRM) into the specified platform. Handles format-specific import requirements and creates prefabs for reuse. IMPORTANT FORMAT NOTES: - **FBX/GLB/OBJ**: Use for wheeled robots (Scout), quadrupeds (Go2), and custom robots - **VRM**: Only for humanoid robots (Robbie, G1) - requires humanoid bone structure - **Scout should NOT use VRM** (not humanoid - has wheels, not legs) Args: robot_type: Type of robot (e.g., "scout", "robbie", "go2", "g1", "custom"). model_path: Path to 3D model file (.fbx, .glb, .obj, .vrm). format: Model format. Default: "fbx". - "fbx": Industry standard, best for Unity (recommended for robots) - "glb": Modern glTF 2.0 format, single file - "obj": Simple mesh format (no animations) - "vrm": VRM format (ONLY for humanoid robots like Robbie, G1) platform: Target platform. Default: "unity". project_path: Unity project path (required for Unity platform). create_prefab: Create Unity prefab after import. Default: True. Returns: Dictionary containing import result with model path and prefab info. Examples: Import Scout FBX model: result = await robot_model_import( robot_type="scout", model_path="D:/Models/scout_model.fbx", format="fbx", platform="unity", project_path="D:/Projects/UnityRobots" ) Import Robbie VRM (humanoid - VRM OK): result = await robot_model_import( robot_type="robbie", model_path="D:/Models/robbie.vrm", format="vrm", platform="unity", project_path="D:/Projects/UnityRobots" ) Import Go2 GLB (quadruped - use GLB, not VRM): result = await robot_model_import( robot_type="go2", model_path="D:/Models/go2_model.glb", format="glb", platform="unity" ) """ try: # Validate format for robot type if format == "vrm" and robot_type in NON_HUMANOID_ROBOT_TYPES: return format_error_response( f"VRM format is for humanoid robots only. {robot_type} is not humanoid. Use FBX/GLB instead.", error_type="validation_error", robot_type=robot_type, format=format, ) if platform == "unity": return await self._import_to_unity(robot_type, model_path, format, project_path, create_prefab) elif platform == "vrchat": return await self._import_to_vrchat(robot_type, model_path, format, project_path) elif platform == "resonite": return await self._import_to_resonite(robot_type, model_path, format) else: return format_error_response(f"Unsupported platform: {platform}", error_type="validation_error") except Exception as e: return handle_tool_error("robot_model_import", e, robot_type=robot_type, format=format, platform=platform) @self.mcp.tool() async def robot_model_export( robot_id: str, format: Literal["fbx", "glb", "obj"] = "fbx", output_path: Optional[str] = None, include_animations: bool = True, ) -> Dict[str, Any]: """Export robot model from Unity to file format. Exports a robot model from Unity scene to FBX, GLB, or OBJ format. Useful for sharing models or converting between formats. Args: robot_id: Virtual robot identifier to export. format: Export format. Default: "fbx". - "fbx": Industry standard, includes animations - "glb": Modern format, single file - "obj": Simple mesh (no animations) output_path: Output file path. Auto-generated if not provided. include_animations: Include animations in export (FBX/GLB only). Default: True. Returns: Dictionary containing export result with file path. Examples: Export Scout model to FBX: result = await robot_model_export( robot_id="vbot_scout_01", format="fbx", output_path="D:/Exports/scout_export.fbx" ) """ try: robot = self.state_manager.get_robot(robot_id) if not robot: return format_error_response(f"Robot {robot_id} not found", error_type="not_found", robot_id=robot_id) if not robot.is_virtual: return format_error_response( f"Robot {robot_id} is not a virtual robot", error_type="validation_error", robot_id=robot_id ) # Export via Unity or Blender if robot.platform == "unity" and "unity" in self.mounted_servers: return await self._export_from_unity(robot_id, format, output_path, include_animations) else: return format_error_response( f"Export not yet implemented for platform: {robot.platform}", error_type="not_implemented", robot_id=robot_id, ) except Exception as e: return handle_tool_error("robot_model_export", e, robot_id=robot_id, format=format) @self.mcp.tool() async def robot_model_create( robot_type: str, output_path: str, format: Literal["fbx", "glb", "obj"] = "fbx", dimensions: Optional[Dict[str, float]] = None, create_textures: bool = True, texture_style: Literal["realistic", "stylized", "simple"] = "realistic", ) -> Dict[str, Any]: """Create robot 3D model from scratch using Blender MCP. Creates a robot 3D model using blender-mcp tools. Supports creating models for Scout, Go2, G1, Robbie, and custom robots. Optionally creates textures using gimp-mcp. WORKFLOW: 1. Create base mesh in Blender (using blender-mcp) 2. Add details (wheels, camera, sensors, etc.) 3. Create/apply textures (using gimp-mcp if enabled) 4. Export to FBX/GLB/OBJ format Args: robot_type: Type of robot to create (e.g., "scout", "go2", "custom"). output_path: Path where model will be exported. format: Export format. Default: "fbx". dimensions: Optional custom dimensions (length, width, height in meters). If not provided, uses default dimensions for robot_type. create_textures: Create textures using gimp-mcp. Default: True. texture_style: Texture style. Default: "realistic". Returns: Dictionary containing creation result with model path and texture info. Examples: Create Scout model: result = await robot_model_create( robot_type="scout", output_path="D:/Models/scout_model.fbx", format="fbx", dimensions={"length": 0.115, "width": 0.10, "height": 0.08}, create_textures=True ) Create custom robot: result = await robot_model_create( robot_type="custom", output_path="D:/Models/my_robot.glb", format="glb", dimensions={"length": 0.2, "width": 0.15, "height": 0.12} ) """ try: # Check if blender-mcp is available if "blender" not in self.mounted_servers: return format_error_response( "blender-mcp not available - required for model creation", error_type="not_available", details={"mounted_servers": list(self.mounted_servers.keys())}, ) # Get default dimensions if not provided if not dimensions: dimensions = self._get_default_dimensions(robot_type) # Create model using blender-mcp return await self._create_model_via_blender( robot_type, output_path, format, dimensions, create_textures, texture_style ) except Exception as e: return handle_tool_error("robot_model_create", e, robot_type=robot_type, format=format) @self.mcp.tool() async def robot_model_convert( source_path: str, source_format: Literal["fbx", "glb", "obj", "blend", "vrm"], target_format: Literal["fbx", "glb", "obj", "vrm"], target_path: Optional[str] = None, robot_type: Optional[str] = None, ) -> Dict[str, Any]: """Convert robot model between formats. Converts a robot 3D model from one format to another. Uses Blender MCP for format conversion when available. IMPORTANT: VRM conversion only works for humanoid robots (Robbie, G1). Non-humanoid robots (Scout, Go2) cannot be converted to VRM. Args: source_path: Path to source model file. source_format: Source file format. target_format: Target file format. target_path: Output file path. Auto-generated if not provided. robot_type: Robot type (used for VRM validation). Optional. Returns: Dictionary containing conversion result. Examples: Convert FBX to GLB: result = await robot_model_convert( source_path="D:/Models/scout.fbx", source_format="fbx", target_format="glb", target_path="D:/Models/scout.glb" ) Convert FBX to VRM (humanoid only): result = await robot_model_convert( source_path="D:/Models/robbie.fbx", source_format="fbx", target_format="vrm", robot_type="robbie" # Humanoid - VRM OK ) """ try: # Validate VRM conversion if target_format == "vrm" and robot_type and robot_type in NON_HUMANOID_ROBOT_TYPES: return format_error_response( f"Cannot convert {robot_type} to VRM - not humanoid. Use FBX/GLB instead.", error_type="validation_error", robot_type=robot_type, target_format=target_format, ) # Use Blender MCP for conversion if available if "blender" in self.mounted_servers: return await self._convert_via_blender(source_path, source_format, target_format, target_path) else: return format_error_response( "Blender MCP not available for format conversion", error_type="not_available", details={"mounted_servers": list(self.mounted_servers.keys())}, ) except Exception as e: return handle_tool_error( "robot_model_convert", e, source_format=source_format, target_format=target_format ) async def _import_to_unity( self, robot_type: str, model_path: str, format: str, project_path: Optional[str], create_prefab: bool ) -> Dict[str, Any]: """Import model to Unity.""" try: if "unity" in self.mounted_servers: async with Client(self.mcp) as client: if format == "vrm": # Use unity3d-mcp VRM import result = await client.call_tool( "import_vrm_avatar", { "vrm_path": model_path, "project_path": project_path or "", "optimize_for_vrchat": False, "create_prefab": create_prefab, }, ) else: # Use unity3d-mcp asset import (if available) # For now, return mock result result = format_success_response( f"Model import initiated: {robot_type}", data={ "model_path": model_path, "format": format, "prefab_path": f"Assets/Prefabs/{robot_type}.prefab" if create_prefab else None, }, ) return result else: return format_success_response( f"Mock import: {robot_type}", data={"model_path": model_path, "format": format, "note": "Unity MCP not available"}, ) except Exception as e: logger.error("Unity import failed", robot_type=robot_type, error=str(e)) return format_error_response(f"Unity import failed: {str(e)}", error_type="import_error") async def _import_to_vrchat( self, robot_type: str, model_path: str, format: str, project_path: Optional[str] ) -> Dict[str, Any]: """Import model to VRChat.""" # VRChat uses Unity, so similar to Unity import return await self._import_to_unity(robot_type, model_path, format, project_path, create_prefab=True) async def _import_to_resonite(self, robot_type: str, model_path: str, format: str) -> Dict[str, Any]: """Import model to Resonite.""" # Resonite imports VRM/GLB directly return format_success_response( f"Resonite import: {robot_type}", data={ "model_path": model_path, "format": format, "note": "Resonite imports VRM/GLB directly - no Unity project needed", }, ) async def _export_from_unity( self, robot_id: str, format: str, output_path: Optional[str], include_animations: bool ) -> Dict[str, Any]: """Export model from Unity.""" # TODO: Implement Unity export via execute_unity_method return format_success_response( f"Export initiated: {robot_id}", data={"robot_id": robot_id, "format": format, "output_path": output_path, "note": "Mock export"}, ) def _get_default_dimensions(self, robot_type: str) -> Dict[str, float]: """Get default dimensions for robot type (in meters).""" defaults = { "scout": {"length": 0.115, "width": 0.10, "height": 0.08}, # 11.5×10×8 cm "scout_e": {"length": 0.12, "width": 0.11, "height": 0.09}, # Slightly larger "go2": {"length": 0.50, "width": 0.30, "height": 0.40}, # Quadruped "g1": {"length": 0.50, "width": 0.40, "height": 1.60}, # Humanoid "robbie": {"length": 0.60, "width": 0.50, "height": 1.80}, # Humanoid robot } return defaults.get(robot_type, {"length": 0.2, "width": 0.15, "height": 0.12}) # Default custom size async def _create_model_via_blender( self, robot_type: str, output_path: str, format: str, dimensions: Dict[str, float], create_textures: bool, texture_style: str, ) -> Dict[str, Any]: """Create robot model using blender-mcp.""" try: # Access blender-mcp directly from mounted servers blender_server = self.mounted_servers.get("blender") if not blender_server: return format_error_response( "blender-mcp server not available", error_type="not_available", details={"mounted_servers": list(self.mounted_servers.keys())}, ) # Create all objects in a SINGLE Blender script execution # This ensures all objects are in the same scene when we save from blender_mcp.utils.blender_executor import get_blender_executor executor = get_blender_executor() # Calculate wheel positions in Python (before script generation) wheel_radius = 0.025 wheel_thickness = 0.015 wheel_positions = [ (-dimensions["length"] / 2 - wheel_radius, dimensions["width"] / 2, wheel_radius), # Front-left (dimensions["length"] / 2 + wheel_radius, dimensions["width"] / 2, wheel_radius), # Front-right (-dimensions["length"] / 2 - wheel_radius, -dimensions["width"] / 2, wheel_radius), # Back-left (dimensions["length"] / 2 + wheel_radius, -dimensions["width"] / 2, wheel_radius), # Back-right ] # Generate a single script that creates all objects create_script = f""" import bpy # Clear default objects bpy.ops.object.select_all(action='SELECT') bpy.ops.object.delete(use_global=False) # Step 1: Create main body bpy.ops.mesh.primitive_cube_add( size=1, location=(0, 0, {dimensions["height"] / 2}), scale=({dimensions["length"]}, {dimensions["width"]}, {dimensions["height"]}) ) body = bpy.context.active_object body.name = "{robot_type}_body" # Step 2: Add robot-specific features if "{robot_type}" in ["scout", "scout_e"]: # Add wheels (4 mecanum wheels) - vertical orientation on sides wheel_radius = 0.025 wheel_thickness = 0.015 # Position wheels on the sides of the body, at the corners # Wheels are on the SIDES (Y-axis), rotated 90° on X-axis to be vertical wheel_positions = [ {wheel_positions[0]}, # Front-left {wheel_positions[1]}, # Front-right {wheel_positions[2]}, # Back-left {wheel_positions[3]}, # Back-right ] for i, pos in enumerate(wheel_positions): bpy.ops.mesh.primitive_cylinder_add( radius=wheel_radius, depth=wheel_thickness, location=pos, rotation=(1.5708, 0, 0) # Rotate 90 degrees (pi/2 radians) on X-axis to make vertical ) wheel = bpy.context.active_object wheel.name = "{robot_type}_wheel_" + str(i+1) # Add camera module camera_size = 0.012 bpy.ops.mesh.primitive_cylinder_add( radius=camera_size, depth=camera_size * 0.8, location=({dimensions["length"] / 2 + 0.008}, 0, {dimensions["height"] / 2}) ) camera = bpy.context.active_object camera.name = "{robot_type}_camera" # Add top mounting plate bpy.ops.mesh.primitive_cube_add( size=1, location=(0, 0, {dimensions["height"] + 0.005}), scale=({dimensions["length"] * 0.8}, {dimensions["width"] * 0.8}, 0.01) ) plate = bpy.context.active_object plate.name = "{robot_type}_mounting_plate" # Add materials for visibility # Body material (blue) mat_body = bpy.data.materials.new(name="ScoutBody") mat_body.use_nodes = True mat_body.node_tree.nodes["Principled BSDF"].inputs[0].default_value = (0.2, 0.2, 0.8, 1.0) body.data.materials.append(mat_body) # Wheel material (dark gray) mat_wheel = bpy.data.materials.new(name="ScoutWheel") mat_wheel.use_nodes = True mat_wheel.node_tree.nodes["Principled BSDF"].inputs[0].default_value = (0.1, 0.1, 0.1, 1.0) for obj in bpy.data.objects: if "wheel" in obj.name.lower(): obj.data.materials.append(mat_wheel) # Camera material (yellow) if "{robot_type}_camera" in bpy.data.objects: mat_camera = bpy.data.materials.new(name="ScoutCamera") mat_camera.use_nodes = True mat_camera.node_tree.nodes["Principled BSDF"].inputs[0].default_value = (0.8, 0.8, 0.2, 1.0) bpy.data.objects["{robot_type}_camera"].data.materials.append(mat_camera) # Select all and frame view bpy.ops.object.select_all(action='SELECT') print(f"Created {{len(bpy.data.objects)}} objects:") for obj in bpy.data.objects: print(f" - {{obj.name}} at {{obj.location}}") # Frame all objects in viewport (zoom to fit) - skip in background mode try: if bpy.context.screen and bpy.context.screen.areas: for area in bpy.context.screen.areas: if area.type == 'VIEW_3D': override = bpy.context.copy() override['area'] = area override['region'] = area.regions[-1] bpy.ops.view3d.view_all(override) break print("Framed all objects in viewport") except Exception as e: print(f"Note: Could not frame viewport (background mode): {{str(e)}}") # Save .blend file (do this BEFORE any potential errors) import os blend_path = r"{str(Path(output_path).with_suffix('.blend'))}" os.makedirs(os.path.dirname(blend_path), exist_ok=True) try: bpy.ops.wm.save_as_mainfile(filepath=blend_path) print(f"SUCCESS: Saved .blend file: {{blend_path}}") print(f"Objects saved: {{len(bpy.data.objects)}}") except Exception as save_error: print(f"ERROR saving blend file: {{save_error}}") raise """ # Execute the creation script (creates objects AND saves blend file) blend_path = Path(output_path).with_suffix('.blend') blend_path_before = blend_path.exists() blend_mtime_before = blend_path.stat().st_mtime if blend_path_before else 0 # Debug: Write script to file for inspection debug_script_path = Path("D:/Models/create_scout_debug.py") debug_script_path.parent.mkdir(parents=True, exist_ok=True) debug_script_path.write_text(create_script) logger.debug(f"Wrote debug script to {debug_script_path}") try: result = await executor.execute_script(create_script, script_name="create_scout") logger.info(f"Created all {robot_type} objects and saved blend file in single script execution") logger.debug(f"Script output: {result}") except Exception as e: # Check if blend file was created/updated despite the error (TBBmalloc warning) error_str = str(e) if blend_path.exists(): blend_mtime_after = blend_path.stat().st_mtime if not blend_path_before or blend_mtime_after > blend_mtime_before: logger.warning(f"Blender exited with error but blend file was created/updated: {error_str}") # Treat as success if file was created - TBBmalloc warning is harmless elif "TBBmalloc" in error_str: # TBBmalloc warning - check if file exists and is recent logger.warning(f"TBBmalloc warning detected, but continuing if file exists") else: raise elif "TBBmalloc" in error_str: # TBBmalloc warning but file doesn't exist - script might have failed before save logger.error(f"TBBmalloc warning and no blend file created - script may have failed: {error_str}") logger.error(f"Debug script saved to {debug_script_path} for inspection") raise else: logger.error(f"Script failed: {error_str}") logger.error(f"Debug script saved to {debug_script_path} for inspection") raise # Now use Client for any additional operations that need the mounted server async with Client(blender_server) as client: # Step 3: Create textures if requested texture_paths = [] if create_textures and "gimp" in self.mounted_servers: texture_paths = await self._create_textures_via_gimp(robot_type, texture_style, client) # Step 4: Apply materials/textures (if created) if texture_paths: # Apply textures using blender-mcp material tools for texture_path in texture_paths: await client.call_tool( "blender_material", { "operation": "create_material", "name": f"{robot_type}_material", "texture_path": texture_path, }, ) # Step 5: Export model (this will also save .blend file) # export_for_unity signature: output_path, scale, apply_modifiers, optimize_materials, bake_textures, lod_levels # But blender_export tool wraps it, so we need to pass operation and output_path export_result = await client.call_tool( "blender_export", { "operation": "export_unity", "output_path": output_path, }, ) logger.info(f"Exported {robot_type} model", result=export_result) # Get blend file path blend_path = str(Path(output_path).with_suffix('.blend')) return format_success_response( f"Created {robot_type} model", data={ "robot_type": robot_type, "output_path": output_path, "blend_path": blend_path, "format": format, "dimensions": dimensions, "textures_created": len(texture_paths) if create_textures else 0, "texture_paths": texture_paths, "note": f"Open {blend_path} in Blender to see the model", }, ) except Exception as e: logger.error("Blender model creation failed", robot_type=robot_type, error=str(e)) return format_error_response(f"Model creation failed: {str(e)}", error_type="creation_error") async def _create_textures_via_gimp( self, robot_type: str, texture_style: str, client: Client ) -> list[str]: """Create textures using gimp-mcp portmanteau tools.""" texture_paths = [] try: # Create output directory texture_dir = Path("D:/Textures") texture_dir.mkdir(parents=True, exist_ok=True) texture_path = texture_dir / f"{robot_type}_texture.png" # For now, create a simple texture by: # 1. Creating a base image (we'll use gimp_file with a temporary file) # 2. Applying filters # 3. Saving the result # Create a temporary base image file first import tempfile temp_base = tempfile.NamedTemporaryFile(suffix=".png", delete=False) temp_base_path = temp_base.name temp_base.close() # Create base image using gimp_file (we'll need to create it first) # For now, we'll create a simple colored image # Note: gimp-mcp doesn't have a direct "create_image" operation # We'll use gimp_transform to create a base, or skip texture creation # and just return empty list for now # Alternative: Use gimp_layer with create operation if available # For simplicity, we'll create a placeholder texture file logger.info(f"Creating texture for {robot_type} with style {texture_style}") # Since gimp-mcp requires input files, we'll create a simple approach: # Create a 1024x1024 solid color image using gimp_file operations # This is a simplified approach - in production, you'd create the base image first # For now, return empty list and log that textures need manual creation logger.warning( f"Texture creation for {robot_type} skipped - gimp-mcp requires input files. " f"Create base texture manually or use gimp_layer/create if available." ) except Exception as e: logger.warning("GIMP texture creation failed", robot_type=robot_type, error=str(e)) # Continue without textures if GIMP fails return texture_paths async def _convert_via_blender( self, source_path: str, source_format: str, target_format: str, target_path: Optional[str] ) -> Dict[str, Any]: """Convert model using Blender MCP.""" try: async with Client(self.mcp) as client: # Import source if source_format == "fbx": result = await client.call_tool("blender_import", {"filepath": source_path, "file_format": "fbx"}) elif source_format == "obj": result = await client.call_tool("blender_import", {"filepath": source_path, "file_format": "obj"}) elif source_format == "blend": result = await client.call_tool("blender_open", {"filepath": source_path}) else: return format_error_response(f"Unsupported source format: {source_format}", error_type="validation_error") # Export target if target_format == "fbx": result = await client.call_tool("blender_export", {"filepath": target_path, "file_format": "fbx"}) elif target_format == "glb": result = await client.call_tool("blender_export", {"filepath": target_path, "file_format": "gltf"}) elif target_format == "vrm": result = await client.call_tool("blender_export", {"filepath": target_path, "file_format": "vrm"}) else: return format_error_response(f"Unsupported target format: {target_format}", error_type="validation_error") return format_success_response( f"Converted {source_format} to {target_format}", data={"source_path": source_path, "target_path": target_path, "formats": [source_format, target_format]}, ) except Exception as e: logger.error("Blender conversion failed", error=str(e)) return format_error_response(f"Blender conversion failed: {str(e)}", error_type="conversion_error")

Implementation Reference

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/sandraschi/robotics-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server