Skip to main content
Glama
lamaalrajih

KiCad MCP Server

by lamaalrajih

export_bom_csv

Generate a CSV Bill of Materials for KiCad projects to document components and quantities for manufacturing and procurement.

Instructions

Export a Bill of Materials for a KiCad project.

This tool attempts to generate a CSV BOM file for a KiCad project. It requires KiCad to be installed with the appropriate command-line tools.

Args: project_path: Path to the KiCad project file (.kicad_pro) ctx: MCP context for progress reporting

Returns: Dictionary with export results

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
project_pathYes
ctxYes

Implementation Reference

  • The primary handler function for the 'export_bom_csv' tool. It orchestrates BOM export by first trying KiCad Python modules and falling back to CLI tools.
    @mcp.tool()
    async def export_bom_csv(project_path: str, ctx: Context | None) -> Dict[str, Any]:
        """Export a Bill of Materials for a KiCad project.
        
        This tool attempts to generate a CSV BOM file for a KiCad project.
        It requires KiCad to be installed with the appropriate command-line tools.
        
        Args:
            project_path: Path to the KiCad project file (.kicad_pro)
            ctx: MCP context for progress reporting
            
        Returns:
            Dictionary with export results
        """
        print(f"Exporting BOM for project: {project_path}")
        
        if not os.path.exists(project_path):
            print(f"Project not found: {project_path}")
            if ctx:
                ctx.info(f"Project not found: {project_path}")
            return {"success": False, "error": f"Project not found: {project_path}"}
        
        # Get access to the app context
        app_context = ctx.request_context.lifespan_context if ctx else None
        kicad_modules_available = app_context.kicad_modules_available if app_context else False
        
        # Report progress
        if ctx:
            await ctx.report_progress(10, 100)
        
        # Get all project files
        files = get_project_files(project_path)
        
        # We need the schematic file to generate a BOM
        if "schematic" not in files:
            print("Schematic file not found in project")
            if ctx:
                ctx.info("Schematic file not found in project")
            return {"success": False, "error": "Schematic file not found"}
        
        schematic_file = files["schematic"]
        project_dir = os.path.dirname(project_path)
        project_name = os.path.basename(project_path)[:-10]  # Remove .kicad_pro extension
        
        if ctx:
            await ctx.report_progress(20, 100)
            ctx.info(f"Found schematic file: {os.path.basename(schematic_file)}")
        
        # Try to export BOM
        # This will depend on KiCad's command-line tools or Python modules
        export_result = {"success": False}
        
        if kicad_modules_available:
            try:
                # Try to use KiCad Python modules
                if ctx:
                    ctx.info("Attempting to export BOM using KiCad Python modules...")
                export_result = await export_bom_with_python(schematic_file, project_dir, project_name, ctx)
            except Exception as e:
                print(f"Error exporting BOM with Python modules: {str(e)}", exc_info=True)
                if ctx:
                    ctx.info(f"Error using Python modules: {str(e)}")
                export_result = {"success": False, "error": str(e)}
        
        # If Python method failed, try command-line method
        if not export_result.get("success", False):
            try:
                if ctx:
                    ctx.info("Attempting to export BOM using command-line tools...")
                export_result = await export_bom_with_cli(schematic_file, project_dir, project_name, ctx)
            except Exception as e:
                print(f"Error exporting BOM with CLI: {str(e)}", exc_info=True)
                if ctx:
                    ctx.info(f"Error using command-line tools: {str(e)}")
                export_result = {"success": False, "error": str(e)}
        
        if ctx:
            await ctx.report_progress(100, 100)
        
        if export_result.get("success", False):
            if ctx:
                ctx.info(f"BOM exported successfully to {export_result.get('output_file', 'unknown location')}")
        else:
            if ctx:
                ctx.info(f"Failed to export BOM: {export_result.get('error', 'Unknown error')}")
        
        return export_result
  • Top-level registration call in the server creation that invokes register_bom_tools(mcp), which registers the export_bom_csv tool among BOM tools.
    register_bom_tools(mcp)
  • Key helper function that implements the command-line BOM export using kicad-cli, handling platform differences (Windows, macOS, Linux).
    async def export_bom_with_cli(schematic_file: str, output_dir: str, project_name: str, ctx: Context | None) -> Dict[str, Any]:
        """Export a BOM using KiCad command-line tools.
        
        Args:
            schematic_file: Path to the schematic file
            output_dir: Directory to save the BOM
            project_name: Name of the project
            ctx: MCP context for progress reporting
            
        Returns:
            Dictionary with export results
        """
        import subprocess
        import platform
        
        system = platform.system()
        print(f"Exporting BOM using CLI tools on {system}")
        if ctx:
            await ctx.report_progress(40, 100)
        
        # Output file path
        output_file = os.path.join(output_dir, f"{project_name}_bom.csv")
        
        # Define the command based on operating system
        if system == "Darwin":  # macOS
            from kicad_mcp.config import KICAD_APP_PATH
            
            # Path to KiCad command-line tools on macOS
            kicad_cli = os.path.join(KICAD_APP_PATH, "Contents/MacOS/kicad-cli")
            
            if not os.path.exists(kicad_cli):
                return {
                    "success": False,
                    "error": f"KiCad CLI tool not found at {kicad_cli}",
                    "schematic_file": schematic_file
                }
            
            # Command to generate BOM
            cmd = [
                kicad_cli,
                "sch",
                "export",
                "bom",
                "--output", output_file,
                schematic_file
            ]
        
        elif system == "Windows":
            from kicad_mcp.config import KICAD_APP_PATH
            
            # Path to KiCad command-line tools on Windows
            kicad_cli = os.path.join(KICAD_APP_PATH, "bin", "kicad-cli.exe")
            
            if not os.path.exists(kicad_cli):
                return {
                    "success": False,
                    "error": f"KiCad CLI tool not found at {kicad_cli}",
                    "schematic_file": schematic_file
                }
            
            # Command to generate BOM
            cmd = [
                kicad_cli,
                "sch",
                "export",
                "bom",
                "--output", output_file,
                schematic_file
            ]
        
        elif system == "Linux":
            # Assume kicad-cli is in the PATH
            kicad_cli = "kicad-cli"
            
            # Command to generate BOM
            cmd = [
                kicad_cli,
                "sch",
                "export",
                "bom",
                "--output", output_file,
                schematic_file
            ]
        
        else:
            return {
                "success": False,
                "error": f"Unsupported operating system: {system}",
                "schematic_file": schematic_file
            }
        
        try:
            print(f"Running command: {' '.join(cmd)}")
            if ctx:
                await ctx.report_progress(60, 100)
            
            # Run the command
            process = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
            
            # Check if the command was successful
            if process.returncode != 0:
                print(f"BOM export command failed with code {process.returncode}")
                print(f"Error output: {process.stderr}")
                
                return {
                    "success": False,
                    "error": f"BOM export command failed: {process.stderr}",
                    "schematic_file": schematic_file,
                    "command": ' '.join(cmd)
                }
            
            # Check if the output file was created
            if not os.path.exists(output_file):
                return {
                    "success": False,
                    "error": "BOM file was not created",
                    "schematic_file": schematic_file,
                    "output_file": output_file
                }
            
            if ctx:
                await ctx.report_progress(80, 100)
            
            # Read the first few lines of the BOM to verify it's valid
            with open(output_file, 'r') as f:
                bom_content = f.read(1024)  # Read first 1KB
            
            if len(bom_content.strip()) == 0:
                return {
                    "success": False,
                    "error": "Generated BOM file is empty",
                    "schematic_file": schematic_file,
                    "output_file": output_file
                }
            
            return {
                "success": True,
                "schematic_file": schematic_file,
                "output_file": output_file,
                "file_size": os.path.getsize(output_file),
                "message": "BOM exported successfully"
            }
        
        except subprocess.TimeoutExpired:
            print("BOM export command timed out after 30 seconds")
            return {
                "success": False,
                "error": "BOM export command timed out after 30 seconds",
                "schematic_file": schematic_file
            }
        
        except Exception as e:
            print(f"Error exporting BOM: {str(e)}", exc_info=True)
            return {
                "success": False,
                "error": f"Error exporting BOM: {str(e)}",
                "schematic_file": schematic_file
            }
  • Helper function for BOM export using KiCad Python modules, called first but falls back to CLI if unavailable.
    async def export_bom_with_python(schematic_file: str, output_dir: str, project_name: str, ctx: Context | None) -> Dict[str, Any]:
        """Export a BOM using KiCad Python modules.
        
        Args:
            schematic_file: Path to the schematic file
            output_dir: Directory to save the BOM
            project_name: Name of the project
            ctx: MCP context for progress reporting
            
        Returns:
            Dictionary with export results
        """
        print(f"Exporting BOM for schematic: {schematic_file}")
        if ctx:
            await ctx.report_progress(30, 100)
        
        try:
            # Try to import KiCad Python modules
            # This is a placeholder since exporting BOMs from schematic files
            # is complex and KiCad's API for this is not well-documented
            import kicad
            import kicad.pcbnew
            
            # For now, return a message indicating this method is not implemented yet
            print("BOM export with Python modules not fully implemented")
            if ctx:
                ctx.info("BOM export with Python modules not fully implemented yet")
            
            return {
                "success": False,
                "error": "BOM export using Python modules is not fully implemented yet. Try using the command-line method.",
                "schematic_file": schematic_file
            }
        
        except ImportError:
            print("Failed to import KiCad Python modules")
            return {
                "success": False,
                "error": "Failed to import KiCad Python modules",
                "schematic_file": schematic_file
            }
  • The register_bom_tools function that defines both BOM tools (analyze_bom and export_bom_csv) with @mcp.tool() decorators for registration.
    def register_bom_tools(mcp: FastMCP) -> None:
        """Register BOM-related tools with the MCP server.
        
        Args:
            mcp: The FastMCP server instance
        """
        
        @mcp.tool()
        async def analyze_bom(project_path: str, ctx: Context | None) -> Dict[str, Any]:
            """Analyze a KiCad project's Bill of Materials.
            
            This tool will look for BOM files related to a KiCad project and provide
            analysis including component counts, categories, and cost estimates if available.
            
            Args:
                project_path: Path to the KiCad project file (.kicad_pro)
                ctx: MCP context for progress reporting
                
            Returns:
                Dictionary with BOM analysis results
            """
            print(f"Analyzing BOM for project: {project_path}")
            
            if not os.path.exists(project_path):
                print(f"Project not found: {project_path}")
                if ctx:
                    ctx.info(f"Project not found: {project_path}")
                return {"success": False, "error": f"Project not found: {project_path}"}
            
            # Report progress
            if ctx:
                await ctx.report_progress(10, 100)
                ctx.info(f"Looking for BOM files related to {os.path.basename(project_path)}")
            
            # Get all project files
            files = get_project_files(project_path)
            
            # Look for BOM files
            bom_files = {}
            for file_type, file_path in files.items():
                if "bom" in file_type.lower() or file_path.lower().endswith(".csv"):
                    bom_files[file_type] = file_path
                    print(f"Found potential BOM file: {file_path}")
            
            if not bom_files:
                print("No BOM files found for project")
                if ctx:
                    ctx.info("No BOM files found for project")
                return {
                    "success": False, 
                    "error": "No BOM files found. Export a BOM from KiCad first.",
                    "project_path": project_path
                }
            
            if ctx:
                await ctx.report_progress(30, 100)
            
            # Analyze each BOM file
            results = {
                "success": True,
                "project_path": project_path,
                "bom_files": {},
                "component_summary": {}
            }
            
            total_unique_components = 0
            total_components = 0
            
            for file_type, file_path in bom_files.items():
                try:
                    if ctx:
                        ctx.info(f"Analyzing {os.path.basename(file_path)}")
                    
                    # Parse the BOM file
                    bom_data, format_info = parse_bom_file(file_path)
                    
                    if not bom_data or len(bom_data) == 0:
                        print(f"Failed to parse BOM file: {file_path}")
                        continue
                    
                    # Analyze the BOM data
                    analysis = analyze_bom_data(bom_data, format_info)
                    
                    # Add to results
                    results["bom_files"][file_type] = {
                        "path": file_path,
                        "format": format_info,
                        "analysis": analysis
                    }
                    
                    # Update totals
                    total_unique_components += analysis["unique_component_count"]
                    total_components += analysis["total_component_count"]
                    
                    print(f"Successfully analyzed BOM file: {file_path}")
                    
                except Exception as e:
                    print(f"Error analyzing BOM file {file_path}: {str(e)}", exc_info=True)
                    results["bom_files"][file_type] = {
                        "path": file_path,
                        "error": str(e)
                    }
            
            if ctx:
                await ctx.report_progress(70, 100)
            
            # Generate overall component summary
            if total_components > 0:
                results["component_summary"] = {
                    "total_unique_components": total_unique_components,
                    "total_components": total_components
                }
                
                # Calculate component categories across all BOMs
                all_categories = {}
                for file_type, file_info in results["bom_files"].items():
                    if "analysis" in file_info and "categories" in file_info["analysis"]:
                        for category, count in file_info["analysis"]["categories"].items():
                            if category not in all_categories:
                                all_categories[category] = 0
                            all_categories[category] += count
                
                results["component_summary"]["categories"] = all_categories
                
                # Calculate total cost if available
                total_cost = 0.0
                cost_available = False
                for file_type, file_info in results["bom_files"].items():
                    if "analysis" in file_info and "total_cost" in file_info["analysis"]:
                        if file_info["analysis"]["total_cost"] > 0:
                            total_cost += file_info["analysis"]["total_cost"]
                            cost_available = True
                
                if cost_available:
                    results["component_summary"]["total_cost"] = round(total_cost, 2)
                    currency = next((
                        file_info["analysis"].get("currency", "USD") 
                        for file_type, file_info in results["bom_files"].items() 
                        if "analysis" in file_info and "currency" in file_info["analysis"]
                    ), "USD")
                    results["component_summary"]["currency"] = currency
            
            if ctx:
                await ctx.report_progress(100, 100)
                ctx.info(f"BOM analysis complete: found {total_components} components")
            
            return results
        
        @mcp.tool()
        async def export_bom_csv(project_path: str, ctx: Context | None) -> Dict[str, Any]:
            """Export a Bill of Materials for a KiCad project.
            
            This tool attempts to generate a CSV BOM file for a KiCad project.
            It requires KiCad to be installed with the appropriate command-line tools.
            
            Args:
                project_path: Path to the KiCad project file (.kicad_pro)
                ctx: MCP context for progress reporting
                
            Returns:
                Dictionary with export results
            """
            print(f"Exporting BOM for project: {project_path}")
            
            if not os.path.exists(project_path):
                print(f"Project not found: {project_path}")
                if ctx:
                    ctx.info(f"Project not found: {project_path}")
                return {"success": False, "error": f"Project not found: {project_path}"}
            
            # Get access to the app context
            app_context = ctx.request_context.lifespan_context if ctx else None
            kicad_modules_available = app_context.kicad_modules_available if app_context else False
            
            # Report progress
            if ctx:
                await ctx.report_progress(10, 100)
            
            # Get all project files
            files = get_project_files(project_path)
            
            # We need the schematic file to generate a BOM
            if "schematic" not in files:
                print("Schematic file not found in project")
                if ctx:
                    ctx.info("Schematic file not found in project")
                return {"success": False, "error": "Schematic file not found"}
            
            schematic_file = files["schematic"]
            project_dir = os.path.dirname(project_path)
            project_name = os.path.basename(project_path)[:-10]  # Remove .kicad_pro extension
            
            if ctx:
                await ctx.report_progress(20, 100)
                ctx.info(f"Found schematic file: {os.path.basename(schematic_file)}")
            
            # Try to export BOM
            # This will depend on KiCad's command-line tools or Python modules
            export_result = {"success": False}
            
            if kicad_modules_available:
                try:
                    # Try to use KiCad Python modules
                    if ctx:
                        ctx.info("Attempting to export BOM using KiCad Python modules...")
                    export_result = await export_bom_with_python(schematic_file, project_dir, project_name, ctx)
                except Exception as e:
                    print(f"Error exporting BOM with Python modules: {str(e)}", exc_info=True)
                    if ctx:
                        ctx.info(f"Error using Python modules: {str(e)}")
                    export_result = {"success": False, "error": str(e)}
            
            # If Python method failed, try command-line method
            if not export_result.get("success", False):
                try:
                    if ctx:
                        ctx.info("Attempting to export BOM using command-line tools...")
                    export_result = await export_bom_with_cli(schematic_file, project_dir, project_name, ctx)
                except Exception as e:
                    print(f"Error exporting BOM with CLI: {str(e)}", exc_info=True)
                    if ctx:
                        ctx.info(f"Error using command-line tools: {str(e)}")
                    export_result = {"success": False, "error": str(e)}
            
            if ctx:
                await ctx.report_progress(100, 100)
            
            if export_result.get("success", False):
                if ctx:
                    ctx.info(f"BOM exported successfully to {export_result.get('output_file', 'unknown location')}")
            else:
                if ctx:
                    ctx.info(f"Failed to export BOM: {export_result.get('error', 'Unknown error')}")
            
            return export_result

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/lamaalrajih/kicad-mcp'

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