@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.
- package/README.md +116 -0
- package/config/mcp/antigravity.json +22 -0
- package/config/mcp/claude.json +22 -0
- package/config/mcp/codex.toml +25 -0
- package/docs/adr/ADR-001-KICAD-OPENSCAD-MCP-STRATEGY.md +334 -0
- package/docs/review_documentation.md +121 -0
- package/installer.js +358 -0
- package/mcp_servers/kicad-mcp-server/.env.example +22 -0
- package/mcp_servers/kicad-mcp-server/.github/workflows/ci.yml +36 -0
- package/mcp_servers/kicad-mcp-server/CLAUDE.md +487 -0
- package/mcp_servers/kicad-mcp-server/README.md +316 -0
- package/mcp_servers/kicad-mcp-server/docs/DEVICE_TREE.md +416 -0
- package/mcp_servers/kicad-mcp-server/docs/INSTALLATION.md +332 -0
- package/mcp_servers/kicad-mcp-server/docs/PIN_ANALYSIS.md +332 -0
- package/mcp_servers/kicad-mcp-server/docs/README.md +240 -0
- package/mcp_servers/kicad-mcp-server/docs/TESTING.md +613 -0
- package/mcp_servers/kicad-mcp-server/docs/VALIDATION.md +268 -0
- package/mcp_servers/kicad-mcp-server/pyproject.toml +96 -0
- package/mcp_servers/kicad-mcp-server/requirements-dev.txt +16 -0
- package/mcp_servers/kicad-mcp-server/requirements-test.txt +24 -0
- package/mcp_servers/kicad-mcp-server/requirements.txt +13 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/__init__.py +3 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/__main__.py +17 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/config.py +46 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/models/__init__.py +1 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/models/types.py +87 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/parsers/__init__.py +1 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/parsers/netlist_parser.py +234 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/parsers/pcb_parser.py +375 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/parsers/pcb_parser_kicad.py +327 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/parsers/schematic_parser.py +902 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/server.py +71 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/__init__.py +1 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/arduino/connectivity_test.cpp.j2 +189 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/device_tree/atmega.dts.j2 +77 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/device_tree/esp32.dts.j2 +77 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/device_tree/nrf52.dts.j2 +77 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/device_tree/stm32f4.dts.j2 +89 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/esp_idf/test_suite.c.j2 +340 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/pytest/test_connectivity.py.j2 +147 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/st_hal/hal_test.c.j2 +313 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/pytest_gpio_test.py.j2 +99 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/pytest_i2c_test.py.j2 +117 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/pytest_pinmux_test.py.j2 +43 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/pytest_spi_test.py.j2 +94 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/unity_gpio_test.c.j2 +113 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/unity_i2c_test.c.j2 +101 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/unity_spi_test.c.j2 +94 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/unittest/test_schematic.py.j2 +172 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/__init__.py +36 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/device_tree.py +1187 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/hierarchical_analysis.py +211 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/netlist.py +320 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/parts_registry.py +142 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/pcb.py +955 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/pcb_layout.py +308 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/pin_analysis.py +765 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/project.py +196 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/schematic.py +319 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/schematic_editor.py +674 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/schematic_search.py +158 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/validation.py +866 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/visualization.py +225 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/utils/__init__.py +1 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/utils/file_handlers.py +65 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/utils/kicad_cli.py +103 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/utils/kicad_version.py +103 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/utils/parts_registry.py +197 -0
- package/mcp_servers/kicad-mcp-server/tests/__init__.py +1 -0
- package/mcp_servers/kicad-mcp-server/tests/examples/ESP32S3_TEST.md +219 -0
- package/mcp_servers/kicad-mcp-server/tests/fixtures/README.md +65 -0
- package/mcp_servers/kicad-mcp-server/tests/fixtures/__init__.py +1 -0
- package/mcp_servers/kicad-mcp-server/tests/fixtures/example_pcb.kicad_pcb +177 -0
- package/mcp_servers/kicad-mcp-server/tests/fixtures/example_schematic.kicad_sch +145 -0
- package/mcp_servers/kicad-mcp-server/tests/fixtures/hier/child.kicad_sch +24 -0
- package/mcp_servers/kicad-mcp-server/tests/fixtures/hier/root.kicad_sch +38 -0
- package/mcp_servers/kicad-mcp-server/tests/test_tools/__init__.py +1 -0
- package/mcp_servers/kicad-mcp-server/tests/test_tools/test_hierarchical_labels.py +195 -0
- package/mcp_servers/kicad-mcp-server/tests/test_tools/test_kicad_cli.py +116 -0
- package/mcp_servers/kicad-mcp-server/tests/test_tools/test_netlist_cache_path.py +29 -0
- package/mcp_servers/kicad-mcp-server/tests/test_tools/test_schematic.py +189 -0
- package/mcp_servers/kicad-mcp-server/tests/test_tools/test_schematic_hierarchy.py +69 -0
- package/mcp_servers/kicad-mcp-server/tests/test_tools/test_visualization.py +136 -0
- package/mcp_servers/kicad-mcp-server/uv.lock +2873 -0
- package/mcp_servers/openscad-mcp-server/.dockerignore +9 -0
- package/mcp_servers/openscad-mcp-server/.github/workflows/test.yml +40 -0
- package/mcp_servers/openscad-mcp-server/Dockerfile +29 -0
- package/mcp_servers/openscad-mcp-server/LICENSE +21 -0
- package/mcp_servers/openscad-mcp-server/README.md +154 -0
- package/mcp_servers/openscad-mcp-server/docs/audit.md +56 -0
- package/mcp_servers/openscad-mcp-server/docs/docker.md +66 -0
- package/mcp_servers/openscad-mcp-server/docs/issue-followup.md +19 -0
- package/mcp_servers/openscad-mcp-server/docs/jetson.md +17 -0
- package/mcp_servers/openscad-mcp-server/glama.json +4 -0
- package/mcp_servers/openscad-mcp-server/legacy/README.md +15 -0
- package/mcp_servers/openscad-mcp-server/legacy/README.original.md +294 -0
- package/mcp_servers/openscad-mcp-server/legacy/implementation_plan.md +100 -0
- package/mcp_servers/openscad-mcp-server/legacy/old/download_sam2_checkpoint.py +115 -0
- package/mcp_servers/openscad-mcp-server/legacy/old/src/ai/sam_segmentation.py +209 -0
- package/mcp_servers/openscad-mcp-server/legacy/old/src/models/threestudio_generator.py +231 -0
- package/mcp_servers/openscad-mcp-server/legacy/old/src/workflow/image_to_model_pipeline.py +260 -0
- package/mcp_servers/openscad-mcp-server/legacy/old/test_sam2_segmentation.py +96 -0
- package/mcp_servers/openscad-mcp-server/legacy/requirements.txt +57 -0
- package/mcp_servers/openscad-mcp-server/legacy/rtfmd/README.md +39 -0
- package/mcp_servers/openscad-mcp-server/legacy/rtfmd/decisions/ai-driven-code-generation.md +122 -0
- package/mcp_servers/openscad-mcp-server/legacy/rtfmd/decisions/export-formats.md +76 -0
- package/mcp_servers/openscad-mcp-server/legacy/rtfmd/files/src/ai/ai_service.py.md +51 -0
- package/mcp_servers/openscad-mcp-server/legacy/rtfmd/files/src/main.py.md +63 -0
- package/mcp_servers/openscad-mcp-server/legacy/rtfmd/files/src/models/code_generator.py.md +63 -0
- package/mcp_servers/openscad-mcp-server/legacy/rtfmd/files/src/nlp/parameter_extractor.py.md +63 -0
- package/mcp_servers/openscad-mcp-server/legacy/rtfmd/knowledge/ai/natural-language-processing.md +78 -0
- package/mcp_servers/openscad-mcp-server/legacy/rtfmd/knowledge/nlp/parameter-extraction.md +173 -0
- package/mcp_servers/openscad-mcp-server/legacy/rtfmd/knowledge/openscad/export-formats.md +91 -0
- package/mcp_servers/openscad-mcp-server/legacy/rtfmd/knowledge/openscad/openscad-basics.md +66 -0
- package/mcp_servers/openscad-mcp-server/legacy/rtfmd/knowledge/openscad/primitive-testing.md +79 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/__init__.py +0 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/ai/ai_service.py +257 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/ai/gemini_api.py +161 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/ai/venice_api.py +203 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/config.py +121 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/main.py +1456 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/main.py.new +404 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/main_remote.py +401 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/models/__init__.py +0 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/models/code_generator.py +321 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/models/cuda_mvs.py +209 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/models/scad_templates/basic_shapes.scad +144 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/nlp/__init__.py +0 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/nlp/parameter_extractor.py +388 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/openscad_wrapper/__init__.py +0 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/openscad_wrapper/wrapper.py +418 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/printer_discovery/__init__.py +1 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/printer_discovery/printer_discovery.py +471 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/remote/connection_manager.py +537 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/remote/cuda_mvs_client.py +435 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/remote/cuda_mvs_server.py +787 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/remote/error_handling.py +415 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/testing/__init__.py +0 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/testing/primitive_tester.py +203 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/testing/test_primitives.py +98 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/utils/__init__.py +1 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/utils/cad_exporter.py +241 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/utils/format_validator.py +206 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/utils/stl_exporter.py +140 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/utils/stl_repair.py +91 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/utils/stl_validator.py +123 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/visualization/__init__.py +0 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/visualization/headless_renderer.py +52 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/visualization/renderer.py +177 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/visualization/web_interface.py +639 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/workflow/image_approval.py +148 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/workflow/multi_view_to_model_pipeline.py +338 -0
- package/mcp_servers/openscad-mcp-server/legacy/test_complete_workflow.py +374 -0
- package/mcp_servers/openscad-mcp-server/legacy/test_cuda_mvs.py +191 -0
- package/mcp_servers/openscad-mcp-server/legacy/test_gemini_api.py +168 -0
- package/mcp_servers/openscad-mcp-server/legacy/test_image_approval.py +192 -0
- package/mcp_servers/openscad-mcp-server/legacy/test_image_approval_workflow.py +251 -0
- package/mcp_servers/openscad-mcp-server/legacy/test_image_to_model_pipeline.py +145 -0
- package/mcp_servers/openscad-mcp-server/legacy/test_model_selection.py +41 -0
- package/mcp_servers/openscad-mcp-server/legacy/test_multi_view_pipeline.py +290 -0
- package/mcp_servers/openscad-mcp-server/legacy/test_primitives.sh +13 -0
- package/mcp_servers/openscad-mcp-server/legacy/test_rabbit_direct.py +71 -0
- package/mcp_servers/openscad-mcp-server/legacy/test_remote_cuda_mvs.py +283 -0
- package/mcp_servers/openscad-mcp-server/legacy/test_venice_example.py +69 -0
- package/mcp_servers/openscad-mcp-server/pyproject.toml +33 -0
- package/mcp_servers/openscad-mcp-server/requirements.txt +2 -0
- package/mcp_servers/openscad-mcp-server/scad/simple_cube.scad +2 -0
- package/mcp_servers/openscad-mcp-server/scripts/test_docker.py +220 -0
- package/mcp_servers/openscad-mcp-server/src/openscad_mcp/__init__.py +3 -0
- package/mcp_servers/openscad-mcp-server/src/openscad_mcp/__main__.py +3 -0
- package/mcp_servers/openscad-mcp-server/src/openscad_mcp/engine.py +94 -0
- package/mcp_servers/openscad-mcp-server/src/openscad_mcp/geometry.py +242 -0
- package/mcp_servers/openscad-mcp-server/src/openscad_mcp/server.py +245 -0
- package/mcp_servers/openscad-mcp-server/src/openscad_mcp/service.py +190 -0
- package/mcp_servers/openscad-mcp-server/tests/conftest.py +17 -0
- package/mcp_servers/openscad-mcp-server/tests/test_engine.py +37 -0
- package/mcp_servers/openscad-mcp-server/tests/test_geometry.py +106 -0
- package/mcp_servers/openscad-mcp-server/tests/test_integration.py +172 -0
- package/mcp_servers/openscad-mcp-server/tests/test_transports.py +314 -0
- package/mcp_servers/openscad-mcp-server/uv.lock +1123 -0
- package/package.json +44 -0
- package/scripts/install_mcps.sh +86 -0
- package/scripts/openscad_wrapper.sh +62 -0
- package/scripts/run_kicad_mcp.sh +25 -0
- package/scripts/run_openscad_mcp.sh +41 -0
- package/scripts/test_mcp_connection.py +626 -0
- package/scripts/test_mcp_connection.sh +168 -0
- package/skills/code-first-hardware-design/SKILL.md +260 -0
- package/skills/pcb-constraint-definition/SKILL.md +236 -0
- package/skills/pcb-layout-routing-automation/SKILL.md +153 -0
- package/skills/pcb-validation-dfm-signoff/SKILL.md +194 -0
- 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()
|