@hybridlabor-api/bdb-hardware-pcb 0.1.0

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 (192) hide show
  1. package/README.md +116 -0
  2. package/config/mcp/antigravity.json +22 -0
  3. package/config/mcp/claude.json +22 -0
  4. package/config/mcp/codex.toml +25 -0
  5. package/docs/adr/ADR-001-KICAD-OPENSCAD-MCP-STRATEGY.md +334 -0
  6. package/docs/review_documentation.md +121 -0
  7. package/installer.js +358 -0
  8. package/mcp_servers/kicad-mcp-server/.env.example +22 -0
  9. package/mcp_servers/kicad-mcp-server/.github/workflows/ci.yml +36 -0
  10. package/mcp_servers/kicad-mcp-server/CLAUDE.md +487 -0
  11. package/mcp_servers/kicad-mcp-server/README.md +316 -0
  12. package/mcp_servers/kicad-mcp-server/docs/DEVICE_TREE.md +416 -0
  13. package/mcp_servers/kicad-mcp-server/docs/INSTALLATION.md +332 -0
  14. package/mcp_servers/kicad-mcp-server/docs/PIN_ANALYSIS.md +332 -0
  15. package/mcp_servers/kicad-mcp-server/docs/README.md +240 -0
  16. package/mcp_servers/kicad-mcp-server/docs/TESTING.md +613 -0
  17. package/mcp_servers/kicad-mcp-server/docs/VALIDATION.md +268 -0
  18. package/mcp_servers/kicad-mcp-server/pyproject.toml +96 -0
  19. package/mcp_servers/kicad-mcp-server/requirements-dev.txt +16 -0
  20. package/mcp_servers/kicad-mcp-server/requirements-test.txt +24 -0
  21. package/mcp_servers/kicad-mcp-server/requirements.txt +13 -0
  22. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/__init__.py +3 -0
  23. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/__main__.py +17 -0
  24. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/config.py +46 -0
  25. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/models/__init__.py +1 -0
  26. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/models/types.py +87 -0
  27. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/parsers/__init__.py +1 -0
  28. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/parsers/netlist_parser.py +234 -0
  29. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/parsers/pcb_parser.py +375 -0
  30. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/parsers/pcb_parser_kicad.py +327 -0
  31. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/parsers/schematic_parser.py +902 -0
  32. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/server.py +71 -0
  33. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/__init__.py +1 -0
  34. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/arduino/connectivity_test.cpp.j2 +189 -0
  35. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/device_tree/atmega.dts.j2 +77 -0
  36. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/device_tree/esp32.dts.j2 +77 -0
  37. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/device_tree/nrf52.dts.j2 +77 -0
  38. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/device_tree/stm32f4.dts.j2 +89 -0
  39. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/esp_idf/test_suite.c.j2 +340 -0
  40. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/pytest/test_connectivity.py.j2 +147 -0
  41. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/st_hal/hal_test.c.j2 +313 -0
  42. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/pytest_gpio_test.py.j2 +99 -0
  43. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/pytest_i2c_test.py.j2 +117 -0
  44. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/pytest_pinmux_test.py.j2 +43 -0
  45. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/pytest_spi_test.py.j2 +94 -0
  46. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/unity_gpio_test.c.j2 +113 -0
  47. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/unity_i2c_test.c.j2 +101 -0
  48. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/unity_spi_test.c.j2 +94 -0
  49. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/unittest/test_schematic.py.j2 +172 -0
  50. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/__init__.py +36 -0
  51. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/device_tree.py +1187 -0
  52. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/hierarchical_analysis.py +211 -0
  53. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/netlist.py +320 -0
  54. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/parts_registry.py +142 -0
  55. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/pcb.py +955 -0
  56. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/pcb_layout.py +308 -0
  57. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/pin_analysis.py +765 -0
  58. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/project.py +196 -0
  59. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/schematic.py +319 -0
  60. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/schematic_editor.py +674 -0
  61. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/schematic_search.py +158 -0
  62. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/validation.py +866 -0
  63. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/visualization.py +225 -0
  64. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/utils/__init__.py +1 -0
  65. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/utils/file_handlers.py +65 -0
  66. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/utils/kicad_cli.py +103 -0
  67. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/utils/kicad_version.py +103 -0
  68. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/utils/parts_registry.py +197 -0
  69. package/mcp_servers/kicad-mcp-server/tests/__init__.py +1 -0
  70. package/mcp_servers/kicad-mcp-server/tests/examples/ESP32S3_TEST.md +219 -0
  71. package/mcp_servers/kicad-mcp-server/tests/fixtures/README.md +65 -0
  72. package/mcp_servers/kicad-mcp-server/tests/fixtures/__init__.py +1 -0
  73. package/mcp_servers/kicad-mcp-server/tests/fixtures/example_pcb.kicad_pcb +177 -0
  74. package/mcp_servers/kicad-mcp-server/tests/fixtures/example_schematic.kicad_sch +145 -0
  75. package/mcp_servers/kicad-mcp-server/tests/fixtures/hier/child.kicad_sch +24 -0
  76. package/mcp_servers/kicad-mcp-server/tests/fixtures/hier/root.kicad_sch +38 -0
  77. package/mcp_servers/kicad-mcp-server/tests/test_tools/__init__.py +1 -0
  78. package/mcp_servers/kicad-mcp-server/tests/test_tools/test_hierarchical_labels.py +195 -0
  79. package/mcp_servers/kicad-mcp-server/tests/test_tools/test_kicad_cli.py +116 -0
  80. package/mcp_servers/kicad-mcp-server/tests/test_tools/test_netlist_cache_path.py +29 -0
  81. package/mcp_servers/kicad-mcp-server/tests/test_tools/test_schematic.py +189 -0
  82. package/mcp_servers/kicad-mcp-server/tests/test_tools/test_schematic_hierarchy.py +69 -0
  83. package/mcp_servers/kicad-mcp-server/tests/test_tools/test_visualization.py +136 -0
  84. package/mcp_servers/kicad-mcp-server/uv.lock +2873 -0
  85. package/mcp_servers/openscad-mcp-server/.dockerignore +9 -0
  86. package/mcp_servers/openscad-mcp-server/.github/workflows/test.yml +40 -0
  87. package/mcp_servers/openscad-mcp-server/Dockerfile +29 -0
  88. package/mcp_servers/openscad-mcp-server/LICENSE +21 -0
  89. package/mcp_servers/openscad-mcp-server/README.md +154 -0
  90. package/mcp_servers/openscad-mcp-server/docs/audit.md +56 -0
  91. package/mcp_servers/openscad-mcp-server/docs/docker.md +66 -0
  92. package/mcp_servers/openscad-mcp-server/docs/issue-followup.md +19 -0
  93. package/mcp_servers/openscad-mcp-server/docs/jetson.md +17 -0
  94. package/mcp_servers/openscad-mcp-server/glama.json +4 -0
  95. package/mcp_servers/openscad-mcp-server/legacy/README.md +15 -0
  96. package/mcp_servers/openscad-mcp-server/legacy/README.original.md +294 -0
  97. package/mcp_servers/openscad-mcp-server/legacy/implementation_plan.md +100 -0
  98. package/mcp_servers/openscad-mcp-server/legacy/old/download_sam2_checkpoint.py +115 -0
  99. package/mcp_servers/openscad-mcp-server/legacy/old/src/ai/sam_segmentation.py +209 -0
  100. package/mcp_servers/openscad-mcp-server/legacy/old/src/models/threestudio_generator.py +231 -0
  101. package/mcp_servers/openscad-mcp-server/legacy/old/src/workflow/image_to_model_pipeline.py +260 -0
  102. package/mcp_servers/openscad-mcp-server/legacy/old/test_sam2_segmentation.py +96 -0
  103. package/mcp_servers/openscad-mcp-server/legacy/requirements.txt +57 -0
  104. package/mcp_servers/openscad-mcp-server/legacy/rtfmd/README.md +39 -0
  105. package/mcp_servers/openscad-mcp-server/legacy/rtfmd/decisions/ai-driven-code-generation.md +122 -0
  106. package/mcp_servers/openscad-mcp-server/legacy/rtfmd/decisions/export-formats.md +76 -0
  107. package/mcp_servers/openscad-mcp-server/legacy/rtfmd/files/src/ai/ai_service.py.md +51 -0
  108. package/mcp_servers/openscad-mcp-server/legacy/rtfmd/files/src/main.py.md +63 -0
  109. package/mcp_servers/openscad-mcp-server/legacy/rtfmd/files/src/models/code_generator.py.md +63 -0
  110. package/mcp_servers/openscad-mcp-server/legacy/rtfmd/files/src/nlp/parameter_extractor.py.md +63 -0
  111. package/mcp_servers/openscad-mcp-server/legacy/rtfmd/knowledge/ai/natural-language-processing.md +78 -0
  112. package/mcp_servers/openscad-mcp-server/legacy/rtfmd/knowledge/nlp/parameter-extraction.md +173 -0
  113. package/mcp_servers/openscad-mcp-server/legacy/rtfmd/knowledge/openscad/export-formats.md +91 -0
  114. package/mcp_servers/openscad-mcp-server/legacy/rtfmd/knowledge/openscad/openscad-basics.md +66 -0
  115. package/mcp_servers/openscad-mcp-server/legacy/rtfmd/knowledge/openscad/primitive-testing.md +79 -0
  116. package/mcp_servers/openscad-mcp-server/legacy/src/__init__.py +0 -0
  117. package/mcp_servers/openscad-mcp-server/legacy/src/ai/ai_service.py +257 -0
  118. package/mcp_servers/openscad-mcp-server/legacy/src/ai/gemini_api.py +161 -0
  119. package/mcp_servers/openscad-mcp-server/legacy/src/ai/venice_api.py +203 -0
  120. package/mcp_servers/openscad-mcp-server/legacy/src/config.py +121 -0
  121. package/mcp_servers/openscad-mcp-server/legacy/src/main.py +1456 -0
  122. package/mcp_servers/openscad-mcp-server/legacy/src/main.py.new +404 -0
  123. package/mcp_servers/openscad-mcp-server/legacy/src/main_remote.py +401 -0
  124. package/mcp_servers/openscad-mcp-server/legacy/src/models/__init__.py +0 -0
  125. package/mcp_servers/openscad-mcp-server/legacy/src/models/code_generator.py +321 -0
  126. package/mcp_servers/openscad-mcp-server/legacy/src/models/cuda_mvs.py +209 -0
  127. package/mcp_servers/openscad-mcp-server/legacy/src/models/scad_templates/basic_shapes.scad +144 -0
  128. package/mcp_servers/openscad-mcp-server/legacy/src/nlp/__init__.py +0 -0
  129. package/mcp_servers/openscad-mcp-server/legacy/src/nlp/parameter_extractor.py +388 -0
  130. package/mcp_servers/openscad-mcp-server/legacy/src/openscad_wrapper/__init__.py +0 -0
  131. package/mcp_servers/openscad-mcp-server/legacy/src/openscad_wrapper/wrapper.py +418 -0
  132. package/mcp_servers/openscad-mcp-server/legacy/src/printer_discovery/__init__.py +1 -0
  133. package/mcp_servers/openscad-mcp-server/legacy/src/printer_discovery/printer_discovery.py +471 -0
  134. package/mcp_servers/openscad-mcp-server/legacy/src/remote/connection_manager.py +537 -0
  135. package/mcp_servers/openscad-mcp-server/legacy/src/remote/cuda_mvs_client.py +435 -0
  136. package/mcp_servers/openscad-mcp-server/legacy/src/remote/cuda_mvs_server.py +787 -0
  137. package/mcp_servers/openscad-mcp-server/legacy/src/remote/error_handling.py +415 -0
  138. package/mcp_servers/openscad-mcp-server/legacy/src/testing/__init__.py +0 -0
  139. package/mcp_servers/openscad-mcp-server/legacy/src/testing/primitive_tester.py +203 -0
  140. package/mcp_servers/openscad-mcp-server/legacy/src/testing/test_primitives.py +98 -0
  141. package/mcp_servers/openscad-mcp-server/legacy/src/utils/__init__.py +1 -0
  142. package/mcp_servers/openscad-mcp-server/legacy/src/utils/cad_exporter.py +241 -0
  143. package/mcp_servers/openscad-mcp-server/legacy/src/utils/format_validator.py +206 -0
  144. package/mcp_servers/openscad-mcp-server/legacy/src/utils/stl_exporter.py +140 -0
  145. package/mcp_servers/openscad-mcp-server/legacy/src/utils/stl_repair.py +91 -0
  146. package/mcp_servers/openscad-mcp-server/legacy/src/utils/stl_validator.py +123 -0
  147. package/mcp_servers/openscad-mcp-server/legacy/src/visualization/__init__.py +0 -0
  148. package/mcp_servers/openscad-mcp-server/legacy/src/visualization/headless_renderer.py +52 -0
  149. package/mcp_servers/openscad-mcp-server/legacy/src/visualization/renderer.py +177 -0
  150. package/mcp_servers/openscad-mcp-server/legacy/src/visualization/web_interface.py +639 -0
  151. package/mcp_servers/openscad-mcp-server/legacy/src/workflow/image_approval.py +148 -0
  152. package/mcp_servers/openscad-mcp-server/legacy/src/workflow/multi_view_to_model_pipeline.py +338 -0
  153. package/mcp_servers/openscad-mcp-server/legacy/test_complete_workflow.py +374 -0
  154. package/mcp_servers/openscad-mcp-server/legacy/test_cuda_mvs.py +191 -0
  155. package/mcp_servers/openscad-mcp-server/legacy/test_gemini_api.py +168 -0
  156. package/mcp_servers/openscad-mcp-server/legacy/test_image_approval.py +192 -0
  157. package/mcp_servers/openscad-mcp-server/legacy/test_image_approval_workflow.py +251 -0
  158. package/mcp_servers/openscad-mcp-server/legacy/test_image_to_model_pipeline.py +145 -0
  159. package/mcp_servers/openscad-mcp-server/legacy/test_model_selection.py +41 -0
  160. package/mcp_servers/openscad-mcp-server/legacy/test_multi_view_pipeline.py +290 -0
  161. package/mcp_servers/openscad-mcp-server/legacy/test_primitives.sh +13 -0
  162. package/mcp_servers/openscad-mcp-server/legacy/test_rabbit_direct.py +71 -0
  163. package/mcp_servers/openscad-mcp-server/legacy/test_remote_cuda_mvs.py +283 -0
  164. package/mcp_servers/openscad-mcp-server/legacy/test_venice_example.py +69 -0
  165. package/mcp_servers/openscad-mcp-server/pyproject.toml +33 -0
  166. package/mcp_servers/openscad-mcp-server/requirements.txt +2 -0
  167. package/mcp_servers/openscad-mcp-server/scad/simple_cube.scad +2 -0
  168. package/mcp_servers/openscad-mcp-server/scripts/test_docker.py +220 -0
  169. package/mcp_servers/openscad-mcp-server/src/openscad_mcp/__init__.py +3 -0
  170. package/mcp_servers/openscad-mcp-server/src/openscad_mcp/__main__.py +3 -0
  171. package/mcp_servers/openscad-mcp-server/src/openscad_mcp/engine.py +94 -0
  172. package/mcp_servers/openscad-mcp-server/src/openscad_mcp/geometry.py +242 -0
  173. package/mcp_servers/openscad-mcp-server/src/openscad_mcp/server.py +245 -0
  174. package/mcp_servers/openscad-mcp-server/src/openscad_mcp/service.py +190 -0
  175. package/mcp_servers/openscad-mcp-server/tests/conftest.py +17 -0
  176. package/mcp_servers/openscad-mcp-server/tests/test_engine.py +37 -0
  177. package/mcp_servers/openscad-mcp-server/tests/test_geometry.py +106 -0
  178. package/mcp_servers/openscad-mcp-server/tests/test_integration.py +172 -0
  179. package/mcp_servers/openscad-mcp-server/tests/test_transports.py +314 -0
  180. package/mcp_servers/openscad-mcp-server/uv.lock +1123 -0
  181. package/package.json +44 -0
  182. package/scripts/install_mcps.sh +86 -0
  183. package/scripts/openscad_wrapper.sh +62 -0
  184. package/scripts/run_kicad_mcp.sh +25 -0
  185. package/scripts/run_openscad_mcp.sh +41 -0
  186. package/scripts/test_mcp_connection.py +626 -0
  187. package/scripts/test_mcp_connection.sh +168 -0
  188. package/skills/code-first-hardware-design/SKILL.md +260 -0
  189. package/skills/pcb-constraint-definition/SKILL.md +236 -0
  190. package/skills/pcb-layout-routing-automation/SKILL.md +153 -0
  191. package/skills/pcb-validation-dfm-signoff/SKILL.md +194 -0
  192. package/skills/schematic-datasheet-analysis/SKILL.md +202 -0
