""" 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 }