Make project-scope sym-lib-table visible to symbol-discovery tools
search_symbols, list_symbol_libraries, list_library_symbols, and get_symbol_info previously only consulted the global sym-lib-table. A library registered with project scope (an entry in <project>/sym-lib-table) was therefore invisible — even right after open_project succeeded — making add_schematic_component the only tool that could see it. Fix has two parts: 1. Wrap project_commands.open_project and project_commands.create_project in handlers that rebuild SymbolLibraryCommands.library_manager against the project directory. After open_project, project-scope libraries are automatically visible to subsequent search/list/info calls. 2. Add an optional projectPath parameter to the four discovery tools (accepts a project directory, .kicad_pro, .kicad_pcb, or .kicad_sch path). Stateless callers can resolve project libraries without first calling open_project. SymbolLibraryCommands._derive_project_path also walks up from schematicPath/boardPath to find the directory that owns the project, mirroring the logic in _handle_add_schematic_component. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
15
CHANGELOG.md
15
CHANGELOG.md
@@ -6,6 +6,21 @@ All notable changes to the KiCAD MCP Server project are documented here.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **Project-scope `sym-lib-table` is now visible to symbol-discovery tools**:
|
||||
`search_symbols`, `list_symbol_libraries`, `list_library_symbols`, and
|
||||
`get_symbol_info` previously only consulted the global `sym-lib-table`. A
|
||||
library registered with project scope (i.e. an entry in
|
||||
`<project>/sym-lib-table`) was therefore invisible — even right after
|
||||
`open_project` succeeded — making `add_schematic_component` the only tool
|
||||
that could see it. Two changes:
|
||||
1. `open_project` and `create_project` now rebuild the
|
||||
`SymbolLibraryManager` against the project directory so subsequent
|
||||
search/list/info calls see project-scope libraries automatically.
|
||||
2. The four discovery tools also accept an optional `projectPath`
|
||||
parameter (a project directory, `.kicad_pro`, `.kicad_pcb`, or
|
||||
`.kicad_sch` path) for stateless callers, so project libraries can be
|
||||
resolved without first calling `open_project`.
|
||||
|
||||
- **Schematic symbol lookup**: `get_schematic_component`,
|
||||
`edit_schematic_component`, `set_schematic_component_property`,
|
||||
`remove_schematic_component_property`, and `delete_schematic_component`
|
||||
|
||||
@@ -515,9 +515,57 @@ class SymbolLibraryCommands:
|
||||
"""Initialize with optional library manager"""
|
||||
self.library_manager = library_manager or SymbolLibraryManager()
|
||||
|
||||
@staticmethod
|
||||
def _derive_project_path(params: Dict) -> Optional[Path]:
|
||||
"""Derive a project directory from caller-supplied params.
|
||||
|
||||
Accepts an explicit project directory or .kicad_pro file via projectPath,
|
||||
or any related file path (schematicPath/boardPath) — in which case the
|
||||
nearest ancestor containing sym-lib-table or a .kicad_pro is used.
|
||||
"""
|
||||
for key in ("projectPath", "project_path"):
|
||||
value = params.get(key)
|
||||
if value:
|
||||
p = Path(value).expanduser()
|
||||
if p.suffix == ".kicad_pro" or p.is_file():
|
||||
p = p.parent
|
||||
return p
|
||||
|
||||
for key in ("schematicPath", "boardPath"):
|
||||
value = params.get(key)
|
||||
if value:
|
||||
start = Path(value).expanduser().parent
|
||||
for ancestor in [start, *start.parents]:
|
||||
if (ancestor / "sym-lib-table").exists() or list(
|
||||
ancestor.glob("*.kicad_pro")
|
||||
):
|
||||
return ancestor
|
||||
return start
|
||||
|
||||
return None
|
||||
|
||||
def use_project(self, project_path: Optional[Path]) -> None:
|
||||
"""Switch the underlying manager to load project-scope libraries.
|
||||
|
||||
Callers (e.g. open_project / create_project) use this to make
|
||||
`<project>/sym-lib-table` visible to subsequent search/list/info calls
|
||||
without requiring every caller to pass projectPath.
|
||||
"""
|
||||
if project_path is None:
|
||||
return
|
||||
if self.library_manager.project_path == project_path:
|
||||
return
|
||||
logger.info(f"Rebuilding SymbolLibraryManager for project: {project_path}")
|
||||
self.library_manager = SymbolLibraryManager(project_path=project_path)
|
||||
|
||||
def _ensure_manager_for(self, params: Dict) -> None:
|
||||
"""Rebuild the library manager if the caller's project differs."""
|
||||
self.use_project(self._derive_project_path(params))
|
||||
|
||||
def list_symbol_libraries(self, params: Dict) -> Dict:
|
||||
"""List all available symbol libraries"""
|
||||
try:
|
||||
self._ensure_manager_for(params)
|
||||
libraries = self.library_manager.list_libraries()
|
||||
return {"success": True, "libraries": libraries, "count": len(libraries)}
|
||||
except Exception as e:
|
||||
@@ -535,6 +583,8 @@ class SymbolLibraryCommands:
|
||||
if not query:
|
||||
return {"success": False, "message": "Missing query parameter"}
|
||||
|
||||
self._ensure_manager_for(params)
|
||||
|
||||
limit = params.get("limit", 20)
|
||||
library_filter = params.get("library")
|
||||
|
||||
@@ -557,6 +607,8 @@ class SymbolLibraryCommands:
|
||||
if not library:
|
||||
return {"success": False, "message": "Missing library parameter"}
|
||||
|
||||
self._ensure_manager_for(params)
|
||||
|
||||
# Check if library exists in sym-lib-table
|
||||
if library not in self.library_manager.libraries:
|
||||
available_libs = list(self.library_manager.libraries.keys())
|
||||
@@ -593,6 +645,8 @@ class SymbolLibraryCommands:
|
||||
if not symbol_spec:
|
||||
return {"success": False, "message": "Missing symbol parameter"}
|
||||
|
||||
self._ensure_manager_for(params)
|
||||
|
||||
result = self.library_manager.find_symbol(symbol_spec)
|
||||
|
||||
if result:
|
||||
|
||||
@@ -305,8 +305,8 @@ class KiCADInterface:
|
||||
# Command routing dictionary
|
||||
self.command_routes = {
|
||||
# Project commands
|
||||
"create_project": self.project_commands.create_project,
|
||||
"open_project": self.project_commands.open_project,
|
||||
"create_project": self._handle_create_project,
|
||||
"open_project": self._handle_open_project,
|
||||
"save_project": self.project_commands.save_project,
|
||||
"snapshot_project": self._handle_snapshot_project,
|
||||
"get_project_info": self.project_commands.get_project_info,
|
||||
@@ -677,6 +677,59 @@ class KiCADInterface:
|
||||
logger.error(f"Error loading schematic: {str(e)}")
|
||||
return {"success": False, "message": str(e)}
|
||||
|
||||
def _project_path_from_filename(self, filename: Optional[str]) -> Optional[Path]:
|
||||
"""Resolve a project directory from a filename param.
|
||||
|
||||
Accepts a .kicad_pro file, a .kicad_pcb file, or a directory.
|
||||
"""
|
||||
if not filename:
|
||||
return None
|
||||
try:
|
||||
p = Path(filename).expanduser()
|
||||
except Exception:
|
||||
return None
|
||||
if p.is_file() or p.suffix in (".kicad_pro", ".kicad_pcb", ".kicad_sch"):
|
||||
return p.parent
|
||||
return p
|
||||
|
||||
def _refresh_symbol_library_for_project(self, project_path: Optional[Path]) -> None:
|
||||
"""Rebuild SymbolLibraryCommands' manager so project-scope sym-lib-table
|
||||
is visible to subsequent search/list/info calls. No-op if unchanged."""
|
||||
if project_path is None:
|
||||
return
|
||||
self._current_project_path = project_path
|
||||
try:
|
||||
self.symbol_library_commands.use_project(project_path)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to refresh symbol library for project {project_path}: {e}")
|
||||
|
||||
def _handle_open_project(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Wrap project_commands.open_project so project-scope symbol libraries
|
||||
become visible to subsequent search_symbols / list_symbol_libraries /
|
||||
get_symbol_info calls."""
|
||||
result = self.project_commands.open_project(params)
|
||||
if result.get("success"):
|
||||
project_info = result.get("project") or {}
|
||||
project_path = self._project_path_from_filename(
|
||||
project_info.get("path") or project_info.get("boardPath") or params.get("filename")
|
||||
)
|
||||
self._refresh_symbol_library_for_project(project_path)
|
||||
return result
|
||||
|
||||
def _handle_create_project(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Wrap project_commands.create_project for the same reason as open_project."""
|
||||
result = self.project_commands.create_project(params)
|
||||
if result.get("success"):
|
||||
project_info = result.get("project") or {}
|
||||
project_path = self._project_path_from_filename(
|
||||
project_info.get("path")
|
||||
or project_info.get("boardPath")
|
||||
or params.get("path")
|
||||
or params.get("filename")
|
||||
)
|
||||
self._refresh_symbol_library_for_project(project_path)
|
||||
return result
|
||||
|
||||
def _handle_place_component(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Place a component on the PCB, with project-local fp-lib-table support.
|
||||
If boardPath is given and differs from the currently loaded board, the
|
||||
|
||||
@@ -10,10 +10,17 @@ export function registerSymbolLibraryTools(server: McpServer, callKicadScript: F
|
||||
// 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", {});
|
||||
"List all available KiCAD symbol libraries from global sym-lib-table, plus the project's sym-lib-table when projectPath (or any related file) is supplied or a project has been opened.",
|
||||
{
|
||||
projectPath: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
"Optional: project directory or .kicad_pro/.kicad_pcb/.kicad_sch path. Including this exposes project-scope sym-lib-table libraries.",
|
||||
),
|
||||
},
|
||||
async (args: { projectPath?: string }) => {
|
||||
const result = await callKicadScript("list_symbol_libraries", args);
|
||||
if (result.success && result.libraries) {
|
||||
return {
|
||||
content: [
|
||||
@@ -51,8 +58,14 @@ Returns symbol references that can be used directly in schematics.`,
|
||||
.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"),
|
||||
projectPath: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
"Optional: project directory or .kicad_pro/.kicad_pcb/.kicad_sch path so project-scope sym-lib-table libraries are searched too.",
|
||||
),
|
||||
},
|
||||
async (args: { query: string; library?: string; limit?: number }) => {
|
||||
async (args: { query: string; library?: string; limit?: number; projectPath?: string }) => {
|
||||
const result = await callKicadScript("search_symbols", args);
|
||||
if (result.success && result.symbols) {
|
||||
if (result.symbols.length === 0) {
|
||||
@@ -99,11 +112,17 @@ Returns symbol references that can be used directly in schematics.`,
|
||||
// List symbols in a specific library
|
||||
server.tool(
|
||||
"list_library_symbols",
|
||||
"List all symbols in a specific KiCAD symbol library",
|
||||
"List all symbols in a specific KiCAD symbol library (global or project-scope when projectPath is supplied or a project has been opened).",
|
||||
{
|
||||
library: z.string().describe("Library name (e.g., 'Device', 'PCM_JLCPCB-MCUs')"),
|
||||
projectPath: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
"Optional: project directory or .kicad_pro/.kicad_pcb/.kicad_sch path to resolve project-scope libraries.",
|
||||
),
|
||||
},
|
||||
async (args: { library: string }) => {
|
||||
async (args: { library: string; projectPath?: string }) => {
|
||||
const result = await callKicadScript("list_library_symbols", args);
|
||||
if (result.success && result.symbols) {
|
||||
const symbolList = result.symbols
|
||||
@@ -137,13 +156,19 @@ Returns symbol references that can be used directly in schematics.`,
|
||||
// Get detailed information about a specific symbol
|
||||
server.tool(
|
||||
"get_symbol_info",
|
||||
"Get detailed information about a specific symbol",
|
||||
"Get detailed information about a specific symbol (global or project-scope when projectPath is supplied or a project has been opened).",
|
||||
{
|
||||
symbol: z
|
||||
.string()
|
||||
.describe("Symbol specification (e.g., 'Device:R' or 'PCM_JLCPCB-MCUs:STM32F103C8T6')"),
|
||||
projectPath: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
"Optional: project directory or .kicad_pro/.kicad_pcb/.kicad_sch path so project-scope libraries are searched.",
|
||||
),
|
||||
},
|
||||
async (args: { symbol: string }) => {
|
||||
async (args: { symbol: string; projectPath?: string }) => {
|
||||
const result = await callKicadScript("get_symbol_info", args);
|
||||
if (result.success && result.symbol_info) {
|
||||
const info = result.symbol_info;
|
||||
|
||||
Reference in New Issue
Block a user