diff --git a/python/commands/component_schematic.py b/python/commands/component_schematic.py index f9bf81f..69da4da 100644 --- a/python/commands/component_schematic.py +++ b/python/commands/component_schematic.py @@ -1,6 +1,9 @@ from skip import Schematic # Symbol class might not be directly importable in the current version import os +import logging + +logger = logging.getLogger(__name__) class ComponentManager: """Manage components in a schematic""" @@ -9,6 +12,9 @@ class ComponentManager: def add_component(schematic: Schematic, component_def: dict): """Add a component to the schematic""" try: + logger.info(f"Adding component: lib={component_def.get('library')}, name={component_def.get('type')}, ref={component_def.get('reference')}") + logger.debug(f"Full component_def: {component_def}") + # Create a new symbol symbol = schematic.add_symbol( lib=component_def.get('library', 'Device'), @@ -33,10 +39,10 @@ class ComponentManager: if key not in ['Reference', 'Value', 'Footprint', 'Datasheet']: symbol.property.append(key, value) - print(f"Added component {symbol.reference} ({symbol.name}) to schematic.") + logger.info(f"Successfully added component {symbol.reference} ({symbol.name}) to schematic.") return symbol except Exception as e: - print(f"Error adding component: {e}") + logger.error(f"Error adding component: {e}", exc_info=True) return None @staticmethod diff --git a/python/commands/library.py b/python/commands/library.py index 1af35fc..7a168af 100644 --- a/python/commands/library.py +++ b/python/commands/library.py @@ -128,6 +128,8 @@ class LibraryManager: 'KICAD8_FOOTPRINT_DIR': self._find_kicad_footprint_dir(), 'KICAD_FOOTPRINT_DIR': self._find_kicad_footprint_dir(), 'KISYSMOD': self._find_kicad_footprint_dir(), + 'KICAD9_3RD_PARTY': self._find_kicad_3rdparty_dir(), + 'KICAD8_3RD_PARTY': self._find_kicad_3rdparty_dir(), } # Project directory @@ -176,6 +178,67 @@ class LibraryManager: return None + def _find_kicad_3rdparty_dir(self) -> Optional[str]: + """ + Find KiCAD 3rd party libraries directory. + + Resolution order: + 1. Shell environment variable KICAD9_3RD_PARTY + 2. User settings in kicad_common.json + 3. Platform-specific defaults based on detected KiCad version + """ + import json + + # 1. Check shell environment variable first + if 'KICAD9_3RD_PARTY' in os.environ: + path = os.environ['KICAD9_3RD_PARTY'] + if os.path.isdir(path): + return path + + # 2. Check kicad_common.json for user-defined variables + kicad_common_paths = [ + Path.home() / "Library" / "Preferences" / "kicad" / "9.0" / "kicad_common.json", # macOS + Path.home() / ".config" / "kicad" / "9.0" / "kicad_common.json", # Linux + Path.home() / "AppData" / "Roaming" / "kicad" / "9.0" / "kicad_common.json", # Windows + ] + + for config_path in kicad_common_paths: + if config_path.exists(): + try: + with open(config_path, 'r') as f: + config = json.load(f) + env_vars = config.get('environment', {}).get('vars', {}) + if env_vars and 'KICAD9_3RD_PARTY' in env_vars: + path = env_vars['KICAD9_3RD_PARTY'] + if os.path.isdir(path): + return path + except (json.JSONDecodeError, KeyError, TypeError): + pass + + # Derive version from config path location + version = config_path.parent.name # e.g., "9.0" + break + else: + version = "9.0" # Default + + # 3. Use platform-specific defaults + possible_paths = [ + # macOS - Documents/KiCad/{version}/3rdparty + Path.home() / "Documents" / "KiCad" / version / "3rdparty", + # Linux - ~/.local/share/kicad/{version}/3rdparty + Path.home() / ".local" / "share" / "kicad" / version / "3rdparty", + # Windows - Documents/KiCad/{version}/3rdparty + Path.home() / "Documents" / "KiCad" / version / "3rdparty", + ] + + for path in possible_paths: + if path.exists(): + logger.info(f"Found KiCad 3rd party directory: {path}") + return str(path) + + logger.warning("Could not find KiCad 3rd party directory") + return None + def list_libraries(self) -> List[str]: """Get list of available library nicknames""" return list(self.libraries.keys()) diff --git a/python/commands/library_symbol.py b/python/commands/library_symbol.py new file mode 100644 index 0000000..5245619 --- /dev/null +++ b/python/commands/library_symbol.py @@ -0,0 +1,601 @@ +""" +Library management for KiCAD symbols + +Handles parsing sym-lib-table files, discovering symbols, +and providing search functionality for component selection. +""" + +import os +import re +import logging +from pathlib import Path +from typing import Dict, List, Optional +from dataclasses import dataclass, asdict + +logger = logging.getLogger('kicad_interface') + + +@dataclass +class SymbolInfo: + """Information about a symbol in a library""" + name: str # Symbol name (without library prefix) + library: str # Library nickname + full_ref: str # "Library:SymbolName" + value: str = "" # Value property + description: str = "" # Description property + footprint: str = "" # Footprint reference if present + lcsc_id: str = "" # LCSC property if present + manufacturer: str = "" # Manufacturer property + mpn: str = "" # Part/MPN property + category: str = "" # Category property + datasheet: str = "" # Datasheet URL + stock: str = "" # Stock (from JLCPCB libs) + price: str = "" # Price (from JLCPCB libs) + lib_class: str = "" # Basic/Preferred/Extended + + +class SymbolLibraryManager: + """ + Manages KiCAD symbol libraries + + Parses sym-lib-table files (both global and project-specific), + indexes available symbols, and provides search functionality. + """ + + def __init__(self, project_path: Optional[Path] = None): + """ + Initialize symbol library manager + + Args: + project_path: Optional path to project directory for project-specific libraries + """ + self.project_path = project_path + self.libraries: Dict[str, str] = {} # nickname -> path mapping + self.symbol_cache: Dict[str, List[SymbolInfo]] = {} # library -> [SymbolInfo] + self._load_libraries() + + def _load_libraries(self): + """Load libraries from sym-lib-table files""" + # Load global libraries + global_table = self._get_global_sym_lib_table() + if global_table and global_table.exists(): + logger.info(f"Loading global sym-lib-table from: {global_table}") + self._parse_sym_lib_table(global_table) + else: + logger.warning(f"Global sym-lib-table not found at: {global_table}") + + # Load project-specific libraries if project path provided + if self.project_path: + project_table = self.project_path / "sym-lib-table" + if project_table.exists(): + logger.info(f"Loading project sym-lib-table from: {project_table}") + self._parse_sym_lib_table(project_table) + + logger.info(f"Loaded {len(self.libraries)} symbol libraries") + + def _get_global_sym_lib_table(self) -> Optional[Path]: + """Get path to global sym-lib-table file""" + # Try different possible locations (same as fp-lib-table but for symbols) + kicad_config_paths = [ + Path.home() / ".config" / "kicad" / "9.0" / "sym-lib-table", + Path.home() / ".config" / "kicad" / "8.0" / "sym-lib-table", + Path.home() / ".config" / "kicad" / "sym-lib-table", + # Windows paths + Path.home() / "AppData" / "Roaming" / "kicad" / "9.0" / "sym-lib-table", + Path.home() / "AppData" / "Roaming" / "kicad" / "8.0" / "sym-lib-table", + # macOS paths + Path.home() / "Library" / "Preferences" / "kicad" / "9.0" / "sym-lib-table", + Path.home() / "Library" / "Preferences" / "kicad" / "8.0" / "sym-lib-table", + ] + + for path in kicad_config_paths: + if path.exists(): + return path + + return None + + def _parse_sym_lib_table(self, table_path: Path): + """ + Parse sym-lib-table file + + Format is S-expression (Lisp-like): + (sym_lib_table + (lib (name "Library_Name")(type KiCad)(uri "${KICAD9_SYMBOL_DIR}/Library.kicad_sym")(options "")(descr "Description")) + ) + """ + try: + with open(table_path, 'r', encoding='utf-8') as f: + content = f.read() + + # Simple regex-based parser for lib entries + # Pattern: (lib (name "NAME")(type TYPE)(uri "URI")...) + lib_pattern = r'\(lib\s+\(name\s+"?([^")\s]+)"?\)\s*\(type\s+[^)]+\)\s*\(uri\s+"?([^")\s]+)"?' + + for match in re.finditer(lib_pattern, content, re.IGNORECASE): + nickname = match.group(1) + uri = match.group(2) + + # Resolve environment variables in URI + resolved_uri = self._resolve_uri(uri) + + if resolved_uri: + self.libraries[nickname] = resolved_uri + logger.debug(f" Found library: {nickname} -> {resolved_uri}") + else: + logger.debug(f" Could not resolve URI for library {nickname}: {uri}") + + except Exception as e: + logger.error(f"Error parsing sym-lib-table at {table_path}: {e}") + + def _resolve_uri(self, uri: str) -> Optional[str]: + """ + Resolve environment variables and paths in library URI + + Handles: + - ${KICAD9_SYMBOL_DIR} -> /Applications/KiCad/KiCad.app/Contents/SharedSupport/symbols + - ${KICAD9_3RD_PARTY} -> ~/Documents/KiCad/9.0/3rdparty + - ${KIPRJMOD} -> project directory + - Relative paths + - Absolute paths + """ + resolved = uri + + # Common KiCAD environment variables + env_vars = { + 'KICAD9_SYMBOL_DIR': self._find_kicad_symbol_dir(), + 'KICAD8_SYMBOL_DIR': self._find_kicad_symbol_dir(), + 'KICAD_SYMBOL_DIR': self._find_kicad_symbol_dir(), + 'KICAD9_3RD_PARTY': self._find_3rd_party_dir(), + 'KICAD8_3RD_PARTY': self._find_3rd_party_dir(), + 'KISYSSYM': self._find_kicad_symbol_dir(), + } + + # Project directory + if self.project_path: + env_vars['KIPRJMOD'] = str(self.project_path) + + # Replace environment variables + for var, value in env_vars.items(): + if value: + resolved = resolved.replace(f'${{{var}}}', value) + resolved = resolved.replace(f'${var}', value) + + # Expand ~ to home directory + resolved = os.path.expanduser(resolved) + + # Convert to absolute path + path = Path(resolved) + + # Check if path exists + if path.exists(): + return str(path) + else: + logger.debug(f" Path does not exist: {path}") + return None + + def _find_kicad_symbol_dir(self) -> Optional[str]: + """Find KiCAD symbol directory""" + possible_paths = [ + "/usr/share/kicad/symbols", + "/usr/local/share/kicad/symbols", + "C:/Program Files/KiCad/9.0/share/kicad/symbols", + "C:/Program Files/KiCad/8.0/share/kicad/symbols", + "/Applications/KiCad/KiCad.app/Contents/SharedSupport/symbols", + ] + + # Check environment variable + if 'KICAD9_SYMBOL_DIR' in os.environ: + possible_paths.insert(0, os.environ['KICAD9_SYMBOL_DIR']) + if 'KICAD8_SYMBOL_DIR' in os.environ: + possible_paths.insert(0, os.environ['KICAD8_SYMBOL_DIR']) + + for path in possible_paths: + if os.path.isdir(path): + return path + + return None + + def _find_3rd_party_dir(self) -> Optional[str]: + """Find KiCAD 3rd party library directory (PCM installed libs)""" + possible_paths = [ + str(Path.home() / "Documents" / "KiCad" / "9.0" / "3rdparty"), + str(Path.home() / "Documents" / "KiCad" / "8.0" / "3rdparty"), + ] + + # Check environment variable + if 'KICAD9_3RD_PARTY' in os.environ: + possible_paths.insert(0, os.environ['KICAD9_3RD_PARTY']) + if 'KICAD8_3RD_PARTY' in os.environ: + possible_paths.insert(0, os.environ['KICAD8_3RD_PARTY']) + + for path in possible_paths: + if os.path.isdir(path): + return path + + return None + + def _parse_kicad_sym_file(self, library_path: str, library_name: str) -> List[SymbolInfo]: + """ + Parse a .kicad_sym file to extract symbol metadata + + Args: + library_path: Path to the .kicad_sym file + library_name: Nickname of the library + + Returns: + List of SymbolInfo objects + """ + symbols = [] + + try: + with open(library_path, 'r', encoding='utf-8') as f: + content = f.read() + + # Find all top-level symbol definitions + # Pattern: (symbol "SYMBOL_NAME" ... ) at the top level + # We need to find symbols that are direct children of kicad_symbol_lib + # and not sub-symbols (which have names like "PARENT_0_1") + + # Simple approach: find all (symbol "NAME" and filter out sub-symbols + symbol_pattern = r'\(symbol\s+"([^"]+)"' + + for match in re.finditer(symbol_pattern, content): + symbol_name = match.group(1) + + # Skip sub-symbols (they contain _0_, _1_, etc. suffixes) + if re.search(r'_\d+_\d+$', symbol_name): + continue + + # Find the start position of this symbol + start_pos = match.start() + + # Extract properties from this symbol block + # We need to find the matching closing paren - use a simple heuristic + # Look for the next 2000 characters for properties + block_end = min(start_pos + 5000, len(content)) + symbol_block = content[start_pos:block_end] + + # Extract properties + properties = self._extract_properties(symbol_block) + + symbol_info = SymbolInfo( + name=symbol_name, + library=library_name, + full_ref=f"{library_name}:{symbol_name}", + value=properties.get('Value', ''), + description=properties.get('Description', ''), + footprint=properties.get('Footprint', ''), + lcsc_id=properties.get('LCSC', ''), + manufacturer=properties.get('Manufacturer', ''), + mpn=properties.get('Part', properties.get('MPN', '')), + category=properties.get('Category', ''), + datasheet=properties.get('Datasheet', ''), + stock=properties.get('Stock', ''), + price=properties.get('Price', ''), + lib_class=properties.get('Class', ''), + ) + + symbols.append(symbol_info) + + logger.debug(f"Parsed {len(symbols)} symbols from {library_name}") + + except Exception as e: + logger.error(f"Error parsing symbol library {library_path}: {e}") + + return symbols + + def _extract_properties(self, symbol_block: str) -> Dict[str, str]: + """Extract properties from a symbol block""" + properties = {} + + # Pattern for properties: (property "KEY" "VALUE" ...) + prop_pattern = r'\(property\s+"([^"]+)"\s+"([^"]*)"' + + for match in re.finditer(prop_pattern, symbol_block): + key = match.group(1) + value = match.group(2) + properties[key] = value + + return properties + + def list_libraries(self) -> List[str]: + """Get list of available library nicknames""" + return list(self.libraries.keys()) + + def get_library_path(self, nickname: str) -> Optional[str]: + """Get filesystem path for a library nickname""" + return self.libraries.get(nickname) + + def list_symbols(self, library_nickname: str) -> List[SymbolInfo]: + """ + List all symbols in a library + + Args: + library_nickname: Library name (e.g., "Device") + + Returns: + List of SymbolInfo objects + """ + # Check cache first + if library_nickname in self.symbol_cache: + return self.symbol_cache[library_nickname] + + library_path = self.libraries.get(library_nickname) + if not library_path: + logger.warning(f"Library not found: {library_nickname}") + return [] + + # Parse the library file + symbols = self._parse_kicad_sym_file(library_path, library_nickname) + + # Cache the results + self.symbol_cache[library_nickname] = symbols + + return symbols + + def search_symbols(self, query: str, limit: int = 20, library_filter: Optional[str] = None) -> List[SymbolInfo]: + """ + Search for symbols matching a query + + Args: + query: Search query (matches name, LCSC ID, description, category, manufacturer) + limit: Maximum number of results to return + library_filter: Optional library name pattern to filter by + + Returns: + List of SymbolInfo objects sorted by relevance + """ + results = [] + query_lower = query.lower() + + # Determine which libraries to search + libraries_to_search = self.libraries.keys() + if library_filter: + filter_lower = library_filter.lower() + libraries_to_search = [lib for lib in libraries_to_search if filter_lower in lib.lower()] + + for library_nickname in libraries_to_search: + symbols = self.list_symbols(library_nickname) + + for symbol in symbols: + score = self._score_match(query_lower, symbol) + if score > 0: + results.append((score, symbol)) + + if len(results) >= limit * 3: # Get extra for sorting + break + + if len(results) >= limit * 3: + break + + # Sort by score (descending) and return top results + results.sort(key=lambda x: x[0], reverse=True) + return [symbol for _, symbol in results[:limit]] + + def _score_match(self, query: str, symbol: SymbolInfo) -> int: + """ + Score how well a symbol matches a query + + Returns: + Score (0 = no match, higher = better match) + """ + score = 0 + + # Exact LCSC ID match - highest priority + if symbol.lcsc_id and symbol.lcsc_id.lower() == query: + score += 1000 + + # Exact name match + if symbol.name.lower() == query: + score += 500 + + # Exact value match + if symbol.value.lower() == query: + score += 400 + + # Partial name match + if query in symbol.name.lower(): + score += 100 + + # Partial value match + if query in symbol.value.lower(): + score += 80 + + # Description match + if query in symbol.description.lower(): + score += 50 + + # MPN match + if symbol.mpn and query in symbol.mpn.lower(): + score += 70 + + # Manufacturer match + if symbol.manufacturer and query in symbol.manufacturer.lower(): + score += 30 + + # Category match + if symbol.category and query in symbol.category.lower(): + score += 20 + + return score + + def get_symbol_info(self, library_nickname: str, symbol_name: str) -> Optional[SymbolInfo]: + """ + Get information about a specific symbol + + Args: + library_nickname: Library name + symbol_name: Symbol name + + Returns: + SymbolInfo or None if not found + """ + symbols = self.list_symbols(library_nickname) + + for symbol in symbols: + if symbol.name == symbol_name: + return symbol + + return None + + def find_symbol(self, symbol_spec: str) -> Optional[SymbolInfo]: + """ + Find a symbol by specification + + Supports multiple formats: + - "Library:Symbol" (e.g., "Device:R") + - "Symbol" (searches all libraries) + + Args: + symbol_spec: Symbol specification + + Returns: + SymbolInfo or None if not found + """ + if ":" in symbol_spec: + # Format: Library:Symbol + library_nickname, symbol_name = symbol_spec.split(":", 1) + return self.get_symbol_info(library_nickname, symbol_name) + else: + # Search all libraries + for library_nickname in self.libraries.keys(): + result = self.get_symbol_info(library_nickname, symbol_spec) + if result: + return result + + return None + + +class SymbolLibraryCommands: + """Command handlers for symbol library operations""" + + def __init__(self, library_manager: Optional[SymbolLibraryManager] = None): + """Initialize with optional library manager""" + self.library_manager = library_manager or SymbolLibraryManager() + + def list_symbol_libraries(self, params: Dict) -> Dict: + """List all available symbol libraries""" + try: + libraries = self.library_manager.list_libraries() + return { + "success": True, + "libraries": libraries, + "count": len(libraries) + } + except Exception as e: + logger.error(f"Error listing symbol libraries: {e}") + return { + "success": False, + "message": "Failed to list symbol libraries", + "errorDetails": str(e) + } + + def search_symbols(self, params: Dict) -> Dict: + """Search for symbols by query""" + try: + query = params.get("query", "") + if not query: + return { + "success": False, + "message": "Missing query parameter" + } + + limit = params.get("limit", 20) + library_filter = params.get("library") + + results = self.library_manager.search_symbols(query, limit, library_filter) + + return { + "success": True, + "symbols": [asdict(s) for s in results], + "count": len(results), + "query": query + } + except Exception as e: + logger.error(f"Error searching symbols: {e}") + return { + "success": False, + "message": "Failed to search symbols", + "errorDetails": str(e) + } + + def list_library_symbols(self, params: Dict) -> Dict: + """List all symbols in a specific library""" + try: + library = params.get("library") + if not library: + return { + "success": False, + "message": "Missing library parameter" + } + + symbols = self.library_manager.list_symbols(library) + + return { + "success": True, + "library": library, + "symbols": [asdict(s) for s in symbols], + "count": len(symbols) + } + except Exception as e: + logger.error(f"Error listing library symbols: {e}") + return { + "success": False, + "message": "Failed to list library symbols", + "errorDetails": str(e) + } + + def get_symbol_info(self, params: Dict) -> Dict: + """Get information about a specific symbol""" + try: + symbol_spec = params.get("symbol") + if not symbol_spec: + return { + "success": False, + "message": "Missing symbol parameter" + } + + result = self.library_manager.find_symbol(symbol_spec) + + if result: + return { + "success": True, + "symbol_info": asdict(result) + } + else: + return { + "success": False, + "message": f"Symbol not found: {symbol_spec}" + } + + except Exception as e: + logger.error(f"Error getting symbol info: {e}") + return { + "success": False, + "message": "Failed to get symbol info", + "errorDetails": str(e) + } + + +if __name__ == '__main__': + # Test the symbol library manager + import json + + logging.basicConfig(level=logging.INFO) + + manager = SymbolLibraryManager() + + print(f"\nFound {len(manager.libraries)} symbol libraries:") + for name in list(manager.libraries.keys())[:10]: + print(f" - {name}") + if len(manager.libraries) > 10: + print(f" ... and {len(manager.libraries) - 10} more") + + # Test search + if manager.libraries: + print("\n\nSearching for 'ESP32':") + results = manager.search_symbols("ESP32", limit=5) + for symbol in results: + print(f" - {symbol.full_ref}: {symbol.description or symbol.value}") + if symbol.lcsc_id: + print(f" LCSC: {symbol.lcsc_id}") diff --git a/python/kicad_interface.py b/python/kicad_interface.py index 2e1b6ee..ddb5f42 100644 --- a/python/kicad_interface.py +++ b/python/kicad_interface.py @@ -212,6 +212,7 @@ try: from commands.connection_schematic import ConnectionManager from commands.library_schematic import LibraryManager as SchematicLibraryManager from commands.library import LibraryManager as FootprintLibraryManager, LibraryCommands + from commands.library_symbol import SymbolLibraryManager, SymbolLibraryCommands logger.info("Successfully imported all command handlers") except ImportError as e: logger.error(f"Failed to import command handlers: {e}") @@ -258,6 +259,9 @@ class KiCADInterface: self.export_commands = ExportCommands(self.board) self.library_commands = LibraryCommands(self.footprint_library) + # Initialize symbol library manager (for searching local KiCad symbol libraries) + self.symbol_library_commands = SymbolLibraryCommands() + # Schematic-related classes don't need board reference # as they operate directly on schematic files @@ -323,6 +327,12 @@ class KiCADInterface: "list_library_footprints": self.library_commands.list_library_footprints, "get_footprint_info": self.library_commands.get_footprint_info, + # Symbol library commands (local KiCad symbol library search) + "list_symbol_libraries": self.symbol_library_commands.list_symbol_libraries, + "search_symbols": self.symbol_library_commands.search_symbols, + "list_library_symbols": self.symbol_library_commands.list_library_symbols, + "get_symbol_info": self.symbol_library_commands.get_symbol_info, + # Schematic commands "create_schematic": self._handle_create_schematic, "load_schematic": self._handle_load_schematic, diff --git a/src/server.ts b/src/server.ts index 4326aeb..a1e8445 100644 --- a/src/server.ts +++ b/src/server.ts @@ -19,6 +19,7 @@ import { registerDesignRuleTools } from './tools/design-rules.js'; import { registerExportTools } from './tools/export.js'; import { registerSchematicTools } from './tools/schematic.js'; import { registerLibraryTools } from './tools/library.js'; +import { registerSymbolLibraryTools } from './tools/library-symbol.js'; import { registerUITools } from './tools/ui.js'; import { registerRouterTools } from './tools/router.js'; @@ -153,6 +154,7 @@ export class KiCADMcpServer { registerExportTools(this.server, this.callKicadScript.bind(this)); registerSchematicTools(this.server, this.callKicadScript.bind(this)); registerLibraryTools(this.server, this.callKicadScript.bind(this)); + registerSymbolLibraryTools(this.server, this.callKicadScript.bind(this)); registerUITools(this.server, this.callKicadScript.bind(this)); // Register all resources diff --git a/src/tools/library-symbol.ts b/src/tools/library-symbol.ts new file mode 100644 index 0000000..8222a63 --- /dev/null +++ b/src/tools/library-symbol.ts @@ -0,0 +1,178 @@ +/** + * Symbol Library tools for KiCAD MCP server + * Provides search/browse access to local KiCad symbol libraries + */ + +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { z } from 'zod'; + +export function registerSymbolLibraryTools(server: McpServer, callKicadScript: Function) { + // List available symbol libraries + server.tool( + "list_symbol_libraries", + "List all available KiCAD symbol libraries from global sym-lib-table", + {}, + async () => { + const result = await callKicadScript("list_symbol_libraries", {}); + if (result.success && result.libraries) { + return { + content: [ + { + type: "text", + text: `Found ${result.count} symbol libraries:\n${result.libraries.join('\n')}` + } + ] + }; + } + return { + content: [ + { + type: "text", + text: `Failed to list symbol libraries: ${result.message || 'Unknown error'}` + } + ] + }; + } + ); + + // Search for symbols across all libraries + server.tool( + "search_symbols", + `Search for symbols in local KiCAD symbol libraries. + +Searches by: symbol name, LCSC ID, description, manufacturer, MPN, category. +Use this to find components already in your local libraries (e.g., JLCPCB-KiCad-Library). + +Returns symbol references that can be used directly in schematics.`, + { + query: z.string() + .describe("Search query (e.g., 'ESP32', 'STM32F103', 'C8734' for LCSC ID)"), + library: z.string().optional() + .describe("Optional: filter to specific library name pattern (e.g., 'JLCPCB')"), + limit: z.number().optional().default(20) + .describe("Maximum number of results to return") + }, + async (args: { query: string; library?: string; limit?: number }) => { + const result = await callKicadScript("search_symbols", args); + if (result.success && result.symbols) { + if (result.symbols.length === 0) { + return { + content: [ + { + type: "text", + text: `No symbols found matching "${args.query}"${args.library ? ` in libraries matching "${args.library}"` : ''}` + } + ] + }; + } + + const symbolList = result.symbols.map((s: any) => { + const parts = [`${s.full_ref}`]; + if (s.lcsc_id) parts.push(`LCSC: ${s.lcsc_id}`); + if (s.description) parts.push(s.description); + else if (s.value) parts.push(s.value); + return parts.join(' | '); + }).join('\n'); + + return { + content: [ + { + type: "text", + text: `Found ${result.count} symbols matching "${args.query}":\n\n${symbolList}` + } + ] + }; + } + return { + content: [ + { + type: "text", + text: `Failed to search symbols: ${result.message || 'Unknown error'}` + } + ] + }; + } + ); + + // List symbols in a specific library + server.tool( + "list_library_symbols", + "List all symbols in a specific KiCAD symbol library", + { + library: z.string() + .describe("Library name (e.g., 'Device', 'PCM_JLCPCB-MCUs')") + }, + async (args: { library: string }) => { + const result = await callKicadScript("list_library_symbols", args); + if (result.success && result.symbols) { + const symbolList = result.symbols.map((s: any) => { + const parts = [` - ${s.name}`]; + if (s.lcsc_id) parts.push(`(LCSC: ${s.lcsc_id})`); + return parts.join(' '); + }).join('\n'); + + return { + content: [ + { + type: "text", + text: `Library "${args.library}" contains ${result.count} symbols:\n${symbolList}` + } + ] + }; + } + return { + content: [ + { + type: "text", + text: `Failed to list symbols in library ${args.library}: ${result.message || 'Unknown error'}` + } + ] + }; + } + ); + + // Get detailed information about a specific symbol + server.tool( + "get_symbol_info", + "Get detailed information about a specific symbol", + { + symbol: z.string() + .describe("Symbol specification (e.g., 'Device:R' or 'PCM_JLCPCB-MCUs:STM32F103C8T6')") + }, + async (args: { symbol: string }) => { + const result = await callKicadScript("get_symbol_info", args); + if (result.success && result.symbol_info) { + const info = result.symbol_info; + const details = [ + `Symbol: ${info.full_ref}`, + info.value ? `Value: ${info.value}` : '', + info.description ? `Description: ${info.description}` : '', + info.lcsc_id ? `LCSC: ${info.lcsc_id}` : '', + info.manufacturer ? `Manufacturer: ${info.manufacturer}` : '', + info.mpn ? `MPN: ${info.mpn}` : '', + info.footprint ? `Footprint: ${info.footprint}` : '', + info.category ? `Category: ${info.category}` : '', + info.lib_class ? `Class: ${info.lib_class}` : '', + info.datasheet ? `Datasheet: ${info.datasheet}` : '', + ].filter(line => line).join('\n'); + + return { + content: [ + { + type: "text", + text: details + } + ] + }; + } + return { + content: [ + { + type: "text", + text: `Failed to get symbol info: ${result.message || 'Unknown error'}` + } + ] + }; + } + ); +} diff --git a/src/tools/schematic.ts b/src/tools/schematic.ts index 5afce94..6e07758 100644 --- a/src/tools/schematic.ts +++ b/src/tools/schematic.ts @@ -28,9 +28,10 @@ export function registerSchematicTools(server: McpServer, callKicadScript: Funct // Add component to schematic server.tool( "add_schematic_component", - "Add a component to the schematic", + "Add a component to the schematic. Symbol format is 'Library:SymbolName' (e.g., 'Device:R', 'EDA-MCP:ESP32-C3')", { - symbol: z.string().describe("Symbol library reference"), + schematicPath: z.string().describe("Path to the schematic file"), + symbol: z.string().describe("Symbol library:name reference (e.g., Device:R, EDA-MCP:ESP32-C3)"), reference: z.string().describe("Component reference (e.g., R1, U1)"), value: z.string().optional().describe("Component value"), position: z.object({ @@ -38,14 +39,41 @@ export function registerSchematicTools(server: McpServer, callKicadScript: Funct y: z.number() }).optional().describe("Position on schematic"), }, - async (args: any) => { - const result = await callKicadScript("add_schematic_component", args); - return { - content: [{ - type: "text", - text: JSON.stringify(result, null, 2) - }] + async (args: { schematicPath: string; symbol: string; reference: string; value?: string; position?: { x: number; y: number } }) => { + // Transform to what Python backend expects + const [library, symbolName] = args.symbol.includes(':') + ? args.symbol.split(':') + : ['Device', args.symbol]; + + const transformed = { + schematicPath: args.schematicPath, + component: { + library, + type: symbolName, + reference: args.reference, + value: args.value, + // Python expects flat x, y not nested position + x: args.position?.x ?? 0, + y: args.position?.y ?? 0 + } }; + + const result = await callKicadScript("add_schematic_component", transformed); + if (result.success) { + return { + content: [{ + type: "text", + text: `Successfully added ${args.reference} (${args.symbol}) to schematic` + }] + }; + } else { + return { + content: [{ + type: "text", + text: `Failed to add component: ${result.message || JSON.stringify(result)}` + }] + }; + } } );