Skip to main content
Glama

export_bc3_budget

Export construction budgets in BC3 format from IFC models in Blender by extracting spatial structures, quantities, and applying unit prices for cost estimation.

Instructions

Export a BC3 budget file (FIEBDC-3/2016) based on the IFC model loaded in Blender.

This tool creates a complete construction budget in BC3 format by:
1. Extracting the complete IFC spatial structure (Project → Site → Building → Storey)
2. Extracting IFC quantities and measurements for all building elements
3. Converting to BC3 hierarchical format with IFC2BC3Converter:
   - Generates budget chapters from IFC spatial hierarchy
   - Groups building elements by type and categories defined in external JSON
   - Assigns unit prices from language-specific JSON database
   - Creates detailed measurements sorted alphabetically
4. Exports to BC3 file with windows-1252 encoding

Features:
- Multi-language support (Spanish/English) for descriptions and labels
- Automatic element categorization using external JSON configuration
- Optimized conversion with O(1) lookups and batch operations
- Detailed measurements with dimensions (units, length, width, height)
- Full FIEBDC-3/2016 format compliance

Configuration files (in resources/bc3_helper_files/):
- precios_unitarios.json / unit_prices.json: Unit prices per IFC type
- spatial_labels_es.json / spatial_labels_en.json: Spatial element translations
- element_categories.json: IFC type to category mappings

Args:
    language: Language for the budget file ('es' for Spanish, 'en' for English). Default is 'es'.

Returns:
    A confirmation message with the path to the generated BC3 file in the exports/ folder.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
languageNoes

