@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,211 @@
1
+ """Hierarchical schematic analysis tools for KiCad MCP Server."""
2
+
3
+ from pathlib import Path
4
+
5
+ from ..parsers.netlist_parser import NetlistParser
6
+ from ..parsers.schematic_parser import SchematicParser
7
+ from ..server import mcp
8
+
9
+
10
+ def _find_component_location(file_path: str, reference: str, sheets: list[dict]) -> tuple[object | None, str]:
11
+ """Find a component across the main sheet and sub-sheets.
12
+
13
+ Returns (component_or_None, sheet_name).
14
+ """
15
+ parser = SchematicParser(file_path)
16
+ for component in parser.get_components():
17
+ if component.reference == reference:
18
+ return component, "Main Schematic"
19
+
20
+ project_dir = Path(file_path).parent
21
+ for sheet in sheets:
22
+ sheet_path = project_dir / sheet.get("file", "")
23
+ if sheet_path.exists():
24
+ try:
25
+ for component in SchematicParser(str(sheet_path)).get_components():
26
+ if component.reference == reference:
27
+ return component, sheet.get("name", sheet.get("file", ""))
28
+ except Exception:
29
+ continue
30
+ return None, ""
31
+
32
+
33
+ @mcp.tool()
34
+ async def trace_hierarchical_connection(
35
+ file_path: str,
36
+ reference: str,
37
+ pin_number: str = ""
38
+ ) -> str:
39
+ """Trace component connections across hierarchical schematics.
40
+
41
+ This tool traces connections through the entire schematic hierarchy,
42
+ including sub-sheets, to show complete signal paths.
43
+
44
+ Args:
45
+ file_path: Path to main .kicad_sch file
46
+ reference: Component reference designator (e.g., 'U1', 'R5')
47
+ pin_number: Optional pin number to trace (if empty, trace all pins)
48
+
49
+ Returns:
50
+ Complete connection trace through hierarchy
51
+ """
52
+ try:
53
+ from .pin_analysis import _ensure_netlist
54
+
55
+ parser = SchematicParser(file_path)
56
+ sheets = parser.get_sheets()
57
+
58
+ # Search the main sheet AND sub-sheets (previously only the root was
59
+ # searched, so any component placed in a sub-sheet was "Not Found").
60
+ target_component, location = _find_component_location(file_path, reference, sheets)
61
+
62
+ if not target_component:
63
+ refs = [c.reference for c in parser.get_components()][:10]
64
+ return f"# Component Not Found\n\nComponent '{reference}' not found in schematic hierarchy.\n\nAvailable components: {', '.join(refs)}..."
65
+
66
+ lines = [
67
+ f"# Connection Trace for {target_component.reference}",
68
+ "",
69
+ f"**Component:** {target_component.reference}",
70
+ f"**Value:** {target_component.value}",
71
+ f"**Location:** {location}",
72
+ f"**File:** {file_path}",
73
+ "",
74
+ ]
75
+
76
+ # The authoritative pin<->net map lives in the netlist. SchematicParser
77
+ # nets carry no pin list (SchematicNet.pins is always empty), so tracing
78
+ # via them always reported "no connections".
79
+ netlist_path = await _ensure_netlist(Path(file_path))
80
+ if netlist_path is None:
81
+ lines.append("⚠️ No netlist available — cannot resolve connections. Run `generate_netlist()` first.")
82
+ return "\n".join(lines)
83
+
84
+ nl = NetlistParser(str(netlist_path))
85
+ comp = nl.get_components().get(reference)
86
+ if not comp or not comp.pins:
87
+ lines.append("No connections found for this component.")
88
+ return "\n".join(lines)
89
+
90
+ pins_to_trace = [pin_number] if pin_number else list(comp.pins.keys())
91
+ for pn in pins_to_trace:
92
+ conn = nl.trace_connection(reference, str(pn))
93
+ net_name = conn.get("net") if isinstance(conn, dict) else None
94
+ connected = conn.get("connected_to", []) if isinstance(conn, dict) else []
95
+ lines.append(f"## Pin {pn}")
96
+ lines.append(f"**Net:** {net_name or '(unconnected)'}")
97
+ if connected:
98
+ lines.append(f"**Connected to ({len(connected)} pins):**")
99
+ for ref, p in connected[:15]:
100
+ lines.append(f" - {ref}:{p}")
101
+ if len(connected) > 15:
102
+ lines.append(f" - ... and {len(connected) - 15} more")
103
+ else:
104
+ lines.append("**No external connections**")
105
+ lines.append("")
106
+
107
+ return "\n".join(lines)
108
+
109
+ except FileNotFoundError:
110
+ return f"Error: File not found: {file_path}"
111
+ except Exception as e:
112
+ return f"Error: {e}"
113
+
114
+
115
+ @mcp.tool()
116
+ async def analyze_hierarchical_nets(
117
+ file_path: str,
118
+ filter_pattern: str = "",
119
+ show_hierarchy: bool = True
120
+ ) -> str:
121
+ """Analyze all nets in hierarchical schematic design.
122
+
123
+ This tool analyzes network connections across the entire hierarchy,
124
+ showing how signals flow between main schematic and sub-sheets.
125
+
126
+ Args:
127
+ file_path: Path to main .kicad_sch file
128
+ filter_pattern: Optional regex pattern to filter net names
129
+ show_hierarchy: Whether to show hierarchical structure (default: True)
130
+
131
+ Returns:
132
+ Complete hierarchical net analysis
133
+ """
134
+ try:
135
+ import re
136
+
137
+ from .pin_analysis import _ensure_netlist
138
+
139
+ parser = SchematicParser(file_path)
140
+ sheets = parser.get_sheets()
141
+
142
+ netlist_path = await _ensure_netlist(Path(file_path))
143
+ if netlist_path is None:
144
+ return "# No Netlist Available\n\nCannot analyze nets without a netlist. Run `generate_netlist()` first."
145
+
146
+ nl = NetlistParser(str(netlist_path))
147
+
148
+ # docstring promises a regex; honour it (was a plain substring check).
149
+ try:
150
+ flt = re.compile(filter_pattern, re.IGNORECASE) if filter_pattern else None
151
+ except re.error:
152
+ flt = None
153
+
154
+ # NetlistNet.pins is a list[(ref, pin)] that is actually populated,
155
+ # unlike SchematicNet.pins which is always empty.
156
+ hierarchical_nets = {}
157
+ for name, net in nl.get_nets().items():
158
+ if flt and not flt.search(name):
159
+ continue
160
+ hierarchical_nets[name] = {
161
+ "location": "Netlist (whole board)",
162
+ "pins": net.pins,
163
+ }
164
+
165
+ if not hierarchical_nets:
166
+ return f"# No Nets Found\n\nNo nets found matching pattern: {filter_pattern if filter_pattern else 'all'}"
167
+
168
+ lines = [
169
+ "# Hierarchical Net Analysis",
170
+ "",
171
+ f"**File:** {file_path}",
172
+ f"**Total nets:** {len(hierarchical_nets)}",
173
+ f"**Filter:** {filter_pattern if filter_pattern else 'none'}",
174
+ f"**Hierarchical sheets:** {len(sheets)}",
175
+ "",
176
+ "## Network Summary",
177
+ "",
178
+ ]
179
+
180
+ power_nets, signal_nets, interface_nets = [], [], []
181
+ for net_name, net_info in hierarchical_nets.items():
182
+ up = net_name.upper()
183
+ if any(p in up for p in ["VDD", "VSS", "GND", "VCC", "VBAT", "VPP", "+3V", "+5V"]):
184
+ power_nets.append((net_name, net_info))
185
+ elif any(i in up for i in ["I2C", "SPI", "UART", "SDA", "SCL", "MOSI", "MISO", "TX", "RX"]):
186
+ interface_nets.append((net_name, net_info))
187
+ else:
188
+ signal_nets.append((net_name, net_info))
189
+
190
+ def _emit(title: str, group: list, limit: int) -> None:
191
+ if not group:
192
+ return
193
+ lines.append(f"### {title}")
194
+ for net_name, net_info in sorted(group)[:limit]:
195
+ lines.append(f"**{net_name}** ({net_info['location']})")
196
+ lines.append(f" Connections: {len(net_info['pins'])} pins")
197
+ lines.append("")
198
+ if len(group) > limit:
199
+ lines.append(f"*... and {len(group) - limit} more*")
200
+ lines.append("")
201
+
202
+ _emit("Power Nets", power_nets, 20)
203
+ _emit("Interface Nets", interface_nets, 30)
204
+ _emit(f"Signal Nets (showing first 20 of {len(signal_nets)})", signal_nets, 20)
205
+
206
+ return "\n".join(lines)
207
+
208
+ except FileNotFoundError:
209
+ return f"Error: File not found: {file_path}"
210
+ except Exception as e:
211
+ return f"Error: {e}"
@@ -0,0 +1,320 @@
1
+ """Netlist generation and analysis tools for KiCad MCP Server."""
2
+
3
+ import hashlib
4
+ import tempfile
5
+ from pathlib import Path
6
+
7
+ from ..parsers.netlist_parser import NetlistParser
8
+ from ..server import mcp
9
+ from ..utils.kicad_cli import run_kicad_cli
10
+
11
+
12
+ def netlist_cache_path(sch_path: Path) -> Path:
13
+ """Deterministic per-schematic netlist path in the temp directory.
14
+
15
+ Keying only on the file stem made two different projects that both have
16
+ e.g. a ``main.kicad_sch`` overwrite each other's exported netlist, so a
17
+ short digest of the resolved schematic path is mixed into the name.
18
+ """
19
+ digest = hashlib.sha256(str(sch_path.resolve()).encode("utf-8")).hexdigest()[:8]
20
+ return Path(tempfile.gettempdir()) / f"{sch_path.stem}_{digest}.xml"
21
+
22
+
23
+ def _find_root_schematic(sch_path: Path) -> Path | None:
24
+ """If sch_path is a sub-sheet, return the root schematic instead.
25
+
26
+ KiCad convention: root schematic has the same stem as the .kicad_pro file.
27
+ Sub-sheets lack the hierarchy context needed to resolve unnamed wire-only
28
+ nets, so we redirect to the root schematic for complete netlist export.
29
+ """
30
+ pro_files = list(sch_path.parent.glob("*.kicad_pro"))
31
+ if len(pro_files) != 1:
32
+ return None
33
+ root_sch = pro_files[0].with_suffix(".kicad_sch")
34
+ if root_sch.exists() and root_sch.resolve() != sch_path.resolve():
35
+ return root_sch
36
+ return None
37
+
38
+
39
+ @mcp.tool()
40
+ async def generate_netlist(
41
+ schematic_path: str,
42
+ ) -> str:
43
+ """Generate KiCad netlist from schematic file.
44
+
45
+ Args:
46
+ schematic_path: Path to .kicad_sch file
47
+
48
+ Returns:
49
+ Path to generated .xml netlist file
50
+ """
51
+ try:
52
+ sch_path = Path(schematic_path)
53
+ if not sch_path.exists():
54
+ return f"❌ Schematic file not found: {schematic_path}"
55
+
56
+ # Detect sub-sheet and redirect to root schematic.
57
+ # Sub-sheets lack hierarchy context to resolve unnamed wire-only nets.
58
+ subsheet_note = ""
59
+ root_sch = _find_root_schematic(sch_path)
60
+ if root_sch:
61
+ subsheet_note = (
62
+ f"\n\n⚠️ **Note:** `{sch_path.name}` is a hierarchical sub-sheet. "
63
+ f"Switched to root schematic `{root_sch.name}` for complete netlist "
64
+ f"(unnamed wire-only nets require full hierarchy context to resolve)."
65
+ )
66
+ sch_path = root_sch
67
+
68
+ # KiCad 7+ uses kicad-cli for headless netlist export
69
+ # Output to temp directory to avoid read-only volume issues
70
+ netlist_path = netlist_cache_path(sch_path)
71
+
72
+ # Try to use KiCad's netlist export
73
+ # Note: Requires a KiCad installation — kicad-cli is located via
74
+ # PATH or the detected install directory (see utils.kicad_cli).
75
+ try:
76
+ # Use kicad-cli for headless export (no display required)
77
+ result = await run_kicad_cli(
78
+ [
79
+ "sch",
80
+ "export",
81
+ "netlist",
82
+ "--format",
83
+ "kicadxml",
84
+ "--output",
85
+ str(netlist_path),
86
+ str(sch_path),
87
+ ],
88
+ timeout=30,
89
+ )
90
+
91
+ if result.returncode == 0 and netlist_path.exists():
92
+ return f"""✅ Netlist generated successfully
93
+
94
+ **Output:** {netlist_path}
95
+
96
+ You can now use netlist-based tools:
97
+ - `trace_netlist_connection()` - Trace component connections via netlist
98
+ - `get_netlist_nets()` - List all nets
99
+ - `get_netlist_components()` - List all components with their nets
100
+ {subsheet_note}"""
101
+ else:
102
+ # Fallback: return instructions
103
+ return f"""⚠️ Automatic netlist generation failed.
104
+
105
+ KiCad command line tools may not be available.
106
+
107
+ **Manual export steps:**
108
+ 1. Open schematic in KiCad: {sch_path}
109
+ 2. Go to: Tools → Generate Netlist
110
+ 3. Select: "KiCad XML" format
111
+ 4. Save to: {netlist_path}
112
+
113
+ **Alternative:**
114
+ Use KiCad Schematic Editor GUI to export netlist.
115
+ """
116
+
117
+ except FileNotFoundError:
118
+ return """⚠️ kicad-cli not found (PATH or KiCad install directory).
119
+
120
+ Please:
121
+ 1. Install KiCad 7+ (https://www.kicad.org/)
122
+ 2. Ensure kicad-cli is in system PATH, or set the KICAD_CLI
123
+ environment variable to its full path
124
+ 3. Or use KiCad GUI to export netlist manually
125
+
126
+ **Manual export:**
127
+ Open schematic → Tools → Generate Netlist → KiCad XML format
128
+ """
129
+
130
+ except Exception as e:
131
+ return f"❌ Error generating netlist: {e}"
132
+
133
+
134
+ @mcp.tool()
135
+ async def trace_netlist_connection(
136
+ netlist_path: str,
137
+ reference: str,
138
+ pin_number: str = "",
139
+ ) -> str:
140
+ """Trace component connections using netlist file (most accurate).
141
+
142
+ Args:
143
+ netlist_path: Path to .xml netlist file
144
+ reference: Component reference (e.g., 'R16')
145
+ pin_number: Optional pin number (e.g., '1'). If empty, trace all pins.
146
+
147
+ Returns:
148
+ Detailed connection information from netlist
149
+ """
150
+ try:
151
+ parser = NetlistParser(netlist_path)
152
+
153
+ if pin_number:
154
+ result = parser.trace_connection(reference, pin_number)
155
+
156
+ if "error" in result:
157
+ return f"❌ {result['error']}"
158
+
159
+ lines = [
160
+ f"## Netlist Connection Trace: {reference}",
161
+ "",
162
+ f"**Pin:** {result['pin']}",
163
+ f"**Net:** {result['net']}",
164
+ "",
165
+ "### Connected Components:",
166
+ "",
167
+ ]
168
+
169
+ if result['connected_to']:
170
+ lines.append("| Reference | Pin |")
171
+ lines.append("|-----------|-----|")
172
+ for ref, pin in result['connected_to']:
173
+ lines.append(f"| {ref} | {pin} |")
174
+ else:
175
+ lines.append("No other components on this net.")
176
+
177
+ return "\n".join(lines)
178
+
179
+ else:
180
+ result = parser.trace_connection(reference)
181
+
182
+ if "error" in result:
183
+ return f"❌ {result['error']}"
184
+
185
+ lines = [
186
+ f"## Netlist Connection Trace: {reference}",
187
+ "",
188
+ f"**Total Nets:** {len(result['nets'])}",
189
+ "",
190
+ ]
191
+
192
+ for net_name, net_info in result['nets'].items():
193
+ lines.append(f"### Net: {net_name}")
194
+ lines.append(f"**Pin:** {net_info['pin']}")
195
+ lines.append("")
196
+
197
+ if net_info['connected_to']:
198
+ lines.append("| Reference | Pin |")
199
+ lines.append("|-----------|-----|")
200
+ for ref, pin in net_info['connected_to']:
201
+ lines.append(f"| {ref} | {pin} |")
202
+ else:
203
+ lines.append("No other components on this net.")
204
+
205
+ lines.append("")
206
+
207
+ return "\n".join(lines)
208
+
209
+ except FileNotFoundError as e:
210
+ return f"❌ File not found: {e}"
211
+ except Exception as e:
212
+ return f"❌ Error tracing netlist: {e}"
213
+
214
+
215
+ @mcp.tool()
216
+ async def get_netlist_nets(
217
+ netlist_path: str,
218
+ filter_pattern: str = "",
219
+ ) -> str:
220
+ """Get all nets from netlist file.
221
+
222
+ Args:
223
+ netlist_path: Path to .xml netlist file
224
+ filter_pattern: Optional regex pattern to filter net names
225
+
226
+ Returns:
227
+ List of all nets with their connections
228
+ """
229
+ try:
230
+ parser = NetlistParser(netlist_path)
231
+ nets = parser.get_nets()
232
+
233
+ lines = [
234
+ f"## Nets from Netlist: {netlist_path}",
235
+ "",
236
+ f"**Total Nets:** {len(nets)}",
237
+ "",
238
+ ]
239
+
240
+ for net_name, net in sorted(nets.items()):
241
+ if filter_pattern:
242
+ import re
243
+ if not re.search(filter_pattern, net_name, re.IGNORECASE):
244
+ continue
245
+
246
+ lines.append(f"### {net_name}")
247
+ lines.append(f"**Code:** {net.code}")
248
+ lines.append(f"**Connections:** {len(net.pins)} pins")
249
+
250
+ if net.pins:
251
+ lines.append("")
252
+ lines.append("| Reference | Pin |")
253
+ lines.append("|-----------|-----|")
254
+ for ref, pin in net.pins[:10]: # Show first 10
255
+ lines.append(f"| {ref} | {pin} |")
256
+ if len(net.pins) > 10:
257
+ lines.append(f"| ... | ({len(net.pins) - 10} more) |")
258
+
259
+ lines.append("")
260
+
261
+ return "\n".join(lines)
262
+
263
+ except FileNotFoundError as e:
264
+ return f"❌ File not found: {e}"
265
+ except Exception as e:
266
+ return f"❌ Error reading netlist: {e}"
267
+
268
+
269
+ @mcp.tool()
270
+ async def get_netlist_components(
271
+ netlist_path: str,
272
+ filter_ref: str = "",
273
+ ) -> str:
274
+ """Get all components from netlist with their network connections.
275
+
276
+ Args:
277
+ netlist_path: Path to .xml netlist file
278
+ filter_ref: Optional reference filter (e.g., 'R', 'U', 'C')
279
+
280
+ Returns:
281
+ List of components with their net connections
282
+ """
283
+ try:
284
+ parser = NetlistParser(netlist_path)
285
+ components = parser.get_components()
286
+
287
+ lines = [
288
+ f"## Components from Netlist: {netlist_path}",
289
+ "",
290
+ f"**Total Components:** {len(components)}",
291
+ "",
292
+ ]
293
+
294
+ for ref, comp in sorted(components.items()):
295
+ if filter_ref and not ref.startswith(filter_ref):
296
+ continue
297
+
298
+ lines.append(f"### {ref}")
299
+ lines.append(f"**Value:** {comp.value}")
300
+ lines.append(f"**Library:** {comp.library}")
301
+ if comp.footprint:
302
+ lines.append(f"**Footprint:** {comp.footprint}")
303
+
304
+ if comp.pins:
305
+ lines.append("")
306
+ lines.append("**Pin Connections:**")
307
+ lines.append("")
308
+ lines.append("| Pin | Net |")
309
+ lines.append("|-----|-----|")
310
+ for pin_num, net_name in sorted(comp.pins.items()):
311
+ lines.append(f"| {pin_num} | {net_name} |")
312
+
313
+ lines.append("")
314
+
315
+ return "\n".join(lines)
316
+
317
+ except FileNotFoundError as e:
318
+ return f"❌ File not found: {e}"
319
+ except Exception as e:
320
+ return f"❌ Error reading netlist: {e}"
@@ -0,0 +1,142 @@
1
+ """Parts registry tools — search and fetch verified KiCad parts.
2
+
3
+ Backed by an open, no-login registry (default: PartReel, https://partreel.com;
4
+ configurable via PARTS_REGISTRY_URL). Lets the assistant pull real, quality-
5
+ gated footprints/symbols/3D models instead of drawing them from scratch.
6
+ """
7
+
8
+ import asyncio
9
+ import json
10
+ import os
11
+ from pathlib import Path
12
+
13
+ from ..server import mcp
14
+ from ..utils.parts_registry import (
15
+ FORMAT_EXTENSIONS,
16
+ RegistryClient,
17
+ RegistryError,
18
+ )
19
+
20
+
21
+ @mcp.tool()
22
+ async def search_parts_registry(query: str, limit: int = 20) -> str:
23
+ """Search an open registry of 21,000+ verified KiCad parts (no login).
24
+
25
+ Matches part id, name, family, category, manufacturer and keywords.
26
+ Example queries: "usb c connector", "0402 capacitor", "nrf9151".
27
+
28
+ Args:
29
+ query: Search terms (all terms must match; '*' wildcards allowed)
30
+ limit: Maximum number of results (default 20)
31
+
32
+ Returns:
33
+ JSON list of matches (id, name, family, page URL)
34
+ """
35
+ client = RegistryClient()
36
+ try:
37
+ hits = await asyncio.to_thread(client.search, query, limit)
38
+ except RegistryError as exc:
39
+ return f"❌ Registry search failed: {exc}"
40
+ if not hits:
41
+ return f"No registry parts matched '{query}'."
42
+ rows = [
43
+ {
44
+ "id": p.get("id"),
45
+ "name": p.get("name"),
46
+ "family": p.get("family"),
47
+ "category": p.get("category"),
48
+ "manufacturer": p.get("manufacturer"),
49
+ "page": p.get("page"),
50
+ }
51
+ for p in hits
52
+ ]
53
+ return json.dumps({"count": len(rows), "parts": rows}, indent=2)
54
+
55
+
56
+ @mcp.tool()
57
+ async def get_registry_part(part_id: str) -> str:
58
+ """Get the full registry record for one part.
59
+
60
+ Includes download URLs (footprint/symbol/3D), datasheet link, dimension
61
+ provenance and field reports from real builds.
62
+
63
+ Args:
64
+ part_id: Registry part id (from search_parts_registry)
65
+
66
+ Returns:
67
+ JSON part record
68
+ """
69
+ client = RegistryClient()
70
+ try:
71
+ part = await asyncio.to_thread(client.get_part, part_id)
72
+ except RegistryError as exc:
73
+ return f"❌ Registry lookup failed: {exc}"
74
+ return json.dumps(part, indent=2)
75
+
76
+
77
+ @mcp.tool()
78
+ async def download_registry_part(
79
+ part_id: str, save_dir: str, formats: list[str] | None = None
80
+ ) -> str:
81
+ """Download a registry part's files into a local directory.
82
+
83
+ Downloads are HTTPS-only, restricted to the registry's own hosts,
84
+ capped at 50 MB per file, and saved with extension-checked filenames.
85
+
86
+ Args:
87
+ part_id: Registry part id (from search_parts_registry)
88
+ save_dir: Existing local directory (e.g. the project library folder)
89
+ formats: Which files — any of "footprint", "symbol", "step",
90
+ "preview" (default: footprint + symbol)
91
+
92
+ Returns:
93
+ Saved file paths, or an error
94
+ """
95
+ formats = formats or ["footprint", "symbol"]
96
+ unknown = [f for f in formats if f not in FORMAT_EXTENSIONS]
97
+ if unknown:
98
+ return f"❌ Unknown formats: {unknown} (allowed: {sorted(FORMAT_EXTENSIONS)})"
99
+
100
+ dest = Path(save_dir)
101
+ if not dest.is_dir():
102
+ return f"❌ Not a directory: {save_dir}"
103
+ # Confine writes to the working dir or the user's home tree
104
+ # (extend via PARTS_REGISTRY_SAVE_ROOTS, comma-separated).
105
+ roots = [Path.cwd(), Path.home()]
106
+ roots += [
107
+ Path(r.strip())
108
+ for r in os.environ.get("PARTS_REGISTRY_SAVE_ROOTS", "").split(",")
109
+ if r.strip()
110
+ ]
111
+ real = dest.resolve()
112
+ if not any(real == r.resolve() or r.resolve() in real.parents for r in roots):
113
+ return f"❌ save_dir outside allowed roots (cwd/home): {save_dir}"
114
+
115
+ client = RegistryClient()
116
+ try:
117
+ part = await asyncio.to_thread(client.get_part, part_id)
118
+ except RegistryError as exc:
119
+ return f"❌ Registry lookup failed: {exc}"
120
+
121
+ files = (part.get("files") if isinstance(part, dict) else None) or {}
122
+ saved, missing = [], []
123
+ for fmt in formats:
124
+ url = files.get(fmt)
125
+ if not url:
126
+ missing.append(fmt)
127
+ continue
128
+ try:
129
+ path = await asyncio.to_thread(
130
+ client.download_asset, url, str(real), part_id, fmt
131
+ )
132
+ saved.append(path)
133
+ except (RegistryError, OSError) as exc:
134
+ # OSError covers disk-full / permission-denied / Windows reserved
135
+ # names — surface the partial result instead of losing it.
136
+ return f"❌ Download failed for {fmt}: {exc} (saved so far: {saved})"
137
+ out = {
138
+ "saved": saved,
139
+ "not_available": missing,
140
+ "license": part.get("license") if isinstance(part, dict) else None,
141
+ }
142
+ return json.dumps(out, indent=2)