#
tokens: 47137/50000 9/94 files (page 4/6)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 4 of 6. Use http://codebase.md/mixelpixx/kicad-mcp-server?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .github
│   └── workflows
│       └── ci.yml
├── .gitignore
├── .pre-commit-config.yaml
├── CHANGELOG_2025-10-26.md
├── CHANGELOG_2025-11-01.md
├── CHANGELOG_2025-11-05.md
├── CHANGELOG_2025-11-30.md
├── config
│   ├── claude-desktop-config.json
│   ├── default-config.json
│   ├── linux-config.example.json
│   ├── macos-config.example.json
│   └── windows-config.example.json
├── CONTRIBUTING.md
├── docs
│   ├── BUILD_AND_TEST_SESSION.md
│   ├── CLIENT_CONFIGURATION.md
│   ├── IPC_API_MIGRATION_PLAN.md
│   ├── IPC_BACKEND_STATUS.md
│   ├── JLCPCB_INTEGRATION_PLAN.md
│   ├── KNOWN_ISSUES.md
│   ├── LIBRARY_INTEGRATION.md
│   ├── LINUX_COMPATIBILITY_AUDIT.md
│   ├── PLATFORM_GUIDE.md
│   ├── REALTIME_WORKFLOW.md
│   ├── ROADMAP.md
│   ├── STATUS_SUMMARY.md
│   ├── UI_AUTO_LAUNCH.md
│   ├── VISUAL_FEEDBACK.md
│   ├── WEEK1_SESSION1_SUMMARY.md
│   ├── WEEK1_SESSION2_SUMMARY.md
│   └── WINDOWS_TROUBLESHOOTING.md
├── LICENSE
├── package-json.json
├── package-lock.json
├── package.json
├── pytest.ini
├── python
│   ├── commands
│   │   ├── __init__.py
│   │   ├── board
│   │   │   ├── __init__.py
│   │   │   ├── layers.py
│   │   │   ├── outline.py
│   │   │   ├── size.py
│   │   │   └── view.py
│   │   ├── board.py
│   │   ├── component_schematic.py
│   │   ├── component.py
│   │   ├── connection_schematic.py
│   │   ├── design_rules.py
│   │   ├── export.py
│   │   ├── library_schematic.py
│   │   ├── library.py
│   │   ├── project.py
│   │   ├── routing.py
│   │   └── schematic.py
│   ├── kicad_api
│   │   ├── __init__.py
│   │   ├── base.py
│   │   ├── factory.py
│   │   ├── ipc_backend.py
│   │   └── swig_backend.py
│   ├── kicad_interface.py
│   ├── requirements.txt
│   ├── resources
│   │   ├── __init__.py
│   │   └── resource_definitions.py
│   ├── schemas
│   │   ├── __init__.py
│   │   └── tool_schemas.py
│   ├── test_ipc_backend.py
│   └── utils
│       ├── __init__.py
│       ├── kicad_process.py
│       └── platform_helper.py
├── README.md
├── requirements-dev.txt
├── requirements.txt
├── scripts
│   ├── auto_refresh_kicad.sh
│   └── install-linux.sh
├── setup-windows.ps1
├── src
│   ├── config.ts
│   ├── index.ts
│   ├── kicad-server.ts
│   ├── logger.ts
│   ├── prompts
│   │   ├── component.ts
│   │   ├── design.ts
│   │   ├── index.ts
│   │   └── routing.ts
│   ├── resources
│   │   ├── board.ts
│   │   ├── component.ts
│   │   ├── index.ts
│   │   ├── library.ts
│   │   └── project.ts
│   ├── server.ts
│   ├── tools
│   │   ├── board.ts
│   │   ├── component.ts
│   │   ├── component.txt
│   │   ├── design-rules.ts
│   │   ├── export.ts
│   │   ├── index.ts
│   │   ├── library.ts
│   │   ├── project.ts
│   │   ├── routing.ts
│   │   ├── schematic.ts
│   │   └── ui.ts
│   └── utils
│       └── resource-helpers.ts
├── tests
│   ├── __init__.py
│   └── test_platform_helper.py
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/python/commands/connection_schematic.py:
--------------------------------------------------------------------------------

```python
  1 | from skip import Schematic
  2 | import os
  3 | import logging
  4 | 
  5 | logger = logging.getLogger(__name__)
  6 | 
  7 | class ConnectionManager:
  8 |     """Manage connections between components in schematics"""
  9 | 
 10 |     @staticmethod
 11 |     def add_wire(schematic: Schematic, start_point: list, end_point: list, properties: dict = None):
 12 |         """
 13 |         Add a wire between two points
 14 | 
 15 |         Args:
 16 |             schematic: Schematic object
 17 |             start_point: [x, y] coordinates for wire start
 18 |             end_point: [x, y] coordinates for wire end
 19 |             properties: Optional wire properties (currently unused)
 20 | 
 21 |         Returns:
 22 |             Wire object or None on error
 23 |         """
 24 |         try:
 25 |             # Check if wire collection exists
 26 |             if not hasattr(schematic, 'wire'):
 27 |                 logger.error("Schematic does not have wire collection")
 28 |                 return None
 29 | 
 30 |             wire = schematic.wire.append(
 31 |                 start={'x': start_point[0], 'y': start_point[1]},
 32 |                 end={'x': end_point[0], 'y': end_point[1]}
 33 |             )
 34 |             logger.info(f"Added wire from {start_point} to {end_point}")
 35 |             return wire
 36 |         except Exception as e:
 37 |             logger.error(f"Error adding wire: {e}")
 38 |             return None
 39 | 
 40 |     @staticmethod
 41 |     def get_pin_location(symbol, pin_name: str):
 42 |         """
 43 |         Get the absolute location of a pin on a symbol
 44 | 
 45 |         Args:
 46 |             symbol: Symbol object
 47 |             pin_name: Name or number of the pin (e.g., "1", "GND", "VCC")
 48 | 
 49 |         Returns:
 50 |             [x, y] coordinates or None if pin not found
 51 |         """
 52 |         try:
 53 |             if not hasattr(symbol, 'pin'):
 54 |                 logger.warning(f"Symbol {symbol.property.Reference.value} has no pins")
 55 |                 return None
 56 | 
 57 |             # Find the pin by name
 58 |             target_pin = None
 59 |             for pin in symbol.pin:
 60 |                 if pin.name == pin_name:
 61 |                     target_pin = pin
 62 |                     break
 63 | 
 64 |             if not target_pin:
 65 |                 logger.warning(f"Pin '{pin_name}' not found on {symbol.property.Reference.value}")
 66 |                 return None
 67 | 
 68 |             # Get pin location relative to symbol
 69 |             pin_loc = target_pin.location
 70 |             # Get symbol location
 71 |             symbol_at = symbol.at.value
 72 | 
 73 |             # Calculate absolute position
 74 |             # pin_loc is relative to symbol origin, need to add symbol position
 75 |             abs_x = symbol_at[0] + pin_loc[0]
 76 |             abs_y = symbol_at[1] + pin_loc[1]
 77 | 
 78 |             return [abs_x, abs_y]
 79 |         except Exception as e:
 80 |             logger.error(f"Error getting pin location: {e}")
 81 |             return None
 82 | 
 83 |     @staticmethod
 84 |     def add_connection(schematic: Schematic, source_ref: str, source_pin: str, target_ref: str, target_pin: str):
 85 |         """
 86 |         Add a wire connection between two component pins
 87 | 
 88 |         Args:
 89 |             schematic: Schematic object
 90 |             source_ref: Reference designator of source component (e.g., "R1")
 91 |             source_pin: Pin name/number on source component
 92 |             target_ref: Reference designator of target component (e.g., "C1")
 93 |             target_pin: Pin name/number on target component
 94 | 
 95 |         Returns:
 96 |             True if connection was successful, False otherwise
 97 |         """
 98 |         try:
 99 |             # Find source and target symbols
100 |             source_symbol = None
101 |             target_symbol = None
102 | 
103 |             if not hasattr(schematic, 'symbol'):
104 |                 logger.error("Schematic has no symbols")
105 |                 return False
106 | 
107 |             for symbol in schematic.symbol:
108 |                 ref = symbol.property.Reference.value
109 |                 if ref == source_ref:
110 |                     source_symbol = symbol
111 |                 if ref == target_ref:
112 |                     target_symbol = symbol
113 | 
114 |             if not source_symbol:
115 |                 logger.error(f"Source component '{source_ref}' not found")
116 |                 return False
117 | 
118 |             if not target_symbol:
119 |                 logger.error(f"Target component '{target_ref}' not found")
120 |                 return False
121 | 
122 |             # Get pin locations
123 |             source_loc = ConnectionManager.get_pin_location(source_symbol, source_pin)
124 |             target_loc = ConnectionManager.get_pin_location(target_symbol, target_pin)
125 | 
126 |             if not source_loc or not target_loc:
127 |                 logger.error("Could not determine pin locations")
128 |                 return False
129 | 
130 |             # Add wire between pins
131 |             wire = ConnectionManager.add_wire(schematic, source_loc, target_loc)
132 | 
133 |             if wire:
134 |                 logger.info(f"Connected {source_ref}/{source_pin} to {target_ref}/{target_pin}")
135 |                 return True
136 |             else:
137 |                 return False
138 | 
139 |         except Exception as e:
140 |             logger.error(f"Error adding connection: {e}")
141 |             return False
142 | 
143 |     @staticmethod
144 |     def add_net_label(schematic: Schematic, net_name: str, position: list):
145 |         """
146 |         Add a net label to the schematic
147 | 
148 |         Args:
149 |             schematic: Schematic object
150 |             net_name: Name of the net (e.g., "VCC", "GND", "SIGNAL_1")
151 |             position: [x, y] coordinates for the label
152 | 
153 |         Returns:
154 |             Label object or None on error
155 |         """
156 |         try:
157 |             if not hasattr(schematic, 'label'):
158 |                 logger.error("Schematic does not have label collection")
159 |                 return None
160 | 
161 |             label = schematic.label.append(
162 |                 text=net_name,
163 |                 at={'x': position[0], 'y': position[1]}
164 |             )
165 |             logger.info(f"Added net label '{net_name}' at {position}")
166 |             return label
167 |         except Exception as e:
168 |             logger.error(f"Error adding net label: {e}")
169 |             return None
170 | 
171 |     @staticmethod
172 |     def connect_to_net(schematic: Schematic, component_ref: str, pin_name: str, net_name: str):
173 |         """
174 |         Connect a component pin to a named net using a label
175 | 
176 |         Args:
177 |             schematic: Schematic object
178 |             component_ref: Reference designator (e.g., "U1")
179 |             pin_name: Pin name/number
180 |             net_name: Name of the net to connect to
181 | 
182 |         Returns:
183 |             True if successful, False otherwise
184 |         """
185 |         try:
186 |             # Find the component
187 |             symbol = None
188 |             if hasattr(schematic, 'symbol'):
189 |                 for s in schematic.symbol:
190 |                     if s.property.Reference.value == component_ref:
191 |                         symbol = s
192 |                         break
193 | 
194 |             if not symbol:
195 |                 logger.error(f"Component '{component_ref}' not found")
196 |                 return False
197 | 
198 |             # Get pin location
199 |             pin_loc = ConnectionManager.get_pin_location(symbol, pin_name)
200 |             if not pin_loc:
201 |                 return False
202 | 
203 |             # Add a small wire stub from the pin (so label has something to attach to)
204 |             stub_end = [pin_loc[0] + 2.54, pin_loc[1]]  # 2.54mm = 0.1 inch grid
205 |             wire = ConnectionManager.add_wire(schematic, pin_loc, stub_end)
206 | 
207 |             if not wire:
208 |                 return False
209 | 
210 |             # Add label at the end of the stub
211 |             label = ConnectionManager.add_net_label(schematic, net_name, stub_end)
212 | 
213 |             if label:
214 |                 logger.info(f"Connected {component_ref}/{pin_name} to net '{net_name}'")
215 |                 return True
216 |             else:
217 |                 return False
218 | 
219 |         except Exception as e:
220 |             logger.error(f"Error connecting to net: {e}")
221 |             return False
222 | 
223 |     @staticmethod
224 |     def get_net_connections(schematic: Schematic, net_name: str):
225 |         """
226 |         Get all connections for a named net
227 | 
228 |         Args:
229 |             schematic: Schematic object
230 |             net_name: Name of the net to query
231 | 
232 |         Returns:
233 |             List of connections: [{"component": ref, "pin": pin_name}, ...]
234 |         """
235 |         try:
236 |             connections = []
237 | 
238 |             if not hasattr(schematic, 'label'):
239 |                 logger.warning("Schematic has no labels")
240 |                 return connections
241 | 
242 |             # Find all labels with this net name
243 |             net_labels = []
244 |             for label in schematic.label:
245 |                 if hasattr(label, 'value') and label.value == net_name:
246 |                     net_labels.append(label)
247 | 
248 |             if not net_labels:
249 |                 logger.info(f"No labels found for net '{net_name}'")
250 |                 return connections
251 | 
252 |             # For each label, find connected symbols
253 |             for label in net_labels:
254 |                 # Find wires connected to this label position
255 |                 label_pos = label.at.value if hasattr(label, 'at') else None
256 |                 if not label_pos:
257 |                     continue
258 | 
259 |                 # Search for symbols near this label
260 |                 if hasattr(schematic, 'symbol'):
261 |                     for symbol in schematic.symbol:
262 |                         # Check if symbol has wires attached
263 |                         if hasattr(symbol, 'attached_labels'):
264 |                             for attached_label in symbol.attached_labels:
265 |                                 if attached_label.value == net_name:
266 |                                     # Find which pin is connected
267 |                                     if hasattr(symbol, 'pin'):
268 |                                         for pin in symbol.pin:
269 |                                             pin_loc = ConnectionManager.get_pin_location(symbol, pin.name)
270 |                                             if pin_loc:
271 |                                                 # Check if pin is connected to any wire attached to this label
272 |                                                 connections.append({
273 |                                                     "component": symbol.property.Reference.value,
274 |                                                     "pin": pin.name
275 |                                                 })
276 | 
277 |             logger.info(f"Found {len(connections)} connections for net '{net_name}'")
278 |             return connections
279 | 
280 |         except Exception as e:
281 |             logger.error(f"Error getting net connections: {e}")
282 |             return []
283 | 
284 |     @staticmethod
285 |     def generate_netlist(schematic: Schematic):
286 |         """
287 |         Generate a netlist from the schematic
288 | 
289 |         Returns:
290 |             Dictionary with net information:
291 |             {
292 |                 "nets": [
293 |                     {
294 |                         "name": "VCC",
295 |                         "connections": [
296 |                             {"component": "R1", "pin": "1"},
297 |                             {"component": "C1", "pin": "1"}
298 |                         ]
299 |                     },
300 |                     ...
301 |                 ],
302 |                 "components": [
303 |                     {"reference": "R1", "value": "10k", "footprint": "..."},
304 |                     ...
305 |                 ]
306 |             }
307 |         """
308 |         try:
309 |             netlist = {
310 |                 "nets": [],
311 |                 "components": []
312 |             }
313 | 
314 |             # Gather all components
315 |             if hasattr(schematic, 'symbol'):
316 |                 for symbol in schematic.symbol:
317 |                     component_info = {
318 |                         "reference": symbol.property.Reference.value,
319 |                         "value": symbol.property.Value.value if hasattr(symbol.property, 'Value') else "",
320 |                         "footprint": symbol.property.Footprint.value if hasattr(symbol.property, 'Footprint') else ""
321 |                     }
322 |                     netlist["components"].append(component_info)
323 | 
324 |             # Gather all nets from labels
325 |             if hasattr(schematic, 'label'):
326 |                 net_names = set()
327 |                 for label in schematic.label:
328 |                     if hasattr(label, 'value'):
329 |                         net_names.add(label.value)
330 | 
331 |                 # For each net, get connections
332 |                 for net_name in net_names:
333 |                     connections = ConnectionManager.get_net_connections(schematic, net_name)
334 |                     if connections:
335 |                         netlist["nets"].append({
336 |                             "name": net_name,
337 |                             "connections": connections
338 |                         })
339 | 
340 |             logger.info(f"Generated netlist with {len(netlist['nets'])} nets and {len(netlist['components'])} components")
341 |             return netlist
342 | 
343 |         except Exception as e:
344 |             logger.error(f"Error generating netlist: {e}")
345 |             return {"nets": [], "components": []}
346 | 
347 | if __name__ == '__main__':
348 |     # Example Usage (for testing)
349 |     from schematic import SchematicManager # Assuming schematic.py is in the same directory
350 | 
351 |     # Create a new schematic
352 |     test_sch = SchematicManager.create_schematic("ConnectionTestSchematic")
353 | 
354 |     # Add some wires
355 |     wire1 = ConnectionManager.add_wire(test_sch, [100, 100], [200, 100])
356 |     wire2 = ConnectionManager.add_wire(test_sch, [200, 100], [200, 200])
357 | 
358 |     # Note: add_connection, remove_connection, get_net_connections are placeholders
359 |     # and require more complex implementation based on kicad-skip's structure.
360 | 
361 |     # Example of how you might add a net label (requires finding a point on a wire)
362 |     # from skip import Label
363 |     # if wire1:
364 |     #     net_label_pos = wire1.start # Or calculate a point on the wire
365 |     #     net_label = test_sch.add_label(text="Net_01", at=net_label_pos)
366 |     #     print(f"Added net label 'Net_01' at {net_label_pos}")
367 | 
368 |     # Save the schematic (optional)
369 |     # SchematicManager.save_schematic(test_sch, "connection_test.kicad_sch")
370 | 
371 |     # Clean up (if saved)
372 |     # if os.path.exists("connection_test.kicad_sch"):
373 |     #     os.remove("connection_test.kicad_sch")
374 |     #     print("Cleaned up connection_test.kicad_sch")
375 | 
```

--------------------------------------------------------------------------------
/setup-windows.ps1:
--------------------------------------------------------------------------------