Implementation Reference

  • MCP tool handler function for 'export_bc3_budget'. Fetches IFC model structure and quantities via other MCP tools, creates an IFC2BC3Converter instance, and calls its export() method to generate the BC3 budget file.
    @mcp.tool()
    def export_bc3_budget(language: str = 'es') -> str:
        """
        Export a BC3 budget file (FIEBDC-3/2016) based on the IFC model loaded in Blender.
    
        This tool creates a complete construction budget in BC3 format by:
        1. Extracting the complete IFC spatial structure (Project → Site → Building → Storey)
        2. Extracting IFC quantities and measurements for all building elements
        3. Converting to BC3 hierarchical format with IFC2BC3Converter:
           - Generates budget chapters from IFC spatial hierarchy
           - Groups building elements by type and categories defined in external JSON
           - Assigns unit prices from language-specific JSON database
           - Creates detailed measurements sorted alphabetically
        4. Exports to BC3 file with windows-1252 encoding
    
        Features:
        - Multi-language support (Spanish/English) for descriptions and labels
        - Automatic element categorization using external JSON configuration
        - Optimized conversion with O(1) lookups and batch operations
        - Detailed measurements with dimensions (units, length, width, height)
        - Full FIEBDC-3/2016 format compliance
    
        Configuration files (in resources/bc3_helper_files/):
        - precios_unitarios.json / unit_prices.json: Unit prices per IFC type
        - spatial_labels_es.json / spatial_labels_en.json: Spatial element translations
        - element_categories.json: IFC type to category mappings
    
        Args:
            language: Language for the budget file ('es' for Spanish, 'en' for English). Default is 'es'.
    
        Returns:
            A confirmation message with the path to the generated BC3 file in the exports/ folder.
        """
        try:
            # Get IFC data
            logger.info("Getting IFC data...")
            ifc_total_structure = get_ifc_total_structure()
            ifc_quantities = get_ifc_quantities()
    
            # Validate that we got valid JSON responses
            # If there's an error, these functions return error strings starting with "Error"
            if isinstance(ifc_total_structure, str) and ifc_total_structure.startswith("Error"):
                return f"Failed to get IFC structure: {ifc_total_structure}"
    
            if isinstance(ifc_quantities, str) and ifc_quantities.startswith("Error"):
                return f"Failed to get IFC quantities: {ifc_quantities}"
    
            # Try to parse the JSON to ensure it's valid
            try:
                structure_data = json.loads(ifc_total_structure) if isinstance(ifc_total_structure, str) else ifc_total_structure
                quantities_data = json.loads(ifc_quantities) if isinstance(ifc_quantities, str) else ifc_quantities
            except json.JSONDecodeError as e:
                return f"Invalid JSON data received from Blender. Structure error: {str(e)}"
    
            converter = IFC2BC3Converter(structure_data, quantities_data, language=language)
            output_path = converter.export()
    
            return f"BC3 file successfully created at: {output_path}"
    
        except Exception as e:
            logger.error(f"Error creating BC3 budget: {str(e)}")
            return f"Error creating BC3 budget: {str(e)}"
  • Core helper class IFC2BC3Converter that performs the actual conversion from IFC JSON data to BC3 format. Includes configuration loading, data processing, spatial hierarchy traversal, element categorization, quantity extraction, BC3 record building, and file export. The export() method generates and saves the BC3 file.
    class IFC2BC3Converter:
        """
        Converts IFC structures (JSON) to BC3 format for construction budgets.
    
        Architecture:
        - Loads and validates input data (IFC structure, quantities, prices)
        - Generates chapter hierarchy from IFC spatial structure
        - Groups building elements into budget items by type
        - Exports to FIEBDC-3 (BC3) format with windows-1252 encoding
    
        Method Groups:
        1. Initialization & Configuration Loading
        2. Data Parsing & Indexing
        3. Code Generation & Formatting
        4. IFC Element Classification
        5. Quantity & Measurement Extraction
        6. BC3 Record Building
        7. Core Processing & Conversion Logic
        8. Public API Methods
        """
    
        # Class constants
        SPATIAL_TYPES = {'IfcProject', 'IfcSite', 'IfcBuilding', 'IfcBuildingStorey', 'IfcBridge', 'IfcBridgePart'}
        IGNORED_TYPES = {'IfcSpace', 'IfcAnnotation', 'IfcGrid', 'IfcAxis'}
    
        def __init__(self, structure_data: Union[str, Dict], quantities_data: Union[str, Dict],
                     language: str = 'es'):
            """
            Initializes the converter with input data.
    
            Args:
                structure_data: JSON string or dict with IFC structure
                quantities_data: JSON string or dict with IFC quantities
                language: Language for the budget ('es' or 'en'). Default 'es'
            """
            # Parse input data
            self.structure_data = self._parse_json_input(structure_data)
            self.quantities_data = self._parse_json_input(quantities_data)
            self.quantities_by_id = self._index_quantities()
    
            # Configuration
            self.language = language
            self.unit_prices = self._load_unit_prices()
            self.spatial_labels = self._load_spatial_labels()
            self.element_categories = self._load_element_categories()
    
            # Counters using defaultdict for simplification
            self.chapter_counters = defaultdict(int)
            self.item_counters = defaultdict(int)
    
            # Registry of items and positions
            self.items_per_chapter = defaultdict(set)
            self.item_positions = defaultdict(dict)
    
            # Global registry of created concepts (to avoid duplicates)
            self.created_concepts = set()
    
            # Invert mapping for O(1) lookup
            self._ifc_to_category = self._build_ifc_category_map()
    
            # Cache for code-to-position conversions
            self._position_cache = {}
    
        # ============================================================================
        # 1. INITIALIZATION & CONFIGURATION LOADING
        # ============================================================================
        # Methods that load external configuration from JSON files and build
        # internal data structures during initialization.
    
        def _load_unit_prices(self) -> Dict[str, Dict]:
            """
            Loads unit prices from JSON file based on language.
            Optimized: Loads all prices at once (more efficient than lazy loading
            since typically most types are used in an IFC model).
    
            Returns:
                Dict with ifc_class as key and dict {code, description, long_description, unit, price} as value
            """
            filename = 'precios_unitarios.json' if self.language == 'es' else 'unit_prices.json'
            prices_path = BASE_PATH / filename
    
            if not prices_path.exists():
                print(f"Warning: Unit prices file not found at {prices_path}")
                return {}
    
            try:
                with open(prices_path, 'r', encoding='utf-8') as f:
                    data = json.load(f)
                    # Dict comprehension is faster than loop + assignment
                    return {
                        item['ifc_class']: {
                            'code': item['code'],
                            'description': item['description'],
                            'long_description': item['long_description'],
                            'unit': item['unit'],
                            'price': item['unit_price']
                        }
                        for item in data.get('prices', [])
                    }
            except Exception as e:
                print(f"Error loading unit prices: {e}")
                return {}
    
        def _load_spatial_labels(self) -> Dict[str, str]:
            """
            Loads spatial element labels from JSON file according to language.
    
            Returns:
                Dict with IFC type as key and translated label as value
            """
            filename = f'spatial_labels_{self.language}.json'
            labels_path = BASE_PATH / filename
    
            if not Path(labels_path).exists():
                print(f"Warning: Spatial labels file not found at {labels_path}")
                return {}
    
            try:
                with open(labels_path, 'r', encoding='utf-8') as f:
                    data = json.load(f)
                    return data.get('spatial_labels', {})
            except Exception as e:
                print(f"Error loading spatial labels: {e}")
                return {}
    
        def _load_element_categories(self) -> Dict[str, set]:
            """
            Loads element categories from JSON file.
    
            Returns:
                Dict with category code as key and set of IFC types as value
            """
            categories_path = BASE_PATH / 'element_categories.json'
    
            if not Path(categories_path).exists():
                print(f"Warning: Element categories file not found at {categories_path}")
                return {}
    
            try:
                with open(categories_path, 'r', encoding='utf-8') as f:
                    data = json.load(f)
                    # Convert lists to sets for O(1) membership testing
                    return {
                        category: set(ifc_types)
                        for category, ifc_types in data.get('element_categories', {}).items()
                    }
            except Exception as e:
                print(f"Error loading element categories: {e}")
                return {}
    
        def _build_ifc_category_map(self) -> Dict[str, str]:
            """Builds reverse mapping of IFC type -> category for O(1) lookup."""
            return {
                ifc_type: category
                for category, types in self.element_categories.items()
                for ifc_type in types
            }
    
        # ============================================================================
        # 2. DATA PARSING & INDEXING
        # ============================================================================
        # Methods that parse and index input data for efficient access during
        # conversion process.
    
        @staticmethod
        def _parse_json_input(data: Union[str, Dict]) -> Dict:
            """Parses input that can be JSON string or dict."""
            return json.loads(data) if isinstance(data, str) else data
    
        def _index_quantities(self) -> Dict[str, Dict]:
            """Indexes quantities by element ID for O(1) access."""
            elements = self.quantities_data.get('elements', [])
            return {elem['id']: elem for elem in elements}
    
        # ============================================================================
        # 3. CODE GENERATION & FORMATTING
        # ============================================================================
        # Methods that generate hierarchical codes, format positions, and escape
        # text for BC3 format compliance.
    
        def _generate_chapter_code(self, parent_code: str = '') -> str:
            """Generates a hierarchical chapter code."""
            # Root level uses sequential numbering: 01#, 02#, 03#...
            if parent_code == 'R_A_I_Z##':
                self.chapter_counters['root'] += 1
                return f'{self.chapter_counters["root"]:02d}#'
    
            # Sub-levels use hierarchical notation: 01.01#, 01.01.01#...
            base_code = parent_code.rstrip('#')
            self.chapter_counters[base_code] += 1
            return f'{base_code}.{self.chapter_counters[base_code]:02d}#'
    
        def _generate_item_code(self, category: str, chapter_code: str = None) -> str:
            """Generates a unique code for a budget item globally (not per chapter)."""
            # Use only category as key to ensure global uniqueness
            self.item_counters[category] += 1
            return f"{category}{self.item_counters[category]:03d}"
    
        def _chapter_code_to_position(self, chapter_code: str) -> str:
            """
            Converts chapter code to position format with caching.
            Example: '01.02.03#' -> '1\\2\\3'
            """
            if chapter_code in self._position_cache:
                return self._position_cache[chapter_code]
    
            clean_code = chapter_code.rstrip('#')
            parts = clean_code.split('.')
            position_parts = [str(int(part)) for part in parts]
            result = '\\'.join(position_parts)
    
            self._position_cache[chapter_code] = result
            return result
    
        @staticmethod
        def _escape_bc3_text(text: str) -> str:
            """Escapes special characters for BC3 format."""
            if not text:
                return ''
            # Normalize and clean whitespace
            text = str(text).strip().replace('\r', ' ').replace('\n', ' ').replace('\t', ' ')
            # Escape BC3 special characters
            return text.replace('|', ' ').replace('~', '-')
    
        # ============================================================================
        # 4. IFC ELEMENT CLASSIFICATION
        # ============================================================================
        # Methods that classify, categorize and filter IFC elements based on their
        # type and properties.
    
        def _get_category_code(self, ifc_type: str) -> str:
            """Gets category code for an IFC type (O(1) lookup)."""
            return self._ifc_to_category.get(ifc_type, 'OTROS')
    
        def _get_spatial_element_label(self, ifc_type: str) -> str:
            """Gets translated label for spatial elements from loaded JSON."""
            return self.spatial_labels.get(ifc_type, ifc_type)
    
        @classmethod
        def _is_spatial_element(cls, ifc_type: str) -> bool:
            """Determines if an IFC element is spatial (container)."""
            return ifc_type in cls.SPATIAL_TYPES
    
        @classmethod
        def _is_ignored_element(cls, ifc_type: str) -> bool:
            """Determines if an element should be ignored."""
            return ifc_type in cls.IGNORED_TYPES
    
        def _group_elements_by_type(self, elements: List[Dict]) -> Dict[str, List[Dict]]:
            """Groups elements by IFC type, ignoring invalid types."""
            groups = defaultdict(list)
            for elem in elements:
                if not self._is_ignored_element(elem['type']):
                    groups[elem['type']].append(elem)
            return groups
    
        def _is_unit_based_element(self, ifc_type: str) -> bool:
            """
            Determines if an element is measured by unit (no dimensions needed).
            Unit-based elements: doors, windows, furniture, stairs, railings, fittings, terminals.
            """
            unit_based_types = {
                'IfcDoor', 'IfcWindow',  # CARP - Carpentry
                'IfcFurnishingElement', 'IfcFurniture',  # MOB - Furniture
                'IfcStair',  # ESTR - Stairs (counted as units)
                'IfcFlowFitting', 'IfcFlowTerminal', 'IfcDistributionElement', 'IfcRailing'  # INST - Installations
            }
            return ifc_type in unit_based_types
    
        def _is_linear_element(self, ifc_type: str) -> bool:
            """
            Determines if an element is measured by length (meters).
            Linear elements: beams, columns, piles.
            """
            linear_types = {
                'IfcBeam',  # ESTR - Beams
                'IfcColumn',  # ESTR - Columns
                'IfcPile'  # ESTR - Piles
            }
            return ifc_type in linear_types
    
        # ============================================================================
        # 5. QUANTITY & MEASUREMENT EXTRACTION
        # ============================================================================
        # Methods that extract quantities, dimensions, and measurements from IFC
        # elements and format them for BC3 records.
    
        def _get_quantities_for_element(self, element_id: str) -> Dict[str, float]:
            """Gets quantities for an element."""
            return self.quantities_by_id.get(element_id, {}).get('quantities', {})
    
        @staticmethod
        def _get_measurement_dimensions(quantities: Dict[str, float], ifc_type: str = None) -> tuple:
            """
            Extracts dimensions from quantities based on element type.
            - Walls (IfcWall*): Use NetSideArea (accounts for doors/windows)
            - Slabs/Roofs (IfcSlab, IfcRoof): Use GrossVolume
            - Other elements: Use NetVolume or fallback values
            Returns (units, length, width, height)
            """
            if not quantities:
                return (1.0, 0.0, 0.0, 0.0)
    
            # Walls: ONLY use NetSideArea (lateral area without openings)
            if ifc_type and ifc_type.startswith('IfcWall'):
                net_side_area = quantities.get('NetSideArea', 0.0)
                # Force return NetSideArea for walls, even if 0
                return (1.0, net_side_area, 0.0, 0.0)
    
            # Slabs and Roofs: ONLY use GrossVolume
            if ifc_type in ('IfcSlab', 'IfcRoof'):
                gross_volume = quantities.get('GrossVolume', 0.0)
                # Force return GrossVolume for slabs/roofs, even if 0
                return (1.0, gross_volume, 0.0, 0.0)
    
            # Priority 1: Use NetVolume (accounts for openings and voids)
            net_volume = quantities.get('NetVolume', 0.0)
            if net_volume > 0:
                return (1.0, net_volume, 0.0, 0.0)
    
            # Priority 2: Use NetSideArea as fallback
            net_side_area = quantities.get('NetSideArea', 0.0)
            if net_side_area > 0:
                return (1.0, net_side_area, 0.0, 0.0)
    
            # Priority 3: Use GrossVolume or GrossSideArea as fallback
            gross_volume = quantities.get('GrossVolume', 0.0)
            gross_side_area = quantities.get('GrossSideArea', 0.0)
    
            if gross_volume > 0:
                return (1.0, gross_volume, 0.0, 0.0)
            elif gross_side_area > 0:
                return (1.0, gross_side_area, 0.0, 0.0)
    
            # Priority 4: Use basic dimensions (for linear elements)
            length = quantities.get('Length', 0.0)
            width = quantities.get('Width', 0.0)
            height = quantities.get('Height', 0.0)
    
            return (1.0, length, width, height)
    
        def _get_item_data(self, ifc_type: str, category: str, chapter_code: str) -> Dict[str, Any]:
            """Gets all necessary data to create a budget item."""
            price_data = self.unit_prices.get(ifc_type, {})
            return {
                'code': price_data.get('code', self._generate_item_code(category, chapter_code)),
                'description': price_data.get('description', ifc_type.replace('Ifc', '')),
                'long_description': price_data.get('long_description', f"Item for {ifc_type}"),
                'unit': price_data.get('unit', 'ud'),
                'price': price_data.get('price', 100.0)
            }
    
        def _create_measurement_lines(self, elements: List[Dict], ifc_type: str) -> List[str]:
            """Creates measurement lines for a list of elements, sorted alphabetically by name."""
            # Sort elements by name before processing (handle None values)
            sorted_elements = sorted(elements, key=lambda e: e.get('name') or '')
    
            measurement_lines = []
            for idx, elem in enumerate(sorted_elements, 1):
                elem_name = self._escape_bc3_text(elem.get('name', f'Element {idx}'))
    
                # Elements measured by unit (doors, windows, furniture) don't need dimensions
                if self._is_unit_based_element(ifc_type):
                    line_parts = [elem_name, "1.000", "", "", ""]
                # Linear elements (beams, columns, piles) measured by length
                elif self._is_linear_element(ifc_type):
                    quantities = self._get_quantities_for_element(elem['id'])
                    length = quantities.get('Length', 0.0)
                    line_parts = [
                        elem_name,
                        "1.000",
                        f"{length:.2f}" if length > 0 else "",
                        "",
                        ""
                    ]
                else:
                    quantities = self._get_quantities_for_element(elem['id'])
                    units, length, width, height = self._get_measurement_dimensions(quantities, ifc_type)
                    line_parts = [
                        elem_name,
                        f"{units:.3f}",
                        f"{length:.2f}" if length > 0 else "",
                        f"{width:.2f}" if width > 0 else "",
                        f"{height:.2f}" if height > 0 else ""
                    ]
    
                measurement_lines.append('\\'.join(line_parts))
    
            return measurement_lines
    
        # ============================================================================
        # 6. BC3 RECORD BUILDING
        # ============================================================================
        # Methods that construct individual BC3 format records (~V, ~K, ~C, ~D, ~T, ~M).
        # These are the low-level builders for BC3 file structure.
    
        @staticmethod
        def _create_bc3_header() -> List[str]:
            """Creates BC3 file header lines."""
            date_code = datetime.now().strftime('%d%m%Y')
            return [
                f'~V||FIEBDC-3/2016\\{date_code}|IFC2BC3 Converter|\\|ANSI||',
                '~K|3\\3\\3\\2\\2\\2\\2\\2\\|0\\0\\0\\0\\0\\|3\\2\\\\2\\2\\\\2\\2\\2\\3\\3\\3\\3\\2\\EUR\\|'
            ]
    
        def _build_chapter_record(self, code: str, name: str) -> str:
            """Builds ~C record for a chapter."""
            return f"~C|{code}\\||{name}|0\\||||||"
    
        def _build_decomposition_record(self, code: str, child_codes: List[str]) -> str:
            """Builds ~D decomposition record."""
            children_str = '\\'.join([f"{c}\\\\1.000" for c in child_codes])
            return f"~D|{code}|{children_str}|"
    
        def _build_item_record(self, code: str, unit: str, name: str, price: float, date: str) -> str:
            """Builds ~C record for a budget item."""
            return f"~C|{code}|{unit}|{name}|{price:.2f}||{date}|"
    
        def _build_text_record(self, code: str, description: str) -> str:
            """Builds ~T descriptive text record."""
            return f"~T|{code}|{description}|"
    
        def _build_measurement_record(self, chapter_code: str, item_code: str,
                                     position: str, measurement_content: str) -> str:
            """Builds ~M measurements record."""
            return f"~M|{chapter_code}\\{item_code}|{position}|0|\\{measurement_content}\\|"
    
        # ============================================================================
        # 7. CORE PROCESSING & CONVERSION LOGIC
        # ============================================================================
        # Methods that orchestrate the conversion process by processing spatial
        # structure and building elements recursively.
    
        def _process_spatial_node(self, node: Dict, parent_code: str, lines: List[str], depth: int = 0) -> str:
            """Recursively processes a spatial node (chapter) from IFC structure."""
            if self._is_ignored_element(node['type']):
                return None
    
            # Generate chapter code and name
            code = 'R_A_I_Z##' if depth == 0 else self._generate_chapter_code(parent_code)
    
            label = self._get_spatial_element_label(node['type'])
            node_name = node.get('name', '')
            full_name = f"{label} - {node_name}" if node_name else label
            name = self._escape_bc3_text(full_name)
    
            # Add chapter record
            lines.append(self._build_chapter_record(code, name))
    
            decomposition_codes = []
    
            # Process building elements
            building_elements = node.get('building_elements', [])
            if building_elements:
                item_codes = self._process_building_elements(building_elements, code, lines)
                decomposition_codes.extend(item_codes)
    
            # Process spatial children recursively
            for child in node.get('children', []):
                if self._is_spatial_element(child['type']):
                    child_code = self._process_spatial_node(child, code, lines, depth + 1)
                    if child_code:
                        decomposition_codes.append(child_code)
    
            # Add decomposition record
            if decomposition_codes:
                lines.append(self._build_decomposition_record(code, decomposition_codes))
    
            return code
    
        def _process_building_elements(self, elements: List[Dict], chapter_code: str, lines: List[str]) -> List[str]:
            """
            Processes building elements and groups them by category.
            Optimized: Batch operations to reduce concatenation overhead.
            """
            created_items = []
            chapter_key = chapter_code.rstrip('#')
    
            # Group elements by type
            elements_by_type = self._group_elements_by_type(elements)
    
            # Pre-calculate common values outside loop
            chapter_position = self._chapter_code_to_position(chapter_code)
            date_str = datetime.now().strftime("%d%m%Y")
    
            # Process each group
            for ifc_type, type_elements in elements_by_type.items():
                category = self._get_category_code(ifc_type)
                item_key = f"{ifc_type}_{chapter_key}"
    
                # Check if this item already exists in this chapter
                if item_key in self.items_per_chapter[chapter_key]:
                    continue
    
                self.items_per_chapter[chapter_key].add(item_key)
    
                # Get item data
                item_data = self._get_item_data(ifc_type, category, chapter_code)
                item_code = item_data['code']
    
                # Register position
                position = len(self.item_positions[chapter_key]) + 1
                self.item_positions[chapter_key][item_code] = position
    
                # Escape texts (batch)
                name = self._escape_bc3_text(item_data['description'])
                long_desc = self._escape_bc3_text(item_data['long_description'])
    
                batch_records = []
    
                # Only create ~C and ~T records if this concept hasn't been created globally
                if item_code not in self.created_concepts:
                    self.created_concepts.add(item_code)
                    batch_records.extend([
                        self._build_item_record(item_code, item_data['unit'], name, item_data['price'], date_str),
                        self._build_text_record(item_code, long_desc)
                    ])
    
                # Always create measurements for this chapter
                measurement_lines = self._create_measurement_lines(type_elements, ifc_type)
                full_position = f"{chapter_position}\\{position}"
                measurement_content = '\\\\'.join(measurement_lines)
    
                batch_records.append(
                    self._build_measurement_record(chapter_code, item_code, full_position, measurement_content)
                )
    
                # Add batch at once (more efficient than 3 individual appends)
                lines.extend(batch_records)
                created_items.append(item_code)
    
            return created_items
    
        # ============================================================================
        # 8. PUBLIC API METHODS
        # ============================================================================
        # Public methods that provide the main interface for converting and
        # exporting BC3 files.
    
        def convert(self) -> str:
            """Performs complete conversion and returns BC3 file content."""
            lines = self._create_bc3_header()
    
            # Process structure from root
            # Try different possible root keys
            root = self.structure_data.get('structure')
            if not root and 'type' in self.structure_data:
                # If structure_data itself is the root node
                root = self.structure_data
    
            if root:
                self._process_spatial_node(root, '', lines, depth=0)
            else:
                print(f"Warning: No structure found. Keys available: {list(self.structure_data.keys())}")
    
            return '\n'.join(lines)
    
        def export(self, output_filename: str = 'ifc2bc3.bc3'):
            """Exports BC3 file to exports folder."""
            script_dir = Path(__file__).parent
            exports_dir = script_dir / 'exports'
            exports_dir.mkdir(exist_ok=True)
    
            bc3_content = self.convert()
            output_path = exports_dir / output_filename
    
            with open(output_path, 'w', encoding='windows-1252', newline='\r\n', errors='strict') as f:
                f.write(bc3_content)
    
            print(f"BC3 file successfully exported: {output_path}")
            return output_path
  • tools.py:631-631 (registration)
    The @mcp.tool() decorator registers the export_bc3_budget function as an MCP tool, using the function name as the tool name.
    @mcp.tool()
  • The convert() method in IFC2BC3Converter orchestrates the BC3 content generation by building header and processing the IFC spatial structure recursively.
    def convert(self) -> str:
        """Performs complete conversion and returns BC3 file content."""
        lines = self._create_bc3_header()
    
        # Process structure from root
        # Try different possible root keys
        root = self.structure_data.get('structure')
        if not root and 'type' in self.structure_data:
            # If structure_data itself is the root node
            root = self.structure_data
    
        if root:
            self._process_spatial_node(root, '', lines, depth=0)
        else:
            print(f"Warning: No structure found. Keys available: {list(self.structure_data.keys())}")
    
        return '\n'.join(lines)

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/JotaDeRodriguez/Bonsai_mcp'

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