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

Output Schema

TableJSON Schema
NameRequiredDescriptionDefault

No arguments

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
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries the full burden of behavioral disclosure. It mentions the tool 'attempts' to generate a CSV, hinting at potential failure modes, and notes KiCad installation requirements, which adds some context. However, it lacks details on error handling, file output location, permissions needed, or what the 'export results' dictionary contains, leaving significant gaps for a mutation tool (export implies file creation).

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is well-structured with a clear opening sentence, followed by additional context and a parameter/return section. It's appropriately sized without unnecessary fluff, though the parameter explanations could be more detailed. Every sentence contributes to understanding the tool's functionality.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool has an output schema (returns a dictionary), the description doesn't need to explain return values in detail. However, with no annotations and a mutation operation (exporting files), the description should provide more behavioral context, such as error conditions or output file handling. It covers basics but leaves gaps for a tool that interacts with external systems (KiCad).

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The description explicitly lists and briefly explains both parameters: 'project_path' as the path to the KiCad project file and 'ctx' for MCP context in progress reporting. With 0% schema description coverage, this adds substantial value beyond the schema, which only provides titles without descriptions. However, it doesn't detail the format or constraints for 'project_path' (e.g., file extensions, absolute vs. relative paths).

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool exports a Bill of Materials (BOM) for a KiCad project as a CSV file, which is a specific verb+resource combination. However, it doesn't explicitly differentiate from sibling tools like 'analyze_bom' or 'extract_project_netlist', which might have overlapping functionality with BOM-related operations.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides some context about when to use this tool by mentioning it requires KiCad installation with command-line tools, which implies a prerequisite. However, it doesn't offer explicit guidance on when to choose this tool versus alternatives like 'analyze_bom' or 'extract_project_netlist', nor does it specify exclusions or complementary tools.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

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