Skip to main content
Glama

MCP-Blender

by shdann
vefrank_mesh_server.py32.3 kB
#!/usr/bin/env python3 """ VeFrank Mesh Generation MCP Server An enhanced Model Context Protocol server that creates automotive component meshes using Blender's parametric modeling capabilities for VeFrank assets. """ import asyncio import json import logging import sys from pathlib import Path from typing import Any, Dict, List, Optional import anyio from mcp.server import Server from mcp.server.stdio import stdio_server from mcp.types import ( CallToolRequest, CallToolResult, ListToolsRequest, ListToolsResult, Tool, TextContent, ) # Set up logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) class VeFrankMeshServer: """MCP Server for generating VeFrank automotive component meshes.""" def __init__(self, blender_host: str = "localhost", blender_port: int = 9876): self.blender_host = blender_host self.blender_port = blender_port self.server = Server("vefrank-mesh") self.assets_dir = Path("../assets/3d_models/automotive") self._setup_handlers() def _setup_handlers(self): """Set up MCP server handlers for VeFrank mesh generation.""" @self.server.list_tools() async def list_tools() -> List[Tool]: """List available VeFrank mesh generation tools.""" return [ Tool( name="create_automotive_component", description="Create a parametric automotive component mesh with realistic details", inputSchema={ "type": "object", "properties": { "component_type": { "type": "string", "enum": ["ecu", "bcm", "pcm", "relay", "fuse", "sensor", "connector", "harness"], "description": "Type of automotive component to create" }, "dimensions": { "type": "object", "properties": { "length": {"type": "number", "description": "Length in mm"}, "width": {"type": "number", "description": "Width in mm"}, "height": {"type": "number", "description": "Height in mm"} }, "required": ["length", "width", "height"] }, "connector_specs": { "type": "array", "items": { "type": "object", "properties": { "type": {"type": "string", "enum": ["weatherpack", "deutsch", "sensor", "iso"]}, "pin_count": {"type": "integer", "minimum": 1, "maximum": 64}, "position": { "type": "object", "properties": { "x": {"type": "number"}, "y": {"type": "number"}, "z": {"type": "number"} } } } }, "description": "Connector specifications with positions" }, "material_type": { "type": "string", "enum": ["metal", "plastic", "ceramic"], "description": "Material type for appearance" }, "detail_level": { "type": "string", "enum": ["low", "medium", "high", "ultra"], "default": "medium", "description": "Level of geometric detail" }, "thermal_features": { "type": "boolean", "default": false, "description": "Add thermal dissipation features (fins, vents)" }, "mounting_holes": { "type": "integer", "minimum": 0, "maximum": 8, "default": 0, "description": "Number of mounting holes" } }, "required": ["component_type", "dimensions"] } ), Tool( name="create_wire_harness", description="Generate a 3D wire harness with proper routing and connectors", inputSchema={ "type": "object", "properties": { "start_connector": { "type": "object", "properties": { "position": {"type": "array", "items": {"type": "number"}, "minItems": 3, "maxItems": 3}, "type": {"type": "string"} } }, "end_connector": { "type": "object", "properties": { "position": {"type": "array", "items": {"type": "number"}, "minItems": 3, "maxItems": 3}, "type": {"type": "string"} } }, "wire_specs": { "type": "array", "items": { "type": "object", "properties": { "gauge": {"type": "string", "enum": ["18AWG", "16AWG", "14AWG", "12AWG", "10AWG"]}, "color": {"type": "string"}, "signal_type": {"type": "string", "enum": ["power", "ground", "signal", "can_high", "can_low"]} } } }, "routing_style": { "type": "string", "enum": ["direct", "curved", "protected", "flexible"], "default": "curved" } }, "required": ["start_connector", "end_connector"] } ), Tool( name="optimize_mesh_for_simulation", description="Optimize mesh topology for electrical simulation and real-time rendering", inputSchema={ "type": "object", "properties": { "object_name": {"type": "string", "description": "Name of object to optimize"}, "target_poly_count": {"type": "integer", "minimum": 100, "maximum": 10000, "default": 1000}, "preserve_features": {"type": "boolean", "default": true}, "generate_lods": {"type": "boolean", "default": true, "description": "Generate LOD versions"} }, "required": ["object_name"] } ), Tool( name="apply_automotive_materials", description="Apply realistic automotive materials with proper PBR properties", inputSchema={ "type": "object", "properties": { "object_name": {"type": "string"}, "material_preset": { "type": "string", "enum": ["aluminum_housing", "plastic_black", "ceramic_white", "copper_trace", "solder_joint"], "description": "Predefined automotive material" }, "custom_properties": { "type": "object", "properties": { "metallic": {"type": "number", "minimum": 0, "maximum": 1}, "roughness": {"type": "number", "minimum": 0, "maximum": 1}, "base_color": {"type": "array", "items": {"type": "number"}, "minItems": 3, "maxItems": 4} } } }, "required": ["object_name", "material_preset"] } ), Tool( name="export_vefrank_asset", description="Export mesh as VeFrank-compatible asset with metadata", inputSchema={ "type": "object", "properties": { "object_name": {"type": "string"}, "export_path": {"type": "string", "description": "Path relative to assets directory"}, "formats": { "type": "array", "items": {"type": "string", "enum": ["obj", "blend", "gltf", "fbx"]}, "default": ["obj", "blend"] }, "include_metadata": {"type": "boolean", "default": true}, "electrical_specs": { "type": "object", "properties": { "voltage_rating": {"type": "string"}, "current_rating": {"type": "string"}, "resistance": {"type": "number"}, "part_number": {"type": "string"} } } }, "required": ["object_name", "export_path"] } ), Tool( name="batch_generate_components", description="Generate multiple components from a specification list", inputSchema={ "type": "object", "properties": { "components": { "type": "array", "items": { "type": "object", "properties": { "name": {"type": "string"}, "type": {"type": "string"}, "specs": {"type": "object"} } } }, "export_directory": {"type": "string"} }, "required": ["components"] } ), Tool( name="validate_component_mesh", description="Validate mesh for VeFrank electrical simulation compatibility", inputSchema={ "type": "object", "properties": { "object_name": {"type": "string"}, "check_manifold": {"type": "boolean", "default": true}, "check_scale": {"type": "boolean", "default": true}, "check_normals": {"type": "boolean", "default": true} }, "required": ["object_name"] } ), Tool( name="search_component_references", description="Search internet for real automotive component images as modeling reference", inputSchema={ "type": "object", "properties": { "component_type": { "type": "string", "description": "Type of automotive component to search for" }, "part_number": { "type": "string", "description": "Optional specific part number for more targeted search" }, "search_terms": { "type": "array", "items": {"type": "string"}, "description": "Additional search terms for refinement" } }, "required": ["component_type"] } ) ] @self.server.call_tool() async def call_tool(name: str, arguments: Dict[str, Any]) -> CallToolResult: """Call a VeFrank mesh generation tool.""" try: if name == "create_automotive_component": result = await self._create_automotive_component(arguments) elif name == "create_wire_harness": result = await self._create_wire_harness(arguments) elif name == "optimize_mesh_for_simulation": result = await self._optimize_mesh(arguments) elif name == "apply_automotive_materials": result = await self._apply_materials(arguments) elif name == "export_vefrank_asset": result = await self._export_asset(arguments) elif name == "batch_generate_components": result = await self._batch_generate(arguments) elif name == "validate_component_mesh": result = await self._validate_mesh(arguments) elif name == "search_component_references": result = await self._search_references(arguments) else: # Fallback to generic Blender command result = await self._send_blender_command(name, arguments) return CallToolResult( content=[ TextContent( type="text", text=json.dumps(result, indent=2) ) ] ) except Exception as e: logger.error(f"Error in tool {name}: {e}") return CallToolResult( content=[ TextContent( type="text", text=f"Error: {str(e)}" ) ], isError=True ) async def _create_automotive_component(self, args: Dict[str, Any]) -> Dict[str, Any]: """Create a parametric automotive component using Blender.""" component_type = args["component_type"] dimensions = args["dimensions"] connectors = args.get("connector_specs", []) material = args.get("material_type", "plastic") detail_level = args.get("detail_level", "medium") thermal = args.get("thermal_features", False) mounting_holes = args.get("mounting_holes", 0) # Generate Blender Python script for component creation blender_script = self._generate_component_script( component_type, dimensions, connectors, material, detail_level, thermal, mounting_holes ) # Execute in Blender result = await self._send_blender_command("execute_code", {"code": blender_script}) if result.get("status") == "success": return { "status": "success", "component_type": component_type, "dimensions": dimensions, "message": f"Created {component_type} component with {len(connectors)} connectors" } else: return { "status": "error", "message": f"Failed to create component: {result.get('message', 'Unknown error')}" } def _generate_component_script(self, comp_type: str, dimensions: Dict, connectors: List, material: str, detail: str, thermal: bool, holes: int) -> str: """Generate Blender Python script for component creation with real-world reference.""" # Add alternator-specific realistic geometry if comp_type == "alternator": script = f''' import bpy import bmesh import mathutils from mathutils import Vector import math # Clear existing mesh bpy.ops.object.select_all(action='SELECT') bpy.ops.object.delete(use_global=False) # Realistic alternator dimensions (convert mm to Blender units) diameter = 130.0 / 1000.0 # 130mm diameter case length = 180.0 / 1000.0 # 180mm length pulley_diameter = 55.0 / 1000.0 # 55mm pulley # Create main cylindrical housing (stator case) bpy.ops.mesh.primitive_cylinder_add( radius=diameter/2, depth=length*0.7, location=(0, 0, 0), vertices=32 # Smooth cylinder ) housing = bpy.context.object housing.name = "alternator_stator_case" # Add cooling fins around the circumference bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_all(action='SELECT') # Create fin geometry for i in range(12): # 12 cooling fins angle = i * (2 * math.pi / 12) x_offset = (diameter/2 + 0.005) * math.cos(angle) y_offset = (diameter/2 + 0.005) * math.sin(angle) bpy.ops.object.mode_set(mode='OBJECT') bpy.ops.mesh.primitive_cube_add( size=0.008, location=(x_offset, y_offset, 0) ) fin = bpy.context.object fin.scale = (0.2, 1.5, 8.0) # Thin radial fins bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) # Rotate fin to align radially fin.rotation_euler = (0, 0, angle) bpy.ops.object.transform_apply(location=False, rotation=True, scale=False) # Select all fins and housing for joining bpy.ops.object.select_all(action='SELECT') bpy.context.view_layer.objects.active = housing bpy.ops.object.join() # Create front pulley bpy.ops.mesh.primitive_cylinder_add( radius=pulley_diameter/2, depth=0.025, location=(0, 0, length*0.4), vertices=24 ) pulley = bpy.context.object pulley.name = "alternator_pulley" # Add belt groove to pulley bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.inset_faces(thickness=0.005, depth=0.008) bpy.ops.object.mode_set(mode='OBJECT') # Create rear end cap bpy.ops.mesh.primitive_cylinder_add( radius=diameter/2 - 0.005, depth=0.02, location=(0, 0, -length*0.4), vertices=16 ) end_cap = bpy.context.object end_cap.name = "alternator_end_cap" # Create mounting brackets for side in [-1, 1]: bpy.ops.mesh.primitive_cube_add( size=0.02, location=(0, side * (diameter/2 + 0.015), 0) ) bracket = bpy.context.object bracket.scale = (3.0, 0.5, 1.5) bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) # Add mounting hole bpy.ops.mesh.primitive_cylinder_add( radius=0.004, # 8mm hole depth=0.1, location=(0, side * (diameter/2 + 0.015), 0) ) hole = bpy.context.object # Boolean subtract hole from bracket mod = bracket.modifiers.new(name="mount_hole", type='BOOLEAN') mod.operation = 'DIFFERENCE' mod.object = hole bpy.context.view_layer.objects.active = bracket bpy.ops.object.modifier_apply(modifier="mount_hole") bpy.data.objects.remove(hole) # Join all components bpy.ops.object.select_all(action='SELECT') bpy.context.view_layer.objects.active = housing bpy.ops.object.join() ''' else: # Default component generation for non-alternator components script = f''' import bpy import bmesh import mathutils from mathutils import Vector # Clear existing mesh bpy.ops.object.select_all(action='SELECT') bpy.ops.object.delete(use_global=False) # Component dimensions (convert mm to Blender units) length = {dimensions["length"]} / 1000.0 width = {dimensions["width"]} / 1000.0 height = {dimensions["height"]} / 1000.0 # Create main housing bpy.ops.mesh.primitive_cube_add(size=1, location=(0, 0, 0)) housing = bpy.context.object housing.name = "{comp_type}_housing" # Scale to correct dimensions housing.scale = (length, width, height) bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) # Add geometric details based on component type ''' # Add component-specific details if comp_type in ["ecu", "bcm", "pcm"]: script += ''' # Add ECU-style details bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.inset_faces(thickness=0.005, depth=0.002) bpy.ops.object.mode_set(mode='OBJECT') ''' # Add thermal features if thermal: script += ''' # Add thermal dissipation fins for i in range(5): bpy.ops.mesh.primitive_cube_add( size=0.002, location=(length/2 + 0.001, -width/2 + (i * width/4), height/2) ) fin = bpy.context.object fin.scale = (0.5, 0.1, 2.0) bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) # Join with housing housing.select_set(True) bpy.context.view_layer.objects.active = housing bpy.ops.object.join() ''' # Add connectors for i, connector in enumerate(connectors): pin_count = connector.get("pin_count", 4) conn_type = connector.get("type", "weatherpack") pos = connector.get("position", {"x": 0, "y": 0, "z": 0}) script += f''' # Add connector {i+1} - {conn_type} {pin_count}-pin conn_size = {pin_count * 0.003} bpy.ops.mesh.primitive_cylinder_add( radius=conn_size, depth=0.015, location=({pos["x"]/1000}, {pos["y"]/1000}, {pos["z"]/1000}) ) connector_{i} = bpy.context.object connector_{i}.name = "{conn_type}_{pin_count}pin_connector" # Join with housing housing.select_set(True) bpy.context.view_layer.objects.active = housing bpy.ops.object.join() ''' # Add mounting holes if holes > 0: script += f''' # Add mounting holes for i in range({holes}): angle = i * (3.14159 * 2 / {holes}) x = length/3 * cos(angle) y = width/3 * sin(angle) bpy.ops.mesh.primitive_cylinder_add( radius=0.0025, # M5 hole depth=height + 0.01, location=(x, y, 0) ) hole = bpy.context.object # Boolean subtract from housing mod = housing.modifiers.new(name=f"hole_{{i}}", type='BOOLEAN') mod.operation = 'DIFFERENCE' mod.object = hole bpy.context.view_layer.objects.active = housing bpy.ops.object.modifier_apply(modifier=f"hole_{{i}}") # Delete hole object bpy.data.objects.remove(hole) ''' # Add material script += f''' # Create and apply material material = bpy.data.materials.new(name="{material}_{comp_type}") material.use_nodes = True bsdf = material.node_tree.nodes["Principled BSDF"] # Material properties if "{material}" == "metal": bsdf.inputs["Base Color"].default_value = (0.6, 0.6, 0.7, 1.0) bsdf.inputs["Metallic"].default_value = 0.8 bsdf.inputs["Roughness"].default_value = 0.2 elif "{material}" == "plastic": bsdf.inputs["Base Color"].default_value = (0.2, 0.2, 0.2, 1.0) bsdf.inputs["Metallic"].default_value = 0.0 bsdf.inputs["Roughness"].default_value = 0.8 else: # ceramic bsdf.inputs["Base Color"].default_value = (0.9, 0.9, 0.8, 1.0) bsdf.inputs["Metallic"].default_value = 0.0 bsdf.inputs["Roughness"].default_value = 0.4 # Assign material housing.data.materials.append(material) print(f"Created {{housing.name}} with dimensions {{length*1000}}x{{width*1000}}x{{height*1000}}mm") ''' return script async def _create_wire_harness(self, args: Dict[str, Any]) -> Dict[str, Any]: """Create a 3D wire harness.""" # Implementation for wire harness generation return {"status": "success", "message": "Wire harness creation not yet implemented"} async def _optimize_mesh(self, args: Dict[str, Any]) -> Dict[str, Any]: """Optimize mesh for simulation.""" # Implementation for mesh optimization return {"status": "success", "message": "Mesh optimization not yet implemented"} async def _apply_materials(self, args: Dict[str, Any]) -> Dict[str, Any]: """Apply automotive materials.""" # Implementation for material application return {"status": "success", "message": "Material application not yet implemented"} async def _export_asset(self, args: Dict[str, Any]) -> Dict[str, Any]: """Export asset with VeFrank metadata.""" object_name = args["object_name"] export_path = args["export_path"] formats = args.get("formats", ["obj", "blend"]) script = f''' import bpy import json import os # Select object obj = bpy.data.objects.get("{object_name}") if not obj: print("ERROR: Object {object_name} not found") else: # Deselect all, select target bpy.ops.object.select_all(action='DESELECT') obj.select_set(True) bpy.context.view_layer.objects.active = obj export_dir = "{export_path}" os.makedirs(export_dir, exist_ok=True) # Export in requested formats formats = {formats} if "obj" in formats: bpy.ops.export_scene.obj( filepath=os.path.join(export_dir, "{object_name}.obj"), use_selection=True, use_materials=True ) if "blend" in formats: bpy.ops.wm.save_as_mainfile( filepath=os.path.join(export_dir, "{object_name}.blend") ) print(f"Exported {{obj.name}} to {{export_dir}}") ''' result = await self._send_blender_command("execute_code", {"code": script}) return result async def _batch_generate(self, args: Dict[str, Any]) -> Dict[str, Any]: """Generate multiple components.""" # Implementation for batch generation return {"status": "success", "message": "Batch generation not yet implemented"} async def _validate_mesh(self, args: Dict[str, Any]) -> Dict[str, Any]: """Validate mesh for VeFrank compatibility.""" # Implementation for mesh validation return {"status": "success", "message": "Mesh validation not yet implemented"} async def _search_references(self, args: Dict[str, Any]) -> Dict[str, Any]: """Search for real automotive component references.""" component_type = args["component_type"] part_number = args.get("part_number", "") search_terms = args.get("search_terms", []) # Build search query query_parts = [component_type, "automotive", "car"] if part_number: query_parts.append(part_number) query_parts.extend(search_terms) search_query = " ".join(query_parts) # For now, return structured guidance based on web search results if component_type == "alternator": return { "status": "success", "component_type": component_type, "search_query": search_query, "visual_reference_guidance": { "main_structure": "Cylindrical stator case with front pulley and rear electrical connections", "key_features": [ "Cylindrical main housing (130mm diameter typical)", "Radial cooling fins around circumference", "Front pulley with belt groove (55mm diameter)", "Rear end cap with electrical terminals", "Mounting brackets with bolt holes", "B+ terminal (large output connection)", "Small connector for field control" ], "proportions": { "diameter": "130mm", "length": "180mm", "pulley_diameter": "55mm", "fin_count": "8-12 radial fins" }, "materials": { "housing": "die-cast aluminum", "pulley": "steel or cast iron", "terminals": "copper/brass", "connector": "black plastic" } }, "reference_sources": [ "Stock photography platforms with 1,400+ car alternator images", "Technical diagrams showing exploded views", "Workshop photos showing real installation context" ] } else: return { "status": "success", "component_type": component_type, "search_query": search_query, "message": f"Reference search for {component_type} components not yet implemented" } async def _send_blender_command(self, command: str, params: Dict[str, Any]) -> Dict[str, Any]: """Send command to Blender via existing MCP infrastructure.""" try: # Use existing Blender MCP connection reader, writer = await anyio.connect_tcp(self.blender_host, self.blender_port) command_data = { "type": command, "params": params } command_json = json.dumps(command_data) writer.write(command_json.encode('utf-8')) await writer.aclose() # Read response response_data = b'' async for chunk in reader: response_data += chunk await reader.aclose() return json.loads(response_data.decode('utf-8')) except Exception as e: return { "status": "error", "message": f"Failed to communicate with Blender: {str(e)}" } async def run(self): """Run the VeFrank mesh MCP server.""" async with stdio_server() as (read_stream, write_stream): await self.server.run(read_stream, write_stream, self.server.create_initialization_options()) def main(): """Main entry point.""" import argparse parser = argparse.ArgumentParser(description="VeFrank Mesh Generation MCP Server") parser.add_argument("--host", default="localhost", help="Blender host") parser.add_argument("--port", type=int, default=9876, help="Blender port") args = parser.parse_args() server = VeFrankMeshServer(args.host, args.port) try: anyio.run(server.run) except KeyboardInterrupt: logger.info("VeFrank Mesh Server stopped by user") except Exception as e: logger.error(f"Server error: {e}") sys.exit(1) if __name__ == "__main__": main()

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/shdann/mcp-blend'

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