@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,283 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Test script for remote CUDA Multi-View Stereo processing.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import logging
|
|
8
|
+
import unittest
|
|
9
|
+
from unittest.mock import patch, MagicMock
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
# Add the src directory to the path
|
|
13
|
+
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
14
|
+
|
|
15
|
+
from src.remote.cuda_mvs_client import CUDAMVSClient
|
|
16
|
+
from src.remote.connection_manager import CUDAMVSConnectionManager
|
|
17
|
+
from src.config import REMOTE_CUDA_MVS
|
|
18
|
+
|
|
19
|
+
# Configure logging
|
|
20
|
+
logging.basicConfig(level=logging.INFO)
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
class TestRemoteCUDAMVS(unittest.TestCase):
|
|
24
|
+
"""
|
|
25
|
+
Test cases for remote CUDA Multi-View Stereo processing.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def setUp(self):
|
|
29
|
+
"""
|
|
30
|
+
Set up test environment.
|
|
31
|
+
"""
|
|
32
|
+
# Create test output directories
|
|
33
|
+
self.test_output_dir = "output/test_remote_cuda_mvs"
|
|
34
|
+
os.makedirs(self.test_output_dir, exist_ok=True)
|
|
35
|
+
|
|
36
|
+
# Mock API key
|
|
37
|
+
self.api_key = "test_api_key"
|
|
38
|
+
|
|
39
|
+
# Create the client with the mock API key
|
|
40
|
+
self.client = CUDAMVSClient(
|
|
41
|
+
api_key=self.api_key,
|
|
42
|
+
output_dir=self.test_output_dir
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# Create the connection manager with the mock API key
|
|
46
|
+
self.connection_manager = CUDAMVSConnectionManager(
|
|
47
|
+
api_key=self.api_key,
|
|
48
|
+
discovery_port=REMOTE_CUDA_MVS["DISCOVERY_PORT"],
|
|
49
|
+
use_lan_discovery=True
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
@patch('src.remote.cuda_mvs_client.requests.post')
|
|
53
|
+
def test_upload_images(self, mock_post):
|
|
54
|
+
"""
|
|
55
|
+
Test uploading images to a remote CUDA MVS server.
|
|
56
|
+
"""
|
|
57
|
+
# Mock response
|
|
58
|
+
mock_response = MagicMock()
|
|
59
|
+
mock_response.status_code = 200
|
|
60
|
+
mock_response.json.return_value = {
|
|
61
|
+
"job_id": "test_job_123",
|
|
62
|
+
"status": "uploaded",
|
|
63
|
+
"message": "Images uploaded successfully"
|
|
64
|
+
}
|
|
65
|
+
mock_post.return_value = mock_response
|
|
66
|
+
|
|
67
|
+
# Test parameters
|
|
68
|
+
server_url = "http://test-server:8765"
|
|
69
|
+
image_paths = [
|
|
70
|
+
os.path.join(self.test_output_dir, "test_image_1.png"),
|
|
71
|
+
os.path.join(self.test_output_dir, "test_image_2.png")
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
# Create test images
|
|
75
|
+
for path in image_paths:
|
|
76
|
+
with open(path, "w") as f:
|
|
77
|
+
f.write("test image data")
|
|
78
|
+
|
|
79
|
+
# Call the method
|
|
80
|
+
result = self.client.upload_images(server_url, image_paths)
|
|
81
|
+
|
|
82
|
+
# Verify the result
|
|
83
|
+
self.assertIsNotNone(result)
|
|
84
|
+
self.assertEqual(result["job_id"], "test_job_123")
|
|
85
|
+
self.assertEqual(result["status"], "uploaded")
|
|
86
|
+
|
|
87
|
+
# Verify the API call
|
|
88
|
+
mock_post.assert_called_once()
|
|
89
|
+
args, kwargs = mock_post.call_args
|
|
90
|
+
self.assertTrue(server_url in args[0])
|
|
91
|
+
self.assertEqual(kwargs["headers"]["Authorization"], f"Bearer {self.api_key}")
|
|
92
|
+
|
|
93
|
+
@patch('src.remote.cuda_mvs_client.requests.post')
|
|
94
|
+
def test_process_job(self, mock_post):
|
|
95
|
+
"""
|
|
96
|
+
Test processing a job on a remote CUDA MVS server.
|
|
97
|
+
"""
|
|
98
|
+
# Mock response
|
|
99
|
+
mock_response = MagicMock()
|
|
100
|
+
mock_response.status_code = 200
|
|
101
|
+
mock_response.json.return_value = {
|
|
102
|
+
"job_id": "test_job_123",
|
|
103
|
+
"status": "processing",
|
|
104
|
+
"message": "Job started processing"
|
|
105
|
+
}
|
|
106
|
+
mock_post.return_value = mock_response
|
|
107
|
+
|
|
108
|
+
# Test parameters
|
|
109
|
+
server_url = "http://test-server:8765"
|
|
110
|
+
job_id = "test_job_123"
|
|
111
|
+
params = {
|
|
112
|
+
"quality": "normal",
|
|
113
|
+
"output_format": "obj"
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
# Call the method
|
|
117
|
+
result = self.client.process_job(server_url, job_id, params)
|
|
118
|
+
|
|
119
|
+
# Verify the result
|
|
120
|
+
self.assertIsNotNone(result)
|
|
121
|
+
self.assertEqual(result["job_id"], "test_job_123")
|
|
122
|
+
self.assertEqual(result["status"], "processing")
|
|
123
|
+
|
|
124
|
+
# Verify the API call
|
|
125
|
+
mock_post.assert_called_once()
|
|
126
|
+
args, kwargs = mock_post.call_args
|
|
127
|
+
self.assertTrue(server_url in args[0])
|
|
128
|
+
self.assertEqual(kwargs["headers"]["Authorization"], f"Bearer {self.api_key}")
|
|
129
|
+
self.assertEqual(kwargs["json"]["job_id"], job_id)
|
|
130
|
+
self.assertEqual(kwargs["json"]["params"]["quality"], "normal")
|
|
131
|
+
|
|
132
|
+
@patch('src.remote.cuda_mvs_client.requests.get')
|
|
133
|
+
def test_get_job_status(self, mock_get):
|
|
134
|
+
"""
|
|
135
|
+
Test getting the status of a job on a remote CUDA MVS server.
|
|
136
|
+
"""
|
|
137
|
+
# Mock response
|
|
138
|
+
mock_response = MagicMock()
|
|
139
|
+
mock_response.status_code = 200
|
|
140
|
+
mock_response.json.return_value = {
|
|
141
|
+
"job_id": "test_job_123",
|
|
142
|
+
"status": "completed",
|
|
143
|
+
"progress": 100,
|
|
144
|
+
"message": "Job completed successfully"
|
|
145
|
+
}
|
|
146
|
+
mock_get.return_value = mock_response
|
|
147
|
+
|
|
148
|
+
# Test parameters
|
|
149
|
+
server_url = "http://test-server:8765"
|
|
150
|
+
job_id = "test_job_123"
|
|
151
|
+
|
|
152
|
+
# Call the method
|
|
153
|
+
result = self.client.get_job_status(server_url, job_id)
|
|
154
|
+
|
|
155
|
+
# Verify the result
|
|
156
|
+
self.assertIsNotNone(result)
|
|
157
|
+
self.assertEqual(result["job_id"], "test_job_123")
|
|
158
|
+
self.assertEqual(result["status"], "completed")
|
|
159
|
+
self.assertEqual(result["progress"], 100)
|
|
160
|
+
|
|
161
|
+
# Verify the API call
|
|
162
|
+
mock_get.assert_called_once()
|
|
163
|
+
args, kwargs = mock_get.call_args
|
|
164
|
+
self.assertTrue(server_url in args[0])
|
|
165
|
+
self.assertEqual(kwargs["headers"]["Authorization"], f"Bearer {self.api_key}")
|
|
166
|
+
|
|
167
|
+
@patch('src.remote.cuda_mvs_client.requests.get')
|
|
168
|
+
def test_download_model(self, mock_get):
|
|
169
|
+
"""
|
|
170
|
+
Test downloading a model from a remote CUDA MVS server.
|
|
171
|
+
"""
|
|
172
|
+
# Mock response
|
|
173
|
+
mock_response = MagicMock()
|
|
174
|
+
mock_response.status_code = 200
|
|
175
|
+
mock_response.content = b"test model data"
|
|
176
|
+
mock_get.return_value = mock_response
|
|
177
|
+
|
|
178
|
+
# Test parameters
|
|
179
|
+
server_url = "http://test-server:8765"
|
|
180
|
+
job_id = "test_job_123"
|
|
181
|
+
output_dir = os.path.join(self.test_output_dir, "models")
|
|
182
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
183
|
+
|
|
184
|
+
# Call the method
|
|
185
|
+
result = self.client.download_model(server_url, job_id, output_dir)
|
|
186
|
+
|
|
187
|
+
# Verify the result
|
|
188
|
+
self.assertIsNotNone(result)
|
|
189
|
+
self.assertTrue("model_path" in result)
|
|
190
|
+
self.assertTrue(os.path.exists(result["model_path"]))
|
|
191
|
+
|
|
192
|
+
# Verify the API call
|
|
193
|
+
mock_get.assert_called()
|
|
194
|
+
args, kwargs = mock_get.call_args_list[0]
|
|
195
|
+
self.assertTrue(server_url in args[0])
|
|
196
|
+
self.assertEqual(kwargs["headers"]["Authorization"], f"Bearer {self.api_key}")
|
|
197
|
+
|
|
198
|
+
@patch('src.remote.connection_manager.zeroconf.Zeroconf')
|
|
199
|
+
def test_discover_servers(self, mock_zeroconf):
|
|
200
|
+
"""
|
|
201
|
+
Test discovering CUDA MVS servers on the network.
|
|
202
|
+
"""
|
|
203
|
+
# Mock Zeroconf
|
|
204
|
+
mock_zeroconf_instance = MagicMock()
|
|
205
|
+
mock_zeroconf.return_value = mock_zeroconf_instance
|
|
206
|
+
|
|
207
|
+
# Mock ServiceBrowser
|
|
208
|
+
with patch('src.remote.connection_manager.ServiceBrowser') as mock_browser:
|
|
209
|
+
# Set up the connection manager to discover servers
|
|
210
|
+
self.connection_manager.discover_servers()
|
|
211
|
+
|
|
212
|
+
# Verify Zeroconf was initialized
|
|
213
|
+
mock_zeroconf.assert_called_once()
|
|
214
|
+
|
|
215
|
+
# Verify ServiceBrowser was initialized
|
|
216
|
+
mock_browser.assert_called_once()
|
|
217
|
+
args, kwargs = mock_browser.call_args
|
|
218
|
+
self.assertEqual(args[0], mock_zeroconf_instance)
|
|
219
|
+
self.assertEqual(args[1], "_cudamvs._tcp.local.")
|
|
220
|
+
|
|
221
|
+
@patch('src.remote.connection_manager.CUDAMVSClient')
|
|
222
|
+
def test_upload_images_with_connection_manager(self, mock_client_class):
|
|
223
|
+
"""
|
|
224
|
+
Test uploading images using the connection manager.
|
|
225
|
+
"""
|
|
226
|
+
# Mock client
|
|
227
|
+
mock_client = MagicMock()
|
|
228
|
+
mock_client_class.return_value = mock_client
|
|
229
|
+
|
|
230
|
+
# Mock upload_images method
|
|
231
|
+
mock_client.upload_images.return_value = {
|
|
232
|
+
"job_id": "test_job_123",
|
|
233
|
+
"status": "uploaded",
|
|
234
|
+
"message": "Images uploaded successfully"
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
# Add a mock server
|
|
238
|
+
self.connection_manager.servers = {
|
|
239
|
+
"test_server": {
|
|
240
|
+
"id": "test_server",
|
|
241
|
+
"name": "Test Server",
|
|
242
|
+
"url": "http://test-server:8765",
|
|
243
|
+
"status": "online"
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
# Test parameters
|
|
248
|
+
server_id = "test_server"
|
|
249
|
+
image_paths = [
|
|
250
|
+
os.path.join(self.test_output_dir, "test_image_1.png"),
|
|
251
|
+
os.path.join(self.test_output_dir, "test_image_2.png")
|
|
252
|
+
]
|
|
253
|
+
|
|
254
|
+
# Create test images
|
|
255
|
+
for path in image_paths:
|
|
256
|
+
with open(path, "w") as f:
|
|
257
|
+
f.write("test image data")
|
|
258
|
+
|
|
259
|
+
# Call the method
|
|
260
|
+
result = self.connection_manager.upload_images(server_id, image_paths)
|
|
261
|
+
|
|
262
|
+
# Verify the result
|
|
263
|
+
self.assertIsNotNone(result)
|
|
264
|
+
self.assertEqual(result["job_id"], "test_job_123")
|
|
265
|
+
self.assertEqual(result["status"], "uploaded")
|
|
266
|
+
|
|
267
|
+
# Verify the client method was called
|
|
268
|
+
mock_client.upload_images.assert_called_once()
|
|
269
|
+
args, kwargs = mock_client.upload_images.call_args
|
|
270
|
+
self.assertEqual(args[0], "http://test-server:8765")
|
|
271
|
+
self.assertEqual(args[1], image_paths)
|
|
272
|
+
|
|
273
|
+
def tearDown(self):
|
|
274
|
+
"""
|
|
275
|
+
Clean up after tests.
|
|
276
|
+
"""
|
|
277
|
+
# Clean up test output directory
|
|
278
|
+
import shutil
|
|
279
|
+
if os.path.exists(self.test_output_dir):
|
|
280
|
+
shutil.rmtree(self.test_output_dir)
|
|
281
|
+
|
|
282
|
+
if __name__ == "__main__":
|
|
283
|
+
unittest.main()
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import requests
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
# Venice.ai API configuration
|
|
6
|
+
VENICE_API_KEY = os.getenv("VENICE_API_KEY", "") # Set via environment variable
|
|
7
|
+
OUTPUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "output", "images")
|
|
8
|
+
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
|
9
|
+
|
|
10
|
+
# API endpoint
|
|
11
|
+
url = "https://api.venice.ai/api/v1/image/generate"
|
|
12
|
+
|
|
13
|
+
# Test prompt
|
|
14
|
+
prompt = "A coffee mug with geometric patterns"
|
|
15
|
+
|
|
16
|
+
# Prepare payload
|
|
17
|
+
payload = {
|
|
18
|
+
"model": "fluently-xl", # Try default model instead of flux
|
|
19
|
+
"prompt": prompt,
|
|
20
|
+
"height": 1024,
|
|
21
|
+
"width": 1024,
|
|
22
|
+
"steps": 20,
|
|
23
|
+
"return_binary": False,
|
|
24
|
+
"hide_watermark": False,
|
|
25
|
+
"format": "png",
|
|
26
|
+
"embed_exif_metadata": False
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
# Set up headers
|
|
30
|
+
headers = {
|
|
31
|
+
"Authorization": f"Bearer {VENICE_API_KEY}",
|
|
32
|
+
"Content-Type": "application/json"
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
print(f"Sending request to {url} with prompt: '{prompt}'")
|
|
36
|
+
|
|
37
|
+
# Make API request
|
|
38
|
+
try:
|
|
39
|
+
response = requests.post(url, json=payload, headers=headers)
|
|
40
|
+
|
|
41
|
+
print(f"Response status: {response.status_code}")
|
|
42
|
+
|
|
43
|
+
if response.status_code == 200:
|
|
44
|
+
result = response.json()
|
|
45
|
+
print("\nImage generation result:")
|
|
46
|
+
print(json.dumps(result, indent=2))
|
|
47
|
+
|
|
48
|
+
# Print image URL if available
|
|
49
|
+
if "images" in result and len(result["images"]) > 0:
|
|
50
|
+
image_url = result["images"][0]
|
|
51
|
+
print(f"\nImage URL: {image_url}")
|
|
52
|
+
|
|
53
|
+
# Download image
|
|
54
|
+
image_filename = f"{prompt[:20].replace(' ', '_')}_flux.png"
|
|
55
|
+
image_path = os.path.join(OUTPUT_DIR, image_filename)
|
|
56
|
+
|
|
57
|
+
print(f"Downloading image to {image_path}...")
|
|
58
|
+
img_response = requests.get(image_url, stream=True)
|
|
59
|
+
if img_response.status_code == 200:
|
|
60
|
+
with open(image_path, 'wb') as f:
|
|
61
|
+
for chunk in img_response.iter_content(chunk_size=8192):
|
|
62
|
+
f.write(chunk)
|
|
63
|
+
print(f"Image saved to {image_path}")
|
|
64
|
+
else:
|
|
65
|
+
print(f"Failed to download image: {img_response.status_code}")
|
|
66
|
+
else:
|
|
67
|
+
print(f"Error: {response.text}")
|
|
68
|
+
except Exception as e:
|
|
69
|
+
print(f"Error: {str(e)}")
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27,<2"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "openscad-mcp-server"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "Local MCP tools for generating, rendering, and exporting OpenSCAD models"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.11"
|
|
13
|
+
dependencies = ["mcp>=1.12,<2", "fastapi>=0.115,<1", "uvicorn>=0.30,<1"]
|
|
14
|
+
|
|
15
|
+
[project.scripts]
|
|
16
|
+
openscad-mcp = "openscad_mcp.server:main"
|
|
17
|
+
|
|
18
|
+
[dependency-groups]
|
|
19
|
+
dev = ["pytest>=8,<10", "pytest-asyncio>=1,<2", "trimesh>=4,<5", "pillow>=11,<13", "ruff>=0.12,<1"]
|
|
20
|
+
|
|
21
|
+
[tool.hatch.build.targets.wheel]
|
|
22
|
+
packages = ["src/openscad_mcp"]
|
|
23
|
+
|
|
24
|
+
[tool.pytest.ini_options]
|
|
25
|
+
testpaths = ["tests"]
|
|
26
|
+
asyncio_mode = "auto"
|
|
27
|
+
markers = ["integration: requires the real OpenSCAD executable"]
|
|
28
|
+
|
|
29
|
+
[tool.ruff]
|
|
30
|
+
include = ["src/openscad_mcp/**/*.py", "tests/**/*.py", "scripts/**/*.py"]
|
|
31
|
+
|
|
32
|
+
[tool.ruff.lint]
|
|
33
|
+
select = ["E4", "E7", "E9", "F", "I"]
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""Exercise the built image through real stdio MCP, without network or a GPU."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import asyncio
|
|
5
|
+
import base64
|
|
6
|
+
import hashlib
|
|
7
|
+
import io
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import subprocess
|
|
11
|
+
import tempfile
|
|
12
|
+
import uuid
|
|
13
|
+
import zipfile
|
|
14
|
+
from contextlib import asynccontextmanager
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
import trimesh
|
|
18
|
+
from mcp import ClientSession, StdioServerParameters
|
|
19
|
+
from mcp.client.stdio import stdio_client
|
|
20
|
+
from PIL import Image
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def docker(*args, check=True):
|
|
24
|
+
return subprocess.run(
|
|
25
|
+
["docker", *args], capture_output=True, text=True, check=check
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def data(result):
|
|
30
|
+
assert not result.isError, result
|
|
31
|
+
return result.structuredContent or json.loads(result.content[0].text)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@asynccontextmanager
|
|
35
|
+
async def session_for(image, volume):
|
|
36
|
+
name = f"openscad-test-{uuid.uuid4().hex}"
|
|
37
|
+
params = StdioServerParameters(
|
|
38
|
+
command="docker",
|
|
39
|
+
args=[
|
|
40
|
+
"run",
|
|
41
|
+
"--rm",
|
|
42
|
+
"-i",
|
|
43
|
+
"--name",
|
|
44
|
+
name,
|
|
45
|
+
"--network",
|
|
46
|
+
"none",
|
|
47
|
+
"-v",
|
|
48
|
+
f"{volume}:/data",
|
|
49
|
+
image,
|
|
50
|
+
],
|
|
51
|
+
env=dict(os.environ),
|
|
52
|
+
)
|
|
53
|
+
try:
|
|
54
|
+
async with stdio_client(params) as (read, write):
|
|
55
|
+
async with ClientSession(read, write) as session:
|
|
56
|
+
await session.initialize()
|
|
57
|
+
assert docker("exec", name, "id", "-u").stdout.strip() == "10001"
|
|
58
|
+
yield session, name
|
|
59
|
+
finally:
|
|
60
|
+
docker("rm", "-f", name, check=False)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
async def check(image):
|
|
64
|
+
volume = f"openscad-test-{uuid.uuid4().hex}"
|
|
65
|
+
docker("volume", "create", volume)
|
|
66
|
+
try:
|
|
67
|
+
async with session_for(image, volume) as (session, name):
|
|
68
|
+
assert len((await session.list_tools()).tools) == 8
|
|
69
|
+
capabilities = data(await session.call_tool("get_capabilities", {}))
|
|
70
|
+
model = data(
|
|
71
|
+
await session.call_tool(
|
|
72
|
+
"create_3d_model",
|
|
73
|
+
{
|
|
74
|
+
"model_type": "box",
|
|
75
|
+
"parameters": {
|
|
76
|
+
"width": 40,
|
|
77
|
+
"depth": 30,
|
|
78
|
+
"height": 20,
|
|
79
|
+
"thickness": 2,
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
model_id = model["model_id"]
|
|
85
|
+
preview_hashes = []
|
|
86
|
+
for view in ("perspective", "front", "top", "right"):
|
|
87
|
+
result = await session.call_tool(
|
|
88
|
+
"get_model_preview", {"model_id": model_id, "view": view}
|
|
89
|
+
)
|
|
90
|
+
assert not result.isError, result
|
|
91
|
+
block = next(block for block in result.content if block.type == "image")
|
|
92
|
+
png = base64.b64decode(block.data)
|
|
93
|
+
with Image.open(io.BytesIO(png)) as rendered:
|
|
94
|
+
assert rendered.size == (800, 600)
|
|
95
|
+
assert len(rendered.getcolors(800 * 600)) >= 2
|
|
96
|
+
preview_hashes.append(hashlib.sha256(png).hexdigest())
|
|
97
|
+
assert len(set(preview_hashes)) == 4
|
|
98
|
+
with tempfile.TemporaryDirectory() as temporary:
|
|
99
|
+
for format in ("stl", "scad", "csg", "3mf"):
|
|
100
|
+
exported = data(
|
|
101
|
+
await session.call_tool(
|
|
102
|
+
"export_model", {"model_id": model_id, "format": format}
|
|
103
|
+
)
|
|
104
|
+
)
|
|
105
|
+
local = Path(temporary) / f"model.{format}"
|
|
106
|
+
docker("cp", f"{name}:{exported['model_file']}", str(local))
|
|
107
|
+
assert local.stat().st_size > 0
|
|
108
|
+
if format == "stl":
|
|
109
|
+
mesh = trimesh.load_mesh(local)
|
|
110
|
+
assert mesh.is_watertight
|
|
111
|
+
assert mesh.extents.tolist() == [40, 30, 20]
|
|
112
|
+
assert abs(mesh.volume - (40 * 30 * 20 - 36 * 26 * 18)) < 0.01
|
|
113
|
+
elif format == "3mf":
|
|
114
|
+
with zipfile.ZipFile(local) as archive:
|
|
115
|
+
assert any(p.endswith(".model") for p in archive.namelist())
|
|
116
|
+
source = data(
|
|
117
|
+
await session.call_tool("get_model_source", {"model_id": model_id})
|
|
118
|
+
)
|
|
119
|
+
assert "width = 40;" in source["scad_code"]
|
|
120
|
+
modified = data(
|
|
121
|
+
await session.call_tool(
|
|
122
|
+
"modify_3d_model",
|
|
123
|
+
{
|
|
124
|
+
"model_id": model_id,
|
|
125
|
+
"parameters": {"height": 25},
|
|
126
|
+
"expected_revision": source["revision"],
|
|
127
|
+
},
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
assert modified["parameters"]["height"] == 25
|
|
131
|
+
stale = await session.call_tool(
|
|
132
|
+
"modify_3d_model",
|
|
133
|
+
{
|
|
134
|
+
"model_id": model_id,
|
|
135
|
+
"parameters": {"height": 50},
|
|
136
|
+
"expected_revision": source["revision"],
|
|
137
|
+
},
|
|
138
|
+
)
|
|
139
|
+
assert stale.isError
|
|
140
|
+
failed = await session.call_tool(
|
|
141
|
+
"modify_3d_model",
|
|
142
|
+
{"model_id": model_id, "scad_code": "invalid SCAD !!!"},
|
|
143
|
+
)
|
|
144
|
+
assert failed.isError
|
|
145
|
+
assert (
|
|
146
|
+
data(await session.call_tool("get_model", {"model_id": model_id}))[
|
|
147
|
+
"revision"
|
|
148
|
+
]
|
|
149
|
+
== modified["revision"]
|
|
150
|
+
)
|
|
151
|
+
custom = data(
|
|
152
|
+
await session.call_tool(
|
|
153
|
+
"create_model_from_scad",
|
|
154
|
+
{"scad_code": "// café\nsize=4; cube(size);"},
|
|
155
|
+
)
|
|
156
|
+
)
|
|
157
|
+
custom_source = data(
|
|
158
|
+
await session.call_tool(
|
|
159
|
+
"get_model_source", {"model_id": custom["model_id"]}
|
|
160
|
+
)
|
|
161
|
+
)
|
|
162
|
+
assert custom_source["scad_code"] == "// café\nsize=4; cube(size);"
|
|
163
|
+
edited_custom = data(
|
|
164
|
+
await session.call_tool(
|
|
165
|
+
"modify_3d_model",
|
|
166
|
+
{
|
|
167
|
+
"model_id": custom["model_id"],
|
|
168
|
+
"scad_code": custom_source["scad_code"].replace(
|
|
169
|
+
"size=4", "size=6"
|
|
170
|
+
),
|
|
171
|
+
"expected_revision": custom_source["revision"],
|
|
172
|
+
},
|
|
173
|
+
)
|
|
174
|
+
)
|
|
175
|
+
with tempfile.TemporaryDirectory() as temporary:
|
|
176
|
+
path = Path(temporary) / "custom.stl"
|
|
177
|
+
docker("cp", f"{name}:{edited_custom['model_file']}", str(path))
|
|
178
|
+
assert abs(trimesh.load_mesh(path).volume - 216) < 0.01
|
|
179
|
+
# A fresh container reuses the named volume and recovers the successful edit.
|
|
180
|
+
async with session_for(image, volume) as (session, name):
|
|
181
|
+
restored = data(
|
|
182
|
+
await session.call_tool("get_model", {"model_id": model_id})
|
|
183
|
+
)
|
|
184
|
+
assert restored["parameters"]["height"] == 25
|
|
185
|
+
assert restored["revision"] == modified["revision"]
|
|
186
|
+
preview = await session.call_tool(
|
|
187
|
+
"get_model_preview", {"model_id": model_id}
|
|
188
|
+
)
|
|
189
|
+
assert not preview.isError
|
|
190
|
+
print(
|
|
191
|
+
json.dumps(
|
|
192
|
+
{
|
|
193
|
+
"image": image,
|
|
194
|
+
"openscad": capabilities["openscad"],
|
|
195
|
+
"result": "passed",
|
|
196
|
+
"checks": [
|
|
197
|
+
"nonroot",
|
|
198
|
+
"network disabled",
|
|
199
|
+
"MCP handshake and tools",
|
|
200
|
+
"geometry and volume",
|
|
201
|
+
"four PNG views",
|
|
202
|
+
"four exports",
|
|
203
|
+
"edit",
|
|
204
|
+
"SCAD source round trip",
|
|
205
|
+
"stale-edit rejection",
|
|
206
|
+
"failed-edit rollback",
|
|
207
|
+
"container restart persistence",
|
|
208
|
+
],
|
|
209
|
+
},
|
|
210
|
+
indent=2,
|
|
211
|
+
)
|
|
212
|
+
)
|
|
213
|
+
finally:
|
|
214
|
+
docker("volume", "rm", volume)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
if __name__ == "__main__":
|
|
218
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
219
|
+
parser.add_argument("--image", default="openscad-mcp:local")
|
|
220
|
+
asyncio.run(check(parser.parse_args().image))
|