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
|
from skip import Schematic
|
||||||
# Symbol class might not be directly importable in the current version
|
# Symbol class might not be directly importable in the current version
|
||||||
import os
|
import os
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
class ComponentManager:
|
class ComponentManager:
|
||||||
"""Manage components in a schematic"""
|
"""Manage components in a schematic"""
|
||||||
@@ -9,6 +12,9 @@ class ComponentManager:
|
|||||||
def add_component(schematic: Schematic, component_def: dict):
|
def add_component(schematic: Schematic, component_def: dict):
|
||||||
"""Add a component to the schematic"""
|
"""Add a component to the schematic"""
|
||||||
try:
|
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
|
# Create a new symbol
|
||||||
symbol = schematic.add_symbol(
|
symbol = schematic.add_symbol(
|
||||||
lib=component_def.get('library', 'Device'),
|
lib=component_def.get('library', 'Device'),
|
||||||
@@ -33,10 +39,10 @@ class ComponentManager:
|
|||||||
if key not in ['Reference', 'Value', 'Footprint', 'Datasheet']:
|
if key not in ['Reference', 'Value', 'Footprint', 'Datasheet']:
|
||||||
symbol.property.append(key, value)
|
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
|
return symbol
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error adding component: {e}")
|
logger.error(f"Error adding component: {e}", exc_info=True)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
@@ -128,6 +128,8 @@ class LibraryManager:
|
|||||||
'KICAD8_FOOTPRINT_DIR': self._find_kicad_footprint_dir(),
|
'KICAD8_FOOTPRINT_DIR': self._find_kicad_footprint_dir(),
|
||||||
'KICAD_FOOTPRINT_DIR': self._find_kicad_footprint_dir(),
|
'KICAD_FOOTPRINT_DIR': self._find_kicad_footprint_dir(),
|
||||||
'KISYSMOD': 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
|
# Project directory
|
||||||
@@ -176,6 +178,67 @@ class LibraryManager:
|
|||||||
|
|
||||||
return None
|
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]:
|
def list_libraries(self) -> List[str]:
|
||||||
"""Get list of available library nicknames"""
|
"""Get list of available library nicknames"""
|
||||||
return list(self.libraries.keys())
|
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.connection_schematic import ConnectionManager
|
||||||
from commands.library_schematic import LibraryManager as SchematicLibraryManager
|
from commands.library_schematic import LibraryManager as SchematicLibraryManager
|
||||||
from commands.library import LibraryManager as FootprintLibraryManager, LibraryCommands
|
from commands.library import LibraryManager as FootprintLibraryManager, LibraryCommands
|
||||||
|
from commands.library_symbol import SymbolLibraryManager, SymbolLibraryCommands
|
||||||
logger.info("Successfully imported all command handlers")
|
logger.info("Successfully imported all command handlers")
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
logger.error(f"Failed to import command handlers: {e}")
|
logger.error(f"Failed to import command handlers: {e}")
|
||||||
@@ -258,6 +259,9 @@ class KiCADInterface:
|
|||||||
self.export_commands = ExportCommands(self.board)
|
self.export_commands = ExportCommands(self.board)
|
||||||
self.library_commands = LibraryCommands(self.footprint_library)
|
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
|
# Schematic-related classes don't need board reference
|
||||||
# as they operate directly on schematic files
|
# as they operate directly on schematic files
|
||||||
|
|
||||||
@@ -323,6 +327,12 @@ class KiCADInterface:
|
|||||||
"list_library_footprints": self.library_commands.list_library_footprints,
|
"list_library_footprints": self.library_commands.list_library_footprints,
|
||||||
"get_footprint_info": self.library_commands.get_footprint_info,
|
"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
|
# Schematic commands
|
||||||
"create_schematic": self._handle_create_schematic,
|
"create_schematic": self._handle_create_schematic,
|
||||||
"load_schematic": self._handle_load_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 { registerExportTools } from './tools/export.js';
|
||||||
import { registerSchematicTools } from './tools/schematic.js';
|
import { registerSchematicTools } from './tools/schematic.js';
|
||||||
import { registerLibraryTools } from './tools/library.js';
|
import { registerLibraryTools } from './tools/library.js';
|
||||||
|
import { registerSymbolLibraryTools } from './tools/library-symbol.js';
|
||||||
import { registerUITools } from './tools/ui.js';
|
import { registerUITools } from './tools/ui.js';
|
||||||
import { registerRouterTools } from './tools/router.js';
|
import { registerRouterTools } from './tools/router.js';
|
||||||
|
|
||||||
@@ -153,6 +154,7 @@ export class KiCADMcpServer {
|
|||||||
registerExportTools(this.server, this.callKicadScript.bind(this));
|
registerExportTools(this.server, this.callKicadScript.bind(this));
|
||||||
registerSchematicTools(this.server, this.callKicadScript.bind(this));
|
registerSchematicTools(this.server, this.callKicadScript.bind(this));
|
||||||
registerLibraryTools(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));
|
registerUITools(this.server, this.callKicadScript.bind(this));
|
||||||
|
|
||||||
// Register all resources
|
// 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
|
// Add component to schematic
|
||||||
server.tool(
|
server.tool(
|
||||||
"add_schematic_component",
|
"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)"),
|
reference: z.string().describe("Component reference (e.g., R1, U1)"),
|
||||||
value: z.string().optional().describe("Component value"),
|
value: z.string().optional().describe("Component value"),
|
||||||
position: z.object({
|
position: z.object({
|
||||||
@@ -38,14 +39,41 @@ export function registerSchematicTools(server: McpServer, callKicadScript: Funct
|
|||||||
y: z.number()
|
y: z.number()
|
||||||
}).optional().describe("Position on schematic"),
|
}).optional().describe("Position on schematic"),
|
||||||
},
|
},
|
||||||
async (args: any) => {
|
async (args: { schematicPath: string; symbol: string; reference: string; value?: string; position?: { x: number; y: number } }) => {
|
||||||
const result = await callKicadScript("add_schematic_component", args);
|
// 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 {
|
return {
|
||||||
content: [{
|
content: [{
|
||||||
type: "text",
|
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