kicad-mcp/kicad_mcp/resources/bom_resources.py
Ryan Malloy bc0f3db97c
Some checks are pending
CI / Lint and Format (push) Waiting to run
CI / Test Python 3.11 on macos-latest (push) Waiting to run
CI / Test Python 3.12 on macos-latest (push) Waiting to run
CI / Test Python 3.13 on macos-latest (push) Waiting to run
CI / Test Python 3.10 on ubuntu-latest (push) Waiting to run
CI / Test Python 3.11 on ubuntu-latest (push) Waiting to run
CI / Test Python 3.12 on ubuntu-latest (push) Waiting to run
CI / Test Python 3.13 on ubuntu-latest (push) Waiting to run
CI / Security Scan (push) Waiting to run
CI / Build Package (push) Blocked by required conditions
Implement comprehensive AI/LLM integration for KiCad MCP server
Add intelligent analysis and recommendation tools for KiCad designs:

## New AI Tools (kicad_mcp/tools/ai_tools.py)
- suggest_components_for_circuit: Smart component suggestions based on circuit analysis
- recommend_design_rules: Automated design rule recommendations for different technologies
- optimize_pcb_layout: PCB layout optimization for signal integrity, thermal, and cost
- analyze_design_completeness: Comprehensive design completeness analysis

## Enhanced Utilities
- component_utils.py: Add ComponentType enum and component classification functions
- pattern_recognition.py: Enhanced circuit pattern analysis and recommendations
- netlist_parser.py: Implement missing parse_netlist_file function for AI tools

## Key Features
- Circuit pattern recognition for power supplies, amplifiers, microcontrollers
- Technology-specific design rules (standard, HDI, RF, automotive)
- Layout optimization suggestions with implementation steps
- Component suggestion system with standard values and examples
- Design completeness scoring with actionable recommendations

## Server Integration
- Register AI tools in FastMCP server
- Integrate with existing KiCad utilities and file parsers
- Error handling and graceful fallbacks for missing data

Fixes ImportError that prevented server startup and enables advanced
AI-powered design assistance for KiCad projects.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-11 16:15:58 -06:00

290 lines
11 KiB
Python

