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
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>
290 lines
11 KiB
Python
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)
|