@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,626 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ ================================================================================
4
+ AOS MCP Multi-Harness Verification & Connection Test Suite
5
+ ================================================================================
6
+ Implements the Model Context Protocol (MCP) JSON-RPC 2.0 protocol over stdio.
7
+ Validates server initialization, handshake, tool enumeration, schema integrity,
8
+ and clean termination without requiring any third-party Python packages.
9
+
10
+ Features:
11
+ - Pure Python standard library (zero external dependencies)
12
+ - Supports JSON config (Antigravity, Claude Code) and TOML config (Codex)
13
+ - Resilient non-blocking stdio I/O handling newline-delimited & framed JSON-RPC
14
+ - Pagination handling for tools/list via nextCursor
15
+ - In-depth tool schema validation
16
+ - Pretty tabular output and JSON report generation
17
+ - Antigravity lazy-tool schema generator
18
+ ================================================================================
19
+ """
20
+
21
+ import sys
22
+ import os
23
+ import json
24
+ import time
25
+ import argparse
26
+ import subprocess
27
+ import select
28
+ from pathlib import Path
29
+ from typing import Dict, Any, List, Optional, Tuple
30
+
31
+ # Attempt to import tomllib (Python 3.11+) or fallback to simple TOML parser
32
+ try:
33
+ import tomllib # type: ignore
34
+ except ImportError:
35
+ tomllib = None # type: ignore
36
+
37
+
38
+ PROTOCOL_VERSION = "2024-11-05"
39
+
40
+
41
+ class ToolSchemaValidationError(Exception):
42
+ """Raised when a tool definition fails MCP schema validation."""
43
+ pass
44
+
45
+
46
+ class MCPStdioClient:
47
+ """Zero-dependency stdio JSON-RPC client for Model Context Protocol servers."""
48
+
49
+ def __init__(
50
+ self,
51
+ command: str,
52
+ args: Optional[List[str]] = None,
53
+ env: Optional[Dict[str, str]] = None,
54
+ cwd: Optional[str] = None,
55
+ timeout: float = 15.0,
56
+ ):
57
+ self.command = command
58
+ self.args = args or []
59
+ self.env = {**os.environ, **(env or {})}
60
+ self.cwd = cwd or os.getcwd()
61
+ self.timeout = timeout
62
+ self.process: Optional[subprocess.Popen] = None
63
+ self.msg_id = 1
64
+ self.server_info: Dict[str, Any] = {}
65
+ self.server_instructions: str = ""
66
+ self.protocol_version: str = ""
67
+ self.capabilities: Dict[str, Any] = {}
68
+
69
+ def start(self):
70
+ """Spawns the MCP server subprocess."""
71
+ cmd = [self.command] + self.args
72
+ try:
73
+ self.process = subprocess.Popen(
74
+ cmd,
75
+ stdin=subprocess.PIPE,
76
+ stdout=subprocess.PIPE,
77
+ stderr=subprocess.PIPE,
78
+ text=True,
79
+ bufsize=0,
80
+ cwd=self.cwd,
81
+ env=self.env,
82
+ )
83
+ except Exception as exc:
84
+ raise RuntimeError(f"Failed to spawn MCP server command '{cmd}': {exc}") from exc
85
+
86
+ def send_message(self, message: Dict[str, Any]):
87
+ """Sends a JSON-RPC message over stdio."""
88
+ if not self.process or not self.process.stdin:
89
+ raise RuntimeError("MCP server process is not running")
90
+ payload = json.dumps(message)
91
+ # We send standard newline-delimited JSON payload
92
+ self.process.stdin.write(f"{payload}\n")
93
+ self.process.stdin.flush()
94
+
95
+ def read_message(self, timeout: Optional[float] = None) -> Dict[str, Any]:
96
+ """Reads a JSON-RPC response, supporting newline-delimited and Content-Length framing."""
97
+ if not self.process or not self.process.stdout:
98
+ raise RuntimeError("MCP server process is not running")
99
+
100
+ wait_time = timeout if timeout is not None else self.timeout
101
+ start_time = time.time()
102
+ content_length = None
103
+
104
+ while time.time() - start_time < wait_time:
105
+ # Check if process died
106
+ if self.process.poll() is not None:
107
+ stderr_output = ""
108
+ if self.process.stderr:
109
+ try:
110
+ stderr_output = self.process.stderr.read()
111
+ except Exception:
112
+ pass
113
+ raise RuntimeError(
114
+ f"Server process terminated unexpectedly with exit code {self.process.returncode}. Stderr: {stderr_output}"
115
+ )
116
+
117
+ # Use select on UNIX systems for non-blocking read
118
+ rlist, _, _ = select.select([self.process.stdout], [], [], 0.1)
119
+ if not rlist:
120
+ continue
121
+
122
+ line = self.process.stdout.readline()
123
+ if not line:
124
+ continue
125
+
126
+ # Check for HTTP/LSP style Content-Length header
127
+ stripped = line.strip()
128
+ if line.lower().startswith("content-length:"):
129
+ try:
130
+ content_length = int(line.split(":", 1)[1].strip())
131
+ except ValueError:
132
+ pass
133
+ continue
134
+ elif stripped == "" and content_length is not None:
135
+ # Header separator, read exact body
136
+ body = self.process.stdout.read(content_length)
137
+ return json.loads(body)
138
+ elif stripped.startswith("{") and stripped.endswith("}"):
139
+ # Direct newline-delimited JSON
140
+ try:
141
+ return json.loads(stripped)
142
+ except json.JSONDecodeError:
143
+ continue
144
+
145
+ # Timeout reached: collect stderr if available
146
+ stderr_info = ""
147
+ if self.process.stderr:
148
+ try:
149
+ rlist, _, _ = select.select([self.process.stderr], [], [], 0.05)
150
+ if rlist:
151
+ stderr_info = self.process.stderr.read()
152
+ except Exception:
153
+ pass
154
+ raise TimeoutError(
155
+ f"Timed out after {wait_time:.1f}s waiting for MCP server response. Stderr: {stderr_info.strip()}"
156
+ )
157
+
158
+ def initialize(self) -> Dict[str, Any]:
159
+ """Performs the MCP initialize handshake and initialized notification."""
160
+ init_req = {
161
+ "jsonrpc": "2.0",
162
+ "id": self.msg_id,
163
+ "method": "initialize",
164
+ "params": {
165
+ "protocolVersion": PROTOCOL_VERSION,
166
+ "capabilities": {
167
+ "roots": {"listChanged": True},
168
+ "sampling": {}
169
+ },
170
+ "clientInfo": {
171
+ "name": "aos-verification-test-suite",
172
+ "version": "1.0.0"
173
+ }
174
+ }
175
+ }
176
+ self.send_message(init_req)
177
+ self.msg_id += 1
178
+
179
+ resp = self.read_message()
180
+ if "error" in resp:
181
+ raise RuntimeError(f"MCP initialize error: {resp['error']}")
182
+
183
+ result = resp.get("result", {})
184
+ self.server_info = result.get("serverInfo", {})
185
+ self.protocol_version = result.get("protocolVersion", "")
186
+ self.server_instructions = result.get("instructions", "")
187
+ self.capabilities = result.get("capabilities", {})
188
+
189
+ # Send initialized notification
190
+ init_notif = {
191
+ "jsonrpc": "2.0",
192
+ "method": "notifications/initialized",
193
+ "params": {}
194
+ }
195
+ self.send_message(init_notif)
196
+ return result
197
+
198
+ def list_tools(self) -> List[Dict[str, Any]]:
199
+ """Enumerates all tools exposed by the MCP server, with pagination support."""
200
+ all_tools: List[Dict[str, Any]] = []
201
+ cursor = None
202
+
203
+ while True:
204
+ params: Dict[str, Any] = {}
205
+ if cursor:
206
+ params["cursor"] = cursor
207
+
208
+ list_req = {
209
+ "jsonrpc": "2.0",
210
+ "id": self.msg_id,
211
+ "method": "tools/list",
212
+ "params": params
213
+ }
214
+ self.send_message(list_req)
215
+ self.msg_id += 1
216
+
217
+ resp = self.read_message()
218
+ if "error" in resp:
219
+ raise RuntimeError(f"tools/list error: {resp['error']}")
220
+
221
+ result = resp.get("result", {})
222
+ tools = result.get("tools", [])
223
+ all_tools.extend(tools)
224
+
225
+ cursor = result.get("nextCursor")
226
+ if not cursor:
227
+ break
228
+
229
+ return all_tools
230
+
231
+ def close(self):
232
+ """Cleanly terminates the MCP server process."""
233
+ if not self.process:
234
+ return
235
+ try:
236
+ if self.process.stdin:
237
+ try:
238
+ self.process.stdin.close()
239
+ except Exception:
240
+ pass
241
+ self.process.terminate()
242
+ self.process.wait(timeout=2.0)
243
+ except Exception:
244
+ try:
245
+ self.process.kill()
246
+ except Exception:
247
+ pass
248
+
249
+
250
+ def validate_tool_schema(tool: Dict[str, Any]) -> Tuple[bool, str, int, int]:
251
+ """
252
+ Validates a tool definition against the MCP tool specification.
253
+ Returns: (is_valid, message, properties_count, required_count)
254
+ """
255
+ name = tool.get("name")
256
+ if not name or not isinstance(name, str) or not name.strip():
257
+ return False, "Missing or invalid 'name' field", 0, 0
258
+
259
+ desc = tool.get("description")
260
+ if desc is not None and not isinstance(desc, str):
261
+ return False, "Description must be a string", 0, 0
262
+
263
+ schema = tool.get("inputSchema")
264
+ if schema is None:
265
+ return False, "Missing 'inputSchema' definition", 0, 0
266
+
267
+ if not isinstance(schema, dict):
268
+ return False, "'inputSchema' must be a JSON object", 0, 0
269
+
270
+ schema_type = schema.get("type")
271
+ if schema_type != "object":
272
+ return False, f"Expected inputSchema.type == 'object', got '{schema_type}'", 0, 0
273
+
274
+ props = schema.get("properties", {})
275
+ if not isinstance(props, dict):
276
+ return False, "inputSchema.properties must be a dict", 0, 0
277
+
278
+ required = schema.get("required", [])
279
+ if not isinstance(required, list):
280
+ return False, "inputSchema.required must be a list", len(props), 0
281
+
282
+ return True, "Valid MCP Tool Schema", len(props), len(required)
283
+
284
+
285
+ def parse_codex_toml(content: str) -> Dict[str, Any]:
286
+ """Parse Codex TOML configuration into a standard dict."""
287
+ if tomllib is not None:
288
+ return tomllib.loads(content)
289
+
290
+ # Minimal fallback parser for standard codex TOML files
291
+ result: Dict[str, Any] = {"mcp_servers": {}}
292
+ current_server = None
293
+ current_env = False
294
+
295
+ for raw_line in content.splitlines():
296
+ line = raw_line.strip()
297
+ if not line or line.startswith("#"):
298
+ continue
299
+
300
+ if line.startswith("[mcp_servers."):
301
+ section = line.strip("[]")
302
+ parts = section.split(".")
303
+ if len(parts) == 2:
304
+ current_server = parts[1]
305
+ current_env = False
306
+ result["mcp_servers"].setdefault(current_server, {"command": "", "args": [], "env": {}})
307
+ elif len(parts) == 3 and parts[2] == "env":
308
+ current_server = parts[1]
309
+ current_env = True
310
+ result["mcp_servers"].setdefault(current_server, {"command": "", "args": [], "env": {}})
311
+ continue
312
+
313
+ if current_server and "=" in line:
314
+ key, val = [x.strip() for x in line.split("=", 1)]
315
+ val = val.strip(' \t\n\r"')
316
+ if current_env:
317
+ result["mcp_servers"][current_server]["env"][key] = val
318
+ else:
319
+ if key == "command":
320
+ result["mcp_servers"][current_server]["command"] = val
321
+ elif key == "args":
322
+ try:
323
+ result["mcp_servers"][current_server]["args"] = json.loads(val.replace("'", '"'))
324
+ except Exception:
325
+ result["mcp_servers"][current_server]["args"] = []
326
+ elif key == "enabled":
327
+ result["mcp_servers"][current_server]["enabled"] = val.lower() == "true"
328
+
329
+ return result
330
+
331
+
332
+ def export_antigravity_lazy_schemas(server_name: str, tools: List[Dict[str, Any]], output_dir: str):
333
+ """
334
+ Exports tools as individual JSON schema files for Google Antigravity lazy loading:
335
+ ~/.gemini/antigravity-cli/mcp/<serverName>/<toolName>.json
336
+ """
337
+ dest_dir = Path(output_dir) / server_name
338
+ dest_dir.mkdir(parents=True, exist_ok=True)
339
+
340
+ for tool in tools:
341
+ tool_name = tool["name"]
342
+ schema_file = dest_dir / f"{tool_name}.json"
343
+ schema_payload = {
344
+ "name": tool_name,
345
+ "description": tool.get("description", ""),
346
+ "parameters": tool.get("inputSchema", {
347
+ "type": "object",
348
+ "properties": {},
349
+ "required": []
350
+ })
351
+ }
352
+ with open(schema_file, "w", encoding="utf-8") as f:
353
+ json.dump(schema_payload, f, indent=2, ensure_ascii=False)
354
+
355
+ # Also create an instructions.md if none exists
356
+ instructions_file = dest_dir / "instructions.md"
357
+ if not instructions_file.exists():
358
+ with open(instructions_file, "w", encoding="utf-8") as f:
359
+ f.write(f"# {server_name} MCP Server Best Practices\n\n")
360
+ f.write(f"This directory contains lazy tool definitions for `{server_name}`.\n")
361
+ f.write(f"Total registered tools: {len(tools)}.\n")
362
+
363
+
364
+ def run_verification_test(
365
+ server_name: str,
366
+ command: str,
367
+ args: Optional[List[str]] = None,
368
+ env: Optional[Dict[str, str]] = None,
369
+ timeout: float = 15.0,
370
+ export_dir: Optional[str] = None,
371
+ ) -> Dict[str, Any]:
372
+ """
373
+ Executes a complete verification cycle on a single MCP server.
374
+ """
375
+ args = args or []
376
+ env = env or {}
377
+ print(f"\n{'=' * 85}")
378
+ print(f"šŸš€ VERIFYING MCP SERVER: {server_name}")
379
+ print(f"Command : {command} {' '.join(args)}")
380
+ if env:
381
+ print(f"Env : {env}")
382
+ print(f"{'=' * 85}")
383
+
384
+ client = MCPStdioClient(command, args, env, timeout=timeout)
385
+ test_result: Dict[str, Any] = {
386
+ "server_name": server_name,
387
+ "command": command,
388
+ "args": args,
389
+ "status": "FAILED",
390
+ "error": None,
391
+ "server_info": {},
392
+ "protocol_version": None,
393
+ "tool_count": 0,
394
+ "tools": [],
395
+ "latency_ms": 0.0,
396
+ }
397
+
398
+ t0 = time.time()
399
+ try:
400
+ # Step 1: Subprocess Startup
401
+ print("[1/4] Spawning server process over stdio...")
402
+ client.start()
403
+ print(" āœ“ Process launched successfully (PID: {})".format(client.process.pid if client.process else "unknown"))
404
+
405
+ # Step 2: Initialize Handshake
406
+ print("[2/4] Executing JSON-RPC 'initialize' handshake...")
407
+ init_result = client.initialize()
408
+ test_result["server_info"] = client.server_info
409
+ test_result["protocol_version"] = client.protocol_version
410
+ print(f" āœ“ Handshake acknowledged.")
411
+ print(f" Server Name : {client.server_info.get('name', 'unknown')}")
412
+ print(f" Server Version : {client.server_info.get('version', 'unknown')}")
413
+ print(f" Protocol Ver : {client.protocol_version}")
414
+
415
+ # Step 3: Tool Enumeration
416
+ print("[3/4] Enumerating tool registry via 'tools/list'...")
417
+ tools = client.list_tools()
418
+ dt_ms = (time.time() - t0) * 1000.0
419
+ test_result["tool_count"] = len(tools)
420
+ test_result["latency_ms"] = round(dt_ms, 2)
421
+ print(f" āœ“ Received {len(tools)} tools in {dt_ms:.1f}ms")
422
+
423
+ # Step 4: Tool Schema Validation
424
+ print("[4/4] Validating tool schema definitions...")
425
+ all_schemas_valid = True
426
+ tool_records = []
427
+
428
+ print("\n" + "-" * 85)
429
+ print(f"{'#':<3} | {'TOOL NAME':<32} | {'PROPS':<5} | {'REQ':<4} | {'STATUS':<7} | {'DESCRIPTION':<24}")
430
+ print("-" * 85)
431
+
432
+ for idx, tool in enumerate(tools, 1):
433
+ is_valid, msg, prop_cnt, req_cnt = validate_tool_schema(tool)
434
+ if not is_valid:
435
+ all_schemas_valid = False
436
+ status_str = "FAIL"
437
+ else:
438
+ status_str = "PASS"
439
+
440
+ desc = (tool.get("description") or "").replace("\n", " ").strip()
441
+ if len(desc) > 23:
442
+ desc = desc[:21] + "..."
443
+
444
+ tool_name = tool.get("name", f"tool_{idx}")
445
+ if len(tool_name) > 31:
446
+ tool_name = tool_name[:29] + ".."
447
+
448
+ print(f"{idx:<3} | {tool_name:<32} | {prop_cnt:<5} | {req_cnt:<4} | {status_str:<7} | {desc:<24}")
449
+
450
+ tool_records.append({
451
+ "name": tool.get("name"),
452
+ "description": tool.get("description"),
453
+ "schema_valid": is_valid,
454
+ "validation_message": msg,
455
+ "property_count": prop_cnt,
456
+ "required_count": req_cnt,
457
+ "inputSchema": tool.get("inputSchema"),
458
+ })
459
+
460
+ print("-" * 85)
461
+
462
+ if not all_schemas_valid:
463
+ raise ToolSchemaValidationError("One or more tool schemas failed MCP validation.")
464
+
465
+ # Optional Antigravity lazy export
466
+ if export_dir:
467
+ print(f"\n[+] Exporting lazy tool schemas for Antigravity to: {export_dir}/{server_name}/")
468
+ export_antigravity_lazy_schemas(server_name, tools, export_dir)
469
+ print(f" āœ“ {len(tools)} schema JSON files written.")
470
+
471
+ test_result["status"] = "PASSED"
472
+ test_result["tools"] = tool_records
473
+ print(f"\nāœ… MCP SERVER '{server_name}' VERIFICATION PASSED ({len(tools)} tools validated)\n")
474
+
475
+ except Exception as exc:
476
+ test_result["status"] = "FAILED"
477
+ test_result["error"] = str(exc)
478
+ print(f"\nāŒ MCP SERVER '{server_name}' VERIFICATION FAILED: {exc}\n", file=sys.stderr)
479
+ finally:
480
+ client.close()
481
+
482
+ return test_result
483
+
484
+
485
+ def load_servers_from_config(config_path: str) -> Dict[str, Dict[str, Any]]:
486
+ """Loads server definitions from either JSON or TOML config file."""
487
+ path = Path(config_path)
488
+ if not path.exists():
489
+ raise FileNotFoundError(f"Configuration file not found: {config_path}")
490
+
491
+ content = path.read_text(encoding="utf-8")
492
+ servers: Dict[str, Dict[str, Any]] = {}
493
+
494
+ if path.suffix.lower() == ".toml":
495
+ parsed = parse_codex_toml(content)
496
+ raw_servers = parsed.get("mcp_servers", {})
497
+ for name, cfg in raw_servers.items():
498
+ if cfg.get("enabled", True):
499
+ servers[name] = {
500
+ "command": cfg.get("command", ""),
501
+ "args": cfg.get("args", []),
502
+ "env": cfg.get("env", {})
503
+ }
504
+ else:
505
+ parsed = json.loads(content)
506
+ raw_servers = parsed.get("mcpServers", {})
507
+ for name, cfg in raw_servers.items():
508
+ servers[name] = {
509
+ "command": cfg.get("command", ""),
510
+ "args": cfg.get("args", []),
511
+ "env": cfg.get("env", {})
512
+ }
513
+
514
+ return servers
515
+
516
+
517
+ def main():
518
+ parser = argparse.ArgumentParser(
519
+ description="AOS MCP Multi-Harness Connection & Verification Test Suite"
520
+ )
521
+ parser.add_argument("--config", help="Path to config file (JSON or TOML)")
522
+ parser.add_argument("--server", help="Specific server name to test from config")
523
+ parser.add_argument("--command", help="Direct command to run over stdio")
524
+ parser.add_argument("--arg", dest="args", action="append", default=[], help="Command arguments")
525
+ parser.add_argument("--env", action="append", default=[], help="Environment variable (KEY=VALUE)")
526
+ parser.add_argument("--timeout", type=float, default=15.0, help="Response timeout in seconds (default: 15.0)")
527
+ parser.add_argument("--export-antigravity", help="Directory to export Antigravity lazy tool JSON schemas")
528
+ parser.add_argument("--json-report", help="Path to write JSON test report")
529
+ parsed = parser.parse_args()
530
+
531
+ custom_env = {}
532
+ for item in parsed.env:
533
+ if "=" in item:
534
+ k, v = item.split("=", 1)
535
+ custom_env[k] = v
536
+
537
+ results: List[Dict[str, Any]] = []
538
+
539
+ if parsed.config:
540
+ servers = load_servers_from_config(parsed.config)
541
+ if parsed.server:
542
+ if parsed.server not in servers:
543
+ print(f"Error: Server '{parsed.server}' not found in {parsed.config}", file=sys.stderr)
544
+ print(f"Available servers: {list(servers.keys())}", file=sys.stderr)
545
+ sys.exit(1)
546
+ servers = {parsed.server: servers[parsed.server]}
547
+
548
+ for sname, scfg in servers.items():
549
+ merged_env = {**scfg.get("env", {}), **custom_env}
550
+ res = run_verification_test(
551
+ server_name=sname,
552
+ command=scfg["command"],
553
+ args=scfg.get("args", []),
554
+ env=merged_env,
555
+ timeout=parsed.timeout,
556
+ export_dir=parsed.export_antigravity,
557
+ )
558
+ results.append(res)
559
+
560
+ elif parsed.command:
561
+ res = run_verification_test(
562
+ server_name="direct-cli",
563
+ command=parsed.command,
564
+ args=parsed.args,
565
+ env=custom_env,
566
+ timeout=parsed.timeout,
567
+ export_dir=parsed.export_antigravity,
568
+ )
569
+ results.append(res)
570
+ else:
571
+ # Default self-test using project's default runner scripts
572
+ repo_dir = Path(__file__).resolve().parent.parent
573
+ kicad_runner = str(repo_dir / "scripts" / "run_kicad_mcp.sh")
574
+ openscad_runner = str(repo_dir / "scripts" / "run_openscad_mcp.sh")
575
+
576
+ print("No --config or --command provided. Running default verification on KiCad and OpenSCAD runners...")
577
+ res_kicad = run_verification_test(
578
+ server_name="kicad",
579
+ command=kicad_runner,
580
+ timeout=parsed.timeout,
581
+ export_dir=parsed.export_antigravity,
582
+ )
583
+ results.append(res_kicad)
584
+
585
+ res_openscad = run_verification_test(
586
+ server_name="openscad",
587
+ command=openscad_runner,
588
+ timeout=parsed.timeout,
589
+ export_dir=parsed.export_antigravity,
590
+ )
591
+ results.append(res_openscad)
592
+
593
+ # Print Overall Summary
594
+ total = len(results)
595
+ passed = sum(1 for r in results if r["status"] == "PASSED")
596
+ failed = total - passed
597
+
598
+ print("\n" + "=" * 85)
599
+ print("šŸ“‹ MCP VERIFICATION TEST SUITE SUMMARY")
600
+ print("=" * 85)
601
+ for r in results:
602
+ sym = "āœ… PASS" if r["status"] == "PASSED" else "āŒ FAIL"
603
+ tools_str = f"{r['tool_count']} tools" if r['status'] == "PASSED" else f"Error: {r['error']}"
604
+ print(f"[{sym}] {r['server_name']:<20} | {tools_str:<35} | {r['latency_ms']}ms")
605
+ print("-" * 85)
606
+ print(f"Total: {total} | Passed: {passed} | Failed: {failed}")
607
+ print("=" * 85 + "\n")
608
+
609
+ if parsed.json_report:
610
+ report_path = Path(parsed.json_report)
611
+ report_path.parent.mkdir(parents=True, exist_ok=True)
612
+ with open(report_path, "w", encoding="utf-8") as f:
613
+ json.dump({
614
+ "timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
615
+ "total": total,
616
+ "passed": passed,
617
+ "failed": failed,
618
+ "results": results
619
+ }, f, indent=2)
620
+ print(f"Wrote machine-readable test report to: {parsed.json_report}")
621
+
622
+ sys.exit(0 if failed == 0 else 1)
623
+
624
+
625
+ if __name__ == "__main__":
626
+ main()