kicad-sch-api 0.0.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.

Potentially problematic release.


This version of kicad-sch-api might be problematic. Click here for more details.

@@ -0,0 +1,500 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Python MCP interface for kicad-sch-api.
4
+
5
+ This script provides the Python side of the MCP bridge, handling commands
6
+ from the TypeScript MCP server and executing them using kicad-sch-api.
7
+ """
8
+
9
+ import json
10
+ import logging
11
+ import sys
12
+ import traceback
13
+ from typing import Any, Dict, Optional
14
+
15
+ # Configure logging
16
+ logging.basicConfig(
17
+ level=logging.INFO,
18
+ format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
19
+ handlers=[logging.StreamHandler(sys.stderr)],
20
+ )
21
+
22
+ logger = logging.getLogger("kicad_sch_api.mcp")
23
+
24
+ # Import kicad-sch-api components
25
+ try:
26
+ from ..core.components import Component
27
+ from ..core.schematic import Schematic
28
+ from ..library.cache import get_symbol_cache
29
+ from ..utils.validation import ValidationError, ValidationIssue
30
+ except ImportError as e:
31
+ logger.error(f"Failed to import kicad-sch-api modules: {e}")
32
+ sys.exit(1)
33
+
34
+
35
+ class MCPInterface:
36
+ """MCP command interface for kicad-sch-api."""
37
+
38
+ def __init__(self):
39
+ """Initialize the MCP interface."""
40
+ self.current_schematic: Optional[Schematic] = None
41
+ self.symbol_cache = get_symbol_cache()
42
+
43
+ # Command handlers
44
+ self.handlers = {
45
+ "ping": self.ping,
46
+ "load_schematic": self.load_schematic,
47
+ "save_schematic": self.save_schematic,
48
+ "create_schematic": self.create_schematic,
49
+ "add_component": self.add_component,
50
+ "update_component": self.update_component,
51
+ "remove_component": self.remove_component,
52
+ "get_component": self.get_component,
53
+ "find_components": self.find_components,
54
+ "add_wire": self.add_wire,
55
+ "connect_components": self.connect_components,
56
+ "bulk_update_components": self.bulk_update_components,
57
+ "validate_schematic": self.validate_schematic,
58
+ "get_schematic_summary": self.get_schematic_summary,
59
+ "search_library_symbols": self.search_library_symbols,
60
+ "add_library_path": self.add_library_path,
61
+ }
62
+
63
+ logger.info("MCP interface initialized")
64
+
65
+ def ping(self, params: Dict[str, Any]) -> Dict[str, Any]:
66
+ """Health check command."""
67
+ return {
68
+ "success": True,
69
+ "message": "kicad-sch-api MCP interface is ready",
70
+ "version": "0.0.1",
71
+ }
72
+
73
+ def load_schematic(self, params: Dict[str, Any]) -> Dict[str, Any]:
74
+ """Load a schematic file."""
75
+ try:
76
+ file_path = params.get("file_path")
77
+ if not file_path:
78
+ return {"success": False, "error": "file_path parameter required"}
79
+
80
+ self.current_schematic = Schematic.load(file_path)
81
+ summary = self.current_schematic.get_summary()
82
+
83
+ return {
84
+ "success": True,
85
+ "message": f"Loaded schematic: {file_path}",
86
+ "summary": summary,
87
+ }
88
+ except Exception as e:
89
+ logger.error(f"Error loading schematic: {e}")
90
+ return {"success": False, "error": str(e)}
91
+
92
+ def save_schematic(self, params: Dict[str, Any]) -> Dict[str, Any]:
93
+ """Save the current schematic."""
94
+ try:
95
+ if not self.current_schematic:
96
+ return {"success": False, "error": "No schematic loaded"}
97
+
98
+ file_path = params.get("file_path")
99
+ preserve_format = params.get("preserve_format", True)
100
+
101
+ self.current_schematic.save(file_path, preserve_format)
102
+
103
+ return {
104
+ "success": True,
105
+ "message": f"Saved schematic to: {self.current_schematic.file_path}",
106
+ }
107
+ except Exception as e:
108
+ logger.error(f"Error saving schematic: {e}")
109
+ return {"success": False, "error": str(e)}
110
+
111
+ def create_schematic(self, params: Dict[str, Any]) -> Dict[str, Any]:
112
+ """Create a new schematic."""
113
+ try:
114
+ name = params.get("name", "New Circuit")
115
+ self.current_schematic = Schematic.create(name)
116
+
117
+ return {
118
+ "success": True,
119
+ "message": f"Created new schematic: {name}",
120
+ "summary": self.current_schematic.get_summary(),
121
+ }
122
+ except Exception as e:
123
+ logger.error(f"Error creating schematic: {e}")
124
+ return {"success": False, "error": str(e)}
125
+
126
+ def add_component(self, params: Dict[str, Any]) -> Dict[str, Any]:
127
+ """Add a component to the schematic."""
128
+ try:
129
+ if not self.current_schematic:
130
+ return {"success": False, "error": "No schematic loaded"}
131
+
132
+ lib_id = params.get("lib_id")
133
+ if not lib_id:
134
+ return {"success": False, "error": "lib_id parameter required"}
135
+
136
+ # Extract parameters
137
+ reference = params.get("reference")
138
+ value = params.get("value", "")
139
+ position = params.get("position")
140
+ footprint = params.get("footprint")
141
+ properties = params.get("properties", {})
142
+
143
+ # Convert position if provided
144
+ pos_tuple = None
145
+ if position:
146
+ pos_tuple = (position["x"], position["y"])
147
+
148
+ # Add component
149
+ component = self.current_schematic.components.add(
150
+ lib_id=lib_id,
151
+ reference=reference,
152
+ value=value,
153
+ position=pos_tuple,
154
+ footprint=footprint,
155
+ **properties,
156
+ )
157
+
158
+ return {
159
+ "success": True,
160
+ "message": f"Added component: {component.reference}",
161
+ "component": component.to_dict(),
162
+ }
163
+ except Exception as e:
164
+ logger.error(f"Error adding component: {e}")
165
+ return {"success": False, "error": str(e)}
166
+
167
+ def update_component(self, params: Dict[str, Any]) -> Dict[str, Any]:
168
+ """Update a component's properties."""
169
+ try:
170
+ if not self.current_schematic:
171
+ return {"success": False, "error": "No schematic loaded"}
172
+
173
+ reference = params.get("reference")
174
+ if not reference:
175
+ return {"success": False, "error": "reference parameter required"}
176
+
177
+ component = self.current_schematic.components.get(reference)
178
+ if not component:
179
+ return {"success": False, "error": f"Component not found: {reference}"}
180
+
181
+ # Apply updates
182
+ updates = 0
183
+ if "value" in params:
184
+ component.value = params["value"]
185
+ updates += 1
186
+
187
+ if "position" in params:
188
+ pos = params["position"]
189
+ component.position = (pos["x"], pos["y"])
190
+ updates += 1
191
+
192
+ if "footprint" in params:
193
+ component.footprint = params["footprint"]
194
+ updates += 1
195
+
196
+ if "properties" in params:
197
+ for name, value in params["properties"].items():
198
+ component.set_property(name, value)
199
+ updates += 1
200
+
201
+ return {
202
+ "success": True,
203
+ "message": f"Updated component {reference} ({updates} changes)",
204
+ "component": component.to_dict(),
205
+ }
206
+ except Exception as e:
207
+ logger.error(f"Error updating component: {e}")
208
+ return {"success": False, "error": str(e)}
209
+
210
+ def remove_component(self, params: Dict[str, Any]) -> Dict[str, Any]:
211
+ """Remove a component from the schematic."""
212
+ try:
213
+ if not self.current_schematic:
214
+ return {"success": False, "error": "No schematic loaded"}
215
+
216
+ reference = params.get("reference")
217
+ if not reference:
218
+ return {"success": False, "error": "reference parameter required"}
219
+
220
+ success = self.current_schematic.components.remove(reference)
221
+
222
+ if success:
223
+ return {"success": True, "message": f"Removed component: {reference}"}
224
+ else:
225
+ return {"success": False, "error": f"Component not found: {reference}"}
226
+ except Exception as e:
227
+ logger.error(f"Error removing component: {e}")
228
+ return {"success": False, "error": str(e)}
229
+
230
+ def get_component(self, params: Dict[str, Any]) -> Dict[str, Any]:
231
+ """Get detailed information about a component."""
232
+ try:
233
+ if not self.current_schematic:
234
+ return {"success": False, "error": "No schematic loaded"}
235
+
236
+ reference = params.get("reference")
237
+ if not reference:
238
+ return {"success": False, "error": "reference parameter required"}
239
+
240
+ component = self.current_schematic.components.get(reference)
241
+ if not component:
242
+ return {"success": False, "error": f"Component not found: {reference}"}
243
+
244
+ return {"success": True, "component": component.to_dict()}
245
+ except Exception as e:
246
+ logger.error(f"Error getting component: {e}")
247
+ return {"success": False, "error": str(e)}
248
+
249
+ def find_components(self, params: Dict[str, Any]) -> Dict[str, Any]:
250
+ """Find components by criteria."""
251
+ try:
252
+ if not self.current_schematic:
253
+ return {"success": False, "error": "No schematic loaded"}
254
+
255
+ # Filter out None values and convert to filter criteria
256
+ criteria = {k: v for k, v in params.items() if v is not None}
257
+
258
+ # Special handling for in_area
259
+ if "in_area" in criteria:
260
+ area = criteria["in_area"]
261
+ if len(area) == 4:
262
+ criteria["in_area"] = tuple(area)
263
+
264
+ components = self.current_schematic.components.filter(**criteria)
265
+
266
+ return {
267
+ "success": True,
268
+ "count": len(components),
269
+ "components": [comp.to_dict() for comp in components],
270
+ }
271
+ except Exception as e:
272
+ logger.error(f"Error finding components: {e}")
273
+ return {"success": False, "error": str(e)}
274
+
275
+ def add_wire(self, params: Dict[str, Any]) -> Dict[str, Any]:
276
+ """Add a wire connection."""
277
+ try:
278
+ if not self.current_schematic:
279
+ return {"success": False, "error": "No schematic loaded"}
280
+
281
+ start = params.get("start")
282
+ end = params.get("end")
283
+
284
+ if not start or not end:
285
+ return {"success": False, "error": "start and end parameters required"}
286
+
287
+ wire_uuid = self.current_schematic.add_wire(
288
+ (start["x"], start["y"]), (end["x"], end["y"])
289
+ )
290
+
291
+ return {
292
+ "success": True,
293
+ "message": f"Added wire from {start} to {end}",
294
+ "wire_uuid": wire_uuid,
295
+ }
296
+ except Exception as e:
297
+ logger.error(f"Error adding wire: {e}")
298
+ return {"success": False, "error": str(e)}
299
+
300
+ def connect_components(self, params: Dict[str, Any]) -> Dict[str, Any]:
301
+ """Connect two component pins with a wire."""
302
+ try:
303
+ if not self.current_schematic:
304
+ return {"success": False, "error": "No schematic loaded"}
305
+
306
+ from_comp = params.get("from_component")
307
+ from_pin = params.get("from_pin")
308
+ to_comp = params.get("to_component")
309
+ to_pin = params.get("to_pin")
310
+
311
+ if not all([from_comp, from_pin, to_comp, to_pin]):
312
+ return {"success": False, "error": "All connection parameters required"}
313
+
314
+ # Get component pin positions
315
+ comp1 = self.current_schematic.components.get(from_comp)
316
+ comp2 = self.current_schematic.components.get(to_comp)
317
+
318
+ if not comp1:
319
+ return {"success": False, "error": f"Component not found: {from_comp}"}
320
+ if not comp2:
321
+ return {"success": False, "error": f"Component not found: {to_comp}"}
322
+
323
+ pin1_pos = comp1.get_pin_position(from_pin)
324
+ pin2_pos = comp2.get_pin_position(to_pin)
325
+
326
+ if not pin1_pos or not pin2_pos:
327
+ return {"success": False, "error": "Could not determine pin positions"}
328
+
329
+ # Add wire between pins
330
+ wire_uuid = self.current_schematic.add_wire(pin1_pos, pin2_pos)
331
+
332
+ return {
333
+ "success": True,
334
+ "message": f"Connected {from_comp}.{from_pin} to {to_comp}.{to_pin}",
335
+ "wire_uuid": wire_uuid,
336
+ }
337
+ except Exception as e:
338
+ logger.error(f"Error connecting components: {e}")
339
+ return {"success": False, "error": str(e)}
340
+
341
+ def bulk_update_components(self, params: Dict[str, Any]) -> Dict[str, Any]:
342
+ """Update multiple components matching criteria."""
343
+ try:
344
+ if not self.current_schematic:
345
+ return {"success": False, "error": "No schematic loaded"}
346
+
347
+ criteria = params.get("criteria", {})
348
+ updates = params.get("updates", {})
349
+
350
+ if not criteria or not updates:
351
+ return {"success": False, "error": "criteria and updates parameters required"}
352
+
353
+ count = self.current_schematic.components.bulk_update(criteria, updates)
354
+
355
+ return {"success": True, "message": f"Updated {count} components", "count": count}
356
+ except Exception as e:
357
+ logger.error(f"Error in bulk update: {e}")
358
+ return {"success": False, "error": str(e)}
359
+
360
+ def validate_schematic(self, params: Dict[str, Any]) -> Dict[str, Any]:
361
+ """Validate the current schematic."""
362
+ try:
363
+ if not self.current_schematic:
364
+ return {"success": False, "error": "No schematic loaded"}
365
+
366
+ issues = self.current_schematic.validate()
367
+
368
+ # Categorize issues
369
+ errors = [issue for issue in issues if issue.level.value in ("error", "critical")]
370
+ warnings = [issue for issue in issues if issue.level.value == "warning"]
371
+
372
+ return {
373
+ "success": True,
374
+ "valid": len(errors) == 0,
375
+ "issue_count": len(issues),
376
+ "errors": [str(issue) for issue in errors],
377
+ "warnings": [str(issue) for issue in warnings],
378
+ }
379
+ except Exception as e:
380
+ logger.error(f"Error validating schematic: {e}")
381
+ return {"success": False, "error": str(e)}
382
+
383
+ def get_schematic_summary(self, params: Dict[str, Any]) -> Dict[str, Any]:
384
+ """Get summary of current schematic."""
385
+ try:
386
+ if not self.current_schematic:
387
+ return {"success": False, "error": "No schematic loaded"}
388
+
389
+ summary = self.current_schematic.get_summary()
390
+
391
+ return {"success": True, "summary": summary}
392
+ except Exception as e:
393
+ logger.error(f"Error getting summary: {e}")
394
+ return {"success": False, "error": str(e)}
395
+
396
+ def search_library_symbols(self, params: Dict[str, Any]) -> Dict[str, Any]:
397
+ """Search for symbols in libraries."""
398
+ try:
399
+ query = params.get("query")
400
+ if not query:
401
+ return {"success": False, "error": "query parameter required"}
402
+
403
+ library = params.get("library")
404
+ limit = params.get("limit", 20)
405
+
406
+ symbols = self.symbol_cache.search_symbols(query, library, limit)
407
+
408
+ symbol_results = []
409
+ for symbol in symbols:
410
+ symbol_results.append(
411
+ {
412
+ "lib_id": symbol.lib_id,
413
+ "name": symbol.name,
414
+ "library": symbol.library,
415
+ "description": symbol.description,
416
+ "reference_prefix": symbol.reference_prefix,
417
+ "pin_count": len(symbol.pins),
418
+ }
419
+ )
420
+
421
+ return {"success": True, "count": len(symbol_results), "symbols": symbol_results}
422
+ except Exception as e:
423
+ logger.error(f"Error searching symbols: {e}")
424
+ return {"success": False, "error": str(e)}
425
+
426
+ def add_library_path(self, params: Dict[str, Any]) -> Dict[str, Any]:
427
+ """Add a custom library path."""
428
+ try:
429
+ library_path = params.get("library_path")
430
+ if not library_path:
431
+ return {"success": False, "error": "library_path parameter required"}
432
+
433
+ success = self.symbol_cache.add_library_path(library_path)
434
+
435
+ if success:
436
+ return {"success": True, "message": f"Added library: {library_path}"}
437
+ else:
438
+ return {"success": False, "error": f"Failed to add library: {library_path}"}
439
+ except Exception as e:
440
+ logger.error(f"Error adding library: {e}")
441
+ return {"success": False, "error": str(e)}
442
+
443
+ def process_commands(self):
444
+ """Main command processing loop."""
445
+ logger.info("Starting command processing loop")
446
+
447
+ try:
448
+ for line in sys.stdin:
449
+ try:
450
+ # Parse command
451
+ request = json.loads(line.strip())
452
+ command = request.get("command")
453
+ params = request.get("params", {})
454
+ request_id = request.get("id")
455
+
456
+ # Execute command
457
+ if command in self.handlers:
458
+ result = self.handlers[command](params)
459
+ else:
460
+ result = {"success": False, "error": f"Unknown command: {command}"}
461
+
462
+ # Send response
463
+ response = {"id": request_id, "result": result}
464
+
465
+ print(json.dumps(response))
466
+ sys.stdout.flush()
467
+
468
+ except json.JSONDecodeError as e:
469
+ logger.error(f"Invalid JSON input: {e}")
470
+ error_response = {"id": None, "error": f"Invalid JSON: {e}"}
471
+ print(json.dumps(error_response))
472
+ sys.stdout.flush()
473
+
474
+ except Exception as e:
475
+ logger.error(f"Error processing command: {e}")
476
+ logger.debug(traceback.format_exc())
477
+ error_response = {
478
+ "id": request.get("id") if "request" in locals() else None,
479
+ "error": str(e),
480
+ }
481
+ print(json.dumps(error_response))
482
+ sys.stdout.flush()
483
+
484
+ except KeyboardInterrupt:
485
+ logger.info("Received interrupt signal")
486
+ except Exception as e:
487
+ logger.error(f"Fatal error in command processing: {e}")
488
+ logger.debug(traceback.format_exc())
489
+ finally:
490
+ logger.info("Command processing stopped")
491
+
492
+
493
+ def main():
494
+ """Main entry point."""
495
+ interface = MCPInterface()
496
+ interface.process_commands()
497
+
498
+
499
+ if __name__ == "__main__":
500
+ main()
kicad_sch_api/py.typed ADDED
@@ -0,0 +1 @@
1
+ # Marker file for PEP 561 - indicates this package supports type hints
@@ -0,0 +1,15 @@
1
+ """Utilities for kicad-sch-api."""
2
+
3
+ from .validation import (
4
+ SchematicValidator,
5
+ ValidationError,
6
+ ValidationIssue,
7
+ validate_schematic_file,
8
+ )
9
+
10
+ __all__ = [
11
+ "ValidationError",
12
+ "ValidationIssue",
13
+ "SchematicValidator",
14
+ "validate_schematic_file",
15
+ ]