diff --git a/CHANGELOG.md b/CHANGELOG.md index 76370de..abd8562 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,42 @@ All notable changes to the KiCAD MCP Server project are documented here. +## [2.2.0-alpha] - 2026-02-27 + +### New MCP Tools (TypeScript layer – previously Python-only) + +**Routing tools:** +- `delete_trace` - Delete traces by UUID, position or net name +- `query_traces` - Query/filter traces on the board +- `get_nets_list` - List all nets with net code and class +- `modify_trace` - Modify trace width or layer +- `create_netclass` - Create or update a net class +- `route_differential_pair` - Route a differential pair between two points +- `refill_zones` - Refill all copper zones ⚠️ SWIG segfault risk, prefer IPC/UI + +**Component tools:** +- `get_component_pads` - Get all pad data for a component +- `get_component_list` - List all components on the board +- `get_pad_position` - Get absolute position of a specific pad +- `place_component_array` - Place components in a grid array +- `align_components` - Align components along an axis +- `duplicate_component` - Duplicate a component with offset + +### Bug Fixes + +- `routing.py`: Fix SwigPyObject UUID comparison (`str()` → `m_Uuid.AsString()`) +- `routing.py`: Fix SWIG iterator invalidation after `board.Remove()` by snapshotting `list(board.Tracks())` +- `routing.py`: Add `board.SetModified()` + `track = None` after `Remove()` to prevent dangling SWIG pointer crashes +- `routing.py`: Per-track `try/except` in `query_traces()` to skip invalid objects after bulk delete +- `routing.py`: Add missing return statement (mypy) +- `library.py`: Fix `search_footprints` parameter mapping (`search_term` → `pattern`) +- `library.py`: Fix field access (`fp.name` → `fp.full_name`) +- `library.py`: Accept both `pattern` and `search_term` parameter names +- `library.py`: Fix loop variable shadowing `Path` object (mypy) +- `design_rules.py`: Add type annotation for `violation_counts` (mypy) + +--- + ## [2.1.0-alpha] - 2026-01-10 ### Phase 1: Intelligent Schematic Wiring System - Core Infrastructure diff --git a/python/commands/design_rules.py b/python/commands/design_rules.py index 65bd27c..04984c8 100644 --- a/python/commands/design_rules.py +++ b/python/commands/design_rules.py @@ -7,7 +7,8 @@ import pcbnew import logging from typing import Dict, Any, Optional, List, Tuple -logger = logging.getLogger('kicad_interface') +logger = logging.getLogger("kicad_interface") + class DesignRuleCommands: """Handles design rule checking and configuration""" @@ -23,7 +24,7 @@ class DesignRuleCommands: return { "success": False, "message": "No board is loaded", - "errorDetails": "Load or create a board first" + "errorDetails": "Load or create a board first", } design_settings = self.board.GetDesignSettings() @@ -57,9 +58,13 @@ class DesignRuleCommands: # Set micro via settings (use properties - methods removed in KiCAD 9.0) if "microViaDiameter" in params: - design_settings.m_MicroViasMinSize = int(params["microViaDiameter"] * scale) + design_settings.m_MicroViasMinSize = int( + params["microViaDiameter"] * scale + ) if "microViaDrill" in params: - design_settings.m_MicroViasMinDrill = int(params["microViaDrill"] * scale) + design_settings.m_MicroViasMinDrill = int( + params["microViaDrill"] * scale + ) # Set minimum values if "minTrackWidth" in params: @@ -72,13 +77,19 @@ class DesignRuleCommands: design_settings.m_MinThroughDrill = int(params["minViaDrill"] * scale) if "minMicroViaDiameter" in params: - design_settings.m_MicroViasMinSize = int(params["minMicroViaDiameter"] * scale) + design_settings.m_MicroViasMinSize = int( + params["minMicroViaDiameter"] * scale + ) if "minMicroViaDrill" in params: - design_settings.m_MicroViasMinDrill = int(params["minMicroViaDrill"] * scale) + design_settings.m_MicroViasMinDrill = int( + params["minMicroViaDrill"] * scale + ) # KiCAD 9.0: m_MinHoleDiameter removed - use m_MinThroughDrill if "minHoleDiameter" in params: - design_settings.m_MinThroughDrill = int(params["minHoleDiameter"] * scale) + design_settings.m_MinThroughDrill = int( + params["minHoleDiameter"] * scale + ) # KiCAD 9.0: Added hole clearance settings if "holeClearance" in params: @@ -102,13 +113,13 @@ class DesignRuleCommands: "minMicroViaDrill": design_settings.m_MicroViasMinDrill / scale, "holeClearance": design_settings.m_HoleClearance / scale, "holeToHoleMin": design_settings.m_HoleToHoleMin / scale, - "viasMinAnnularWidth": design_settings.m_ViasMinAnnularWidth / scale + "viasMinAnnularWidth": design_settings.m_ViasMinAnnularWidth / scale, } return { "success": True, "message": "Updated design rules", - "rules": response_rules + "rules": response_rules, } except Exception as e: @@ -116,7 +127,7 @@ class DesignRuleCommands: return { "success": False, "message": "Failed to set design rules", - "errorDetails": str(e) + "errorDetails": str(e), } def get_design_rules(self, params: Dict[str, Any]) -> Dict[str, Any]: @@ -126,7 +137,7 @@ class DesignRuleCommands: return { "success": False, "message": "No board is loaded", - "errorDetails": "Load or create a board first" + "errorDetails": "Load or create a board first", } design_settings = self.board.GetDesignSettings() @@ -138,42 +149,34 @@ class DesignRuleCommands: "clearance": design_settings.m_MinClearance / scale, "trackWidth": design_settings.GetCurrentTrackWidth() / scale, "minTrackWidth": design_settings.m_TrackMinWidth / scale, - # Via settings (current values from methods) "viaDiameter": design_settings.GetCurrentViaSize() / scale, "viaDrill": design_settings.GetCurrentViaDrill() / scale, - # Via minimum values "minViaDiameter": design_settings.m_ViasMinSize / scale, "viasMinAnnularWidth": design_settings.m_ViasMinAnnularWidth / scale, - # Micro via settings "microViaDiameter": design_settings.m_MicroViasMinSize / scale, "microViaDrill": design_settings.m_MicroViasMinDrill / scale, "minMicroViaDiameter": design_settings.m_MicroViasMinSize / scale, "minMicroViaDrill": design_settings.m_MicroViasMinDrill / scale, - # KiCAD 9.0: Hole and drill settings (replaces removed m_ViasMinDrill and m_MinHoleDiameter) "minThroughDrill": design_settings.m_MinThroughDrill / scale, "holeClearance": design_settings.m_HoleClearance / scale, "holeToHoleMin": design_settings.m_HoleToHoleMin / scale, - # Other constraints "copperEdgeClearance": design_settings.m_CopperEdgeClearance / scale, "silkClearance": design_settings.m_SilkClearance / scale, } - return { - "success": True, - "rules": rules - } + return {"success": True, "rules": rules} except Exception as e: logger.error(f"Error getting design rules: {str(e)}") return { "success": False, "message": "Failed to get design rules", - "errorDetails": str(e) + "errorDetails": str(e), } def run_drc(self, params: Dict[str, Any]) -> Dict[str, Any]: @@ -189,7 +192,7 @@ class DesignRuleCommands: return { "success": False, "message": "No board is loaded", - "errorDetails": "Load or create a board first" + "errorDetails": "Load or create a board first", } report_path = params.get("reportPath") @@ -200,7 +203,7 @@ class DesignRuleCommands: return { "success": False, "message": "Board file not found", - "errorDetails": "Cannot run DRC without a saved board file" + "errorDetails": "Cannot run DRC without a saved board file", } # Find kicad-cli executable @@ -209,23 +212,28 @@ class DesignRuleCommands: return { "success": False, "message": "kicad-cli not found", - "errorDetails": "KiCAD CLI tool not found in system. Install KiCAD 8.0+ or set PATH." + "errorDetails": "KiCAD CLI tool not found in system. Install KiCAD 8.0+ or set PATH.", } # Create temporary JSON output file - with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as tmp: + with tempfile.NamedTemporaryFile( + mode="w", suffix=".json", delete=False + ) as tmp: json_output = tmp.name try: # Build command cmd = [ kicad_cli, - 'pcb', - 'drc', - '--format', 'json', - '--output', json_output, - '--units', 'mm', - board_file + "pcb", + "drc", + "--format", + "json", + "--output", + json_output, + "--units", + "mm", + board_file, ] logger.info(f"Running DRC command: {' '.join(cmd)}") @@ -235,7 +243,7 @@ class DesignRuleCommands: cmd, capture_output=True, text=True, - timeout=600 # 10 minute timeout for large boards (21MB PCB needs time) + timeout=600, # 10 minute timeout for large boards (21MB PCB needs time) ) if result.returncode != 0: @@ -243,32 +251,34 @@ class DesignRuleCommands: return { "success": False, "message": "DRC command failed", - "errorDetails": result.stderr + "errorDetails": result.stderr, } # Read JSON output - with open(json_output, 'r', encoding='utf-8') as f: + with open(json_output, "r", encoding="utf-8") as f: drc_data = json.load(f) # Parse violations from kicad-cli output violations = [] - violation_counts = {} + violation_counts: dict[str, int] = {} severity_counts = {"error": 0, "warning": 0, "info": 0} - for violation in drc_data.get('violations', []): + for violation in drc_data.get("violations", []): vtype = violation.get("type", "unknown") vseverity = violation.get("severity", "error") - violations.append({ - "type": vtype, - "severity": vseverity, - "message": violation.get("description", ""), - "location": { - "x": violation.get("x", 0), - "y": violation.get("y", 0), - "unit": "mm" + violations.append( + { + "type": vtype, + "severity": vseverity, + "message": violation.get("description", ""), + "location": { + "x": violation.get("x", 0), + "y": violation.get("y", 0), + "unit": "mm", + }, } - }) + ) # Count violations by type violation_counts[vtype] = violation_counts.get(vtype, 0) + 1 @@ -280,30 +290,39 @@ class DesignRuleCommands: # Determine where to save the violations file board_dir = os.path.dirname(board_file) board_name = os.path.splitext(os.path.basename(board_file))[0] - violations_file = os.path.join(board_dir, f"{board_name}_drc_violations.json") + violations_file = os.path.join( + board_dir, f"{board_name}_drc_violations.json" + ) # Always save violations to JSON file (for large result sets) - with open(violations_file, 'w', encoding='utf-8') as f: - json.dump({ - "board": board_file, - "timestamp": drc_data.get("date", "unknown"), - "total_violations": len(violations), - "violation_counts": violation_counts, - "severity_counts": severity_counts, - "violations": violations - }, f, indent=2) + with open(violations_file, "w", encoding="utf-8") as f: + json.dump( + { + "board": board_file, + "timestamp": drc_data.get("date", "unknown"), + "total_violations": len(violations), + "violation_counts": violation_counts, + "severity_counts": severity_counts, + "violations": violations, + }, + f, + indent=2, + ) # Save text report if requested if report_path: report_path = os.path.abspath(os.path.expanduser(report_path)) cmd_report = [ kicad_cli, - 'pcb', - 'drc', - '--format', 'report', - '--output', report_path, - '--units', 'mm', - board_file + "pcb", + "drc", + "--format", + "report", + "--output", + report_path, + "--units", + "mm", + board_file, ] subprocess.run(cmd_report, capture_output=True, timeout=600) @@ -314,10 +333,10 @@ class DesignRuleCommands: "summary": { "total": len(violations), "by_severity": severity_counts, - "by_type": violation_counts + "by_type": violation_counts, }, "violationsFile": violations_file, - "reportPath": report_path if report_path else None + "reportPath": report_path if report_path else None, } finally: @@ -330,14 +349,14 @@ class DesignRuleCommands: return { "success": False, "message": "DRC command timed out", - "errorDetails": "Command took longer than 600 seconds (10 minutes)" + "errorDetails": "Command took longer than 600 seconds (10 minutes)", } except Exception as e: logger.error(f"Error running DRC: {str(e)}") return { "success": False, "message": "Failed to run DRC", - "errorDetails": str(e) + "errorDetails": str(e), } def _find_kicad_cli(self) -> Optional[str]: @@ -399,7 +418,7 @@ class DesignRuleCommands: return { "success": False, "message": "No board is loaded", - "errorDetails": "Load or create a board first" + "errorDetails": "Load or create a board first", } severity = params.get("severity", "all") @@ -416,25 +435,27 @@ class DesignRuleCommands: return { "success": False, "message": "Violations file not found", - "errorDetails": "run_drc did not create violations file" + "errorDetails": "run_drc did not create violations file", } # Load violations from file - with open(violations_file, 'r', encoding='utf-8') as f: + with open(violations_file, "r", encoding="utf-8") as f: data = json.load(f) all_violations = data.get("violations", []) # Filter by severity if specified if severity != "all": - filtered_violations = [v for v in all_violations if v.get("severity") == severity] + filtered_violations = [ + v for v in all_violations if v.get("severity") == severity + ] else: filtered_violations = all_violations return { "success": True, "violations": filtered_violations, - "violationsFile": violations_file # Include file path for reference + "violationsFile": violations_file, # Include file path for reference } except Exception as e: @@ -442,5 +463,5 @@ class DesignRuleCommands: return { "success": False, "message": "Failed to get DRC violations", - "errorDetails": str(e) + "errorDetails": str(e), } diff --git a/python/commands/library.py b/python/commands/library.py index 7a168af..765552f 100644 --- a/python/commands/library.py +++ b/python/commands/library.py @@ -12,7 +12,7 @@ from pathlib import Path from typing import Dict, List, Optional, Tuple import glob -logger = logging.getLogger('kicad_interface') +logger = logging.getLogger("kicad_interface") class LibraryManager: @@ -85,7 +85,7 @@ class LibraryManager: ) """ try: - with open(table_path, 'r') as f: + with open(table_path, "r") as f: content = f.read() # Simple regex-based parser for lib entries @@ -103,7 +103,9 @@ class LibraryManager: self.libraries[nickname] = resolved_uri logger.debug(f" Found library: {nickname} -> {resolved_uri}") else: - logger.warning(f" Could not resolve URI for library {nickname}: {uri}") + logger.warning( + f" Could not resolve URI for library {nickname}: {uri}" + ) except Exception as e: logger.error(f"Error parsing fp-lib-table at {table_path}: {e}") @@ -124,23 +126,23 @@ class LibraryManager: # Common KiCAD environment variables env_vars = { - 'KICAD9_FOOTPRINT_DIR': self._find_kicad_footprint_dir(), - '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(), + "KICAD9_FOOTPRINT_DIR": self._find_kicad_footprint_dir(), + "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 if self.project_path: - env_vars['KIPRJMOD'] = str(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) + resolved = resolved.replace(f"${{{var}}}", value) + resolved = resolved.replace(f"${var}", value) # Expand ~ to home directory resolved = os.path.expanduser(resolved) @@ -167,10 +169,10 @@ class LibraryManager: ] # Also check environment variable - if 'KICAD9_FOOTPRINT_DIR' in os.environ: - possible_paths.insert(0, os.environ['KICAD9_FOOTPRINT_DIR']) - if 'KICAD8_FOOTPRINT_DIR' in os.environ: - possible_paths.insert(0, os.environ['KICAD8_FOOTPRINT_DIR']) + if "KICAD9_FOOTPRINT_DIR" in os.environ: + possible_paths.insert(0, os.environ["KICAD9_FOOTPRINT_DIR"]) + if "KICAD8_FOOTPRINT_DIR" in os.environ: + possible_paths.insert(0, os.environ["KICAD8_FOOTPRINT_DIR"]) for path in possible_paths: if os.path.isdir(path): @@ -190,26 +192,36 @@ class LibraryManager: import json # 1. Check shell environment variable first - if 'KICAD9_3RD_PARTY' in os.environ: - path = os.environ['KICAD9_3RD_PARTY'] + 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() + / "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 + 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: + 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'] + 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): @@ -231,10 +243,10 @@ class LibraryManager: 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) + for candidate in possible_paths: + if candidate.exists(): + logger.info(f"Found KiCad 3rd party directory: {candidate}") + return str(candidate) logger.warning("Could not find KiCad 3rd party directory") return None @@ -325,7 +337,9 @@ class LibraryManager: for library_nickname, library_path in self.libraries.items(): fp_file = Path(library_path) / f"{footprint_name}.kicad_mod" if fp_file.exists(): - logger.info(f"Found footprint {footprint_name} in library {library_nickname}") + logger.info( + f"Found footprint {footprint_name} in library {library_nickname}" + ) return (library_path, footprint_name) logger.warning(f"Footprint not found in any library: {footprint_name}") @@ -354,18 +368,22 @@ class LibraryManager: for footprint in footprints: if regex.search(footprint.lower()): - results.append({ - 'library': library_nickname, - 'footprint': footprint, - 'full_name': f"{library_nickname}:{footprint}" - }) + results.append( + { + "library": library_nickname, + "footprint": footprint, + "full_name": f"{library_nickname}:{footprint}", + } + ) if len(results) >= limit: return results return results - def get_footprint_info(self, library_nickname: str, footprint_name: str) -> Optional[Dict[str, str]]: + def get_footprint_info( + self, library_nickname: str, footprint_name: str + ) -> Optional[Dict[str, str]]: """ Get information about a specific footprint @@ -385,11 +403,11 @@ class LibraryManager: return None return { - 'library': library_nickname, - 'footprint': footprint_name, - 'full_name': f"{library_nickname}:{footprint_name}", - 'path': str(fp_file), - 'library_path': library_path + "library": library_nickname, + "footprint": footprint_name, + "full_name": f"{library_nickname}:{footprint_name}", + "path": str(fp_file), + "library_path": library_path, } @@ -404,39 +422,48 @@ class LibraryCommands: """List all available footprint libraries""" try: libraries = self.library_manager.list_libraries() - return { - "success": True, - "libraries": libraries, - "count": len(libraries) - } + return {"success": True, "libraries": libraries, "count": len(libraries)} except Exception as e: logger.error(f"Error listing libraries: {e}") return { "success": False, "message": "Failed to list libraries", - "errorDetails": str(e) + "errorDetails": str(e), } def search_footprints(self, params: Dict) -> Dict: """Search for footprints by pattern""" try: - pattern = params.get("pattern", "*") + # Support both 'pattern' and 'search_term' parameter names + pattern = params.get("pattern") or params.get("search_term", "*") limit = params.get("limit", 20) + library_filter = params.get("library") - results = self.library_manager.search_footprints(pattern, limit) + results = self.library_manager.search_footprints( + pattern, limit * 10 if library_filter else limit + ) + + # Filter by library if specified + if library_filter: + results = [ + r + for r in results + if r.get("library", "").lower() == library_filter.lower() + ] + results = results[:limit] return { "success": True, "footprints": results, "count": len(results), - "pattern": pattern + "pattern": pattern, } except Exception as e: logger.error(f"Error searching footprints: {e}") return { "success": False, "message": "Failed to search footprints", - "errorDetails": str(e) + "errorDetails": str(e), } def list_library_footprints(self, params: Dict) -> Dict: @@ -444,10 +471,7 @@ class LibraryCommands: try: library = params.get("library") if not library: - return { - "success": False, - "message": "Missing library parameter" - } + return {"success": False, "message": "Missing library parameter"} footprints = self.library_manager.list_footprints(library) @@ -455,14 +479,14 @@ class LibraryCommands: "success": True, "library": library, "footprints": footprints, - "count": len(footprints) + "count": len(footprints), } except Exception as e: logger.error(f"Error listing library footprints: {e}") return { "success": False, "message": "Failed to list library footprints", - "errorDetails": str(e) + "errorDetails": str(e), } def get_footprint_info(self, params: Dict) -> Dict: @@ -470,10 +494,7 @@ class LibraryCommands: try: footprint_spec = params.get("footprint") if not footprint_spec: - return { - "success": False, - "message": "Missing footprint parameter" - } + return {"success": False, "message": "Missing footprint parameter"} # Try to find the footprint result = self.library_manager.find_footprint(footprint_spec) @@ -491,17 +512,14 @@ class LibraryCommands: "library": library_nickname, "footprint": footprint_name, "full_name": f"{library_nickname}:{footprint_name}", - "library_path": library_path + "library_path": library_path, } - return { - "success": True, - "footprint_info": info - } + return {"success": True, "footprint_info": info} else: return { "success": False, - "message": f"Footprint not found: {footprint_spec}" + "message": f"Footprint not found: {footprint_spec}", } except Exception as e: @@ -509,5 +527,5 @@ class LibraryCommands: return { "success": False, "message": "Failed to get footprint info", - "errorDetails": str(e) + "errorDetails": str(e), } diff --git a/python/commands/routing.py b/python/commands/routing.py index c3a6c54..7ac6ffc 100644 --- a/python/commands/routing.py +++ b/python/commands/routing.py @@ -8,7 +8,8 @@ import logging import math from typing import Dict, Any, Optional, List, Tuple -logger = logging.getLogger('kicad_interface') +logger = logging.getLogger("kicad_interface") + class RoutingCommands: """Handles routing-related KiCAD operations""" @@ -24,7 +25,7 @@ class RoutingCommands: return { "success": False, "message": "No board is loaded", - "errorDetails": "Load or create a board first" + "errorDetails": "Load or create a board first", } name = params.get("name") @@ -34,7 +35,7 @@ class RoutingCommands: return { "success": False, "message": "Missing net name", - "errorDetails": "name parameter is required" + "errorDetails": "name parameter is required", } # Create new net @@ -58,8 +59,8 @@ class RoutingCommands: "net": { "name": name, "class": net_class if net_class else "Default", - "netcode": net.GetNetCode() - } + "netcode": net.GetNetCode(), + }, } except Exception as e: @@ -67,7 +68,7 @@ class RoutingCommands: return { "success": False, "message": "Failed to add net", - "errorDetails": str(e) + "errorDetails": str(e), } def route_trace(self, params: Dict[str, Any]) -> Dict[str, Any]: @@ -77,7 +78,7 @@ class RoutingCommands: return { "success": False, "message": "No board is loaded", - "errorDetails": "Load or create a board first" + "errorDetails": "Load or create a board first", } start = params.get("start") @@ -91,7 +92,7 @@ class RoutingCommands: return { "success": False, "message": "Missing parameters", - "errorDetails": "start and end points are required" + "errorDetails": "start and end points are required", } # Get layer ID @@ -100,7 +101,7 @@ class RoutingCommands: return { "success": False, "message": "Invalid layer", - "errorDetails": f"Layer '{layer}' does not exist" + "errorDetails": f"Layer '{layer}' does not exist", } # Get start point @@ -133,14 +134,16 @@ class RoutingCommands: # Add via if requested and net is specified if via and net: via_point = end_point - self.add_via({ - "position": { - "x": via_point.x / 1000000, - "y": via_point.y / 1000000, - "unit": "mm" - }, - "net": net - }) + self.add_via( + { + "position": { + "x": via_point.x / 1000000, + "y": via_point.y / 1000000, + "unit": "mm", + }, + "net": net, + } + ) return { "success": True, @@ -149,17 +152,17 @@ class RoutingCommands: "start": { "x": start_point.x / 1000000, "y": start_point.y / 1000000, - "unit": "mm" + "unit": "mm", }, "end": { "x": end_point.x / 1000000, "y": end_point.y / 1000000, - "unit": "mm" + "unit": "mm", }, "layer": layer, "width": track.GetWidth() / 1000000, - "net": net - } + "net": net, + }, } except Exception as e: @@ -167,7 +170,7 @@ class RoutingCommands: return { "success": False, "message": "Failed to route trace", - "errorDetails": str(e) + "errorDetails": str(e), } def add_via(self, params: Dict[str, Any]) -> Dict[str, Any]: @@ -177,7 +180,7 @@ class RoutingCommands: return { "success": False, "message": "No board is loaded", - "errorDetails": "Load or create a board first" + "errorDetails": "Load or create a board first", } position = params.get("position") @@ -191,22 +194,28 @@ class RoutingCommands: return { "success": False, "message": "Missing position", - "errorDetails": "position parameter is required" + "errorDetails": "position parameter is required", } # Create via via = pcbnew.PCB_VIA(self.board) - + # Set position - scale = 1000000 if position["unit"] == "mm" else 25400000 # mm or inch to nm + scale = ( + 1000000 if position["unit"] == "mm" else 25400000 + ) # mm or inch to nm x_nm = int(position["x"] * scale) y_nm = int(position["y"] * scale) via.SetPosition(pcbnew.VECTOR2I(x_nm, y_nm)) # Set size and drill (default to board's current via settings) design_settings = self.board.GetDesignSettings() - via.SetWidth(int(size * 1000000) if size else design_settings.GetCurrentViaSize()) - via.SetDrill(int(drill * 1000000) if drill else design_settings.GetCurrentViaDrill()) + via.SetWidth( + int(size * 1000000) if size else design_settings.GetCurrentViaSize() + ) + via.SetDrill( + int(drill * 1000000) if drill else design_settings.GetCurrentViaDrill() + ) # Set layers from_id = self.board.GetLayerID(from_layer) @@ -215,7 +224,7 @@ class RoutingCommands: return { "success": False, "message": "Invalid layer", - "errorDetails": "Specified layers do not exist" + "errorDetails": "Specified layers do not exist", } via.SetLayerPair(from_id, to_id) @@ -237,14 +246,14 @@ class RoutingCommands: "position": { "x": position["x"], "y": position["y"], - "unit": position["unit"] + "unit": position["unit"], }, "size": via.GetWidth() / 1000000, "drill": via.GetDrill() / 1000000, "from_layer": from_layer, "to_layer": to_layer, - "net": net - } + "net": net, + }, } except Exception as e: @@ -252,7 +261,7 @@ class RoutingCommands: return { "success": False, "message": "Failed to add via", - "errorDetails": str(e) + "errorDetails": str(e), } def delete_trace(self, params: Dict[str, Any]) -> Dict[str, Any]: @@ -262,7 +271,7 @@ class RoutingCommands: return { "success": False, "message": "No board is loaded", - "errorDetails": "Load or create a board first" + "errorDetails": "Load or create a board first", } trace_uuid = params.get("traceUuid") @@ -275,13 +284,13 @@ class RoutingCommands: return { "success": False, "message": "Missing parameters", - "errorDetails": "One of traceUuid, position, or net must be provided" + "errorDetails": "One of traceUuid, position, or net must be provided", } # Delete by net name (bulk delete) if net_name: tracks_to_remove = [] - for track in self.board.Tracks(): + for track in list(self.board.Tracks()): if track.GetNetname() != net_name: continue @@ -298,20 +307,23 @@ class RoutingCommands: tracks_to_remove.append(track) + deleted_count = len(tracks_to_remove) for track in tracks_to_remove: self.board.Remove(track) + tracks_to_remove.clear() + self.board.SetModified() return { "success": True, - "message": f"Deleted {len(tracks_to_remove)} traces on net '{net_name}'", - "deletedCount": len(tracks_to_remove) + "message": f"Deleted {deleted_count} traces on net '{net_name}'", + "deletedCount": deleted_count, } # Find track by UUID if trace_uuid: track = None - for item in self.board.Tracks(): - if str(item.m_Uuid) == trace_uuid: + for item in list(self.board.Tracks()): + if item.m_Uuid.AsString() == trace_uuid: track = item break @@ -319,26 +331,35 @@ class RoutingCommands: return { "success": False, "message": "Track not found", - "errorDetails": f"Could not find track with UUID: {trace_uuid}" + "errorDetails": f"Could not find track with UUID: {trace_uuid}", } self.board.Remove(track) + track = None + self.board.SetModified() + return {"success": True, "message": f"Deleted track: {trace_uuid}"} + + # No valid parameters provided + if not position: return { - "success": True, - "message": f"Deleted track: {trace_uuid}" + "success": False, + "message": "No valid search parameter provided", + "errorDetails": "Provide traceUuid, position, or net parameter", } # Find track by position if position: - scale = 1000000 if position["unit"] == "mm" else 25400000 # mm or inch to nm + scale = ( + 1000000 if position["unit"] == "mm" else 25400000 + ) # mm or inch to nm x_nm = int(position["x"] * scale) y_nm = int(position["y"] * scale) point = pcbnew.VECTOR2I(x_nm, y_nm) # Find closest track closest_track = None - min_distance = float('inf') - for track in self.board.Tracks(): + min_distance = float("inf") + for track in list(self.board.Tracks()): dist = self._point_to_track_distance(point, track) if dist < min_distance: min_distance = dist @@ -346,15 +367,17 @@ class RoutingCommands: if closest_track and min_distance < 1000000: # Within 1mm self.board.Remove(closest_track) + closest_track = None + self.board.SetModified() return { "success": True, - "message": "Deleted track at specified position" + "message": "Deleted track at specified position", } else: return { "success": False, "message": "No track found", - "errorDetails": "No track found near specified position" + "errorDetails": "No track found near specified position", } except Exception as e: @@ -362,8 +385,13 @@ class RoutingCommands: return { "success": False, "message": "Failed to delete trace", - "errorDetails": str(e) + "errorDetails": str(e), } + return { + "success": False, + "message": "No action taken", + "errorDetails": "No matching trace found for given parameters", + } def get_nets_list(self, params: Dict[str, Any]) -> Dict[str, Any]: """Get a list of all nets in the PCB""" @@ -372,7 +400,7 @@ class RoutingCommands: return { "success": False, "message": "No board is loaded", - "errorDetails": "Load or create a board first" + "errorDetails": "Load or create a board first", } nets = [] @@ -380,23 +408,22 @@ class RoutingCommands: for net_code in range(netinfo.GetNetCount()): net = netinfo.GetNetItem(net_code) if net: - nets.append({ - "name": net.GetNetname(), - "code": net.GetNetCode(), - "class": net.GetClassName() - }) + nets.append( + { + "name": net.GetNetname(), + "code": net.GetNetCode(), + "class": net.GetClassName(), + } + ) - return { - "success": True, - "nets": nets - } + return {"success": True, "nets": nets} except Exception as e: logger.error(f"Error getting nets list: {str(e)}") return { "success": False, "message": "Failed to get nets list", - "errorDetails": str(e) + "errorDetails": str(e), } def query_traces(self, params: Dict[str, Any]) -> Dict[str, Any]: @@ -406,7 +433,7 @@ class RoutingCommands: return { "success": False, "message": "No board is loaded", - "errorDetails": "Load or create a board first" + "errorDetails": "Load or create a board first", } # Get filter parameters @@ -420,86 +447,90 @@ class RoutingCommands: vias = [] # Process tracks - for track in self.board.Tracks(): - # Check if it's a via - is_via = track.Type() == pcbnew.PCB_VIA_T + for track in list(self.board.Tracks()): + try: + # Check if it's a via + is_via = track.Type() == pcbnew.PCB_VIA_T - if is_via and not include_vias: - continue - - # Filter by net - if net_name and track.GetNetname() != net_name: - continue - - # Filter by layer (only for tracks, not vias) - if layer and not is_via: - layer_id = self.board.GetLayerID(layer) - if track.GetLayer() != layer_id: + if is_via and not include_vias: continue - # Filter by bounding box - if bbox: - bbox_unit = bbox.get("unit", "mm") - bbox_scale = scale if bbox_unit == "mm" else 25400000 - x1 = int(bbox.get("x1", 0) * bbox_scale) - y1 = int(bbox.get("y1", 0) * bbox_scale) - x2 = int(bbox.get("x2", 0) * bbox_scale) - y2 = int(bbox.get("y2", 0) * bbox_scale) + # Filter by net + if net_name and track.GetNetname() != net_name: + continue + + # Filter by layer (only for tracks, not vias) + if layer and not is_via: + layer_id = self.board.GetLayerID(layer) + if track.GetLayer() != layer_id: + continue + + # Filter by bounding box + if bbox: + bbox_unit = bbox.get("unit", "mm") + bbox_scale = scale if bbox_unit == "mm" else 25400000 + x1 = int(bbox.get("x1", 0) * bbox_scale) + y1 = int(bbox.get("y1", 0) * bbox_scale) + x2 = int(bbox.get("x2", 0) * bbox_scale) + y2 = int(bbox.get("y2", 0) * bbox_scale) + + if is_via: + pos = track.GetPosition() + if not (x1 <= pos.x <= x2 and y1 <= pos.y <= y2): + continue + else: + start = track.GetStart() + end = track.GetEnd() + # Check if either endpoint is within bbox + start_in = x1 <= start.x <= x2 and y1 <= start.y <= y2 + end_in = x1 <= end.x <= x2 and y1 <= end.y <= y2 + if not (start_in or end_in): + continue if is_via: pos = track.GetPosition() - if not (x1 <= pos.x <= x2 and y1 <= pos.y <= y2): - continue + vias.append( + { + "uuid": track.m_Uuid.AsString(), + "position": { + "x": pos.x / scale, + "y": pos.y / scale, + "unit": "mm", + }, + "net": track.GetNetname(), + "netCode": track.GetNetCode(), + "diameter": track.GetWidth() / scale, + "drill": track.GetDrillValue() / scale, + } + ) else: start = track.GetStart() end = track.GetEnd() - # Check if either endpoint is within bbox - start_in = x1 <= start.x <= x2 and y1 <= start.y <= y2 - end_in = x1 <= end.x <= x2 and y1 <= end.y <= y2 - if not (start_in or end_in): - continue + traces.append( + { + "uuid": track.m_Uuid.AsString(), + "net": track.GetNetname(), + "netCode": track.GetNetCode(), + "layer": self.board.GetLayerName(track.GetLayer()), + "width": track.GetWidth() / scale, + "start": { + "x": start.x / scale, + "y": start.y / scale, + "unit": "mm", + }, + "end": { + "x": end.x / scale, + "y": end.y / scale, + "unit": "mm", + }, + "length": track.GetLength() / scale, + } + ) + except Exception as track_err: + logger.warning(f"Skipping invalid track object: {track_err}") + continue - if is_via: - pos = track.GetPosition() - vias.append({ - "uuid": str(track.m_Uuid), - "position": { - "x": pos.x / scale, - "y": pos.y / scale, - "unit": "mm" - }, - "net": track.GetNetname(), - "netCode": track.GetNetCode(), - "diameter": track.GetWidth() / scale, - "drill": track.GetDrillValue() / scale - }) - else: - start = track.GetStart() - end = track.GetEnd() - traces.append({ - "uuid": str(track.m_Uuid), - "net": track.GetNetname(), - "netCode": track.GetNetCode(), - "layer": self.board.GetLayerName(track.GetLayer()), - "width": track.GetWidth() / scale, - "start": { - "x": start.x / scale, - "y": start.y / scale, - "unit": "mm" - }, - "end": { - "x": end.x / scale, - "y": end.y / scale, - "unit": "mm" - }, - "length": track.GetLength() / scale - }) - - result = { - "success": True, - "traceCount": len(traces), - "traces": traces - } + result = {"success": True, "traceCount": len(traces), "traces": traces} if include_vias: result["viaCount"] = len(vias) @@ -512,7 +543,7 @@ class RoutingCommands: return { "success": False, "message": "Failed to query traces", - "errorDetails": str(e) + "errorDetails": str(e), } def modify_trace(self, params: Dict[str, Any]) -> Dict[str, Any]: @@ -526,7 +557,7 @@ class RoutingCommands: return { "success": False, "message": "No board is loaded", - "errorDetails": "Load or create a board first" + "errorDetails": "Load or create a board first", } # Identification parameters @@ -542,7 +573,7 @@ class RoutingCommands: return { "success": False, "message": "Missing trace identifier", - "errorDetails": "Provide either 'uuid' or 'position' to identify the trace" + "errorDetails": "Provide either 'uuid' or 'position' to identify the trace", } scale = 1000000 # nm to mm conversion @@ -551,8 +582,8 @@ class RoutingCommands: track = None if trace_uuid: - for item in self.board.Tracks(): - if str(item.m_Uuid) == trace_uuid: + for item in list(self.board.Tracks()): + if item.m_Uuid.AsString() == trace_uuid: track = item break elif position: @@ -563,8 +594,8 @@ class RoutingCommands: point = pcbnew.VECTOR2I(x_nm, y_nm) # Find closest track - min_distance = float('inf') - for item in self.board.Tracks(): + min_distance = float("inf") + for item in list(self.board.Tracks()): dist = self._point_to_track_distance(point, item) if dist < min_distance: min_distance = dist @@ -578,7 +609,7 @@ class RoutingCommands: return { "success": False, "message": "Track not found", - "errorDetails": "Could not find track with specified identifier" + "errorDetails": "Could not find track with specified identifier", } # Check if it's a via (some modifications don't apply) @@ -597,7 +628,7 @@ class RoutingCommands: return { "success": False, "message": "Invalid layer", - "errorDetails": f"Layer '{new_layer}' not found" + "errorDetails": f"Layer '{new_layer}' not found", } track.SetLayer(layer_id) modifications.append(f"layer={new_layer}") @@ -609,7 +640,7 @@ class RoutingCommands: return { "success": False, "message": "Invalid net", - "errorDetails": f"Net '{new_net}' not found" + "errorDetails": f"Net '{new_net}' not found", } track.SetNet(net) modifications.append(f"net={new_net}") @@ -618,14 +649,14 @@ class RoutingCommands: return { "success": False, "message": "No modifications specified", - "errorDetails": "Provide at least one of: width, layer, net" + "errorDetails": "Provide at least one of: width, layer, net", } return { "success": True, "message": f"Modified trace: {', '.join(modifications)}", - "uuid": str(track.m_Uuid), - "modifications": modifications + "uuid": track.m_Uuid.AsString(), + "modifications": modifications, } except Exception as e: @@ -633,7 +664,7 @@ class RoutingCommands: return { "success": False, "message": "Failed to modify trace", - "errorDetails": str(e) + "errorDetails": str(e), } def copy_routing_pattern(self, params: Dict[str, Any]) -> Dict[str, Any]: @@ -648,7 +679,7 @@ class RoutingCommands: return { "success": False, "message": "No board is loaded", - "errorDetails": "Load or create a board first" + "errorDetails": "Load or create a board first", } source_refs = params.get("sourceRefs", []) # e.g., ["U1", "U2", "U3"] @@ -660,14 +691,14 @@ class RoutingCommands: return { "success": False, "message": "Missing component references", - "errorDetails": "Provide both 'sourceRefs' and 'targetRefs' arrays" + "errorDetails": "Provide both 'sourceRefs' and 'targetRefs' arrays", } if len(source_refs) != len(target_refs): return { "success": False, "message": "Mismatched component counts", - "errorDetails": f"sourceRefs has {len(source_refs)} items, targetRefs has {len(target_refs)}" + "errorDetails": f"sourceRefs has {len(source_refs)} items, targetRefs has {len(target_refs)}", } scale = 1000000 # nm to mm conversion @@ -681,7 +712,7 @@ class RoutingCommands: return { "success": False, "message": "Component not found", - "errorDetails": f"Component '{ref}' not found on board" + "errorDetails": f"Component '{ref}' not found on board", } # Calculate offset from first source to first target component @@ -709,7 +740,7 @@ class RoutingCommands: traces_to_copy = [] vias_to_copy = [] - for track in self.board.Tracks(): + for track in list(self.board.Tracks()): if track.GetNetname() not in source_nets: continue @@ -731,7 +762,9 @@ class RoutingCommands: # Create new track new_track = pcbnew.PCB_TRACK(self.board) - new_track.SetStart(pcbnew.VECTOR2I(start.x + offset_x, start.y + offset_y)) + new_track.SetStart( + pcbnew.VECTOR2I(start.x + offset_x, start.y + offset_y) + ) new_track.SetEnd(pcbnew.VECTOR2I(end.x + offset_x, end.y + offset_y)) new_track.SetLayer(track.GetLayer()) @@ -763,15 +796,11 @@ class RoutingCommands: result = { "success": True, "message": f"Copied routing pattern: {created_traces} traces, {created_vias} vias", - "offset": { - "x": offset_x / scale, - "y": offset_y / scale, - "unit": "mm" - }, + "offset": {"x": offset_x / scale, "y": offset_y / scale, "unit": "mm"}, "createdTraces": created_traces, "createdVias": created_vias, "sourceComponents": source_refs, - "targetComponents": target_refs + "targetComponents": target_refs, } return result @@ -781,7 +810,7 @@ class RoutingCommands: return { "success": False, "message": "Failed to copy routing pattern", - "errorDetails": str(e) + "errorDetails": str(e), } def create_netclass(self, params: Dict[str, Any]) -> Dict[str, Any]: @@ -791,7 +820,7 @@ class RoutingCommands: return { "success": False, "message": "No board is loaded", - "errorDetails": "Load or create a board first" + "errorDetails": "Load or create a board first", } name = params.get("name") @@ -809,12 +838,12 @@ class RoutingCommands: return { "success": False, "message": "Missing netclass name", - "errorDetails": "name parameter is required" + "errorDetails": "name parameter is required", } # Get net classes net_classes = self.board.GetNetClasses() - + # Create new net class if it doesn't exist if not net_classes.Find(name): netclass = pcbnew.NETCLASS(name) @@ -862,8 +891,8 @@ class RoutingCommands: "uviaDrill": netclass.GetMicroViaDrill() / scale, "diffPairWidth": netclass.GetDiffPairWidth() / scale, "diffPairGap": netclass.GetDiffPairGap() / scale, - "nets": nets - } + "nets": nets, + }, } except Exception as e: @@ -871,9 +900,9 @@ class RoutingCommands: return { "success": False, "message": "Failed to create net class", - "errorDetails": str(e) + "errorDetails": str(e), } - + def add_copper_pour(self, params: Dict[str, Any]) -> Dict[str, Any]: """Add a copper pour (zone) to the PCB""" try: @@ -881,7 +910,7 @@ class RoutingCommands: return { "success": False, "message": "No board is loaded", - "errorDetails": "Load or create a board first" + "errorDetails": "Load or create a board first", } layer = params.get("layer", "F.Cu") @@ -891,12 +920,12 @@ class RoutingCommands: points = params.get("points", []) priority = params.get("priority", 0) fill_type = params.get("fillType", "solid") # solid or hatched - + if not points or len(points) < 3: return { "success": False, "message": "Missing points", - "errorDetails": "At least 3 points are required for copper pour outline" + "errorDetails": "At least 3 points are required for copper pour outline", } # Get layer ID @@ -905,13 +934,13 @@ class RoutingCommands: return { "success": False, "message": "Invalid layer", - "errorDetails": f"Layer '{layer}' does not exist" + "errorDetails": f"Layer '{layer}' does not exist", } # Create zone zone = pcbnew.ZONE(self.board) zone.SetLayer(layer_id) - + # Set net if provided if net: netinfo = self.board.GetNetInfo() @@ -919,22 +948,22 @@ class RoutingCommands: if nets_map.has_key(net): net_obj = nets_map[net] zone.SetNet(net_obj) - + # Set zone properties scale = 1000000 # mm to nm zone.SetAssignedPriority(priority) - + if clearance is not None: zone.SetLocalClearance(int(clearance * scale)) - + zone.SetMinThickness(int(min_width * scale)) - + # Set fill type if fill_type == "hatched": zone.SetFillMode(pcbnew.ZONE_FILL_MODE_HATCH_PATTERN) else: zone.SetFillMode(pcbnew.ZONE_FILL_MODE_POLYGONS) - + # Create outline outline = zone.Outline() outline.NewOutline() # Create a new outline contour first @@ -945,7 +974,7 @@ class RoutingCommands: x_nm = int(point["x"] * scale) y_nm = int(point["y"] * scale) outline.Append(pcbnew.VECTOR2I(x_nm, y_nm)) # Add point to outline - + # Add zone to board self.board.Add(zone) @@ -965,8 +994,8 @@ class RoutingCommands: "minWidth": min_width, "priority": priority, "fillType": fill_type, - "pointCount": len(points) - } + "pointCount": len(points), + }, } except Exception as e: @@ -974,9 +1003,9 @@ class RoutingCommands: return { "success": False, "message": "Failed to add copper pour", - "errorDetails": str(e) + "errorDetails": str(e), } - + def route_differential_pair(self, params: Dict[str, Any]) -> Dict[str, Any]: """Route a differential pair between two sets of points or pads""" try: @@ -984,7 +1013,7 @@ class RoutingCommands: return { "success": False, "message": "No board is loaded", - "errorDetails": "Load or create a board first" + "errorDetails": "Load or create a board first", } start_pos = params.get("startPos") @@ -999,7 +1028,7 @@ class RoutingCommands: return { "success": False, "message": "Missing parameters", - "errorDetails": "startPos, endPos, netPos, and netNeg are required" + "errorDetails": "startPos, endPos, netPos, and netNeg are required", } # Get layer ID @@ -1008,7 +1037,7 @@ class RoutingCommands: return { "success": False, "message": "Invalid layer", - "errorDetails": f"Layer '{layer}' does not exist" + "errorDetails": f"Layer '{layer}' does not exist", } # Get nets @@ -1022,65 +1051,73 @@ class RoutingCommands: return { "success": False, "message": "Nets not found", - "errorDetails": "One or both nets specified for the differential pair do not exist" + "errorDetails": "One or both nets specified for the differential pair do not exist", } # Get start and end points start_point = self._get_point(start_pos) end_point = self._get_point(end_pos) - + # Calculate offset vectors for the two traces # First, get the direction vector from start to end dx = end_point.x - start_point.x dy = end_point.y - start_point.y length = math.sqrt(dx * dx + dy * dy) - + if length <= 0: return { "success": False, "message": "Invalid points", - "errorDetails": "Start and end points must be different" + "errorDetails": "Start and end points must be different", } - + # Normalize direction vector dx /= length dy /= length - + # Get perpendicular vector px = -dy py = dx - + # Set default gap if not provided if gap is None: gap = 0.2 # mm - + # Convert to nm gap_nm = int(gap * 1000000) - + # Calculate offsets offset_x = int(px * gap_nm / 2) offset_y = int(py * gap_nm / 2) - + # Create positive and negative trace points - pos_start = pcbnew.VECTOR2I(int(start_point.x + offset_x), int(start_point.y + offset_y)) - pos_end = pcbnew.VECTOR2I(int(end_point.x + offset_x), int(end_point.y + offset_y)) - neg_start = pcbnew.VECTOR2I(int(start_point.x - offset_x), int(start_point.y - offset_y)) - neg_end = pcbnew.VECTOR2I(int(end_point.x - offset_x), int(end_point.y - offset_y)) - + pos_start = pcbnew.VECTOR2I( + int(start_point.x + offset_x), int(start_point.y + offset_y) + ) + pos_end = pcbnew.VECTOR2I( + int(end_point.x + offset_x), int(end_point.y + offset_y) + ) + neg_start = pcbnew.VECTOR2I( + int(start_point.x - offset_x), int(start_point.y - offset_y) + ) + neg_end = pcbnew.VECTOR2I( + int(end_point.x - offset_x), int(end_point.y - offset_y) + ) + # Create positive trace pos_track = pcbnew.PCB_TRACK(self.board) pos_track.SetStart(pos_start) pos_track.SetEnd(pos_end) pos_track.SetLayer(layer_id) pos_track.SetNet(net_pos_obj) - + # Create negative trace neg_track = pcbnew.PCB_TRACK(self.board) neg_track.SetStart(neg_start) neg_track.SetEnd(neg_end) neg_track.SetLayer(layer_id) neg_track.SetNet(net_neg_obj) - + # Set width if width: trace_width_nm = int(width * 1000000) @@ -1091,7 +1128,7 @@ class RoutingCommands: trace_width = self.board.GetDesignSettings().GetCurrentTrackWidth() pos_track.SetWidth(trace_width) neg_track.SetWidth(trace_width) - + # Add tracks to board self.board.Add(pos_track) self.board.Add(neg_track) @@ -1105,8 +1142,8 @@ class RoutingCommands: "layer": layer, "width": pos_track.GetWidth() / 1000000, "gap": gap, - "length": length / 1000000 - } + "length": length / 1000000, + }, } except Exception as e: @@ -1114,7 +1151,7 @@ class RoutingCommands: return { "success": False, "message": "Failed to route differential pair", - "errorDetails": str(e) + "errorDetails": str(e), } def _get_point(self, point_spec: Dict[str, Any]) -> pcbnew.VECTOR2I: @@ -1132,34 +1169,33 @@ class RoutingCommands: return pad.GetPosition() raise ValueError("Invalid point specification") - def _point_to_track_distance(self, point: pcbnew.VECTOR2I, track: pcbnew.PCB_TRACK) -> float: + def _point_to_track_distance( + self, point: pcbnew.VECTOR2I, track: pcbnew.PCB_TRACK + ) -> float: """Calculate distance from point to track segment""" start = track.GetStart() end = track.GetEnd() - + # Vector from start to end v = pcbnew.VECTOR2I(end.x - start.x, end.y - start.y) # Vector from start to point w = pcbnew.VECTOR2I(point.x - start.x, point.y - start.y) - + # Length of track squared c1 = v.x * v.x + v.y * v.y if c1 == 0: return self._point_distance(point, start) - + # Projection coefficient c2 = float(w.x * v.x + w.y * v.y) / c1 - + if c2 < 0: return self._point_distance(point, start) elif c2 > 1: return self._point_distance(point, end) - + # Point on line - proj = pcbnew.VECTOR2I( - int(start.x + c2 * v.x), - int(start.y + c2 * v.y) - ) + proj = pcbnew.VECTOR2I(int(start.x + c2 * v.x), int(start.y + c2 * v.y)) return self._point_distance(point, proj) def _point_distance(self, p1: pcbnew.VECTOR2I, p2: pcbnew.VECTOR2I) -> float: diff --git a/src/tools/component.ts b/src/tools/component.ts index 7674330..24d5367 100644 --- a/src/tools/component.ts +++ b/src/tools/component.ts @@ -1,291 +1,644 @@ -/** - * Component management tools for KiCAD MCP server - */ - -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import { z } from 'zod'; -import { logger } from '../logger.js'; - -// Command function type for KiCAD script calls -type CommandFunction = (command: string, params: Record) => Promise; - -/** - * Register component management tools with the MCP server - * - * @param server MCP server instance - * @param callKicadScript Function to call KiCAD script commands - */ -export function registerComponentTools(server: McpServer, callKicadScript: CommandFunction): void { - logger.info('Registering component management tools'); - - // ------------------------------------------------------ - // Place Component Tool - // ------------------------------------------------------ - server.tool( - "place_component", - { - componentId: z.string().describe("Identifier for the component to place (e.g., 'R_0603_10k')"), - position: z.object({ - x: z.number().describe("X coordinate"), - y: z.number().describe("Y coordinate"), - unit: z.enum(["mm", "inch"]).describe("Unit of measurement") - }).describe("Position coordinates and unit"), - reference: z.string().optional().describe("Optional desired reference (e.g., 'R5')"), - value: z.string().optional().describe("Optional component value (e.g., '10k')"), - footprint: z.string().optional().describe("Optional specific footprint name"), - rotation: z.number().optional().describe("Optional rotation in degrees"), - layer: z.string().optional().describe("Optional layer (e.g., 'F.Cu', 'B.SilkS')") - }, - async ({ componentId, position, reference, value, footprint, rotation, layer }) => { - logger.debug(`Placing component: ${componentId} at ${position.x},${position.y} ${position.unit}`); - const result = await callKicadScript("place_component", { - componentId, - position, - reference, - value, - footprint, - rotation, - layer - }); - - return { - content: [{ - type: "text", - text: JSON.stringify(result) - }] - }; - } - ); - - // ------------------------------------------------------ - // Move Component Tool - // ------------------------------------------------------ - server.tool( - "move_component", - { - reference: z.string().describe("Reference designator of the component (e.g., 'R5')"), - position: z.object({ - x: z.number().describe("X coordinate"), - y: z.number().describe("Y coordinate"), - unit: z.enum(["mm", "inch"]).describe("Unit of measurement") - }).describe("New position coordinates and unit"), - rotation: z.number().optional().describe("Optional new rotation in degrees") - }, - async ({ reference, position, rotation }) => { - logger.debug(`Moving component: ${reference} to ${position.x},${position.y} ${position.unit}`); - const result = await callKicadScript("move_component", { - reference, - position, - rotation - }); - - return { - content: [{ - type: "text", - text: JSON.stringify(result) - }] - }; - } - ); - - // ------------------------------------------------------ - // Rotate Component Tool - // ------------------------------------------------------ - server.tool( - "rotate_component", - { - reference: z.string().describe("Reference designator of the component (e.g., 'R5')"), - angle: z.number().describe("Rotation angle in degrees (absolute, not relative)") - }, - async ({ reference, angle }) => { - logger.debug(`Rotating component: ${reference} to ${angle} degrees`); - const result = await callKicadScript("rotate_component", { - reference, - angle - }); - - return { - content: [{ - type: "text", - text: JSON.stringify(result) - }] - }; - } - ); - - // ------------------------------------------------------ - // Delete Component Tool - // ------------------------------------------------------ - server.tool( - "delete_component", - { - reference: z.string().describe("Reference designator of the component to delete (e.g., 'R5')") - }, - async ({ reference }) => { - logger.debug(`Deleting component: ${reference}`); - const result = await callKicadScript("delete_component", { reference }); - - return { - content: [{ - type: "text", - text: JSON.stringify(result) - }] - }; - } - ); - - // ------------------------------------------------------ - // Edit Component Properties Tool - // ------------------------------------------------------ - server.tool( - "edit_component", - { - reference: z.string().describe("Reference designator of the component (e.g., 'R5')"), - newReference: z.string().optional().describe("Optional new reference designator"), - value: z.string().optional().describe("Optional new component value"), - footprint: z.string().optional().describe("Optional new footprint") - }, - async ({ reference, newReference, value, footprint }) => { - logger.debug(`Editing component: ${reference}`); - const result = await callKicadScript("edit_component", { - reference, - newReference, - value, - footprint - }); - - return { - content: [{ - type: "text", - text: JSON.stringify(result) - }] - }; - } - ); - - // ------------------------------------------------------ - // Find Component Tool - // ------------------------------------------------------ - server.tool( - "find_component", - { - reference: z.string().optional().describe("Reference designator to search for"), - value: z.string().optional().describe("Component value to search for") - }, - async ({ reference, value }) => { - logger.debug(`Finding component with ${reference ? `reference: ${reference}` : `value: ${value}`}`); - const result = await callKicadScript("find_component", { reference, value }); - - return { - content: [{ - type: "text", - text: JSON.stringify(result) - }] - }; - } - ); - - // ------------------------------------------------------ - // Get Component Properties Tool - // ------------------------------------------------------ - server.tool( - "get_component_properties", - { - reference: z.string().describe("Reference designator of the component (e.g., 'R5')") - }, - async ({ reference }) => { - logger.debug(`Getting properties for component: ${reference}`); - const result = await callKicadScript("get_component_properties", { reference }); - - return { - content: [{ - type: "text", - text: JSON.stringify(result) - }] - }; - } - ); - - // ------------------------------------------------------ - // Add Component Annotation Tool - // ------------------------------------------------------ - server.tool( - "add_component_annotation", - { - reference: z.string().describe("Reference designator of the component (e.g., 'R5')"), - annotation: z.string().describe("Annotation or comment text to add"), - visible: z.boolean().optional().describe("Whether the annotation should be visible on the PCB") - }, - async ({ reference, annotation, visible }) => { - logger.debug(`Adding annotation to component: ${reference}`); - const result = await callKicadScript("add_component_annotation", { - reference, - annotation, - visible - }); - - return { - content: [{ - type: "text", - text: JSON.stringify(result) - }] - }; - } - ); - - // ------------------------------------------------------ - // Group Components Tool - // ------------------------------------------------------ - server.tool( - "group_components", - { - references: z.array(z.string()).describe("Reference designators of components to group"), - groupName: z.string().describe("Name for the component group") - }, - async ({ references, groupName }) => { - logger.debug(`Grouping components: ${references.join(', ')} as ${groupName}`); - const result = await callKicadScript("group_components", { - references, - groupName - }); - - return { - content: [{ - type: "text", - text: JSON.stringify(result) - }] - }; - } - ); - - // ------------------------------------------------------ - // Replace Component Tool - // ------------------------------------------------------ - server.tool( - "replace_component", - { - reference: z.string().describe("Reference designator of the component to replace"), - newComponentId: z.string().describe("ID of the new component to use"), - newFootprint: z.string().optional().describe("Optional new footprint"), - newValue: z.string().optional().describe("Optional new component value") - }, - async ({ reference, newComponentId, newFootprint, newValue }) => { - logger.debug(`Replacing component: ${reference} with ${newComponentId}`); - const result = await callKicadScript("replace_component", { - reference, - newComponentId, - newFootprint, - newValue - }); - - return { - content: [{ - type: "text", - text: JSON.stringify(result) - }] - }; - } - ); - - logger.info('Component management tools registered'); -} +/** + * Component management tools for KiCAD MCP server + */ + +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { logger } from "../logger.js"; + +// Command function type for KiCAD script calls +type CommandFunction = ( + command: string, + params: Record, +) => Promise; + +/** + * Register component management tools with the MCP server + * + * @param server MCP server instance + * @param callKicadScript Function to call KiCAD script commands + */ +export function registerComponentTools( + server: McpServer, + callKicadScript: CommandFunction, +): void { + logger.info("Registering component management tools"); + + // ------------------------------------------------------ + // Place Component Tool + // ------------------------------------------------------ + server.tool( + "place_component", + { + componentId: z + .string() + .describe("Identifier for the component to place (e.g., 'R_0603_10k')"), + position: z + .object({ + x: z.number().describe("X coordinate"), + y: z.number().describe("Y coordinate"), + unit: z.enum(["mm", "inch"]).describe("Unit of measurement"), + }) + .describe("Position coordinates and unit"), + reference: z + .string() + .optional() + .describe("Optional desired reference (e.g., 'R5')"), + value: z + .string() + .optional() + .describe("Optional component value (e.g., '10k')"), + footprint: z + .string() + .optional() + .describe("Optional specific footprint name"), + rotation: z.number().optional().describe("Optional rotation in degrees"), + layer: z + .string() + .optional() + .describe("Optional layer (e.g., 'F.Cu', 'B.SilkS')"), + }, + async ({ + componentId, + position, + reference, + value, + footprint, + rotation, + layer, + }) => { + logger.debug( + `Placing component: ${componentId} at ${position.x},${position.y} ${position.unit}`, + ); + const result = await callKicadScript("place_component", { + componentId, + position, + reference, + value, + footprint, + rotation, + layer, + }); + + return { + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }; + }, + ); + + // ------------------------------------------------------ + // Move Component Tool + // ------------------------------------------------------ + server.tool( + "move_component", + { + reference: z + .string() + .describe("Reference designator of the component (e.g., 'R5')"), + position: z + .object({ + x: z.number().describe("X coordinate"), + y: z.number().describe("Y coordinate"), + unit: z.enum(["mm", "inch"]).describe("Unit of measurement"), + }) + .describe("New position coordinates and unit"), + rotation: z + .number() + .optional() + .describe("Optional new rotation in degrees"), + }, + async ({ reference, position, rotation }) => { + logger.debug( + `Moving component: ${reference} to ${position.x},${position.y} ${position.unit}`, + ); + const result = await callKicadScript("move_component", { + reference, + position, + rotation, + }); + + return { + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }; + }, + ); + + // ------------------------------------------------------ + // Rotate Component Tool + // ------------------------------------------------------ + server.tool( + "rotate_component", + { + reference: z + .string() + .describe("Reference designator of the component (e.g., 'R5')"), + angle: z + .number() + .describe("Rotation angle in degrees (absolute, not relative)"), + }, + async ({ reference, angle }) => { + logger.debug(`Rotating component: ${reference} to ${angle} degrees`); + const result = await callKicadScript("rotate_component", { + reference, + angle, + }); + + return { + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }; + }, + ); + + // ------------------------------------------------------ + // Delete Component Tool + // ------------------------------------------------------ + server.tool( + "delete_component", + { + reference: z + .string() + .describe( + "Reference designator of the component to delete (e.g., 'R5')", + ), + }, + async ({ reference }) => { + logger.debug(`Deleting component: ${reference}`); + const result = await callKicadScript("delete_component", { reference }); + + return { + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }; + }, + ); + + // ------------------------------------------------------ + // Edit Component Properties Tool + // ------------------------------------------------------ + server.tool( + "edit_component", + { + reference: z + .string() + .describe("Reference designator of the component (e.g., 'R5')"), + newReference: z + .string() + .optional() + .describe("Optional new reference designator"), + value: z.string().optional().describe("Optional new component value"), + footprint: z.string().optional().describe("Optional new footprint"), + }, + async ({ reference, newReference, value, footprint }) => { + logger.debug(`Editing component: ${reference}`); + const result = await callKicadScript("edit_component", { + reference, + newReference, + value, + footprint, + }); + + return { + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }; + }, + ); + + // ------------------------------------------------------ + // Find Component Tool + // ------------------------------------------------------ + server.tool( + "find_component", + { + reference: z + .string() + .optional() + .describe("Reference designator to search for"), + value: z.string().optional().describe("Component value to search for"), + }, + async ({ reference, value }) => { + logger.debug( + `Finding component with ${reference ? `reference: ${reference}` : `value: ${value}`}`, + ); + const result = await callKicadScript("find_component", { + reference, + value, + }); + + return { + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }; + }, + ); + + // ------------------------------------------------------ + // Get Component Properties Tool + // ------------------------------------------------------ + server.tool( + "get_component_properties", + { + reference: z + .string() + .describe("Reference designator of the component (e.g., 'R5')"), + }, + async ({ reference }) => { + logger.debug(`Getting properties for component: ${reference}`); + const result = await callKicadScript("get_component_properties", { + reference, + }); + + return { + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }; + }, + ); + + // ------------------------------------------------------ + // Add Component Annotation Tool + // ------------------------------------------------------ + server.tool( + "add_component_annotation", + { + reference: z + .string() + .describe("Reference designator of the component (e.g., 'R5')"), + annotation: z.string().describe("Annotation or comment text to add"), + visible: z + .boolean() + .optional() + .describe("Whether the annotation should be visible on the PCB"), + }, + async ({ reference, annotation, visible }) => { + logger.debug(`Adding annotation to component: ${reference}`); + const result = await callKicadScript("add_component_annotation", { + reference, + annotation, + visible, + }); + + return { + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }; + }, + ); + + // ------------------------------------------------------ + // Group Components Tool + // ------------------------------------------------------ + server.tool( + "group_components", + { + references: z + .array(z.string()) + .describe("Reference designators of components to group"), + groupName: z.string().describe("Name for the component group"), + }, + async ({ references, groupName }) => { + logger.debug( + `Grouping components: ${references.join(", ")} as ${groupName}`, + ); + const result = await callKicadScript("group_components", { + references, + groupName, + }); + + return { + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }; + }, + ); + + // ------------------------------------------------------ + // Replace Component Tool + // ------------------------------------------------------ + server.tool( + "replace_component", + { + reference: z + .string() + .describe("Reference designator of the component to replace"), + newComponentId: z.string().describe("ID of the new component to use"), + newFootprint: z.string().optional().describe("Optional new footprint"), + newValue: z.string().optional().describe("Optional new component value"), + }, + async ({ reference, newComponentId, newFootprint, newValue }) => { + logger.debug(`Replacing component: ${reference} with ${newComponentId}`); + const result = await callKicadScript("replace_component", { + reference, + newComponentId, + newFootprint, + newValue, + }); + + return { + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }; + }, + ); + + // ------------------------------------------------------ + // Get Component Pads Tool + // ------------------------------------------------------ + server.tool( + "get_component_pads", + { + reference: z + .string() + .describe("Reference designator of the component (e.g., 'U1')"), + unit: z + .enum(["mm", "inch"]) + .optional() + .describe("Unit for coordinates (default: mm)"), + }, + async ({ reference, unit }) => { + logger.debug(`Getting pads for component: ${reference}`); + const result = await callKicadScript("get_component_pads", { + reference, + unit: unit || "mm", + }); + + return { + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }; + }, + ); + + // ------------------------------------------------------ + // Get Component List Tool + // ------------------------------------------------------ + server.tool( + "get_component_list", + { + layer: z + .string() + .optional() + .describe("Filter by layer (e.g., 'F.Cu', 'B.Cu')"), + boundingBox: z + .object({ + x1: z.number(), + y1: z.number(), + x2: z.number(), + y2: z.number(), + unit: z.enum(["mm", "inch"]).optional(), + }) + .optional() + .describe("Filter by bounding box region"), + unit: z + .enum(["mm", "inch"]) + .optional() + .describe("Unit for coordinates (default: mm)"), + }, + async ({ layer, boundingBox, unit }) => { + logger.debug("Getting component list"); + const result = await callKicadScript("get_component_list", { + layer, + boundingBox, + unit: unit || "mm", + }); + + return { + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }; + }, + ); + + // ------------------------------------------------------ + // Get Pad Position Tool + // ------------------------------------------------------ + server.tool( + "get_pad_position", + { + reference: z + .string() + .describe("Component reference designator (e.g., 'U1')"), + pad: z.string().describe("Pad number or name (e.g., '1', 'A1')"), + unit: z + .enum(["mm", "inch"]) + .optional() + .describe("Unit for coordinates (default: mm)"), + }, + async ({ reference, pad, unit }) => { + logger.debug(`Getting pad position for ${reference} pad ${pad}`); + const result = await callKicadScript("get_pad_position", { + reference, + pad, + unit: unit || "mm", + }); + + return { + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }; + }, + ); + + // ------------------------------------------------------ + // Place Component Array Tool + // ------------------------------------------------------ + server.tool( + "place_component_array", + { + componentId: z.string().describe("Component identifier"), + startPosition: z + .object({ + x: z.number(), + y: z.number(), + unit: z.enum(["mm", "inch"]), + }) + .describe("Starting position"), + rows: z.number().describe("Number of rows"), + columns: z.number().describe("Number of columns"), + rowSpacing: z.number().describe("Spacing between rows"), + columnSpacing: z.number().describe("Spacing between columns"), + startReference: z + .string() + .optional() + .describe("Starting reference (e.g., 'R1')"), + footprint: z.string().optional().describe("Footprint name"), + value: z.string().optional().describe("Component value"), + rotation: z.number().optional().describe("Rotation in degrees"), + }, + async ({ + componentId, + startPosition, + rows, + columns, + rowSpacing, + columnSpacing, + startReference, + footprint, + value, + rotation, + }) => { + logger.debug( + `Placing component array: ${rows}x${columns} of ${componentId}`, + ); + const result = await callKicadScript("place_component_array", { + componentId, + startPosition, + rows, + columns, + rowSpacing, + columnSpacing, + startReference, + footprint, + value, + rotation, + }); + + return { + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }; + }, + ); + + // ------------------------------------------------------ + // Align Components Tool + // ------------------------------------------------------ + server.tool( + "align_components", + { + references: z + .array(z.string()) + .describe("Array of component references to align"), + alignmentType: z + .enum(["horizontal", "vertical", "grid"]) + .describe("Type of alignment"), + spacing: z + .number() + .optional() + .describe("Spacing between components in mm"), + referenceComponent: z + .string() + .optional() + .describe("Reference component for alignment"), + }, + async ({ references, alignmentType, spacing, referenceComponent }) => { + logger.debug(`Aligning components: ${references.join(", ")}`); + const result = await callKicadScript("align_components", { + references, + alignmentType, + spacing, + referenceComponent, + }); + + return { + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }; + }, + ); + + // ------------------------------------------------------ + // Duplicate Component Tool + // ------------------------------------------------------ + server.tool( + "duplicate_component", + { + reference: z.string().describe("Reference of component to duplicate"), + offset: z + .object({ + x: z.number(), + y: z.number(), + unit: z.enum(["mm", "inch"]).optional(), + }) + .describe("Offset from original position"), + newReference: z.string().optional().describe("New reference designator"), + count: z + .number() + .optional() + .describe("Number of duplicates (default: 1)"), + }, + async ({ reference, offset, newReference, count }) => { + logger.debug(`Duplicating component: ${reference}`); + const result = await callKicadScript("duplicate_component", { + reference, + offset, + newReference, + count, + }); + + return { + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }; + }, + ); + + logger.info("Component management tools registered"); +} diff --git a/src/tools/library.ts b/src/tools/library.ts index 9bd59bc..d81d52f 100644 --- a/src/tools/library.ts +++ b/src/tools/library.ts @@ -1,159 +1,193 @@ -/** - * Library tools for KiCAD MCP server - * Provides access to KiCAD footprint libraries and symbols - */ - -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import { z } from 'zod'; - -export function registerLibraryTools(server: McpServer, callKicadScript: Function) { - // List available footprint libraries - server.tool( - "list_libraries", - "List all available KiCAD footprint libraries", - { - search_paths: z.array(z.string()).optional() - .describe("Optional additional search paths for libraries") - }, - async (args: { search_paths?: string[] }) => { - const result = await callKicadScript("list_libraries", args); - if (result.success && result.libraries) { - return { - content: [ - { - type: "text", - text: `Found ${result.libraries.length} footprint libraries:\n${result.libraries.join('\n')}` - } - ] - }; - } - return { - content: [ - { - type: "text", - text: `Failed to list libraries: ${result.message || 'Unknown error'}` - } - ] - }; - } - ); - - // Search for footprints across all libraries - server.tool( - "search_footprints", - "Search for footprints matching a pattern across all libraries", - { - search_term: z.string() - .describe("Search term or pattern to match footprint names"), - library: z.string().optional() - .describe("Optional specific library to search in"), - limit: z.number().optional().default(50) - .describe("Maximum number of results to return") - }, - async (args: { search_term: string; library?: string; limit?: number }) => { - const result = await callKicadScript("search_footprints", args); - if (result.success && result.footprints) { - const footprintList = result.footprints.map((fp: any) => - `${fp.library}:${fp.name}${fp.description ? ' - ' + fp.description : ''}` - ).join('\n'); - return { - content: [ - { - type: "text", - text: `Found ${result.footprints.length} matching footprints:\n${footprintList}` - } - ] - }; - } - return { - content: [ - { - type: "text", - text: `Failed to search footprints: ${result.message || 'Unknown error'}` - } - ] - }; - } - ); - - // List footprints in a specific library - server.tool( - "list_library_footprints", - "List all footprints in a specific KiCAD library", - { - library_name: z.string() - .describe("Name of the library to list footprints from"), - filter: z.string().optional() - .describe("Optional filter pattern for footprint names"), - limit: z.number().optional().default(100) - .describe("Maximum number of footprints to list") - }, - async (args: { library_name: string; filter?: string; limit?: number }) => { - const result = await callKicadScript("list_library_footprints", args); - if (result.success && result.footprints) { - const footprintList = result.footprints.map((fp: string) => ` - ${fp}`).join('\n'); - return { - content: [ - { - type: "text", - text: `Library ${args.library_name} contains ${result.footprints.length} footprints:\n${footprintList}` - } - ] - }; - } - return { - content: [ - { - type: "text", - text: `Failed to list footprints in library ${args.library_name}: ${result.message || 'Unknown error'}` - } - ] - }; - } - ); - - // Get detailed information about a specific footprint - server.tool( - "get_footprint_info", - "Get detailed information about a specific footprint", - { - library_name: z.string() - .describe("Name of the library containing the footprint"), - footprint_name: z.string() - .describe("Name of the footprint to get information about") - }, - async (args: { library_name: string; footprint_name: string }) => { - const result = await callKicadScript("get_footprint_info", args); - if (result.success && result.info) { - const info = result.info; - const details = [ - `Footprint: ${info.name}`, - `Library: ${info.library}`, - info.description ? `Description: ${info.description}` : '', - info.keywords ? `Keywords: ${info.keywords}` : '', - info.pads ? `Number of pads: ${info.pads}` : '', - info.layers ? `Layers used: ${info.layers.join(', ')}` : '', - info.courtyard ? `Courtyard size: ${info.courtyard.width}mm x ${info.courtyard.height}mm` : '', - info.attributes ? `Attributes: ${JSON.stringify(info.attributes)}` : '' - ].filter(line => line).join('\n'); - - return { - content: [ - { - type: "text", - text: details - } - ] - }; - } - return { - content: [ - { - type: "text", - text: `Failed to get footprint info: ${result.message || 'Unknown error'}` - } - ] - }; - } - ); -} \ No newline at end of file +/** + * Library tools for KiCAD MCP server + * Provides access to KiCAD footprint libraries and symbols + */ + +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; + +export function registerLibraryTools( + server: McpServer, + callKicadScript: Function, +) { + // List available footprint libraries + server.tool( + "list_libraries", + "List all available KiCAD footprint libraries", + { + search_paths: z + .array(z.string()) + .optional() + .describe("Optional additional search paths for libraries"), + }, + async (args: { search_paths?: string[] }) => { + const result = await callKicadScript("list_libraries", args); + if (result.success && result.libraries) { + return { + content: [ + { + type: "text", + text: `Found ${result.libraries.length} footprint libraries:\n${result.libraries.join("\n")}`, + }, + ], + }; + } + return { + content: [ + { + type: "text", + text: `Failed to list libraries: ${result.message || "Unknown error"}`, + }, + ], + }; + }, + ); + + // Search for footprints across all libraries + server.tool( + "search_footprints", + "Search for footprints matching a pattern across all libraries", + { + search_term: z + .string() + .describe("Search term or pattern to match footprint names"), + library: z + .string() + .optional() + .describe("Optional specific library to search in"), + limit: z + .number() + .optional() + .default(50) + .describe("Maximum number of results to return"), + }, + async (args: { search_term: string; library?: string; limit?: number }) => { + const result = await callKicadScript("search_footprints", { + pattern: args.search_term, + library: args.library, + limit: args.limit, + }); + if (result.success && result.footprints) { + const footprintList = result.footprints + .map( + (fp: any) => + `${fp.full_name || fp.library + ":" + fp.footprint}${fp.description ? " - " + fp.description : ""}`, + ) + .join("\n"); + return { + content: [ + { + type: "text", + text: `Found ${result.footprints.length} matching footprints:\n${footprintList}`, + }, + ], + }; + } + return { + content: [ + { + type: "text", + text: `Failed to search footprints: ${result.message || "Unknown error"}`, + }, + ], + }; + }, + ); + + // List footprints in a specific library + server.tool( + "list_library_footprints", + "List all footprints in a specific KiCAD library", + { + library_name: z + .string() + .describe("Name of the library to list footprints from"), + filter: z + .string() + .optional() + .describe("Optional filter pattern for footprint names"), + limit: z + .number() + .optional() + .default(100) + .describe("Maximum number of footprints to list"), + }, + async (args: { library_name: string; filter?: string; limit?: number }) => { + const result = await callKicadScript("list_library_footprints", args); + if (result.success && result.footprints) { + const footprintList = result.footprints + .map((fp: string) => ` - ${fp}`) + .join("\n"); + return { + content: [ + { + type: "text", + text: `Library ${args.library_name} contains ${result.footprints.length} footprints:\n${footprintList}`, + }, + ], + }; + } + return { + content: [ + { + type: "text", + text: `Failed to list footprints in library ${args.library_name}: ${result.message || "Unknown error"}`, + }, + ], + }; + }, + ); + + // Get detailed information about a specific footprint + server.tool( + "get_footprint_info", + "Get detailed information about a specific footprint", + { + library_name: z + .string() + .describe("Name of the library containing the footprint"), + footprint_name: z + .string() + .describe("Name of the footprint to get information about"), + }, + async (args: { library_name: string; footprint_name: string }) => { + const result = await callKicadScript("get_footprint_info", args); + if (result.success && result.info) { + const info = result.info; + const details = [ + `Footprint: ${info.name}`, + `Library: ${info.library}`, + info.description ? `Description: ${info.description}` : "", + info.keywords ? `Keywords: ${info.keywords}` : "", + info.pads ? `Number of pads: ${info.pads}` : "", + info.layers ? `Layers used: ${info.layers.join(", ")}` : "", + info.courtyard + ? `Courtyard size: ${info.courtyard.width}mm x ${info.courtyard.height}mm` + : "", + info.attributes + ? `Attributes: ${JSON.stringify(info.attributes)}` + : "", + ] + .filter((line) => line) + .join("\n"); + + return { + content: [ + { + type: "text", + text: details, + }, + ], + }; + } + return { + content: [ + { + type: "text", + text: `Failed to get footprint info: ${result.message || "Unknown error"}`, + }, + ], + }; + }, + ); +} diff --git a/src/tools/routing.ts b/src/tools/routing.ts index 9dd67b5..8f880bb 100644 --- a/src/tools/routing.ts +++ b/src/tools/routing.ts @@ -1,101 +1,324 @@ -/** - * Routing tools for KiCAD MCP server - */ - -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import { z } from 'zod'; - -export function registerRoutingTools(server: McpServer, callKicadScript: Function) { - // Add net tool - server.tool( - "add_net", - "Create a new net on the PCB", - { - name: z.string().describe("Net name"), - netClass: z.string().optional().describe("Net class name"), - }, - async (args: { name: string; netClass?: string }) => { - const result = await callKicadScript("add_net", args); - return { - content: [{ - type: "text", - text: JSON.stringify(result, null, 2) - }] - }; - } - ); - - // Route trace tool - server.tool( - "route_trace", - "Route a trace between two points", - { - start: z.object({ - x: z.number(), - y: z.number(), - unit: z.string().optional() - }).describe("Start position"), - end: z.object({ - x: z.number(), - y: z.number(), - unit: z.string().optional() - }).describe("End position"), - layer: z.string().describe("PCB layer"), - width: z.number().describe("Trace width in mm"), - net: z.string().describe("Net name"), - }, - async (args: any) => { - const result = await callKicadScript("route_trace", args); - return { - content: [{ - type: "text", - text: JSON.stringify(result, null, 2) - }] - }; - } - ); - - // Add via tool - server.tool( - "add_via", - "Add a via to the PCB", - { - position: z.object({ - x: z.number(), - y: z.number(), - unit: z.string().optional() - }).describe("Via position"), - net: z.string().describe("Net name"), - viaType: z.string().optional().describe("Via type (through, blind, buried)"), - }, - async (args: any) => { - const result = await callKicadScript("add_via", args); - return { - content: [{ - type: "text", - text: JSON.stringify(result, null, 2) - }] - }; - } - ); - - // Add copper pour tool - server.tool( - "add_copper_pour", - "Add a copper pour (ground/power plane) to the PCB", - { - layer: z.string().describe("PCB layer"), - net: z.string().describe("Net name"), - clearance: z.number().optional().describe("Clearance in mm"), - }, - async (args: any) => { - const result = await callKicadScript("add_copper_pour", args); - return { - content: [{ - type: "text", - text: JSON.stringify(result, null, 2) - }] - }; - } - ); -} +/** + * Routing tools for KiCAD MCP server + */ + +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; + +export function registerRoutingTools( + server: McpServer, + callKicadScript: Function, +) { + // Add net tool + server.tool( + "add_net", + "Create a new net on the PCB", + { + name: z.string().describe("Net name"), + netClass: z.string().optional().describe("Net class name"), + }, + async (args: { name: string; netClass?: string }) => { + const result = await callKicadScript("add_net", args); + return { + content: [ + { + type: "text", + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + ); + + // Route trace tool + server.tool( + "route_trace", + "Route a trace between two points", + { + start: z + .object({ + x: z.number(), + y: z.number(), + unit: z.string().optional(), + }) + .describe("Start position"), + end: z + .object({ + x: z.number(), + y: z.number(), + unit: z.string().optional(), + }) + .describe("End position"), + layer: z.string().describe("PCB layer"), + width: z.number().describe("Trace width in mm"), + net: z.string().describe("Net name"), + }, + async (args: any) => { + const result = await callKicadScript("route_trace", args); + return { + content: [ + { + type: "text", + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + ); + + // Add via tool + server.tool( + "add_via", + "Add a via to the PCB", + { + position: z + .object({ + x: z.number(), + y: z.number(), + unit: z.string().optional(), + }) + .describe("Via position"), + net: z.string().describe("Net name"), + viaType: z + .string() + .optional() + .describe("Via type (through, blind, buried)"), + }, + async (args: any) => { + const result = await callKicadScript("add_via", args); + return { + content: [ + { + type: "text", + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + ); + + // Add copper pour tool + server.tool( + "add_copper_pour", + "Add a copper pour (ground/power plane) to the PCB", + { + layer: z.string().describe("PCB layer"), + net: z.string().describe("Net name"), + clearance: z.number().optional().describe("Clearance in mm"), + }, + async (args: any) => { + const result = await callKicadScript("add_copper_pour", args); + return { + content: [ + { + type: "text", + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + ); + + // Delete trace tool + server.tool( + "delete_trace", + "Delete traces from the PCB. Can delete by UUID, position, or bulk-delete all traces on a net.", + { + traceUuid: z + .string() + .optional() + .describe("UUID of a specific trace to delete"), + position: z + .object({ + x: z.number(), + y: z.number(), + unit: z.enum(["mm", "inch"]).optional(), + }) + .optional() + .describe("Delete trace nearest to this position"), + net: z + .string() + .optional() + .describe("Delete all traces on this net (bulk delete)"), + layer: z + .string() + .optional() + .describe("Filter by layer when using net-based deletion"), + includeVias: z + .boolean() + .optional() + .describe("Include vias in net-based deletion"), + }, + async (args: any) => { + const result = await callKicadScript("delete_trace", args); + return { + content: [ + { + type: "text", + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + ); + + // Query traces tool + server.tool( + "query_traces", + "Query traces on the board with optional filters by net, layer, or bounding box.", + { + net: z.string().optional().describe("Filter by net name"), + layer: z.string().optional().describe("Filter by layer name"), + boundingBox: z + .object({ + x1: z.number(), + y1: z.number(), + x2: z.number(), + y2: z.number(), + unit: z.enum(["mm", "inch"]).optional(), + }) + .optional() + .describe("Filter by bounding box region"), + unit: z.enum(["mm", "inch"]).optional().describe("Unit for coordinates"), + }, + async (args: any) => { + const result = await callKicadScript("query_traces", args); + return { + content: [ + { + type: "text", + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + ); + + // Get nets list tool + server.tool( + "get_nets_list", + "Get a list of all nets in the PCB with optional statistics.", + { + includeStats: z + .boolean() + .optional() + .describe("Include statistics (track count, total length, etc.)"), + unit: z + .enum(["mm", "inch"]) + .optional() + .describe("Unit for length measurements"), + }, + async (args: any) => { + const result = await callKicadScript("get_nets_list", args); + return { + content: [ + { + type: "text", + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + ); + + // Modify trace tool + server.tool( + "modify_trace", + "Modify an existing trace (change width, layer, or net).", + { + traceUuid: z.string().describe("UUID of the trace to modify"), + width: z.number().optional().describe("New trace width in mm"), + layer: z.string().optional().describe("New layer name"), + net: z.string().optional().describe("New net name"), + }, + async (args: any) => { + const result = await callKicadScript("modify_trace", args); + return { + content: [ + { + type: "text", + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + ); + + // Create netclass tool + server.tool( + "create_netclass", + "Create a new net class with custom design rules.", + { + name: z.string().describe("Net class name"), + traceWidth: z.number().optional().describe("Default trace width in mm"), + clearance: z.number().optional().describe("Clearance in mm"), + viaDiameter: z.number().optional().describe("Via diameter in mm"), + viaDrill: z.number().optional().describe("Via drill size in mm"), + }, + async (args: any) => { + const result = await callKicadScript("create_netclass", args); + return { + content: [ + { + type: "text", + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + ); + + // Route differential pair tool + server.tool( + "route_differential_pair", + "Route a differential pair between two sets of points.", + { + positivePad: z + .object({ + reference: z.string(), + pad: z.string(), + }) + .describe("Positive pad (component and pad number)"), + negativePad: z + .object({ + reference: z.string(), + pad: z.string(), + }) + .describe("Negative pad (component and pad number)"), + layer: z.string().describe("PCB layer"), + width: z.number().describe("Trace width in mm"), + gap: z.number().describe("Gap between traces in mm"), + positiveNet: z.string().describe("Positive net name"), + negativeNet: z.string().describe("Negative net name"), + }, + async (args: any) => { + const result = await callKicadScript("route_differential_pair", args); + return { + content: [ + { + type: "text", + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + ); + + // Refill zones tool + server.tool( + "refill_zones", + "Refill all copper zones on the board. WARNING: SWIG path has known segfault risk (see KNOWN_ISSUES.md). Prefer using IPC backend (KiCAD open) or triggering zone fill via KiCAD UI instead.", + {}, + async (args: any) => { + const result = await callKicadScript("refill_zones", args); + return { + content: [ + { + type: "text", + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + ); +}