Skip to main content
Glama
tools.py44.2 kB
# blender_mcp_server.py from mcp.server.fastmcp import FastMCP, Context, Image import socket import json import asyncio import logging from dataclasses import dataclass from contextlib import asynccontextmanager from typing import AsyncIterator, Dict, Any, List, TypedDict import os from pathlib import Path import base64 from urllib.parse import urlparse from typing import Optional import sys # Configure logging logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') logger = logging.getLogger("BlenderMCPServer") @dataclass class BlenderConnection: host: str port: int sock: socket.socket = None # Changed from 'socket' to 'sock' to avoid naming conflict def connect(self) -> bool: """Connect to the Blender addon socket server""" if self.sock: return True try: self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.sock.connect((self.host, self.port)) logger.info(f"Connected to Blender at {self.host}:{self.port}") return True except Exception as e: logger.error(f"Failed to connect to Blender: {str(e)}") self.sock = None return False def disconnect(self): """Disconnect from the Blender addon""" if self.sock: try: self.sock.close() except Exception as e: logger.error(f"Error disconnecting from Blender: {str(e)}") finally: self.sock = None def receive_full_response(self, sock, buffer_size=8192): """Receive the complete response, potentially in multiple chunks""" chunks = [] # Use a consistent timeout value that matches the addon's timeout sock.settimeout(15.0) # Match the addon's timeout try: while True: try: chunk = sock.recv(buffer_size) if not chunk: # If we get an empty chunk, the connection might be closed if not chunks: # If we haven't received anything yet, this is an error raise Exception("Connection closed before receiving any data") break chunks.append(chunk) # Check if we've received a complete JSON object try: data = b''.join(chunks) json.loads(data.decode('utf-8')) # If we get here, it parsed successfully logger.info(f"Received complete response ({len(data)} bytes)") return data except json.JSONDecodeError: # Incomplete JSON, continue receiving continue except socket.timeout: # If we hit a timeout during receiving, break the loop and try to use what we have logger.warning("Socket timeout during chunked receive") break except (ConnectionError, BrokenPipeError, ConnectionResetError) as e: logger.error(f"Socket connection error during receive: {str(e)}") raise # Re-raise to be handled by the caller except socket.timeout: logger.warning("Socket timeout during chunked receive") except Exception as e: logger.error(f"Error during receive: {str(e)}") raise # If we get here, we either timed out or broke out of the loop # Try to use what we have if chunks: data = b''.join(chunks) logger.info(f"Returning data after receive completion ({len(data)} bytes)") try: # Try to parse what we have json.loads(data.decode('utf-8')) return data except json.JSONDecodeError: # If we can't parse it, it's incomplete raise Exception("Incomplete JSON response received") else: raise Exception("No data received") def send_command(self, command_type: str, params: Dict[str, Any] | None = None) -> Dict[str, Any]: """Send a command to Blender and return the response""" if not self.sock and not self.connect(): raise ConnectionError("Not connected to Blender") command = { "type": command_type, "params": params or {} } try: # Log the command being sent logger.info(f"Sending command: {command_type} with params: {params}") # Send the command self.sock.sendall(json.dumps(command).encode('utf-8')) logger.info(f"Command sent, waiting for response...") # Set a timeout for receiving - use the same timeout as in receive_full_response self.sock.settimeout(15.0) # Match the addon's timeout # Receive the response using the improved receive_full_response method response_data = self.receive_full_response(self.sock) logger.info(f"Received {len(response_data)} bytes of data") response = json.loads(response_data.decode('utf-8')) logger.info(f"Response parsed, status: {response.get('status', 'unknown')}") if response.get("status") == "error": logger.error(f"Blender error: {response.get('message')}") raise Exception(response.get("message", "Unknown error from Blender")) return response.get("result", {}) except socket.timeout: logger.error("Socket timeout while waiting for response from Blender") # Don't try to reconnect here - let the get_blender_connection handle reconnection # Just invalidate the current socket so it will be recreated next time self.sock = None raise Exception("Timeout waiting for Blender response - try simplifying your request") except (ConnectionError, BrokenPipeError, ConnectionResetError) as e: logger.error(f"Socket connection error: {str(e)}") self.sock = None raise Exception(f"Connection to Blender lost: {str(e)}") except json.JSONDecodeError as e: logger.error(f"Invalid JSON response from Blender: {str(e)}") # Try to log what was received if 'response_data' in locals() and response_data: logger.error(f"Raw response (first 200 bytes): {response_data[:200]}") raise Exception(f"Invalid response from Blender: {str(e)}") except Exception as e: logger.error(f"Error communicating with Blender: {str(e)}") # Don't try to reconnect here - let the get_blender_connection handle reconnection self.sock = None raise Exception(f"Communication error with Blender: {str(e)}") @asynccontextmanager async def server_lifespan(server: FastMCP) -> AsyncIterator[Dict[str, Any]]: """Manage server startup and shutdown lifecycle""" # We don't need to create a connection here since we're using the global connection # for resources and tools try: # Just log that we're starting up logger.info("BlenderMCP server starting up") # Try to connect to Blender on startup to verify it's available try: # This will initialize the global connection if needed blender = get_blender_connection() logger.info("Successfully connected to Blender on startup") except Exception as e: logger.warning(f"Could not connect to Blender on startup: {str(e)}") logger.warning("Make sure the Blender addon is running before using Blender resources or tools") # Return an empty context - we're using the global connection yield {} finally: # Clean up the global connection on shutdown global _blender_connection if _blender_connection: logger.info("Disconnecting from Blender on shutdown") _blender_connection.disconnect() _blender_connection = None logger.info("BlenderMCP server shut down") # Create the MCP server with lifespan support mcp = FastMCP( "Bonsai MCP", description="IFC manipulation through Blender and MCP", lifespan=server_lifespan ) # Resource endpoints # Global connection for resources (since resources can't access context) _blender_connection = None def get_blender_connection(): """Get or create a persistent Blender connection""" global _blender_connection # If we have an existing connection, check if it's still valid if _blender_connection is not None: try: # Simple ping to check if connection is still alive _blender_connection.send_command("get_ifc_project_info") return _blender_connection except Exception as e: # Connection is dead, close it and create a new one logger.warning(f"Existing connection is no longer valid: {str(e)}") try: _blender_connection.disconnect() except: pass _blender_connection = None # Create a new connection if needed if _blender_connection is None: _blender_connection = BlenderConnection(host="localhost", port=9876) if not _blender_connection.connect(): logger.error("Failed to connect to Blender") _blender_connection = None raise Exception("Could not connect to Blender. Make sure the Blender addon is running.") logger.info("Created new persistent connection to Blender") return _blender_connection # ------------------------------- # MCP TOOLS # ------------------------------- @mcp.tool() def execute_blender_code(ctx: Context, code: str) -> str: """ Execute arbitrary Python code in Blender. Parameters: - code: The Python code to execute """ try: # Get the global connection blender = get_blender_connection() result = blender.send_command("execute_code", {"code": code}) return f"Code executed successfully: {result.get('result', '')}" except Exception as e: logger.error(f"Error executing code: {str(e)}") return f"Error executing code: {str(e)}" ### IFC Tools @mcp.tool() def get_ifc_project_info() -> str: """ Get basic information about the IFC project, including name, description, and counts of different entity types. Returns: A JSON-formatted string with project information """ try: blender = get_blender_connection() result = blender.send_command("get_ifc_project_info") # Return the formatted JSON of the results return json.dumps(result, indent=2) except Exception as e: logger.error(f"Error getting IFC project info: {str(e)}") return f"Error getting IFC project info: {str(e)}" @mcp.tool() def get_selected_ifc_entities() -> str: """ Get IFC entities corresponding to the currently selected objects in Blender. This allows working specifically with objects the user has manually selected in the Blender UI. Returns: A JSON-formatted string with information about the selected IFC entities """ try: blender = get_blender_connection() result = blender.send_command("get_selected_ifc_entities") # Return the formatted JSON of the results return json.dumps(result, indent=2) except Exception as e: logger.error(f"Error getting selected IFC entities: {str(e)}") return f"Error getting selected IFC entities: {str(e)}" # Modify the existing list_ifc_entities function to accept a selected_only parameter @mcp.tool() def list_ifc_entities(entity_type: str | None = None, limit: int = 50, selected_only: bool = False) -> str: """ List IFC entities of a specific type. Can be filtered to only include objects currently selected in the Blender UI. Args: entity_type: Type of IFC entity to list (e.g., "IfcWall") limit: Maximum number of entities to return selected_only: If True, only return information about selected objects Returns: A JSON-formatted string listing the specified entities """ try: blender = get_blender_connection() result = blender.send_command("list_ifc_entities", { "entity_type": entity_type, "limit": limit, "selected_only": selected_only }) # Return the formatted JSON of the results return json.dumps(result, indent=2) except Exception as e: logger.error(f"Error listing IFC entities: {str(e)}") return f"Error listing IFC entities: {str(e)}" # Modify the existing get_ifc_properties function to accept a selected_only parameter @mcp.tool() def get_ifc_properties(global_id: str | None = None, selected_only: bool = False) -> str: """ Get properties of IFC entities. Can be used to get properties of a specific entity by GlobalId, or to get properties of all currently selected objects in Blender. Args: global_id: GlobalId of a specific IFC entity (optional if selected_only is True) selected_only: If True, return properties for all selected objects instead of a specific entity Returns: A JSON-formatted string with entity information and properties """ try: blender = get_blender_connection() # Validate parameters if not global_id and not selected_only: return json.dumps({"error": "Either global_id or selected_only must be specified"}, indent=2) result = blender.send_command("get_ifc_properties", { "global_id": global_id, "selected_only": selected_only }) # Return the formatted JSON of the results return json.dumps(result, indent=2) except Exception as e: logger.error(f"Error getting IFC properties: {str(e)}") return f"Error getting IFC properties: {str(e)}" @mcp.tool() def get_ifc_spatial_structure() -> str: """ Get the spatial structure of the IFC model (site, building, storey, space hierarchy). Returns: A JSON-formatted string representing the hierarchical structure of the IFC model """ try: blender = get_blender_connection() result = blender.send_command("get_ifc_spatial_structure") # Return the formatted JSON of the results return json.dumps(result, indent=2) except Exception as e: logger.error(f"Error getting IFC spatial structure: {str(e)}") return f"Error getting IFC spatial structure: {str(e)}" @mcp.tool() def get_ifc_relationships(global_id: str) -> str: """ Get all relationships for a specific IFC entity. Args: global_id: GlobalId of the IFC entity Returns: A JSON-formatted string with all relationships the entity participates in """ try: blender = get_blender_connection() result = blender.send_command("get_ifc_relationships", { "global_id": global_id }) # Return the formatted JSON of the results return json.dumps(result, indent=2) except Exception as e: logger.error(f"Error getting IFC relationships: {str(e)}") return f"Error getting IFC relationships: {str(e)}" @mcp.tool() def export_ifc_data( entity_type: str | None = None, level_name: str | None = None, output_format: str = "csv", ctx: Context | None = None ) -> str: """ Export IFC data to a file in JSON or CSV format. This tool extracts IFC data and creates a structured export file. You can filter by entity type and/or building level, and choose the output format. Args: entity_type: Type of IFC entity to export (e.g., "IfcWall") - leave empty for all entities level_name: Name of the building level to filter by (e.g., "Level 1") - leave empty for all levels output_format: "json" or "csv" format for the output file Returns: Confirmation message with the export file path or an error message """ try: # Get Blender connection blender = get_blender_connection() # Validate output format if output_format not in ["json", "csv"]: return "Error: output_format must be 'json' or 'csv'" # Execute the export code in Blender result = blender.send_command("export_ifc_data", { "entity_type": entity_type, "level_name": level_name, "output_format": output_format }) # Check for errors from Blender if isinstance(result, dict) and "error" in result: return f"Error: {result['error']}" # Return the result with export summary # return result return json.dumps(result, indent=2) except Exception as e: logger.error(f"Error exporting IFC data: {str(e)}") return f"Error exporting IFC data: {str(e)}" @mcp.tool() def place_ifc_object( type_name: str, x: float, y: float, z: float, rotation: float = 0.0, ctx: Context| None = None ) -> str: """ Place an IFC object at a specified location with optional rotation. This tool allows you to create and position IFC elements in the model. The object is placed using the specified IFC type and positioned at the given coordinates with optional rotation around the Z axis. Args: type_name: Name of the IFC element type to place (must exist in the model) x: X-coordinate in model space y: Y-coordinate in model space z: Z-coordinate in model space rotation: Rotation angle in degrees around the Z axis (default: 0) Returns: A message with the result of the placement operation """ try: # Get Blender connection blender = get_blender_connection() # Send command to place the object result = blender.send_command("place_ifc_object", { "type_name": type_name, "location": [x, y, z], "rotation": rotation }) # Check for errors if isinstance(result, dict) and "error" in result: return f"Error placing object: {result['error']}" # Format success message if isinstance(result, dict) and result.get("success"): return (f"Successfully placed '{type_name}' object at ({x}, {y}, {z}) " f"with {rotation}° rotation.\nObject name: {result.get('blender_name')}, " f"Global ID: {result.get('global_id')}") # Return the raw result as string if it's not a success or error dict return f"Placement result: {json.dumps(result, indent=2)}" except Exception as e: logger.error(f"Error placing IFC object: {str(e)}") return f"Error placing IFC object: {str(e)}" @mcp.tool() def get_user_view() -> Image: """ Capture and return the current Blender viewport as an image. Shows what the user is currently seeing in Blender. Focus mostly on the 3D viewport. Use the UI to assist in your understanding of the scene but only refer to it if specifically prompted. Args: max_dimension: Maximum dimension (width or height) in pixels for the returned image compression_quality: Image compression quality (1-100, higher is better quality but larger) Returns: An image of the current Blender viewport """ max_dimension = 800 compression_quality = 85 # Use PIL to compress the image from PIL import Image as PILImage import io try: # Get the global connection blender = get_blender_connection() # Request current view result = blender.send_command("get_current_view") if "error" in result: # logger.error(f"Error getting view from Blender: {result.get('error')}") raise Exception(f"Error getting current view: {result.get('error')}") # Extract image information if "data" not in result or "width" not in result or "height" not in result: # logger.error("Incomplete image data returned from Blender") raise Exception("Incomplete image data returned from Blender") # Decode the base64 image data image_data = base64.b64decode(result["data"]) original_width = result["width"] original_height = result["height"] original_format = result.get("format", "png") # Compression is only needed if the image is large if original_width > 800 or original_height > 800 or len(image_data) > 1000000: # logger.info(f"Compressing image (original size: {len(image_data)} bytes)") # Open image from binary data img = PILImage.open(io.BytesIO(image_data)) # Resize if needed if original_width > max_dimension or original_height > max_dimension: # Calculate new dimensions maintaining aspect ratio if original_width > original_height: new_width = max_dimension new_height = int(original_height * (max_dimension / original_width)) else: new_height = max_dimension new_width = int(original_width * (max_dimension / original_height)) # Resize using high-quality resampling img = img.resize((new_width, new_height), PILImage.Resampling.LANCZOS) # Convert to RGB if needed if img.mode == 'RGBA': img = img.convert('RGB') # Save as JPEG with compression output = io.BytesIO() img.save(output, format='JPEG', quality=compression_quality, optimize=True) compressed_data = output.getvalue() # logger.info(f"Image compressed from {len(image_data)} to {len(compressed_data)} bytes") # Return compressed image return Image(data=compressed_data, format="jpeg") else: # Image is small enough, return as-is return Image(data=image_data, format=original_format) except Exception as e: # logger.error(f"Error processing viewport image: {str(e)}") raise Exception(f"Error processing viewport image: {str(e)}") @mcp.tool() def get_ifc_quantities() -> str: """ Extract and get basic qtos about the IFC project. Returns: A JSON-formatted string with project quantities information """ try: blender = get_blender_connection() result = blender.send_command("get_ifc_quantities") # Return the formatted JSON of the results return json.dumps(result, indent=2) except Exception as e: logger.error(f"Error getting IFC project quantities: {str(e)}") return f"Error getting IFC project quantities: {str(e)}" # WIP, not ready to be implemented: # @mcp.tool() # def create_plan_view(height_offset: float = 0.5, view_type: str = "top", # resolution_x: int = 400, resolution_y: int = 400, # output_path: str = None) -> Image: # """ # Create a plan view (top-down view) at the specified height above the first building story. # Args: # height_offset: Height in meters above the building story (default 0.5m) # view_type: Type of view - "top", "front", "right", "left" (note: only "top" is fully implemented) # resolution_x: Horizontal resolution of the render in pixels - Keep it small, max 800 x 800, recomended 400 x 400 # resolution_y: Vertical resolution of the render in pixels # output_path: Optional path to save the rendered image # Returns: # A rendered image showing the plan view of the model # """ # try: # # Get the global connection # blender = get_blender_connection() # # Request an orthographic render # result = blender.send_command("create_orthographic_render", { # "view_type": view_type, # "height_offset": height_offset, # "resolution_x": resolution_x, # "resolution_y": resolution_y, # "output_path": output_path # Can be None to use a temporary file # }) # if "error" in result: # raise Exception(f"Error creating plan view: {result.get('error', 'Unknown error')}") # if "data" not in result: # raise Exception("No image data returned from Blender") # # Decode the base64 image data # image_data = base64.b64decode(result["data"]) # # Return as an Image object # return Image(data=image_data, format="png") # except Exception as e: # logger.error(f"Error creating plan view: {str(e)}") # raise Exception(f"Error creating plan view: {str(e)}") @mcp.tool() def export_drawing_png( height_offset: float = 0.5, view_type: str = "top", resolution_x: int = 1920, resolution_y: int = 1080, storey_name: str | None = None, output_path: str | None = None ) -> dict: """Export drawings as PNG images with custom resolution. Creates a drawing, with the view type specified, of the IFC building at the specified height above the floor level. Supports custom resolution for high-quality architectural drawings. Args: height_offset: Height in meters above the storey level for the camera position (default 0.5m) view_type: Type of view - "top" for plan view, "front", "right" and "left" for elevation views, and "isometric" for 3D view resolution_x: Horizontal resolution in pixels (default 1920, max recommended 4096) resolution_y: Vertical resolution in pixels (default 1080, max recommended 4096) storey_name: Specific storey name to add to the file name (if None, prints default in the file name) output_path: Optional file path to save the PNG (if None, returns as base64 image) Returns: metadata and the path of the file image of the drawing at the specified resolution """ try: # Validate resolution limits for performance if resolution_x > 4096 or resolution_y > 4096: raise Exception("Resolution too high. Maximum recommended: 4096x4096 pixels") if resolution_x < 100 or resolution_y < 100: raise Exception("Resolution too low. Minimum: 100x100 pixels") # Get the global connection blender = get_blender_connection() # Request drawing render result = blender.send_command("export_drawing_png", { "view_type": view_type, "height_offset": height_offset, "resolution_x": resolution_x, "resolution_y": resolution_y, "storey_name": storey_name, "output_path": output_path }) if "error" in result: raise Exception(f"Error creating {view_type} drawing: {result.get('error', 'Unknown error')}") if "data" not in result: raise Exception("No image data returned from Blender") # Decode the base64 image data image_data = base64.b64decode(result["data"]) # Ensure output path exists if not output_path: os.makedirs("./exports/drawings", exist_ok=True) # Generate filename based on view type view_name = { "top": "plan_view", "front": "front_elevation", "right": "right_elevation", "left": "left_elevation", "isometric": "isometric_view" }.get(view_type, view_type) filename = f"{view_name}_{storey_name or 'default'}.png" output_path = os.path.join("./exports/drawings", filename) # Save to file with open(output_path, "wb") as f: f.write(image_data) # Return only metadata return { "status": "success", "file_path": os.path.abspath(output_path), # Opcional: si tienes un servidor de archivos, podrías devolver también una URL # "url": f"http://localhost:8000/files/{filename}" } except Exception as e: logger.error(f"Error exporting drawing: {str(e)}") return { "status": "error", "message": str(e) } @mcp.tool() def get_ifc_georeferencing_info(include_contexts: bool = False) -> str: """ Checks whether the IFC currently opened in Bonsai/BlenderBIM is georeferenced and returns the key georeferencing information. Parameters ---------- include_contexts : bool If True, adds a breakdown of the RepresentationContexts and operations. Returns -------- str (JSON pretty-printed) { "georeferenced": true|false, "crs": { "name": str|null, "geodetic_datum": str|null, "vertical_datum": str|null, "map_unit": str|null }, "map_conversion": { "eastings": float|null, "northings": float|null, "orthogonal_height": float|null, "scale": float|null, "x_axis_abscissa": float|null, "x_axis_ordinate": float|null }, "world_coordinate_system": { "origin": [x, y, z]|null }, "true_north": { "direction_ratios": [x, y]|null }, "site": { "local_placement_origin": [x, y, z]|null, "ref_latitude": [deg, min, sec, millionth]|null, "ref_longitude": [deg, min, sec, millionth]|null, "ref_elevation": float|null }, "contexts": [...], # only if include_contexts = true "warnings": [ ... ] # Informational message } Notes ----- - This tool acts as a wrapper: it sends the "get_ifc_georeferencing_info" command to the Blender add-on. The add-on must implement that logic (reading IfcProject/IfcGeometricRepresentationContext, IfcMapConversion, TargetCRS, IfcSite.RefLatitude/RefLongitude/RefElevation, etc.). - It always returns a JSON string with indentation for easier reading. """ blender = get_blender_connection() params = { "include_contexts": bool(include_contexts) } try: result = blender.send_command("get_ifc_georeferencing_info", params) # Ensures that the result is serializable and easy to read return json.dumps(result, ensure_ascii=False, indent=2) except Exception as e: logger.exception("get_ifc_georeferencing_info error") return json.dumps( { "georeferenced": False, "error": "Unable to retrieve georeferencing information from the IFC model.", "details": str(e) }, ensure_ascii=False, indent=2 ) @mcp.tool() def georeference_ifc_model( crs_mode: str, epsg: int = None, crs_name: str = None, geodetic_datum: str = None, map_projection: str = None, map_zone: str = None, eastings: float = None, northings: float = None, orthogonal_height: float = 0.0, scale: float = 1.0, x_axis_abscissa: float = None, x_axis_ordinate: float = None, true_north_azimuth_deg: float = None, context_filter: str = "Model", context_index: int = None, site_ref_latitude: list = None, # [deg, min, sec, millionth] site_ref_longitude: list = None, # [deg, min, sec, millionth] site_ref_elevation: float = None, site_ref_latitude_dd: float = None, # Decimal degrees (optional) site_ref_longitude_dd: float = None, # Decimal degrees (optional) overwrite: bool = False, dry_run: bool = False, write_path: str = None, ) -> str: """ Georeferences the IFC currently opened in Bonsai/BlenderBIM by creating or updating IfcProjectedCRS and IfcMapConversion. Optionally updates IfcSite and writes the file to disk. """ import json blender = get_blender_connection() # Build params excluding None values to keep the payload clean params = { "crs_mode": crs_mode, "epsg": epsg, "crs_name": crs_name, "geodetic_datum": geodetic_datum, "map_projection": map_projection, "map_zone": map_zone, "eastings": eastings, "northings": northings, "orthogonal_height": orthogonal_height, "scale": scale, "x_axis_abscissa": x_axis_abscissa, "x_axis_ordinate": x_axis_ordinate, "true_north_azimuth_deg": true_north_azimuth_deg, "context_filter": context_filter, "context_index": context_index, "site_ref_latitude": site_ref_latitude, "site_ref_longitude": site_ref_longitude, "site_ref_elevation": site_ref_elevation, "site_ref_latitude_dd": site_ref_latitude_dd, "site_ref_longitude_dd": site_ref_longitude_dd, "overwrite": overwrite, "dry_run": dry_run, "write_path": write_path, } params = {k: v for k, v in params.items() if v is not None} try: result = blender.send_command("georeference_ifc_model", params) return json.dumps(result, ensure_ascii=False, indent=2) except Exception as e: logger.exception("georeference_ifc_model error") return json.dumps( {"success": False, "error": "Could not georeference the model.", "details": str(e)}, ensure_ascii=False, indent=2, ) # ------------------------------- # MCP RESOURCES # ------------------------------- # Base path of the resource files BASE_PATH = Path("./resources") @mcp.resource("file://table_of_contents.md") def formulas_rp() -> str: """Leer el contenido del archivo table_of_contents.md""" file_path = BASE_PATH / "table_of_contents.md" try: with open(file_path, 'r', encoding='utf-8') as f: return f.read() except FileNotFoundError: return f"Error: No se encontró el archivo {file_path}" except Exception as e: return f"Error al leer el archivo: {str(e)}" # ------------------------------- # MCP PROMPTS # ------------------------------- @mcp.prompt("Technical_building_report") def technical_building_report(project_name: str, project_location: str, language: str = "english") -> str: """ Generate a comprehensive technical building report based on an IFC model loaded in Blender. Args: project_name: Name of the project/building project_location: Building location (city, address) language: Report language - "english", "spanish", "french", "german", "italian", "portuguese" Returns: Structured technical report following basic project standards in the selected language. """ # Language-specific instructions language_instructions = { "english": { "role": "You are a technical architect specialized in creating technical reports for basic building projects.", "objective": f"Your objective is to generate a comprehensive technical report for the building \"{project_name}\" located in \"{project_location}\", using data from the IFC model loaded in Blender.", "workflow_title": "## MANDATORY WORKFLOW:", "report_language": "Write the entire report in English." }, "spanish": { "role": "Eres un arquitecto técnico especializado en la creación de memorias técnicas de proyectos básicos de edificación.", "objective": f"Tu objetivo es generar una memoria técnica completa del edificio \"{project_name}\" localizado en \"{project_location}\", utilizando los datos del modelo IFC cargado en Blender.", "workflow_title": "## FLUJO DE TRABAJO OBLIGATORIO:", "report_language": "Redacta todo el informe en español." }, "french": { "role": "Vous êtes un architecte technique spécialisé dans la création de rapports techniques pour les projets de bâtiment de base.", "objective": f"Votre objectif est de générer un rapport technique complet pour le bâtiment \"{project_name}\" situé à \"{project_location}\", en utilisant les données du modèle IFC chargé dans Blender.", "workflow_title": "## FLUX DE TRAVAIL OBLIGATOIRE:", "report_language": "Rédigez tout le rapport en français." }, "german": { "role": "Sie sind ein technischer Architekt, der sich auf die Erstellung technischer Berichte für grundlegende Bauprojekte spezialisiert hat.", "objective": f"Ihr Ziel ist es, einen umfassenden technischen Bericht für das Gebäude \"{project_name}\" in \"{project_location}\" zu erstellen, unter Verwendung der Daten aus dem in Blender geladenen IFC-Modell.", "workflow_title": "## OBLIGATORISCHER ARBEITSABLAUF:", "report_language": "Verfassen Sie den gesamten Bericht auf Deutsch." }, "italian": { "role": "Sei un architetto tecnico specializzato nella creazione di relazioni tecniche per progetti edilizi di base.", "objective": f"Il tuo obiettivo è generare una relazione tecnica completa per l'edificio \"{project_name}\" situato a \"{project_location}\", utilizzando i dati del modello IFC caricato in Blender.", "workflow_title": "## FLUSSO DI LAVORO OBBLIGATORIO:", "report_language": "Scrivi tutto il rapporto in italiano." }, "portuguese": { "role": "Você é um arquiteto técnico especializado na criação de relatórios técnicos para projetos básicos de construção.", "objective": f"Seu objetivo é gerar um relatório técnico abrangente para o edifício \"{project_name}\" localizado em \"{project_location}\", usando dados do modelo IFC carregado no Blender.", "workflow_title": "## FLUXO DE TRABALHO OBRIGATÓRIO:", "report_language": "Escreva todo o relatório em português." } } # Get language instructions (default to English if language not supported) lang_config = language_instructions.get(language.lower(), language_instructions["english"]) return f""" {lang_config["role"]} {lang_config["objective"]} **LANGUAGE REQUIREMENT:** {lang_config["report_language"]} {lang_config["workflow_title"]} ### 1. INITIAL IFC MODEL ANALYSIS - **Use MCP tool:** `get_ifc_project_info` to get basic project information - **Use MCP tool:** `get_ifc_spatial_structure` to understand the building's spatial structure - **Use MCP tool:** `get_user_view` to capture a general view of the model ### 2. OBTAIN TABLE OF CONTENTS - **Access MCP resource:** `file://table_of_contents.md` to get the complete technical report structure ### 3. DETAILED ANALYSIS BY SECTIONS #### 3.1 For "General Building Data" Section: - **Use:** `get_ifc_quantities` to obtain areas and volumes - **Use:** `list_ifc_entities` with entity_type="IfcSpace" for spaces - **Use:** `list_ifc_entities` with entity_type="IfcBuildingStorey" for floors #### 3.2 For "Architectural Solution" Section: - **Use:** `list_ifc_entities` with entity_type="IfcWall" for walls - **Use:** `list_ifc_entities` with entity_type="IfcDoor" for doors - **Use:** `list_ifc_entities` with entity_type="IfcWindow" for windows - **Use:** `get_user_view` to capture representative views #### 3.3 For "Construction Systems" Section: - **Use:** `list_ifc_entities` with entity_type="IfcBeam" for beams - **Use:** `list_ifc_entities` with entity_type="IfcColumn" for columns - **Use:** `list_ifc_entities` with entity_type="IfcSlab" for slabs - **Use:** `list_ifc_entities` with entity_type="IfcRoof" for roofs - **Use:** `get_ifc_properties` to obtain material properties #### 3.4 For Building Services: - **Use:** `list_ifc_entities` with entity_type="IfcPipeSegment" for plumbing - **Use:** `list_ifc_entities` with entity_type="IfcCableSegment" for electrical - **Use:** `list_ifc_entities` with entity_type="IfcDuctSegment" for HVAC #### 3.5 For drawings and Graphic Documentation: - **Use:** `export_drawing_png` 5 times, using as parameter each time "top", "front", "right", "left" and "isometric", to generate architectural drawings. - **Configure:** resolution_x=1920, resolution_y=1080 for adequate quality - **Use:** `get_user_view` for complementary 3D views ### 4. TECHNICAL REPORT STRUCTURE Organize the document following exactly the structure from the `table_of_contents.md` resource: **TECHNICAL REPORT – BASIC PROJECT: {project_name}** **Location:** {project_location} #### 1. INTRODUCTION - Define object and scope based on IFC model data - Justify the adopted architectural solution #### 2. GENERAL BUILDING DATA - **Location:** {project_location} - **Areas:** Extract from quantities and spaces analysis - **Distribution:** Based on IFC spatial structure - **Regulations:** Identify applicable regulations according to use and location #### 3-11. DEVELOPMENT OF ALL SECTIONS - Complete each section according to the index, using data extracted from the IFC model - Include summary tables of areas, materials and construction elements - Generate technical conclusions based on evidence ### 5. MANDATORY GRAPHIC DOCUMENTATION - **2D drawings:**Include the 4 2D drawings generated before in the 3.5 section with the Tool `export_drawing_png` ("top", "front", "right", "left") - **3D views:** Include the isometric 3D view generated before in the 3.5 section with the Tool `export_drawing_png` - **Organize:** All images in section 11. Annexes ### 6. TECHNICAL TABLES AND CHARTS - **Areas summary table:** Extracted from quantities - **Elements listing:** By typologies (walls, columns, beams, etc.) - **Material properties:** From IFC properties ## RESPONSE FORMAT: ### MARKDOWN STRUCTURE: ```markdown # TECHNICAL REPORT – BASIC PROJECT ## {project_name} ### Project Data: - **Location:** {project_location} - **Date:** [current date] - **IFC Model:** [model information] [Complete development of all index sections] ``` ### QUALITY CRITERIA: - **Technical precision:** All numerical data extracted directly from IFC model - **Completeness:** Cover all index sections mandatory - **Professional format:** Markdown tables, structured text, integrated images - **Consistency:** Verify data consistency between sections ## CRITICAL VALIDATIONS: 1. **Verify Blender connection:** Confirm IFC model is loaded 2. **Complete all sections:** Do not omit any index section 3. **Include graphic documentation:** drawings and 3D views mandatory 4. **Quantitative data:** Areas, volumes and quantities verified 5. **Regulatory consistency:** Applicable regulations according to use and location **IMPORTANT:** If any MCP tool fails or doesn't return data, document the limitation and indicate that section requires manual completion in executive project phase. Proceed to generate the technical report following this detailed workflow. """ # Main execution def main(): """Run the MCP server""" mcp.run() 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/JotaDeRodriguez/Bonsai_mcp'

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