#
tokens: 35475/50000 3/94 files (page 5/6)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 5 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/component.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Component-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 | import base64
 11 | from commands.library import LibraryManager
 12 | 
 13 | logger = logging.getLogger('kicad_interface')
 14 | 
 15 | class ComponentCommands:
 16 |     """Handles component-related KiCAD operations"""
 17 | 
 18 |     def __init__(self, board: Optional[pcbnew.BOARD] = None, library_manager: Optional[LibraryManager] = None):
 19 |         """Initialize with optional board instance and library manager"""
 20 |         self.board = board
 21 |         self.library_manager = library_manager or LibraryManager()
 22 | 
 23 |     def place_component(self, params: Dict[str, Any]) -> Dict[str, Any]:
 24 |         """Place a component on the PCB"""
 25 |         try:
 26 |             if not self.board:
 27 |                 return {
 28 |                     "success": False,
 29 |                     "message": "No board is loaded",
 30 |                     "errorDetails": "Load or create a board first"
 31 |                 }
 32 | 
 33 |             # Get parameters
 34 |             component_id = params.get("componentId")
 35 |             position = params.get("position")
 36 |             reference = params.get("reference")
 37 |             value = params.get("value")
 38 |             footprint = params.get("footprint")
 39 |             rotation = params.get("rotation", 0)
 40 |             layer = params.get("layer", "F.Cu")
 41 | 
 42 |             if not component_id or not position:
 43 |                 return {
 44 |                     "success": False,
 45 |                     "message": "Missing parameters",
 46 |                     "errorDetails": "componentId and position are required"
 47 |                 }
 48 | 
 49 |             # Find footprint using library manager
 50 |             # component_id can be "Library:Footprint" or just "Footprint"
 51 |             footprint_result = self.library_manager.find_footprint(component_id)
 52 | 
 53 |             if not footprint_result:
 54 |                 # Try to suggest similar footprints
 55 |                 suggestions = self.library_manager.search_footprints(f"*{component_id}*", limit=5)
 56 |                 suggestion_text = ""
 57 |                 if suggestions:
 58 |                     suggestion_text = "\n\nDid you mean one of these?\n" + \
 59 |                                     "\n".join([f"  - {s['full_name']}" for s in suggestions])
 60 | 
 61 |                 return {
 62 |                     "success": False,
 63 |                     "message": "Footprint not found",
 64 |                     "errorDetails": f"Could not find footprint: {component_id}{suggestion_text}"
 65 |                 }
 66 | 
 67 |             library_path, footprint_name = footprint_result
 68 | 
 69 |             # Load footprint from library
 70 |             # Extract library nickname from path
 71 |             library_nickname = None
 72 |             for nick, path in self.library_manager.libraries.items():
 73 |                 if path == library_path:
 74 |                     library_nickname = nick
 75 |                     break
 76 | 
 77 |             if not library_nickname:
 78 |                 return {
 79 |                     "success": False,
 80 |                     "message": "Internal error",
 81 |                     "errorDetails": "Could not determine library nickname"
 82 |                 }
 83 | 
 84 |             # Load the footprint
 85 |             module = pcbnew.FootprintLoad(library_path, footprint_name)
 86 |             if not module:
 87 |                 return {
 88 |                     "success": False,
 89 |                     "message": "Failed to load footprint",
 90 |                     "errorDetails": f"Could not load footprint from {library_path}/{footprint_name}"
 91 |                 }
 92 | 
 93 |             # Set position
 94 |             scale = 1000000 if position["unit"] == "mm" else 25400000  # mm or inch to nm
 95 |             x_nm = int(position["x"] * scale)
 96 |             y_nm = int(position["y"] * scale)
 97 |             module.SetPosition(pcbnew.VECTOR2I(x_nm, y_nm))
 98 | 
 99 |             # Set reference if provided
