feat: Add local symbol library search and 3rd party library support (#25)
Adds comprehensive local KiCad symbol library search functionality and fixes KICAD9_3RD_PARTY environment variable resolution. Features: - Symbol library search by name, LCSC ID, description, manufacturer, MPN - Support for 3rd party libraries installed via Plugin and Content Manager - New MCP tools: search_symbols, list_symbol_libraries, get_symbol_info - Enhanced library path resolution for KiCad 8 and 9 This enables users with locally installed JLCPCB libraries to search and use components directly. Co-authored-by: l3wi <l3wi@users.noreply.github.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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())
|
||||
|
||||
601
python/commands/library_symbol.py
Normal file
601
python/commands/library_symbol.py
Normal file
@@ -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}")
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
178
src/tools/library-symbol.ts
Normal file
178
src/tools/library-symbol.ts
Normal file
@@ -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'}`
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
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: JSON.stringify(result, null, 2)
|
||||
text: `Successfully added ${args.reference} (${args.symbol}) to schematic`
|
||||
}]
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `Failed to add component: ${result.message || JSON.stringify(result)}`
|
||||
}]
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user