@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,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,2 @@
1
+ # Install the supported server. Development: uv sync --group dev
2
+ .
@@ -0,0 +1,2 @@
1
+ // Simple cube without dependencies
2
+ cube([20, 20, 20], center=true);
@@ -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))
@@ -0,0 +1,3 @@
1
+ """Local OpenSCAD MCP server."""
2
+
3
+ __version__ = "0.2.0"
@@ -0,0 +1,3 @@
1
+ from .server import main
2
+
3
+ main()