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>
546 lines
21 KiB
Python
546 lines
21 KiB
Python
"""
|
|
Symbol Library Management Tools for KiCad MCP Server.
|
|
|
|
Provides MCP tools for analyzing, validating, and managing KiCad symbol libraries
|
|
including library analysis, symbol validation, and organization recommendations.
|
|
"""
|
|
|
|
import os
|
|
from typing import Any
|
|
|
|
from fastmcp import FastMCP
|
|
|
|
from kicad_mcp.utils.symbol_library import create_symbol_analyzer
|
|
|
|
|
|
def register_symbol_tools(mcp: FastMCP) -> None:
|
|
"""Register symbol library management tools with the MCP server."""
|
|
|
|
@mcp.tool()
|
|
def analyze_symbol_library(library_path: str) -> dict[str, Any]:
|
|
"""
|
|
Analyze a KiCad symbol library file for coverage, statistics, and issues.
|
|
|
|
Performs comprehensive analysis of symbol library including symbol count,
|
|
categories, pin distributions, validation issues, and recommendations.
|
|
|
|
Args:
|
|
library_path: Full path to the .kicad_sym library file to analyze
|
|
|
|
Returns:
|
|
Dictionary with symbol counts, categories, pin statistics, and validation results
|
|
|
|
Examples:
|
|
analyze_symbol_library("/path/to/MyLibrary.kicad_sym")
|
|
analyze_symbol_library("~/kicad/symbols/Microcontrollers.kicad_sym")
|
|
"""
|
|
try:
|
|
# Validate library file path
|
|
if not os.path.exists(library_path):
|
|
return {
|
|
"success": False,
|
|
"error": f"Library file not found: {library_path}"
|
|
}
|
|
|
|
if not library_path.endswith('.kicad_sym'):
|
|
return {
|
|
"success": False,
|
|
"error": "File must be a KiCad symbol library (.kicad_sym)"
|
|
}
|
|
|
|
# Create analyzer and load library
|
|
analyzer = create_symbol_analyzer()
|
|
library = analyzer.load_library(library_path)
|
|
|
|
# Generate comprehensive report
|
|
report = analyzer.export_symbol_report(library)
|
|
|
|
return {
|
|
"success": True,
|
|
"library_path": library_path,
|
|
"report": report
|
|
}
|
|
|
|
except Exception as e:
|
|
return {
|
|
"success": False,
|
|
"error": str(e),
|
|
"library_path": library_path
|
|
}
|
|
|
|
@mcp.tool()
|
|
def validate_symbol_library(library_path: str) -> dict[str, Any]:
|
|
"""
|
|
Validate symbols in a KiCad library and report issues.
|
|
|
|
Checks for common symbol issues including missing properties,
|
|
invalid pin configurations, and design rule violations.
|
|
|
|
Args:
|
|
library_path: Path to the .kicad_sym library file
|
|
|
|
Returns:
|
|
Dictionary containing validation results and issue details
|
|
"""
|
|
try:
|
|
if not os.path.exists(library_path):
|
|
return {
|
|
"success": False,
|
|
"error": f"Library file not found: {library_path}"
|
|
}
|
|
|
|
analyzer = create_symbol_analyzer()
|
|
library = analyzer.load_library(library_path)
|
|
|
|
# Validate all symbols
|
|
validation_results = []
|
|
total_issues = 0
|
|
|
|
for symbol in library.symbols:
|
|
issues = analyzer.validate_symbol(symbol)
|
|
if issues:
|
|
validation_results.append({
|
|
"symbol_name": symbol.name,
|
|
"issues": issues,
|
|
"issue_count": len(issues),
|
|
"severity": "error" if any("Missing essential" in issue for issue in issues) else "warning"
|
|
})
|
|
total_issues += len(issues)
|
|
|
|
return {
|
|
"success": True,
|
|
"library_path": library_path,
|
|
"validation_summary": {
|
|
"total_symbols": len(library.symbols),
|
|
"symbols_with_issues": len(validation_results),
|
|
"total_issues": total_issues,
|
|
"pass_rate": ((len(library.symbols) - len(validation_results)) / len(library.symbols) * 100) if library.symbols else 100
|
|
},
|
|
"issues_by_symbol": validation_results,
|
|
"recommendations": [
|
|
"Fix symbols with missing essential properties first",
|
|
"Ensure all pins have valid electrical types",
|
|
"Check for duplicate pin numbers",
|
|
"Add meaningful pin names for better usability"
|
|
] if validation_results else ["All symbols pass validation checks"]
|
|
}
|
|
|
|
except Exception as e:
|
|
return {
|
|
"success": False,
|
|
"error": str(e),
|
|
"library_path": library_path
|
|
}
|
|
|
|
@mcp.tool()
|
|
def find_similar_symbols(library_path: str, symbol_name: str,
|
|
similarity_threshold: float = 0.7) -> dict[str, Any]:
|
|
"""
|
|
Find symbols similar to a specified symbol in the library.
|
|
|
|
Uses pin count, keywords, and name similarity to identify potentially
|
|
related or duplicate symbols in the library.
|
|
|
|
Args:
|
|
library_path: Path to the .kicad_sym library file
|
|
symbol_name: Name of the symbol to find similarities for
|
|
similarity_threshold: Minimum similarity score (0.0 to 1.0)
|
|
|
|
Returns:
|
|
Dictionary containing similar symbols with similarity scores
|
|
"""
|
|
try:
|
|
if not os.path.exists(library_path):
|
|
return {
|
|
"success": False,
|
|
"error": f"Library file not found: {library_path}"
|
|
}
|
|
|
|
analyzer = create_symbol_analyzer()
|
|
library = analyzer.load_library(library_path)
|
|
|
|
# Find target symbol
|
|
target_symbol = None
|
|
for symbol in library.symbols:
|
|
if symbol.name == symbol_name:
|
|
target_symbol = symbol
|
|
break
|
|
|
|
if not target_symbol:
|
|
return {
|
|
"success": False,
|
|
"error": f"Symbol '{symbol_name}' not found in library"
|
|
}
|
|
|
|
# Find similar symbols
|
|
similar_symbols = analyzer.find_similar_symbols(
|
|
target_symbol, library, similarity_threshold
|
|
)
|
|
|
|
similar_list = []
|
|
for symbol, score in similar_symbols:
|
|
similar_list.append({
|
|
"symbol_name": symbol.name,
|
|
"similarity_score": round(score, 3),
|
|
"pin_count": len(symbol.pins),
|
|
"keywords": symbol.keywords,
|
|
"description": symbol.description,
|
|
"differences": {
|
|
"pin_count_diff": abs(len(symbol.pins) - len(target_symbol.pins)),
|
|
"unique_keywords": list(set(symbol.keywords) - set(target_symbol.keywords)),
|
|
"missing_keywords": list(set(target_symbol.keywords) - set(symbol.keywords))
|
|
}
|
|
})
|
|
|
|
return {
|
|
"success": True,
|
|
"library_path": library_path,
|
|
"target_symbol": {
|
|
"name": target_symbol.name,
|
|
"pin_count": len(target_symbol.pins),
|
|
"keywords": target_symbol.keywords,
|
|
"description": target_symbol.description
|
|
},
|
|
"similar_symbols": similar_list,
|
|
"similarity_threshold": similarity_threshold,
|
|
"matches_found": len(similar_list)
|
|
}
|
|
|
|
except Exception as e:
|
|
return {
|
|
"success": False,
|
|
"error": str(e),
|
|
"library_path": library_path
|
|
}
|
|
|
|
@mcp.tool()
|
|
def get_symbol_details(library_path: str, symbol_name: str) -> dict[str, Any]:
|
|
"""
|
|
Get detailed information about a specific symbol in a library.
|
|
|
|
Provides comprehensive symbol information including pins, properties,
|
|
graphics, and metadata for detailed analysis.
|
|
|
|
Args:
|
|
library_path: Path to the .kicad_sym library file
|
|
symbol_name: Name of the symbol to analyze
|
|
|
|
Returns:
|
|
Dictionary containing detailed symbol information
|
|
"""
|
|
try:
|
|
if not os.path.exists(library_path):
|
|
return {
|
|
"success": False,
|
|
"error": f"Library file not found: {library_path}"
|
|
}
|
|
|
|
analyzer = create_symbol_analyzer()
|
|
library = analyzer.load_library(library_path)
|
|
|
|
# Find target symbol
|
|
target_symbol = None
|
|
for symbol in library.symbols:
|
|
if symbol.name == symbol_name:
|
|
target_symbol = symbol
|
|
break
|
|
|
|
if not target_symbol:
|
|
return {
|
|
"success": False,
|
|
"error": f"Symbol '{symbol_name}' not found in library"
|
|
}
|
|
|
|
# Extract detailed information
|
|
pin_details = []
|
|
for pin in target_symbol.pins:
|
|
pin_details.append({
|
|
"number": pin.number,
|
|
"name": pin.name,
|
|
"position": pin.position,
|
|
"orientation": pin.orientation,
|
|
"electrical_type": pin.electrical_type,
|
|
"graphic_style": pin.graphic_style,
|
|
"length_mm": pin.length
|
|
})
|
|
|
|
property_details = []
|
|
for prop in target_symbol.properties:
|
|
property_details.append({
|
|
"name": prop.name,
|
|
"value": prop.value,
|
|
"position": prop.position,
|
|
"rotation": prop.rotation,
|
|
"visible": prop.visible
|
|
})
|
|
|
|
# Validate symbol
|
|
validation_issues = analyzer.validate_symbol(target_symbol)
|
|
|
|
return {
|
|
"success": True,
|
|
"library_path": library_path,
|
|
"symbol_details": {
|
|
"name": target_symbol.name,
|
|
"library_id": target_symbol.library_id,
|
|
"description": target_symbol.description,
|
|
"keywords": target_symbol.keywords,
|
|
"power_symbol": target_symbol.power_symbol,
|
|
"extends": target_symbol.extends,
|
|
"pin_count": len(target_symbol.pins),
|
|
"pins": pin_details,
|
|
"properties": property_details,
|
|
"footprint_filters": target_symbol.footprint_filters,
|
|
"graphics_summary": {
|
|
"rectangles": len(target_symbol.graphics.rectangles),
|
|
"circles": len(target_symbol.graphics.circles),
|
|
"polylines": len(target_symbol.graphics.polylines)
|
|
}
|
|
},
|
|
"validation": {
|
|
"valid": len(validation_issues) == 0,
|
|
"issues": validation_issues
|
|
},
|
|
"statistics": {
|
|
"electrical_types": {etype: len([p for p in target_symbol.pins if p.electrical_type == etype])
|
|
for etype in set(p.electrical_type for p in target_symbol.pins)},
|
|
"pin_orientations": {orient: len([p for p in target_symbol.pins if p.orientation == orient])
|
|
for orient in set(p.orientation for p in target_symbol.pins)}
|
|
}
|
|
}
|
|
|
|
except Exception as e:
|
|
return {
|
|
"success": False,
|
|
"error": str(e),
|
|
"library_path": library_path
|
|
}
|
|
|
|
@mcp.tool()
|
|
def organize_library_by_category(library_path: str) -> dict[str, Any]:
|
|
"""
|
|
Organize symbols in a library by categories based on keywords and function.
|
|
|
|
Analyzes symbol keywords, names, and properties to suggest logical
|
|
groupings and organization improvements for the library.
|
|
|
|
Args:
|
|
library_path: Path to the .kicad_sym library file
|
|
|
|
Returns:
|
|
Dictionary containing suggested organization and category analysis
|
|
"""
|
|
try:
|
|
if not os.path.exists(library_path):
|
|
return {
|
|
"success": False,
|
|
"error": f"Library file not found: {library_path}"
|
|
}
|
|
|
|
analyzer = create_symbol_analyzer()
|
|
library = analyzer.load_library(library_path)
|
|
|
|
# Analyze library for categorization
|
|
analysis = analyzer.analyze_library_coverage(library)
|
|
|
|
# Create category-based organization
|
|
categories = {}
|
|
uncategorized = []
|
|
|
|
for symbol in library.symbols:
|
|
symbol_categories = []
|
|
|
|
# Categorize by keywords
|
|
if symbol.keywords:
|
|
symbol_categories.extend(symbol.keywords)
|
|
|
|
# Categorize by name patterns
|
|
name_lower = symbol.name.lower()
|
|
if any(term in name_lower for term in ['resistor', 'res', 'r_']):
|
|
symbol_categories.append('resistors')
|
|
elif any(term in name_lower for term in ['capacitor', 'cap', 'c_']):
|
|
symbol_categories.append('capacitors')
|
|
elif any(term in name_lower for term in ['inductor', 'ind', 'l_']):
|
|
symbol_categories.append('inductors')
|
|
elif any(term in name_lower for term in ['diode', 'led']):
|
|
symbol_categories.append('diodes')
|
|
elif any(term in name_lower for term in ['transistor', 'mosfet', 'bjt']):
|
|
symbol_categories.append('transistors')
|
|
elif any(term in name_lower for term in ['connector', 'conn']):
|
|
symbol_categories.append('connectors')
|
|
elif any(term in name_lower for term in ['ic', 'chip', 'processor']):
|
|
symbol_categories.append('integrated_circuits')
|
|
elif symbol.power_symbol:
|
|
symbol_categories.append('power')
|
|
|
|
# Categorize by pin count
|
|
pin_count = len(symbol.pins)
|
|
if pin_count <= 2:
|
|
symbol_categories.append('two_terminal')
|
|
elif pin_count <= 4:
|
|
symbol_categories.append('low_pin_count')
|
|
elif pin_count <= 20:
|
|
symbol_categories.append('medium_pin_count')
|
|
else:
|
|
symbol_categories.append('high_pin_count')
|
|
|
|
if symbol_categories:
|
|
for category in symbol_categories:
|
|
if category not in categories:
|
|
categories[category] = []
|
|
categories[category].append({
|
|
"name": symbol.name,
|
|
"description": symbol.description,
|
|
"pin_count": pin_count
|
|
})
|
|
else:
|
|
uncategorized.append(symbol.name)
|
|
|
|
# Generate organization recommendations
|
|
recommendations = []
|
|
|
|
if uncategorized:
|
|
recommendations.append(f"Add keywords to {len(uncategorized)} uncategorized symbols")
|
|
|
|
large_categories = {k: v for k, v in categories.items() if len(v) > 50}
|
|
if large_categories:
|
|
recommendations.append(f"Consider splitting large categories: {list(large_categories.keys())}")
|
|
|
|
if len(categories) < 5:
|
|
recommendations.append("Library could benefit from more detailed categorization")
|
|
|
|
return {
|
|
"success": True,
|
|
"library_path": library_path,
|
|
"organization": {
|
|
"categories": {k: len(v) for k, v in categories.items()},
|
|
"detailed_categories": categories,
|
|
"uncategorized_symbols": uncategorized,
|
|
"total_categories": len(categories),
|
|
"largest_category": max(categories.items(), key=lambda x: len(x[1]))[0] if categories else None
|
|
},
|
|
"statistics": {
|
|
"categorization_rate": ((len(library.symbols) - len(uncategorized)) / len(library.symbols) * 100) if library.symbols else 100,
|
|
"average_symbols_per_category": sum(len(v) for v in categories.values()) / len(categories) if categories else 0
|
|
},
|
|
"recommendations": recommendations
|
|
}
|
|
|
|
except Exception as e:
|
|
return {
|
|
"success": False,
|
|
"error": str(e),
|
|
"library_path": library_path
|
|
}
|
|
|
|
@mcp.tool()
|
|
def compare_symbol_libraries(library1_path: str, library2_path: str) -> dict[str, Any]:
|
|
"""
|
|
Compare two KiCad symbol libraries and identify differences.
|
|
|
|
Analyzes differences in symbol content, organization, and coverage
|
|
between two libraries for migration or consolidation planning.
|
|
|
|
Args:
|
|
library1_path: Path to the first .kicad_sym library file
|
|
library2_path: Path to the second .kicad_sym library file
|
|
|
|
Returns:
|
|
Dictionary containing detailed comparison results
|
|
"""
|
|
try:
|
|
# Validate both library files
|
|
for path in [library1_path, library2_path]:
|
|
if not os.path.exists(path):
|
|
return {
|
|
"success": False,
|
|
"error": f"Library file not found: {path}"
|
|
}
|
|
|
|
analyzer = create_symbol_analyzer()
|
|
|
|
# Load both libraries
|
|
library1 = analyzer.load_library(library1_path)
|
|
library2 = analyzer.load_library(library2_path)
|
|
|
|
# Get symbol lists
|
|
symbols1 = {s.name: s for s in library1.symbols}
|
|
symbols2 = {s.name: s for s in library2.symbols}
|
|
|
|
# Find differences
|
|
common_symbols = set(symbols1.keys()).intersection(set(symbols2.keys()))
|
|
unique_to_lib1 = set(symbols1.keys()) - set(symbols2.keys())
|
|
unique_to_lib2 = set(symbols2.keys()) - set(symbols1.keys())
|
|
|
|
# Analyze common symbols for differences
|
|
symbol_differences = []
|
|
for symbol_name in common_symbols:
|
|
sym1 = symbols1[symbol_name]
|
|
sym2 = symbols2[symbol_name]
|
|
|
|
differences = []
|
|
|
|
if len(sym1.pins) != len(sym2.pins):
|
|
differences.append(f"Pin count: {len(sym1.pins)} vs {len(sym2.pins)}")
|
|
|
|
if sym1.description != sym2.description:
|
|
differences.append("Description differs")
|
|
|
|
if set(sym1.keywords) != set(sym2.keywords):
|
|
differences.append("Keywords differ")
|
|
|
|
if differences:
|
|
symbol_differences.append({
|
|
"symbol": symbol_name,
|
|
"differences": differences
|
|
})
|
|
|
|
# Analyze library statistics
|
|
analysis1 = analyzer.analyze_library_coverage(library1)
|
|
analysis2 = analyzer.analyze_library_coverage(library2)
|
|
|
|
return {
|
|
"success": True,
|
|
"comparison": {
|
|
"library1": {
|
|
"name": library1.name,
|
|
"path": library1_path,
|
|
"symbol_count": len(library1.symbols),
|
|
"unique_symbols": len(unique_to_lib1)
|
|
},
|
|
"library2": {
|
|
"name": library2.name,
|
|
"path": library2_path,
|
|
"symbol_count": len(library2.symbols),
|
|
"unique_symbols": len(unique_to_lib2)
|
|
},
|
|
"common_symbols": len(common_symbols),
|
|
"symbol_differences": len(symbol_differences),
|
|
"coverage_comparison": {
|
|
"categories_lib1": len(analysis1["categories"]),
|
|
"categories_lib2": len(analysis2["categories"]),
|
|
"common_categories": len(set(analysis1["categories"].keys()).intersection(set(analysis2["categories"].keys())))
|
|
}
|
|
},
|
|
"detailed_differences": {
|
|
"unique_to_library1": list(unique_to_lib1),
|
|
"unique_to_library2": list(unique_to_lib2),
|
|
"symbol_differences": symbol_differences
|
|
},
|
|
"recommendations": [
|
|
f"Consider merging libraries - {len(common_symbols)} symbols are common",
|
|
f"Review {len(symbol_differences)} symbols that differ between libraries",
|
|
"Standardize symbol naming and categorization across libraries"
|
|
] if common_symbols else [
|
|
"Libraries have no common symbols - they appear to serve different purposes"
|
|
]
|
|
}
|
|
|
|
except Exception as e:
|
|
return {
|
|
"success": False,
|
|
"error": str(e),
|
|
"library1_path": library1_path,
|
|
"library2_path": library2_path
|
|
}
|