@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,674 @@
1
+ """Schematic editing tools for KiCad MCP Server."""
2
+
3
+ import re
4
+ import uuid
5
+ from pathlib import Path
6
+
7
+ from ..server import mcp
8
+
9
+
10
+ def _get_date_string() -> str:
11
+ """Get current date in ISO format."""
12
+ from datetime import datetime
13
+
14
+ return datetime.now().strftime("%Y-%m-%d")
15
+
16
+
17
+ # KiCad standard library symbols mapping
18
+ KICAD_STANDARD_SYMBOLS = {
19
+ "ESP32-S3-WROOM-1": {
20
+ "library": "RF_Module",
21
+ "symbol": "ESP32-S3-WROOM-1",
22
+ },
23
+ "SSD1306": {
24
+ "library": "Display_Graphic",
25
+ "symbol": "OLED-128O064D",
26
+ },
27
+ "MPU6050": {
28
+ "library": "Sensor_Motion",
29
+ "symbol": "MPU-6050",
30
+ },
31
+ }
32
+
33
+ # Pin definitions for common symbols (fallback when library not found)
34
+ SYMBOL_PINS = {
35
+ "Device:R": [(1, "passive", ""), (2, "passive", "")],
36
+ "Device:LED": [(1, "passive", "K"), (2, "passive", "A")],
37
+ "Device:C": [(1, "passive", ""), (2, "passive", "")],
38
+ "RF_Module:ESP32-S3-WROOM-1": [(1, "input", "GPIO0"), (2, "input", "GPIO1")],
39
+ "Display_Graphic:OLED-128O064D": [(1, "input", "GND"), (2, "input", "SCL")],
40
+ "Sensor_Motion:MPU-6050": [(1, "input", "VDD"), (2, "input", "GND")],
41
+ }
42
+
43
+
44
+ def get_pins_for_symbol(library_name: str, symbol_name: str) -> list[tuple[int, str, str]]:
45
+ """Get pin definitions for a symbol."""
46
+ lib_id = f"{library_name}:{symbol_name}"
47
+ if lib_id in SYMBOL_PINS:
48
+ return SYMBOL_PINS[lib_id]
49
+ return [(1, "passive", ""), (2, "passive", "")]
50
+
51
+
52
+ def _find_symbol_library_file(library_name: str) -> Path | None:
53
+ """Find the .kicad_sym file for a KiCad symbol library."""
54
+ from ..utils.kicad_version import get_kicad_symbol_dir
55
+
56
+ sym_dir = get_kicad_symbol_dir()
57
+ if sym_dir:
58
+ sym_path = sym_dir / f"{library_name}.kicad_sym"
59
+ if sym_path.exists():
60
+ return sym_path
61
+ return None
62
+
63
+
64
+ def _extract_symbol_from_kicad_sym(kicad_sym_path: Path, symbol_name: str) -> str | None:
65
+ """Extract a complete symbol block from a .kicad_sym library file."""
66
+ try:
67
+ content = kicad_sym_path.read_text(encoding="utf-8", errors="replace")
68
+ except OSError:
69
+ return None
70
+
71
+ # Match the top-level symbol definition (not sub-symbols like "R_0_1")
72
+ pattern = r'\(symbol\s+"' + re.escape(symbol_name) + r'"(?!\w)'
73
+ match = re.search(pattern, content)
74
+ if not match:
75
+ return None
76
+
77
+ # Extract balanced block using parenthesis counting
78
+ start = match.start()
79
+ depth = 0
80
+ for i in range(start, len(content)):
81
+ if content[i] == "(":
82
+ depth += 1
83
+ elif content[i] == ")":
84
+ depth -= 1
85
+ if depth == 0:
86
+ return content[start : i + 1]
87
+ return None
88
+
89
+
90
+ def _convert_symbol_to_lib_symbols_format(
91
+ symbol_block: str, library_name: str, symbol_name: str
92
+ ) -> str:
93
+ """Convert a symbol block from .kicad_sym to lib_symbols format.
94
+
95
+ Key changes:
96
+ - Prefix symbol name with library: "R" -> "Device:R"
97
+ - Strip kicad_sym-specific attributes: show_name, do_not_autoplace,
98
+ in_pos_files, duplicate_pin_numbers_are_jumpers
99
+ - Keep: exclude_from_sim, in_bom, on_board, embedded_fonts
100
+ - Normalize indentation
101
+ """
102
+ result = symbol_block
103
+
104
+ # Prefix the symbol name with library name
105
+ result = re.sub(
106
+ r'\(symbol\s+"' + re.escape(symbol_name) + r'"',
107
+ f'(symbol "{library_name}:{symbol_name}"',
108
+ result,
109
+ count=1,
110
+ )
111
+
112
+ # Also prefix sub-symbol references (e.g. "R_0_1" stays as-is since
113
+ # they are nested and don't need the library prefix)
114
+
115
+ # Strip kicad_sym-specific attributes
116
+ result = re.sub(r"\n?\s*\(show_name\s+\w+\)", "", result)
117
+ result = re.sub(r"\n?\s*\(do_not_autoplace\s+\w+\)", "", result)
118
+ result = re.sub(r"\n?\s*\(in_pos_files\s+\w+\)", "", result)
119
+ result = re.sub(r"\n?\s*\(duplicate_pin_numbers_are_jumpers\s+\w+\)", "", result)
120
+
121
+ # Normalize indentation: strip leading whitespace and re-indent with tabs
122
+ lines = result.split("\n")
123
+ # Find minimum indentation (ignoring empty lines)
124
+ min_indent = float("inf")
125
+ for line in lines:
126
+ stripped = line.lstrip()
127
+ if stripped:
128
+ indent = len(line) - len(stripped)
129
+ min_indent = min(min_indent, indent)
130
+
131
+ if min_indent == float("inf"):
132
+ min_indent = 0
133
+
134
+ normalized = []
135
+ for line in lines:
136
+ stripped = line.strip()
137
+ if not stripped:
138
+ normalized.append("")
139
+ continue
140
+ original_indent = len(line) - len(line.lstrip())
141
+ # Convert to tab-based indentation (base = 2 tabs inside lib_symbols)
142
+ tab_depth = max(0, (original_indent - min_indent)) // 2
143
+ normalized.append("\t\t" + "\t" * tab_depth + stripped)
144
+
145
+ return "\n".join(normalized)
146
+
147
+
148
+ def _symbol_exists_in_lib_symbols(content: str, lib_id: str) -> bool:
149
+ """Check if a symbol definition already exists in the lib_symbols section."""
150
+ ls_match = re.search(r"\(lib_symbols\b", content)
151
+ if not ls_match:
152
+ return False
153
+ # Search from lib_symbols start for the symbol
154
+ section = content[ls_match.start() :]
155
+ return bool(re.search(r'\(symbol\s+"' + re.escape(lib_id) + r'"', section))
156
+
157
+
158
+ def _insert_symbol_into_lib_symbols(content: str, symbol_text: str) -> str:
159
+ """Insert a symbol definition into the lib_symbols section of a schematic."""
160
+ ls_match = re.search(r"\(lib_symbols\b", content)
161
+ if not ls_match:
162
+ # No lib_symbols section - create one before the first schematic element
163
+ insert_pattern = r"(\n\t\((?:symbol|wire|label|global_label|junction|text|sheet|sheet_instances)\b)"
164
+ m = re.search(insert_pattern, content)
165
+ if m:
166
+ insert_pos = m.start()
167
+ new_section = f"\n\t(lib_symbols\n{symbol_text}\n\t)"
168
+ return content[:insert_pos] + new_section + content[insert_pos:]
169
+ # Last resort: insert before final closing paren
170
+ stripped = content.rstrip()
171
+ if stripped.endswith(")"):
172
+ return stripped[:-1] + f"\n\t(lib_symbols\n{symbol_text}\n\t)\n)\n"
173
+ return content
174
+
175
+ # lib_symbols exists - find its closing paren
176
+ start = ls_match.start()
177
+ depth = 0
178
+ end_pos = start
179
+ for i in range(start, len(content)):
180
+ if content[i] == "(":
181
+ depth += 1
182
+ elif content[i] == ")":
183
+ depth -= 1
184
+ if depth == 0:
185
+ end_pos = i
186
+ break
187
+
188
+ # Insert before the closing paren of lib_symbols
189
+ return content[:end_pos] + "\n" + symbol_text + "\n" + content[end_pos:]
190
+
191
+
192
+ def _extract_pins_from_symbol_block(symbol_block: str) -> list[tuple[str, str, str]]:
193
+ """Extract pin numbers, types, and names from a symbol definition."""
194
+ pins = []
195
+ pin_pattern = re.compile(
196
+ r"\(pin\s+(\w+)\s+\w+\s+[\s\S]*?"
197
+ r'\(name\s+"([^"]*)"[\s\S]*?\)'
198
+ r'\s*\(number\s+"([^"]*)"',
199
+ )
200
+ for m in pin_pattern.finditer(symbol_block):
201
+ elec_type = m.group(1)
202
+ pin_name = m.group(2)
203
+ pin_number = m.group(3)
204
+ pins.append((pin_number, elec_type, pin_name))
205
+ return pins
206
+
207
+
208
+ # ---------------------------------------------------------------------------
209
+ # Pin anchor geometry (for label placement)
210
+ # ---------------------------------------------------------------------------
211
+
212
+ _PIN_GEOM_RE = re.compile(
213
+ r"\(pin\s+(?P<etype>\w+)\s+\w+\s+"
214
+ r"\(at\s+(?P<x>-?[\d.]+)\s+(?P<y>-?[\d.]+)\s+(?P<angle>-?[\d.]+)\)"
215
+ r"[\s\S]*?"
216
+ r'\(number\s+"(?P<number>[^"]*)"'
217
+ )
218
+
219
+
220
+ def _lib_symbols_block(content: str) -> str:
221
+ """Text of the (lib_symbols ...) section, or ''."""
222
+ m = re.search(r"\(lib_symbols\b", content)
223
+ if not m:
224
+ return ""
225
+ start = m.start()
226
+ depth = 0
227
+ for i in range(start, len(content)):
228
+ if content[i] == "(":
229
+ depth += 1
230
+ elif content[i] == ")":
231
+ depth -= 1
232
+ if depth == 0:
233
+ return content[start : i + 1]
234
+ return ""
235
+
236
+
237
+ def _symbol_def_block(lib_block: str, lib_id: str) -> str | None:
238
+ m = re.search(r'\(symbol\s+"' + re.escape(lib_id) + r'"(?!\w)', lib_block)
239
+ if not m:
240
+ return None
241
+ start = m.start()
242
+ depth = 0
243
+ for i in range(start, len(lib_block)):
244
+ if lib_block[i] == "(":
245
+ depth += 1
246
+ elif lib_block[i] == ")":
247
+ depth -= 1
248
+ if depth == 0:
249
+ return lib_block[start : i + 1]
250
+ return None
251
+
252
+
253
+ def _lib_pin_offset(symbol_block: str, lib_id: str, unit: int, pin: str) -> tuple[float, float] | None:
254
+ """Lib-space offset of one pin, honouring the unit sub-symbol filter.
255
+
256
+ Sub-symbols are named "<lib_id>_<unit>_<convert>" (or the bare
257
+ "<name>_<unit>_<convert>" form); unit 0 = common to all units.
258
+ """
259
+ base = lib_id.split(":")[-1]
260
+ for sub_m in re.finditer(r'\(symbol\s+"([^"]+)"', symbol_block):
261
+ sub_name = sub_m.group(1)
262
+ if sub_name == lib_id:
263
+ continue
264
+ sub_unit = None
265
+ for prefix in (lib_id, base):
266
+ mm = re.match(re.escape(prefix) + r"_(\d+)_(\d+)$", sub_name)
267
+ if mm:
268
+ sub_unit = int(mm.group(1))
269
+ break
270
+ if sub_unit is None or sub_unit not in (0, unit):
271
+ continue
272
+ # balanced span of this sub-symbol block
273
+ start = sub_m.start()
274
+ depth = 0
275
+ for i in range(start, len(symbol_block)):
276
+ if symbol_block[i] == "(":
277
+ depth += 1
278
+ elif symbol_block[i] == ")":
279
+ depth -= 1
280
+ if depth == 0:
281
+ break
282
+ for pm in _PIN_GEOM_RE.finditer(symbol_block[start : i + 1]):
283
+ if pm.group("number") == pin:
284
+ return float(pm.group("x")), float(pm.group("y"))
285
+ return None
286
+
287
+
288
+ def _pin_anchor(content: str, reference: str, pin: str) -> tuple[float, float, int] | None:
289
+ """Absolute (x, y) and outward angle of a placed component's pin.
290
+
291
+ Symbol libraries use a Y-UP axis while schematic sheets store Y growing
292
+ DOWNWARD, so the pin offset's Y is negated on placement; the returned
293
+ angle (0/90/180/270) points away from the symbol body so labels fan out.
294
+ """
295
+ lib = _lib_symbols_block(content)
296
+ inst_re = re.compile(
297
+ r'\(symbol\s+\(lib_id\s+"(?P<lib_id>[^"]+)"\)\s+'
298
+ r"\(at\s+(?P<x>-?[\d.]+)\s+(?P<y>-?[\d.]+)\s+(?P<angle>-?[\d.]+)\)\s+"
299
+ r"\(unit\s+(?P<unit>\d+)\)"
300
+ )
301
+ body = content.replace(lib, "") if lib else content
302
+ for m in inst_re.finditer(body):
303
+ tail = body[m.start() : m.start() + 4000]
304
+ ref_m = re.search(r'\(property\s+"Reference"\s+"([^"]*)"', tail)
305
+ if not ref_m or ref_m.group(1) != reference:
306
+ continue
307
+ ix, iy, iangle, unit = (
308
+ float(m.group("x")),
309
+ float(m.group("y")),
310
+ float(m.group("angle")),
311
+ int(m.group("unit")),
312
+ )
313
+ block = _symbol_def_block(lib, m.group("lib_id"))
314
+ if block is None:
315
+ return None
316
+ offset = _lib_pin_offset(block, m.group("lib_id"), unit, pin)
317
+ if offset is None:
318
+ return None
319
+ dx, dy = offset
320
+ r = int(round(iangle)) % 360
321
+ if r == 90:
322
+ dx, dy = -dy, dx
323
+ elif r == 180:
324
+ dx, dy = -dx, -dy
325
+ elif r == 270:
326
+ dx, dy = dy, -dx
327
+ sx, sy = dx, -dy # lib Y-UP -> sheet Y-DOWN
328
+ if abs(sx) >= abs(sy):
329
+ angle = 0 if sx >= 0 else 180
330
+ else:
331
+ angle = 90 if sy < 0 else 270
332
+ return ix + sx, iy + sy, angle
333
+ return None
334
+
335
+
336
+ def _append_top_level(content: str, entry: str) -> str:
337
+ stripped = content.rstrip()
338
+ if not stripped.endswith(")"):
339
+ return content + "\n" + entry + "\n"
340
+ return stripped[:-1] + entry + "\n)\n"
341
+
342
+
343
+ _LABEL_SHAPES = ("input", "output", "bidirectional", "tri_state", "passive")
344
+
345
+ _JUSTIFY = {0: "left", 180: "right", 90: "bottom", 270: "top"}
346
+
347
+
348
+ def _resolve_label_target(
349
+ file_path: str, reference: str, pin: str, x: float | None, y: float | None
350
+ ) -> tuple[str, float, float, int] | str:
351
+ """Common target resolution for label tools: exact pin anchor by default,
352
+ explicit coordinates when pin lookups are not applicable."""
353
+ content = Path(file_path).read_text(encoding="utf-8", errors="replace")
354
+ if x is not None and y is not None:
355
+ return content, x, y, 0
356
+ anchor = _pin_anchor(content, reference, pin)
357
+ if anchor is None:
358
+ return (
359
+ f"❌ Pin '{pin}' not found on component '{reference}' — place the "
360
+ f"component first, or pass explicit x/y coordinates."
361
+ )
362
+ return content, anchor[0], anchor[1], anchor[2]
363
+
364
+
365
+ @mcp.tool()
366
+ async def add_hierarchical_label(
367
+ file_path: str,
368
+ reference: str,
369
+ pin: str,
370
+ label_name: str = "",
371
+ shape: str = "input",
372
+ x: float | None = None,
373
+ y: float | None = None,
374
+ ) -> str:
375
+ """Add a hierarchical label on a component pin (cross-sheet connection).
376
+
377
+ Hierarchical labels are how signals cross sheet boundaries in KiCad: a
378
+ label placed in a sub-sheet appears as a same-named pin on the parent
379
+ sheet's sheet symbol, connecting the two. Placing them by hand for
380
+ every cross-sheet signal is the classic tedium this automates.
381
+
382
+ The label anchors EXACTLY on the pin (computed from the embedded
383
+ symbol geometry incl. rotation and the library-vs-sheet Y flip), so
384
+ the connection is electrically valid — verify with generate_netlist.
385
+
386
+ Args:
387
+ file_path: Path to the .kicad_sch file (the SUB-sheet for signals
388
+ going up to its parent).
389
+ reference: Component reference (e.g. 'U1').
390
+ pin: Pin number.
391
+ label_name: Label text; defaults to the reference+pin when empty.
392
+ shape: Electrical direction: input, output, bidirectional,
393
+ tri_state or passive (ERC uses this).
394
+ x/y: Explicit anchor coordinates (overrides reference/pin).
395
+
396
+ Returns:
397
+ Confirmation with the exact anchor position.
398
+ """
399
+ if shape not in _LABEL_SHAPES:
400
+ return f"❌ Invalid shape '{shape}'. Options: {', '.join(_LABEL_SHAPES)}"
401
+ try:
402
+ path = Path(file_path)
403
+ if not path.exists():
404
+ return f"Error: File {file_path} does not exist"
405
+
406
+ resolved = _resolve_label_target(file_path, reference, pin, x, y)
407
+ if isinstance(resolved, str):
408
+ return resolved
409
+ content, ax, ay, angle = resolved
410
+
411
+ name = label_name or f"{reference}.{pin}"
412
+ justify = _JUSTIFY.get(angle, "left")
413
+ entry = (
414
+ f'\t(hierarchical_label "{name}" (shape {shape}) (at {round(ax, 3)} {round(ay, 3)} {angle})\n'
415
+ f"\t\t(effects (font (size 1.27 1.27)) (justify {justify}))\n"
416
+ f'\t\t(uuid "{uuid.uuid4()}")\n'
417
+ f"\t)\n"
418
+ )
419
+ path.write_text(_append_top_level(content, entry), encoding="utf-8")
420
+ return (
421
+ f"✅ Hierarchical label '{name}' ({shape}) anchored on {reference}.{pin} "
422
+ f"at ({round(ax, 3)}, {round(ay, 3)}) angle {angle}°"
423
+ )
424
+ except Exception as e:
425
+ import traceback
426
+
427
+ return f"Error adding hierarchical label: {e}\n\n{traceback.format_exc()}"
428
+
429
+
430
+ @mcp.tool()
431
+ async def add_global_label(
432
+ file_path: str,
433
+ reference: str,
434
+ pin: str,
435
+ label_name: str,
436
+ shape: str = "input",
437
+ x: float | None = None,
438
+ y: float | None = None,
439
+ ) -> str:
440
+ """Add a global label on a component pin (project-wide connection).
441
+
442
+ Global labels connect same-named signals anywhere in the whole
443
+ hierarchy regardless of sheets — useful for power-style rails and
444
+ cross-cutting signals where a hierarchical parent/child chain would
445
+ be awkward.
446
+
447
+ Args:
448
+ file_path: Path to the .kicad_sch file.
449
+ reference: Component reference (e.g. 'U1').
450
+ pin: Pin number.
451
+ label_name: Global net name (required — globals are named nets).
452
+ shape: Electrical direction: input, output, bidirectional,
453
+ tri_state or passive.
454
+ x/y: Explicit anchor coordinates (overrides reference/pin).
455
+
456
+ Returns:
457
+ Confirmation with the exact anchor position.
458
+ """
459
+ if shape not in _LABEL_SHAPES:
460
+ return f"❌ Invalid shape '{shape}'. Options: {', '.join(_LABEL_SHAPES)}"
461
+ try:
462
+ path = Path(file_path)
463
+ if not path.exists():
464
+ return f"Error: File {file_path} does not exist"
465
+
466
+ resolved = _resolve_label_target(file_path, reference, pin, x, y)
467
+ if isinstance(resolved, str):
468
+ return resolved
469
+ content, ax, ay, angle = resolved
470
+
471
+ justify = _JUSTIFY.get(angle, "left")
472
+ entry = (
473
+ f'\t(global_label "{label_name}" (shape {shape}) (at {round(ax, 3)} {round(ay, 3)} {angle})\n'
474
+ f"\t\t(effects (font (size 1.27 1.27)) (justify {justify}))\n"
475
+ f'\t\t(uuid "{uuid.uuid4()}")\n'
476
+ f'\t\t(property "Intersheetrefs" "${{INTERSHEET_REFS}}" (at 0 0 0)\n'
477
+ f"\t\t\t(effects (font (size 1.27 1.27)) hide)\n"
478
+ f"\t\t)\n"
479
+ f"\t)\n"
480
+ )
481
+ path.write_text(_append_top_level(content, entry), encoding="utf-8")
482
+ return (
483
+ f"✅ Global label '{label_name}' ({shape}) anchored on {reference}.{pin} "
484
+ f"at ({round(ax, 3)}, {round(ay, 3)}) angle {angle}°"
485
+ )
486
+ except Exception as e:
487
+ import traceback
488
+
489
+ return f"Error adding global label: {e}\n\n{traceback.format_exc()}"
490
+
491
+
492
+ @mcp.tool()
493
+ async def add_component_from_library(
494
+ file_path: str,
495
+ library_name: str,
496
+ symbol_name: str,
497
+ reference: str,
498
+ value: str,
499
+ footprint: str = "",
500
+ x: float = 100,
501
+ y: float = 100,
502
+ unit: int = 1,
503
+ ) -> str:
504
+ """Add a component from KiCad's built-in library to the schematic."""
505
+ try:
506
+ path = Path(file_path)
507
+ if not path.exists():
508
+ return f"Error: File {file_path} does not exist"
509
+
510
+ content = path.read_text()
511
+ comp_uuid = str(uuid.uuid4())
512
+ lib_id = f"{library_name}:{symbol_name}"
513
+
514
+ # --- Insert symbol definition into lib_symbols if needed ---
515
+ lib_inserted = False
516
+ lib_file = _find_symbol_library_file(library_name)
517
+ if lib_file:
518
+ symbol_block = _extract_symbol_from_kicad_sym(lib_file, symbol_name)
519
+ if symbol_block:
520
+ if not _symbol_exists_in_lib_symbols(content, lib_id):
521
+ converted = _convert_symbol_to_lib_symbols_format(
522
+ symbol_block, library_name, symbol_name
523
+ )
524
+ content = _insert_symbol_into_lib_symbols(content, converted)
525
+ lib_inserted = True
526
+
527
+ # --- Determine pins ---
528
+ pins = []
529
+ if lib_file:
530
+ symbol_block = _extract_symbol_from_kicad_sym(lib_file, symbol_name)
531
+ if symbol_block:
532
+ pins = _extract_pins_from_symbol_block(symbol_block)
533
+ if not pins:
534
+ pins = get_pins_for_symbol(library_name, symbol_name)
535
+
536
+ pin_entries = []
537
+ for pin_num, _pin_type, _pin_name in pins:
538
+ pin_uuid = str(uuid.uuid4())
539
+ pin_entries.append(f' (pin "{pin_num}" (uuid "{pin_uuid}"))')
540
+ pins_str = "\n".join(pin_entries) if pin_entries else ""
541
+
542
+ # --- Insert component instance ---
543
+ # NOTE: schematic symbol-instance (property ...) blocks must NOT contain
544
+ # a nested (uuid ...) field in this KiCad 9 file format (generator_version
545
+ # "9.0", version 20250114) -- confirmed against KiCad's own
546
+ # template/Arduino_Mega/Arduino_Mega.kicad_sch. Adding one is silently
547
+ # accepted by lenient parsers (sexpdata, kicad-skip) but makes KiCad's own
548
+ # strict loader (kicad-cli and the GUI) refuse to open the file at all.
549
+ component_entry = f''' (symbol (lib_id "{lib_id}") (at {x} {y} 0) (unit {unit})
550
+ (exclude_from_sim no) (in_bom yes) (on_board yes) (dnp no)
551
+ (uuid "{comp_uuid}")
552
+ (property "Reference" "{reference}" (at {x} {y - 5} 0)
553
+ (effects (font (size 1.27 1.27)))
554
+ )
555
+ (property "Value" "{value}" (at {x} {y + 2.54} 0)
556
+ (effects (font (size 1.27 1.27)))
557
+ )
558
+ (property "Footprint" "{footprint}" (at {x} {y + 5.08} 0)
559
+ (effects (font (size 1.27 1.27)) (hide yes))
560
+ )
561
+ {pins_str}
562
+ )'''
563
+
564
+ # Refuse to write a symbol instance whose lib_id has no matching
565
+ # definition in (lib_symbols) — that yields a broken symbol in KiCad.
566
+ if not lib_inserted:
567
+ return (f"❌ Cannot add component: symbol definition for "
568
+ f'"{lib_id}" is unavailable (library "{library_name}" not '
569
+ f"found, or symbol not in it). Install the library or pick a "
570
+ f"symbol that exists.")
571
+
572
+ if content.rstrip().endswith(")"):
573
+ content = content.rstrip()
574
+ if content.endswith(")"):
575
+ content = content[:-1] + component_entry + "\n)\n"
576
+ else:
577
+ content = content + "\n" + component_entry + "\n"
578
+ else:
579
+ content = content + "\n" + component_entry + "\n"
580
+
581
+ path.write_text(content)
582
+
583
+ return f"""✅ Component added successfully!
584
+
585
+ **File:** {file_path}
586
+ **Library:** {library_name}
587
+ **Symbol:** {symbol_name}
588
+ **Reference:** {reference}
589
+ **Value:** {value}
590
+ **Position:** ({x}, {y})
591
+ **Lib ID:** {lib_id}
592
+ **Symbol definition:** inserted into lib_symbols
593
+ **Pins:** {len(pins)} pins detected
594
+
595
+ """
596
+
597
+ except Exception as e:
598
+ import traceback
599
+
600
+ return f"Error adding component: {e}\n\n{traceback.format_exc()}"
601
+
602
+
603
+ @mcp.tool()
604
+ async def add_wire(
605
+ file_path: str,
606
+ points: list[tuple[float, float]],
607
+ ) -> str:
608
+ """Add a wire (connection line) to the schematic."""
609
+ try:
610
+ path = Path(file_path)
611
+ if not path.exists():
612
+ return f"Error: File {file_path} does not exist"
613
+
614
+ content = path.read_text()
615
+ pts_str = " ".join([f"(xy {x} {y})" for x, y in points])
616
+ wire_entry = f""" (wire (pts {pts_str})
617
+ (stroke (width 0) (type default))
618
+ (uuid "{uuid.uuid4()}")
619
+ )"""
620
+
621
+ if content.rstrip().endswith(")"):
622
+ content = content.rstrip()
623
+ if content.endswith(")"):
624
+ content = content[:-1] + wire_entry + "\n)\n"
625
+ else:
626
+ content = content + "\n" + wire_entry + "\n)"
627
+ else:
628
+ content = content + "\n" + wire_entry + "\n"
629
+
630
+ path.write_text(content)
631
+
632
+ return "✅ Wire added"
633
+ except Exception as e:
634
+ import traceback
635
+
636
+ return f"Error adding wire: {e}\n\n{traceback.format_exc()}"
637
+
638
+
639
+ @mcp.tool()
640
+ async def add_label(
641
+ file_path: str,
642
+ text: str,
643
+ x: float,
644
+ y: float,
645
+ orientation: float = 0,
646
+ ) -> str:
647
+ """Add a local label (text label) to the schematic."""
648
+ try:
649
+ path = Path(file_path)
650
+ if not path.exists():
651
+ return f"Error: File {file_path} does not exist"
652
+
653
+ content = path.read_text()
654
+ label_uuid = str(uuid.uuid4())
655
+ label_entry = f""" (label "{text}" (at {x} {y} {orientation})
656
+ (effects (font (size 1.27 1.27)) (justify left))
657
+ (uuid "{label_uuid}")
658
+ )"""
659
+
660
+ if content.rstrip().endswith(")"):
661
+ content = content.rstrip()
662
+ if content.endswith(")"):
663
+ content = content[:-1] + label_entry + "\n)\n"
664
+ else:
665
+ content = content + "\n" + label_entry + "\n)"
666
+ else:
667
+ content = content + "\n" + label_entry + "\n"
668
+
669
+ path.write_text(content)
670
+ return f"✅ Label '{text}' added at ({x}, {y})"
671
+ except Exception as e:
672
+ import traceback
673
+
674
+ return f"Error adding label: {e}\n\n{traceback.format_exc()}"