"""
Bill of Materials (BOM) resources for KiCad projects.
"""
import json
import os
from mcp.server.fastmcp import FastMCP
import pandas as pd
# Import the helper functions from bom_tools.py to avoid code duplication
from kicad_mcp.tools.bom_tools import analyze_bom_data, parse_bom_file
from kicad_mcp.utils.file_utils import get_project_files
def register_bom_resources(mcp: FastMCP) -> None:
"""Register BOM-related resources with the MCP server.
Args:
mcp: The FastMCP server instance
"""
@mcp.resource("kicad://bom/{project_path}")
def get_bom_resource(project_path: str) -> str:
"""Get a formatted BOM report for a KiCad project.
Args:
project_path: Path to the KiCad project file (.kicad_pro)
Returns:
Markdown-formatted BOM report
"""
print(f"Generating BOM report for project: {project_path}")
if not os.path.exists(project_path):
return f"Project not found: {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")
return f"# BOM Report\n\nNo BOM files found for project: {os.path.basename(project_path)}.\n\nExport a BOM from KiCad first, or use the `export_bom_csv` tool to generate one."
# Format as Markdown report
project_name = os.path.basename(project_path)[:-10] # Remove .kicad_pro
report = f"# Bill of Materials for {project_name}\n\n"
# Process each BOM file
for file_type, file_path in bom_files.items():
try:
# Parse and analyze the BOM
bom_data, format_info = parse_bom_file(file_path)
if not bom_data:
report += f"## {file_type}\n\nFailed to parse BOM file: {os.path.basename(file_path)}\n\n"
continue
analysis = analyze_bom_data(bom_data, format_info)
# Add file section
report += f"## {file_type.capitalize()}\n\n"
report += f"**File**: {os.path.basename(file_path)}\n\n"
report += f"**Format**: {format_info.get('detected_format', 'Unknown')}\n\n"
# Add summary
report += "### Summary\n\n"
report += f"- **Total Components**: {analysis.get('total_component_count', 0)}\n"
report += f"- **Unique Components**: {analysis.get('unique_component_count', 0)}\n"
# Add cost if available
if analysis.get("has_cost_data", False) and "total_cost" in analysis:
currency = analysis.get("currency", "USD")
currency_symbols = {"USD": "$", "EUR": "", "GBP": "£"}
symbol = currency_symbols.get(currency, "")
report += f"- **Estimated Cost**: {symbol}{analysis['total_cost']} {currency}\n"
report += "\n"
# Add categories breakdown
if "categories" in analysis and analysis["categories"]:
report += "### Component Categories\n\n"
for category, count in analysis["categories"].items():
report += f"- **{category}**: {count}\n"
report += "\n"
# Add most common components if available
if "most_common_values" in analysis and analysis["most_common_values"]:
report += "### Most Common Components\n\n"
for value, count in analysis["most_common_values"].items():
report += f"- **{value}**: {count}\n"
report += "\n"
# Add component table (first 20 items)
if bom_data:
report += "### Component List\n\n"
# Try to identify key columns
columns = []
if format_info.get("header_fields"):
# Use a subset of columns for readability
preferred_cols = [
"Reference",
"Value",
"Footprint",
"Quantity",
"Description",
]
# Find matching columns (case-insensitive)
header_lower = [h.lower() for h in format_info["header_fields"]]
for col in preferred_cols:
col_lower = col.lower()
if col_lower in header_lower:
idx = header_lower.index(col_lower)
columns.append(format_info["header_fields"][idx])
# If we didn't find any preferred columns, use the first 4
if not columns and len(format_info["header_fields"]) > 0:
columns = format_info["header_fields"][
: min(4, len(format_info["header_fields"]))
]
# Generate the table header
if columns:
report += "| " + " | ".join(columns) + " |\n"
report += "| " + " | ".join(["---"] * len(columns)) + " |\n"
# Add rows (limit to first 20 for readability)
for i, component in enumerate(bom_data[:20]):
row = []
for col in columns:
value = component.get(col, "")
# Clean up cell content for Markdown table
value = str(value).replace("|", "\\|").replace("\n", " ")
row.append(value)
report += "| " + " | ".join(row) + " |\n"
# Add note if there are more components
if len(bom_data) > 20:
report += f"\n*...and {len(bom_data) - 20} more components*\n"
else:
report += "*Component table could not be generated - column headers not recognized*\n"
report += "\n---\n\n"
except Exception as e:
print(f"Error processing BOM file {file_path}: {str(e)}")
report += f"## {file_type}\n\nError processing BOM file: {str(e)}\n\n"
# Add export instructions
report += "## How to Export a BOM\n\n"
report += "To generate a new BOM from your KiCad project:\n\n"
report += "1. Open your schematic in KiCad\n"
report += "2. Go to **Tools → Generate BOM**\n"
report += "3. Choose a BOM plugin and click **Generate**\n"
report += "4. Save the BOM file in your project directory\n\n"
report += "Alternatively, use the `export_bom_csv` tool in this MCP server to generate a BOM file.\n"
return report
@mcp.resource("kicad://bom/{project_path}/csv")
def get_bom_csv_resource(project_path: str) -> str:
"""Get a CSV representation of the BOM for a KiCad project.
Args:
project_path: Path to the KiCad project file (.kicad_pro)
Returns:
CSV-formatted BOM data
"""
print(f"Generating CSV BOM for project: {project_path}")
if not os.path.exists(project_path):
return f"Project not found: {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")
return "No BOM files found for project. Export a BOM from KiCad first."
# Use the first BOM file found
file_type = next(iter(bom_files))
file_path = bom_files[file_type]
try:
# If it's already a CSV, just return its contents
if file_path.lower().endswith(".csv"):
with open(file_path, encoding="utf-8-sig") as f:
return f.read()
# Otherwise, try to parse and convert to CSV
bom_data, format_info = parse_bom_file(file_path)
if not bom_data:
return f"Failed to parse BOM file: {file_path}"
# Convert to DataFrame and then to CSV
df = pd.DataFrame(bom_data)
return df.to_csv(index=False)
except Exception as e:
print(f"Error generating CSV from BOM file: {str(e)}")
return f"Error generating CSV from BOM file: {str(e)}"
@mcp.resource("kicad://bom/{project_path}/json")
def get_bom_json_resource(project_path: str) -> str:
"""Get a JSON representation of the BOM for a KiCad project.
Args:
project_path: Path to the KiCad project file (.kicad_pro)
Returns:
JSON-formatted BOM data
"""
print(f"Generating JSON BOM for project: {project_path}")
if not os.path.exists(project_path):
return f"Project not found: {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", ".json")):
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")
return json.dumps({"error": "No BOM files found for project"}, indent=2)
try:
# Collect data from all BOM files
result = {"project": os.path.basename(project_path)[:-10], "bom_files": {}}
for file_type, file_path in bom_files.items():
# If it's already JSON, parse it directly
if file_path.lower().endswith(".json"):
with open(file_path) as f:
try:
result["bom_files"][file_type] = json.load(f)
continue
except:
# If JSON parsing fails, fall back to regular parsing
pass
# Otherwise parse with our utility
bom_data, format_info = parse_bom_file(file_path)
if bom_data:
analysis = analyze_bom_data(bom_data, format_info)
result["bom_files"][file_type] = {
"file": os.path.basename(file_path),
"format": format_info,
"analysis": analysis,
"components": bom_data,
}
return json.dumps(result, indent=2, default=str)
except Exception as e:
print(f"Error generating JSON from BOM file: {str(e)}")
return json.dumps({"error": str(e)}, indent=2)