100 |             if reference:
101 |                 module.SetReference(reference)
102 | 
103 |             # Set value if provided
104 |             if value:
105 |                 module.SetValue(value)
106 | 
107 |             # Set footprint if provided
108 |             if footprint:
109 |                 module.SetFootprintName(footprint)
110 | 
111 |             # Set rotation (KiCAD 9.0 uses EDA_ANGLE)
112 |             angle = pcbnew.EDA_ANGLE(rotation, pcbnew.DEGREES_T)
113 |             module.SetOrientation(angle)
114 | 
115 |             # Set layer
116 |             layer_id = self.board.GetLayerID(layer)
117 |             if layer_id >= 0:
118 |                 module.SetLayer(layer_id)
119 | 
120 |             # Add to board
121 |             self.board.Add(module)
122 | 
123 |             return {
124 |                 "success": True,
125 |                 "message": f"Placed component: {component_id}",
126 |                 "component": {
127 |                     "reference": module.GetReference(),
128 |                     "value": module.GetValue(),
129 |                     "position": {
130 |                         "x": position["x"],
131 |                         "y": position["y"],
132 |                         "unit": position["unit"]
133 |                     },
134 |                     "rotation": rotation,
135 |                     "layer": layer
136 |                 }
137 |             }
138 | 
139 |         except Exception as e:
140 |             logger.error(f"Error placing component: {str(e)}")
141 |             return {
142 |                 "success": False,
143 |                 "message": "Failed to place component",
144 |                 "errorDetails": str(e)
145 |             }
146 | 
147 |     def move_component(self, params: Dict[str, Any]) -> Dict[str, Any]:
148 |         """Move an existing component to a new position"""
149 |         try:
150 |             if not self.board:
151 |                 return {
152 |                     "success": False,
153 |                     "message": "No board is loaded",
154 |                     "errorDetails": "Load or create a board first"
155 |                 }
156 | 
157 |             reference = params.get("reference")
158 |             position = params.get("position")
159 |             rotation = params.get("rotation")
160 | 
161 |             if not reference or not position:
162 |                 return {
163 |                     "success": False,
164 |                     "message": "Missing parameters",
165 |                     "errorDetails": "reference and position are required"
166 |                 }
167 | 
168 |             # Find the component
169 |             module = self.board.FindFootprintByReference(reference)
170 |             if not module:
171 |                 return {
172 |                     "success": False,
173 |                     "message": "Component not found",
174 |                     "errorDetails": f"Could not find component: {reference}"
175 |                 }
176 | 
177 |             # Set new position
178 |             scale = 1000000 if position["unit"] == "mm" else 25400000  # mm or inch to nm
179 |             x_nm = int(position["x"] * scale)
180 |             y_nm = int(position["y"] * scale)
181 |             module.SetPosition(pcbnew.VECTOR2I(x_nm, y_nm))
182 | 
183 |             # Set new rotation if provided
184 |             if rotation is not None:
185 |                 angle = pcbnew.EDA_ANGLE(rotation, pcbnew.DEGREES_T)
186 |                 module.SetOrientation(angle)
187 | 
188 |             return {
189 |                 "success": True,
190 |                 "message": f"Moved component: {reference}",
191 |                 "component": {
192 |                     "reference": reference,
193 |                     "position": {
194 |                         "x": position["x"],
195 |                         "y": position["y"],
196 |                         "unit": position["unit"]
197 |                     },
198 |                     "rotation": rotation if rotation is not None else module.GetOrientation().AsDegrees()
199 |                 }
200 |             }
201 | 
202 |         except Exception as e:
203 |             logger.error(f"Error moving component: {str(e)}")
204 |             return {
205 |                 "success": False,
206 |                 "message": "Failed to move component",
207 |                 "errorDetails": str(e)
208 |             }
209 | 
210 |     def rotate_component(self, params: Dict[str, Any]) -> Dict[str, Any]:
211 |         """Rotate an existing component"""
212 |         try:
213 |             if not self.board:
214 |                 return {
215 |                     "success": False,
216 |                     "message": "No board is loaded",
217 |                     "errorDetails": "Load or create a board first"
218 |                 }
219 | 
220 |             reference = params.get("reference")
221 |             angle = params.get("angle")
222 | 
223 |             if not reference or angle is None:
224 |                 return {
225 |                     "success": False,
226 |                     "message": "Missing parameters",
227 |                     "errorDetails": "reference and angle are required"
228 |                 }
229 | 
230 |             # Find the component
231 |             module = self.board.FindFootprintByReference(reference)
232 |             if not module:
233 |                 return {
234 |                     "success": False,
235 |                     "message": "Component not found",
236 |                     "errorDetails": f"Could not find component: {reference}"
237 |                 }
238 | 
239 |             # Set rotation
240 |             rotation_angle = pcbnew.EDA_ANGLE(angle, pcbnew.DEGREES_T)
241 |             module.SetOrientation(rotation_angle)
242 | 
243 |             return {
244 |                 "success": True,
245 |                 "message": f"Rotated component: {reference}",
246 |                 "component": {
247 |                     "reference": reference,
248 |                     "rotation": angle
249 |                 }
250 |             }
251 | 
252 |         except Exception as e:
253 |             logger.error(f"Error rotating component: {str(e)}")
254 |             return {
255 |                 "success": False,
256 |                 "message": "Failed to rotate component",
257 |                 "errorDetails": str(e)
258 |             }
259 | 
260 |     def delete_component(self, params: Dict[str, Any]) -> Dict[str, Any]:
261 |         """Delete a component from the PCB"""
262 |         try:
263 |             if not self.board:
264 |                 return {
265 |                     "success": False,
266 |                     "message": "No board is loaded",
267 |                     "errorDetails": "Load or create a board first"
268 |                 }
269 | 
270 |             reference = params.get("reference")
271 |             if not reference:
272 |                 return {
273 |                     "success": False,
274 |                     "message": "Missing reference",
275 |                     "errorDetails": "reference parameter is required"
276 |                 }
277 | 
278 |             # Find the component
279 |             module = self.board.FindFootprintByReference(reference)
280 |             if not module:
281 |                 return {
282 |                     "success": False,
283 |                     "message": "Component not found",
284 |                     "errorDetails": f"Could not find component: {reference}"
285 |                 }
286 | 
287 |             # Remove from board
288 |             self.board.Remove(module)
289 | 
290 |             return {
291 |                 "success": True,
292 |                 "message": f"Deleted component: {reference}"
293 |             }
294 | 
295 |         except Exception as e:
296 |             logger.error(f"Error deleting component: {str(e)}")
297 |             return {
298 |                 "success": False,
299 |                 "message": "Failed to delete component",
300 |                 "errorDetails": str(e)
301 |             }
302 | 
303 |     def edit_component(self, params: Dict[str, Any]) -> Dict[str, Any]:
304 |         """Edit the properties of an existing component"""
305 |         try:
306 |             if not self.board:
307 |                 return {
308 |                     "success": False,
309 |                     "message": "No board is loaded",
310 |                     "errorDetails": "Load or create a board first"
311 |                 }
312 | 
313 |             reference = params.get("reference")
314 |             new_reference = params.get("newReference")
315 |             value = params.get("value")
316 |             footprint = params.get("footprint")
317 | 
318 |             if not reference:
319 |                 return {
320 |                     "success": False,
321 |                     "message": "Missing reference",
322 |                     "errorDetails": "reference parameter is required"
323 |                 }
324 | 
325 |             # Find the component
326 |             module = self.board.FindFootprintByReference(reference)
327 |             if not module:
328 |                 return {
329 |                     "success": False,
330 |                     "message": "Component not found",
331 |                     "errorDetails": f"Could not find component: {reference}"
332 |                 }
333 | 
334 |             # Update properties
335 |             if new_reference:
336 |                 module.SetReference(new_reference)
337 |             if value:
338 |                 module.SetValue(value)
339 |             if footprint:
340 |                 module.SetFootprintName(footprint)
341 | 
342 |             return {
343 |                 "success": True,
344 |                 "message": f"Updated component: {reference}",
345 |                 "component": {
346 |                     "reference": new_reference or reference,
347 |                     "value": value or module.GetValue(),
348 |                     "footprint": footprint or module.GetFPIDAsString()
349 |                 }
350 |             }
351 | 
352 |         except Exception as e:
353 |             logger.error(f"Error editing component: {str(e)}")
354 |             return {
355 |                 "success": False,
356 |                 "message": "Failed to edit component",
357 |                 "errorDetails": str(e)
358 |             }
359 | 
360 |     def get_component_properties(self, params: Dict[str, Any]) -> Dict[str, Any]:
361 |         """Get detailed properties of a component"""
362 |         try:
363 |             if not self.board:
364 |                 return {
365 |                     "success": False,
366 |                     "message": "No board is loaded",
367 |                     "errorDetails": "Load or create a board first"
368 |                 }
369 | 
370 |             reference = params.get("reference")
371 |             if not reference:
372 |                 return {
373 |                     "success": False,
374 |                     "message": "Missing reference",
375 |                     "errorDetails": "reference parameter is required"
376 |                 }
377 | 
378 |             # Find the component
379 |             module = self.board.FindFootprintByReference(reference)
380 |             if not module:
381 |                 return {
382 |                     "success": False,
383 |                     "message": "Component not found",
384 |                     "errorDetails": f"Could not find component: {reference}"
385 |                 }
386 | 
387 |             # Get position in mm
388 |             pos = module.GetPosition()
389 |             x_mm = pos.x / 1000000
390 |             y_mm = pos.y / 1000000
391 | 
392 |             return {
393 |                 "success": True,
394 |                 "component": {
395 |                     "reference": module.GetReference(),
396 |                     "value": module.GetValue(),
397 |                     "footprint": module.GetFPIDAsString(),
398 |                     "position": {
399 |                         "x": x_mm,
400 |                         "y": y_mm,
401 |                         "unit": "mm"
402 |                     },
403 |                     "rotation": module.GetOrientation().AsDegrees(),
404 |                     "layer": self.board.GetLayerName(module.GetLayer()),
405 |                     "attributes": {
406 |                         "smd": module.GetAttributes() & pcbnew.FP_SMD,
407 |                         "through_hole": module.GetAttributes() & pcbnew.FP_THROUGH_HOLE,
408 |                         "board_only": module.GetAttributes() & pcbnew.FP_BOARD_ONLY
409 |                     }
410 |                 }
411 |             }
412 | 
413 |         except Exception as e:
414 |             logger.error(f"Error getting component properties: {str(e)}")
415 |             return {
416 |                 "success": False,
417 |                 "message": "Failed to get component properties",
418 |                 "errorDetails": str(e)
419 |             }
420 | 
421 |     def get_component_list(self, params: Dict[str, Any]) -> Dict[str, Any]:
422 |         """Get a list of all components on the board"""
423 |         try:
424 |             if not self.board:
425 |                 return {
426 |                     "success": False,
427 |                     "message": "No board is loaded",
428 |                     "errorDetails": "Load or create a board first"
429 |                 }
430 | 
431 |             components = []
432 |             for module in self.board.GetFootprints():
433 |                 pos = module.GetPosition()
434 |                 x_mm = pos.x / 1000000
435 |                 y_mm = pos.y / 1000000
436 | 
437 |                 components.append({
438 |                     "reference": module.GetReference(),
439 |                     "value": module.GetValue(),
440 |                     "footprint": module.GetFPIDAsString(),
441 |                     "position": {
442 |                         "x": x_mm,
443 |                         "y": y_mm,
444 |                         "unit": "mm"
445 |                     },
446 |                     "rotation": module.GetOrientation().AsDegrees(),
447 |                     "layer": self.board.GetLayerName(module.GetLayer())
448 |                 })
449 | 
450 |             return {
451 |                 "success": True,
452 |                 "components": components
453 |             }
454 | 
455 |         except Exception as e:
456 |             logger.error(f"Error getting component list: {str(e)}")
457 |             return {
458 |                 "success": False,
459 |                 "message": "Failed to get component list",
460 |                 "errorDetails": str(e)
461 |             }
462 |             
463 |     def place_component_array(self, params: Dict[str, Any]) -> Dict[str, Any]:
464 |         """Place an array of components in a grid or circular pattern"""
465 |         try:
466 |             if not self.board:
467 |                 return {
468 |                     "success": False,
469 |                     "message": "No board is loaded",
470 |                     "errorDetails": "Load or create a board first"
471 |                 }
472 | 
473 |             component_id = params.get("componentId")
474 |             pattern = params.get("pattern", "grid")  # grid or circular
475 |             count = params.get("count")
476 |             reference_prefix = params.get("referencePrefix", "U")
477 |             value = params.get("value")
478 |             
479 |             if not component_id or not count:
480 |                 return {
481 |                     "success": False,
482 |                     "message": "Missing parameters",
483 |                     "errorDetails": "componentId and count are required"
484 |                 }
485 |                 
486 |             if pattern == "grid":
487 |                 start_position = params.get("startPosition")
488 |                 rows = params.get("rows")
489 |                 columns = params.get("columns")
490 |                 spacing_x = params.get("spacingX")
491 |                 spacing_y = params.get("spacingY")
492 |                 rotation = params.get("rotation", 0)
493 |                 layer = params.get("layer", "F.Cu")
494 |                 
495 |                 if not start_position or not rows or not columns or not spacing_x or not spacing_y:
496 |                     return {
497 |                         "success": False,
498 |                         "message": "Missing grid parameters",
499 |                         "errorDetails": "For grid pattern, startPosition, rows, columns, spacingX, and spacingY are required"
500 |                     }
501 |                     
502 |                 if rows * columns != count:
503 |                     return {
504 |                         "success": False,
505 |                         "message": "Invalid grid parameters",
506 |                         "errorDetails": "rows * columns must equal count"
507 |                     }
508 |                     
509 |                 placed_components = self._place_grid_array(
510 |                     component_id,
511 |                     start_position,
512 |                     rows,
513 |                     columns,
514 |                     spacing_x,
515 |                     spacing_y,
516 |                     reference_prefix,
517 |                     value,
518 |                     rotation,
519 |                     layer
520 |                 )
521 |                 
522 |             elif pattern == "circular":
523 |                 center = params.get("center")
524 |                 radius = params.get("radius")
525 |                 angle_start = params.get("angleStart", 0)
526 |                 angle_step = params.get("angleStep")
527 |                 rotation_offset = params.get("rotationOffset", 0)
528 |                 layer = params.get("layer", "F.Cu")
529 |                 
530 |                 if not center or not radius or not angle_step:
531 |                     return {
532 |                         "success": False,
533 |                         "message": "Missing circular parameters",
534 |                         "errorDetails": "For circular pattern, center, radius, and angleStep are required"
535 |                     }
536 |                     
537 |                 placed_components = self._place_circular_array(
538 |                     component_id,
539 |                     center,
540 |                     radius,
541 |                     count,
542 |                     angle_start,
543 |                     angle_step,
544 |                     reference_prefix,
545 |                     value,
546 |                     rotation_offset,
547 |                     layer
548 |                 )
549 |                 
550 |             else:
551 |                 return {
552 |                     "success": False,
553 |                     "message": "Invalid pattern",
554 |                     "errorDetails": "Pattern must be 'grid' or 'circular'"
555 |                 }
556 | 
557 |             return {
558 |                 "success": True,
559 |                 "message": f"Placed {count} components in {pattern} pattern",
560 |                 "components": placed_components
561 |             }
562 | 
563 |         except Exception as e:
564 |             logger.error(f"Error placing component array: {str(e)}")
565 |             return {
566 |                 "success": False,
567 |                 "message": "Failed to place component array",
568 |                 "errorDetails": str(e)
569 |             }
570 |             
571 |     def align_components(self, params: Dict[str, Any]) -> Dict[str, Any]:
572 |         """Align multiple components along a line or distribute them evenly"""
573 |         try:
574 |             if not self.board:
575 |                 return {
576 |                     "success": False,
577 |                     "message": "No board is loaded",
578 |                     "errorDetails": "Load or create a board first"
579 |                 }
580 | 
581 |             references = params.get("references", [])
582 |             alignment = params.get("alignment", "horizontal")  # horizontal, vertical, or edge
583 |             distribution = params.get("distribution", "none")  # none, equal, or spacing
584 |             spacing = params.get("spacing")
585 |             
586 |             if not references or len(references) < 2:
587 |                 return {
588 |                     "success": False,
589 |                     "message": "Missing references",
590 |                     "errorDetails": "At least two component references are required"
591 |                 }
592 |                 
593 |             # Find all referenced components
594 |             components = []
595 |             for ref in references:
596 |                 module = self.board.FindFootprintByReference(ref)
597 |                 if not module:
598 |                     return {
599 |                         "success": False,
600 |                         "message": "Component not found",
601 |                         "errorDetails": f"Could not find component: {ref}"
602 |                     }
603 |                 components.append(module)
604 |             
605 |             # Perform alignment based on selected option
606 |             if alignment == "horizontal":
607 |                 self._align_components_horizontally(components, distribution, spacing)
608 |             elif alignment == "vertical":
609 |                 self._align_components_vertically(components, distribution, spacing)
610 |             elif alignment == "edge":
611 |                 edge = params.get("edge")
612 |                 if not edge:
613 |                     return {
614 |                         "success": False,
615 |                         "message": "Missing edge parameter",
616 |                         "errorDetails": "Edge parameter is required for edge alignment"
617 |                     }
618 |                 self._align_components_to_edge(components, edge)
619 |             else:
620 |                 return {
621 |                     "success": False,
622 |                     "message": "Invalid alignment option",
623 |                     "errorDetails": "Alignment must be 'horizontal', 'vertical', or 'edge'"
624 |                 }
625 | 
626 |             # Prepare result data
627 |             aligned_components = []
628 |             for module in components:
629 |                 pos = module.GetPosition()
630 |                 aligned_components.append({
631 |                     "reference": module.GetReference(),
632 |                     "position": {
633 |                         "x": pos.x / 1000000,
634 |                         "y": pos.y / 1000000,
635 |                         "unit": "mm"
636 |                     },
637 |                     "rotation": module.GetOrientation().AsDegrees()
638 |                 })
639 | 
640 |             return {
641 |                 "success": True,
642 |                 "message": f"Aligned {len(components)} components",
643 |                 "alignment": alignment,
644 |                 "distribution": distribution,
645 |                 "components": aligned_components
646 |             }
647 | 
648 |         except Exception as e:
649 |             logger.error(f"Error aligning components: {str(e)}")
650 |             return {
651 |                 "success": False,
652 |                 "message": "Failed to align components",
653 |                 "errorDetails": str(e)
654 |             }
655 |             
656 |     def duplicate_component(self, params: Dict[str, Any]) -> Dict[str, Any]:
657 |         """Duplicate an existing component"""
658 |         try:
659 |             if not self.board:
660 |                 return {
661 |                     "success": False,
662 |                     "message": "No board is loaded",
663 |                     "errorDetails": "Load or create a board first"
664 |                 }
665 | 
666 |             reference = params.get("reference")
667 |             new_reference = params.get("newReference")
668 |             position = params.get("position")
669 |             rotation = params.get("rotation")
670 |             
671 |             if not reference or not new_reference:
672 |                 return {
673 |                     "success": False,
674 |                     "message": "Missing parameters",
675 |                     "errorDetails": "reference and newReference are required"
676 |                 }
677 |                 
678 |             # Find the source component
679 |             source = self.board.FindFootprintByReference(reference)
680 |             if not source:
681 |                 return {
682 |                     "success": False,
683 |                     "message": "Component not found",
684 |                     "errorDetails": f"Could not find component: {reference}"
685 |                 }
686 |                 
687 |             # Check if new reference already exists
688 |             if self.board.FindFootprintByReference(new_reference):
689 |                 return {
690 |                     "success": False,
691 |                     "message": "Reference already exists",
692 |                     "errorDetails": f"A component with reference {new_reference} already exists"
693 |                 }
694 |                 
695 |             # Create new footprint with the same properties
696 |             new_module = pcbnew.FOOTPRINT(self.board)
697 |             new_module.SetFootprintName(source.GetFPIDAsString())
698 |             new_module.SetValue(source.GetValue())
699 |             new_module.SetReference(new_reference)
700 |             new_module.SetLayer(source.GetLayer())
701 |             
702 |             # Copy pads and other items
703 |             for pad in source.Pads():
704 |                 new_pad = pcbnew.PAD(new_module)
705 |                 new_pad.Copy(pad)
706 |                 new_module.Add(new_pad)
707 |                 
708 |             # Set position if provided, otherwise use offset from original
709 |             if position:
710 |                 scale = 1000000 if position.get("unit", "mm") == "mm" else 25400000
711 |                 x_nm = int(position["x"] * scale)
712 |                 y_nm = int(position["y"] * scale)
713 |                 new_module.SetPosition(pcbnew.VECTOR2I(x_nm, y_nm))
714 |             else:
715 |                 # Offset by 5mm
716 |                 source_pos = source.GetPosition()
717 |                 new_module.SetPosition(pcbnew.VECTOR2I(source_pos.x + 5000000, source_pos.y))
718 |                 
719 |             # Set rotation if provided, otherwise use same as original
720 |             if rotation is not None:
721 |                 rotation_angle = pcbnew.EDA_ANGLE(rotation, pcbnew.DEGREES_T)
722 |                 new_module.SetOrientation(rotation_angle)
723 |             else:
724 |                 new_module.SetOrientation(source.GetOrientation())
725 |                 
726 |             # Add to board
727 |             self.board.Add(new_module)
728 |             
729 |             # Get final position in mm
730 |             pos = new_module.GetPosition()
731 | 
732 |             return {
733 |                 "success": True,
734 |                 "message": f"Duplicated component {reference} to {new_reference}",
735 |                 "component": {
736 |                     "reference": new_reference,
737 |                     "value": new_module.GetValue(),
738 |                     "footprint": new_module.GetFPIDAsString(),
739 |                     "position": {
740 |                         "x": pos.x / 1000000,
741 |                         "y": pos.y / 1000000,
742 |                         "unit": "mm"
743 |                     },
744 |                     "rotation": new_module.GetOrientation().AsDegrees(),
745 |                     "layer": self.board.GetLayerName(new_module.GetLayer())
746 |                 }
747 |             }
748 | 
749 |         except Exception as e:
750 |             logger.error(f"Error duplicating component: {str(e)}")
751 |             return {
752 |                 "success": False,
753 |                 "message": "Failed to duplicate component",
754 |                 "errorDetails": str(e)
755 |             }
756 |             
757 |     def _place_grid_array(self, component_id: str, start_position: Dict[str, Any], 
758 |                        rows: int, columns: int, spacing_x: float, spacing_y: float,
759 |                        reference_prefix: str, value: str, rotation: float, layer: str) -> List[Dict[str, Any]]:
760 |         """Place components in a grid pattern and return the list of placed components"""
761 |         placed = []
762 |         
763 |         # Convert spacing to nm
764 |         unit = start_position.get("unit", "mm")
765 |         scale = 1000000 if unit == "mm" else 25400000  # mm or inch to nm
766 |         spacing_x_nm = int(spacing_x * scale)
767 |         spacing_y_nm = int(spacing_y * scale)
768 |         
769 |         # Get layer ID
770 |         layer_id = self.board.GetLayerID(layer)
771 |         
772 |         for row in range(rows):
773 |             for col in range(columns):
774 |                 # Calculate position
775 |                 x = start_position["x"] + (col * spacing_x)
776 |                 y = start_position["y"] + (row * spacing_y)
777 |                 
778 |                 # Generate reference
779 |                 index = row * columns + col + 1
780 |                 component_reference = f"{reference_prefix}{index}"
781 |                 
782 |                 # Place component
783 |                 result = self.place_component({
784 |                     "componentId": component_id,
785 |                     "position": {"x": x, "y": y, "unit": unit},
786 |                     "reference": component_reference,
787 |                     "value": value,
788 |                     "rotation": rotation,
789 |                     "layer": layer
790 |                 })
791 |                 
792 |                 if result["success"]:
793 |                     placed.append(result["component"])
794 |                 
795 |         return placed
796 |         
797 |     def _place_circular_array(self, component_id: str, center: Dict[str, Any], 
798 |                           radius: float, count: int, angle_start: float, 
799 |                           angle_step: float, reference_prefix: str, 
800 |                           value: str, rotation_offset: float, layer: str) -> List[Dict[str, Any]]:
801 |         """Place components in a circular pattern and return the list of placed components"""
802 |         placed = []
803 |         
804 |         # Get unit
805 |         unit = center.get("unit", "mm")
806 |         
807 |         for i in range(count):
808 |             # Calculate angle for this component
809 |             angle = angle_start + (i * angle_step)
810 |             angle_rad = math.radians(angle)
811 |             
812 |             # Calculate position
813 |             x = center["x"] + (radius * math.cos(angle_rad))
814 |             y = center["y"] + (radius * math.sin(angle_rad))
815 |             
816 |             # Generate reference
817 |             component_reference = f"{reference_prefix}{i+1}"
818 |             
819 |             # Calculate rotation (pointing outward from center)
820 |             component_rotation = angle + rotation_offset
821 |             
822 |             # Place component
823 |             result = self.place_component({
824 |                 "componentId": component_id,
825 |                 "position": {"x": x, "y": y, "unit": unit},
826 |                 "reference": component_reference,
827 |                 "value": value,
828 |                 "rotation": component_rotation,
829 |                 "layer": layer
830 |             })
831 |             
832 |             if result["success"]:
833 |                 placed.append(result["component"])
834 |                 
835 |         return placed
836 |         
837 |     def _align_components_horizontally(self, components: List[pcbnew.FOOTPRINT], 
838 |                                    distribution: str, spacing: Optional[float]) -> None:
839 |         """Align components horizontally and optionally distribute them"""
840 |         if not components:
841 |             return
842 |             
843 |         # Find the average Y coordinate
844 |         y_sum = sum(module.GetPosition().y for module in components)
845 |         y_avg = y_sum // len(components)
846 |         
847 |         # Sort components by X position
848 |         components.sort(key=lambda m: m.GetPosition().x)
849 |         
850 |         # Set Y coordinate for all components
851 |         for module in components:
852 |             pos = module.GetPosition()
853 |             module.SetPosition(pcbnew.VECTOR2I(pos.x, y_avg))
854 |             
855 |         # Handle distribution if requested
856 |         if distribution == "equal" and len(components) > 1:
857 |             # Get leftmost and rightmost X coordinates
858 |             x_min = components[0].GetPosition().x
859 |             x_max = components[-1].GetPosition().x
860 |             
861 |             # Calculate equal spacing
862 |             total_space = x_max - x_min
863 |             spacing_nm = total_space // (len(components) - 1)
864 |             
865 |             # Set X positions with equal spacing
866 |             for i in range(1, len(components) - 1):
867 |                 pos = components[i].GetPosition()
868 |                 new_x = x_min + (i * spacing_nm)
869 |                 components[i].SetPosition(pcbnew.VECTOR2I(new_x, pos.y))
870 |                 
871 |         elif distribution == "spacing" and spacing is not None:
872 |             # Convert spacing to nanometers
873 |             spacing_nm = int(spacing * 1000000)  # assuming mm
874 |             
875 |             # Set X positions with the specified spacing
876 |             x_current = components[0].GetPosition().x
877 |             for i in range(1, len(components)):
878 |                 pos = components[i].GetPosition()
879 |                 x_current += spacing_nm
880 |                 components[i].SetPosition(pcbnew.VECTOR2I(x_current, pos.y))
881 |                 
882 |     def _align_components_vertically(self, components: List[pcbnew.FOOTPRINT], 
883 |                                  distribution: str, spacing: Optional[float]) -> None:
884 |         """Align components vertically and optionally distribute them"""
885 |         if not components:
886 |             return
887 |             
888 |         # Find the average X coordinate
889 |         x_sum = sum(module.GetPosition().x for module in components)
890 |         x_avg = x_sum // len(components)
891 |         
892 |         # Sort components by Y position
893 |         components.sort(key=lambda m: m.GetPosition().y)
894 |         
895 |         # Set X coordinate for all components
896 |         for module in components:
897 |             pos = module.GetPosition()
898 |             module.SetPosition(pcbnew.VECTOR2I(x_avg, pos.y))
899 |             
900 |         # Handle distribution if requested
901 |         if distribution == "equal" and len(components) > 1:
902 |             # Get topmost and bottommost Y coordinates
903 |             y_min = components[0].GetPosition().y
904 |             y_max = components[-1].GetPosition().y
905 |             
906 |             # Calculate equal spacing
907 |             total_space = y_max - y_min
908 |             spacing_nm = total_space // (len(components) - 1)
909 |             
910 |             # Set Y positions with equal spacing
911 |             for i in range(1, len(components) - 1):
912 |                 pos = components[i].GetPosition()
913 |                 new_y = y_min + (i * spacing_nm)
914 |                 components[i].SetPosition(pcbnew.VECTOR2I(pos.x, new_y))
915 |                 
916 |         elif distribution == "spacing" and spacing is not None:
917 |             # Convert spacing to nanometers
918 |             spacing_nm = int(spacing * 1000000)  # assuming mm
919 |             
920 |             # Set Y positions with the specified spacing
921 |             y_current = components[0].GetPosition().y
922 |             for i in range(1, len(components)):
923 |                 pos = components[i].GetPosition()
924 |                 y_current += spacing_nm
925 |                 components[i].SetPosition(pcbnew.VECTOR2I(pos.x, y_current))
926 |                 
927 |     def _align_components_to_edge(self, components: List[pcbnew.FOOTPRINT], edge: str) -> None:
928 |         """Align components to the specified edge of the board"""
929 |         if not components:
930 |             return
931 |             
932 |         # Get board bounds
933 |         board_box = self.board.GetBoardEdgesBoundingBox()
934 |         left = board_box.GetLeft()
935 |         right = board_box.GetRight()
936 |         top = board_box.GetTop()
937 |         bottom = board_box.GetBottom()
938 |         
939 |         # Align based on specified edge
940 |         if edge == "left":
941 |             for module in components:
942 |                 pos = module.GetPosition()
943 |                 module.SetPosition(pcbnew.VECTOR2I(left + 2000000, pos.y))  # 2mm offset from edge
944 |         elif edge == "right":
945 |             for module in components:
946 |                 pos = module.GetPosition()
947 |                 module.SetPosition(pcbnew.VECTOR2I(right - 2000000, pos.y))  # 2mm offset from edge
948 |         elif edge == "top":
949 |             for module in components:
950 |                 pos = module.GetPosition()
951 |                 module.SetPosition(pcbnew.VECTOR2I(pos.x, top + 2000000))  # 2mm offset from edge
952 |         elif edge == "bottom":
953 |             for module in components:
954 |                 pos = module.GetPosition()
955 |                 module.SetPosition(pcbnew.VECTOR2I(pos.x, bottom - 2000000))  # 2mm offset from edge
956 |         else:
957 |             logger.warning(f"Unknown edge alignment: {edge}")
958 | 
```

--------------------------------------------------------------------------------
/python/kicad_api/ipc_backend.py:
--------------------------------------------------------------------------------

```python
   1 | """
   2 | IPC API Backend (KiCAD 9.0+)
   3 | 
   4 | Uses the official kicad-python library for inter-process communication
   5 | with a running KiCAD instance. This enables REAL-TIME UI synchronization.
   6 | 
   7 | Note: Requires KiCAD to be running with IPC server enabled:
   8 |     Preferences > Plugins > Enable IPC API Server
   9 | 
  10 | Key Benefits over SWIG:
  11 | - Changes appear instantly in KiCAD UI (no reload needed)
  12 | - Transaction support for undo/redo
  13 | - Stable API that won't break between versions
  14 | - Multi-language support
  15 | """
  16 | import logging
  17 | import os
  18 | from pathlib import Path
  19 | from typing import Optional, Dict, Any, List, Callable
  20 | 
  21 | from kicad_api.base import (
  22 |     KiCADBackend,
  23 |     BoardAPI,
  24 |     ConnectionError,
  25 |     APINotAvailableError
  26 | )
  27 | 
  28 | logger = logging.getLogger(__name__)
  29 | 
  30 | # Unit conversion constant: KiCAD IPC uses nanometers internally
  31 | MM_TO_NM = 1_000_000
  32 | INCH_TO_NM = 25_400_000
  33 | 
  34 | 
  35 | class IPCBackend(KiCADBackend):
  36 |     """
  37 |     KiCAD IPC API backend for real-time UI synchronization.
  38 | 
  39 |     Communicates with KiCAD via Protocol Buffers over UNIX sockets.
  40 |     Requires KiCAD 9.0+ to be running with IPC enabled.
  41 | 
  42 |     Changes made through this backend appear immediately in the KiCAD UI
  43 |     without requiring manual reload.
  44 |     """
  45 | 
  46 |     def __init__(self):
  47 |         self._kicad = None
  48 |         self._connected = False
  49 |         self._version = None
  50 |         self._on_change_callbacks: List[Callable] = []
  51 | 
  52 |     def connect(self, socket_path: Optional[str] = None) -> bool:
  53 |         """
  54 |         Connect to running KiCAD instance via IPC.
  55 | 
  56 |         Args:
  57 |             socket_path: Optional socket path. If not provided, will try common locations.
  58 |                         Use format: ipc:///tmp/kicad/api.sock
  59 | 
  60 |         Returns:
  61 |             True if connection successful
  62 | 
  63 |         Raises:
  64 |             ConnectionError: If connection fails
  65 |         """
  66 |         try:
  67 |             # Import here to allow module to load even without kicad-python
  68 |             from kipy import KiCad
  69 | 
  70 |             logger.info("Connecting to KiCAD via IPC...")
  71 | 
  72 |             # Try to connect with provided path or auto-detect
  73 |             socket_paths_to_try = []
  74 |             if socket_path:
  75 |                 socket_paths_to_try.append(socket_path)
  76 |             else:
  77 |                 # Common socket locations
  78 |                 socket_paths_to_try = [
  79 |                     'ipc:///tmp/kicad/api.sock',  # Linux default
  80 |                     f'ipc:///run/user/{os.getuid()}/kicad/api.sock',  # XDG runtime
  81 |                     None  # Let kipy auto-detect
  82 |                 ]
  83 | 
  84 |             last_error = None
  85 |             for path in socket_paths_to_try:
  86 |                 try:
  87 |                     if path:
  88 |                         logger.debug(f"Trying socket path: {path}")
  89 |                         self._kicad = KiCad(socket_path=path)
  90 |                     else:
  91 |                         logger.debug("Trying auto-detection")
  92 |                         self._kicad = KiCad()
  93 | 
  94 |                     # Verify connection with ping (ping returns None on success)
  95 |                     self._kicad.ping()
  96 |                     logger.info(f"Connected via socket: {path or 'auto-detected'}")
  97 |                     break
  98 |                 except Exception as e:
  99 |                     last_error = e
 100 |                     logger.debug(f"Failed to connect via {path}: {e}")
 101 |                     continue
 102 |             else:
 103 |                 # None of the paths worked
 104 |                 raise ConnectionError(f"Could not connect to KiCAD IPC: {last_error}")
 105 | 
 106 |             # Get version info
 107 |             self._version = self._get_kicad_version()
 108 |             logger.info(f"Connected to KiCAD {self._version} via IPC")
 109 |             self._connected = True
 110 |             return True
 111 | 
 112 |         except ImportError as e:
 113 |             logger.error("kicad-python library not found")
 114 |             raise APINotAvailableError(
 115 |                 "IPC backend requires kicad-python. "
 116 |                 "Install with: pip install kicad-python"
 117 |             ) from e
 118 |         except Exception as e:
 119 |             logger.error(f"Failed to connect via IPC: {e}")
 120 |             logger.info(
 121 |                 "Ensure KiCAD is running with IPC enabled: "
 122 |                 "Preferences > Plugins > Enable IPC API Server"
 123 |             )
 124 |             raise ConnectionError(f"IPC connection failed: {e}") from e
 125 | 
 126 |     def _get_kicad_version(self) -> str:
 127 |         """Get KiCAD version string."""
 128 |         try:
 129 |             if self._kicad.check_version():
 130 |                 return self._kicad.get_api_version()
 131 |             return "9.0+ (version mismatch)"
 132 |         except Exception:
 133 |             return "unknown"
 134 | 
 135 |     def disconnect(self) -> None:
 136 |         """Disconnect from KiCAD."""
 137 |         if self._kicad:
 138 |             self._kicad = None
 139 |             self._connected = False
 140 |             logger.info("Disconnected from KiCAD IPC")
 141 | 
 142 |     def is_connected(self) -> bool:
 143 |         """Check if connected to KiCAD."""
 144 |         if not self._connected or not self._kicad:
 145 |             return False
 146 |         try:
 147 |             # ping() returns None on success, raises on failure
 148 |             self._kicad.ping()
 149 |             return True
 150 |         except Exception:
 151 |             self._connected = False
 152 |             return False
 153 | 
 154 |     def get_version(self) -> str:
 155 |         """Get KiCAD version."""
 156 |         return self._version or "unknown"
 157 | 
 158 |     def register_change_callback(self, callback: Callable) -> None:
 159 |         """Register a callback to be called when changes are made."""
 160 |         self._on_change_callbacks.append(callback)
 161 | 
 162 |     def _notify_change(self, change_type: str, details: Dict[str, Any]) -> None:
 163 |         """Notify registered callbacks of a change."""
 164 |         for callback in self._on_change_callbacks:
 165 |             try:
 166 |                 callback(change_type, details)
 167 |             except Exception as e:
 168 |                 logger.warning(f"Change callback error: {e}")
 169 | 
 170 |     # Project Operations
 171 |     def create_project(self, path: Path, name: str) -> Dict[str, Any]:
 172 |         """
 173 |         Create a new KiCAD project.
 174 | 
 175 |         Note: The IPC API doesn't directly create projects.
 176 |         Projects must be created through the UI or file system.
 177 |         """
 178 |         if not self.is_connected():
 179 |             raise ConnectionError("Not connected to KiCAD")
 180 | 
 181 |         # IPC API doesn't have project creation - use file-based approach
 182 |         logger.warning("Project creation via IPC not fully supported - using hybrid approach")
 183 | 
 184 |         # For now, we'll return info about what needs to happen
 185 |         return {
 186 |             "success": False,
 187 |             "message": "Direct project creation not supported via IPC",
 188 |             "suggestion": "Open KiCAD and create a new project, or use SWIG backend"
 189 |         }
 190 | 
 191 |     def open_project(self, path: Path) -> Dict[str, Any]:
 192 |         """Open existing project via IPC."""
 193 |         if not self.is_connected():
 194 |             raise ConnectionError("Not connected to KiCAD")
 195 | 
 196 |         try:
 197 |             # Check for open documents
 198 |             documents = self._kicad.get_open_documents()
 199 | 
 200 |             # Look for matching project
 201 |             path_str = str(path)
 202 |             for doc in documents:
 203 |                 if path_str in str(doc):
 204 |                     return {
 205 |                         "success": True,
 206 |                         "message": f"Project already open: {path}",
 207 |                         "path": str(path)
 208 |                     }
 209 | 
 210 |             return {
 211 |                 "success": False,
 212 |                 "message": "Project not currently open in KiCAD",
 213 |                 "suggestion": "Open the project in KiCAD first, then connect via IPC"
 214 |             }
 215 | 
 216 |         except Exception as e:
 217 |             logger.error(f"Failed to check project: {e}")
 218 |             return {
 219 |                 "success": False,
 220 |                 "message": "Failed to check project",
 221 |                 "errorDetails": str(e)
 222 |             }
 223 | 
 224 |     def save_project(self, path: Optional[Path] = None) -> Dict[str, Any]:
 225 |         """Save current project via IPC."""
 226 |         if not self.is_connected():
 227 |             raise ConnectionError("Not connected to KiCAD")
 228 | 
 229 |         try:
 230 |             board = self._kicad.get_board()
 231 |             if path:
 232 |                 board.save_as(str(path))
 233 |             else:
 234 |                 board.save()
 235 | 
 236 |             self._notify_change("save", {"path": str(path) if path else "current"})
 237 | 
 238 |             return {
 239 |                 "success": True,
 240 |                 "message": "Project saved successfully"
 241 |             }
 242 |         except Exception as e:
 243 |             logger.error(f"Failed to save project: {e}")
 244 |             return {
 245 |                 "success": False,
 246 |                 "message": "Failed to save project",
 247 |                 "errorDetails": str(e)
 248 |             }
 249 | 
 250 |     def close_project(self) -> None:
 251 |         """Close current project (not supported via IPC)."""
 252 |         logger.warning("Closing projects via IPC is not supported")
 253 | 
 254 |     # Board Operations
 255 |     def get_board(self) -> BoardAPI:
 256 |         """Get board API for real-time manipulation."""
 257 |         if not self.is_connected():
 258 |             raise ConnectionError("Not connected to KiCAD")
 259 | 
 260 |         return IPCBoardAPI(self._kicad, self._notify_change)
 261 | 
 262 | 
 263 | class IPCBoardAPI(BoardAPI):
 264 |     """
 265 |     Board API implementation for IPC backend.
 266 | 
 267 |     All changes made through this API appear immediately in the KiCAD UI.
 268 |     Uses transactions for proper undo/redo support.
 269 |     """
 270 | 
 271 |     def __init__(self, kicad_instance, notify_callback: Callable):
 272 |         self._kicad = kicad_instance
 273 |         self._board = None
 274 |         self._notify = notify_callback
 275 |         self._current_commit = None
 276 | 
 277 |     def _get_board(self):
 278 |         """Get board instance, connecting if needed."""
 279 |         if self._board is None:
 280 |             try:
 281 |                 self._board = self._kicad.get_board()
 282 |             except Exception as e:
 283 |                 logger.error(f"Failed to get board: {e}")
 284 |                 raise ConnectionError(f"No board open in KiCAD: {e}")
 285 |         return self._board
 286 | 
 287 |     def begin_transaction(self, description: str = "MCP Operation") -> None:
 288 |         """Begin a transaction for grouping operations into a single undo step."""
 289 |         board = self._get_board()
 290 |         self._current_commit = board.begin_commit()
 291 |         logger.debug(f"Started transaction: {description}")
 292 | 
 293 |     def commit_transaction(self, description: str = "MCP Operation") -> None:
 294 |         """Commit the current transaction."""
 295 |         if self._current_commit:
 296 |             board = self._get_board()
 297 |             board.push_commit(self._current_commit, description)
 298 |             self._current_commit = None
 299 |             logger.debug(f"Committed transaction: {description}")
 300 | 
 301 |     def rollback_transaction(self) -> None:
 302 |         """Roll back the current transaction."""
 303 |         if self._current_commit:
 304 |             board = self._get_board()
 305 |             board.drop_commit(self._current_commit)
 306 |             self._current_commit = None
 307 |             logger.debug("Rolled back transaction")
 308 | 
 309 |     def save(self) -> bool:
 310 |         """Save the board immediately."""
 311 |         try:
 312 |             board = self._get_board()
 313 |             board.save()
 314 |             self._notify("save", {})
 315 |             return True
 316 |         except Exception as e:
 317 |             logger.error(f"Failed to save board: {e}")
 318 |             return False
 319 | 
 320 |     def set_size(self, width: float, height: float, unit: str = "mm") -> bool:
 321 |         """
 322 |         Set board size.
 323 | 
 324 |         Note: Board size in KiCAD is typically defined by the board outline,
 325 |         not a direct size property. This method may need to create/modify
 326 |         the board outline.
 327 |         """
 328 |         try:
 329 |             from kipy.board_types import BoardRectangle
 330 |             from kipy.geometry import Vector2
 331 |             from kipy.util.units import from_mm
 332 |             from kipy.proto.board.board_types_pb2 import BoardLayer
 333 | 
 334 |             board = self._get_board()
 335 | 
 336 |             # Convert to nm
 337 |             if unit == "mm":
 338 |                 w = from_mm(width)
 339 |                 h = from_mm(height)
 340 |             else:
 341 |                 w = int(width * INCH_TO_NM)
 342 |                 h = int(height * INCH_TO_NM)
 343 | 
 344 |             # Create board outline rectangle on Edge.Cuts layer
 345 |             rect = BoardRectangle()
 346 |             rect.start = Vector2.from_xy(0, 0)
 347 |             rect.end = Vector2.from_xy(w, h)
 348 |             rect.layer = BoardLayer.BL_Edge_Cuts
 349 |             rect.width = from_mm(0.1)  # Standard edge cut width
 350 | 
 351 |             # Begin transaction for undo support
 352 |             commit = board.begin_commit()
 353 |             board.create_items(rect)
 354 |             board.push_commit(commit, f"Set board size to {width}x{height} {unit}")
 355 | 
 356 |             self._notify("board_size", {"width": width, "height": height, "unit": unit})
 357 | 
 358 |             return True
 359 | 
 360 |         except Exception as e:
 361 |             logger.error(f"Failed to set board size: {e}")
 362 |             return False
 363 | 
 364 |     def get_size(self) -> Dict[str, float]:
 365 |         """Get current board size from bounding box."""
 366 |         try:
 367 |             board = self._get_board()
 368 | 
 369 |             # Get shapes on Edge.Cuts layer to determine board size
 370 |             shapes = board.get_shapes()
 371 | 
 372 |             if not shapes:
 373 |                 return {"width": 0, "height": 0, "unit": "mm"}
 374 | 
 375 |             # Find bounding box of edge cuts
 376 |             from kipy.util.units import to_mm
 377 | 
 378 |             min_x = min_y = float('inf')
 379 |             max_x = max_y = float('-inf')
 380 | 
 381 |             for shape in shapes:
 382 |                 # Check if on Edge.Cuts layer
 383 |                 bbox = board.get_item_bounding_box(shape)
 384 |                 if bbox:
 385 |                     min_x = min(min_x, bbox.min.x)
 386 |                     min_y = min(min_y, bbox.min.y)
 387 |                     max_x = max(max_x, bbox.max.x)
 388 |                     max_y = max(max_y, bbox.max.y)
 389 | 
 390 |             if min_x == float('inf'):
 391 |                 return {"width": 0, "height": 0, "unit": "mm"}
 392 | 
 393 |             return {
 394 |                 "width": to_mm(max_x - min_x),
 395 |                 "height": to_mm(max_y - min_y),
 396 |                 "unit": "mm"
 397 |             }
 398 | 
 399 |         except Exception as e:
 400 |             logger.error(f"Failed to get board size: {e}")
 401 |             return {"width": 0, "height": 0, "unit": "mm", "error": str(e)}
 402 | 
 403 |     def add_layer(self, layer_name: str, layer_type: str) -> bool:
 404 |         """Add layer to the board (layers are typically predefined in KiCAD)."""
 405 |         logger.warning("Layer management via IPC is limited - layers are predefined")
 406 |         return False
 407 | 
 408 |     def get_enabled_layers(self) -> List[str]:
 409 |         """Get list of enabled layers."""
 410 |         try:
 411 |             board = self._get_board()
 412 |             layers = board.get_enabled_layers()
 413 |             return [str(layer) for layer in layers]
 414 |         except Exception as e:
 415 |             logger.error(f"Failed to get enabled layers: {e}")
 416 |             return []
 417 | 
 418 |     def list_components(self) -> List[Dict[str, Any]]:
 419 |         """List all components (footprints) on the board."""
 420 |         try:
 421 |             from kipy.util.units import to_mm
 422 | 
 423 |             board = self._get_board()
 424 |             footprints = board.get_footprints()
 425 | 
 426 |             components = []
 427 |             for fp in footprints:
 428 |                 try:
 429 |                     pos = fp.position
 430 |                     components.append({
 431 |                         "reference": fp.reference_field.text.value if fp.reference_field else "",
 432 |                         "value": fp.value_field.text.value if fp.value_field else "",
 433 |                         "footprint": str(fp.definition.library_link) if fp.definition else "",
 434 |                         "position": {
 435 |                             "x": to_mm(pos.x) if pos else 0,
 436 |                             "y": to_mm(pos.y) if pos else 0,
 437 |                             "unit": "mm"
 438 |                         },
 439 |                         "rotation": fp.orientation.degrees if fp.orientation else 0,
 440 |                         "layer": str(fp.layer) if hasattr(fp, 'layer') else "F.Cu",
 441 |                         "id": str(fp.id) if hasattr(fp, 'id') else ""
 442 |                     })
 443 |                 except Exception as e:
 444 |                     logger.warning(f"Error processing footprint: {e}")
 445 |                     continue
 446 | 
 447 |             return components
 448 | 
 449 |         except Exception as e:
 450 |             logger.error(f"Failed to list components: {e}")
 451 |             return []
 452 | 
 453 |     def place_component(
 454 |         self,
 455 |         reference: str,
 456 |         footprint: str,
 457 |         x: float,
 458 |         y: float,
 459 |         rotation: float = 0,
 460 |         layer: str = "F.Cu",
 461 |         value: str = ""
 462 |     ) -> bool:
 463 |         """
 464 |         Place a component on the board.
 465 | 
 466 |         The component appears immediately in the KiCAD UI.
 467 | 
 468 |         This method uses a hybrid approach:
 469 |         1. Load the footprint definition from the library using pcbnew (SWIG)
 470 |         2. Place it on the board via IPC for real-time UI updates
 471 | 
 472 |         Args:
 473 |             reference: Component reference designator (e.g., "R1", "U1")
 474 |             footprint: Footprint path in format "Library:FootprintName" or just "FootprintName"
 475 |             x: X position in mm
 476 |             y: Y position in mm
 477 |             rotation: Rotation angle in degrees
 478 |             layer: Layer name ("F.Cu" for top, "B.Cu" for bottom)
 479 |             value: Component value (optional)
 480 |         """
 481 |         try:
 482 |             # First, try to load the footprint from library using pcbnew SWIG
 483 |             loaded_fp = self._load_footprint_from_library(footprint)
 484 | 
 485 |             if loaded_fp:
 486 |                 # We have the footprint from the library - place it via SWIG
 487 |                 # then sync to IPC for UI update
 488 |                 return self._place_loaded_footprint(
 489 |                     loaded_fp, reference, x, y, rotation, layer, value
 490 |                 )
 491 |             else:
 492 |                 # Fallback: Create a basic placeholder footprint via IPC
 493 |                 logger.warning(f"Could not load footprint '{footprint}' from library, creating placeholder")
 494 |                 return self._place_placeholder_footprint(
 495 |                     reference, footprint, x, y, rotation, layer, value
 496 |                 )
 497 | 
 498 |         except Exception as e:
 499 |             logger.error(f"Failed to place component: {e}")
 500 |             return False
 501 | 
 502 |     def _load_footprint_from_library(self, footprint_path: str):
 503 |         """
 504 |         Load a footprint from the library using pcbnew SWIG API.
 505 | 
 506 |         Args:
 507 |             footprint_path: Either "Library:FootprintName" or just "FootprintName"
 508 | 
 509 |         Returns:
 510 |             pcbnew.FOOTPRINT object or None if not found
 511 |         """
 512 |         try:
 513 |             import pcbnew
 514 | 
 515 |             # Parse library and footprint name
 516 |             if ':' in footprint_path:
 517 |                 lib_name, fp_name = footprint_path.split(':', 1)
 518 |             else:
 519 |                 # Try to find the footprint in all libraries
 520 |                 lib_name = None
 521 |                 fp_name = footprint_path
 522 | 
 523 |             # Get the footprint library table
 524 |             fp_lib_table = pcbnew.GetGlobalFootprintLib()
 525 | 
 526 |             if lib_name:
 527 |                 # Load from specific library
 528 |                 try:
 529 |                     loaded_fp = pcbnew.FootprintLoad(fp_lib_table, lib_name, fp_name)
 530 |                     if loaded_fp:
 531 |                         logger.info(f"Loaded footprint '{fp_name}' from library '{lib_name}'")
 532 |                         return loaded_fp
 533 |                 except Exception as e:
 534 |                     logger.warning(f"Could not load from {lib_name}: {e}")
 535 |             else:
 536 |                 # Search all libraries for the footprint
 537 |                 lib_names = fp_lib_table.GetLogicalLibs()
 538 |                 for lib in lib_names:
 539 |                     try:
 540 |                         loaded_fp = pcbnew.FootprintLoad(fp_lib_table, lib, fp_name)
 541 |                         if loaded_fp:
 542 |                             logger.info(f"Found footprint '{fp_name}' in library '{lib}'")
 543 |                             return loaded_fp
 544 |                     except:
 545 |                         continue
 546 | 
 547 |             logger.warning(f"Footprint '{footprint_path}' not found in any library")
 548 |             return None
 549 | 
 550 |         except ImportError:
 551 |             logger.warning("pcbnew not available - cannot load footprints from library")
 552 |             return None
 553 |         except Exception as e:
 554 |             logger.error(f"Error loading footprint from library: {e}")
 555 |             return None
 556 | 
 557 |     def _place_loaded_footprint(
 558 |         self,
 559 |         loaded_fp,
 560 |         reference: str,
 561 |         x: float,
 562 |         y: float,
 563 |         rotation: float,
 564 |         layer: str,
 565 |         value: str
 566 |     ) -> bool:
 567 |         """
 568 |         Place a loaded pcbnew footprint onto the board.
 569 | 
 570 |         Uses SWIG to add the footprint, then notifies for IPC sync.
 571 |         """
 572 |         try:
 573 |             import pcbnew
 574 | 
 575 |             # Get the board file path from IPC to load via pcbnew
 576 |             board = self._get_board()
 577 | 
 578 |             # Get the pcbnew board instance
 579 |             # We need to get the actual board file path
 580 |             project = board.get_project()
 581 |             board_path = None
 582 | 
 583 |             # Try to get the board path from kipy
 584 |             try:
 585 |                 docs = self._kicad.get_open_documents()
 586 |                 for doc in docs:
 587 |                     if hasattr(doc, 'path') and str(doc.path).endswith('.kicad_pcb'):
 588 |                         board_path = str(doc.path)
 589 |                         break
 590 |             except Exception as e:
 591 |                 logger.debug(f"Could not get board path from IPC: {e}")
 592 | 
 593 |             if board_path and os.path.exists(board_path):
 594 |                 # Load board via pcbnew
 595 |                 pcb_board = pcbnew.LoadBoard(board_path)
 596 |             else:
 597 |                 # Try to get from pcbnew directly
 598 |                 pcb_board = pcbnew.GetBoard()
 599 | 
 600 |             if not pcb_board:
 601 |                 logger.error("Could not get pcbnew board instance")
 602 |                 return self._place_placeholder_footprint(
 603 |                     reference, "", x, y, rotation, layer, value
 604 |                 )
 605 | 
 606 |             # Set footprint position and properties
 607 |             scale = MM_TO_NM
 608 |             loaded_fp.SetPosition(pcbnew.VECTOR2I(int(x * scale), int(y * scale)))
 609 |             loaded_fp.SetOrientationDegrees(rotation)
 610 | 
 611 |             # Set reference
 612 |             loaded_fp.SetReference(reference)
 613 | 
 614 |             # Set value if provided
 615 |             if value:
 616 |                 loaded_fp.SetValue(value)
 617 | 
 618 |             # Set layer (flip if bottom)
 619 |             if layer == "B.Cu":
 620 |                 if not loaded_fp.IsFlipped():
 621 |                     loaded_fp.Flip(loaded_fp.GetPosition(), False)
 622 | 
 623 |             # Add to board
 624 |             pcb_board.Add(loaded_fp)
 625 | 
 626 |             # Save the board so IPC can see the changes
 627 |             pcbnew.SaveBoard(board_path, pcb_board)
 628 | 
 629 |             # Refresh IPC view
 630 |             try:
 631 |                 board.revert()  # Reload from disk to sync IPC
 632 |             except Exception as e:
 633 |                 logger.debug(f"Could not refresh IPC board: {e}")
 634 | 
 635 |             self._notify("component_placed", {
 636 |                 "reference": reference,
 637 |                 "footprint": loaded_fp.GetFPIDAsString(),
 638 |                 "position": {"x": x, "y": y},
 639 |                 "rotation": rotation,
 640 |                 "layer": layer,
 641 |                 "loaded_from_library": True
 642 |             })
 643 | 
 644 |             logger.info(f"Placed component {reference} ({loaded_fp.GetFPIDAsString()}) at ({x}, {y}) mm")
 645 |             return True
 646 | 
 647 |         except Exception as e:
 648 |             logger.error(f"Error placing loaded footprint: {e}")
 649 |             # Fall back to placeholder
 650 |             return self._place_placeholder_footprint(
 651 |                 reference, "", x, y, rotation, layer, value
 652 |             )
 653 | 
 654 |     def _place_placeholder_footprint(
 655 |         self,
 656 |         reference: str,
 657 |         footprint: str,
 658 |         x: float,
 659 |         y: float,
 660 |         rotation: float,
 661 |         layer: str,
 662 |         value: str
 663 |     ) -> bool:
 664 |         """
 665 |         Place a placeholder footprint when library loading fails.
 666 | 
 667 |         Creates a basic footprint via IPC with just reference/value fields.
 668 |         """
 669 |         try:
 670 |             from kipy.board_types import Footprint
 671 |             from kipy.geometry import Vector2, Angle
 672 |             from kipy.util.units import from_mm
 673 |             from kipy.proto.board.board_types_pb2 import BoardLayer
 674 | 
 675 |             board = self._get_board()
 676 | 
 677 |             # Create footprint
 678 |             fp = Footprint()
 679 |             fp.position = Vector2.from_xy(from_mm(x), from_mm(y))
 680 |             fp.orientation = Angle.from_degrees(rotation)
 681 | 
 682 |             # Set layer
 683 |             if layer == "B.Cu":
 684 |                 fp.layer = BoardLayer.BL_B_Cu
 685 |             else:
 686 |                 fp.layer = BoardLayer.BL_F_Cu
 687 | 
 688 |             # Set reference and value
 689 |             if fp.reference_field:
 690 |                 fp.reference_field.text.value = reference
 691 |             if fp.value_field:
 692 |                 fp.value_field.text.value = value if value else footprint
 693 | 
 694 |             # Begin transaction
 695 |             commit = board.begin_commit()
 696 |             board.create_items(fp)
 697 |             board.push_commit(commit, f"Placed component {reference}")
 698 | 
 699 |             self._notify("component_placed", {
 700 |                 "reference": reference,
 701 |                 "footprint": footprint,
 702 |                 "position": {"x": x, "y": y},
 703 |                 "rotation": rotation,
 704 |                 "layer": layer,
 705 |                 "loaded_from_library": False,
 706 |                 "is_placeholder": True
 707 |             })
 708 | 
 709 |             logger.info(f"Placed placeholder component {reference} at ({x}, {y}) mm")
 710 |             return True
 711 | 
 712 |         except Exception as e:
 713 |             logger.error(f"Failed to place placeholder component: {e}")
 714 |             return False
 715 | 
 716 |     def move_component(self, reference: str, x: float, y: float, rotation: Optional[float] = None) -> bool:
 717 |         """Move a component to a new position (updates UI immediately)."""
 718 |         try:
 719 |             from kipy.geometry import Vector2, Angle
 720 |             from kipy.util.units import from_mm
 721 | 
 722 |             board = self._get_board()
 723 |             footprints = board.get_footprints()
 724 | 
 725 |             # Find the footprint by reference
 726 |             target_fp = None
 727 |             for fp in footprints:
 728 |                 if fp.reference_field and fp.reference_field.text.value == reference:
 729 |                     target_fp = fp
 730 |                     break
 731 | 
 732 |             if not target_fp:
 733 |                 logger.error(f"Component not found: {reference}")
 734 |                 return False
 735 | 
 736 |             # Update position
 737 |             target_fp.position = Vector2.from_xy(from_mm(x), from_mm(y))
 738 | 
 739 |             if rotation is not None:
 740 |                 target_fp.orientation = Angle.from_degrees(rotation)
 741 | 
 742 |             # Apply changes
 743 |             commit = board.begin_commit()
 744 |             board.update_items([target_fp])
 745 |             board.push_commit(commit, f"Moved component {reference}")
 746 | 
 747 |             self._notify("component_moved", {
 748 |                 "reference": reference,
 749 |                 "position": {"x": x, "y": y},
 750 |                 "rotation": rotation
 751 |             })
 752 | 
 753 |             return True
 754 | 
 755 |         except Exception as e:
 756 |             logger.error(f"Failed to move component: {e}")
 757 |             return False
 758 | 
 759 |     def delete_component(self, reference: str) -> bool:
 760 |         """Delete a component from the board."""
 761 |         try:
 762 |             board = self._get_board()
 763 |             footprints = board.get_footprints()
 764 | 
 765 |             # Find the footprint by reference
 766 |             target_fp = None
 767 |             for fp in footprints:
 768 |                 if fp.reference_field and fp.reference_field.text.value == reference:
 769 |                     target_fp = fp
 770 |                     break
 771 | 
 772 |             if not target_fp:
 773 |                 logger.error(f"Component not found: {reference}")
 774 |                 return False
 775 | 
 776 |             # Remove component
 777 |             commit = board.begin_commit()
 778 |             board.remove_items([target_fp])
 779 |             board.push_commit(commit, f"Deleted component {reference}")
 780 | 
 781 |             self._notify("component_deleted", {"reference": reference})
 782 | 
 783 |             return True
 784 | 
 785 |         except Exception as e:
 786 |             logger.error(f"Failed to delete component: {e}")
 787 |             return False
 788 | 
 789 |     def add_track(
 790 |         self,
 791 |         start_x: float,
 792 |         start_y: float,
 793 |         end_x: float,
 794 |         end_y: float,
 795 |         width: float = 0.25,
 796 |         layer: str = "F.Cu",
 797 |         net_name: Optional[str] = None
 798 |     ) -> bool:
 799 |         """
 800 |         Add a track (trace) to the board.
 801 | 
 802 |         The track appears immediately in the KiCAD UI.
 803 |         """
 804 |         try:
 805 |             from kipy.board_types import Track
 806 |             from kipy.geometry import Vector2
 807 |             from kipy.util.units import from_mm
 808 |             from kipy.proto.board.board_types_pb2 import BoardLayer
 809 | 
 810 |             board = self._get_board()
 811 | 
 812 |             # Create track
 813 |             track = Track()
 814 |             track.start = Vector2.from_xy(from_mm(start_x), from_mm(start_y))
 815 |             track.end = Vector2.from_xy(from_mm(end_x), from_mm(end_y))
 816 |             track.width = from_mm(width)
 817 | 
 818 |             # Set layer
 819 |             layer_map = {
 820 |                 "F.Cu": BoardLayer.BL_F_Cu,
 821 |                 "B.Cu": BoardLayer.BL_B_Cu,
 822 |                 "In1.Cu": BoardLayer.BL_In1_Cu,
 823 |                 "In2.Cu": BoardLayer.BL_In2_Cu,
 824 |             }
 825 |             track.layer = layer_map.get(layer, BoardLayer.BL_F_Cu)
 826 | 
 827 |             # Set net if specified
 828 |             if net_name:
 829 |                 nets = board.get_nets()
 830 |                 for net in nets:
 831 |                     if net.name == net_name:
 832 |                         track.net = net
 833 |                         break
 834 | 
 835 |             # Add track with transaction
 836 |             commit = board.begin_commit()
 837 |             board.create_items(track)
 838 |             board.push_commit(commit, "Added track")
 839 | 
 840 |             self._notify("track_added", {
 841 |                 "start": {"x": start_x, "y": start_y},
 842 |                 "end": {"x": end_x, "y": end_y},
 843 |                 "width": width,
 844 |                 "layer": layer,
 845 |                 "net": net_name
 846 |             })
 847 | 
 848 |             logger.info(f"Added track from ({start_x}, {start_y}) to ({end_x}, {end_y}) mm")
 849 |             return True
 850 | 
 851 |         except Exception as e:
 852 |             logger.error(f"Failed to add track: {e}")
 853 |             return False
 854 | 
 855 |     def add_via(
 856 |         self,
 857 |         x: float,
 858 |         y: float,
 859 |         diameter: float = 0.8,
 860 |         drill: float = 0.4,
 861 |         net_name: Optional[str] = None,
 862 |         via_type: str = "through"
 863 |     ) -> bool:
 864 |         """
 865 |         Add a via to the board.
 866 | 
 867 |         The via appears immediately in the KiCAD UI.
 868 |         """
 869 |         try:
 870 |             from kipy.board_types import Via
 871 |             from kipy.geometry import Vector2
 872 |             from kipy.util.units import from_mm
 873 |             from kipy.proto.board.board_types_pb2 import ViaType
 874 | 
 875 |             board = self._get_board()
 876 | 
 877 |             # Create via
 878 |             via = Via()
 879 |             via.position = Vector2.from_xy(from_mm(x), from_mm(y))
 880 |             via.diameter = from_mm(diameter)
 881 |             via.drill_diameter = from_mm(drill)
 882 | 
 883 |             # Set via type (enum values: VT_THROUGH=1, VT_BLIND_BURIED=2, VT_MICRO=3)
 884 |             type_map = {
 885 |                 "through": ViaType.VT_THROUGH,
 886 |                 "blind": ViaType.VT_BLIND_BURIED,
 887 |                 "micro": ViaType.VT_MICRO,
 888 |             }
 889 |             via.type = type_map.get(via_type, ViaType.VT_THROUGH)
 890 | 
 891 |             # Set net if specified
 892 |             if net_name:
 893 |                 nets = board.get_nets()
 894 |                 for net in nets:
 895 |                     if net.name == net_name:
 896 |                         via.net = net
 897 |                         break
 898 | 
 899 |             # Add via with transaction
 900 |             commit = board.begin_commit()
 901 |             board.create_items(via)
 902 |             board.push_commit(commit, "Added via")
 903 | 
 904 |             self._notify("via_added", {
 905 |                 "position": {"x": x, "y": y},
 906 |                 "diameter": diameter,
 907 |                 "drill": drill,
 908 |                 "net": net_name,
 909 |                 "type": via_type
 910 |             })
 911 | 
 912 |             logger.info(f"Added via at ({x}, {y}) mm")
 913 |             return True
 914 | 
 915 |         except Exception as e:
 916 |             logger.error(f"Failed to add via: {e}")
 917 |             return False
 918 | 
 919 |     def add_text(
 920 |         self,
 921 |         text: str,
 922 |         x: float,
 923 |         y: float,
 924 |         layer: str = "F.SilkS",
 925 |         size: float = 1.0,
 926 |         rotation: float = 0
 927 |     ) -> bool:
 928 |         """Add text to the board."""
 929 |         try:
 930 |             from kipy.board_types import BoardText
 931 |             from kipy.geometry import Vector2, Angle
 932 |             from kipy.util.units import from_mm
 933 |             from kipy.proto.board.board_types_pb2 import BoardLayer
 934 | 
 935 |             board = self._get_board()
 936 | 
 937 |             # Create text
 938 |             board_text = BoardText()
 939 |             board_text.value = text
 940 |             board_text.position = Vector2.from_xy(from_mm(x), from_mm(y))
 941 |             board_text.angle = Angle.from_degrees(rotation)
 942 | 
 943 |             # Set layer
 944 |             layer_map = {
 945 |                 "F.SilkS": BoardLayer.BL_F_SilkS,
 946 |                 "B.SilkS": BoardLayer.BL_B_SilkS,
 947 |                 "F.Cu": BoardLayer.BL_F_Cu,
 948 |                 "B.Cu": BoardLayer.BL_B_Cu,
 949 |             }
 950 |             board_text.layer = layer_map.get(layer, BoardLayer.BL_F_SilkS)
 951 | 
 952 |             # Add text with transaction
 953 |             commit = board.begin_commit()
 954 |             board.create_items(board_text)
 955 |             board.push_commit(commit, f"Added text: {text}")
 956 | 
 957 |             self._notify("text_added", {
 958 |                 "text": text,
 959 |                 "position": {"x": x, "y": y},
 960 |                 "layer": layer
 961 |             })
 962 | 
 963 |             return True
 964 | 
 965 |         except Exception as e:
 966 |             logger.error(f"Failed to add text: {e}")
 967 |             return False
 968 | 
 969 |     def get_tracks(self) -> List[Dict[str, Any]]:
 970 |         """Get all tracks on the board."""
 971 |         try:
 972 |             from kipy.util.units import to_mm
 973 | 
 974 |             board = self._get_board()
 975 |             tracks = board.get_tracks()
 976 | 
 977 |             result = []
 978 |             for track in tracks:
 979 |                 try:
 980 |                     result.append({
 981 |                         "start": {
 982 |                             "x": to_mm(track.start.x),
 983 |                             "y": to_mm(track.start.y)
 984 |                         },
 985 |                         "end": {
 986 |                             "x": to_mm(track.end.x),
 987 |                             "y": to_mm(track.end.y)
 988 |                         },
 989 |                         "width": to_mm(track.width),
 990 |                         "layer": str(track.layer),
 991 |                         "net": track.net.name if track.net else "",
 992 |                         "id": str(track.id) if hasattr(track, 'id') else ""
 993 |                     })
 994 |                 except Exception as e:
 995 |                     logger.warning(f"Error processing track: {e}")
 996 |                     continue
 997 | 
 998 |             return result
 999 | 
