vefrank_mesh_server.py•32.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()