kicad-sch-api 0.4.1__py3-none-any.whl → 0.5.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. kicad_sch_api/__init__.py +67 -2
  2. kicad_sch_api/cli/kicad_to_python.py +169 -0
  3. kicad_sch_api/collections/__init__.py +23 -8
  4. kicad_sch_api/collections/base.py +369 -59
  5. kicad_sch_api/collections/components.py +1376 -187
  6. kicad_sch_api/collections/junctions.py +129 -289
  7. kicad_sch_api/collections/labels.py +391 -287
  8. kicad_sch_api/collections/wires.py +202 -316
  9. kicad_sch_api/core/__init__.py +37 -2
  10. kicad_sch_api/core/component_bounds.py +34 -12
  11. kicad_sch_api/core/components.py +146 -7
  12. kicad_sch_api/core/config.py +25 -12
  13. kicad_sch_api/core/connectivity.py +692 -0
  14. kicad_sch_api/core/exceptions.py +175 -0
  15. kicad_sch_api/core/factories/element_factory.py +3 -1
  16. kicad_sch_api/core/formatter.py +24 -7
  17. kicad_sch_api/core/geometry.py +94 -5
  18. kicad_sch_api/core/managers/__init__.py +4 -0
  19. kicad_sch_api/core/managers/base.py +76 -0
  20. kicad_sch_api/core/managers/file_io.py +3 -1
  21. kicad_sch_api/core/managers/format_sync.py +3 -2
  22. kicad_sch_api/core/managers/graphics.py +3 -2
  23. kicad_sch_api/core/managers/hierarchy.py +661 -0
  24. kicad_sch_api/core/managers/metadata.py +4 -2
  25. kicad_sch_api/core/managers/sheet.py +52 -14
  26. kicad_sch_api/core/managers/text_elements.py +3 -2
  27. kicad_sch_api/core/managers/validation.py +3 -2
  28. kicad_sch_api/core/managers/wire.py +112 -54
  29. kicad_sch_api/core/parsing_utils.py +63 -0
  30. kicad_sch_api/core/pin_utils.py +103 -9
  31. kicad_sch_api/core/schematic.py +343 -29
  32. kicad_sch_api/core/types.py +79 -7
  33. kicad_sch_api/exporters/__init__.py +10 -0
  34. kicad_sch_api/exporters/python_generator.py +610 -0
  35. kicad_sch_api/exporters/templates/default.py.jinja2 +65 -0
  36. kicad_sch_api/geometry/__init__.py +15 -3
  37. kicad_sch_api/geometry/routing.py +211 -0
  38. kicad_sch_api/parsers/elements/label_parser.py +30 -8
  39. kicad_sch_api/parsers/elements/symbol_parser.py +255 -83
  40. kicad_sch_api/utils/logging.py +555 -0
  41. kicad_sch_api/utils/logging_decorators.py +587 -0
  42. kicad_sch_api/utils/validation.py +16 -22
  43. kicad_sch_api/wrappers/__init__.py +14 -0
  44. kicad_sch_api/wrappers/base.py +89 -0
  45. kicad_sch_api/wrappers/wire.py +198 -0
  46. kicad_sch_api-0.5.1.dist-info/METADATA +540 -0
  47. kicad_sch_api-0.5.1.dist-info/RECORD +114 -0
  48. kicad_sch_api-0.5.1.dist-info/entry_points.txt +4 -0
  49. {kicad_sch_api-0.4.1.dist-info → kicad_sch_api-0.5.1.dist-info}/top_level.txt +1 -0
  50. mcp_server/__init__.py +34 -0
  51. mcp_server/example_logging_integration.py +506 -0
  52. mcp_server/models.py +252 -0
  53. mcp_server/server.py +357 -0
  54. mcp_server/tools/__init__.py +32 -0
  55. mcp_server/tools/component_tools.py +516 -0
  56. mcp_server/tools/connectivity_tools.py +532 -0
  57. mcp_server/tools/consolidated_tools.py +1216 -0
  58. mcp_server/tools/pin_discovery.py +333 -0
  59. mcp_server/utils/__init__.py +38 -0
  60. mcp_server/utils/logging.py +127 -0
  61. mcp_server/utils.py +36 -0
  62. kicad_sch_api-0.4.1.dist-info/METADATA +0 -491
  63. kicad_sch_api-0.4.1.dist-info/RECORD +0 -87
  64. kicad_sch_api-0.4.1.dist-info/entry_points.txt +0 -2
  65. {kicad_sch_api-0.4.1.dist-info → kicad_sch_api-0.5.1.dist-info}/WHEEL +0 -0
  66. {kicad_sch_api-0.4.1.dist-info → kicad_sch_api-0.5.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,532 @@
1
+ """
2
+ MCP tools for schematic connectivity.
3
+
4
+ Provides MCP-compatible tools for adding wires, labels, junctions, and managing
5
+ circuit connections in KiCAD schematics.
6
+ """
7
+
8
+ import logging
9
+ from typing import TYPE_CHECKING, Optional, Tuple, List
10
+
11
+ if TYPE_CHECKING:
12
+ from fastmcp import Context
13
+ else:
14
+ try:
15
+ from fastmcp import Context
16
+ except ImportError:
17
+ Context = None # type: ignore
18
+
19
+ import kicad_sch_api as ksa
20
+ from kicad_sch_api.core.types import WireType
21
+ from kicad_sch_api.geometry import create_orthogonal_routing, CornerDirection
22
+ from mcp_server.models import PointModel
23
+
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ # Import global schematic state from pin_discovery
29
+ from mcp_server.tools.pin_discovery import get_current_schematic
30
+
31
+
32
+ async def add_wire(
33
+ start: Tuple[float, float],
34
+ end: Tuple[float, float],
35
+ ctx: Optional[Context] = None,
36
+ ) -> dict:
37
+ """
38
+ Add a wire between two points.
39
+
40
+ Creates a wire connection between start and end points. Wires are used to
41
+ connect component pins and establish electrical nets.
42
+
43
+ Args:
44
+ start: Start point as (x, y) tuple in mm
45
+ end: End point as (x, y) tuple in mm
46
+ ctx: MCP context for progress reporting (optional)
47
+
48
+ Returns:
49
+ Dictionary with success status and wire information
50
+
51
+ Examples:
52
+ >>> # Connect two points horizontally
53
+ >>> result = await add_wire(
54
+ ... start=(100.0, 100.0),
55
+ ... end=(150.0, 100.0)
56
+ ... )
57
+
58
+ >>> # Vertical wire
59
+ >>> result = await add_wire(
60
+ ... start=(100.0, 100.0),
61
+ ... end=(100.0, 150.0)
62
+ ... )
63
+ """
64
+ logger.info(f"[MCP] add_wire called: start={start}, end={end}")
65
+
66
+ if ctx:
67
+ await ctx.report_progress(0, 100, "Adding wire")
68
+
69
+ # Check if schematic is loaded
70
+ schematic = get_current_schematic()
71
+ if schematic is None:
72
+ logger.error("[MCP] No schematic loaded")
73
+ return {
74
+ "success": False,
75
+ "error": "NO_SCHEMATIC_LOADED",
76
+ "message": "No schematic is currently loaded",
77
+ }
78
+
79
+ try:
80
+ if ctx:
81
+ await ctx.report_progress(50, 100, "Creating wire connection")
82
+
83
+ # Add wire using library API
84
+ wire_uuid = schematic.wires.add(
85
+ start=start,
86
+ end=end,
87
+ wire_type=WireType.WIRE,
88
+ )
89
+
90
+ if ctx:
91
+ await ctx.report_progress(100, 100, "Complete: wire added")
92
+
93
+ logger.info(f"[MCP] Successfully added wire {wire_uuid}")
94
+ return {
95
+ "success": True,
96
+ "uuid": wire_uuid,
97
+ "start": {"x": start[0], "y": start[1]},
98
+ "end": {"x": end[0], "y": end[1]},
99
+ "message": f"Added wire from ({start[0]}, {start[1]}) to ({end[0]}, {end[1]})",
100
+ }
101
+
102
+ except Exception as e:
103
+ logger.error(f"[MCP] Unexpected error: {e}", exc_info=True)
104
+ return {
105
+ "success": False,
106
+ "error": "INTERNAL_ERROR",
107
+ "message": f"Unexpected error adding wire: {str(e)}",
108
+ }
109
+
110
+
111
+ async def add_label(
112
+ text: str,
113
+ position: Tuple[float, float],
114
+ rotation: float = 0.0,
115
+ size: float = 1.27,
116
+ ctx: Optional[Context] = None,
117
+ ) -> dict:
118
+ """
119
+ Add a net label to the schematic.
120
+
121
+ Net labels are used to name electrical nets and establish connections
122
+ between non-physically connected wires with the same label name.
123
+
124
+ Args:
125
+ text: Label text (net name)
126
+ position: Label position as (x, y) tuple in mm
127
+ rotation: Label rotation in degrees (0, 90, 180, 270), defaults to 0
128
+ size: Text size in mm, defaults to 1.27 (KiCAD standard)
129
+ ctx: MCP context for progress reporting (optional)
130
+
131
+ Returns:
132
+ Dictionary with success status and label information
133
+
134
+ Examples:
135
+ >>> # Add VCC label
136
+ >>> result = await add_label(
137
+ ... text="VCC",
138
+ ... position=(100.0, 100.0)
139
+ ... )
140
+
141
+ >>> # Add label with rotation
142
+ >>> result = await add_label(
143
+ ... text="GND",
144
+ ... position=(150.0, 100.0),
145
+ ... rotation=90.0
146
+ ... )
147
+ """
148
+ logger.info(f"[MCP] add_label called: text={text}, position={position}")
149
+
150
+ if ctx:
151
+ await ctx.report_progress(0, 100, f"Adding label {text}")
152
+
153
+ # Check if schematic is loaded
154
+ schematic = get_current_schematic()
155
+ if schematic is None:
156
+ logger.error("[MCP] No schematic loaded")
157
+ return {
158
+ "success": False,
159
+ "error": "NO_SCHEMATIC_LOADED",
160
+ "message": "No schematic is currently loaded",
161
+ }
162
+
163
+ try:
164
+ if ctx:
165
+ await ctx.report_progress(25, 100, "Validating label parameters")
166
+
167
+ # Validate rotation (KiCAD supports 0, 90, 180, 270)
168
+ if rotation not in [0.0, 90.0, 180.0, 270.0]:
169
+ logger.warning(f"[MCP] Invalid rotation {rotation}")
170
+ return {
171
+ "success": False,
172
+ "error": "VALIDATION_ERROR",
173
+ "message": f"Rotation must be 0, 90, 180, or 270 degrees, got {rotation}",
174
+ }
175
+
176
+ if ctx:
177
+ await ctx.report_progress(50, 100, "Creating label")
178
+
179
+ # Add label using library API
180
+ label = schematic.labels.add(
181
+ text=text,
182
+ position=position,
183
+ rotation=rotation,
184
+ size=size,
185
+ )
186
+
187
+ if ctx:
188
+ await ctx.report_progress(100, 100, f"Complete: label {text} added")
189
+
190
+ logger.info(f"[MCP] Successfully added label {text}")
191
+ return {
192
+ "success": True,
193
+ "uuid": str(label.uuid),
194
+ "text": text,
195
+ "position": {"x": position[0], "y": position[1]},
196
+ "rotation": rotation,
197
+ "size": size,
198
+ "message": f"Added label '{text}' at ({position[0]}, {position[1]})",
199
+ }
200
+
201
+ except Exception as e:
202
+ logger.error(f"[MCP] Unexpected error: {e}", exc_info=True)
203
+ return {
204
+ "success": False,
205
+ "error": "INTERNAL_ERROR",
206
+ "message": f"Unexpected error adding label: {str(e)}",
207
+ }
208
+
209
+
210
+ async def add_junction(
211
+ position: Tuple[float, float],
212
+ diameter: float = 0.0,
213
+ ctx: Optional[Context] = None,
214
+ ) -> dict:
215
+ """
216
+ Add a wire junction at the specified position.
217
+
218
+ Junctions indicate T-connections where three or more wires meet. They are
219
+ required in KiCAD when a wire branches into multiple paths.
220
+
221
+ Args:
222
+ position: Junction position as (x, y) tuple in mm
223
+ diameter: Junction diameter in mm (0 = use KiCAD default)
224
+ ctx: MCP context for progress reporting (optional)
225
+
226
+ Returns:
227
+ Dictionary with success status and junction information
228
+
229
+ Examples:
230
+ >>> # Add junction at T-connection
231
+ >>> result = await add_junction(
232
+ ... position=(100.0, 100.0)
233
+ ... )
234
+
235
+ >>> # Add junction with custom diameter
236
+ >>> result = await add_junction(
237
+ ... position=(150.0, 100.0),
238
+ ... diameter=0.8
239
+ ... )
240
+ """
241
+ logger.info(f"[MCP] add_junction called: position={position}")
242
+
243
+ if ctx:
244
+ await ctx.report_progress(0, 100, "Adding junction")
245
+
246
+ # Check if schematic is loaded
247
+ schematic = get_current_schematic()
248
+ if schematic is None:
249
+ logger.error("[MCP] No schematic loaded")
250
+ return {
251
+ "success": False,
252
+ "error": "NO_SCHEMATIC_LOADED",
253
+ "message": "No schematic is currently loaded",
254
+ }
255
+
256
+ try:
257
+ if ctx:
258
+ await ctx.report_progress(50, 100, "Creating junction")
259
+
260
+ # Add junction using library API
261
+ junction_uuid = schematic.junctions.add(
262
+ position=position,
263
+ diameter=diameter,
264
+ )
265
+
266
+ if ctx:
267
+ await ctx.report_progress(100, 100, "Complete: junction added")
268
+
269
+ logger.info(f"[MCP] Successfully added junction {junction_uuid}")
270
+ return {
271
+ "success": True,
272
+ "uuid": junction_uuid,
273
+ "position": {"x": position[0], "y": position[1]},
274
+ "diameter": diameter,
275
+ "message": f"Added junction at ({position[0]}, {position[1]})",
276
+ }
277
+
278
+ except Exception as e:
279
+ logger.error(f"[MCP] Unexpected error: {e}", exc_info=True)
280
+ return {
281
+ "success": False,
282
+ "error": "INTERNAL_ERROR",
283
+ "message": f"Unexpected error adding junction: {str(e)}",
284
+ }
285
+
286
+
287
+ async def connect_components(
288
+ from_component: str,
289
+ from_pin: str,
290
+ to_component: str,
291
+ to_pin: str,
292
+ corner_direction: str = "auto",
293
+ add_label: Optional[str] = None,
294
+ add_junction: bool = True,
295
+ ctx: Optional[Context] = None,
296
+ ) -> dict:
297
+ """
298
+ Connect two component pins with automatic orthogonal routing.
299
+
300
+ Uses Manhattan-style (orthogonal) routing to create L-shaped or direct wire
301
+ paths between component pins. Automatically calculates pin positions and
302
+ generates appropriate wire segments with optional junctions and labels.
303
+
304
+ Args:
305
+ from_component: Source component reference (e.g., "R1")
306
+ from_pin: Source pin number (e.g., "2")
307
+ to_component: Destination component reference (e.g., "R2")
308
+ to_pin: Destination pin number (e.g., "1")
309
+ corner_direction: Routing direction preference:
310
+ - "auto": Smart heuristic (horizontal if dx >= dy, else vertical)
311
+ - "horizontal_first": Route horizontally then vertically
312
+ - "vertical_first": Route vertically then horizontally
313
+ add_label: Optional net label text to add at start of routing
314
+ add_junction: Whether to add junction at L-shaped corners (default: True)
315
+ ctx: MCP context for progress reporting (optional)
316
+
317
+ Returns:
318
+ Dictionary with success status and connection information including:
319
+ - from/to component and pin details with positions
320
+ - routing type (direct or L-shaped)
321
+ - segments list with start/end positions
322
+ - wire_uuids list
323
+ - junction_uuid (if junction was added)
324
+ - label_uuid (if label was added)
325
+
326
+ Examples:
327
+ >>> # Simple connection with auto routing
328
+ >>> result = await connect_components("R1", "2", "R2", "1")
329
+
330
+ >>> # With label and horizontal-first routing
331
+ >>> result = await connect_components(
332
+ ... "R1", "2", "R2", "1",
333
+ ... corner_direction="horizontal_first",
334
+ ... add_label="VCC"
335
+ ... )
336
+
337
+ >>> # Vertical-first without junction
338
+ >>> result = await connect_components(
339
+ ... "R1", "1", "C1", "1",
340
+ ... corner_direction="vertical_first",
341
+ ... add_junction=False
342
+ ... )
343
+ """
344
+ logger.info(
345
+ f"[MCP] connect_components called: {from_component}:{from_pin} -> "
346
+ f"{to_component}:{to_pin}, direction={corner_direction}"
347
+ )
348
+
349
+ if ctx:
350
+ await ctx.report_progress(0, 100, "Connecting components")
351
+
352
+ # Check if schematic is loaded
353
+ schematic = get_current_schematic()
354
+ if schematic is None:
355
+ logger.error("[MCP] No schematic loaded")
356
+ return {
357
+ "success": False,
358
+ "error": "NO_SCHEMATIC_LOADED",
359
+ "message": "No schematic is currently loaded",
360
+ }
361
+
362
+ try:
363
+ if ctx:
364
+ await ctx.report_progress(10, 100, "Looking up components")
365
+
366
+ # Get components
367
+ try:
368
+ from_comp = schematic.components.get(from_component)
369
+ to_comp = schematic.components.get(to_component)
370
+ except KeyError as e:
371
+ logger.error(f"[MCP] Component not found: {e}")
372
+ return {
373
+ "success": False,
374
+ "error": "COMPONENT_NOT_FOUND",
375
+ "message": f"Component not found: {str(e)}",
376
+ }
377
+
378
+ if ctx:
379
+ await ctx.report_progress(30, 100, "Getting pin positions")
380
+
381
+ # Get pin positions
382
+ from_pins = schematic.components.get_pins_info(from_component)
383
+ to_pins = schematic.components.get_pins_info(to_component)
384
+
385
+ # Check if pin info was successfully retrieved
386
+ if from_pins is None:
387
+ logger.error(f"[MCP] Could not get pins for {from_component}")
388
+ return {
389
+ "success": False,
390
+ "error": "PIN_INFO_ERROR",
391
+ "message": f"Could not get pin information for component {from_component}",
392
+ }
393
+
394
+ if to_pins is None:
395
+ logger.error(f"[MCP] Could not get pins for {to_component}")
396
+ return {
397
+ "success": False,
398
+ "error": "PIN_INFO_ERROR",
399
+ "message": f"Could not get pin information for component {to_component}",
400
+ }
401
+
402
+ from_pin_obj = next((p for p in from_pins if p.number == from_pin), None)
403
+ to_pin_obj = next((p for p in to_pins if p.number == to_pin), None)
404
+
405
+ if not from_pin_obj:
406
+ logger.error(f"[MCP] Pin {from_pin} not found on {from_component}")
407
+ return {
408
+ "success": False,
409
+ "error": "PIN_NOT_FOUND",
410
+ "message": f"Pin {from_pin} not found on component {from_component}",
411
+ }
412
+
413
+ if not to_pin_obj:
414
+ logger.error(f"[MCP] Pin {to_pin} not found on {to_component}")
415
+ return {
416
+ "success": False,
417
+ "error": "PIN_NOT_FOUND",
418
+ "message": f"Pin {to_pin} not found on component {to_component}",
419
+ }
420
+
421
+ if ctx:
422
+ await ctx.report_progress(50, 100, "Calculating routing")
423
+
424
+ # Parse corner direction
425
+ try:
426
+ if corner_direction.upper() == "AUTO":
427
+ direction = CornerDirection.AUTO
428
+ elif corner_direction.upper() == "HORIZONTAL_FIRST":
429
+ direction = CornerDirection.HORIZONTAL_FIRST
430
+ elif corner_direction.upper() == "VERTICAL_FIRST":
431
+ direction = CornerDirection.VERTICAL_FIRST
432
+ else:
433
+ logger.warning(f"[MCP] Invalid corner direction: {corner_direction}, using AUTO")
434
+ direction = CornerDirection.AUTO
435
+ except Exception:
436
+ logger.warning(f"[MCP] Error parsing corner direction, using AUTO")
437
+ direction = CornerDirection.AUTO
438
+
439
+ # Create orthogonal routing
440
+ routing_result = create_orthogonal_routing(
441
+ from_pin_obj.position,
442
+ to_pin_obj.position,
443
+ corner_direction=direction
444
+ )
445
+
446
+ logger.info(
447
+ f"[MCP] Routing calculated: {len(routing_result.segments)} segments, "
448
+ f"direct={routing_result.is_direct}, corner={routing_result.corner}"
449
+ )
450
+
451
+ if ctx:
452
+ await ctx.report_progress(70, 100, "Adding wires")
453
+
454
+ # Add wire segments
455
+ wire_uuids = []
456
+ for start, end in routing_result.segments:
457
+ wire_uuid = schematic.wires.add(start=start, end=end)
458
+ wire_uuids.append(wire_uuid)
459
+
460
+ # Add junction at corner if requested and routing is L-shaped
461
+ junction_uuid = None
462
+ if add_junction and routing_result.corner and not routing_result.is_direct:
463
+ if ctx:
464
+ await ctx.report_progress(80, 100, "Adding junction at corner")
465
+
466
+ junction_uuid = schematic.junctions.add(
467
+ position=(routing_result.corner.x, routing_result.corner.y)
468
+ )
469
+ logger.info(f"[MCP] Added junction at corner: {junction_uuid}")
470
+
471
+ # Add label if requested
472
+ label_uuid = None
473
+ if add_label:
474
+ if ctx:
475
+ await ctx.report_progress(90, 100, f"Adding label {add_label}")
476
+
477
+ label = schematic.labels.add(
478
+ text=add_label,
479
+ position=(from_pin_obj.position.x, from_pin_obj.position.y),
480
+ )
481
+ label_uuid = str(label.uuid)
482
+ logger.info(f"[MCP] Added label '{add_label}': {label_uuid}")
483
+
484
+ if ctx:
485
+ await ctx.report_progress(100, 100, "Complete: components connected")
486
+
487
+ logger.info(
488
+ f"[MCP] Successfully connected {from_component}:{from_pin} to "
489
+ f"{to_component}:{to_pin} with {len(wire_uuids)} wires"
490
+ )
491
+
492
+ return {
493
+ "success": True,
494
+ "from": {
495
+ "component": from_component,
496
+ "pin": from_pin,
497
+ "position": {"x": from_pin_obj.position.x, "y": from_pin_obj.position.y}
498
+ },
499
+ "to": {
500
+ "component": to_component,
501
+ "pin": to_pin,
502
+ "position": {"x": to_pin_obj.position.x, "y": to_pin_obj.position.y}
503
+ },
504
+ "routing": {
505
+ "type": "direct" if routing_result.is_direct else "l_shaped",
506
+ "segments": len(routing_result.segments),
507
+ "corner": {
508
+ "x": routing_result.corner.x,
509
+ "y": routing_result.corner.y
510
+ } if routing_result.corner else None,
511
+ },
512
+ "segments": [
513
+ {
514
+ "start": {"x": s.x, "y": s.y},
515
+ "end": {"x": e.x, "y": e.y}
516
+ }
517
+ for s, e in routing_result.segments
518
+ ],
519
+ "wire_uuids": wire_uuids,
520
+ "junction_uuid": junction_uuid,
521
+ "label_uuid": label_uuid,
522
+ "message": f"Connected {from_component}:{from_pin} to {to_component}:{to_pin} "
523
+ f"with {len(wire_uuids)} wire segment(s)"
524
+ }
525
+
526
+ except Exception as e:
527
+ logger.error(f"[MCP] Unexpected error: {e}", exc_info=True)
528
+ return {
529
+ "success": False,
530
+ "error": "INTERNAL_ERROR",
531
+ "message": f"Unexpected error connecting components: {str(e)}",
532
+ }