@@ -0,0 +1,866 @@
1
+ """Design Rule Checking (DRC) and Electrical Rules Check (ERC) tools for KiCad MCP Server."""
2
+
3
+ import json
4
+ import re
5
+ import tempfile
6
+ import xml.etree.ElementTree as ET
7
+ from pathlib import Path
8
+
9
+ from ..models.types import DRCError, ERCError
10
+ from ..server import mcp
11
+ from ..utils.kicad_cli import run_kicad_cli
12
+
13
+
14
+ def _temp_dir() -> Path:
15
+ return Path(tempfile.gettempdir())
16
+
17
+
18
+ def _find_root_schematic(sch_path: Path) -> Path | None:
19
+ """If sch_path is a sub-sheet, return the root schematic instead.
20
+
21
+ KiCad convention: root schematic has the same stem as the .kicad_pro file.
22
+ kicad-cli silently drops unnamed local nets when exporting from a sub-sheet,
23
+ so we detect this and redirect to the root schematic.
24
+ """
25
+ pro_files = list(sch_path.parent.glob("*.kicad_pro"))
26
+ if not pro_files:
27
+ return None
28
+ root_sch = pro_files[0].with_suffix(".kicad_sch")
29
+ if root_sch.exists() and root_sch.resolve() != sch_path.resolve():
30
+ return root_sch
31
+ return None
32
+
33
+
34
+ def _parse_erc_report(report_path: Path) -> list[ERCError]:
35
+ """Parse ERC report file (JSON or XML format).
36
+
37
+ Args:
38
+ report_path: Path to ERC report file
39
+
40
+ Returns:
41
+ List of ERC errors/warnings
42
+ """
43
+ errors = []
44
+ content = report_path.read_text(encoding="utf-8", errors="replace")
45
+
46
+ try:
47
+ data = json.loads(content)
48
+ # KiCad 9+ ERC JSON: violations are a top-level list (there is no
49
+ # "sheets" key — reading data["sheets"] silently returned []).
50
+ for v in data.get("violations", []):
51
+ components = []
52
+ for item in v.get("items", []):
53
+ desc = item.get("description", "")
54
+ # Description looks like "Pin U1 of ...": pull the reference.
55
+ # Don't gate on the literal English word "Symbol" — localized
56
+ # KiCad uses other words and the ref would be missed.
57
+ m = re.search(r"\b(?:Pin|Symbol)\s+([A-Za-z]+\d+)", desc)
58
+ if m:
59
+ components.append(m.group(1))
60
+ errors.append(
61
+ ERCError(
62
+ severity=v.get("severity", "error"),
63
+ type=v.get("type", "unknown"),
64
+ description=v.get("description", ""),
65
+ components=components,
66
+ )
67
+ )
68
+ return errors
69
+ except (json.JSONDecodeError, KeyError):
70
+ pass
71
+
72
+ # Fallback: XML format
73
+ try:
74
+ root = ET.fromstring(content)
75
+ for violation in root.findall(".//violation"):
76
+ severity = violation.get("severity", "error")
77
+ error_type = violation.get("type", "unknown")
78
+ description = violation.get("description", "")
79
+
80
+ components = []
81
+ for comp in violation.findall(".//component"):
82
+ ref = comp.get("ref", "")
83
+ if ref:
84
+ components.append(ref)
85
+
86
+ errors.append(
87
+ ERCError(
88
+ severity=severity,
89
+ type=error_type,
90
+ description=description,
91
+ components=components,
92
+ )
93
+ )
94
+ except ET.ParseError as e:
95
+ errors.append(
96
+ ERCError(
97
+ severity="error",
98
+ type="parse_error",
99
+ description=f"Failed to parse ERC report: {str(e)}",
100
+ components=[],
101
+ )
102
+ )
103
+
104
+ return errors
105
+
106
+
107
+ def _parse_drc_report(report_path: Path) -> list[DRCError]:
108
+ """Parse DRC report file (JSON or XML format).
109
+
110
+ Args:
111
+ report_path: Path to DRC report file
112
+
113
+ Returns:
114
+ List of DRC errors/warnings
115
+ """
116
+ errors = []
117
+ content = report_path.read_text(encoding="utf-8", errors="replace")
118
+
119
+ try:
120
+ data = json.loads(content)
121
+ # KiCad 9+ DRC JSON: violations are a top-level list (no "sheets" key —
122
+ # reading data["sheets"] returned [] and made run_drc report PASS with
123
+ # every violation silently dropped).
124
+ for v in data.get("violations", []):
125
+ x, y = 0.0, 0.0
126
+ items = v.get("items", [])
127
+ if items:
128
+ pos = items[0].get("pos", {})
129
+ x = float(pos.get("x", 0))
130
+ y = float(pos.get("y", 0))
131
+
132
+ errors.append(
133
+ DRCError(
134
+ severity=v.get("severity", "error"),
135
+ type=v.get("type", "unknown"),
136
+ description=v.get("description", ""),
137
+ location=(x, y),
138
+ )
139
+ )
140
+ return errors
141
+ except (json.JSONDecodeError, KeyError):
142
+ pass
143
+
144
+ # Fallback: XML format
145
+ try:
146
+ root = ET.fromstring(content)
147
+ for violation in root.findall(".//violation"):
148
+ severity = violation.get("severity", "error")
149
+ error_type = violation.get("type", "unknown")
150
+ description = violation.get("description", "")
151
+
152
+ location_elem = violation.find(".//location")
153
+ x = float(location_elem.get("x", "0")) if location_elem is not None else 0.0
154
+ y = float(location_elem.get("y", "0")) if location_elem is not None else 0.0
155
+
156
+ errors.append(
157
+ DRCError(
158
+ severity=severity,
159
+ type=error_type,
160
+ description=description,
161
+ location=(x, y),
162
+ )
163
+ )
164
+ except ET.ParseError as e:
165
+ errors.append(
166
+ DRCError(
167
+ severity="error",
168
+ type="parse_error",
169
+ description=f"Failed to parse DRC report: {str(e)}",
170
+ location=(0.0, 0.0),
171
+ )
172
+ )
173
+
174
+ return errors
175
+
176
+
177
+ def _filter_erc_errors(errors: list[ERCError], severity: str = "") -> list[ERCError]:
178
+ """Filter ERC errors by severity.
179
+
180
+ Args:
181
+ errors: List of ERC errors
182
+ severity: Severity filter ('error', 'warning', '') for all
183
+
184
+ Returns:
185
+ Filtered list of ERC errors
186
+ """
187
+ if not severity:
188
+ return errors
189
+
190
+ return [e for e in errors if e.severity == severity]
191
+
192
+
193
+ def _filter_drc_errors(errors: list[DRCError], violation_type: str = "") -> list[DRCError]:
194
+ """Filter DRC errors by type.
195
+
196
+ Args:
197
+ errors: List of DRC errors
198
+ violation_type: Type filter (e.g., 'clearance', 'spacing', '') for all
199
+
200
+ Returns:
201
+ Filtered list of DRC errors
202
+ """
203
+ if not violation_type:
204
+ return errors
205
+
206
+ return [e for e in errors if e.type == violation_type]
207
+
208
+
209
+ @mcp.tool()
210
+ async def run_erc(
211
+ schematic_path: str,
212
+ ) -> str:
213
+ """Run Electrical Rules Check (ERC) on schematic.
214
+
215
+ Checks for:
216
+ - Unconnected pins
217
+ - Power conflicts
218
+ - Multiple outputs on same net
219
+ - Pin type mismatches
220
+
221
+ Args:
222
+ schematic_path: Path to .kicad_sch file
223
+
224
+ Returns:
225
+ ERC report with violations and recommendations
226
+ """
227
+ try:
228
+ sch_path = Path(schematic_path)
229
+ if not sch_path.exists():
230
+ return f"❌ **Schematic file not found:** {schematic_path}"
231
+
232
+ # Detect sub-sheet and redirect to root schematic
233
+ subsheet_note = ""
234
+ root_sch = _find_root_schematic(sch_path)
235
+ if root_sch:
236
+ subsheet_note = (
237
+ f"\n\n⚠️ **Note:** `{sch_path.name}` is a hierarchical sub-sheet. "
238
+ f"Switched to root schematic `{root_sch.name}` for complete ERC check."
239
+ )
240
+ sch_path = root_sch
241
+
242
+ # ERC report output path. run_erc redirects sub-sheets to the root
243
+ # schematic, so the report is named after the ROOT stem — not the
244
+ # (possibly sub-sheet) input stem, which would never match.
245
+ root = _find_root_schematic(sch_path) or sch_path
246
+ erc_report_path = _temp_dir() / (root.stem + "_erc.rpt")
247
+
248
+ # Run ERC using kicad-cli (KiCad 7+)
249
+ result = await run_kicad_cli(
250
+ [
251
+ "sch",
252
+ "erc",
253
+ "--format",
254
+ "json",
255
+ "--output",
256
+ str(erc_report_path),
257
+ str(sch_path),
258
+ ],
259
+ timeout=60,
260
+ )
261
+
262
+ stderr_text = result.stderr.decode("utf-8", errors="replace") if result.stderr else ""
263
+
264
+ if result.returncode != 0:
265
+ return f"""❌ **ERC Check Failed**
266
+
267
+ **Schematic:** {schematic_path}
268
+
269
+ **Error:**
270
+ ```
271
+ {stderr_text}
272
+ ```
273
+
274
+ **Possible causes:**
275
+ - KiCad command line tools not available
276
+ - Invalid schematic file
277
+ - Missing project file (.kicad_pro)
278
+
279
+ **Manual steps:**
280
+ 1. Open schematic in KiCad Eeschema
281
+ 2. Run: Inspect → Electrical Rules Checker
282
+ 3. Export report manually{subsheet_note}"""
283
+
284
+ # Parse ERC report
285
+ if not erc_report_path.exists():
286
+ # kicad-cli returned success but produced no report — do NOT claim
287
+ # "all pins connected" (that was never verified).
288
+ return f"""⚠️ **ERC Report Not Generated**
289
+
290
+ **Schematic:** {schematic_path}
291
+
292
+ kicad-cli exited cleanly but wrote no report file, so no checks could be
293
+ verified. Ensure KiCad 9+ is installed and the schematic has a matching
294
+ .kicad_pro project file.{subsheet_note}"""
295
+
296
+ # Parse and format results
297
+ try:
298
+ errors = _parse_erc_report(erc_report_path)
299
+
300
+ if not errors:
301
+ return f"""✅ **ERC Check Passed**
302
+
303
+ **Schematic:** {schematic_path}
304
+
305
+ No electrical violations detected!{subsheet_note}"""
306
+
307
+ # Count by severity
308
+ error_count = sum(1 for e in errors if e.severity == "error")
309
+ warning_count = sum(1 for e in errors if e.severity == "warning")
310
+
311
+ # Format violations
312
+ violations_table = "| Severity | Type | Description | Components |\n"
313
+ violations_table += "|----------|------|-------------|------------|\n"
314
+
315
+ for error in errors[:20]: # Limit to first 20
316
+ severity_icon = "❌" if error.severity == "error" else "⚠️"
317
+ components_str = ", ".join(error.components) if error.components else "N/A"
318
+ violations_table += f"| {severity_icon} {error.severity} | {error.type} | {error.description[:50]} | {components_str} |\n"
319
+
320
+ if len(errors) > 20:
321
+ violations_table += f"\n*... and {len(errors) - 20} more violations*\n"
322
+
323
+ return f"""❌ **ERC Violations Detected**
324
+
325
+ **Schematic:** {schematic_path}
326
+ **Total Violations:** {len(errors)}
327
+ - Errors: {error_count}
328
+ - Warnings: {warning_count}{subsheet_note}
329
+
330
+ ## Violations
331
+
332
+ {violations_table}
333
+
334
+ ## Recommendations
335
+
336
+ 1. **Fix all errors** before proceeding to PCB layout
337
+ 2. Review warnings - some may be acceptable
338
+ 3. Re-run ERC after fixes
339
+ 4. Use `get_erc_violations()` for detailed filtering
340
+
341
+ ## Next Steps
342
+
343
+ - Run `get_erc_violations()` with filters to analyze specific issues
344
+ - Fix violations in KiCad Eeschema
345
+ - Re-run `run_erc()` to verify fixes"""
346
+
347
+ except Exception as e:
348
+ return f"""⚠️ **ERC Report Parse Error**
349
+
350
+ **Schematic:** {schematic_path}
351
+
352
+ ERC check completed but failed to parse report.
353
+
354
+ **Error:** {str(e)}
355
+
356
+ **Report location:** {erc_report_path}
357
+
358
+ You can manually inspect the report file or re-run ERC in KiCad GUI.{subsheet_note}"""
359
+
360
+ except FileNotFoundError as e:
361
+ # Distinguish kicad-cli missing (raised by subprocess) from a missing
362
+ # schematic file (already checked at the top of the function).
363
+ if "kicad-cli" in str(e):
364
+ return f"""❌ **kicad-cli Not Found**
365
+
366
+ **Schematic:** {schematic_path}
367
+
368
+ KiCad's command-line tool (kicad-cli) could not be located on PATH or in the
369
+ detected KiCad install directory. Install KiCad 7+ (https://www.kicad.org/),
370
+ or point the KICAD_CLI environment variable at the kicad-cli executable."""
371
+ return f"""❌ **File Not Found**
372
+
373
+ **Schematic:** {schematic_path}
374
+
375
+ {e}
376
+ Please check the file path and try again."""
377
+ except Exception as e:
378
+ import traceback
379
+
380
+ return f"""❌ **Unexpected Error**
381
+
382
+ **Schematic:** {schematic_path}
383
+
384
+ **Error:** {str(e)}
385
+
386
+ **Traceback:**
387
+ ```
388
+ {traceback.format_exc()}
389
+ ```"""
390
+
391
+
392
+ @mcp.tool()
393
+ async def run_drc(
394
+ pcb_path: str,
395
+ ) -> str:
396
+ """Run Design Rules Check (DRC) on PCB.
397
+
398
+ Checks for:
399
+ - Clearance violations
400
+ - Track spacing violations
401
+ - Missing connections
402
+ - Pad/footprint overlaps
403
+ - Board edge constraints
404
+
405
+ Args:
406
+ pcb_path: Path to .kicad_pcb file
407
+
408
+ Returns:
409
+ DRC report with violations and locations
410
+ """
411
+ try:
412
+ pcb = Path(pcb_path)
413
+ if not pcb.exists():
414
+ return f"❌ **PCB file not found:** {pcb_path}"
415
+
416
+ # DRC report output path
417
+ drc_report_path = _temp_dir() / (pcb.stem + "_drc.rpt")
418
+
419
+ # Run DRC using kicad-cli (KiCad 7+)
420
+ result = await run_kicad_cli(
421
+ [
422
+ "pcb",
423
+ "drc",
424
+ "--format",
425
+ "json",
426
+ "--output",
427
+ str(drc_report_path),
428
+ str(pcb),
429
+ ],
430
+ timeout=60,
431
+ )
432
+
433
+ stderr_text = result.stderr.decode("utf-8", errors="replace") if result.stderr else ""
434
+
435
+ if result.returncode != 0:
436
+ return f"""❌ **DRC Check Failed**
437
+
438
+ **PCB:** {pcb_path}
439
+
440
+ **Error:**
441
+ ```
442
+ {stderr_text}
443
+ ```
444
+
445
+ **Possible causes:**
446
+ - KiCad command line tools not available
447
+ - Invalid PCB file
448
+ - Missing design rules
449
+
450
+ **Manual steps:**
451
+ 1. Open PCB in KiCad Pcbnew
452
+ 2. Run: Inspect → Design Rules Checker
453
+ 3. Export report manually"""
454
+
455
+ # Parse DRC report
456
+ if not drc_report_path.exists():
457
+ # No violations found
458
+ return f"""✅ **DRC Check Passed**
459
+
460
+ **PCB:** {pcb_path}
461
+
462
+ No design rule violations detected!
463
+
464
+ **Checked:**
465
+ - ✅ All clearance requirements met
466
+ - ✅ No spacing violations
467
+ - ✅ All connections routed
468
+ - ✅ No pad/footprint overlaps
469
+ - ✅ Board edge constraints satisfied"""
470
+
471
+ # Parse and format results
472
+ try:
473
+ errors = _parse_drc_report(drc_report_path)
474
+
475
+ if not errors:
476
+ return f"""✅ **DRC Check Passed**
477
+
478
+ **PCB:** {pcb_path}
479
+
480
+ No design rule violations detected!"""
481
+
482
+ # Count by severity
483
+ error_count = sum(1 for e in errors if e.severity == "error")
484
+ warning_count = sum(1 for e in errors if e.severity == "warning")
485
+
486
+ # Group by type
487
+ type_counts = {}
488
+ for error in errors:
489
+ type_counts[error.type] = type_counts.get(error.type, 0) + 1
490
+
491
+ # Format violations
492
+ violations_table = "| Severity | Type | Location | Description |\n"
493
+ violations_table += "|----------|------|----------|-------------|\n"
494
+
495
+ for error in errors[:20]: # Limit to first 20
496
+ severity_icon = "❌" if error.severity == "error" else "⚠️"
497
+ x, y = error.location
498
+ violations_table += f"| {severity_icon} {error.severity} | {error.type} | ({x:.2f}, {y:.2f}) | {error.description[:40]} |\n"
499
+
500
+ if len(errors) > 20:
501
+ violations_table += f"\n*... and {len(errors) - 20} more violations*\n"
502
+
503
+ # Format type summary
504
+ type_summary = "\n## Violation Summary\n\n"
505
+ for vtype, count in sorted(type_counts.items()):
506
+ type_summary += f"- **{vtype}**: {count} violations\n"
507
+
508
+ return f"""❌ **DRC Violations Detected**
509
+
510
+ **PCB:** {pcb_path}
511
+ **Total Violations:** {len(errors)}
512
+ - Errors: {error_count}
513
+ - Warnings: {warning_count}
514
+
515
+ {type_summary}
516
+ ## Violations
517
+
518
+ {violations_table}
519
+
520
+ ## Recommendations
521
+
522
+ 1. **Fix all errors** before manufacturing
523
+ 2. Review warnings - some may be acceptable
524
+ 3. Pay attention to clearance violations
525
+ 4. Re-run DRC after fixes
526
+ 5. Use `get_drc_violations()` for detailed filtering
527
+
528
+ ## Next Steps
529
+
530
+ - Run `get_drc_violations()` with filters to analyze specific issues
531
+ - Fix violations in KiCad Pcbnew
532
+ - Re-run `run_drc()` to verify fixes"""
533
+
534
+ except Exception as e:
535
+ return f"""⚠️ **DRC Report Parse Error**
536
+
537
+ **PCB:** {pcb_path}
538
+
539
+ DRC check completed but failed to parse report.
540
+
541
+ **Error:** {str(e)}
542
+
543
+ **Report location:** {drc_report_path}
544
+
545
+ You can manually inspect the report file or re-run DRC in KiCad GUI."""
546
+
547
+ except FileNotFoundError:
548
+ return f"""❌ **File Not Found**
549
+
550
+ **PCB:** {pcb_path}
551
+
552
+ Please check the file path and try again."""
553
+ except Exception as e:
554
+ import traceback
555
+
556
+ return f"""❌ **Unexpected Error**
557
+
558
+ **PCB:** {pcb_path}
559
+
560
+ **Error:** {str(e)}
561
+
562
+ **Traceback:**
563
+ ```
564
+ {traceback.format_exc()}
565
+ ```"""
566
+
567
+
568
+ @mcp.tool()
569
+ async def get_erc_violations(
570
+ schematic_path: str,
571
+ severity: str = "",
572
+ ) -> str:
573
+ """Get filtered ERC violations from schematic.
574
+
575
+ Args:
576
+ schematic_path: Path to .kicad_sch file
577
+ severity: Filter by severity ('error', 'warning', '') for all
578
+
579
+ Returns:
580
+ Filtered list of ERC violations
581
+ """
582
+ try:
583
+ sch_path = Path(schematic_path)
584
+ if not sch_path.exists():
585
+ return f"❌ **Schematic file not found:** {schematic_path}"
586
+
587
+ # ERC report output path. run_erc redirects sub-sheets to the root
588
+ # schematic, so the report is named after the ROOT stem — not the
589
+ # (possibly sub-sheet) input stem, which would never match.
590
+ root = _find_root_schematic(sch_path) or sch_path
591
+ erc_report_path = _temp_dir() / (root.stem + "_erc.rpt")
592
+
593
+ if not erc_report_path.exists():
594
+ # Run ERC first
595
+ initial_result = await run_erc(schematic_path)
596
+ if "❌" in initial_result or "⚠️" in initial_result:
597
+ return initial_result
598
+
599
+ # Parse ERC report
600
+ errors = _parse_erc_report(erc_report_path)
601
+
602
+ if not errors:
603
+ return f"""✅ **No ERC Violations**
604
+
605
+ **Schematic:** {schematic_path}
606
+
607
+ No electrical violations detected."""
608
+
609
+ # Apply filter
610
+ filtered_errors = _filter_erc_errors(errors, severity)
611
+
612
+ if not filtered_errors:
613
+ return f"""✅ **No Matching Violations**
614
+
615
+ **Schematic:** {schematic_path}
616
+ **Filter:** severity = '{severity or 'all'}'
617
+
618
+ No violations match the specified filter."""
619
+
620
+ # Format results
621
+ result = f"""# ERC Violations
622
+
623
+ **Schematic:** {schematic_path}
624
+ **Filter:** severity = '{severity or 'all'}'
625
+ **Count:** {len(filtered_errors)}
626
+
627
+ ## Violations
628
+
629
+ | Severity | Type | Description | Components |
630
+ |----------|------|-------------|------------|
631
+ """
632
+
633
+ for error in filtered_errors:
634
+ severity_icon = "❌" if error.severity == "error" else "⚠️"
635
+ components_str = ", ".join(error.components) if error.components else "N/A"
636
+ result += f"| {severity_icon} {error.severity} | {error.type} | {error.description} | {components_str} |\n"
637
+
638
+ return result
639
+
640
+ except Exception as e:
641
+ import traceback
642
+
643
+ return f"""❌ **Error**
644
+
645
+ **Schematic:** {schematic_path}
646
+
647
+ **Error:** {str(e)}
648
+
649
+ **Traceback:**
650
+ ```
651
+ {traceback.format_exc()}
652
+ ```"""
653
+
654
+
655
+ @mcp.tool()
656
+ async def get_drc_violations(
657
+ pcb_path: str,
658
+ violation_type: str = "",
659
+ ) -> str:
660
+ """Get filtered DRC violations from PCB.
661
+
662
+ Args:
663
+ pcb_path: Path to .kicad_pcb file
664
+ violation_type: Filter by type (e.g., 'clearance', 'spacing', '') for all
665
+
666
+ Returns:
667
+ Filtered list of DRC violations
668
+ """
669
+ try:
670
+ pcb = Path(pcb_path)
671
+ if not pcb.exists():
672
+ return f"❌ **PCB file not found:** {pcb_path}"
673
+
674
+ # DRC report output path
675
+ drc_report_path = _temp_dir() / (pcb.stem + "_drc.rpt")
676
+
677
+ if not drc_report_path.exists():
678
+ # Run DRC first
679
+ initial_result = await run_drc(pcb_path)
680
+ if "❌" in initial_result or "⚠️" in initial_result:
681
+ return initial_result
682
+
683
+ # Parse DRC report
684
+ errors = _parse_drc_report(drc_report_path)
685
+
686
+ if not errors:
687
+ return f"""✅ **No DRC Violations**
688
+
689
+ **PCB:** {pcb_path}
690
+
691
+ No design rule violations detected."""
692
+
693
+ # Apply filter
694
+ filtered_errors = _filter_drc_errors(errors, violation_type)
695
+
696
+ if not filtered_errors:
697
+ return f"""✅ **No Matching Violations**
698
+
699
+ **PCB:** {pcb_path}
700
+ **Filter:** type = '{violation_type or 'all'}'
701
+
702
+ No violations match the specified filter."""
703
+
704
+ # Format results
705
+ result = f"""# DRC Violations
706
+
707
+ **PCB:** {pcb_path}
708
+ **Filter:** type = '{violation_type or 'all'}'
709
+ **Count:** {len(filtered_errors)}
710
+
711
+ ## Violations
712
+
713
+ | Severity | Type | Location | Description |
714
+ |----------|------|----------|-------------|
715
+ """
716
+
717
+ for error in filtered_errors:
718
+ severity_icon = "❌" if error.severity == "error" else "⚠️"
719
+ x, y = error.location
720
+ result += f"| {severity_icon} {error.severity} | {error.type} | ({x:.2f}, {y:.2f}) | {error.description} |\n"
721
+
722
+ return result
723
+
724
+ except Exception as e:
725
+ import traceback
726
+
727
+ return f"""❌ **Error**
728
+
729
+ **PCB:** {pcb_path}
730
+
731
+ **Error:** {str(e)}
732
+
733
+ **Traceback:**
734
+ ```
735
+ {traceback.format_exc()}
736
+ ```"""
737
+
738
+
739
+ @mcp.tool()
740
+ async def export_erc_report(
741
+ schematic_path: str,
742
+ output_path: str = "",
743
+ ) -> str:
744
+ """Export ERC report to file.
745
+
746
+ Args:
747
+ schematic_path: Path to .kicad_sch file
748
+ output_path: Optional output file path (default: schematic_name_erc.txt)
749
+
750
+ Returns:
751
+ Confirmation with output file location
752
+ """
753
+ try:
754
+ sch_path = Path(schematic_path)
755
+ if not sch_path.exists():
756
+ return f"❌ **Schematic file not found:** {schematic_path}"
757
+
758
+ # Run ERC first to generate report
759
+ initial_result = await run_erc(schematic_path)
760
+
761
+ if "❌" in initial_result:
762
+ return f"❌ **ERC Check Failed**\n\n{initial_result}"
763
+
764
+ # ERC report path
765
+ erc_report_path = _temp_dir() / (sch_path.stem + "_erc.rpt")
766
+
767
+ if not erc_report_path.exists():
768
+ return "❌ **ERC report not found.** Run ERC check first."
769
+
770
+ # Determine output path
771
+ if not output_path:
772
+ output_path = str(sch_path.parent / (sch_path.stem + "_erc_report.txt"))
773
+
774
+ output = Path(output_path)
775
+
776
+ # Copy report to output location
777
+ import shutil
778
+
779
+ shutil.copy(erc_report_path, output)
780
+
781
+ return f"""✅ **ERC Report Exported**
782
+
783
+ **Schematic:** {schematic_path}
784
+ **Output:** {output}
785
+
786
+ The ERC report has been successfully exported to the specified location."""
787
+
788
+ except Exception as e:
789
+ import traceback
790
+
791
+ return f"""❌ **Export Failed**
792
+
793
+ **Schematic:** {schematic_path}
794
+ **Output:** {output_path}
795
+
796
+ **Error:** {str(e)}
797
+
798
+ **Traceback:**
799
+ ```
800
+ {traceback.format_exc()}
801
+ ```"""
802
+
803
+
804
+ @mcp.tool()
805
+ async def export_drc_report(
806
+ pcb_path: str,
807
+ output_path: str = "",
808
+ ) -> str:
809
+ """Export DRC report to file.
810
+
811
+ Args:
812
+ pcb_path: Path to .kicad_pcb file
813
+ output_path: Optional output file path (default: pcb_name_drc.txt)
814
+
815
+ Returns:
816
+ Confirmation with output file location
817
+ """
818
+ try:
819
+ pcb = Path(pcb_path)
820
+ if not pcb.exists():
821
+ return f"❌ **PCB file not found:** {pcb_path}"
822
+
823
+ # Run DRC first to generate report
824
+ initial_result = await run_drc(pcb_path)
825
+
826
+ if "❌" in initial_result:
827
+ return f"❌ **DRC Check Failed**\n\n{initial_result}"
828
+
829
+ # DRC report path
830
+ drc_report_path = _temp_dir() / (pcb.stem + "_drc.rpt")
831
+
832
+ if not drc_report_path.exists():
833
+ return "❌ **DRC report not found.** Run DRC check first."
834
+
835
+ # Determine output path
836
+ if not output_path:
837
+ output_path = str(pcb.parent / (pcb.stem + "_drc_report.txt"))
838
+
839
+ output = Path(output_path)
840
+
841
+ # Copy report to output location
842
+ import shutil
843
+
844
+ shutil.copy(drc_report_path, output)
845
+
846
+ return f"""✅ **DRC Report Exported**
847
+
848
+ **PCB:** {pcb_path}
849
+ **Output:** {output}
850
+
851
+ The DRC report has been successfully exported to the specified location."""
852
+
853
+ except Exception as e:
854
+ import traceback
855
+
856
+ return f"""❌ **Export Failed**
857
+
858
+ **PCB:** {pcb_path}
859
+ **Output:** {output_path}
860
+
861
+ **Error:** {str(e)}
862
+
863
+ **Traceback:**
864
+ ```
865
+ {traceback.format_exc()}
866
+ ```"""