1000 |         except Exception as e:
1001 |             logger.error(f"Failed to get tracks: {e}")
1002 |             return []
1003 | 
1004 |     def get_vias(self) -> List[Dict[str, Any]]:
1005 |         """Get all vias on the board."""
1006 |         try:
1007 |             from kipy.util.units import to_mm
1008 | 
1009 |             board = self._get_board()
1010 |             vias = board.get_vias()
1011 | 
1012 |             result = []
1013 |             for via in vias:
1014 |                 try:
1015 |                     result.append({
1016 |                         "position": {
1017 |                             "x": to_mm(via.position.x),
1018 |                             "y": to_mm(via.position.y)
1019 |                         },
1020 |                         "diameter": to_mm(via.diameter),
1021 |                         "drill": to_mm(via.drill_diameter),
1022 |                         "net": via.net.name if via.net else "",
1023 |                         "type": str(via.type),
1024 |                         "id": str(via.id) if hasattr(via, 'id') else ""
1025 |                     })
1026 |                 except Exception as e:
1027 |                     logger.warning(f"Error processing via: {e}")
1028 |                     continue
1029 | 
1030 |             return result
1031 | 
1032 |         except Exception as e:
1033 |             logger.error(f"Failed to get vias: {e}")
1034 |             return []
1035 | 
1036 |     def get_nets(self) -> List[Dict[str, Any]]:
1037 |         """Get all nets on the board."""
1038 |         try:
1039 |             board = self._get_board()
1040 |             nets = board.get_nets()
1041 | 
1042 |             result = []
1043 |             for net in nets:
1044 |                 try:
1045 |                     result.append({
1046 |                         "name": net.name,
1047 |                         "code": net.code if hasattr(net, 'code') else 0
1048 |                     })
1049 |                 except Exception as e:
1050 |                     logger.warning(f"Error processing net: {e}")
1051 |                     continue
1052 | 
1053 |             return result
1054 | 
1055 |         except Exception as e:
1056 |             logger.error(f"Failed to get nets: {e}")
1057 |             return []
1058 | 
1059 |     def add_zone(
1060 |         self,
1061 |         points: List[Dict[str, float]],
1062 |         layer: str = "F.Cu",
1063 |         net_name: Optional[str] = None,
1064 |         clearance: float = 0.5,
1065 |         min_thickness: float = 0.25,
1066 |         priority: int = 0,
1067 |         fill_mode: str = "solid",
1068 |         name: str = ""
1069 |     ) -> bool:
1070 |         """
1071 |         Add a copper pour zone to the board.
1072 | 
1073 |         The zone appears immediately in the KiCAD UI.
1074 | 
1075 |         Args:
1076 |             points: List of points defining the zone outline, e.g. [{"x": 0, "y": 0}, ...]
1077 |             layer: Layer name (F.Cu, B.Cu, etc.)
1078 |             net_name: Net to connect the zone to (e.g., "GND")
1079 |             clearance: Clearance from other copper in mm
1080 |             min_thickness: Minimum copper thickness in mm
1081 |             priority: Zone priority (higher = fills first)
1082 |             fill_mode: "solid" or "hatched"
1083 |             name: Optional zone name
1084 |         """
1085 |         try:
1086 |             from kipy.board_types import Zone, ZoneFillMode, ZoneType
1087 |             from kipy.geometry import PolyLine, PolyLineNode, Vector2
1088 |             from kipy.util.units import from_mm
1089 |             from kipy.proto.board.board_types_pb2 import BoardLayer
1090 | 
1091 |             board = self._get_board()
1092 | 
1093 |             if len(points) < 3:
1094 |                 logger.error("Zone requires at least 3 points")
1095 |                 return False
1096 | 
1097 |             # Create zone
1098 |             zone = Zone()
1099 |             zone.type = ZoneType.ZT_COPPER
1100 | 
1101 |             # Set layer
1102 |             layer_map = {
1103 |                 "F.Cu": BoardLayer.BL_F_Cu,
1104 |                 "B.Cu": BoardLayer.BL_B_Cu,
1105 |                 "In1.Cu": BoardLayer.BL_In1_Cu,
1106 |                 "In2.Cu": BoardLayer.BL_In2_Cu,
1107 |                 "In3.Cu": BoardLayer.BL_In3_Cu,
1108 |                 "In4.Cu": BoardLayer.BL_In4_Cu,
1109 |             }
1110 |             zone.layers = [layer_map.get(layer, BoardLayer.BL_F_Cu)]
1111 | 
1112 |             # Set net if specified
1113 |             if net_name:
1114 |                 nets = board.get_nets()
1115 |                 for net in nets:
1116 |                     if net.name == net_name:
1117 |                         zone.net = net
1118 |                         break
1119 | 
1120 |             # Set zone properties
1121 |             zone.clearance = from_mm(clearance)
1122 |             zone.min_thickness = from_mm(min_thickness)
1123 |             zone.priority = priority
1124 | 
1125 |             if name:
1126 |                 zone.name = name
1127 | 
1128 |             # Set fill mode
1129 |             if fill_mode == "hatched":
1130 |                 zone.fill_mode = ZoneFillMode.ZFM_HATCHED
1131 |             else:
1132 |                 zone.fill_mode = ZoneFillMode.ZFM_SOLID
1133 | 
1134 |             # Create outline polyline
1135 |             outline = PolyLine()
1136 |             outline.closed = True
1137 | 
1138 |             for point in points:
1139 |                 x = point.get("x", 0)
1140 |                 y = point.get("y", 0)
1141 |                 node = PolyLineNode.from_xy(from_mm(x), from_mm(y))
1142 |                 outline.append(node)
1143 | 
1144 |             # Set the outline on the zone
1145 |             # Note: Zone outline is set via the proto directly since kipy
1146 |             # doesn't expose a direct setter for creating new zones
1147 |             zone._proto.outline.polygons.add()
1148 |             zone._proto.outline.polygons[0].outline.CopyFrom(outline._proto)
1149 | 
1150 |             # Add zone with transaction
1151 |             commit = board.begin_commit()
1152 |             board.create_items(zone)
1153 |             board.push_commit(commit, f"Added copper zone on {layer}")
1154 | 
1155 |             self._notify("zone_added", {
1156 |                 "layer": layer,
1157 |                 "net": net_name,
1158 |                 "points": len(points),
1159 |                 "priority": priority
1160 |             })
1161 | 
1162 |             logger.info(f"Added zone on {layer} with {len(points)} points")
1163 |             return True
1164 | 
1165 |         except Exception as e:
1166 |             logger.error(f"Failed to add zone: {e}")
1167 |             return False
1168 | 
1169 |     def get_zones(self) -> List[Dict[str, Any]]:
1170 |         """Get all zones on the board."""
1171 |         try:
1172 |             from kipy.util.units import to_mm
1173 | 
1174 |             board = self._get_board()
1175 |             zones = board.get_zones()
1176 | 
1177 |             result = []
1178 |             for zone in zones:
1179 |                 try:
1180 |                     result.append({
1181 |                         "name": zone.name if hasattr(zone, 'name') else "",
1182 |                         "net": zone.net.name if zone.net else "",
1183 |                         "priority": zone.priority if hasattr(zone, 'priority') else 0,
1184 |                         "layers": [str(l) for l in zone.layers] if hasattr(zone, 'layers') else [],
1185 |                         "filled": zone.filled if hasattr(zone, 'filled') else False,
1186 |                         "id": str(zone.id) if hasattr(zone, 'id') else ""
1187 |                     })
1188 |                 except Exception as e:
1189 |                     logger.warning(f"Error processing zone: {e}")
1190 |                     continue
1191 | 
1192 |             return result
1193 | 
1194 |         except Exception as e:
1195 |             logger.error(f"Failed to get zones: {e}")
1196 |             return []
1197 | 
1198 |     def refill_zones(self) -> bool:
1199 |         """Refill all copper pour zones."""
1200 |         try:
1201 |             board = self._get_board()
1202 |             board.refill_zones()
1203 |             self._notify("zones_refilled", {})
1204 |             return True
1205 |         except Exception as e:
1206 |             logger.error(f"Failed to refill zones: {e}")
1207 |             return False
1208 | 
1209 |     def get_selection(self) -> List[Dict[str, Any]]:
1210 |         """Get currently selected items in the KiCAD UI."""
1211 |         try:
1212 |             board = self._get_board()
1213 |             selection = board.get_selection()
1214 | 
1215 |             result = []
1216 |             for item in selection:
1217 |                 result.append({
1218 |                     "type": type(item).__name__,
1219 |                     "id": str(item.id) if hasattr(item, 'id') else ""
1220 |                 })
1221 | 
1222 |             return result
1223 |         except Exception as e:
1224 |             logger.error(f"Failed to get selection: {e}")
1225 |             return []
1226 | 
1227 |     def clear_selection(self) -> bool:
1228 |         """Clear the current selection in KiCAD UI."""
1229 |         try:
1230 |             board = self._get_board()
1231 |             board.clear_selection()
1232 |             return True
1233 |         except Exception as e:
1234 |             logger.error(f"Failed to clear selection: {e}")
1235 |             return False
1236 | 
1237 | 
1238 | # Export for factory
1239 | __all__ = ['IPCBackend', 'IPCBoardAPI']
1240 | 
```

--------------------------------------------------------------------------------
/python/schemas/tool_schemas.py:
--------------------------------------------------------------------------------

```python
   1 | """
   2 | Comprehensive tool schema definitions for all KiCAD MCP commands
   3 | 
   4 | Following MCP 2025-06-18 specification for tool definitions.
   5 | Each tool includes:
   6 | - name: Unique identifier
   7 | - title: Human-readable display name
   8 | - description: Detailed explanation of what the tool does
   9 | - inputSchema: JSON Schema for parameters
  10 | - outputSchema: Optional JSON Schema for return values (structured content)
  11 | """
  12 | 
  13 | from typing import Dict, Any
  14 | 
  15 | # =============================================================================
  16 | # PROJECT TOOLS
  17 | # =============================================================================
  18 | 
  19 | PROJECT_TOOLS = [
  20 |     {
  21 |         "name": "create_project",
  22 |         "title": "Create New KiCAD Project",
  23 |         "description": "Creates a new KiCAD project with PCB board file and optional project configuration. Automatically creates project directory and initializes board with default settings.",
  24 |         "inputSchema": {
  25 |             "type": "object",
  26 |             "properties": {
  27 |                 "projectName": {
  28 |                     "type": "string",
  29 |                     "description": "Name of the project (used for file naming)",
  30 |                     "minLength": 1
  31 |                 },
  32 |                 "path": {
  33 |                     "type": "string",
  34 |                     "description": "Directory path where project will be created (defaults to current working directory)"
  35 |                 },
  36 |                 "template": {
  37 |                     "type": "string",
  38 |                     "description": "Optional path to template board file to copy settings from"
  39 |                 }
  40 |             },
  41 |             "required": ["projectName"]
  42 |         }
  43 |     },
  44 |     {
  45 |         "name": "open_project",
  46 |         "title": "Open Existing KiCAD Project",
  47 |         "description": "Opens an existing KiCAD project file (.kicad_pro or .kicad_pcb) and loads the board into memory for manipulation.",
  48 |         "inputSchema": {
  49 |             "type": "object",
  50 |             "properties": {
  51 |                 "filename": {
  52 |                     "type": "string",
  53 |                     "description": "Path to .kicad_pro or .kicad_pcb file"
  54 |                 }
  55 |             },
  56 |             "required": ["filename"]
  57 |         }
  58 |     },
  59 |     {
  60 |         "name": "save_project",
  61 |         "title": "Save Current Project",
  62 |         "description": "Saves the current board to disk. Can optionally save to a new location.",
  63 |         "inputSchema": {
  64 |             "type": "object",
  65 |             "properties": {
  66 |                 "filename": {
  67 |                     "type": "string",
  68 |                     "description": "Optional new path to save the board (if not provided, saves to current location)"
  69 |                 }
  70 |             }
  71 |         }
  72 |     },
  73 |     {
  74 |         "name": "get_project_info",
  75 |         "title": "Get Project Information",
  76 |         "description": "Retrieves metadata and properties of the currently open project including name, paths, and board status.",
  77 |         "inputSchema": {
  78 |             "type": "object",
  79 |             "properties": {}
  80 |         }
  81 |     }
  82 | ]
  83 | 
  84 | # =============================================================================
  85 | # BOARD TOOLS
  86 | # =============================================================================
  87 | 
  88 | BOARD_TOOLS = [
  89 |     {
  90 |         "name": "set_board_size",
  91 |         "title": "Set Board Dimensions",
  92 |         "description": "Sets the PCB board dimensions. The board outline must be added separately using add_board_outline.",
  93 |         "inputSchema": {
  94 |             "type": "object",
  95 |             "properties": {
  96 |                 "width": {
  97 |                     "type": "number",
  98 |                     "description": "Board width in millimeters",
  99 |                     "minimum": 1
 100 |                 },
 101 |                 "height": {
 102 |                     "type": "number",
 103 |                     "description": "Board height in millimeters",
 104 |                     "minimum": 1
 105 |                 }
 106 |             },
 107 |             "required": ["width", "height"]
 108 |         }
 109 |     },
 110 |     {
 111 |         "name": "add_board_outline",
 112 |         "title": "Add Board Outline",
 113 |         "description": "Adds a board outline shape (rectangle, rounded rectangle, circle, or polygon) on the Edge.Cuts layer.",
 114 |         "inputSchema": {
 115 |             "type": "object",
 116 |             "properties": {
 117 |                 "shape": {
 118 |                     "type": "string",
 119 |                     "enum": ["rectangle", "rounded_rectangle", "circle", "polygon"],
 120 |                     "description": "Shape type for the board outline"
 121 |                 },
 122 |                 "width": {
 123 |                     "type": "number",
 124 |                     "description": "Width in mm (for rectangle/rounded_rectangle)",
 125 |                     "minimum": 1
 126 |                 },
 127 |                 "height": {
 128 |                     "type": "number",
 129 |                     "description": "Height in mm (for rectangle/rounded_rectangle)",
 130 |                     "minimum": 1
 131 |                 },
 132 |                 "radius": {
 133 |                     "type": "number",
 134 |                     "description": "Radius in mm (for circle) or corner radius (for rounded_rectangle)",
 135 |                     "minimum": 0
 136 |                 },
 137 |                 "points": {
 138 |                     "type": "array",
 139 |                     "description": "Array of [x, y] coordinates in mm (for polygon)",
 140 |                     "items": {
 141 |                         "type": "array",
 142 |                         "items": {"type": "number"},
 143 |                         "minItems": 2,
 144 |                         "maxItems": 2
 145 |                     },
 146 |                     "minItems": 3
 147 |                 }
 148 |             },
 149 |             "required": ["shape"]
 150 |         }
 151 |     },
 152 |     {
 153 |         "name": "add_layer",
 154 |         "title": "Add Custom Layer",
 155 |         "description": "Adds a new custom layer to the board stack (e.g., User.1, User.Comments).",
 156 |         "inputSchema": {
 157 |             "type": "object",
 158 |             "properties": {
 159 |                 "layerName": {
 160 |                     "type": "string",
 161 |                     "description": "Name of the layer to add"
 162 |                 },
 163 |                 "layerType": {
 164 |                     "type": "string",
 165 |                     "enum": ["signal", "power", "mixed", "jumper"],
 166 |                     "description": "Type of layer (for copper layers)"
 167 |                 }
 168 |             },
 169 |             "required": ["layerName"]
 170 |         }
 171 |     },
 172 |     {
 173 |         "name": "set_active_layer",
 174 |         "title": "Set Active Layer",
 175 |         "description": "Sets the currently active layer for drawing operations.",
 176 |         "inputSchema": {
 177 |             "type": "object",
 178 |             "properties": {
 179 |                 "layerName": {
 180 |                     "type": "string",
 181 |                     "description": "Name of the layer to make active (e.g., F.Cu, B.Cu, Edge.Cuts)"
 182 |                 }
 183 |             },
 184 |             "required": ["layerName"]
 185 |         }
 186 |     },
 187 |     {
 188 |         "name": "get_layer_list",
 189 |         "title": "List Board Layers",
 190 |         "description": "Returns a list of all layers in the board with their properties.",
 191 |         "inputSchema": {
 192 |             "type": "object",
 193 |             "properties": {}
 194 |         }
 195 |     },
 196 |     {
 197 |         "name": "get_board_info",
 198 |         "title": "Get Board Information",
 199 |         "description": "Retrieves comprehensive board information including dimensions, layer count, component count, and design rules.",
 200 |         "inputSchema": {
 201 |             "type": "object",
 202 |             "properties": {}
 203 |         }
 204 |     },
 205 |     {
 206 |         "name": "get_board_2d_view",
 207 |         "title": "Render Board Preview",
 208 |         "description": "Generates a 2D visual representation of the current board state as a PNG image.",
 209 |         "inputSchema": {
 210 |             "type": "object",
 211 |             "properties": {
 212 |                 "width": {
 213 |                     "type": "number",
 214 |                     "description": "Image width in pixels (default: 800)",
 215 |                     "minimum": 100,
 216 |                     "default": 800
 217 |                 },
 218 |                 "height": {
 219 |                     "type": "number",
 220 |                     "description": "Image height in pixels (default: 600)",
 221 |                     "minimum": 100,
 222 |                     "default": 600
 223 |                 }
 224 |             }
 225 |         }
 226 |     },
 227 |     {
 228 |         "name": "add_mounting_hole",
 229 |         "title": "Add Mounting Hole",
 230 |         "description": "Adds a mounting hole (non-plated through hole) at the specified position with given diameter.",
 231 |         "inputSchema": {
 232 |             "type": "object",
 233 |             "properties": {
 234 |                 "x": {
 235 |                     "type": "number",
 236 |                     "description": "X coordinate in millimeters"
 237 |                 },
 238 |                 "y": {
 239 |                     "type": "number",
 240 |                     "description": "Y coordinate in millimeters"
 241 |                 },
 242 |                 "diameter": {
 243 |                     "type": "number",
 244 |                     "description": "Hole diameter in millimeters",
 245 |                     "minimum": 0.1
 246 |                 }
 247 |             },
 248 |             "required": ["x", "y", "diameter"]
 249 |         }
 250 |     },
 251 |     {
 252 |         "name": "add_board_text",
 253 |         "title": "Add Text to Board",
 254 |         "description": "Adds text annotation to the board on a specified layer (e.g., F.SilkS for top silkscreen).",
 255 |         "inputSchema": {
 256 |             "type": "object",
 257 |             "properties": {
 258 |                 "text": {
 259 |                     "type": "string",
 260 |                     "description": "Text content to add",
 261 |                     "minLength": 1
 262 |                 },
 263 |                 "x": {
 264 |                     "type": "number",
 265 |                     "description": "X coordinate in millimeters"
 266 |                 },
 267 |                 "y": {
 268 |                     "type": "number",
 269 |                     "description": "Y coordinate in millimeters"
 270 |                 },
 271 |                 "layer": {
 272 |                     "type": "string",
 273 |                     "description": "Layer name (e.g., F.SilkS, B.SilkS, F.Cu)",
 274 |                     "default": "F.SilkS"
 275 |                 },
 276 |                 "size": {
 277 |                     "type": "number",
 278 |                     "description": "Text size in millimeters",
 279 |                     "minimum": 0.1,
 280 |                     "default": 1.0
 281 |                 },
 282 |                 "thickness": {
 283 |                     "type": "number",
 284 |                     "description": "Text thickness in millimeters",
 285 |                     "minimum": 0.01,
 286 |                     "default": 0.15
 287 |                 }
 288 |             },
 289 |             "required": ["text", "x", "y"]
 290 |         }
 291 |     }
 292 | ]
 293 | 
 294 | # =============================================================================
 295 | # COMPONENT TOOLS
 296 | # =============================================================================
 297 | 
 298 | COMPONENT_TOOLS = [
 299 |     {
 300 |         "name": "place_component",
 301 |         "title": "Place Component",
 302 |         "description": "Places a component with specified footprint at given coordinates on the board.",
 303 |         "inputSchema": {
 304 |             "type": "object",
 305 |             "properties": {
 306 |                 "reference": {
 307 |                     "type": "string",
 308 |                     "description": "Component reference designator (e.g., R1, C2, U3)"
 309 |                 },
 310 |                 "footprint": {
 311 |                     "type": "string",
 312 |                     "description": "Footprint library:name (e.g., Resistor_SMD:R_0805_2012Metric)"
 313 |                 },
 314 |                 "x": {
 315 |                     "type": "number",
 316 |                     "description": "X coordinate in millimeters"
 317 |                 },
 318 |                 "y": {
 319 |                     "type": "number",
 320 |                     "description": "Y coordinate in millimeters"
 321 |                 },
 322 |                 "rotation": {
 323 |                     "type": "number",
 324 |                     "description": "Rotation angle in degrees (0-360)",
 325 |                     "minimum": 0,
 326 |                     "maximum": 360,
 327 |                     "default": 0
 328 |                 },
 329 |                 "layer": {
 330 |                     "type": "string",
 331 |                     "enum": ["F.Cu", "B.Cu"],
 332 |                     "description": "Board layer (top or bottom)",
 333 |                     "default": "F.Cu"
 334 |                 }
 335 |             },
 336 |             "required": ["reference", "footprint", "x", "y"]
 337 |         }
 338 |     },
 339 |     {
 340 |         "name": "move_component",
 341 |         "title": "Move Component",
 342 |         "description": "Moves an existing component to a new position on the board.",
 343 |         "inputSchema": {
 344 |             "type": "object",
 345 |             "properties": {
 346 |                 "reference": {
 347 |                     "type": "string",
 348 |                     "description": "Component reference designator"
 349 |                 },
 350 |                 "x": {
 351 |                     "type": "number",
 352 |                     "description": "New X coordinate in millimeters"
 353 |                 },
 354 |                 "y": {
 355 |                     "type": "number",
 356 |                     "description": "New Y coordinate in millimeters"
 357 |                 }
 358 |             },
 359 |             "required": ["reference", "x", "y"]
 360 |         }
 361 |     },
 362 |     {
 363 |         "name": "rotate_component",
 364 |         "title": "Rotate Component",
 365 |         "description": "Rotates a component by specified angle. Rotation is cumulative with existing rotation.",
 366 |         "inputSchema": {
 367 |             "type": "object",
 368 |             "properties": {
 369 |                 "reference": {
 370 |                     "type": "string",
 371 |                     "description": "Component reference designator"
 372 |                 },
 373 |                 "angle": {
 374 |                     "type": "number",
 375 |                     "description": "Rotation angle in degrees (positive = counterclockwise)"
 376 |                 }
 377 |             },
 378 |             "required": ["reference", "angle"]
 379 |         }
 380 |     },
 381 |     {
 382 |         "name": "delete_component",
 383 |         "title": "Delete Component",
 384 |         "description": "Removes a component from the board.",
 385 |         "inputSchema": {
 386 |             "type": "object",
 387 |             "properties": {
 388 |                 "reference": {
 389 |                     "type": "string",
 390 |                     "description": "Component reference designator"
 391 |                 }
 392 |             },
 393 |             "required": ["reference"]
 394 |         }
 395 |     },
 396 |     {
 397 |         "name": "edit_component",
 398 |         "title": "Edit Component Properties",
 399 |         "description": "Modifies properties of an existing component (value, footprint, etc.).",
 400 |         "inputSchema": {
 401 |             "type": "object",
 402 |             "properties": {
 403 |                 "reference": {
 404 |                     "type": "string",
 405 |                     "description": "Component reference designator"
 406 |                 },
 407 |                 "value": {
 408 |                     "type": "string",
 409 |                     "description": "New component value"
 410 |                 },
 411 |                 "footprint": {
 412 |                     "type": "string",
 413 |                     "description": "New footprint library:name"
 414 |                 }
 415 |             },
 416 |             "required": ["reference"]
 417 |         }
 418 |     },
 419 |     {
 420 |         "name": "get_component_properties",
 421 |         "title": "Get Component Properties",
 422 |         "description": "Retrieves detailed properties of a specific component.",
 423 |         "inputSchema": {
 424 |             "type": "object",
 425 |             "properties": {
 426 |                 "reference": {
 427 |                     "type": "string",
 428 |                     "description": "Component reference designator"
 429 |                 }
 430 |             },
 431 |             "required": ["reference"]
 432 |         }
 433 |     },
 434 |     {
 435 |         "name": "get_component_list",
 436 |         "title": "List All Components",
 437 |         "description": "Returns a list of all components on the board with their properties.",
 438 |         "inputSchema": {
 439 |             "type": "object",
 440 |             "properties": {}
 441 |         }
 442 |     },
 443 |     {
 444 |         "name": "place_component_array",
 445 |         "title": "Place Component Array",
 446 |         "description": "Places multiple copies of a component in a grid or circular pattern.",
 447 |         "inputSchema": {
 448 |             "type": "object",
 449 |             "properties": {
 450 |                 "referencePrefix": {
 451 |                     "type": "string",
 452 |                     "description": "Reference prefix (e.g., 'R' for R1, R2, R3...)"
 453 |                 },
 454 |                 "startNumber": {
 455 |                     "type": "integer",
 456 |                     "description": "Starting number for references",
 457 |                     "minimum": 1,
 458 |                     "default": 1
 459 |                 },
 460 |                 "footprint": {
 461 |                     "type": "string",
 462 |                     "description": "Footprint library:name"
 463 |                 },
 464 |                 "pattern": {
 465 |                     "type": "string",
 466 |                     "enum": ["grid", "circular"],
 467 |                     "description": "Array pattern type"
 468 |                 },
 469 |                 "count": {
 470 |                     "type": "integer",
 471 |                     "description": "Total number of components to place",
 472 |                     "minimum": 1
 473 |                 },
 474 |                 "startX": {
 475 |                     "type": "number",
 476 |                     "description": "Starting X coordinate in millimeters"
 477 |                 },
 478 |                 "startY": {
 479 |                     "type": "number",
 480 |                     "description": "Starting Y coordinate in millimeters"
 481 |                 },
 482 |                 "spacingX": {
 483 |                     "type": "number",
 484 |                     "description": "Horizontal spacing in mm (for grid pattern)"
 485 |                 },
 486 |                 "spacingY": {
 487 |                     "type": "number",
 488 |                     "description": "Vertical spacing in mm (for grid pattern)"
 489 |                 },
 490 |                 "radius": {
 491 |                     "type": "number",
 492 |                     "description": "Circle radius in mm (for circular pattern)"
 493 |                 },
 494 |                 "rows": {
 495 |                     "type": "integer",
 496 |                     "description": "Number of rows (for grid pattern)",
 497 |                     "minimum": 1
 498 |                 },
 499 |                 "columns": {
 500 |                     "type": "integer",
 501 |                     "description": "Number of columns (for grid pattern)",
 502 |                     "minimum": 1
 503 |                 }
 504 |             },
 505 |             "required": ["referencePrefix", "footprint", "pattern", "count", "startX", "startY"]
 506 |         }
 507 |     },
 508 |     {
 509 |         "name": "align_components",
 510 |         "title": "Align Components",
 511 |         "description": "Aligns multiple components horizontally or vertically.",
 512 |         "inputSchema": {
 513 |             "type": "object",
 514 |             "properties": {
 515 |                 "references": {
 516 |                     "type": "array",
 517 |                     "description": "Array of component reference designators to align",
 518 |                     "items": {"type": "string"},
 519 |                     "minItems": 2
 520 |                 },
 521 |                 "direction": {
 522 |                     "type": "string",
 523 |                     "enum": ["horizontal", "vertical"],
 524 |                     "description": "Alignment direction"
 525 |                 },
 526 |                 "spacing": {
 527 |                     "type": "number",
 528 |                     "description": "Spacing between components in mm (optional, for even distribution)"
 529 |                 }
 530 |             },
 531 |             "required": ["references", "direction"]
 532 |         }
 533 |     },
 534 |     {
 535 |         "name": "duplicate_component",
 536 |         "title": "Duplicate Component",
 537 |         "description": "Creates a copy of an existing component with new reference designator.",
 538 |         "inputSchema": {
 539 |             "type": "object",
 540 |             "properties": {
 541 |                 "sourceReference": {
 542 |                     "type": "string",
 543 |                     "description": "Reference of component to duplicate"
 544 |                 },
 545 |                 "newReference": {
 546 |                     "type": "string",
 547 |                     "description": "Reference designator for the new component"
 548 |                 },
 549 |                 "offsetX": {
 550 |                     "type": "number",
 551 |                     "description": "X offset from original position in mm",
 552 |                     "default": 0
 553 |                 },
 554 |                 "offsetY": {
 555 |                     "type": "number",
 556 |                     "description": "Y offset from original position in mm",
 557 |                     "default": 0
 558 |                 }
 559 |             },
 560 |             "required": ["sourceReference", "newReference"]
 561 |         }
 562 |     }
 563 | ]
 564 | 
 565 | # =============================================================================
 566 | # ROUTING TOOLS
 567 | # =============================================================================
 568 | 
 569 | ROUTING_TOOLS = [
 570 |     {
 571 |         "name": "add_net",
 572 |         "title": "Create Electrical Net",
 573 |         "description": "Creates a new electrical net for signal routing.",
 574 |         "inputSchema": {
 575 |             "type": "object",
 576 |             "properties": {
 577 |                 "netName": {
 578 |                     "type": "string",
 579 |                     "description": "Name of the net (e.g., VCC, GND, SDA)",
 580 |                     "minLength": 1
 581 |                 },
 582 |                 "netClass": {
 583 |                     "type": "string",
 584 |                     "description": "Optional net class to assign (must exist first)"
 585 |                 }
 586 |             },
 587 |             "required": ["netName"]
 588 |         }
 589 |     },
 590 |     {
 591 |         "name": "route_trace",
 592 |         "title": "Route PCB Trace",
 593 |         "description": "Routes a copper trace between two points or pads on a specified layer.",
 594 |         "inputSchema": {
 595 |             "type": "object",
 596 |             "properties": {
 597 |                 "netName": {
 598 |                     "type": "string",
 599 |                     "description": "Net name for this trace"
 600 |                 },
 601 |                 "layer": {
 602 |                     "type": "string",
 603 |                     "description": "Layer to route on (e.g., F.Cu, B.Cu)",
 604 |                     "default": "F.Cu"
 605 |                 },
 606 |                 "width": {
 607 |                     "type": "number",
 608 |                     "description": "Trace width in millimeters",
 609 |                     "minimum": 0.1
 610 |                 },
 611 |                 "points": {
 612 |                     "type": "array",
 613 |                     "description": "Array of [x, y] waypoints in millimeters",
 614 |                     "items": {
 615 |                         "type": "array",
 616 |                         "items": {"type": "number"},
 617 |                         "minItems": 2,
 618 |                         "maxItems": 2
 619 |                     },
 620 |                     "minItems": 2
 621 |                 }
 622 |             },
 623 |             "required": ["points", "width"]
 624 |         }
 625 |     },
 626 |     {
 627 |         "name": "add_via",
 628 |         "title": "Add Via",
 629 |         "description": "Adds a via (plated through-hole) to connect traces between layers.",
 630 |         "inputSchema": {
 631 |             "type": "object",
 632 |             "properties": {
 633 |                 "x": {
 634 |                     "type": "number",
 635 |                     "description": "X coordinate in millimeters"
 636 |                 },
 637 |                 "y": {
 638 |                     "type": "number",
 639 |                     "description": "Y coordinate in millimeters"
 640 |                 },
 641 |                 "diameter": {
 642 |                     "type": "number",
 643 |                     "description": "Via diameter in millimeters",
 644 |                     "minimum": 0.1
 645 |                 },
 646 |                 "drill": {
 647 |                     "type": "number",
 648 |                     "description": "Drill diameter in millimeters",
 649 |                     "minimum": 0.1
 650 |                 },
 651 |                 "netName": {
 652 |                     "type": "string",
 653 |                     "description": "Net name to assign to this via"
 654 |                 }
 655 |             },
 656 |             "required": ["x", "y", "diameter", "drill"]
 657 |         }
 658 |     },
 659 |     {
 660 |         "name": "delete_trace",
 661 |         "title": "Delete Trace",
 662 |         "description": "Removes a trace or segment from the board.",
 663 |         "inputSchema": {
 664 |             "type": "object",
 665 |             "properties": {
 666 |                 "traceId": {
 667 |                     "type": "string",
 668 |                     "description": "Identifier of the trace to delete"
 669 |                 }
 670 |             },
 671 |             "required": ["traceId"]
 672 |         }
 673 |     },
 674 |     {
 675 |         "name": "get_nets_list",
 676 |         "title": "List All Nets",
 677 |         "description": "Returns a list of all electrical nets defined on the board.",
 678 |         "inputSchema": {
 679 |             "type": "object",
 680 |             "properties": {}
 681 |         }
 682 |     },
 683 |     {
 684 |         "name": "create_netclass",
 685 |         "title": "Create Net Class",
 686 |         "description": "Defines a net class with specific routing rules (trace width, clearance, etc.).",
 687 |         "inputSchema": {
 688 |             "type": "object",
 689 |             "properties": {
 690 |                 "name": {
 691 |                     "type": "string",
 692 |                     "description": "Net class name",
 693 |                     "minLength": 1
 694 |                 },
 695 |                 "traceWidth": {
 696 |                     "type": "number",
 697 |                     "description": "Default trace width in millimeters",
 698 |                     "minimum": 0.1
 699 |                 },
 700 |                 "clearance": {
 701 |                     "type": "number",
 702 |                     "description": "Clearance in millimeters",
 703 |                     "minimum": 0.1
 704 |                 },
 705 |                 "viaDiameter": {
 706 |                     "type": "number",
 707 |                     "description": "Via diameter in millimeters"
 708 |                 },
 709 |                 "viaDrill": {
 710 |                     "type": "number",
 711 |                     "description": "Via drill diameter in millimeters"
 712 |                 }
 713 |             },
 714 |             "required": ["name", "traceWidth", "clearance"]
 715 |         }
 716 |     },
 717 |     {
 718 |         "name": "add_copper_pour",
 719 |         "title": "Add Copper Pour",
 720 |         "description": "Creates a copper pour/zone (typically for ground or power planes).",
 721 |         "inputSchema": {
 722 |             "type": "object",
 723 |             "properties": {
 724 |                 "netName": {
 725 |                     "type": "string",
 726 |                     "description": "Net to connect this copper pour to (e.g., GND, VCC)"
 727 |                 },
 728 |                 "layer": {
 729 |                     "type": "string",
 730 |                     "description": "Layer for the copper pour (e.g., F.Cu, B.Cu)"
 731 |                 },
 732 |                 "priority": {
 733 |                     "type": "integer",
 734 |                     "description": "Pour priority (higher priorities fill first)",
 735 |                     "minimum": 0,
 736 |                     "default": 0
 737 |                 },
 738 |                 "clearance": {
 739 |                     "type": "number",
 740 |                     "description": "Clearance from other objects in millimeters",
 741 |                     "minimum": 0.1
 742 |                 },
 743 |                 "outline": {
 744 |                     "type": "array",
 745 |                     "description": "Array of [x, y] points defining the pour boundary",
 746 |                     "items": {
 747 |                         "type": "array",
 748 |                         "items": {"type": "number"},
 749 |                         "minItems": 2,
 750 |                         "maxItems": 2
 751 |                     },
 752 |                     "minItems": 3
 753 |                 }
 754 |             },
 755 |             "required": ["netName", "layer", "outline"]
 756 |         }
 757 |     },
 758 |     {
 759 |         "name": "route_differential_pair",
 760 |         "title": "Route Differential Pair",
 761 |         "description": "Routes a differential signal pair with matched lengths and spacing.",
 762 |         "inputSchema": {
 763 |             "type": "object",
 764 |             "properties": {
 765 |                 "positiveName": {
 766 |                     "type": "string",
 767 |                     "description": "Positive signal net name"
 768 |                 },
 769 |                 "negativeName": {
 770 |                     "type": "string",
 771 |                     "description": "Negative signal net name"
 772 |                 },
 773 |                 "layer": {
 774 |                     "type": "string",
 775 |                     "description": "Layer to route on"
 776 |                 },
 777 |                 "width": {
 778 |                     "type": "number",
 779 |                     "description": "Trace width in millimeters"
 780 |                 },
 781 |                 "gap": {
 782 |                     "type": "number",
 783 |                     "description": "Gap between traces in millimeters"
 784 |                 },
 785 |                 "points": {
 786 |                     "type": "array",
 787 |                     "description": "Waypoints for the pair routing",
 788 |                     "items": {
 789 |                         "type": "array",
 790 |                         "items": {"type": "number"},
 791 |                         "minItems": 2,
 792 |                         "maxItems": 2
 793 |                     },
 794 |                     "minItems": 2
 795 |                 }
 796 |             },
 797 |             "required": ["positiveName", "negativeName", "width", "gap", "points"]
 798 |         }
 799 |     }
 800 | ]
 801 | 
 802 | # =============================================================================
 803 | # LIBRARY TOOLS
 804 | # =============================================================================
 805 | 
 806 | LIBRARY_TOOLS = [
 807 |     {
 808 |         "name": "list_libraries",
 809 |         "title": "List Footprint Libraries",
 810 |         "description": "Lists all available footprint libraries accessible to KiCAD.",
 811 |         "inputSchema": {
 812 |             "type": "object",
 813 |             "properties": {}
 814 |         }
 815 |     },
 816 |     {
 817 |         "name": "search_footprints",
 818 |         "title": "Search Footprints",
 819 |         "description": "Searches for footprints matching a query string across all libraries.",
 820 |         "inputSchema": {
 821 |             "type": "object",
 822 |             "properties": {
 823 |                 "query": {
 824 |                     "type": "string",
 825 |                     "description": "Search query (e.g., '0805', 'SOIC', 'QFP')",
 826 |                     "minLength": 1
 827 |                 },
 828 |                 "library": {
 829 |                     "type": "string",
 830 |                     "description": "Optional library to restrict search to"
 831 |                 }
 832 |             },
 833 |             "required": ["query"]
 834 |         }
 835 |     },
 836 |     {
 837 |         "name": "list_library_footprints",
 838 |         "title": "List Footprints in Library",
 839 |         "description": "Lists all footprints available in a specific library.",
 840 |         "inputSchema": {
 841 |             "type": "object",
 842 |             "properties": {
 843 |                 "library": {
 844 |                     "type": "string",
 845 |                     "description": "Library name (e.g., Resistor_SMD, Connector_PinHeader)",
 846 |                     "minLength": 1
 847 |                 }
 848 |             },
 849 |             "required": ["library"]
 850 |         }
 851 |     },
 852 |     {
 853 |         "name": "get_footprint_info",
 854 |         "title": "Get Footprint Details",
 855 |         "description": "Retrieves detailed information about a specific footprint including pad layout, dimensions, and description.",
 856 |         "inputSchema": {
 857 |             "type": "object",
 858 |             "properties": {
 859 |                 "library": {
 860 |                     "type": "string",
 861 |                     "description": "Library name"
 862 |                 },
 863 |                 "footprint": {
 864 |                     "type": "string",
 865 |                     "description": "Footprint name"
 866 |                 }
 867 |             },
 868 |             "required": ["library", "footprint"]
 869 |         }
 870 |     }
 871 | ]
 872 | 
 873 | # =============================================================================
 874 | # DESIGN RULE TOOLS
 875 | # =============================================================================
 876 | 
 877 | DESIGN_RULE_TOOLS = [
 878 |     {
 879 |         "name": "set_design_rules",
 880 |         "title": "Set Design Rules",
 881 |         "description": "Configures board design rules including clearances, trace widths, and via sizes.",
 882 |         "inputSchema": {
 883 |             "type": "object",
 884 |             "properties": {
 885 |                 "clearance": {
 886 |                     "type": "number",
 887 |                     "description": "Minimum clearance between copper in millimeters",
 888 |                     "minimum": 0.1
 889 |                 },
 890 |                 "trackWidth": {
 891 |                     "type": "number",
 892 |                     "description": "Minimum track width in millimeters",
 893 |                     "minimum": 0.1
 894 |                 },
 895 |                 "viaDiameter": {
 896 |                     "type": "number",
 897 |                     "description": "Minimum via diameter in millimeters"
 898 |                 },
 899 |                 "viaDrill": {
 900 |                     "type": "number",
 901 |                     "description": "Minimum via drill diameter in millimeters"
 902 |                 },
 903 |                 "microViaD iameter": {
 904 |                     "type": "number",
 905 |                     "description": "Minimum micro-via diameter in millimeters"
 906 |                 }
 907 |             }
 908 |         }
 909 |     },
 910 |     {
 911 |         "name": "get_design_rules",
 912 |         "title": "Get Current Design Rules",
 913 |         "description": "Retrieves the currently configured design rules from the board.",
 914 |         "inputSchema": {
 915 |             "type": "object",
 916 |             "properties": {}
 917 |         }
 918 |     },
 919 |     {
 920 |         "name": "run_drc",
 921 |         "title": "Run Design Rule Check",
 922 |         "description": "Executes a design rule check (DRC) on the current board and reports violations.",
 923 |         "inputSchema": {
 924 |             "type": "object",
 925 |             "properties": {
 926 |                 "includeWarnings": {
 927 |                     "type": "boolean",
 928 |                     "description": "Include warnings in addition to errors",
 929 |                     "default": True
 930 |                 }
 931 |             }
 932 |         }
 933 |     },
 934 |     {
 935 |         "name": "get_drc_violations",
 936 |         "title": "Get DRC Violations",
 937 |         "description": "Returns a list of design rule violations from the most recent DRC run.",
 938 |         "inputSchema": {
 939 |             "type": "object",
 940 |             "properties": {}
 941 |         }
 942 |     }
 943 | ]
 944 | 
 945 | # =============================================================================
 946 | # EXPORT TOOLS
 947 | # =============================================================================
 948 | 
 949 | EXPORT_TOOLS = [
 950 |     {
 951 |         "name": "export_gerber",
 952 |         "title": "Export Gerber Files",
 953 |         "description": "Generates Gerber files for PCB fabrication (industry standard format).",
 954 |         "inputSchema": {
 955 |             "type": "object",
 956 |             "properties": {
 957 |                 "outputPath": {
 958 |                     "type": "string",
 959 |                     "description": "Directory path for output files"
 960 |                 },
 961 |                 "layers": {
 962 |                     "type": "array",
 963 |                     "description": "List of layers to export (if not provided, exports all copper and mask layers)",
 964 |                     "items": {"type": "string"}
 965 |                 },
 966 |                 "includeDrillFiles": {
 967 |                     "type": "boolean",
 968 |                     "description": "Include drill files (Excellon format)",
 969 |                     "default": True
 970 |                 }
 971 |             },
 972 |             "required": ["outputPath"]
 973 |         }
 974 |     },
 975 |     {
 976 |         "name": "export_pdf",
 977 |         "title": "Export PDF",
 978 |         "description": "Exports the board layout as a PDF document for documentation or review.",
 979 |         "inputSchema": {
 980 |             "type": "object",
 981 |             "properties": {
 982 |                 "outputPath": {
 983 |                     "type": "string",
 984 |                     "description": "Path for output PDF file"
 985 |                 },
 986 |                 "layers": {
 987 |                     "type": "array",
 988 |                     "description": "Layers to include in PDF",
 989 |                     "items": {"type": "string"}
 990 |                 },
 991 |                 "colorMode": {
 992 |                     "type": "string",
 993 |                     "enum": ["color", "black_white"],
 994 |                     "description": "Color mode for output",
 995 |                     "default": "color"
 996 |                 }
 997 |             },
 998 |             "required": ["outputPath"]
 999 |         }
1000 |     },
1001 |     {
1002 |         "name": "export_svg",
1003 |         "title": "Export SVG",
1004 |         "description": "Exports the board as Scalable Vector Graphics for documentation or web display.",
1005 |         "inputSchema": {
1006 |             "type": "object",
1007 |             "properties": {
1008 |                 "outputPath": {
1009 |                     "type": "string",
1010 |                     "description": "Path for output SVG file"
1011 |                 },
1012 |                 "layers": {
1013 |                     "type": "array",
1014 |                     "description": "Layers to include in SVG",
1015 |                     "items": {"type": "string"}
1016 |                 }
1017 |             },
1018 |             "required": ["outputPath"]
1019 |         }
1020 |     },
1021 |     {
1022 |         "name": "export_3d",
1023 |         "title": "Export 3D Model",
1024 |         "description": "Exports a 3D model of the board in STEP or VRML format for mechanical CAD integration.",
1025 |         "inputSchema": {
1026 |             "type": "object",
1027 |             "properties": {
1028 |                 "outputPath": {
1029 |                     "type": "string",
1030 |                     "description": "Path for output 3D file"
1031 |                 },
1032 |                 "format": {
1033 |                     "type": "string",
1034 |                     "enum": ["step", "vrml"],
1035 |                     "description": "3D model format",
1036 |                     "default": "step"
1037 |                 },
1038 |                 "includeComponents": {
1039 |                     "type": "boolean",
1040 |                     "description": "Include 3D component models",
1041 |                     "default": True
1042 |                 }
1043 |             },
1044 |             "required": ["outputPath"]
1045 |         }
1046 |     },
1047 |     {
1048 |         "name": "export_bom",
1049 |         "title": "Export Bill of Materials",
1050 |         "description": "Generates a bill of materials (BOM) listing all components with references, values, and footprints.",
1051 |         "inputSchema": {
1052 |             "type": "object",
1053 |             "properties": {
1054 |                 "outputPath": {
1055 |                     "type": "string",
1056 |                     "description": "Path for output BOM file"
1057 |                 },
1058 |                 "format": {
1059 |                     "type": "string",
1060 |                     "enum": ["csv", "xml", "html"],
1061 |                     "description": "BOM output format",
1062 |                     "default": "csv"
1063 |                 },
1064 |                 "groupByValue": {
1065 |                     "type": "boolean",
1066 |                     "description": "Group components with same value together",
1067 |                     "default": True
1068 |                 }
1069 |             },
1070 |             "required": ["outputPath"]
1071 |         }
1072 |     }
1073 | ]
1074 | 
1075 | # =============================================================================
1076 | # SCHEMATIC TOOLS
1077 | # =============================================================================
1078 | 
1079 | SCHEMATIC_TOOLS = [
1080 |     {
1081 |         "name": "create_schematic",
1082 |         "title": "Create New Schematic",
1083 |         "description": "Creates a new KiCAD schematic file for circuit design.",
1084 |         "inputSchema": {
1085 |             "type": "object",
1086 |             "properties": {
1087 |                 "filename": {
1088 |                     "type": "string",
1089 |                     "description": "Path for the new schematic file (.kicad_sch)"
1090 |                 },
1091 |                 "title": {
1092 |                     "type": "string",
1093 |                     "description": "Schematic title"
1094 |                 }
1095 |             },
1096 |             "required": ["filename"]
1097 |         }
1098 |     },
1099 |     {
1100 |         "name": "load_schematic",
1101 |         "title": "Load Existing Schematic",
1102 |         "description": "Opens an existing KiCAD schematic file for editing.",
1103 |         "inputSchema": {
1104 |             "type": "object",
1105 |             "properties": {
1106 |                 "filename": {
1107 |                     "type": "string",
1108 |                     "description": "Path to schematic file (.kicad_sch)"
1109 |                 }
1110 |             },
1111 |             "required": ["filename"]
1112 |         }
1113 |     },
1114 |     {
1115 |         "name": "add_schematic_component",
1116 |         "title": "Add Component to Schematic",
1117 |         "description": "Places a symbol (resistor, capacitor, IC, etc.) on the schematic.",
1118 |         "inputSchema": {
1119 |             "type": "object",
1120 |             "properties": {
1121 |                 "reference": {
1122 |                     "type": "string",
1123 |                     "description": "Reference designator (e.g., R1, C2, U3)"
1124 |                 },
1125 |                 "symbol": {
1126 |                     "type": "string",
1127 |                     "description": "Symbol library:name (e.g., Device:R, Device:C)"
1128 |                 },
1129 |                 "value": {
1130 |                     "type": "string",
1131 |                     "description": "Component value (e.g., 10k, 0.1uF)"
1132 |                 },
1133 |                 "x": {
1134 |                     "type": "number",
1135 |                     "description": "X coordinate on schematic"
1136 |                 },
1137 |                 "y": {
1138 |                     "type": "number",
1139 |                     "description": "Y coordinate on schematic"
1140 |                 }
1141 |             },
1142 |             "required": ["reference", "symbol", "x", "y"]
1143 |         }
1144 |     },
1145 |     {
1146 |         "name": "add_schematic_wire",
1147 |         "title": "Connect Components",
1148 |         "description": "Draws a wire connection between component pins on the schematic.",
1149 |         "inputSchema": {
1150 |             "type": "object",
1151 |             "properties": {
1152 |                 "points": {
1153 |                     "type": "array",
1154 |                     "description": "Array of [x, y] waypoints for the wire",
1155 |                     "items": {
1156 |                         "type": "array",
1157 |                         "items": {"type": "number"},
1158 |                         "minItems": 2,
1159 |                         "maxItems": 2
1160 |                     },
1161 |                     "minItems": 2
1162 |                 }
1163 |             },
1164 |             "required": ["points"]
1165 |         }
1166 |     },
1167 |     {
1168 |         "name": "list_schematic_libraries",
1169 |         "title": "List Symbol Libraries",
1170 |         "description": "Lists all available symbol libraries for schematic design.",
1171 |         "inputSchema": {
1172 |             "type": "object",
1173 |             "properties": {
1174 |                 "searchPaths": {
1175 |                     "type": "array",
1176 |                     "description": "Optional additional paths to search for libraries",
1177 |                     "items": {"type": "string"}
1178 |                 }
1179 |             }
1180 |         }
1181 |     },
1182 |     {
1183 |         "name": "export_schematic_pdf",
1184 |         "title": "Export Schematic to PDF",
1185 |         "description": "Exports the schematic as a PDF document for printing or documentation.",
1186 |         "inputSchema": {
1187 |             "type": "object",
1188 |             "properties": {
1189 |                 "schematicPath": {
1190 |                     "type": "string",
1191 |                     "description": "Path to schematic file"
1192 |                 },
1193 |                 "outputPath": {
1194 |                     "type": "string",
1195 |                     "description": "Path for output PDF"
1196 |                 }
1197 |             },
1198 |             "required": ["schematicPath", "outputPath"]
1199 |         }
1200 |     }
1201 | ]
1202 | 
1203 | # =============================================================================
1204 | # UI/PROCESS TOOLS
1205 | # =============================================================================
1206 | 
1207 | UI_TOOLS = [
1208 |     {
1209 |         "name": "check_kicad_ui",
1210 |         "title": "Check KiCAD UI Status",
1211 |         "description": "Checks if KiCAD user interface is currently running and returns process information.",
1212 |         "inputSchema": {
1213 |             "type": "object",
1214 |             "properties": {}
1215 |         }
1216 |     },
1217 |     {
1218 |         "name": "launch_kicad_ui",
1219 |         "title": "Launch KiCAD Application",
1220 |         "description": "Opens the KiCAD graphical user interface, optionally with a specific project loaded.",
1221 |         "inputSchema": {
1222 |             "type": "object",
1223 |             "properties": {
1224 |                 "projectPath": {
1225 |                     "type": "string",
1226 |                     "description": "Optional path to project file to open in UI"
1227 |                 },
1228 |                 "autoLaunch": {
1229 |                     "type": "boolean",
1230 |                     "description": "Whether to automatically launch if not running",
1231 |                     "default": True
1232 |                 }
1233 |             }
1234 |         }
1235 |     }
1236 | ]
1237 | 
1238 | # =============================================================================
1239 | # COMBINED TOOL SCHEMAS
1240 | # =============================================================================
1241 | 
1242 | TOOL_SCHEMAS: Dict[str, Any] = {}
1243 | 
1244 | # Combine all tool categories
1245 | for tool in (PROJECT_TOOLS + BOARD_TOOLS + COMPONENT_TOOLS + ROUTING_TOOLS +
1246 |              LIBRARY_TOOLS + DESIGN_RULE_TOOLS + EXPORT_TOOLS +
1247 |              SCHEMATIC_TOOLS + UI_TOOLS):
1248 |     TOOL_SCHEMAS[tool["name"]] = tool
1249 | 
1250 | # Total: 46 tools with comprehensive schemas
1251 | 
```
Page 5/6FirstPrevNextLast