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,516 @@
1
+ """
2
+ MCP tools for component management.
3
+
4
+ Provides MCP-compatible tools for adding, updating, listing, filtering, and
5
+ removing components in KiCAD schematics.
6
+ """
7
+
8
+ import logging
9
+ from typing import TYPE_CHECKING, List, Optional, Tuple, Dict, Any
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.exceptions import LibraryError, ValidationError
21
+ from mcp_server.models import ComponentInfoOutput, ErrorOutput, PointModel
22
+
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ # Import global schematic state from pin_discovery
28
+ from mcp_server.tools.pin_discovery import get_current_schematic
29
+
30
+
31
+ def _component_to_output(component: Any) -> ComponentInfoOutput:
32
+ """Convert a Component to ComponentInfoOutput."""
33
+ return ComponentInfoOutput(
34
+ reference=component.reference,
35
+ lib_id=component.lib_id,
36
+ value=component.value,
37
+ position=PointModel(x=component.position.x, y=component.position.y),
38
+ rotation=component.rotation,
39
+ footprint=component.footprint,
40
+ uuid=str(component.uuid),
41
+ success=True,
42
+ )
43
+
44
+
45
+ async def add_component(
46
+ lib_id: str,
47
+ value: str,
48
+ reference: Optional[str] = None,
49
+ position: Optional[Tuple[float, float]] = None,
50
+ rotation: float = 0.0,
51
+ footprint: Optional[str] = None,
52
+ ctx: Optional[Context] = None,
53
+ ) -> ComponentInfoOutput | ErrorOutput:
54
+ """
55
+ Add a component to the current schematic.
56
+
57
+ Creates a new component with the specified library ID, value, and optional
58
+ parameters. If no reference is provided, one will be auto-generated based on
59
+ the component type (e.g., R1, C1, U1). If no position is provided, the
60
+ component will be auto-placed.
61
+
62
+ Args:
63
+ lib_id: Library identifier (e.g., "Device:R", "Amplifier_Operational:TL072")
64
+ value: Component value or part description (e.g., "10k", "100nF", "TL072")
65
+ reference: Component reference designator (e.g., "R1", "U2") - auto-generated if None
66
+ position: Component position as (x, y) tuple in mm - auto-placed if None
67
+ rotation: Component rotation in degrees (0, 90, 180, or 270), defaults to 0
68
+ footprint: PCB footprint identifier (e.g., "Resistor_SMD:R_0603_1608Metric")
69
+ ctx: MCP context for progress reporting (optional)
70
+
71
+ Returns:
72
+ ComponentInfoOutput with component information, or ErrorOutput on failure
73
+
74
+ Examples:
75
+ >>> # Add a resistor with auto-generated reference
76
+ >>> result = await add_component(
77
+ ... lib_id="Device:R",
78
+ ... value="10k",
79
+ ... position=(100.0, 100.0)
80
+ ... )
81
+ >>> print(f"Added {result.reference}")
82
+
83
+ >>> # Add a capacitor with specific reference and footprint
84
+ >>> result = await add_component(
85
+ ... lib_id="Device:C",
86
+ ... value="100nF",
87
+ ... reference="C1",
88
+ ... position=(120.0, 100.0),
89
+ ... footprint="Capacitor_SMD:C_0603_1608Metric"
90
+ ... )
91
+ """
92
+ logger.info(
93
+ f"[MCP] add_component called: lib_id={lib_id}, value={value}, "
94
+ f"reference={reference}, position={position}"
95
+ )
96
+
97
+ # Report progress if context available
98
+ if ctx:
99
+ await ctx.report_progress(0, 100, f"Adding component {lib_id}")
100
+
101
+ # Check if schematic is loaded
102
+ schematic = get_current_schematic()
103
+ if schematic is None:
104
+ logger.error("[MCP] No schematic loaded")
105
+ return ErrorOutput(
106
+ error="NO_SCHEMATIC_LOADED",
107
+ message="No schematic is currently loaded. Please load or create a schematic first.",
108
+ )
109
+
110
+ try:
111
+ if ctx:
112
+ await ctx.report_progress(25, 100, f"Validating component parameters")
113
+
114
+ # Validate rotation
115
+ if rotation not in [0.0, 90.0, 180.0, 270.0]:
116
+ logger.warning(f"[MCP] Invalid rotation {rotation}, must be 0, 90, 180, or 270")
117
+ return ErrorOutput(
118
+ error="VALIDATION_ERROR",
119
+ message=f"Rotation must be 0, 90, 180, or 270 degrees, got {rotation}",
120
+ )
121
+
122
+ if ctx:
123
+ await ctx.report_progress(50, 100, f"Adding component to schematic")
124
+
125
+ # Add component using library API
126
+ component = schematic.components.add(
127
+ lib_id=lib_id,
128
+ reference=reference,
129
+ value=value,
130
+ position=position,
131
+ rotation=rotation,
132
+ footprint=footprint,
133
+ )
134
+
135
+ if ctx:
136
+ await ctx.report_progress(75, 100, f"Converting to MCP output format")
137
+
138
+ # Convert to MCP output model
139
+ result = _component_to_output(component)
140
+ result.message = f"Added component {component.reference}"
141
+
142
+ if ctx:
143
+ await ctx.report_progress(100, 100, f"Complete: added {component.reference}")
144
+
145
+ logger.info(f"[MCP] Successfully added component {component.reference}")
146
+ return result
147
+
148
+ except ValidationError as e:
149
+ logger.error(f"[MCP] Validation error: {e}")
150
+ return ErrorOutput(
151
+ error="VALIDATION_ERROR",
152
+ message=f"Component validation failed: {str(e)}",
153
+ )
154
+ except LibraryError as e:
155
+ logger.error(f"[MCP] Library error: {e}")
156
+ return ErrorOutput(
157
+ error="LIBRARY_ERROR",
158
+ message=f"Symbol library error: {str(e)}",
159
+ )
160
+ except Exception as e:
161
+ logger.error(f"[MCP] Unexpected error: {e}", exc_info=True)
162
+ return ErrorOutput(
163
+ error="INTERNAL_ERROR",
164
+ message=f"Unexpected error adding component: {str(e)}",
165
+ )
166
+
167
+
168
+ async def list_components(
169
+ ctx: Optional[Context] = None,
170
+ ) -> dict:
171
+ """
172
+ List all components in the current schematic.
173
+
174
+ Returns all components with their complete metadata including position,
175
+ rotation, footprint, and properties.
176
+
177
+ Args:
178
+ ctx: MCP context for progress reporting (optional)
179
+
180
+ Returns:
181
+ Dictionary with success status and list of components, or error information
182
+
183
+ Examples:
184
+ >>> result = await list_components()
185
+ >>> print(f"Found {result['count']} components")
186
+ >>> for comp in result['components']:
187
+ ... print(f"{comp['reference']}: {comp['value']}")
188
+ """
189
+ logger.info("[MCP] list_components called")
190
+
191
+ if ctx:
192
+ await ctx.report_progress(0, 100, "Listing all components")
193
+
194
+ # Check if schematic is loaded
195
+ schematic = get_current_schematic()
196
+ if schematic is None:
197
+ logger.error("[MCP] No schematic loaded")
198
+ return {
199
+ "success": False,
200
+ "error": "NO_SCHEMATIC_LOADED",
201
+ "message": "No schematic is currently loaded",
202
+ }
203
+
204
+ try:
205
+ if ctx:
206
+ await ctx.report_progress(50, 100, "Retrieving components")
207
+
208
+ # Get all components (iterate directly)
209
+ components = list(schematic.components)
210
+
211
+ if ctx:
212
+ await ctx.report_progress(75, 100, f"Converting {len(components)} components")
213
+
214
+ # Convert to output format
215
+ component_list = []
216
+ for comp in components:
217
+ comp_output = _component_to_output(comp)
218
+ component_list.append(comp_output.model_dump())
219
+
220
+ if ctx:
221
+ await ctx.report_progress(100, 100, f"Complete: {len(components)} components")
222
+
223
+ logger.info(f"[MCP] Listed {len(components)} components")
224
+ return {
225
+ "success": True,
226
+ "count": len(components),
227
+ "components": component_list,
228
+ }
229
+
230
+ except Exception as e:
231
+ logger.error(f"[MCP] Unexpected error: {e}", exc_info=True)
232
+ return {
233
+ "success": False,
234
+ "error": "INTERNAL_ERROR",
235
+ "message": f"Unexpected error listing components: {str(e)}",
236
+ }
237
+
238
+
239
+ async def update_component(
240
+ reference: str,
241
+ value: Optional[str] = None,
242
+ position: Optional[Tuple[float, float]] = None,
243
+ rotation: Optional[float] = None,
244
+ footprint: Optional[str] = None,
245
+ ctx: Optional[Context] = None,
246
+ ) -> ComponentInfoOutput | ErrorOutput:
247
+ """
248
+ Update component properties.
249
+
250
+ Updates one or more properties of an existing component. Only provided
251
+ parameters will be updated.
252
+
253
+ Args:
254
+ reference: Component reference designator to update (e.g., "R1")
255
+ value: New component value (if provided)
256
+ position: New position as (x, y) tuple in mm (if provided)
257
+ rotation: New rotation in degrees (0, 90, 180, or 270) (if provided)
258
+ footprint: New footprint identifier (if provided)
259
+ ctx: MCP context for progress reporting (optional)
260
+
261
+ Returns:
262
+ ComponentInfoOutput with updated component information, or ErrorOutput on failure
263
+
264
+ Examples:
265
+ >>> # Update value only
266
+ >>> result = await update_component("R1", value="20k")
267
+
268
+ >>> # Update multiple properties
269
+ >>> result = await update_component(
270
+ ... "R1",
271
+ ... value="20k",
272
+ ... footprint="Resistor_SMD:R_0805_2012Metric",
273
+ ... rotation=90.0
274
+ ... )
275
+ """
276
+ logger.info(f"[MCP] update_component called for {reference}")
277
+
278
+ if ctx:
279
+ await ctx.report_progress(0, 100, f"Updating component {reference}")
280
+
281
+ # Check if schematic is loaded
282
+ schematic = get_current_schematic()
283
+ if schematic is None:
284
+ logger.error("[MCP] No schematic loaded")
285
+ return ErrorOutput(
286
+ error="NO_SCHEMATIC_LOADED",
287
+ message="No schematic is currently loaded",
288
+ )
289
+
290
+ try:
291
+ if ctx:
292
+ await ctx.report_progress(25, 100, f"Finding component {reference}")
293
+
294
+ # Get component
295
+ component = schematic.components.get(reference)
296
+ if component is None:
297
+ logger.warning(f"[MCP] Component not found: {reference}")
298
+ return ErrorOutput(
299
+ error="COMPONENT_NOT_FOUND",
300
+ message=f"Component '{reference}' not found in schematic",
301
+ )
302
+
303
+ if ctx:
304
+ await ctx.report_progress(50, 100, f"Validating updates")
305
+
306
+ # Validate rotation if provided
307
+ if rotation is not None and rotation not in [0.0, 90.0, 180.0, 270.0]:
308
+ logger.warning(f"[MCP] Invalid rotation {rotation}")
309
+ return ErrorOutput(
310
+ error="VALIDATION_ERROR",
311
+ message=f"Rotation must be 0, 90, 180, or 270 degrees, got {rotation}",
312
+ )
313
+
314
+ if ctx:
315
+ await ctx.report_progress(75, 100, f"Applying updates")
316
+
317
+ # Update provided properties
318
+ if value is not None:
319
+ component.value = value
320
+ if position is not None:
321
+ component.position = position
322
+ if rotation is not None:
323
+ component.rotation = rotation
324
+ if footprint is not None:
325
+ component.footprint = footprint
326
+
327
+ # Convert to output
328
+ result = _component_to_output(component)
329
+ result.message = f"Updated component {reference}"
330
+
331
+ if ctx:
332
+ await ctx.report_progress(100, 100, f"Complete: updated {reference}")
333
+
334
+ logger.info(f"[MCP] Successfully updated component {reference}")
335
+ return result
336
+
337
+ except ValidationError as e:
338
+ logger.error(f"[MCP] Validation error: {e}")
339
+ return ErrorOutput(
340
+ error="VALIDATION_ERROR",
341
+ message=f"Update validation failed: {str(e)}",
342
+ )
343
+ except Exception as e:
344
+ logger.error(f"[MCP] Unexpected error: {e}", exc_info=True)
345
+ return ErrorOutput(
346
+ error="INTERNAL_ERROR",
347
+ message=f"Unexpected error updating component: {str(e)}",
348
+ )
349
+
350
+
351
+ async def remove_component(
352
+ reference: str,
353
+ ctx: Optional[Context] = None,
354
+ ) -> dict:
355
+ """
356
+ Remove a component from the schematic.
357
+
358
+ Removes the component with the specified reference designator. This also
359
+ removes all associated wires and connections.
360
+
361
+ Args:
362
+ reference: Component reference designator to remove (e.g., "R1")
363
+ ctx: MCP context for progress reporting (optional)
364
+
365
+ Returns:
366
+ Dictionary with success status
367
+
368
+ Examples:
369
+ >>> result = await remove_component("R1")
370
+ >>> if result['success']:
371
+ ... print(f"Removed component {result['reference']}")
372
+ """
373
+ logger.info(f"[MCP] remove_component called for {reference}")
374
+
375
+ if ctx:
376
+ await ctx.report_progress(0, 100, f"Removing component {reference}")
377
+
378
+ # Check if schematic is loaded
379
+ schematic = get_current_schematic()
380
+ if schematic is None:
381
+ logger.error("[MCP] No schematic loaded")
382
+ return {
383
+ "success": False,
384
+ "error": "NO_SCHEMATIC_LOADED",
385
+ "message": "No schematic is currently loaded",
386
+ }
387
+
388
+ try:
389
+ if ctx:
390
+ await ctx.report_progress(50, 100, f"Removing {reference}")
391
+
392
+ # Remove component
393
+ removed = schematic.components.remove(reference)
394
+
395
+ if not removed:
396
+ logger.warning(f"[MCP] Component not found: {reference}")
397
+ return {
398
+ "success": False,
399
+ "error": "COMPONENT_NOT_FOUND",
400
+ "message": f"Component '{reference}' not found in schematic",
401
+ }
402
+
403
+ if ctx:
404
+ await ctx.report_progress(100, 100, f"Complete: removed {reference}")
405
+
406
+ logger.info(f"[MCP] Successfully removed component {reference}")
407
+ return {
408
+ "success": True,
409
+ "reference": reference,
410
+ "message": f"Removed component {reference}",
411
+ }
412
+
413
+ except Exception as e:
414
+ logger.error(f"[MCP] Unexpected error: {e}", exc_info=True)
415
+ return {
416
+ "success": False,
417
+ "error": "INTERNAL_ERROR",
418
+ "message": f"Unexpected error removing component: {str(e)}",
419
+ }
420
+
421
+
422
+ async def filter_components(
423
+ lib_id: Optional[str] = None,
424
+ value: Optional[str] = None,
425
+ value_pattern: Optional[str] = None,
426
+ footprint: Optional[str] = None,
427
+ ctx: Optional[Context] = None,
428
+ ) -> dict:
429
+ """
430
+ Filter components by various criteria.
431
+
432
+ Returns components matching the specified filter criteria. All provided
433
+ criteria must match (AND logic).
434
+
435
+ Args:
436
+ lib_id: Filter by library ID (exact match, e.g., "Device:R")
437
+ value: Filter by value (exact match, e.g., "10k")
438
+ value_pattern: Filter by value pattern (substring match, e.g., "10")
439
+ footprint: Filter by footprint (exact match)
440
+ ctx: MCP context for progress reporting (optional)
441
+
442
+ Returns:
443
+ Dictionary with success status and list of matching components
444
+
445
+ Examples:
446
+ >>> # Find all resistors
447
+ >>> result = await filter_components(lib_id="Device:R")
448
+ >>> print(f"Found {result['count']} resistors")
449
+
450
+ >>> # Find all 10k resistors
451
+ >>> result = await filter_components(lib_id="Device:R", value="10k")
452
+
453
+ >>> # Find all components with "100" in value
454
+ >>> result = await filter_components(value_pattern="100")
455
+ """
456
+ logger.info(f"[MCP] filter_components called with criteria: "
457
+ f"lib_id={lib_id}, value={value}, value_pattern={value_pattern}")
458
+
459
+ if ctx:
460
+ await ctx.report_progress(0, 100, "Filtering components")
461
+
462
+ # Check if schematic is loaded
463
+ schematic = get_current_schematic()
464
+ if schematic is None:
465
+ logger.error("[MCP] No schematic loaded")
466
+ return {
467
+ "success": False,
468
+ "error": "NO_SCHEMATIC_LOADED",
469
+ "message": "No schematic is currently loaded",
470
+ }
471
+
472
+ try:
473
+ if ctx:
474
+ await ctx.report_progress(50, 100, "Applying filters")
475
+
476
+ # Build filter criteria
477
+ criteria = {}
478
+ if lib_id is not None:
479
+ criteria["lib_id"] = lib_id
480
+ if value is not None:
481
+ criteria["value"] = value
482
+ if value_pattern is not None:
483
+ criteria["value_pattern"] = value_pattern
484
+ if footprint is not None:
485
+ criteria["footprint"] = footprint
486
+
487
+ # Apply filter
488
+ components = schematic.components.filter(**criteria)
489
+
490
+ if ctx:
491
+ await ctx.report_progress(75, 100, f"Converting {len(components)} components")
492
+
493
+ # Convert to output format
494
+ component_list = []
495
+ for comp in components:
496
+ comp_output = _component_to_output(comp)
497
+ component_list.append(comp_output.model_dump())
498
+
499
+ if ctx:
500
+ await ctx.report_progress(100, 100, f"Complete: {len(components)} matches")
501
+
502
+ logger.info(f"[MCP] Found {len(components)} matching components")
503
+ return {
504
+ "success": True,
505
+ "count": len(components),
506
+ "components": component_list,
507
+ "criteria": criteria,
508
+ }
509
+
510
+ except Exception as e:
511
+ logger.error(f"[MCP] Unexpected error: {e}", exc_info=True)
512
+ return {
513
+ "success": False,
514
+ "error": "INTERNAL_ERROR",
515
+ "message": f"Unexpected error filtering components: {str(e)}",
516
+ }