```
  1 | <#
  2 | .SYNOPSIS
  3 |     KiCAD MCP Server - Windows Setup and Configuration Script
  4 | 
  5 | .DESCRIPTION
  6 |     This script automates the setup of KiCAD MCP Server on Windows by:
  7 |     - Detecting KiCAD installation and version
  8 |     - Verifying Python and Node.js installations
  9 |     - Testing KiCAD Python module (pcbnew)
 10 |     - Installing required Python dependencies
 11 |     - Building the TypeScript project
 12 |     - Generating Claude Desktop configuration
 13 |     - Running diagnostic tests
 14 | 
 15 | .PARAMETER SkipBuild
 16 |     Skip the npm build step (useful if already built)
 17 | 
 18 | .PARAMETER ClientType
 19 |     Type of MCP client to configure: 'claude-desktop', 'cline', or 'manual'
 20 |     Default: 'claude-desktop'
 21 | 
 22 | .EXAMPLE
 23 |     .\setup-windows.ps1
 24 |     Run the full setup with default options
 25 | 
 26 | .EXAMPLE
 27 |     .\setup-windows.ps1 -ClientType cline
 28 |     Setup for Cline VSCode extension
 29 | 
 30 | .EXAMPLE
 31 |     .\setup-windows.ps1 -SkipBuild
 32 |     Run setup without rebuilding the project
 33 | #>
 34 | 
 35 | param(
 36 |     [switch]$SkipBuild,
 37 |     [ValidateSet('claude-desktop', 'cline', 'manual')]
 38 |     [string]$ClientType = 'claude-desktop'
 39 | )
 40 | 
 41 | # Color output helpers
 42 | function Write-Success { param([string]$Message) Write-Host "[OK] $Message" -ForegroundColor Green }
 43 | function Write-Error-Custom { param([string]$Message) Write-Host "[ERROR] $Message" -ForegroundColor Red }
 44 | function Write-Warning-Custom { param([string]$Message) Write-Host "[WARN] $Message" -ForegroundColor Yellow }
 45 | function Write-Info { param([string]$Message) Write-Host "[INFO] $Message" -ForegroundColor Cyan }
 46 | function Write-Step { param([string]$Message) Write-Host "`n=== $Message ===" -ForegroundColor Magenta }
 47 | 
 48 | Write-Host @"
 49 | ╔════════════════════════════════════════════════════════════╗
 50 | ║         KiCAD MCP Server - Windows Setup Script           ║
 51 | ║                                                            ║
 52 | ║  This script will configure KiCAD MCP for Windows         ║
 53 | ╚════════════════════════════════════════════════════════════╝
 54 | "@ -ForegroundColor Cyan
 55 | 
 56 | # Store results for final report
 57 | $script:Results = @{
 58 |     KiCADFound = $false
 59 |     KiCADVersion = ""
 60 |     KiCADPythonPath = ""
 61 |     PythonFound = $false
 62 |     PythonVersion = ""
 63 |     NodeFound = $false
 64 |     NodeVersion = ""
 65 |     PcbnewImport = $false
 66 |     DependenciesInstalled = $false
 67 |     ProjectBuilt = $false
 68 |     ConfigGenerated = $false
 69 |     Errors = @()
 70 | }
 71 | 
 72 | # Get script directory (project root)
 73 | $ProjectRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
 74 | 
 75 | Write-Step "Step 1: Detecting KiCAD Installation"
 76 | 
 77 | # Function to find KiCAD installation
 78 | function Find-KiCAD {
 79 |     $possiblePaths = @(
 80 |         "C:\Program Files\KiCad",
 81 |         "C:\Program Files (x86)\KiCad"
 82 |         "$env:USERPROFILE\AppData\Local\Programs\KiCad"
 83 |     )
 84 | 
 85 |     $versions = @("9.0", "9.1", "10.0", "8.0")
 86 | 
 87 |     foreach ($basePath in $possiblePaths) {
 88 |         foreach ($version in $versions) {
 89 |             $kicadPath = Join-Path $basePath $version
 90 |             $pythonExe = Join-Path $kicadPath "bin\python.exe"
 91 |             $pythonLib = Join-Path $kicadPath "lib\python3\dist-packages"
 92 | 
 93 |             if (Test-Path $pythonExe) {
 94 |                 Write-Success "Found KiCAD $version at: $kicadPath"
 95 |                 return @{
 96 |                     Path = $kicadPath
 97 |                     Version = $version
 98 |                     PythonExe = $pythonExe
 99 |                     PythonLib = $pythonLib
100 |                 }
101 |             }
102 |         }
103 |     }
104 | 
105 |     return $null
106 | }
107 | 
108 | $kicad = Find-KiCAD
109 | 
110 | if ($kicad) {
111 |     $script:Results.KiCADFound = $true
112 |     $script:Results.KiCADVersion = $kicad.Version
113 |     $script:Results.KiCADPythonPath = $kicad.PythonLib
114 |     Write-Info "KiCAD Version: $($kicad.Version)"
115 |     Write-Info "Python Path: $($kicad.PythonLib)"
116 | } else {
117 |     Write-Error-Custom "KiCAD not found in standard locations"
118 |     Write-Warning-Custom "Checked: C:\Program Files, C:\Program Files (x86), and $env:USERPROFILE\AppData\Local\Programs"
119 |     Write-Warning-Custom "Please install KiCAD 9.0+ from https://www.kicad.org/download/windows/"
120 |     $script:Results.Errors += "KiCAD not found"
121 | }
122 | 
123 | Write-Step "Step 2: Checking Node.js Installation"
124 | 
125 | try {
126 |     $nodeVersion = node --version 2>$null
127 |     if ($LASTEXITCODE -eq 0) {
128 |         Write-Success "Node.js found: $nodeVersion"
129 |         $script:Results.NodeFound = $true
130 |         $script:Results.NodeVersion = $nodeVersion
131 | 
132 |         # Check if version is 18+
133 |         $versionNumber = [int]($nodeVersion -replace 'v(\d+)\..*', '$1')
134 |         if ($versionNumber -lt 18) {
135 |             Write-Warning-Custom "Node.js version 18+ is recommended (you have $nodeVersion)"
136 |         }
137 |     }
138 | } catch {
139 |     Write-Error-Custom "Node.js not found"
140 |     Write-Warning-Custom "Please install Node.js 18+ from https://nodejs.org/"
141 |     $script:Results.Errors += "Node.js not found"
142 | }
143 | 
144 | Write-Step "Step 3: Testing KiCAD Python Module"
145 | 
146 | if ($kicad) {
147 |     Write-Info "Testing pcbnew module import..."
148 | 
149 |     $testScript = "import sys; import pcbnew; print(f'SUCCESS:{pcbnew.GetBuildVersion()}')"
150 |     $result = & $kicad.PythonExe -c $testScript 2>&1
151 | 
152 |     if ($result -match "SUCCESS:(.+)") {
153 |         $pcbnewVersion = $matches[1]
154 |         Write-Success "pcbnew module imported successfully: $pcbnewVersion"
155 |         $script:Results.PcbnewImport = $true
156 |     } else {
157 |         Write-Error-Custom "Failed to import pcbnew module"
158 |         Write-Warning-Custom "Error: $result"
159 |         Write-Info "This usually means KiCAD was not installed with Python support"
160 |         $script:Results.Errors += "pcbnew import failed: $result"
161 |     }
162 | } else {
163 |     Write-Warning-Custom "Skipping pcbnew test (KiCAD not found)"
164 | }
165 | 
166 | Write-Step "Step 4: Checking Python Installation"
167 | 
168 | try {
169 |     $pythonVersion = python --version 2>&1
170 |     if ($pythonVersion -match "Python (\d+\.\d+\.\d+)") {
171 |         Write-Success "System Python found: $pythonVersion"
172 |         $script:Results.PythonFound = $true
173 |         $script:Results.PythonVersion = $pythonVersion
174 |     }
175 | } catch {
176 |     Write-Warning-Custom "System Python not found (using KiCAD's Python)"
177 | }
178 | 
179 | Write-Step "Step 5: Installing Node.js Dependencies"
180 | 
181 | if ($script:Results.NodeFound) {
182 |     Write-Info "Running npm install..."
183 |     Push-Location $ProjectRoot
184 |     try {
185 |         npm install 2>&1 | Out-Null
186 |         if ($LASTEXITCODE -eq 0) {
187 |             Write-Success "Node.js dependencies installed"
188 |         } else {
189 |             Write-Error-Custom "npm install failed"
190 |             $script:Results.Errors += "npm install failed"
191 |         }
192 |     } finally {
193 |         Pop-Location
194 |     }
195 | } else {
196 |     Write-Warning-Custom "Skipping npm install (Node.js not found)"
197 | }
198 | 
199 | Write-Step "Step 6: Installing Python Dependencies"
200 | 
201 | if ($kicad) {
202 |     Write-Info "Installing Python packages..."
203 |     Push-Location $ProjectRoot
204 |     try {
205 |         $requirementsFile = Join-Path $ProjectRoot "requirements.txt"
206 |         if (Test-Path $requirementsFile) {
207 |             & $kicad.PythonExe -m pip install -r $requirementsFile 2>&1 | Out-Null
208 |             if ($LASTEXITCODE -eq 0) {
209 |                 Write-Success "Python dependencies installed"
210 |                 $script:Results.DependenciesInstalled = $true
211 |             } else {
212 |                 Write-Warning-Custom "Some Python packages may have failed to install"
213 |             }
214 |         } else {
215 |             Write-Warning-Custom "requirements.txt not found"
216 |         }
217 |     } finally {
218 |         Pop-Location
219 |     }
220 | } else {
221 |     Write-Warning-Custom "Skipping Python dependencies (KiCAD Python not found)"
222 | }
223 | 
224 | Write-Step "Step 7: Building TypeScript Project"
225 | 
226 | if (-not $SkipBuild -and $script:Results.NodeFound) {
227 |     Write-Info "Running npm run build..."
228 |     Push-Location $ProjectRoot
229 |     try {
230 |         npm run build 2>&1 | Out-Null
231 |         if ($LASTEXITCODE -eq 0) {
232 |             $distPath = Join-Path $ProjectRoot "dist\index.js"
233 |             if (Test-Path $distPath) {
234 |                 Write-Success "Project built successfully"
235 |                 $script:Results.ProjectBuilt = $true
236 |             } else {
237 |                 Write-Error-Custom "Build completed but dist/index.js not found"
238 |                 $script:Results.Errors += "Build output missing"
239 |             }
240 |         } else {
241 |             Write-Error-Custom "Build failed"
242 |             $script:Results.Errors += "TypeScript build failed"
243 |         }
244 |     } finally {
245 |         Pop-Location
246 |     }
247 | } else {
248 |     if ($SkipBuild) {
249 |         Write-Info "Skipping build (--SkipBuild specified)"
250 |     } else {
251 |         Write-Warning-Custom "Skipping build (Node.js not found)"
252 |     }
253 | }
254 | 
255 | Write-Step "Step 8: Generating Configuration"
256 | 
257 | if ($kicad -and $script:Results.ProjectBuilt) {
258 |     $distPath = Join-Path $ProjectRoot "dist\index.js"
259 |     $distPathEscaped = $distPath -replace '\\', '\\'
260 |     $pythonLibEscaped = $kicad.PythonLib -replace '\\', '\\'
261 | 
262 |     $config = @"
263 | {
264 |   "mcpServers": {
265 |     "kicad": {
266 |       "command": "node",
267 |       "args": ["$distPathEscaped"],
268 |       "env": {
269 |         "PYTHONPATH": "$pythonLibEscaped",
270 |         "NODE_ENV": "production",
271 |         "LOG_LEVEL": "info"
272 |       }
273 |     }
274 |   }
275 | }
276 | "@
277 | 
278 |     $configPath = Join-Path $ProjectRoot "windows-mcp-config.json"
279 |     $config | Out-File -FilePath $configPath -Encoding UTF8
280 |     Write-Success "Configuration generated: $configPath"
281 |     $script:Results.ConfigGenerated = $true
282 | 
283 |     Write-Info "`nConfiguration Preview:"
284 |     Write-Host $config -ForegroundColor Gray
285 | 
286 |     # Provide instructions based on client type
287 |     Write-Info "`nTo use this configuration:"
288 | 
289 |     if ($ClientType -eq 'claude-desktop') {
290 |         $claudeConfigPath = "$env:APPDATA\Claude\claude_desktop_config.json"
291 |         Write-Host "`n1. Open Claude Desktop configuration:" -ForegroundColor Yellow
292 |         Write-Host "   $claudeConfigPath" -ForegroundColor White
293 |         Write-Host "`n2. Copy the contents from:" -ForegroundColor Yellow
294 |         Write-Host "   $configPath" -ForegroundColor White
295 |         Write-Host "`n3. Restart Claude Desktop" -ForegroundColor Yellow
296 |     } elseif ($ClientType -eq 'cline') {
297 |         $clineConfigPath = "$env:APPDATA\Code\User\globalStorage\saoudrizwan.claude-dev\settings\cline_mcp_settings.json"
298 |         Write-Host "`n1. Open Cline configuration:" -ForegroundColor Yellow
299 |         Write-Host "   $clineConfigPath" -ForegroundColor White
300 |         Write-Host "`n2. Copy the contents from:" -ForegroundColor Yellow
301 |         Write-Host "   $configPath" -ForegroundColor White
302 |         Write-Host "`n3. Restart VSCode" -ForegroundColor Yellow
303 |     } else {
304 |         Write-Host "`n1. Configuration saved to:" -ForegroundColor Yellow
305 |         Write-Host "   $configPath" -ForegroundColor White
306 |         Write-Host "`n2. Copy to your MCP client configuration" -ForegroundColor Yellow
307 |     }
308 | 
309 | } else {
310 |     Write-Warning-Custom "Skipping configuration generation (prerequisites not met)"
311 | }
312 | 
313 | Write-Step "Step 9: Running Diagnostic Test"
314 | 
315 | if ($kicad -and $script:Results.ProjectBuilt) {
316 |     Write-Info "Testing server startup..."
317 | 
318 |     $env:PYTHONPATH = $kicad.PythonLib
319 |     $distPath = Join-Path $ProjectRoot "dist\index.js"
320 | 
321 |     # Start the server process
322 |     $process = Start-Process -FilePath "node" `
323 |                             -ArgumentList $distPath `
324 |                             -NoNewWindow `
325 |                             -PassThru `
326 |                             -RedirectStandardError (Join-Path $env:TEMP "kicad-mcp-test-error.txt") `
327 |                             -RedirectStandardOutput (Join-Path $env:TEMP "kicad-mcp-test-output.txt")
328 | 
329 |     # Wait a moment for startup
330 |     Start-Sleep -Seconds 2
331 | 
332 |     if (-not $process.HasExited) {
333 |         Write-Success "Server started successfully (PID: $($process.Id))"
334 |         Write-Info "Stopping test server..."
335 |         Stop-Process -Id $process.Id -Force
336 |     } else {
337 |         Write-Error-Custom "Server exited immediately (exit code: $($process.ExitCode))"
338 | 
339 |         $errorLog = Join-Path $env:TEMP "kicad-mcp-test-error.txt"
340 |         if (Test-Path $errorLog) {
341 |             $errorContent = Get-Content $errorLog -Raw
342 |             if ($errorContent) {
343 |                 Write-Warning-Custom "Error output:"
344 |                 Write-Host $errorContent -ForegroundColor Red
345 |             }
346 |         }
347 | 
348 |         $script:Results.Errors += "Server startup test failed"
349 |     }
350 | } else {
351 |     Write-Warning-Custom "Skipping diagnostic test (prerequisites not met)"
352 | }
353 | 
354 | # Final Report
355 | Write-Step "Setup Summary"
356 | 
357 | Write-Host "`nComponent Status:" -ForegroundColor Cyan
358 | Write-Host "  KiCAD Installation:     $(if ($script:Results.KiCADFound) { '[OK] Found' } else { '[ERROR] Not Found' })" -ForegroundColor $(if ($script:Results.KiCADFound) { 'Green' } else { 'Red' })
359 | if ($script:Results.KiCADVersion) {
360 |     Write-Host "    Version:              $($script:Results.KiCADVersion)" -ForegroundColor Gray
361 | }
362 | Write-Host "  pcbnew Module:          $(if ($script:Results.PcbnewImport) { '[OK] Working' } else { '[ERROR] Failed' })" -ForegroundColor $(if ($script:Results.PcbnewImport) { 'Green' } else { 'Red' })
363 | Write-Host "  Node.js:                $(if ($script:Results.NodeFound) { '[OK] Found' } else { '[ERROR] Not Found' })" -ForegroundColor $(if ($script:Results.NodeFound) { 'Green' } else { 'Red' })
364 | if ($script:Results.NodeVersion) {
365 |     Write-Host "    Version:              $($script:Results.NodeVersion)" -ForegroundColor Gray
366 | }
367 | Write-Host "  Python Dependencies:    $(if ($script:Results.DependenciesInstalled) { '[OK] Installed' } else { '[ERROR] Failed' })" -ForegroundColor $(if ($script:Results.DependenciesInstalled) { 'Green' } else { 'Red' })
368 | Write-Host "  Project Build:          $(if ($script:Results.ProjectBuilt) { '[OK] Success' } else { '[ERROR] Failed' })" -ForegroundColor $(if ($script:Results.ProjectBuilt) { 'Green' } else { 'Red' })
369 | Write-Host "  Configuration:          $(if ($script:Results.ConfigGenerated) { '[OK] Generated' } else { '[ERROR] Not Generated' })" -ForegroundColor $(if ($script:Results.ConfigGenerated) { 'Green' } else { 'Red' })
370 | 
371 | if ($script:Results.Errors.Count -gt 0) {
372 |     Write-Host "`nErrors Encountered:" -ForegroundColor Red
373 |     foreach ($error in $script:Results.Errors) {
374 |         Write-Host "  • $error" -ForegroundColor Red
375 |     }
376 | }
377 | 
378 | # Check for log file
379 | $logPath = "$env:USERPROFILE\.kicad-mcp\logs\kicad_interface.log"
380 | if (Test-Path $logPath) {
381 |     Write-Host "`nLog file location:" -ForegroundColor Cyan
382 |     Write-Host "  $logPath" -ForegroundColor Gray
383 | }
384 | 
385 | # Success criteria
386 | $isSuccess = $script:Results.KiCADFound -and
387 |              $script:Results.PcbnewImport -and
388 |              $script:Results.NodeFound -and
389 |              $script:Results.ProjectBuilt
390 | 
391 | if ($isSuccess) {
392 |     Write-Host "`n============================================================" -ForegroundColor Green
393 |     Write-Host "  [OK] Setup completed successfully!" -ForegroundColor Green
394 |     Write-Host "" -ForegroundColor Green
395 |     Write-Host "  Next steps:" -ForegroundColor Green
396 |     Write-Host "  1. Copy the generated config to your MCP client" -ForegroundColor Green
397 |     Write-Host "  2. Restart your MCP client (Claude Desktop/Cline)" -ForegroundColor Green
398 |     Write-Host "  3. Try: 'Create a new KiCAD project'" -ForegroundColor Green
399 |     Write-Host "============================================================" -ForegroundColor Green
400 | } else {
401 |     Write-Host "`n============================================================" -ForegroundColor Red
402 |     Write-Host "  [ERROR] Setup incomplete - issues detected" -ForegroundColor Red
403 |     Write-Host "" -ForegroundColor Red
404 |     Write-Host "  Please resolve the errors above and run again" -ForegroundColor Red
405 |     Write-Host "" -ForegroundColor Red
406 |     Write-Host "  For help:" -ForegroundColor Red
407 |     Write-Host "  https://github.com/mixelpixx/KiCAD-MCP-Server/issues" -ForegroundColor Red
408 |     Write-Host "============================================================" -ForegroundColor Red
409 |     exit 1
410 | }
411 | 
```

--------------------------------------------------------------------------------
/python/commands/library.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Library management for KiCAD footprints
  3 | 
  4 | Handles parsing fp-lib-table files, discovering footprints,
  5 | and providing search functionality for component placement.
  6 | """
  7 | 
  8 | import os
  9 | import re
 10 | import logging
 11 | from pathlib import Path
 12 | from typing import Dict, List, Optional, Tuple
 13 | import glob
 14 | 
 15 | logger = logging.getLogger('kicad_interface')
 16 | 
 17 | 
 18 | class LibraryManager:
 19 |     """
 20 |     Manages KiCAD footprint libraries
 21 | 
 22 |     Parses fp-lib-table files (both global and project-specific),
 23 |     indexes available footprints, and provides search functionality.
 24 |     """
 25 | 
 26 |     def __init__(self, project_path: Optional[Path] = None):
 27 |         """
 28 |         Initialize library manager
 29 | 
 30 |         Args:
 31 |             project_path: Optional path to project directory for project-specific libraries
 32 |         """
 33 |         self.project_path = project_path
 34 |         self.libraries: Dict[str, str] = {}  # nickname -> path mapping
 35 |         self.footprint_cache: Dict[str, List[str]] = {}  # library -> [footprint names]
 36 |         self._load_libraries()
 37 | 
 38 |     def _load_libraries(self):
 39 |         """Load libraries from fp-lib-table files"""
 40 |         # Load global libraries
 41 |         global_table = self._get_global_fp_lib_table()
 42 |         if global_table and global_table.exists():
 43 |             logger.info(f"Loading global fp-lib-table from: {global_table}")
 44 |             self._parse_fp_lib_table(global_table)
 45 |         else:
 46 |             logger.warning(f"Global fp-lib-table not found at: {global_table}")
 47 | 
 48 |         # Load project-specific libraries if project path provided
 49 |         if self.project_path:
 50 |             project_table = self.project_path / "fp-lib-table"
 51 |             if project_table.exists():
 52 |                 logger.info(f"Loading project fp-lib-table from: {project_table}")
 53 |                 self._parse_fp_lib_table(project_table)
 54 | 
 55 |         logger.info(f"Loaded {len(self.libraries)} footprint libraries")
 56 | 
 57 |     def _get_global_fp_lib_table(self) -> Optional[Path]:
 58 |         """Get path to global fp-lib-table file"""
 59 |         # Try different possible locations
 60 |         kicad_config_paths = [
 61 |             Path.home() / ".config" / "kicad" / "9.0" / "fp-lib-table",
 62 |             Path.home() / ".config" / "kicad" / "8.0" / "fp-lib-table",
 63 |             Path.home() / ".config" / "kicad" / "fp-lib-table",
 64 |             # Windows paths
 65 |             Path.home() / "AppData" / "Roaming" / "kicad" / "9.0" / "fp-lib-table",
 66 |             Path.home() / "AppData" / "Roaming" / "kicad" / "8.0" / "fp-lib-table",
 67 |             # macOS paths
 68 |             Path.home() / "Library" / "Preferences" / "kicad" / "9.0" / "fp-lib-table",
 69 |             Path.home() / "Library" / "Preferences" / "kicad" / "8.0" / "fp-lib-table",
 70 |         ]
 71 | 
 72 |         for path in kicad_config_paths:
 73 |             if path.exists():
 74 |                 return path
 75 | 
 76 |         return None
 77 | 
 78 |     def _parse_fp_lib_table(self, table_path: Path):
 79 |         """
 80 |         Parse fp-lib-table file
 81 | 
 82 |         Format is S-expression (Lisp-like):
 83 |         (fp_lib_table
 84 |           (lib (name "Library_Name")(type KiCad)(uri "${KICAD9_FOOTPRINT_DIR}/Library.pretty")(options "")(descr "Description"))
 85 |         )
 86 |         """
 87 |         try:
 88 |             with open(table_path, 'r') as f:
 89 |                 content = f.read()
 90 | 
 91 |             # Simple regex-based parser for lib entries
 92 |             # Pattern: (lib (name "NAME")(type TYPE)(uri "URI")...)
 93 |             lib_pattern = r'\(lib\s+\(name\s+"?([^")\s]+)"?\)\s*\(type\s+[^)]+\)\s*\(uri\s+"?([^")\s]+)"?'
 94 | 
 95 |             for match in re.finditer(lib_pattern, content, re.IGNORECASE):
 96 |                 nickname = match.group(1)
 97 |                 uri = match.group(2)
 98 | 
 99 |                 # Resolve environment variables in URI
100 |                 resolved_uri = self._resolve_uri(uri)
101 | 
102 |                 if resolved_uri:
103 |                     self.libraries[nickname] = resolved_uri
104 |                     logger.debug(f"  Found library: {nickname} -> {resolved_uri}")
105 |                 else:
106 |                     logger.warning(f"  Could not resolve URI for library {nickname}: {uri}")
107 | 
108 |         except Exception as e:
109 |             logger.error(f"Error parsing fp-lib-table at {table_path}: {e}")
110 | 
111 |     def _resolve_uri(self, uri: str) -> Optional[str]:
112 |         """
113 |         Resolve environment variables and paths in library URI
114 | 
115 |         Handles:
116 |         - ${KICAD9_FOOTPRINT_DIR} -> /usr/share/kicad/footprints
117 |         - ${KICAD8_FOOTPRINT_DIR} -> /usr/share/kicad/footprints
118 |         - ${KIPRJMOD} -> project directory
119 |         - Relative paths
120 |         - Absolute paths
121 |         """
122 |         # Replace environment variables
123 |         resolved = uri
124 | 
125 |         # Common KiCAD environment variables
126 |         env_vars = {
127 |             'KICAD9_FOOTPRINT_DIR': self._find_kicad_footprint_dir(),
128 |             'KICAD8_FOOTPRINT_DIR': self._find_kicad_footprint_dir(),
129 |             'KICAD_FOOTPRINT_DIR': self._find_kicad_footprint_dir(),
130 |             'KISYSMOD': self._find_kicad_footprint_dir(),
131 |         }
132 | 
133 |         # Project directory
134 |         if self.project_path:
135 |             env_vars['KIPRJMOD'] = str(self.project_path)
136 | 
137 |         # Replace environment variables
138 |         for var, value in env_vars.items():
139 |             if value:
140 |                 resolved = resolved.replace(f'${{{var}}}', value)
141 |                 resolved = resolved.replace(f'${var}', value)
142 | 
143 |         # Expand ~ to home directory
144 |         resolved = os.path.expanduser(resolved)
145 | 
146 |         # Convert to absolute path
147 |         path = Path(resolved)
148 | 
149 |         # Check if path exists
150 |         if path.exists():
151 |             return str(path)
152 |         else:
153 |             logger.debug(f"    Path does not exist: {path}")
154 |             return None
155 | 
156 |     def _find_kicad_footprint_dir(self) -> Optional[str]:
157 |         """Find KiCAD footprint directory"""
158 |         # Try common locations
159 |         possible_paths = [
160 |             "/usr/share/kicad/footprints",
161 |             "/usr/local/share/kicad/footprints",
162 |             "C:/Program Files/KiCad/9.0/share/kicad/footprints",
163 |             "C:/Program Files/KiCad/8.0/share/kicad/footprints",
164 |             "/Applications/KiCad/KiCad.app/Contents/SharedSupport/footprints",
165 |         ]
166 | 
167 |         # Also check environment variable
168 |         if 'KICAD9_FOOTPRINT_DIR' in os.environ:
169 |             possible_paths.insert(0, os.environ['KICAD9_FOOTPRINT_DIR'])
170 |         if 'KICAD8_FOOTPRINT_DIR' in os.environ:
171 |             possible_paths.insert(0, os.environ['KICAD8_FOOTPRINT_DIR'])
172 | 
173 |         for path in possible_paths:
174 |             if os.path.isdir(path):
175 |                 return path
176 | 
177 |         return None
178 | 
179 |     def list_libraries(self) -> List[str]:
180 |         """Get list of available library nicknames"""
181 |         return list(self.libraries.keys())
182 | 
183 |     def get_library_path(self, nickname: str) -> Optional[str]:
184 |         """Get filesystem path for a library nickname"""
185 |         return self.libraries.get(nickname)
186 | 
187 |     def list_footprints(self, library_nickname: str) -> List[str]:
188 |         """
189 |         List all footprints in a library
190 | 
191 |         Args:
192 |             library_nickname: Library name (e.g., "Resistor_SMD")
193 | 
194 |         Returns:
195 |             List of footprint names (without .kicad_mod extension)
196 |         """
197 |         # Check cache first
198 |         if library_nickname in self.footprint_cache:
199 |             return self.footprint_cache[library_nickname]
200 | 
201 |         library_path = self.libraries.get(library_nickname)
202 |         if not library_path:
203 |             logger.warning(f"Library not found: {library_nickname}")
204 |             return []
205 | 
206 |         try:
207 |             footprints = []
208 |             lib_dir = Path(library_path)
209 | 
210 |             # List all .kicad_mod files
211 |             for fp_file in lib_dir.glob("*.kicad_mod"):
212 |                 # Remove .kicad_mod extension
213 |                 footprint_name = fp_file.stem
214 |                 footprints.append(footprint_name)
215 | 
216 |             # Cache the results
217 |             self.footprint_cache[library_nickname] = footprints
218 |             logger.debug(f"Found {len(footprints)} footprints in {library_nickname}")
219 | 
220 |             return footprints
221 | 
222 |         except Exception as e:
223 |             logger.error(f"Error listing footprints in {library_nickname}: {e}")
224 |             return []
225 | 
226 |     def find_footprint(self, footprint_spec: str) -> Optional[Tuple[str, str]]:
227 |         """
228 |         Find a footprint by specification
229 | 
230 |         Supports multiple formats:
231 |         - "Library:Footprint" (e.g., "Resistor_SMD:R_0603_1608Metric")
232 |         - "Footprint" (searches all libraries)
233 | 
234 |         Args:
235 |             footprint_spec: Footprint specification
236 | 
237 |         Returns:
238 |             Tuple of (library_path, footprint_name) or None if not found
239 |         """
240 |         # Parse specification
241 |         if ":" in footprint_spec:
242 |             # Format: Library:Footprint
243 |             library_nickname, footprint_name = footprint_spec.split(":", 1)
244 |             library_path = self.libraries.get(library_nickname)
245 | 
246 |             if not library_path:
247 |                 logger.warning(f"Library not found: {library_nickname}")
248 |                 return None
249 | 
250 |             # Check if footprint exists
251 |             fp_file = Path(library_path) / f"{footprint_name}.kicad_mod"
252 |             if fp_file.exists():
253 |                 return (library_path, footprint_name)
254 |             else:
255 |                 logger.warning(f"Footprint not found: {footprint_spec}")
256 |                 return None
257 |         else:
258 |             # Format: Footprint (search all libraries)
259 |             footprint_name = footprint_spec
260 | 
261 |             # Search in all libraries
262 |             for library_nickname, library_path in self.libraries.items():
263 |                 fp_file = Path(library_path) / f"{footprint_name}.kicad_mod"
264 |                 if fp_file.exists():
265 |                     logger.info(f"Found footprint {footprint_name} in library {library_nickname}")
266 |                     return (library_path, footprint_name)
267 | 
268 |             logger.warning(f"Footprint not found in any library: {footprint_name}")
269 |             return None
270 | 
271 |     def search_footprints(self, pattern: str, limit: int = 20) -> List[Dict[str, str]]:
272 |         """
273 |         Search for footprints matching a pattern
274 | 
275 |         Args:
276 |             pattern: Search pattern (supports wildcards *, case-insensitive)
277 |             limit: Maximum number of results to return
278 | 
279 |         Returns:
280 |             List of dicts with 'library', 'footprint', and 'full_name' keys
281 |         """
282 |         results = []
283 |         pattern_lower = pattern.lower()
284 | 
285 |         # Convert wildcards to regex
286 |         regex_pattern = pattern_lower.replace("*", ".*")
287 |         regex = re.compile(regex_pattern)
288 | 
289 |         for library_nickname in self.libraries.keys():
290 |             footprints = self.list_footprints(library_nickname)
291 | 
292 |             for footprint in footprints:
293 |                 if regex.search(footprint.lower()):
294 |                     results.append({
295 |                         'library': library_nickname,
296 |                         'footprint': footprint,
297 |                         'full_name': f"{library_nickname}:{footprint}"
298 |                     })
299 | 
300 |                     if len(results) >= limit:
301 |                         return results
302 | 
303 |         return results
304 | 
305 |     def get_footprint_info(self, library_nickname: str, footprint_name: str) -> Optional[Dict[str, str]]:
306 |         """
307 |         Get information about a specific footprint
308 | 
309 |         Args:
310 |             library_nickname: Library name
311 |             footprint_name: Footprint name
312 | 
313 |         Returns:
314 |             Dict with footprint information or None if not found
315 |         """
316 |         library_path = self.libraries.get(library_nickname)
317 |         if not library_path:
318 |             return None
319 | 
320 |         fp_file = Path(library_path) / f"{footprint_name}.kicad_mod"
321 |         if not fp_file.exists():
322 |             return None
323 | 
324 |         return {
325 |             'library': library_nickname,
326 |             'footprint': footprint_name,
327 |             'full_name': f"{library_nickname}:{footprint_name}",
328 |             'path': str(fp_file),
329 |             'library_path': library_path
330 |         }
331 | 
332 | 
333 | class LibraryCommands:
334 |     """Command handlers for library operations"""
335 | 
336 |     def __init__(self, library_manager: Optional[LibraryManager] = None):
337 |         """Initialize with optional library manager"""
338 |         self.library_manager = library_manager or LibraryManager()
339 | 
340 |     def list_libraries(self, params: Dict) -> Dict:
341 |         """List all available footprint libraries"""
342 |         try:
343 |             libraries = self.library_manager.list_libraries()
344 |             return {
345 |                 "success": True,
346 |                 "libraries": libraries,
347 |                 "count": len(libraries)
348 |             }
349 |         except Exception as e:
350 |             logger.error(f"Error listing libraries: {e}")
351 |             return {
352 |                 "success": False,
353 |                 "message": "Failed to list libraries",
354 |                 "errorDetails": str(e)
355 |             }
356 | 
357 |     def search_footprints(self, params: Dict) -> Dict:
358 |         """Search for footprints by pattern"""
359 |         try:
360 |             pattern = params.get("pattern", "*")
361 |             limit = params.get("limit", 20)
362 | 
363 |             results = self.library_manager.search_footprints(pattern, limit)
364 | 
365 |             return {
366 |                 "success": True,
367 |                 "footprints": results,
368 |                 "count": len(results),
369 |                 "pattern": pattern
370 |             }
371 |         except Exception as e:
372 |             logger.error(f"Error searching footprints: {e}")
373 |             return {
374 |                 "success": False,
375 |                 "message": "Failed to search footprints",
376 |                 "errorDetails": str(e)
377 |             }
378 | 
379 |     def list_library_footprints(self, params: Dict) -> Dict:
380 |         """List all footprints in a specific library"""
381 |         try:
382 |             library = params.get("library")
383 |             if not library:
384 |                 return {
385 |                     "success": False,
386 |                     "message": "Missing library parameter"
387 |                 }
388 | 
389 |             footprints = self.library_manager.list_footprints(library)
390 | 
391 |             return {
392 |                 "success": True,
393 |                 "library": library,
394 |                 "footprints": footprints,
395 |                 "count": len(footprints)
396 |             }
397 |         except Exception as e:
398 |             logger.error(f"Error listing library footprints: {e}")
399 |             return {
400 |                 "success": False,
401 |                 "message": "Failed to list library footprints",
402 |                 "errorDetails": str(e)
403 |             }
404 | 
405 |     def get_footprint_info(self, params: Dict) -> Dict:
406 |         """Get information about a specific footprint"""
407 |         try:
408 |             footprint_spec = params.get("footprint")
409 |             if not footprint_spec:
410 |                 return {
411 |                     "success": False,
412 |                     "message": "Missing footprint parameter"
413 |                 }
414 | 
415 |             # Try to find the footprint
416 |             result = self.library_manager.find_footprint(footprint_spec)
417 | 
418 |             if result:
419 |                 library_path, footprint_name = result
420 |                 # Extract library nickname from path
421 |                 library_nickname = None
422 |                 for nick, path in self.library_manager.libraries.items():
423 |                     if path == library_path:
424 |                         library_nickname = nick
425 |                         break
426 | 
427 |                 info = {
428 |                     "library": library_nickname,
429 |                     "footprint": footprint_name,
430 |                     "full_name": f"{library_nickname}:{footprint_name}",
431 |                     "library_path": library_path
432 |                 }
433 | 
434 |                 return {
435 |                     "success": True,
436 |                     "footprint_info": info
437 |                 }
438 |             else:
439 |                 return {
440 |                     "success": False,
441 |                     "message": f"Footprint not found: {footprint_spec}"
442 |                 }
443 | 
444 |         except Exception as e:
445 |             logger.error(f"Error getting footprint info: {e}")
446 |             return {
447 |                 "success": False,
448 |                 "message": "Failed to get footprint info",
449 |                 "errorDetails": str(e)
450 |             }
451 | 
```

--------------------------------------------------------------------------------
/src/server.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * KiCAD MCP Server implementation
  3 |  */
  4 | 
  5 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
  6 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
  7 | import express from 'express';
  8 | import { spawn, exec, ChildProcess } from 'child_process';
  9 | import { existsSync } from 'fs';
 10 | import { join, dirname } from 'path';
 11 | import { logger } from './logger.js';
 12 | 
 13 | // Import tool registration functions
 14 | import { registerProjectTools } from './tools/project.js';
 15 | import { registerBoardTools } from './tools/board.js';
 16 | import { registerComponentTools } from './tools/component.js';
 17 | import { registerRoutingTools } from './tools/routing.js';
 18 | import { registerDesignRuleTools } from './tools/design-rules.js';
 19 | import { registerExportTools } from './tools/export.js';
 20 | import { registerSchematicTools } from './tools/schematic.js';
 21 | import { registerLibraryTools } from './tools/library.js';
 22 | import { registerUITools } from './tools/ui.js';
 23 | 
 24 | // Import resource registration functions
 25 | import { registerProjectResources } from './resources/project.js';
 26 | import { registerBoardResources } from './resources/board.js';
 27 | import { registerComponentResources } from './resources/component.js';
 28 | import { registerLibraryResources } from './resources/library.js';
 29 | 
 30 | // Import prompt registration functions
 31 | import { registerComponentPrompts } from './prompts/component.js';
 32 | import { registerRoutingPrompts } from './prompts/routing.js';
 33 | import { registerDesignPrompts } from './prompts/design.js';
 34 | 
 35 | /**
 36 |  * Find the Python executable to use
 37 |  * Prioritizes virtual environment if available, falls back to system Python
 38 |  */
 39 | function findPythonExecutable(scriptPath: string): string {
 40 |   const isWindows = process.platform === 'win32';
 41 | 
 42 |   // Get the project root (parent of the python/ directory)
 43 |   const projectRoot = dirname(dirname(scriptPath));
 44 | 
 45 |   // Check for virtual environment
 46 |   const venvPaths = [
 47 |     join(projectRoot, 'venv', isWindows ? 'Scripts' : 'bin', isWindows ? 'python.exe' : 'python'),
 48 |     join(projectRoot, '.venv', isWindows ? 'Scripts' : 'bin', isWindows ? 'python.exe' : 'python'),
 49 |   ];
 50 | 
 51 |   for (const venvPath of venvPaths) {
 52 |     if (existsSync(venvPath)) {
 53 |       logger.info(`Found virtual environment Python at: ${venvPath}`);
 54 |       return venvPath;
 55 |     }
 56 |   }
 57 | 
 58 |   // Fall back to system Python or environment-specified Python
 59 |   if (isWindows && process.env.KICAD_PYTHON) {
 60 |     // Allow override via KICAD_PYTHON environment variable
 61 |     return process.env.KICAD_PYTHON;
 62 |   } else if (isWindows && process.env.PYTHONPATH?.includes('KiCad')) {
 63 |     // Windows: Try KiCAD's bundled Python
 64 |     const kicadPython = 'C:\\Program Files\\KiCad\\9.0\\bin\\python.exe';
 65 |     if (existsSync(kicadPython)) {
 66 |       return kicadPython;
 67 |     }
 68 |   }
 69 | 
 70 |   // Default to system Python
 71 |   logger.info('Using system Python (no venv found)');
 72 |   return isWindows ? 'python.exe' : 'python3';
 73 | }
 74 | 
 75 | /**
 76 |  * KiCAD MCP Server class
 77 |  */
 78 | export class KiCADMcpServer {
 79 |   private server: McpServer;
 80 |   private pythonProcess: ChildProcess | null = null;
 81 |   private kicadScriptPath: string;
 82 |   private stdioTransport!: StdioServerTransport;
 83 |   private requestQueue: Array<{ request: any, resolve: Function, reject: Function }> = [];
 84 |   private processingRequest = false;
 85 |   private responseBuffer: string = '';
 86 |   private currentRequestHandler: { resolve: Function, reject: Function, timeoutHandle: NodeJS.Timeout } | null = null;
 87 |   
 88 |   /**
 89 |    * Constructor for the KiCAD MCP Server
 90 |    * @param kicadScriptPath Path to the Python KiCAD interface script
 91 |    * @param logLevel Log level for the server
 92 |    */
 93 |   constructor(
 94 |     kicadScriptPath: string,
 95 |     logLevel: 'error' | 'warn' | 'info' | 'debug' = 'info'
 96 |   ) {
 97 |     // Set up the logger
 98 |     logger.setLogLevel(logLevel);
 99 |     
100 |     // Check if KiCAD script exists
101 |     this.kicadScriptPath = kicadScriptPath;
102 |     if (!existsSync(this.kicadScriptPath)) {
103 |       throw new Error(`KiCAD interface script not found: ${this.kicadScriptPath}`);
104 |     }
105 |     
106 |     // Initialize the MCP server
107 |     this.server = new McpServer({
108 |       name: 'kicad-mcp-server',
109 |       version: '1.0.0',
110 |       description: 'MCP server for KiCAD PCB design operations'
111 |     });
112 |     
113 |     // Initialize STDIO transport
114 |     this.stdioTransport = new StdioServerTransport();
115 |     logger.info('Using STDIO transport for local communication');
116 |     
117 |     // Register tools, resources, and prompts
118 |     this.registerAll();
119 |   }
120 |   
121 |   /**
122 |    * Register all tools, resources, and prompts
123 |    */
124 |   private registerAll(): void {
125 |     logger.info('Registering KiCAD tools, resources, and prompts...');
126 |     
127 |     // Register all tools
128 |     registerProjectTools(this.server, this.callKicadScript.bind(this));
129 |     registerBoardTools(this.server, this.callKicadScript.bind(this));
130 |     registerComponentTools(this.server, this.callKicadScript.bind(this));
131 |     registerRoutingTools(this.server, this.callKicadScript.bind(this));
132 |     registerDesignRuleTools(this.server, this.callKicadScript.bind(this));
133 |     registerExportTools(this.server, this.callKicadScript.bind(this));
134 |     registerSchematicTools(this.server, this.callKicadScript.bind(this));
135 |     registerLibraryTools(this.server, this.callKicadScript.bind(this));
136 |     registerUITools(this.server, this.callKicadScript.bind(this));
137 |     
138 |     // Register all resources
139 |     registerProjectResources(this.server, this.callKicadScript.bind(this));
140 |     registerBoardResources(this.server, this.callKicadScript.bind(this));
141 |     registerComponentResources(this.server, this.callKicadScript.bind(this));
142 |     registerLibraryResources(this.server, this.callKicadScript.bind(this));
143 |     
144 |     // Register all prompts
145 |     registerComponentPrompts(this.server);
146 |     registerRoutingPrompts(this.server);
147 |     registerDesignPrompts(this.server);
148 |     
149 |     logger.info('All KiCAD tools, resources, and prompts registered');
150 |   }
151 |   
152 |   /**
153 |    * Validate prerequisites before starting the server
154 |    */
155 |   private async validatePrerequisites(pythonExe: string): Promise<boolean> {
156 |     const isWindows = process.platform === 'win32';
157 |     const errors: string[] = [];
158 | 
159 |     // Check if Python executable exists
160 |     if (!existsSync(pythonExe)) {
161 |       errors.push(`Python executable not found: ${pythonExe}`);
162 | 
163 |       if (isWindows) {
164 |         errors.push('Windows: Install KiCAD 9.0+ from https://www.kicad.org/download/windows/');
165 |         errors.push('Or run: .\\setup-windows.ps1 for automatic configuration');
166 |       }
167 |     }
168 | 
169 |     // Check if kicad_interface.py exists
170 |     if (!existsSync(this.kicadScriptPath)) {
171 |       errors.push(`KiCAD interface script not found: ${this.kicadScriptPath}`);
172 |     }
173 | 
174 |     // Check if dist/index.js exists (if running from compiled code)
175 |     const distPath = join(dirname(dirname(this.kicadScriptPath)), 'dist', 'index.js');
176 |     if (!existsSync(distPath)) {
177 |       errors.push('Project not built. Run: npm run build');
178 |     }
179 | 
180 |     // Try to test pcbnew import (quick validation)
181 |     if (existsSync(pythonExe) && existsSync(this.kicadScriptPath)) {
182 |       logger.info('Validating pcbnew module access...');
183 | 
184 |       const testCommand = `"${pythonExe}" -c "import pcbnew; print('OK')"`;
185 | 
186 |       try {
187 |         const { stdout, stderr } = await new Promise<{stdout: string, stderr: string}>((resolve, reject) => {
188 |           exec(testCommand, {
189 |             timeout: 5000,
190 |             env: { ...process.env }
191 |           }, (error: any, stdout: string, stderr: string) => {
192 |             if (error) {
193 |               reject(error);
194 |             } else {
195 |               resolve({ stdout, stderr });
196 |             }
197 |           });
198 |         });
199 | 
200 |         if (!stdout.includes('OK')) {
201 |           errors.push('pcbnew module import test failed');
202 |           errors.push(`Output: ${stdout}`);
203 |           errors.push(`Errors: ${stderr}`);
204 | 
205 |           if (isWindows) {
206 |             errors.push('');
207 |             errors.push('Windows troubleshooting:');
208 |             errors.push('1. Set PYTHONPATH=C:\\Program Files\\KiCad\\9.0\\lib\\python3\\dist-packages');
209 |             errors.push('2. Test: "C:\\Program Files\\KiCad\\9.0\\bin\\python.exe" -c "import pcbnew"');
210 |             errors.push('3. Run: .\\setup-windows.ps1 for automatic fix');
211 |             errors.push('4. See: docs/WINDOWS_TROUBLESHOOTING.md');
212 |           }
213 |         } else {
214 |           logger.info('✓ pcbnew module validated successfully');
215 |         }
216 |       } catch (error: any) {
217 |         errors.push(`pcbnew validation failed: ${error.message}`);
218 | 
219 |         if (isWindows) {
220 |           errors.push('');
221 |           errors.push('This usually means:');
222 |           errors.push('- KiCAD is not installed');
223 |           errors.push('- PYTHONPATH is incorrect');
224 |           errors.push('- Python cannot find pcbnew module');
225 |           errors.push('');
226 |           errors.push('Quick fix: Run .\\setup-windows.ps1');
227 |         }
228 |       }
229 |     }
230 | 
231 |     // Log all errors
232 |     if (errors.length > 0) {
233 |       logger.error('='.repeat(70));
234 |       logger.error('STARTUP VALIDATION FAILED');
235 |       logger.error('='.repeat(70));
236 |       errors.forEach(err => logger.error(err));
237 |       logger.error('='.repeat(70));
238 | 
239 |       // Also write to stderr for Claude Desktop to capture
240 |       process.stderr.write('\n' + '='.repeat(70) + '\n');
241 |       process.stderr.write('KiCAD MCP Server - Startup Validation Failed\n');
242 |       process.stderr.write('='.repeat(70) + '\n');
243 |       errors.forEach(err => process.stderr.write(err + '\n'));
244 |       process.stderr.write('='.repeat(70) + '\n\n');
245 | 
246 |       return false;
247 |     }
248 | 
249 |     return true;
250 |   }
251 | 
252 |   /**
253 |    * Start the MCP server and the Python KiCAD interface
254 |    */
255 |   async start(): Promise<void> {
256 |     try {
257 |       logger.info('Starting KiCAD MCP server...');
258 | 
259 |       // Start the Python process for KiCAD scripting
260 |       logger.info(`Starting Python process with script: ${this.kicadScriptPath}`);
261 |       const pythonExe = findPythonExecutable(this.kicadScriptPath);
262 | 
263 |       logger.info(`Using Python executable: ${pythonExe}`);
264 | 
265 |       // Validate prerequisites
266 |       const isValid = await this.validatePrerequisites(pythonExe);
267 |       if (!isValid) {
268 |         throw new Error('Prerequisites validation failed. See logs above for details.');
269 |       }
270 |       this.pythonProcess = spawn(pythonExe, [this.kicadScriptPath], {
271 |         stdio: ['pipe', 'pipe', 'pipe'],
272 |         env: {
273 |           ...process.env,
274 |           PYTHONPATH: process.env.PYTHONPATH || 'C:/Program Files/KiCad/9.0/lib/python3/dist-packages'
275 |         }
276 |       });
277 |       
278 |       // Listen for process exit
279 |       this.pythonProcess.on('exit', (code, signal) => {
280 |         logger.warn(`Python process exited with code ${code} and signal ${signal}`);
281 |         this.pythonProcess = null;
282 |       });
283 |       
284 |       // Listen for process errors
285 |       this.pythonProcess.on('error', (err) => {
286 |         logger.error(`Python process error: ${err.message}`);
287 |       });
288 |       
289 |       // Set up error logging for stderr
290 |       if (this.pythonProcess.stderr) {
291 |         this.pythonProcess.stderr.on('data', (data: Buffer) => {
292 |           logger.error(`Python stderr: ${data.toString()}`);
293 |         });
294 |       }
295 | 
296 |       // Set up persistent stdout handler (instead of adding/removing per request)
297 |       if (this.pythonProcess.stdout) {
298 |         this.pythonProcess.stdout.on('data', (data: Buffer) => {
299 |           this.handlePythonResponse(data);
300 |         });
301 |       }
302 | 
303 |       // Connect server to STDIO transport
304 |       logger.info('Connecting MCP server to STDIO transport...');
305 |       try {
306 |         await this.server.connect(this.stdioTransport);
307 |         logger.info('Successfully connected to STDIO transport');
308 |       } catch (error) {
309 |         logger.error(`Failed to connect to STDIO transport: ${error}`);
310 |         throw error;
311 |       }
312 |       
313 |       // Write a ready message to stderr (for debugging)
314 |       process.stderr.write('KiCAD MCP SERVER READY\n');
315 |       
316 |       logger.info('KiCAD MCP server started and ready');
317 |     } catch (error) {
318 |       logger.error(`Failed to start KiCAD MCP server: ${error}`);
319 |       throw error;
320 |     }
321 |   }
322 |   
323 |   /**
324 |    * Stop the MCP server and clean up resources
325 |    */
326 |   async stop(): Promise<void> {
327 |     logger.info('Stopping KiCAD MCP server...');
328 |     
329 |     // Kill the Python process if it's running
330 |     if (this.pythonProcess) {
331 |       this.pythonProcess.kill();
332 |       this.pythonProcess = null;
333 |     }
334 |     
335 |     logger.info('KiCAD MCP server stopped');
336 |   }
337 |   
338 |   /**
339 |    * Call the KiCAD scripting interface to execute commands
340 |    *
341 |    * @param command The command to execute
342 |    * @param params The parameters for the command
343 |    * @returns The result of the command execution
344 |    */
345 |   private async callKicadScript(command: string, params: any): Promise<any> {
346 |     return new Promise((resolve, reject) => {
347 |       // Check if Python process is running
348 |       if (!this.pythonProcess) {
349 |         logger.error('Python process is not running');
350 |         reject(new Error("Python process for KiCAD scripting is not running"));
351 |         return;
352 |       }
353 | 
354 |       // Determine timeout based on command type
355 |       // DRC and export operations need longer timeouts for large boards
356 |       let commandTimeout = 30000; // Default 30 seconds
357 |       const longRunningCommands = ['run_drc', 'export_gerber', 'export_pdf', 'export_3d'];
358 |       if (longRunningCommands.includes(command)) {
359 |         commandTimeout = 600000; // 10 minutes for long operations
360 |         logger.info(`Using extended timeout (${commandTimeout/1000}s) for command: ${command}`);
361 |       }
362 | 
363 |       // Add request to queue with timeout info
364 |       this.requestQueue.push({
365 |         request: { command, params, timeout: commandTimeout },
366 |         resolve,
367 |         reject
368 |       });
369 | 
370 |       // Process the queue if not already processing
371 |       if (!this.processingRequest) {
372 |         this.processNextRequest();
373 |       }
374 |     });
375 |   }
376 |   
377 |   /**
378 |    * Handle incoming data from Python process stdout
379 |    * This is a persistent handler that processes all responses
380 |    */
381 |   private handlePythonResponse(data: Buffer): void {
382 |     const chunk = data.toString();
383 |     logger.debug(`Received data chunk: ${chunk.length} bytes`);
384 |     this.responseBuffer += chunk;
385 | 
386 |     // Try to parse complete JSON responses (may have multiple or partial)
387 |     this.tryParseResponse();
388 |   }
389 | 
390 |   /**
391 |    * Try to parse a complete JSON response from the buffer
392 |    */
393 |   private tryParseResponse(): void {
394 |     if (!this.currentRequestHandler) {
395 |       // No pending request, clear buffer if it has data (shouldn't happen)
396 |       if (this.responseBuffer.trim()) {
397 |         logger.warn(`Received data with no pending request: ${this.responseBuffer.substring(0, 100)}...`);
398 |         this.responseBuffer = '';
399 |       }
400 |       return;
401 |     }
402 | 
403 |     try {
404 |       // Try to parse the response as JSON
405 |       const result = JSON.parse(this.responseBuffer);
406 | 
407 |       // If we get here, we have a valid JSON response
408 |       logger.debug(`Completed KiCAD command with result: ${result.success ? 'success' : 'failure'}`);
409 | 
410 |       // Clear the timeout since we got a response
411 |       if (this.currentRequestHandler.timeoutHandle) {
412 |         clearTimeout(this.currentRequestHandler.timeoutHandle);
413 |       }
414 | 
415 |       // Get the handler before clearing
416 |       const handler = this.currentRequestHandler;
417 | 
418 |       // Clear state
419 |       this.responseBuffer = '';
420 |       this.currentRequestHandler = null;
421 |       this.processingRequest = false;
422 | 
423 |       // Resolve the promise with the result
424 |       handler.resolve(result);
425 | 
426 |       // Process next request if any
427 |       setTimeout(() => this.processNextRequest(), 0);
428 | 
429 |     } catch (e) {
430 |       // Not a complete JSON yet, keep collecting data
431 |       // This is normal for large responses that come in chunks
432 |     }
433 |   }
434 | 
435 |   /**
436 |    * Process the next request in the queue
437 |    */
438 |   private processNextRequest(): void {
439 |     // If no more requests or already processing, return
440 |     if (this.requestQueue.length === 0 || this.processingRequest) {
441 |       return;
442 |     }
443 | 
444 |     // Set processing flag
445 |     this.processingRequest = true;
446 | 
447 |     // Get the next request
448 |     const { request, resolve, reject } = this.requestQueue.shift()!;
449 | 
450 |     try {
451 |       logger.debug(`Processing KiCAD command: ${request.command}`);
452 | 
453 |       // Format the command and parameters as JSON
454 |       const requestStr = JSON.stringify(request);
455 | 
456 |       // Clear response buffer for new request
457 |       this.responseBuffer = '';
458 | 
459 |       // Set a timeout (use command-specific timeout or default)
460 |       const timeoutDuration = request.timeout || 30000;
461 |       const timeoutHandle = setTimeout(() => {
462 |         logger.error(`Command timeout after ${timeoutDuration/1000}s: ${request.command}`);
463 |         logger.error(`Buffer contents: ${this.responseBuffer.substring(0, 200)}...`);
464 | 
465 |         // Clear state
466 |         this.responseBuffer = '';
467 |         this.currentRequestHandler = null;
468 |         this.processingRequest = false;
469 | 
470 |         // Reject the promise
471 |         reject(new Error(`Command timeout after ${timeoutDuration/1000}s: ${request.command}`));
472 | 
473 |         // Process next request
474 |         setTimeout(() => this.processNextRequest(), 0);
475 |       }, timeoutDuration);
476 | 
477 |       // Store the current request handler
478 |       this.currentRequestHandler = { resolve, reject, timeoutHandle };
479 | 
480 |       // Write the request to the Python process
481 |       logger.debug(`Sending request: ${requestStr}`);
482 |       this.pythonProcess?.stdin?.write(requestStr + '\n');
483 |     } catch (error) {
484 |       logger.error(`Error processing request: ${error}`);
485 | 
486 |       // Reset processing flag
487 |       this.processingRequest = false;
488 |       this.currentRequestHandler = null;
489 | 
490 |       // Process next request
491 |       setTimeout(() => this.processNextRequest(), 0);
492 | 
493 |       // Reject the promise
494 |       reject(error);
495 |     }
496 |   }
497 | }
498 | 
```

--------------------------------------------------------------------------------
/python/commands/board/outline.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Board outline command implementations for KiCAD interface
  3 | """
  4 | 
  5 | import pcbnew
  6 | import logging
  7 | import math
  8 | from typing import Dict, Any, Optional
  9 | 
 10 | logger = logging.getLogger('kicad_interface')
 11 | 
 12 | class BoardOutlineCommands:
 13 |     """Handles board outline operations"""
 14 | 
 15 |     def __init__(self, board: Optional[pcbnew.BOARD] = None):
 16 |         """Initialize with optional board instance"""
 17 |         self.board = board
 18 | 
 19 |     def add_board_outline(self, params: Dict[str, Any]) -> Dict[str, Any]:
 20 |         """Add a board outline to the PCB"""
 21 |         try:
 22 |             if not self.board:
 23 |                 return {
 24 |                     "success": False,
 25 |                     "message": "No board is loaded",
 26 |                     "errorDetails": "Load or create a board first"
 27 |                 }
 28 | 
 29 |             shape = params.get("shape", "rectangle")
 30 |             width = params.get("width")
 31 |             height = params.get("height")
 32 |             center_x = params.get("centerX", 0)
 33 |             center_y = params.get("centerY", 0)
 34 |             radius = params.get("radius")
 35 |             corner_radius = params.get("cornerRadius", 0)
 36 |             points = params.get("points", [])
 37 |             unit = params.get("unit", "mm")
 38 | 
 39 |             if shape not in ["rectangle", "circle", "polygon", "rounded_rectangle"]:
 40 |                 return {
 41 |                     "success": False,
 42 |                     "message": "Invalid shape",
 43 |                     "errorDetails": f"Shape '{shape}' not supported"
 44 |                 }
 45 | 
 46 |             # Convert to internal units (nanometers)
 47 |             scale = 1000000 if unit == "mm" else 25400000  # mm or inch to nm
 48 |             
 49 |             # Create drawing for edge cuts
 50 |             edge_layer = self.board.GetLayerID("Edge.Cuts")
 51 |             
 52 |             if shape == "rectangle":
 53 |                 if width is None or height is None:
 54 |                     return {
 55 |                         "success": False,
 56 |                         "message": "Missing dimensions",
 57 |                         "errorDetails": "Both width and height are required for rectangle"
 58 |                     }
 59 | 
 60 |                 width_nm = int(width * scale)
 61 |                 height_nm = int(height * scale)
 62 |                 center_x_nm = int(center_x * scale)
 63 |                 center_y_nm = int(center_y * scale)
 64 |                 
 65 |                 # Create rectangle
 66 |                 top_left = pcbnew.VECTOR2I(center_x_nm - width_nm // 2, center_y_nm - height_nm // 2)
 67 |                 top_right = pcbnew.VECTOR2I(center_x_nm + width_nm // 2, center_y_nm - height_nm // 2)
 68 |                 bottom_right = pcbnew.VECTOR2I(center_x_nm + width_nm // 2, center_y_nm + height_nm // 2)
 69 |                 bottom_left = pcbnew.VECTOR2I(center_x_nm - width_nm // 2, center_y_nm + height_nm // 2)
 70 |                 
 71 |                 # Add lines for rectangle
 72 |                 self._add_edge_line(top_left, top_right, edge_layer)
 73 |                 self._add_edge_line(top_right, bottom_right, edge_layer)
 74 |                 self._add_edge_line(bottom_right, bottom_left, edge_layer)
 75 |                 self._add_edge_line(bottom_left, top_left, edge_layer)
 76 |                 
 77 |             elif shape == "rounded_rectangle":
 78 |                 if width is None or height is None:
 79 |                     return {
 80 |                         "success": False,
 81 |                         "message": "Missing dimensions",
 82 |                         "errorDetails": "Both width and height are required for rounded rectangle"
 83 |                     }
 84 | 
 85 |                 width_nm = int(width * scale)
 86 |                 height_nm = int(height * scale)
 87 |                 center_x_nm = int(center_x * scale)
 88 |                 center_y_nm = int(center_y * scale)
 89 |                 corner_radius_nm = int(corner_radius * scale)
 90 |                 
 91 |                 # Create rounded rectangle
 92 |                 self._add_rounded_rect(
 93 |                     center_x_nm, center_y_nm, 
 94 |                     width_nm, height_nm, 
 95 |                     corner_radius_nm, edge_layer
 96 |                 )
 97 |                 
 98 |             elif shape == "circle":
 99 |                 if radius is None:
100 |                     return {
101 |                         "success": False,
102 |                         "message": "Missing radius",
103 |                         "errorDetails": "Radius is required for circle"
104 |                     }
105 | 
106 |                 center_x_nm = int(center_x * scale)
107 |                 center_y_nm = int(center_y * scale)
108 |                 radius_nm = int(radius * scale)
109 |                 
110 |                 # Create circle
111 |                 circle = pcbnew.PCB_SHAPE(self.board)
112 |                 circle.SetShape(pcbnew.SHAPE_T_CIRCLE)
113 |                 circle.SetCenter(pcbnew.VECTOR2I(center_x_nm, center_y_nm))
114 |                 circle.SetEnd(pcbnew.VECTOR2I(center_x_nm + radius_nm, center_y_nm))
115 |                 circle.SetLayer(edge_layer)
116 |                 circle.SetWidth(0)  # Zero width for edge cuts
117 |                 self.board.Add(circle)
118 | 
119 |             elif shape == "polygon":
120 |                 if not points or len(points) < 3:
121 |                     return {
122 |                         "success": False,
123 |                         "message": "Missing points",
124 |                         "errorDetails": "At least 3 points are required for polygon"
125 |                     }
126 | 
127 |                 # Convert points to nm
128 |                 polygon_points = []
129 |                 for point in points:
130 |                     x_nm = int(point["x"] * scale)
131 |                     y_nm = int(point["y"] * scale)
132 |                     polygon_points.append(pcbnew.VECTOR2I(x_nm, y_nm))
133 |                 
134 |                 # Add lines for polygon
135 |                 for i in range(len(polygon_points)):
136 |                     self._add_edge_line(
137 |                         polygon_points[i], 
138 |                         polygon_points[(i + 1) % len(polygon_points)], 
139 |                         edge_layer
140 |                     )
141 | 
142 |             return {
143 |                 "success": True,
144 |                 "message": f"Added board outline: {shape}",
145 |                 "outline": {
146 |                     "shape": shape,
147 |                     "width": width,
148 |                     "height": height,
149 |                     "center": {"x": center_x, "y": center_y, "unit": unit},
150 |                     "radius": radius,
151 |                     "cornerRadius": corner_radius,
152 |                     "points": points
153 |                 }
154 |             }
155 | 
156 |         except Exception as e:
157 |             logger.error(f"Error adding board outline: {str(e)}")
158 |             return {
159 |                 "success": False,
160 |                 "message": "Failed to add board outline",
161 |                 "errorDetails": str(e)
162 |             }
163 | 
164 |     def add_mounting_hole(self, params: Dict[str, Any]) -> Dict[str, Any]:
165 |         """Add a mounting hole to the PCB"""
166 |         try:
167 |             if not self.board:
168 |                 return {
169 |                     "success": False,
170 |                     "message": "No board is loaded",
171 |                     "errorDetails": "Load or create a board first"
172 |                 }
173 | 
174 |             position = params.get("position")
175 |             diameter = params.get("diameter")
176 |             pad_diameter = params.get("padDiameter")
177 |             plated = params.get("plated", False)
178 | 
179 |             if not position or not diameter:
180 |                 return {
181 |                     "success": False,
182 |                     "message": "Missing parameters",
183 |                     "errorDetails": "position and diameter are required"
184 |                 }
185 | 
186 |             # Convert to internal units (nanometers)
187 |             scale = 1000000 if position.get("unit", "mm") == "mm" else 25400000  # mm or inch to nm
188 |             x_nm = int(position["x"] * scale)
189 |             y_nm = int(position["y"] * scale)
190 |             diameter_nm = int(diameter * scale)
191 |             pad_diameter_nm = int(pad_diameter * scale) if pad_diameter else diameter_nm + scale  # 1mm larger by default
192 | 
193 |             # Create footprint for mounting hole
194 |             module = pcbnew.FOOTPRINT(self.board)
195 |             module.SetReference(f"MH")
196 |             module.SetValue(f"MountingHole_{diameter}mm")
197 |             
198 |             # Create the pad for the hole
199 |             pad = pcbnew.PAD(module)
200 |             pad.SetNumber(1)
201 |             pad.SetShape(pcbnew.PAD_SHAPE_CIRCLE)
202 |             pad.SetAttribute(pcbnew.PAD_ATTRIB_PTH if plated else pcbnew.PAD_ATTRIB_NPTH)
203 |             pad.SetSize(pcbnew.VECTOR2I(pad_diameter_nm, pad_diameter_nm))
204 |             pad.SetDrillSize(pcbnew.VECTOR2I(diameter_nm, diameter_nm))
205 |             pad.SetPosition(pcbnew.VECTOR2I(0, 0))  # Position relative to module
206 |             module.Add(pad)
207 |             
208 |             # Position the mounting hole
209 |             module.SetPosition(pcbnew.VECTOR2I(x_nm, y_nm))
210 |             
211 |             # Add to board
212 |             self.board.Add(module)
213 | 
214 |             return {
215 |                 "success": True,
216 |                 "message": "Added mounting hole",
217 |                 "mountingHole": {
218 |                     "position": position,
219 |                     "diameter": diameter,
220 |                     "padDiameter": pad_diameter or diameter + 1,
221 |                     "plated": plated
222 |                 }
223 |             }
224 | 
225 |         except Exception as e:
226 |             logger.error(f"Error adding mounting hole: {str(e)}")
227 |             return {
228 |                 "success": False,
229 |                 "message": "Failed to add mounting hole",
230 |                 "errorDetails": str(e)
231 |             }
232 | 
233 |     def add_text(self, params: Dict[str, Any]) -> Dict[str, Any]:
234 |         """Add text annotation to the PCB"""
235 |         try:
236 |             if not self.board:
237 |                 return {
238 |                     "success": False,
239 |                     "message": "No board is loaded",
240 |                     "errorDetails": "Load or create a board first"
241 |                 }
242 | 
243 |             text = params.get("text")
244 |             position = params.get("position")
245 |             layer = params.get("layer", "F.SilkS")
246 |             size = params.get("size", 1.0)
247 |             thickness = params.get("thickness", 0.15)
248 |             rotation = params.get("rotation", 0)
249 |             mirror = params.get("mirror", False)
250 | 
251 |             if not text or not position:
252 |                 return {
253 |                     "success": False,
254 |                     "message": "Missing parameters",
255 |                     "errorDetails": "text and position are required"
256 |                 }
257 | 
258 |             # Convert to internal units (nanometers)
259 |             scale = 1000000 if position.get("unit", "mm") == "mm" else 25400000  # mm or inch to nm
260 |             x_nm = int(position["x"] * scale)
261 |             y_nm = int(position["y"] * scale)
262 |             size_nm = int(size * scale)
263 |             thickness_nm = int(thickness * scale)
264 | 
265 |             # Get layer ID
266 |             layer_id = self.board.GetLayerID(layer)
267 |             if layer_id < 0:
268 |                 return {
269 |                     "success": False,
270 |                     "message": "Invalid layer",
271 |                     "errorDetails": f"Layer '{layer}' does not exist"
272 |                 }
273 | 
274 |             # Create text
275 |             pcb_text = pcbnew.PCB_TEXT(self.board)
276 |             pcb_text.SetText(text)
277 |             pcb_text.SetPosition(pcbnew.VECTOR2I(x_nm, y_nm))
278 |             pcb_text.SetLayer(layer_id)
279 |             pcb_text.SetTextSize(pcbnew.VECTOR2I(size_nm, size_nm))
280 |             pcb_text.SetTextThickness(thickness_nm)
281 | 
282 |             # Set rotation angle - KiCAD 9.0 uses EDA_ANGLE
283 |             try:
284 |                 # Try KiCAD 9.0+ API (EDA_ANGLE)
285 |                 angle = pcbnew.EDA_ANGLE(rotation, pcbnew.DEGREES_T)
286 |                 pcb_text.SetTextAngle(angle)
287 |             except (AttributeError, TypeError):
288 |                 # Fall back to older API (decidegrees as integer)
289 |                 pcb_text.SetTextAngle(int(rotation * 10))
290 | 
291 |             pcb_text.SetMirrored(mirror)
292 |             
293 |             # Add to board
294 |             self.board.Add(pcb_text)
295 | 
296 |             return {
297 |                 "success": True,
298 |                 "message": "Added text annotation",
299 |                 "text": {
300 |                     "text": text,
301 |                     "position": position,
302 |                     "layer": layer,
303 |                     "size": size,
304 |                     "thickness": thickness,
305 |                     "rotation": rotation,
306 |                     "mirror": mirror
307 |                 }
308 |             }
309 | 
310 |         except Exception as e:
311 |             logger.error(f"Error adding text: {str(e)}")
312 |             return {
313 |                 "success": False,
314 |                 "message": "Failed to add text",
315 |                 "errorDetails": str(e)
316 |             }
317 | 
318 |     def _add_edge_line(self, start: pcbnew.VECTOR2I, end: pcbnew.VECTOR2I, layer: int) -> None:
319 |         """Add a line to the edge cuts layer"""
320 |         line = pcbnew.PCB_SHAPE(self.board)
321 |         line.SetShape(pcbnew.SHAPE_T_SEGMENT)
322 |         line.SetStart(start)
323 |         line.SetEnd(end)
324 |         line.SetLayer(layer)
325 |         line.SetWidth(0)  # Zero width for edge cuts
326 |         self.board.Add(line)
327 | 
328 |     def _add_rounded_rect(self, center_x_nm: int, center_y_nm: int, 
329 |                          width_nm: int, height_nm: int, 
330 |                          radius_nm: int, layer: int) -> None:
331 |         """Add a rounded rectangle to the edge cuts layer"""
332 |         if radius_nm <= 0:
333 |             # If no radius, create regular rectangle
334 |             top_left = pcbnew.VECTOR2I(center_x_nm - width_nm // 2, center_y_nm - height_nm // 2)
335 |             top_right = pcbnew.VECTOR2I(center_x_nm + width_nm // 2, center_y_nm - height_nm // 2)
336 |             bottom_right = pcbnew.VECTOR2I(center_x_nm + width_nm // 2, center_y_nm + height_nm // 2)
337 |             bottom_left = pcbnew.VECTOR2I(center_x_nm - width_nm // 2, center_y_nm + height_nm // 2)
338 |             
339 |             self._add_edge_line(top_left, top_right, layer)
340 |             self._add_edge_line(top_right, bottom_right, layer)
341 |             self._add_edge_line(bottom_right, bottom_left, layer)
342 |             self._add_edge_line(bottom_left, top_left, layer)
343 |             return
344 |         
345 |         # Calculate corner centers
346 |         half_width = width_nm // 2
347 |         half_height = height_nm // 2
348 |         
349 |         # Ensure radius is not larger than half the smallest dimension
350 |         max_radius = min(half_width, half_height)
351 |         if radius_nm > max_radius:
352 |             radius_nm = max_radius
353 |         
354 |         # Calculate corner centers
355 |         top_left_center = pcbnew.VECTOR2I(
356 |             center_x_nm - half_width + radius_nm,
357 |             center_y_nm - half_height + radius_nm
358 |         )
359 |         top_right_center = pcbnew.VECTOR2I(
360 |             center_x_nm + half_width - radius_nm,
361 |             center_y_nm - half_height + radius_nm
362 |         )
363 |         bottom_right_center = pcbnew.VECTOR2I(
364 |             center_x_nm + half_width - radius_nm,
365 |             center_y_nm + half_height - radius_nm
366 |         )
367 |         bottom_left_center = pcbnew.VECTOR2I(
368 |             center_x_nm - half_width + radius_nm,
369 |             center_y_nm + half_height - radius_nm
370 |         )
371 |         
372 |         # Add arcs for corners
373 |         self._add_corner_arc(top_left_center, radius_nm, 180, 270, layer)
374 |         self._add_corner_arc(top_right_center, radius_nm, 270, 0, layer)
375 |         self._add_corner_arc(bottom_right_center, radius_nm, 0, 90, layer)
376 |         self._add_corner_arc(bottom_left_center, radius_nm, 90, 180, layer)
377 |         
378 |         # Add lines for straight edges
379 |         # Top edge
380 |         self._add_edge_line(
381 |             pcbnew.VECTOR2I(top_left_center.x, top_left_center.y - radius_nm),
382 |             pcbnew.VECTOR2I(top_right_center.x, top_right_center.y - radius_nm),
383 |             layer
384 |         )
385 |         # Right edge
386 |         self._add_edge_line(
387 |             pcbnew.VECTOR2I(top_right_center.x + radius_nm, top_right_center.y),
388 |             pcbnew.VECTOR2I(bottom_right_center.x + radius_nm, bottom_right_center.y),
389 |             layer
390 |         )
391 |         # Bottom edge
392 |         self._add_edge_line(
393 |             pcbnew.VECTOR2I(bottom_right_center.x, bottom_right_center.y + radius_nm),
394 |             pcbnew.VECTOR2I(bottom_left_center.x, bottom_left_center.y + radius_nm),
395 |             layer
396 |         )
397 |         # Left edge
398 |         self._add_edge_line(
399 |             pcbnew.VECTOR2I(bottom_left_center.x - radius_nm, bottom_left_center.y),
400 |             pcbnew.VECTOR2I(top_left_center.x - radius_nm, top_left_center.y),
401 |             layer
402 |         )
403 | 
404 |     def _add_corner_arc(self, center: pcbnew.VECTOR2I, radius: int, 
405 |                         start_angle: float, end_angle: float, layer: int) -> None:
406 |         """Add an arc for a rounded corner"""
407 |         # Create arc for corner
408 |         arc = pcbnew.PCB_SHAPE(self.board)
409 |         arc.SetShape(pcbnew.SHAPE_T_ARC)
410 |         arc.SetCenter(center)
411 |         
412 |         # Calculate start and end points
413 |         start_x = center.x + int(radius * math.cos(math.radians(start_angle)))
414 |         start_y = center.y + int(radius * math.sin(math.radians(start_angle)))
415 |         end_x = center.x + int(radius * math.cos(math.radians(end_angle)))
416 |         end_y = center.y + int(radius * math.sin(math.radians(end_angle)))
417 |         
418 |         arc.SetStart(pcbnew.VECTOR2I(start_x, start_y))
419 |         arc.SetEnd(pcbnew.VECTOR2I(end_x, end_y))
420 |         arc.SetLayer(layer)
421 |         arc.SetWidth(0)  # Zero width for edge cuts
422 |         self.board.Add(arc)
423 | 
```

--------------------------------------------------------------------------------
/python/commands/design_rules.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Design rules command implementations for KiCAD interface
  3 | """
  4 | 
  5 | import os
  6 | import pcbnew
  7 | import logging
  8 | from typing import Dict, Any, Optional, List, Tuple
  9 | 
 10 | logger = logging.getLogger('kicad_interface')
 11 | 
 12 | class DesignRuleCommands:
 13 |     """Handles design rule checking and configuration"""
 14 | 
 15 |     def __init__(self, board: Optional[pcbnew.BOARD] = None):
 16 |         """Initialize with optional board instance"""
 17 |         self.board = board
 18 | 
 19 |     def set_design_rules(self, params: Dict[str, Any]) -> Dict[str, Any]:
 20 |         """Set design rules for the PCB"""
 21 |         try:
 22 |             if not self.board:
 23 |                 return {
 24 |                     "success": False,
 25 |                     "message": "No board is loaded",
 26 |                     "errorDetails": "Load or create a board first"
 27 |                 }
 28 | 
 29 |             design_settings = self.board.GetDesignSettings()
 30 | 
 31 |             # Convert mm to nanometers for KiCAD internal units
 32 |             scale = 1000000  # mm to nm
 33 | 
 34 |             # Set clearance
 35 |             if "clearance" in params:
 36 |                 design_settings.m_MinClearance = int(params["clearance"] * scale)
 37 | 
 38 |             # KiCAD 9.0: Use SetCustom* methods instead of SetCurrent* (which were removed)
 39 |             # Track if we set any custom track/via values
 40 |             custom_values_set = False
 41 | 
 42 |             if "trackWidth" in params:
 43 |                 design_settings.SetCustomTrackWidth(int(params["trackWidth"] * scale))
 44 |                 custom_values_set = True
 45 | 
 46 |             # Via settings
 47 |             if "viaDiameter" in params:
 48 |                 design_settings.SetCustomViaSize(int(params["viaDiameter"] * scale))
 49 |                 custom_values_set = True
 50 |             if "viaDrill" in params:
 51 |                 design_settings.SetCustomViaDrill(int(params["viaDrill"] * scale))
 52 |                 custom_values_set = True
 53 | 
 54 |             # KiCAD 9.0: Activate custom track/via values so they become the current values
 55 |             if custom_values_set:
 56 |                 design_settings.UseCustomTrackViaSize(True)
 57 | 
 58 |             # Set micro via settings (use properties - methods removed in KiCAD 9.0)
 59 |             if "microViaDiameter" in params:
 60 |                 design_settings.m_MicroViasMinSize = int(params["microViaDiameter"] * scale)
 61 |             if "microViaDrill" in params:
 62 |                 design_settings.m_MicroViasMinDrill = int(params["microViaDrill"] * scale)
 63 | 
 64 |             # Set minimum values
 65 |             if "minTrackWidth" in params:
 66 |                 design_settings.m_TrackMinWidth = int(params["minTrackWidth"] * scale)
 67 |             if "minViaDiameter" in params:
 68 |                 design_settings.m_ViasMinSize = int(params["minViaDiameter"] * scale)
 69 | 
 70 |             # KiCAD 9.0: m_ViasMinDrill removed - use m_MinThroughDrill instead
 71 |             if "minViaDrill" in params:
 72 |                 design_settings.m_MinThroughDrill = int(params["minViaDrill"] * scale)
 73 | 
 74 |             if "minMicroViaDiameter" in params:
 75 |                 design_settings.m_MicroViasMinSize = int(params["minMicroViaDiameter"] * scale)
 76 |             if "minMicroViaDrill" in params:
 77 |                 design_settings.m_MicroViasMinDrill = int(params["minMicroViaDrill"] * scale)
 78 | 
 79 |             # KiCAD 9.0: m_MinHoleDiameter removed - use m_MinThroughDrill
 80 |             if "minHoleDiameter" in params:
 81 |                 design_settings.m_MinThroughDrill = int(params["minHoleDiameter"] * scale)
 82 | 
 83 |             # KiCAD 9.0: Added hole clearance settings
 84 |             if "holeClearance" in params:
 85 |                 design_settings.m_HoleClearance = int(params["holeClearance"] * scale)
 86 |             if "holeToHoleMin" in params:
 87 |                 design_settings.m_HoleToHoleMin = int(params["holeToHoleMin"] * scale)
 88 | 
 89 |             # Build response with KiCAD 9.0 compatible properties
 90 |             # After UseCustomTrackViaSize(True), GetCurrent* returns the custom values
 91 |             response_rules = {
 92 |                 "clearance": design_settings.m_MinClearance / scale,
 93 |                 "trackWidth": design_settings.GetCurrentTrackWidth() / scale,
 94 |                 "viaDiameter": design_settings.GetCurrentViaSize() / scale,
 95 |                 "viaDrill": design_settings.GetCurrentViaDrill() / scale,
 96 |                 "microViaDiameter": design_settings.m_MicroViasMinSize / scale,
 97 |                 "microViaDrill": design_settings.m_MicroViasMinDrill / scale,
 98 |                 "minTrackWidth": design_settings.m_TrackMinWidth / scale,
 99 |                 "minViaDiameter": design_settings.m_ViasMinSize / scale,
100 |                 "minThroughDrill": design_settings.m_MinThroughDrill / scale,
101 |                 "minMicroViaDiameter": design_settings.m_MicroViasMinSize / scale,
102 |                 "minMicroViaDrill": design_settings.m_MicroViasMinDrill / scale,
103 |                 "holeClearance": design_settings.m_HoleClearance / scale,
104 |                 "holeToHoleMin": design_settings.m_HoleToHoleMin / scale,
105 |                 "viasMinAnnularWidth": design_settings.m_ViasMinAnnularWidth / scale
106 |             }
107 | 
108 |             return {
109 |                 "success": True,
110 |                 "message": "Updated design rules",
111 |                 "rules": response_rules
112 |             }
113 | 
114 |         except Exception as e:
115 |             logger.error(f"Error setting design rules: {str(e)}")
116 |             return {
117 |                 "success": False,
118 |                 "message": "Failed to set design rules",
119 |                 "errorDetails": str(e)
120 |             }
121 | 
122 |     def get_design_rules(self, params: Dict[str, Any]) -> Dict[str, Any]:
123 |         """Get current design rules - KiCAD 9.0 compatible"""
124 |         try:
125 |             if not self.board:
126 |                 return {
127 |                     "success": False,
128 |                     "message": "No board is loaded",
129 |                     "errorDetails": "Load or create a board first"
130 |                 }
131 | 
132 |             design_settings = self.board.GetDesignSettings()
133 |             scale = 1000000  # nm to mm
134 | 
135 |             # Build rules dict with KiCAD 9.0 compatible properties
136 |             rules = {
137 |                 # Core clearance and track settings
138 |                 "clearance": design_settings.m_MinClearance / scale,
139 |                 "trackWidth": design_settings.GetCurrentTrackWidth() / scale,
140 |                 "minTrackWidth": design_settings.m_TrackMinWidth / scale,
141 | 
142 |                 # Via settings (current values from methods)
143 |                 "viaDiameter": design_settings.GetCurrentViaSize() / scale,
144 |                 "viaDrill": design_settings.GetCurrentViaDrill() / scale,
145 | 
146 |                 # Via minimum values
147 |                 "minViaDiameter": design_settings.m_ViasMinSize / scale,
148 |                 "viasMinAnnularWidth": design_settings.m_ViasMinAnnularWidth / scale,
149 | 
150 |                 # Micro via settings
151 |                 "microViaDiameter": design_settings.m_MicroViasMinSize / scale,
152 |                 "microViaDrill": design_settings.m_MicroViasMinDrill / scale,
153 |                 "minMicroViaDiameter": design_settings.m_MicroViasMinSize / scale,
154 |                 "minMicroViaDrill": design_settings.m_MicroViasMinDrill / scale,
155 | 
156 |                 # KiCAD 9.0: Hole and drill settings (replaces removed m_ViasMinDrill and m_MinHoleDiameter)
157 |                 "minThroughDrill": design_settings.m_MinThroughDrill / scale,
158 |                 "holeClearance": design_settings.m_HoleClearance / scale,
159 |                 "holeToHoleMin": design_settings.m_HoleToHoleMin / scale,
160 | 
161 |                 # Other constraints
162 |                 "copperEdgeClearance": design_settings.m_CopperEdgeClearance / scale,
163 |                 "silkClearance": design_settings.m_SilkClearance / scale,
164 |             }
165 | 
166 |             return {
167 |                 "success": True,
168 |                 "rules": rules
169 |             }
170 | 
171 |         except Exception as e:
172 |             logger.error(f"Error getting design rules: {str(e)}")
173 |             return {
174 |                 "success": False,
175 |                 "message": "Failed to get design rules",
176 |                 "errorDetails": str(e)
177 |             }
178 | 
179 |     def run_drc(self, params: Dict[str, Any]) -> Dict[str, Any]:
180 |         """Run Design Rule Check using kicad-cli"""
181 |         import subprocess
182 |         import json
183 |         import tempfile
184 |         import platform
185 |         import shutil
186 | 
187 |         try:
188 |             if not self.board:
189 |                 return {
190 |                     "success": False,
191 |                     "message": "No board is loaded",
192 |                     "errorDetails": "Load or create a board first"
193 |                 }
194 | 
195 |             report_path = params.get("reportPath")
196 | 
197 |             # Get the board file path
198 |             board_file = self.board.GetFileName()
199 |             if not board_file or not os.path.exists(board_file):
200 |                 return {
201 |                     "success": False,
202 |                     "message": "Board file not found",
203 |                     "errorDetails": "Cannot run DRC without a saved board file"
204 |                 }
205 | 
206 |             # Find kicad-cli executable
207 |             kicad_cli = self._find_kicad_cli()
208 |             if not kicad_cli:
209 |                 return {
210 |                     "success": False,
211 |                     "message": "kicad-cli not found",
212 |                     "errorDetails": "KiCAD CLI tool not found in system. Install KiCAD 8.0+ or set PATH."
213 |                 }
214 | 
215 |             # Create temporary JSON output file
216 |             with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as tmp:
217 |                 json_output = tmp.name
218 | 
219 |             try:
220 |                 # Build command
221 |                 cmd = [
222 |                     kicad_cli,
223 |                     'pcb',
224 |                     'drc',
225 |                     '--format', 'json',
226 |                     '--output', json_output,
227 |                     '--units', 'mm',
228 |                     board_file
229 |                 ]
230 | 
231 |                 logger.info(f"Running DRC command: {' '.join(cmd)}")
232 | 
233 |                 # Run DRC
234 |                 result = subprocess.run(
235 |                     cmd,
236 |                     capture_output=True,
237 |                     text=True,
238 |                     timeout=600  # 10 minute timeout for large boards (21MB PCB needs time)
239 |                 )
240 | 
241 |                 if result.returncode != 0:
242 |                     logger.error(f"DRC command failed: {result.stderr}")
243 |                     return {
244 |                         "success": False,
245 |                         "message": "DRC command failed",
246 |                         "errorDetails": result.stderr
247 |                     }
248 | 
249 |                 # Read JSON output
250 |                 with open(json_output, 'r', encoding='utf-8') as f:
251 |                     drc_data = json.load(f)
252 | 
253 |                 # Parse violations from kicad-cli output
254 |                 violations = []
255 |                 violation_counts = {}
256 |                 severity_counts = {"error": 0, "warning": 0, "info": 0}
257 | 
258 |                 for violation in drc_data.get('violations', []):
259 |                     vtype = violation.get("type", "unknown")
260 |                     vseverity = violation.get("severity", "error")
261 | 
262 |                     violations.append({
263 |                         "type": vtype,
264 |                         "severity": vseverity,
265 |                         "message": violation.get("description", ""),
266 |                         "location": {
267 |                             "x": violation.get("x", 0),
268 |                             "y": violation.get("y", 0),
269 |                             "unit": "mm"
270 |                         }
271 |                     })
272 | 
273 |                     # Count violations by type
274 |                     violation_counts[vtype] = violation_counts.get(vtype, 0) + 1
275 | 
276 |                     # Count by severity
277 |                     if vseverity in severity_counts:
278 |                         severity_counts[vseverity] += 1
279 | 
280 |                 # Determine where to save the violations file
281 |                 board_dir = os.path.dirname(board_file)
282 |                 board_name = os.path.splitext(os.path.basename(board_file))[0]
283 |                 violations_file = os.path.join(board_dir, f"{board_name}_drc_violations.json")
284 | 
285 |                 # Always save violations to JSON file (for large result sets)
286 |                 with open(violations_file, 'w', encoding='utf-8') as f:
287 |                     json.dump({
288 |                         "board": board_file,
289 |                         "timestamp": drc_data.get("date", "unknown"),
290 |                         "total_violations": len(violations),
291 |                         "violation_counts": violation_counts,
292 |                         "severity_counts": severity_counts,
293 |                         "violations": violations
294 |                     }, f, indent=2)
295 | 
296 |                 # Save text report if requested
297 |                 if report_path:
298 |                     report_path = os.path.abspath(os.path.expanduser(report_path))
299 |                     cmd_report = [
300 |                         kicad_cli,
301 |                         'pcb',
302 |                         'drc',
303 |                         '--format', 'report',
304 |                         '--output', report_path,
305 |                         '--units', 'mm',
306 |                         board_file
307 |                     ]
308 |                     subprocess.run(cmd_report, capture_output=True, timeout=600)
309 | 
310 |                 # Return summary only (not full violations list)
311 |                 return {
312 |                     "success": True,
313 |                     "message": f"Found {len(violations)} DRC violations",
314 |                     "summary": {
315 |                         "total": len(violations),
316 |                         "by_severity": severity_counts,
317 |                         "by_type": violation_counts
318 |                     },
319 |                     "violationsFile": violations_file,
320 |                     "reportPath": report_path if report_path else None
321 |                 }
322 | 
323 |             finally:
324 |                 # Clean up temp JSON file
325 |                 if os.path.exists(json_output):
326 |                     os.unlink(json_output)
327 | 
328 |         except subprocess.TimeoutExpired:
329 |             logger.error("DRC command timed out")
330 |             return {
331 |                 "success": False,
332 |                 "message": "DRC command timed out",
333 |                 "errorDetails": "Command took longer than 600 seconds (10 minutes)"
334 |             }
335 |         except Exception as e:
336 |             logger.error(f"Error running DRC: {str(e)}")
337 |             return {
338 |                 "success": False,
339 |                 "message": "Failed to run DRC",
340 |                 "errorDetails": str(e)
341 |             }
342 | 
343 |     def _find_kicad_cli(self) -> Optional[str]:
344 |         """Find kicad-cli executable"""
345 |         import platform
346 |         import shutil
347 | 
348 |         # Try system PATH first
349 |         cli_name = "kicad-cli.exe" if platform.system() == "Windows" else "kicad-cli"
350 |         cli_path = shutil.which(cli_name)
351 |         if cli_path:
352 |             return cli_path
353 | 
354 |         # Try common installation paths (version-specific)
355 |         if platform.system() == "Windows":
356 |             common_paths = [
357 |                 r"C:\Program Files\KiCad\10.0\bin\kicad-cli.exe",
358 |                 r"C:\Program Files\KiCad\9.0\bin\kicad-cli.exe",
359 |                 r"C:\Program Files\KiCad\8.0\bin\kicad-cli.exe",
360 |                 r"C:\Program Files (x86)\KiCad\10.0\bin\kicad-cli.exe",
361 |                 r"C:\Program Files (x86)\KiCad\9.0\bin\kicad-cli.exe",
362 |                 r"C:\Program Files (x86)\KiCad\8.0\bin\kicad-cli.exe",
363 |                 r"C:\Program Files\KiCad\bin\kicad-cli.exe",
364 |             ]
365 |             for path in common_paths:
366 |                 if os.path.exists(path):
367 |                     return path
368 |         elif platform.system() == "Darwin":  # macOS
369 |             common_paths = [
370 |                 "/Applications/KiCad/KiCad.app/Contents/MacOS/kicad-cli",
371 |                 "/usr/local/bin/kicad-cli",
372 |             ]
373 |             for path in common_paths:
374 |                 if os.path.exists(path):
375 |                     return path
376 |         else:  # Linux
377 |             common_paths = [
378 |                 "/usr/bin/kicad-cli",
379 |                 "/usr/local/bin/kicad-cli",
380 |             ]
381 |             for path in common_paths:
382 |                 if os.path.exists(path):
383 |                     return path
384 | 
385 |         return None
386 | 
387 |     def get_drc_violations(self, params: Dict[str, Any]) -> Dict[str, Any]:
388 |         """Get list of DRC violations"""
389 |         try:
390 |             if not self.board:
391 |                 return {
392 |                     "success": False,
393 |                     "message": "No board is loaded",
394 |                     "errorDetails": "Load or create a board first"
395 |                 }
396 | 
397 |             severity = params.get("severity", "all")
398 | 
399 |             # Get DRC markers
400 |             violations = []
401 |             for marker in self.board.GetDRCMarkers():
402 |                 violation = {
403 |                     "type": marker.GetErrorCode(),
404 |                     "severity": "error",  # KiCAD DRC markers are always errors
405 |                     "message": marker.GetDescription(),
406 |                     "location": {
407 |                         "x": marker.GetPos().x / 1000000,
408 |                         "y": marker.GetPos().y / 1000000,
409 |                         "unit": "mm"
410 |                     }
411 |                 }
412 | 
413 |                 # Filter by severity if specified
414 |                 if severity == "all" or severity == violation["severity"]:
415 |                     violations.append(violation)
416 | 
417 |             return {
418 |                 "success": True,
419 |                 "violations": violations
420 |             }
421 | 
422 |         except Exception as e:
423 |             logger.error(f"Error getting DRC violations: {str(e)}")
424 |             return {
425 |                 "success": False,
426 |                 "message": "Failed to get DRC violations",
427 |                 "errorDetails": str(e)
428 |             }
429 | 
```

--------------------------------------------------------------------------------
/src/kicad-server.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { Server } from '@modelcontextprotocol/sdk/server/index.js';
  2 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
  3 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
  4 | import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
  5 | import { spawn, ChildProcess } from 'child_process';
  6 | import { existsSync } from 'fs';
  7 | import path from 'path';
  8 | 
  9 | // Import all tool definitions for reference
 10 | // import { registerBoardTools } from './tools/board.js';
 11 | // import { registerComponentTools } from './tools/component.js';
 12 | // import { registerRoutingTools } from './tools/routing.js';
 13 | // import { registerDesignRuleTools } from './tools/design-rules.js';
 14 | // import { registerExportTools } from './tools/export.js';
 15 | // import { registerProjectTools } from './tools/project.js';
 16 | // import { registerSchematicTools } from './tools/schematic.js';
 17 | 
 18 | class KiCADServer {
 19 |   private server: Server;
 20 |   private pythonProcess: ChildProcess | null = null;
 21 |   private kicadScriptPath: string;
 22 |   private requestQueue: Array<{ request: any, resolve: Function, reject: Function }> = [];
 23 |   private processingRequest = false;
 24 | 
 25 |   constructor() {
 26 |     // Set absolute path to the Python KiCAD interface script
 27 |     // Using a hardcoded path to avoid cwd() issues when running from Cline
 28 |     this.kicadScriptPath = 'c:/repo/KiCAD-MCP/python/kicad_interface.py';
 29 |     
 30 |     // Check if script exists
 31 |     if (!existsSync(this.kicadScriptPath)) {
 32 |       throw new Error(`KiCAD interface script not found: ${this.kicadScriptPath}`);
 33 |     }
 34 |     
 35 |     // Initialize the server
 36 |     this.server = new Server(
 37 |       {
 38 |         name: 'kicad-mcp-server',
 39 |         version: '1.0.0'
 40 |       },
 41 |       {
 42 |         capabilities: {
 43 |           tools: {
 44 |             // Empty object here, tools will be registered dynamically
 45 |           }
 46 |         }
 47 |       }
 48 |     );
 49 |     
 50 |     // Initialize handler with direct pass-through to Python KiCAD interface
 51 |     // We don't register TypeScript tools since we'll handle everything in Python
 52 | 
 53 |     // Register tool list handler
 54 |     this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
 55 |       tools: [
 56 |         // Project tools
 57 |         {
 58 |           name: 'create_project',
 59 |           description: 'Create a new KiCAD project',
 60 |           inputSchema: {
 61 |             type: 'object',
 62 |             properties: {
 63 |               projectName: { type: 'string', description: 'Name of the new project' },
 64 |               path: { type: 'string', description: 'Path where to create the project' },
 65 |               template: { type: 'string', description: 'Optional template to use' }
 66 |             },
 67 |             required: ['projectName']
 68 |           }
 69 |         },
 70 |         {
 71 |           name: 'open_project',
 72 |           description: 'Open an existing KiCAD project',
 73 |           inputSchema: {
 74 |             type: 'object',
 75 |             properties: {
 76 |               filename: { type: 'string', description: 'Path to the project file' }
 77 |             },
 78 |             required: ['filename']
 79 |           }
 80 |         },
 81 |         {
 82 |           name: 'save_project',
 83 |           description: 'Save the current KiCAD project',
 84 |           inputSchema: {
 85 |             type: 'object',
 86 |             properties: {
 87 |               filename: { type: 'string', description: 'Optional path to save to' }
 88 |             }
 89 |           }
 90 |         },
 91 |         {
 92 |           name: 'get_project_info',
 93 |           description: 'Get information about the current project',
 94 |           inputSchema: {
 95 |             type: 'object',
 96 |             properties: {}
 97 |           }
 98 |         },
 99 |         
100 |         // Board tools
101 |         {
102 |           name: 'set_board_size',
103 |           description: 'Set the size of the PCB board',
104 |           inputSchema: {
105 |             type: 'object',
106 |             properties: {
107 |               width: { type: 'number', description: 'Board width' },
108 |               height: { type: 'number', description: 'Board height' },
109 |               unit: { type: 'string', description: 'Unit of measurement (mm or inch)' }
110 |             },
111 |             required: ['width', 'height']
112 |           }
113 |         },
114 |         {
115 |           name: 'add_board_outline',
116 |           description: 'Add a board outline to the PCB',
117 |           inputSchema: {
118 |             type: 'object',
119 |             properties: {
120 |               shape: { type: 'string', description: 'Shape of outline (rectangle, circle, polygon, rounded_rectangle)' },
121 |               width: { type: 'number', description: 'Width for rectangle shapes' },
122 |               height: { type: 'number', description: 'Height for rectangle shapes' },
123 |               radius: { type: 'number', description: 'Radius for circle shapes' },
124 |               cornerRadius: { type: 'number', description: 'Corner radius for rounded rectangles' },
125 |               points: { type: 'array', description: 'Array of points for polygon shapes' },
126 |               centerX: { type: 'number', description: 'X coordinate of center' },
127 |               centerY: { type: 'number', description: 'Y coordinate of center' },
128 |               unit: { type: 'string', description: 'Unit of measurement (mm or inch)' }
129 |             }
130 |           }
131 |         },
132 |         
133 |         // Component tools
134 |         {
135 |           name: 'place_component',
136 |           description: 'Place a component on the PCB',
137 |           inputSchema: {
138 |             type: 'object',
139 |             properties: {
140 |               componentId: { type: 'string', description: 'Component ID/footprint to place' },
141 |               position: { type: 'object', description: 'Position coordinates' },
142 |               reference: { type: 'string', description: 'Component reference designator' },
143 |               value: { type: 'string', description: 'Component value' },
144 |               rotation: { type: 'number', description: 'Rotation angle in degrees' },
145 |               layer: { type: 'string', description: 'Layer to place component on' }
146 |             },
147 |             required: ['componentId', 'position']
148 |           }
149 |         },
150 |         
151 |         // Routing tools
152 |         {
153 |           name: 'add_net',
154 |           description: 'Add a new net to the PCB',
155 |           inputSchema: {
156 |             type: 'object',
157 |             properties: {
158 |               name: { type: 'string', description: 'Net name' },
159 |               class: { type: 'string', description: 'Net class' }
160 |             },
161 |             required: ['name']
162 |           }
163 |         },
164 |         {
165 |           name: 'route_trace',
166 |           description: 'Route a trace between two points or pads',
167 |           inputSchema: {
168 |             type: 'object',
169 |             properties: {
170 |               start: { type: 'object', description: 'Start point or pad' },
171 |               end: { type: 'object', description: 'End point or pad' },
172 |               layer: { type: 'string', description: 'Layer to route on' },
173 |               width: { type: 'number', description: 'Track width' },
174 |               net: { type: 'string', description: 'Net name' }
175 |             },
176 |             required: ['start', 'end']
177 |           }
178 |         },
179 |         
180 |         // Schematic tools
181 |         {
182 |           name: 'create_schematic',
183 |           description: 'Create a new KiCAD schematic',
184 |           inputSchema: {
185 |             type: 'object',
186 |             properties: {
187 |               projectName: { type: 'string', description: 'Name of the schematic project' },
188 |               path: { type: 'string', description: 'Path where to create the schematic file' },
189 |               metadata: { type: 'object', description: 'Optional metadata for the schematic' }
190 |             },
191 |             required: ['projectName']
192 |           }
193 |         },
194 |         {
195 |           name: 'load_schematic',
196 |           description: 'Load an existing KiCAD schematic',
197 |           inputSchema: {
198 |             type: 'object',
199 |             properties: {
200 |               filename: { type: 'string', description: 'Path to the schematic file to load' }
201 |             },
202 |             required: ['filename']
203 |           }
204 |         },
205 |         {
206 |           name: 'add_schematic_component',
207 |           description: 'Add a component to a KiCAD schematic',
208 |           inputSchema: {
209 |             type: 'object',
210 |             properties: {
211 |               schematicPath: { type: 'string', description: 'Path to the schematic file' },
212 |               component: { 
213 |                 type: 'object', 
214 |                 description: 'Component definition',
215 |                 properties: {
216 |                   type: { type: 'string', description: 'Component type (e.g., R, C, LED)' },
217 |                   reference: { type: 'string', description: 'Reference designator (e.g., R1, C2)' },
218 |                   value: { type: 'string', description: 'Component value (e.g., 10k, 0.1uF)' },
219 |                   library: { type: 'string', description: 'Symbol library name' },
220 |                   x: { type: 'number', description: 'X position in schematic' },
221 |                   y: { type: 'number', description: 'Y position in schematic' },
222 |                   rotation: { type: 'number', description: 'Rotation angle in degrees' },
223 |                   properties: { type: 'object', description: 'Additional properties' }
224 |                 },
225 |                 required: ['type', 'reference']
226 |               }
227 |             },
228 |             required: ['schematicPath', 'component']
229 |           }
230 |         },
231 |         {
232 |           name: 'add_schematic_wire',
233 |           description: 'Add a wire connection to a KiCAD schematic',
234 |           inputSchema: {
235 |             type: 'object',
236 |             properties: {
237 |               schematicPath: { type: 'string', description: 'Path to the schematic file' },
238 |               startPoint: { 
239 |                 type: 'array', 
240 |                 description: 'Starting point coordinates [x, y]',
241 |                 items: { type: 'number' },
242 |                 minItems: 2,
243 |                 maxItems: 2
244 |               },
245 |               endPoint: { 
246 |                 type: 'array', 
247 |                 description: 'Ending point coordinates [x, y]',
248 |                 items: { type: 'number' },
249 |                 minItems: 2,
250 |                 maxItems: 2
251 |               }
252 |             },
253 |             required: ['schematicPath', 'startPoint', 'endPoint']
254 |           }
255 |         },
256 |         {
257 |           name: 'list_schematic_libraries',
258 |           description: 'List available KiCAD symbol libraries',
259 |           inputSchema: {
260 |             type: 'object',
261 |             properties: {
262 |               searchPaths: { 
263 |                 type: 'array', 
264 |                 description: 'Optional search paths for libraries',
265 |                 items: { type: 'string' }
266 |               }
267 |             }
268 |           }
269 |         },
270 |         {
271 |           name: 'export_schematic_pdf',
272 |           description: 'Export a KiCAD schematic to PDF',
273 |           inputSchema: {
274 |             type: 'object',
275 |             properties: {
276 |               schematicPath: { type: 'string', description: 'Path to the schematic file' },
277 |               outputPath: { type: 'string', description: 'Path for the output PDF file' }
278 |             },
279 |             required: ['schematicPath', 'outputPath']
280 |           }
281 |         }
282 |       ]
283 |     }));
284 | 
285 |     // Register tool call handler
286 |     this.server.setRequestHandler(CallToolRequestSchema, async (request: any) => {
287 |       const toolName = request.params.name;
288 |       const args = request.params.arguments || {};
289 |       
290 |       // Pass all commands directly to KiCAD Python interface
291 |       try {
292 |         return await this.callKicadScript(toolName, args);
293 |       } catch (error) {
294 |         console.error(`Error executing tool ${toolName}:`, error);
295 |         throw new Error(`Unknown tool: ${toolName}`);
296 |       }
297 |     });
298 |   }
299 | 
300 |   async start() {
301 |     try {
302 |       console.error('Starting KiCAD MCP server...');
303 |       
304 |       // Start the Python process for KiCAD scripting
305 |       console.error(`Starting Python process with script: ${this.kicadScriptPath}`);
306 |       const pythonExe = 'C:\\Program Files\\KiCad\\9.0\\bin\\python.exe';
307 |       
308 |       console.error(`Using Python executable: ${pythonExe}`);
309 |       this.pythonProcess = spawn(pythonExe, [this.kicadScriptPath], {
310 |         stdio: ['pipe', 'pipe', 'pipe'],
311 |         env: {
312 |           ...process.env,
313 |           PYTHONPATH: 'C:/Program Files/KiCad/9.0/lib/python3/dist-packages'
314 |         }
315 |       });
316 |       
317 |       // Listen for process exit
318 |       this.pythonProcess.on('exit', (code, signal) => {
319 |         console.error(`Python process exited with code ${code} and signal ${signal}`);
320 |         this.pythonProcess = null;
321 |       });
322 |       
323 |       // Listen for process errors
324 |       this.pythonProcess.on('error', (err) => {
325 |         console.error(`Python process error: ${err.message}`);
326 |       });
327 |       
328 |       // Set up error logging for stderr
329 |       if (this.pythonProcess.stderr) {
330 |         this.pythonProcess.stderr.on('data', (data: Buffer) => {
331 |           console.error(`Python stderr: ${data.toString()}`);
332 |         });
333 |       }
334 |       
335 |       // Connect to transport
336 |       const transport = new StdioServerTransport();
337 |       await this.server.connect(transport);
338 |       console.error('KiCAD MCP server running');
339 |       
340 |       // Keep the process running
341 |       process.on('SIGINT', () => {
342 |         if (this.pythonProcess) {
343 |           this.pythonProcess.kill();
344 |         }
345 |         this.server.close().catch(console.error);
346 |         process.exit(0);
347 |       });
348 |       
349 |     } catch (error: unknown) {
350 |       if (error instanceof Error) {
351 |         console.error('Failed to start MCP server:', error.message);
352 |       } else {
353 |         console.error('Failed to start MCP server: Unknown error');
354 |       }
355 |       process.exit(1);
356 |     }
357 |   }
358 | 
359 |   private async callKicadScript(command: string, params: any): Promise<any> {
360 |     return new Promise((resolve, reject) => {
361 |       // Check if Python process is running
362 |       if (!this.pythonProcess) {
363 |         console.error('Python process is not running');
364 |         reject(new Error("Python process for KiCAD scripting is not running"));
365 |         return;
366 |       }
367 |       
368 |       // Add request to queue
369 |       this.requestQueue.push({
370 |         request: { command, params },
371 |         resolve,
372 |         reject
373 |       });
374 |       
375 |       // Process the queue if not already processing
376 |       if (!this.processingRequest) {
377 |         this.processNextRequest();
378 |       }
379 |     });
380 |   }
381 |   
382 |   private processNextRequest(): void {
383 |     // If no more requests or already processing, return
384 |     if (this.requestQueue.length === 0 || this.processingRequest) {
385 |       return;
386 |     }
387 |     
388 |     // Set processing flag
389 |     this.processingRequest = true;
390 |     
391 |     // Get the next request
392 |     const { request, resolve, reject } = this.requestQueue.shift()!;
393 |     
394 |     try {
395 |       console.error(`Processing KiCAD command: ${request.command}`);
396 |       
397 |       // Format the command and parameters as JSON
398 |       const requestStr = JSON.stringify(request);
399 |       
400 |       // Set up response handling
401 |       let responseData = '';
402 |       
403 |       // Clear any previous listeners
404 |       if (this.pythonProcess?.stdout) {
405 |         this.pythonProcess.stdout.removeAllListeners('data');
406 |       }
407 |       
408 |       // Set up new listeners
409 |       if (this.pythonProcess?.stdout) {
410 |         this.pythonProcess.stdout.on('data', (data: Buffer) => {
411 |           const chunk = data.toString();
412 |           console.error(`Received data chunk: ${chunk.length} bytes`);
413 |           responseData += chunk;
414 |           
415 |           // Check if we have a complete response
416 |           try {
417 |             // Try to parse the response as JSON
418 |             const result = JSON.parse(responseData);
419 |             
420 |             // If we get here, we have a valid JSON response
421 |             console.error(`Completed KiCAD command: ${request.command} with result: ${JSON.stringify(result)}`);
422 |             
423 |             // Reset processing flag
424 |             this.processingRequest = false;
425 |             
426 |             // Process next request if any
427 |             setTimeout(() => this.processNextRequest(), 0);
428 |             
429 |             // Clear listeners
430 |             if (this.pythonProcess?.stdout) {
431 |               this.pythonProcess.stdout.removeAllListeners('data');
432 |             }
433 |             
434 |             // Resolve with the expected MCP tool response format
435 |             if (result.success) {
436 |               resolve({
437 |                 content: [
438 |                   {
439 |                     type: 'text',
440 |                     text: JSON.stringify(result, null, 2)
441 |                   }
442 |                 ]
443 |               });
444 |             } else {
445 |               resolve({
446 |                 content: [
447 |                   {
448 |                     type: 'text',
449 |                     text: result.errorDetails || result.message || 'Unknown error'
450 |                   }
451 |                 ],
452 |                 isError: true
453 |               });
454 |             }
455 |           } catch (e) {
456 |             // Not a complete JSON yet, keep collecting data
457 |           }
458 |         });
459 |       }
460 |       
461 |       // Set a timeout
462 |       const timeout = setTimeout(() => {
463 |         console.error(`Command timeout: ${request.command}`);
464 |         
465 |         // Clear listeners
466 |         if (this.pythonProcess?.stdout) {
467 |           this.pythonProcess.stdout.removeAllListeners('data');
468 |         }
469 |         
470 |         // Reset processing flag
471 |         this.processingRequest = false;
472 |         
473 |         // Process next request
474 |         setTimeout(() => this.processNextRequest(), 0);
475 |         
476 |         // Reject the promise
477 |         reject(new Error(`Command timeout: ${request.command}`));
478 |       }, 30000); // 30 seconds timeout
479 |       
480 |       // Write the request to the Python process
481 |       console.error(`Sending request: ${requestStr}`);
482 |       this.pythonProcess?.stdin?.write(requestStr + '\n');
483 |     } catch (error) {
484 |       console.error(`Error processing request: ${error}`);
485 |       
486 |       // Reset processing flag
487 |       this.processingRequest = false;
488 |       
489 |       // Process next request
490 |       setTimeout(() => this.processNextRequest(), 0);
491 |       
492 |       // Reject the promise
493 |       reject(error);
494 |     }
495 |   }
496 | }
497 | 
498 | // Start the server
499 | const server = new KiCADServer();
500 | server.start().catch(console.error);
501 | 
```

--------------------------------------------------------------------------------
/python/commands/export.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Export command implementations for KiCAD interface
  3 | """
  4 | 
  5 | import os
  6 | import pcbnew
  7 | import logging
  8 | from typing import Dict, Any, Optional, List, Tuple
  9 | import base64
 10 | 
 11 | logger = logging.getLogger('kicad_interface')
 12 | 
 13 | class ExportCommands:
 14 |     """Handles export-related KiCAD operations"""
 15 | 
 16 |     def __init__(self, board: Optional[pcbnew.BOARD] = None):
 17 |         """Initialize with optional board instance"""
 18 |         self.board = board
 19 | 
 20 |     def export_gerber(self, params: Dict[str, Any]) -> Dict[str, Any]:
 21 |         """Export Gerber files"""
 22 |         try:
 23 |             if not self.board:
 24 |                 return {
 25 |                     "success": False,
 26 |                     "message": "No board is loaded",
 27 |                     "errorDetails": "Load or create a board first"
 28 |                 }
 29 | 
 30 |             output_dir = params.get("outputDir")
 31 |             layers = params.get("layers", [])
 32 |             use_protel_extensions = params.get("useProtelExtensions", False)
 33 |             generate_drill_files = params.get("generateDrillFiles", True)
 34 |             generate_map_file = params.get("generateMapFile", False)
 35 |             use_aux_origin = params.get("useAuxOrigin", False)
 36 | 
 37 |             if not output_dir:
 38 |                 return {
 39 |                     "success": False,
 40 |                     "message": "Missing output directory",
 41 |                     "errorDetails": "outputDir parameter is required"
 42 |                 }
 43 | 
 44 |             # Create output directory if it doesn't exist
 45 |             output_dir = os.path.abspath(os.path.expanduser(output_dir))
 46 |             os.makedirs(output_dir, exist_ok=True)
 47 | 
 48 |             # Create plot controller
 49 |             plotter = pcbnew.PLOT_CONTROLLER(self.board)
 50 |             
 51 |             # Set up plot options
 52 |             plot_opts = plotter.GetPlotOptions()
 53 |             plot_opts.SetOutputDirectory(output_dir)
 54 |             plot_opts.SetFormat(pcbnew.PLOT_FORMAT_GERBER)
 55 |             plot_opts.SetUseGerberProtelExtensions(use_protel_extensions)
 56 |             plot_opts.SetUseAuxOrigin(use_aux_origin)
 57 |             plot_opts.SetCreateGerberJobFile(generate_map_file)
 58 |             plot_opts.SetSubtractMaskFromSilk(True)
 59 | 
 60 |             # Plot specified layers or all copper layers
 61 |             plotted_layers = []
 62 |             if layers:
 63 |                 for layer_name in layers:
 64 |                     layer_id = self.board.GetLayerID(layer_name)
 65 |                     if layer_id >= 0:
 66 |                         plotter.SetLayer(layer_id)
 67 |                         plotter.PlotLayer()
 68 |                         plotted_layers.append(layer_name)
 69 |             else:
 70 |                 for layer_id in range(pcbnew.PCB_LAYER_ID_COUNT):
 71 |                     if self.board.IsLayerEnabled(layer_id):
 72 |                         layer_name = self.board.GetLayerName(layer_id)
 73 |                         plotter.SetLayer(layer_id)
 74 |                         plotter.PlotLayer()
 75 |                         plotted_layers.append(layer_name)
 76 | 
 77 |             # Generate drill files if requested
 78 |             drill_files = []
 79 |             if generate_drill_files:
 80 |                 # KiCAD 9.0: Use kicad-cli for more reliable drill file generation
 81 |                 # The Python API's EXCELLON_WRITER.SetOptions() signature changed
 82 |                 board_file = self.board.GetFileName()
 83 |                 kicad_cli = self._find_kicad_cli()
 84 | 
 85 |                 if kicad_cli and board_file and os.path.exists(board_file):
 86 |                     import subprocess
 87 |                     # Generate drill files using kicad-cli
 88 |                     cmd = [
 89 |                         kicad_cli,
 90 |                         'pcb', 'export', 'drill',
 91 |                         '--output', output_dir,
 92 |                         '--format', 'excellon',
 93 |                         '--drill-origin', 'absolute',
 94 |                         '--excellon-separate-th',  # Separate plated/non-plated
 95 |                         board_file
 96 |                     ]
 97 | 
 98 |                     try:
 99 |                         result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
100 |                         if result.returncode == 0:
101 |                             # Get list of generated drill files
102 |                             for file in os.listdir(output_dir):
103 |                                 if file.endswith((".drl", ".cnc")):
104 |                                     drill_files.append(file)
105 |                         else:
106 |                             logger.warning(f"Drill file generation failed: {result.stderr}")
107 |                     except Exception as drill_error:
108 |                         logger.warning(f"Could not generate drill files: {str(drill_error)}")
109 |                 else:
110 |                     logger.warning("kicad-cli not available for drill file generation")
111 | 
112 |             return {
113 |                 "success": True,
114 |                 "message": "Exported Gerber files",
115 |                 "files": {
116 |                     "gerber": plotted_layers,
117 |                     "drill": drill_files,
118 |                     "map": ["job.gbrjob"] if generate_map_file else []
119 |                 },
120 |                 "outputDir": output_dir
121 |             }
122 | 
123 |         except Exception as e:
124 |             logger.error(f"Error exporting Gerber files: {str(e)}")
125 |             return {
126 |                 "success": False,
127 |                 "message": "Failed to export Gerber files",
128 |                 "errorDetails": str(e)
129 |             }
130 | 
131 |     def export_pdf(self, params: Dict[str, Any]) -> Dict[str, Any]:
132 |         """Export PDF files"""
133 |         try:
134 |             if not self.board:
135 |                 return {
136 |                     "success": False,
137 |                     "message": "No board is loaded",
138 |                     "errorDetails": "Load or create a board first"
139 |                 }
140 | 
141 |             output_path = params.get("outputPath")
142 |             layers = params.get("layers", [])
143 |             black_and_white = params.get("blackAndWhite", False)
144 |             frame_reference = params.get("frameReference", True)
145 |             page_size = params.get("pageSize", "A4")
146 | 
147 |             if not output_path:
148 |                 return {
149 |                     "success": False,
150 |                     "message": "Missing output path",
151 |                     "errorDetails": "outputPath parameter is required"
152 |                 }
153 | 
154 |             # Create output directory if it doesn't exist
155 |             output_path = os.path.abspath(os.path.expanduser(output_path))
156 |             os.makedirs(os.path.dirname(output_path), exist_ok=True)
157 | 
158 |             # Create plot controller
159 |             plotter = pcbnew.PLOT_CONTROLLER(self.board)
160 | 
161 |             # Set up plot options
162 |             plot_opts = plotter.GetPlotOptions()
163 |             plot_opts.SetOutputDirectory(os.path.dirname(output_path))
164 |             plot_opts.SetFormat(pcbnew.PLOT_FORMAT_PDF)
165 |             plot_opts.SetPlotFrameRef(frame_reference)
166 |             plot_opts.SetPlotValue(True)
167 |             plot_opts.SetPlotReference(True)
168 |             plot_opts.SetBlackAndWhite(black_and_white)
169 | 
170 |             # KiCAD 9.0 page size handling:
171 |             # - SetPageSettings() was removed in KiCAD 9.0
172 |             # - SetA4Output(bool) forces A4 page size when True
173 |             # - For other sizes, KiCAD auto-scales to fit the board
174 |             # - SetAutoScale(True) enables automatic scaling to fit page
175 |             if page_size == "A4":
176 |                 plot_opts.SetA4Output(True)
177 |             else:
178 |                 # For non-A4 sizes, disable A4 forcing and use auto-scale
179 |                 plot_opts.SetA4Output(False)
180 |                 plot_opts.SetAutoScale(True)
181 |                 # Note: KiCAD 9.0 doesn't support explicit page size selection
182 |                 # for formats other than A4. The PDF will auto-scale to fit.
183 |                 logger.warning(f"Page size '{page_size}' requested, but KiCAD 9.0 only supports A4 explicitly. Using auto-scale instead.")
184 | 
185 |             # Open plot for writing
186 |             # Note: For PDF, all layers are combined into a single file
187 |             # KiCAD prepends the board filename to the plot file name
188 |             base_name = os.path.basename(output_path).replace('.pdf', '')
189 |             plotter.OpenPlotfile(base_name, pcbnew.PLOT_FORMAT_PDF, '')
190 | 
191 |             # Plot specified layers or all enabled layers
192 |             plotted_layers = []
193 |             if layers:
194 |                 for layer_name in layers:
195 |                     layer_id = self.board.GetLayerID(layer_name)
196 |                     if layer_id >= 0:
197 |                         plotter.SetLayer(layer_id)
198 |                         plotter.PlotLayer()
199 |                         plotted_layers.append(layer_name)
200 |             else:
201 |                 for layer_id in range(pcbnew.PCB_LAYER_ID_COUNT):
202 |                     if self.board.IsLayerEnabled(layer_id):
203 |                         layer_name = self.board.GetLayerName(layer_id)
204 |                         plotter.SetLayer(layer_id)
205 |                         plotter.PlotLayer()
206 |                         plotted_layers.append(layer_name)
207 | 
208 |             # Close the plot file to finalize the PDF
209 |             plotter.ClosePlot()
210 | 
211 |             # KiCAD automatically prepends the board name to the output file
212 |             # Get the actual output filename that was created
213 |             board_name = os.path.splitext(os.path.basename(self.board.GetFileName()))[0]
214 |             actual_filename = f"{board_name}-{base_name}.pdf"
215 |             actual_output_path = os.path.join(os.path.dirname(output_path), actual_filename)
216 | 
217 |             return {
218 |                 "success": True,
219 |                 "message": "Exported PDF file",
220 |                 "file": {
221 |                     "path": actual_output_path,
222 |                     "requestedPath": output_path,
223 |                     "layers": plotted_layers,
224 |                     "pageSize": page_size if page_size == "A4" else "auto-scaled"
225 |                 }
226 |             }
227 | 
228 |         except Exception as e:
229 |             logger.error(f"Error exporting PDF file: {str(e)}")
230 |             return {
231 |                 "success": False,
232 |                 "message": "Failed to export PDF file",
233 |                 "errorDetails": str(e)
234 |             }
235 | 
236 |     def export_svg(self, params: Dict[str, Any]) -> Dict[str, Any]:
237 |         """Export SVG files"""
238 |         try:
239 |             if not self.board:
240 |                 return {
241 |                     "success": False,
242 |                     "message": "No board is loaded",
243 |                     "errorDetails": "Load or create a board first"
244 |                 }
245 | 
246 |             output_path = params.get("outputPath")
247 |             layers = params.get("layers", [])
248 |             black_and_white = params.get("blackAndWhite", False)
249 |             include_components = params.get("includeComponents", True)
250 | 
251 |             if not output_path:
252 |                 return {
253 |                     "success": False,
254 |                     "message": "Missing output path",
255 |                     "errorDetails": "outputPath parameter is required"
256 |                 }
257 | 
258 |             # Create output directory if it doesn't exist
259 |             output_path = os.path.abspath(os.path.expanduser(output_path))
260 |             os.makedirs(os.path.dirname(output_path), exist_ok=True)
261 | 
262 |             # Create plot controller
263 |             plotter = pcbnew.PLOT_CONTROLLER(self.board)
264 |             
265 |             # Set up plot options
266 |             plot_opts = plotter.GetPlotOptions()
267 |             plot_opts.SetOutputDirectory(os.path.dirname(output_path))
268 |             plot_opts.SetFormat(pcbnew.PLOT_FORMAT_SVG)
269 |             plot_opts.SetPlotValue(include_components)
270 |             plot_opts.SetPlotReference(include_components)
271 |             plot_opts.SetBlackAndWhite(black_and_white)
272 | 
273 |             # Plot specified layers or all enabled layers
274 |             plotted_layers = []
275 |             if layers:
276 |                 for layer_name in layers:
277 |                     layer_id = self.board.GetLayerID(layer_name)
278 |                     if layer_id >= 0:
279 |                         plotter.SetLayer(layer_id)
280 |                         plotter.PlotLayer()
281 |                         plotted_layers.append(layer_name)
282 |             else:
283 |                 for layer_id in range(pcbnew.PCB_LAYER_ID_COUNT):
284 |                     if self.board.IsLayerEnabled(layer_id):
285 |                         layer_name = self.board.GetLayerName(layer_id)
286 |                         plotter.SetLayer(layer_id)
287 |                         plotter.PlotLayer()
288 |                         plotted_layers.append(layer_name)
289 | 
290 |             return {
291 |                 "success": True,
292 |                 "message": "Exported SVG file",
293 |                 "file": {
294 |                     "path": output_path,
295 |                     "layers": plotted_layers
296 |                 }
297 |             }
298 | 
299 |         except Exception as e:
300 |             logger.error(f"Error exporting SVG file: {str(e)}")
301 |             return {
302 |                 "success": False,
303 |                 "message": "Failed to export SVG file",
304 |                 "errorDetails": str(e)
305 |             }
306 | 
307 |     def export_3d(self, params: Dict[str, Any]) -> Dict[str, Any]:
308 |         """Export 3D model files using kicad-cli (KiCAD 9.0 compatible)"""
309 |         import subprocess
310 |         import platform
311 |         import shutil
312 | 
313 |         try:
314 |             if not self.board:
315 |                 return {
316 |                     "success": False,
317 |                     "message": "No board is loaded",
318 |                     "errorDetails": "Load or create a board first"
319 |                 }
320 | 
321 |             output_path = params.get("outputPath")
322 |             format = params.get("format", "STEP")
323 |             include_components = params.get("includeComponents", True)
324 |             include_copper = params.get("includeCopper", True)
325 |             include_solder_mask = params.get("includeSolderMask", True)
326 |             include_silkscreen = params.get("includeSilkscreen", True)
327 | 
328 |             if not output_path:
329 |                 return {
330 |                     "success": False,
331 |                     "message": "Missing output path",
332 |                     "errorDetails": "outputPath parameter is required"
333 |                 }
334 | 
335 |             # Get board file path
336 |             board_file = self.board.GetFileName()
337 |             if not board_file or not os.path.exists(board_file):
338 |                 return {
339 |                     "success": False,
340 |                     "message": "Board file not found",
341 |                     "errorDetails": "Board must be saved before exporting 3D models"
342 |                 }
343 | 
344 |             # Create output directory if it doesn't exist
345 |             output_path = os.path.abspath(os.path.expanduser(output_path))
346 |             os.makedirs(os.path.dirname(output_path), exist_ok=True)
347 | 
348 |             # Find kicad-cli executable
349 |             kicad_cli = self._find_kicad_cli()
350 |             if not kicad_cli:
351 |                 return {
352 |                     "success": False,
353 |                     "message": "kicad-cli not found",
354 |                     "errorDetails": "KiCAD CLI tool not found. Install KiCAD 8.0+ or set PATH."
355 |                 }
356 | 
357 |             # Build command based on format
358 |             format_upper = format.upper()
359 | 
360 |             if format_upper == "STEP":
361 |                 cmd = [
362 |                     kicad_cli,
363 |                     'pcb', 'export', 'step',
364 |                     '--output', output_path,
365 |                     '--force'  # Overwrite existing file
366 |                 ]
367 | 
368 |                 # Add options based on parameters
369 |                 if not include_components:
370 |                     cmd.append('--no-components')
371 |                 if include_copper:
372 |                     cmd.extend(['--include-tracks', '--include-pads', '--include-zones'])
373 |                 if include_silkscreen:
374 |                     cmd.append('--include-silkscreen')
375 |                 if include_solder_mask:
376 |                     cmd.append('--include-soldermask')
377 | 
378 |                 cmd.append(board_file)
379 | 
380 |             elif format_upper == "VRML":
381 |                 cmd = [
382 |                     kicad_cli,
383 |                     'pcb', 'export', 'vrml',
384 |                     '--output', output_path,
385 |                     '--units', 'mm',  # Use mm for consistency
386 |                     '--force'
387 |                 ]
388 | 
389 |                 if not include_components:
390 |                     # Note: VRML export doesn't have a direct --no-components flag
391 |                     # The models will be included by default, but can be controlled via 3D settings
392 |                     pass
393 | 
394 |                 cmd.append(board_file)
395 | 
396 |             else:
397 |                 return {
398 |                     "success": False,
399 |                     "message": "Unsupported format",
400 |                     "errorDetails": f"Format {format} is not supported. Use 'STEP' or 'VRML'."
401 |                 }
402 | 
403 |             # Execute kicad-cli command
404 |             logger.info(f"Running 3D export command: {' '.join(cmd)}")
405 | 
406 |             result = subprocess.run(
407 |                 cmd,
408 |                 capture_output=True,
409 |                 text=True,
410 |                 timeout=300  # 5 minute timeout for 3D export
411 |             )
412 | 
413 |             if result.returncode != 0:
414 |                 logger.error(f"3D export command failed: {result.stderr}")
415 |                 return {
416 |                     "success": False,
417 |                     "message": "3D export command failed",
418 |                     "errorDetails": result.stderr
419 |                 }
420 | 
421 |             return {
422 |                 "success": True,
423 |                 "message": f"Exported {format_upper} file",
424 |                 "file": {
425 |                     "path": output_path,
426 |                     "format": format_upper
427 |                 }
428 |             }
429 | 
430 |         except subprocess.TimeoutExpired:
431 |             logger.error("3D export command timed out")
432 |             return {
433 |                 "success": False,
434 |                 "message": "3D export timed out",
435 |                 "errorDetails": "Export took longer than 5 minutes"
436 |             }
437 |         except Exception as e:
438 |             logger.error(f"Error exporting 3D model: {str(e)}")
439 |             return {
440 |                 "success": False,
441 |                 "message": "Failed to export 3D model",
442 |                 "errorDetails": str(e)
443 |             }
444 | 
445 |     def export_bom(self, params: Dict[str, Any]) -> Dict[str, Any]:
446 |         """Export Bill of Materials"""
447 |         try:
448 |             if not self.board:
449 |                 return {
450 |                     "success": False,
451 |                     "message": "No board is loaded",
452 |                     "errorDetails": "Load or create a board first"
453 |                 }
454 | 
455 |             output_path = params.get("outputPath")
456 |             format = params.get("format", "CSV")
457 |             group_by_value = params.get("groupByValue", True)
458 |             include_attributes = params.get("includeAttributes", [])
459 | 
460 |             if not output_path:
461 |                 return {
462 |                     "success": False,
463 |                     "message": "Missing output path",
464 |                     "errorDetails": "outputPath parameter is required"
465 |                 }
466 | 
467 |             # Create output directory if it doesn't exist
468 |             output_path = os.path.abspath(os.path.expanduser(output_path))
469 |             os.makedirs(os.path.dirname(output_path), exist_ok=True)
470 | 
471 |             # Get all components
472 |             components = []
473 |             for module in self.board.GetFootprints():
474 |                 component = {
475 |                     "reference": module.GetReference(),
476 |                     "value": module.GetValue(),
477 |                     "footprint": str(module.GetFPID()),
478 |                     "layer": self.board.GetLayerName(module.GetLayer())
479 |                 }
480 | 
481 |                 # Add requested attributes
482 |                 for attr in include_attributes:
483 |                     if hasattr(module, f"Get{attr}"):
484 |                         component[attr] = getattr(module, f"Get{attr}")()
485 | 
486 |                 components.append(component)
487 | 
488 |             # Group by value if requested
489 |             if group_by_value:
490 |                 grouped = {}
491 |                 for comp in components:
492 |                     key = f"{comp['value']}_{comp['footprint']}"
493 |                     if key not in grouped:
494 |                         grouped[key] = {
495 |                             "value": comp["value"],
496 |                             "footprint": comp["footprint"],
497 |                             "quantity": 1,
498 |                             "references": [comp["reference"]]
499 |                         }
500 |                     else:
501 |                         grouped[key]["quantity"] += 1
502 |                         grouped[key]["references"].append(comp["reference"])
503 |                 components = list(grouped.values())
504 | 
505 |             # Export based on format
506 |             if format == "CSV":
507 |                 self._export_bom_csv(output_path, components)
508 |             elif format == "XML":
509 |                 self._export_bom_xml(output_path, components)
510 |             elif format == "HTML":
511 |                 self._export_bom_html(output_path, components)
512 |             elif format == "JSON":
513 |                 self._export_bom_json(output_path, components)
514 |             else:
515 |                 return {
516 |                     "success": False,
517 |                     "message": "Unsupported format",
518 |                     "errorDetails": f"Format {format} is not supported"
519 |                 }
520 | 
521 |             return {
522 |                 "success": True,
523 |                 "message": f"Exported BOM to {format}",
524 |                 "file": {
525 |                     "path": output_path,
526 |                     "format": format,
527 |                     "componentCount": len(components)
528 |                 }
529 |             }
530 | 
531 |         except Exception as e:
532 |             logger.error(f"Error exporting BOM: {str(e)}")
533 |             return {
534 |                 "success": False,
535 |                 "message": "Failed to export BOM",
536 |                 "errorDetails": str(e)
537 |             }
538 | 
539 |     def _export_bom_csv(self, path: str, components: List[Dict[str, Any]]) -> None:
540 |         """Export BOM to CSV format"""
541 |         import csv
542 |         with open(path, 'w', newline='') as f:
543 |             writer = csv.DictWriter(f, fieldnames=components[0].keys())
544 |             writer.writeheader()
545 |             writer.writerows(components)
546 | 
547 |     def _export_bom_xml(self, path: str, components: List[Dict[str, Any]]) -> None:
548 |         """Export BOM to XML format"""
549 |         import xml.etree.ElementTree as ET
550 |         root = ET.Element("bom")
551 |         for comp in components:
552 |             comp_elem = ET.SubElement(root, "component")
553 |             for key, value in comp.items():
554 |                 elem = ET.SubElement(comp_elem, key)
555 |                 elem.text = str(value)
556 |         tree = ET.ElementTree(root)
557 |         tree.write(path, encoding='utf-8', xml_declaration=True)
558 | 
559 |     def _export_bom_html(self, path: str, components: List[Dict[str, Any]]) -> None:
560 |         """Export BOM to HTML format"""
561 |         html = ["<html><head><title>Bill of Materials</title></head><body>"]
562 |         html.append("<table border='1'><tr>")
563 |         # Headers
564 |         for key in components[0].keys():
565 |             html.append(f"<th>{key}</th>")
566 |         html.append("</tr>")
567 |         # Data
568 |         for comp in components:
569 |             html.append("<tr>")
570 |             for value in comp.values():
571 |                 html.append(f"<td>{value}</td>")
572 |             html.append("</tr>")
573 |         html.append("</table></body></html>")
574 |         with open(path, 'w') as f:
575 |             f.write("\n".join(html))
576 | 
577 |     def _export_bom_json(self, path: str, components: List[Dict[str, Any]]) -> None:
578 |         """Export BOM to JSON format"""
579 |         import json
580 |         with open(path, 'w') as f:
581 |             json.dump({"components": components}, f, indent=2)
582 | 
583 |     def _find_kicad_cli(self) -> Optional[str]:
584 |         """Find kicad-cli executable in system PATH or common locations
585 | 
586 |         Returns:
587 |             Path to kicad-cli executable, or None if not found
588 |         """
589 |         import shutil
590 |         import platform
591 | 
592 |         # Try system PATH first
593 |         cli_path = shutil.which("kicad-cli")
594 |         if cli_path:
595 |             return cli_path
596 | 
597 |         # Try platform-specific default locations
598 |         system = platform.system()
599 | 
600 |         if system == "Windows":
601 |             possible_paths = [
602 |                 r"C:\Program Files\KiCad\9.0\bin\kicad-cli.exe",
603 |                 r"C:\Program Files\KiCad\8.0\bin\kicad-cli.exe",
604 |                 r"C:\Program Files (x86)\KiCad\9.0\bin\kicad-cli.exe",
605 |                 r"C:\Program Files (x86)\KiCad\8.0\bin\kicad-cli.exe",
606 |             ]
607 |         elif system == "Darwin":  # macOS
608 |             possible_paths = [
609 |                 "/Applications/KiCad/KiCad.app/Contents/MacOS/kicad-cli",
610 |                 "/usr/local/bin/kicad-cli",
611 |             ]
612 |         else:  # Linux
613 |             possible_paths = [
614 |                 "/usr/bin/kicad-cli",
615 |                 "/usr/local/bin/kicad-cli",
616 |             ]
617 | 
618 |         for path in possible_paths:
619 |             if os.path.exists(path):
620 |                 return path
621 | 
622 |         return None
623 | 
```

--------------------------------------------------------------------------------
/python/commands/routing.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Routing-related command implementations for KiCAD interface
  3 | """
  4 | 
  5 | import os
  6 | import pcbnew
  7 | import logging
  8 | import math
  9 | from typing import Dict, Any, Optional, List, Tuple
 10 | 
 11 | logger = logging.getLogger('kicad_interface')
 12 | 
 13 | class RoutingCommands:
 14 |     """Handles routing-related KiCAD operations"""
 15 | 
 16 |     def __init__(self, board: Optional[pcbnew.BOARD] = None):
 17 |         """Initialize with optional board instance"""
 18 |         self.board = board
 19 | 
 20 |     def add_net(self, params: Dict[str, Any]) -> Dict[str, Any]:
 21 |         """Add a new net to the PCB"""
 22 |         try:
 23 |             if not self.board:
 24 |                 return {
 25 |                     "success": False,
 26 |                     "message": "No board is loaded",
 27 |                     "errorDetails": "Load or create a board first"
 28 |                 }
 29 | 
 30 |             name = params.get("name")
 31 |             net_class = params.get("class")
 32 | 
 33 |             if not name:
 34 |                 return {
 35 |                     "success": False,
 36 |                     "message": "Missing net name",
 37 |                     "errorDetails": "name parameter is required"
 38 |                 }
 39 | 
 40 |             # Create new net
 41 |             netinfo = self.board.GetNetInfo()
 42 |             nets_map = netinfo.NetsByName()
 43 |             if nets_map.has_key(name):
 44 |                 net = nets_map[name]
 45 |             else:
 46 |                 net = pcbnew.NETINFO_ITEM(self.board, name)
 47 |                 self.board.Add(net)
 48 | 
 49 |             # Set net class if provided
 50 |             if net_class:
 51 |                 net_classes = self.board.GetNetClasses()
 52 |                 if net_classes.Find(net_class):
 53 |                     net.SetClass(net_classes.Find(net_class))
 54 | 
 55 |             return {
 56 |                 "success": True,
 57 |                 "message": f"Added net: {name}",
 58 |                 "net": {
 59 |                     "name": name,
 60 |                     "class": net_class if net_class else "Default",
 61 |                     "netcode": net.GetNetCode()
 62 |                 }
 63 |             }
 64 | 
 65 |         except Exception as e:
 66 |             logger.error(f"Error adding net: {str(e)}")
 67 |             return {
 68 |                 "success": False,
 69 |                 "message": "Failed to add net",
 70 |                 "errorDetails": str(e)
 71 |             }
 72 | 
 73 |     def route_trace(self, params: Dict[str, Any]) -> Dict[str, Any]:
 74 |         """Route a trace between two points or pads"""
 75 |         try:
 76 |             if not self.board:
 77 |                 return {
 78 |                     "success": False,
 79 |                     "message": "No board is loaded",
 80 |                     "errorDetails": "Load or create a board first"
 81 |                 }
 82 | 
 83 |             start = params.get("start")
 84 |             end = params.get("end")
 85 |             layer = params.get("layer", "F.Cu")
 86 |             width = params.get("width")
 87 |             net = params.get("net")
 88 |             via = params.get("via", False)
 89 | 
 90 |             if not start or not end:
 91 |                 return {
 92 |                     "success": False,
 93 |                     "message": "Missing parameters",
 94 |                     "errorDetails": "start and end points are required"
 95 |                 }
 96 | 
 97 |             # Get layer ID
 98 |             layer_id = self.board.GetLayerID(layer)
 99 |             if layer_id < 0:
100 |                 return {
101 |                     "success": False,
102 |                     "message": "Invalid layer",
103 |                     "errorDetails": f"Layer '{layer}' does not exist"
104 |                 }
105 | 
106 |             # Get start point
107 |             start_point = self._get_point(start)
108 |             end_point = self._get_point(end)
109 | 
110 |             # Create track segment
111 |             track = pcbnew.PCB_TRACK(self.board)
112 |             track.SetStart(start_point)
113 |             track.SetEnd(end_point)
114 |             track.SetLayer(layer_id)
115 | 
116 |             # Set width (default to board's current track width)
117 |             if width:
118 |                 track.SetWidth(int(width * 1000000))  # Convert mm to nm
119 |             else:
120 |                 track.SetWidth(self.board.GetDesignSettings().GetCurrentTrackWidth())
121 | 
122 |             # Set net if provided
123 |             if net:
124 |                 netinfo = self.board.GetNetInfo()
125 |                 nets_map = netinfo.NetsByName()
126 |                 if nets_map.has_key(net):
127 |                     net_obj = nets_map[net]
128 |                     track.SetNet(net_obj)
129 | 
130 |             # Add track to board
131 |             self.board.Add(track)
132 | 
133 |             # Add via if requested and net is specified
134 |             if via and net:
135 |                 via_point = end_point
136 |                 self.add_via({
137 |                     "position": {
138 |                         "x": via_point.x / 1000000,
139 |                         "y": via_point.y / 1000000,
140 |                         "unit": "mm"
141 |                     },
142 |                     "net": net
143 |                 })
144 | 
145 |             return {
146 |                 "success": True,
147 |                 "message": "Added trace",
148 |                 "trace": {
149 |                     "start": {
150 |                         "x": start_point.x / 1000000,
151 |                         "y": start_point.y / 1000000,
152 |                         "unit": "mm"
153 |                     },
154 |                     "end": {
155 |                         "x": end_point.x / 1000000,
156 |                         "y": end_point.y / 1000000,
157 |                         "unit": "mm"
158 |                     },
159 |                     "layer": layer,
160 |                     "width": track.GetWidth() / 1000000,
161 |                     "net": net
162 |                 }
163 |             }
164 | 
165 |         except Exception as e:
166 |             logger.error(f"Error routing trace: {str(e)}")
167 |             return {
168 |                 "success": False,
169 |                 "message": "Failed to route trace",
170 |                 "errorDetails": str(e)
171 |             }
172 | 
173 |     def add_via(self, params: Dict[str, Any]) -> Dict[str, Any]:
174 |         """Add a via at the specified location"""
175 |         try:
176 |             if not self.board:
177 |                 return {
178 |                     "success": False,
179 |                     "message": "No board is loaded",
180 |                     "errorDetails": "Load or create a board first"
181 |                 }
182 | 
183 |             position = params.get("position")
184 |             size = params.get("size")
185 |             drill = params.get("drill")
186 |             net = params.get("net")
187 |             from_layer = params.get("from_layer", "F.Cu")
188 |             to_layer = params.get("to_layer", "B.Cu")
189 | 
190 |             if not position:
191 |                 return {
192 |                     "success": False,
193 |                     "message": "Missing position",
194 |                     "errorDetails": "position parameter is required"
195 |                 }
196 | 
197 |             # Create via
198 |             via = pcbnew.PCB_VIA(self.board)
199 |             
200 |             # Set position
201 |             scale = 1000000 if position["unit"] == "mm" else 25400000  # mm or inch to nm
202 |             x_nm = int(position["x"] * scale)
203 |             y_nm = int(position["y"] * scale)
204 |             via.SetPosition(pcbnew.VECTOR2I(x_nm, y_nm))
205 | 
206 |             # Set size and drill (default to board's current via settings)
207 |             design_settings = self.board.GetDesignSettings()
208 |             via.SetWidth(int(size * 1000000) if size else design_settings.GetCurrentViaSize())
209 |             via.SetDrill(int(drill * 1000000) if drill else design_settings.GetCurrentViaDrill())
210 | 
211 |             # Set layers
212 |             from_id = self.board.GetLayerID(from_layer)
213 |             to_id = self.board.GetLayerID(to_layer)
214 |             if from_id < 0 or to_id < 0:
215 |                 return {
216 |                     "success": False,
217 |                     "message": "Invalid layer",
218 |                     "errorDetails": "Specified layers do not exist"
219 |                 }
220 |             via.SetLayerPair(from_id, to_id)
221 | 
222 |             # Set net if provided
223 |             if net:
224 |                 netinfo = self.board.GetNetInfo()
225 |                 nets_map = netinfo.NetsByName()
226 |                 if nets_map.has_key(net):
227 |                     net_obj = nets_map[net]
228 |                     via.SetNet(net_obj)
229 | 
230 |             # Add via to board
231 |             self.board.Add(via)
232 | 
233 |             return {
234 |                 "success": True,
235 |                 "message": "Added via",
236 |                 "via": {
237 |                     "position": {
238 |                         "x": position["x"],
239 |                         "y": position["y"],
240 |                         "unit": position["unit"]
241 |                     },
242 |                     "size": via.GetWidth() / 1000000,
243 |                     "drill": via.GetDrill() / 1000000,
244 |                     "from_layer": from_layer,
245 |                     "to_layer": to_layer,
246 |                     "net": net
247 |                 }
248 |             }
249 | 
250 |         except Exception as e:
251 |             logger.error(f"Error adding via: {str(e)}")
252 |             return {
253 |                 "success": False,
254 |                 "message": "Failed to add via",
255 |                 "errorDetails": str(e)
256 |             }
257 | 
258 |     def delete_trace(self, params: Dict[str, Any]) -> Dict[str, Any]:
259 |         """Delete a trace from the PCB"""
260 |         try:
261 |             if not self.board:
262 |                 return {
263 |                     "success": False,
264 |                     "message": "No board is loaded",
265 |                     "errorDetails": "Load or create a board first"
266 |                 }
267 | 
268 |             trace_uuid = params.get("traceUuid")
269 |             position = params.get("position")
270 | 
271 |             if not trace_uuid and not position:
272 |                 return {
273 |                     "success": False,
274 |                     "message": "Missing parameters",
275 |                     "errorDetails": "Either traceUuid or position must be provided"
276 |                 }
277 | 
278 |             # Find track by UUID
279 |             if trace_uuid:
280 |                 track = None
281 |                 for item in self.board.Tracks():
282 |                     if str(item.m_Uuid) == trace_uuid:
283 |                         track = item
284 |                         break
285 | 
286 |                 if not track:
287 |                     return {
288 |                         "success": False,
289 |                         "message": "Track not found",
290 |                         "errorDetails": f"Could not find track with UUID: {trace_uuid}"
291 |                     }
292 | 
293 |                 self.board.Remove(track)
294 |                 return {
295 |                     "success": True,
296 |                     "message": f"Deleted track: {trace_uuid}"
297 |                 }
298 | 
299 |             # Find track by position
300 |             if position:
301 |                 scale = 1000000 if position["unit"] == "mm" else 25400000  # mm or inch to nm
302 |                 x_nm = int(position["x"] * scale)
303 |                 y_nm = int(position["y"] * scale)
304 |                 point = pcbnew.VECTOR2I(x_nm, y_nm)
305 | 
306 |                 # Find closest track
307 |                 closest_track = None
308 |                 min_distance = float('inf')
309 |                 for track in self.board.Tracks():
310 |                     dist = self._point_to_track_distance(point, track)
311 |                     if dist < min_distance:
312 |                         min_distance = dist
313 |                         closest_track = track
314 | 
315 |                 if closest_track and min_distance < 1000000:  # Within 1mm
316 |                     self.board.Remove(closest_track)
317 |                     return {
318 |                         "success": True,
319 |                         "message": "Deleted track at specified position"
320 |                     }
321 |                 else:
322 |                     return {
323 |                         "success": False,
324 |                         "message": "No track found",
325 |                         "errorDetails": "No track found near specified position"
326 |                     }
327 | 
328 |         except Exception as e:
329 |             logger.error(f"Error deleting trace: {str(e)}")
330 |             return {
331 |                 "success": False,
332 |                 "message": "Failed to delete trace",
333 |                 "errorDetails": str(e)
334 |             }
335 | 
336 |     def get_nets_list(self, params: Dict[str, Any]) -> Dict[str, Any]:
337 |         """Get a list of all nets in the PCB"""
338 |         try:
339 |             if not self.board:
340 |                 return {
341 |                     "success": False,
342 |                     "message": "No board is loaded",
343 |                     "errorDetails": "Load or create a board first"
344 |                 }
345 | 
346 |             nets = []
347 |             netinfo = self.board.GetNetInfo()
348 |             for net_code in range(netinfo.GetNetCount()):
349 |                 net = netinfo.GetNetItem(net_code)
350 |                 if net:
351 |                     nets.append({
352 |                         "name": net.GetNetname(),
353 |                         "code": net.GetNetCode(),
354 |                         "class": net.GetClassName()
355 |                     })
356 | 
357 |             return {
358 |                 "success": True,
359 |                 "nets": nets
360 |             }
361 | 
362 |         except Exception as e:
363 |             logger.error(f"Error getting nets list: {str(e)}")
364 |             return {
365 |                 "success": False,
366 |                 "message": "Failed to get nets list",
367 |                 "errorDetails": str(e)
368 |             }
369 |             
370 |     def create_netclass(self, params: Dict[str, Any]) -> Dict[str, Any]:
371 |         """Create a new net class with specified properties"""
372 |         try:
373 |             if not self.board:
374 |                 return {
375 |                     "success": False,
376 |                     "message": "No board is loaded",
377 |                     "errorDetails": "Load or create a board first"
378 |                 }
379 | 
380 |             name = params.get("name")
381 |             clearance = params.get("clearance")
382 |             track_width = params.get("trackWidth")
383 |             via_diameter = params.get("viaDiameter")
384 |             via_drill = params.get("viaDrill")
385 |             uvia_diameter = params.get("uviaDiameter")
386 |             uvia_drill = params.get("uviaDrill")
387 |             diff_pair_width = params.get("diffPairWidth")
388 |             diff_pair_gap = params.get("diffPairGap")
389 |             nets = params.get("nets", [])
390 | 
391 |             if not name:
392 |                 return {
393 |                     "success": False,
394 |                     "message": "Missing netclass name",
395 |                     "errorDetails": "name parameter is required"
396 |                 }
397 | 
398 |             # Get net classes
399 |             net_classes = self.board.GetNetClasses()
400 |             
401 |             # Create new net class if it doesn't exist
402 |             if not net_classes.Find(name):
403 |                 netclass = pcbnew.NETCLASS(name)
404 |                 net_classes.Add(netclass)
405 |             else:
406 |                 netclass = net_classes.Find(name)
407 | 
408 |             # Set properties
409 |             scale = 1000000  # mm to nm
410 |             if clearance is not None:
411 |                 netclass.SetClearance(int(clearance * scale))
412 |             if track_width is not None:
413 |                 netclass.SetTrackWidth(int(track_width * scale))
414 |             if via_diameter is not None:
415 |                 netclass.SetViaDiameter(int(via_diameter * scale))
416 |             if via_drill is not None:
417 |                 netclass.SetViaDrill(int(via_drill * scale))
418 |             if uvia_diameter is not None:
419 |                 netclass.SetMicroViaDiameter(int(uvia_diameter * scale))
420 |             if uvia_drill is not None:
421 |                 netclass.SetMicroViaDrill(int(uvia_drill * scale))
422 |             if diff_pair_width is not None:
423 |                 netclass.SetDiffPairWidth(int(diff_pair_width * scale))
424 |             if diff_pair_gap is not None:
425 |                 netclass.SetDiffPairGap(int(diff_pair_gap * scale))
426 | 
427 |             # Add nets to net class
428 |             netinfo = self.board.GetNetInfo()
429 |             nets_map = netinfo.NetsByName()
430 |             for net_name in nets:
431 |                 if nets_map.has_key(net_name):
432 |                     net = nets_map[net_name]
433 |                     net.SetClass(netclass)
434 | 
435 |             return {
436 |                 "success": True,
437 |                 "message": f"Created net class: {name}",
438 |                 "netClass": {
439 |                     "name": name,
440 |                     "clearance": netclass.GetClearance() / scale,
441 |                     "trackWidth": netclass.GetTrackWidth() / scale,
442 |                     "viaDiameter": netclass.GetViaDiameter() / scale,
443 |                     "viaDrill": netclass.GetViaDrill() / scale,
444 |                     "uviaDiameter": netclass.GetMicroViaDiameter() / scale,
445 |                     "uviaDrill": netclass.GetMicroViaDrill() / scale,
446 |                     "diffPairWidth": netclass.GetDiffPairWidth() / scale,
447 |                     "diffPairGap": netclass.GetDiffPairGap() / scale,
448 |                     "nets": nets
449 |                 }
450 |             }
451 | 
452 |         except Exception as e:
453 |             logger.error(f"Error creating net class: {str(e)}")
454 |             return {
455 |                 "success": False,
456 |                 "message": "Failed to create net class",
457 |                 "errorDetails": str(e)
458 |             }
459 |             
460 |     def add_copper_pour(self, params: Dict[str, Any]) -> Dict[str, Any]:
461 |         """Add a copper pour (zone) to the PCB"""
462 |         try:
463 |             if not self.board:
464 |                 return {
465 |                     "success": False,
466 |                     "message": "No board is loaded",
467 |                     "errorDetails": "Load or create a board first"
468 |                 }
469 | 
470 |             layer = params.get("layer", "F.Cu")
471 |             net = params.get("net")
472 |             clearance = params.get("clearance")
473 |             min_width = params.get("minWidth", 0.2)
474 |             points = params.get("points", [])
475 |             priority = params.get("priority", 0)
476 |             fill_type = params.get("fillType", "solid")  # solid or hatched
477 |             
478 |             if not points or len(points) < 3:
479 |                 return {
480 |                     "success": False,
481 |                     "message": "Missing points",
482 |                     "errorDetails": "At least 3 points are required for copper pour outline"
483 |                 }
484 | 
485 |             # Get layer ID
486 |             layer_id = self.board.GetLayerID(layer)
487 |             if layer_id < 0:
488 |                 return {
489 |                     "success": False,
490 |                     "message": "Invalid layer",
491 |                     "errorDetails": f"Layer '{layer}' does not exist"
492 |                 }
493 | 
494 |             # Create zone
495 |             zone = pcbnew.ZONE(self.board)
496 |             zone.SetLayer(layer_id)
497 |             
498 |             # Set net if provided
499 |             if net:
500 |                 netinfo = self.board.GetNetInfo()
501 |                 nets_map = netinfo.NetsByName()
502 |                 if nets_map.has_key(net):
503 |                     net_obj = nets_map[net]
504 |                     zone.SetNet(net_obj)
505 |             
506 |             # Set zone properties
507 |             scale = 1000000  # mm to nm
508 |             zone.SetAssignedPriority(priority)
509 |             
510 |             if clearance is not None:
511 |                 zone.SetLocalClearance(int(clearance * scale))
512 |             
513 |             zone.SetMinThickness(int(min_width * scale))
514 |             
515 |             # Set fill type
516 |             if fill_type == "hatched":
517 |                 zone.SetFillMode(pcbnew.ZONE_FILL_MODE_HATCH_PATTERN)
518 |             else:
519 |                 zone.SetFillMode(pcbnew.ZONE_FILL_MODE_POLYGONS)
520 |             
521 |             # Create outline
522 |             outline = zone.Outline()
523 |             outline.NewOutline()  # Create a new outline contour first
524 | 
525 |             # Add points to outline
526 |             for point in points:
527 |                 scale = 1000000 if point.get("unit", "mm") == "mm" else 25400000
528 |                 x_nm = int(point["x"] * scale)
529 |                 y_nm = int(point["y"] * scale)
530 |                 outline.Append(pcbnew.VECTOR2I(x_nm, y_nm))  # Add point to outline
531 |             
532 |             # Add zone to board
533 |             self.board.Add(zone)
534 | 
535 |             # Fill zone
536 |             # Note: Zone filling can cause issues with SWIG API
537 |             # Comment out for now - zones will be filled when board is saved/opened in KiCAD
538 |             # filler = pcbnew.ZONE_FILLER(self.board)
539 |             # filler.Fill(self.board.Zones())
540 | 
541 |             return {
542 |                 "success": True,
543 |                 "message": "Added copper pour",
544 |                 "pour": {
545 |                     "layer": layer,
546 |                     "net": net,
547 |                     "clearance": clearance,
548 |                     "minWidth": min_width,
549 |                     "priority": priority,
550 |                     "fillType": fill_type,
551 |                     "pointCount": len(points)
552 |                 }
553 |             }
554 | 
555 |         except Exception as e:
556 |             logger.error(f"Error adding copper pour: {str(e)}")
557 |             return {
558 |                 "success": False,
559 |                 "message": "Failed to add copper pour",
560 |                 "errorDetails": str(e)
561 |             }
562 |             
563 |     def route_differential_pair(self, params: Dict[str, Any]) -> Dict[str, Any]:
564 |         """Route a differential pair between two sets of points or pads"""
565 |         try:
566 |             if not self.board:
567 |                 return {
568 |                     "success": False,
569 |                     "message": "No board is loaded",
570 |                     "errorDetails": "Load or create a board first"
571 |                 }
572 | 
573 |             start_pos = params.get("startPos")
574 |             end_pos = params.get("endPos")
575 |             net_pos = params.get("netPos")
576 |             net_neg = params.get("netNeg")
577 |             layer = params.get("layer", "F.Cu")
578 |             width = params.get("width")
579 |             gap = params.get("gap")
580 | 
581 |             if not start_pos or not end_pos or not net_pos or not net_neg:
582 |                 return {
583 |                     "success": False,
584 |                     "message": "Missing parameters",
585 |                     "errorDetails": "startPos, endPos, netPos, and netNeg are required"
586 |                 }
587 | 
588 |             # Get layer ID
589 |             layer_id = self.board.GetLayerID(layer)
590 |             if layer_id < 0:
591 |                 return {
592 |                     "success": False,
593 |                     "message": "Invalid layer",
594 |                     "errorDetails": f"Layer '{layer}' does not exist"
595 |                 }
596 | 
597 |             # Get nets
598 |             netinfo = self.board.GetNetInfo()
599 |             nets_map = netinfo.NetsByName()
600 | 
601 |             net_pos_obj = nets_map[net_pos] if nets_map.has_key(net_pos) else None
602 |             net_neg_obj = nets_map[net_neg] if nets_map.has_key(net_neg) else None
603 | 
604 |             if not net_pos_obj or not net_neg_obj:
605 |                 return {
606 |                     "success": False,
607 |                     "message": "Nets not found",
608 |                     "errorDetails": "One or both nets specified for the differential pair do not exist"
609 |                 }
610 | 
611 |             # Get start and end points
612 |             start_point = self._get_point(start_pos)
613 |             end_point = self._get_point(end_pos)
614 |             
615 |             # Calculate offset vectors for the two traces
616 |             # First, get the direction vector from start to end
617 |             dx = end_point.x - start_point.x
618 |             dy = end_point.y - start_point.y
619 |             length = math.sqrt(dx * dx + dy * dy)
620 |             
621 |             if length <= 0:
622 |                 return {
623 |                     "success": False,
624 |                     "message": "Invalid points",
625 |                     "errorDetails": "Start and end points must be different"
626 |                 }
627 |                 
628 |             # Normalize direction vector
629 |             dx /= length
630 |             dy /= length
631 |             
632 |             # Get perpendicular vector
633 |             px = -dy
634 |             py = dx
635 |             
636 |             # Set default gap if not provided
637 |             if gap is None:
638 |                 gap = 0.2  # mm
639 |                 
640 |             # Convert to nm
641 |             gap_nm = int(gap * 1000000)
642 |             
643 |             # Calculate offsets
644 |             offset_x = int(px * gap_nm / 2)
645 |             offset_y = int(py * gap_nm / 2)
646 |             
647 |             # Create positive and negative trace points
648 |             pos_start = pcbnew.VECTOR2I(int(start_point.x + offset_x), int(start_point.y + offset_y))
649 |             pos_end = pcbnew.VECTOR2I(int(end_point.x + offset_x), int(end_point.y + offset_y))
650 |             neg_start = pcbnew.VECTOR2I(int(start_point.x - offset_x), int(start_point.y - offset_y))
651 |             neg_end = pcbnew.VECTOR2I(int(end_point.x - offset_x), int(end_point.y - offset_y))
652 |             
653 |             # Create positive trace
654 |             pos_track = pcbnew.PCB_TRACK(self.board)
655 |             pos_track.SetStart(pos_start)
656 |             pos_track.SetEnd(pos_end)
657 |             pos_track.SetLayer(layer_id)
658 |             pos_track.SetNet(net_pos_obj)
659 |             
660 |             # Create negative trace
661 |             neg_track = pcbnew.PCB_TRACK(self.board)
662 |             neg_track.SetStart(neg_start)
663 |             neg_track.SetEnd(neg_end)
664 |             neg_track.SetLayer(layer_id)
665 |             neg_track.SetNet(net_neg_obj)
666 |             
667 |             # Set width
668 |             if width:
669 |                 trace_width_nm = int(width * 1000000)
670 |                 pos_track.SetWidth(trace_width_nm)
671 |                 neg_track.SetWidth(trace_width_nm)
672 |             else:
673 |                 # Get default width from design rules or net class
674 |                 trace_width = self.board.GetDesignSettings().GetCurrentTrackWidth()
675 |                 pos_track.SetWidth(trace_width)
676 |                 neg_track.SetWidth(trace_width)
677 |             
678 |             # Add tracks to board
679 |             self.board.Add(pos_track)
680 |             self.board.Add(neg_track)
681 | 
682 |             return {
683 |                 "success": True,
684 |                 "message": "Added differential pair traces",
685 |                 "diffPair": {
686 |                     "posNet": net_pos,
687 |                     "negNet": net_neg,
688 |                     "layer": layer,
689 |                     "width": pos_track.GetWidth() / 1000000,
690 |                     "gap": gap,
691 |                     "length": length / 1000000
692 |                 }
693 |             }
694 | 
695 |         except Exception as e:
696 |             logger.error(f"Error routing differential pair: {str(e)}")
697 |             return {
698 |                 "success": False,
699 |                 "message": "Failed to route differential pair",
700 |                 "errorDetails": str(e)
701 |             }
702 | 
703 |     def _get_point(self, point_spec: Dict[str, Any]) -> pcbnew.VECTOR2I:
704 |         """Convert point specification to KiCAD point"""
705 |         if "x" in point_spec and "y" in point_spec:
706 |             scale = 1000000 if point_spec.get("unit", "mm") == "mm" else 25400000
707 |             x_nm = int(point_spec["x"] * scale)
708 |             y_nm = int(point_spec["y"] * scale)
709 |             return pcbnew.VECTOR2I(x_nm, y_nm)
710 |         elif "pad" in point_spec and "componentRef" in point_spec:
711 |             module = self.board.FindFootprintByReference(point_spec["componentRef"])
712 |             if module:
713 |                 pad = module.FindPadByName(point_spec["pad"])
714 |                 if pad:
715 |                     return pad.GetPosition()
716 |         raise ValueError("Invalid point specification")
717 | 
718 |     def _point_to_track_distance(self, point: pcbnew.VECTOR2I, track: pcbnew.PCB_TRACK) -> float:
719 |         """Calculate distance from point to track segment"""
720 |         start = track.GetStart()
721 |         end = track.GetEnd()
722 |         
723 |         # Vector from start to end
724 |         v = pcbnew.VECTOR2I(end.x - start.x, end.y - start.y)
725 |         # Vector from start to point
726 |         w = pcbnew.VECTOR2I(point.x - start.x, point.y - start.y)
727 |         
728 |         # Length of track squared
729 |         c1 = v.x * v.x + v.y * v.y
730 |         if c1 == 0:
731 |             return self._point_distance(point, start)
732 |             
733 |         # Projection coefficient
734 |         c2 = float(w.x * v.x + w.y * v.y) / c1
735 |         
736 |         if c2 < 0:
737 |             return self._point_distance(point, start)
738 |         elif c2 > 1:
739 |             return self._point_distance(point, end)
740 |             
741 |         # Point on line
742 |         proj = pcbnew.VECTOR2I(
743 |             int(start.x + c2 * v.x),
744 |             int(start.y + c2 * v.y)
745 |         )
746 |         return self._point_distance(point, proj)
747 | 
748 |     def _point_distance(self, p1: pcbnew.VECTOR2I, p2: pcbnew.VECTOR2I) -> float:
749 |         """Calculate distance between two points"""
750 |         dx = p1.x - p2.x
751 |         dy = p1.y - p2.y
752 |         return (dx * dx + dy * dy) ** 0.5
753 | 
```
Page 4/6FirstPrevNextLast