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:
Lewis Freiberg
2025-12-31 16:57:10 +01:00
committed by GitHub
parent 8a1cb46b39
commit 0227dd48d2
7 changed files with 899 additions and 11 deletions
+8 -2
View File
@@ -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
+63
View File
@@ -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
View 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}")
+10
View File
@@ -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,
+2
View File
@@ -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
View 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'}`
}
]
};
}
);
}
+37 -9
View File
@@ -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)}`
}]
};
}
}
);