From 890746c6ac0a38c121b0f5544229383eb5359574 Mon Sep 17 00:00:00 2001 From: Michael Holm Date: Sat, 2 May 2026 20:35:15 +0200 Subject: [PATCH] Make project-scope sym-lib-table visible to symbol-discovery tools MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 /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) --- CHANGELOG.md | 15 ++++++++ python/commands/library_symbol.py | 54 +++++++++++++++++++++++++++++ python/kicad_interface.py | 57 +++++++++++++++++++++++++++++-- src/tools/library-symbol.ts | 43 ++++++++++++++++++----- 4 files changed, 158 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a140c5e..be78b46 100644 --- a/CHANGELOG.md +++ b/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 + `/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` diff --git a/python/commands/library_symbol.py b/python/commands/library_symbol.py index ba1592b..1f30de3 100644 --- a/python/commands/library_symbol.py +++ b/python/commands/library_symbol.py @@ -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 + `/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: diff --git a/python/kicad_interface.py b/python/kicad_interface.py index c9dd3ef..d505857 100644 --- a/python/kicad_interface.py +++ b/python/kicad_interface.py @@ -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 diff --git a/src/tools/library-symbol.ts b/src/tools/library-symbol.ts index 31a2c4c..3ed6fed 100644 --- a/src/tools/library-symbol.ts +++ b/src/tools/library-symbol.ts @@ -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;