@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,314 @@
1
+ """Real subprocesses, real MCP clients, real HTTP downloads, real OpenSCAD."""
2
+
3
+ import asyncio
4
+ import base64
5
+ import io
6
+ import json
7
+ import os
8
+ import socket
9
+ import subprocess
10
+ import sys
11
+ from contextlib import asynccontextmanager
12
+ from pathlib import Path
13
+
14
+ import httpx
15
+ import pytest
16
+ import trimesh
17
+ from mcp import ClientSession, StdioServerParameters
18
+ from mcp.client.stdio import stdio_client
19
+ from mcp.client.streamable_http import streamable_http_client
20
+ from PIL import Image
21
+
22
+ pytestmark = pytest.mark.integration
23
+
24
+
25
+ def tool_data(result):
26
+ assert not result.isError, result
27
+ return result.structuredContent or json.loads(result.content[0].text)
28
+
29
+
30
+ @asynccontextmanager
31
+ async def http_server(tmp_path):
32
+ with socket.socket() as sock:
33
+ sock.bind(("127.0.0.1", 0))
34
+ port = sock.getsockname()[1]
35
+ log = (tmp_path / "server.log").open("w+")
36
+ process = subprocess.Popen(
37
+ [
38
+ sys.executable,
39
+ "-m",
40
+ "openscad_mcp",
41
+ "--transport",
42
+ "http",
43
+ "--port",
44
+ str(port),
45
+ "--output-dir",
46
+ str(tmp_path / "models"),
47
+ ],
48
+ cwd=tmp_path,
49
+ stdout=log,
50
+ stderr=log,
51
+ )
52
+ base = f"http://127.0.0.1:{port}"
53
+ try:
54
+ async with httpx.AsyncClient(base_url=base, timeout=180) as client:
55
+ for _ in range(150):
56
+ if process.poll() is not None:
57
+ log.seek(0)
58
+ pytest.fail(log.read())
59
+ try:
60
+ if (await client.get("/health")).status_code == 200:
61
+ break
62
+ except httpx.ConnectError:
63
+ pass
64
+ await asyncio.sleep(0.1)
65
+ else:
66
+ pytest.fail("Server failed to become healthy")
67
+ yield base, client
68
+ finally:
69
+ process.terminate()
70
+ try:
71
+ process.wait(timeout=10)
72
+ except subprocess.TimeoutExpired:
73
+ process.kill()
74
+ process.wait(timeout=5)
75
+ log.close()
76
+
77
+
78
+ async def test_stdio_end_to_end(tmp_path):
79
+ params = StdioServerParameters(
80
+ command=sys.executable,
81
+ args=["-m", "openscad_mcp", "--output-dir", str(tmp_path / "models")],
82
+ cwd=str(tmp_path),
83
+ env=dict(os.environ),
84
+ )
85
+ async with stdio_client(params) as (read, write):
86
+ async with ClientSession(read, write) as session:
87
+ await session.initialize()
88
+ listed = await session.list_tools()
89
+ assert {tool.name for tool in listed.tools} == {
90
+ "create_3d_model",
91
+ "create_model_from_scad",
92
+ "modify_3d_model",
93
+ "get_model",
94
+ "get_model_source",
95
+ "get_model_preview",
96
+ "get_capabilities",
97
+ "export_model",
98
+ }
99
+ caps = tool_data(await session.call_tool("get_capabilities", {}))
100
+ assert "OpenSCAD" in caps["openscad"]
101
+ model = tool_data(
102
+ await session.call_tool(
103
+ "create_3d_model",
104
+ {
105
+ "model_type": "cube",
106
+ "parameters": {
107
+ "width": 12,
108
+ "depth": 15,
109
+ "height": 9,
110
+ "center": True,
111
+ },
112
+ },
113
+ )
114
+ )
115
+ model_id = model["model_id"]
116
+ mesh = trimesh.load_mesh(model["model_file"])
117
+ assert mesh.volume == pytest.approx(1620)
118
+ assert mesh.bounds.mean(axis=0).tolist() == pytest.approx([0, 0, 0])
119
+ preview = await session.call_tool(
120
+ "get_model_preview", {"model_id": model_id}
121
+ )
122
+ assert not preview.isError
123
+ image = next(block for block in preview.content if block.type == "image")
124
+ with Image.open(io.BytesIO(base64.b64decode(image.data))) as png:
125
+ assert png.size == (800, 600)
126
+ source = tool_data(
127
+ await session.call_tool("get_model_source", {"model_id": model_id})
128
+ )
129
+ assert source["scad_code"] == Path(model["scad_file"]).read_text()
130
+ assert source["revision"] == model["revision"]
131
+ modified = tool_data(
132
+ await session.call_tool(
133
+ "modify_3d_model",
134
+ {
135
+ "model_id": model_id,
136
+ "parameters": {"height": 18},
137
+ "expected_revision": source["revision"],
138
+ },
139
+ )
140
+ )
141
+ assert trimesh.load_mesh(modified["model_file"]).volume == pytest.approx(
142
+ 3240
143
+ )
144
+ stale = await session.call_tool(
145
+ "modify_3d_model",
146
+ {
147
+ "model_id": model_id,
148
+ "parameters": {"height": 99},
149
+ "expected_revision": source["revision"],
150
+ },
151
+ )
152
+ assert stale.isError
153
+ assert "Model changed" in stale.content[0].text
154
+ exported = tool_data(
155
+ await session.call_tool(
156
+ "export_model", {"model_id": model_id, "format": "3mf"}
157
+ )
158
+ )
159
+ assert Path(exported["model_file"]).read_bytes().startswith(b"PK")
160
+ assert (
161
+ await session.call_tool(
162
+ "create_model_from_scad", {"scad_code": "bad SCAD !!!"}
163
+ )
164
+ ).isError
165
+ assert (
166
+ await session.call_tool("get_model", {"model_id": "../../etc/passwd"})
167
+ ).isError
168
+ # New process must recover metadata and artifacts from disk.
169
+ async with stdio_client(params) as (read, write):
170
+ async with ClientSession(read, write) as session:
171
+ await session.initialize()
172
+ persisted = tool_data(
173
+ await session.call_tool("get_model", {"model_id": model_id})
174
+ )
175
+ assert persisted["parameters"]["height"] == 18
176
+ saved_source = tool_data(
177
+ await session.call_tool("get_model_source", {"model_id": model_id})
178
+ )
179
+ assert saved_source["revision"] == persisted["revision"]
180
+ assert "height = 18;" in saved_source["scad_code"]
181
+
182
+
183
+ async def test_http_mcp_and_downloads(tmp_path):
184
+ async with http_server(tmp_path) as (base, client):
185
+ assert (await client.get("/")).json()["mcp_endpoint"] == "/mcp"
186
+ async with streamable_http_client(base + "/mcp") as (read, write, _):
187
+ async with ClientSession(read, write) as session:
188
+ await session.initialize()
189
+ assert len((await session.list_tools()).tools) == 8
190
+ model = tool_data(
191
+ await session.call_tool(
192
+ "create_model_from_scad",
193
+ {
194
+ "scad_code": "difference() { cube([30,20,8]); translate([15,10,-1]) cylinder(h=10,r=4,$fn=48); }",
195
+ "description": '<script>alert("x")</script>',
196
+ },
197
+ )
198
+ )
199
+ source = tool_data(
200
+ await session.call_tool(
201
+ "get_model_source", {"model_id": model["model_id"]}
202
+ )
203
+ )
204
+ assert source["scad_code"].startswith("difference()")
205
+ changed = tool_data(
206
+ await session.call_tool(
207
+ "modify_3d_model",
208
+ {
209
+ "model_id": model["model_id"],
210
+ "scad_code": source["scad_code"].replace(
211
+ "[30,20,8]", "[36,20,8]"
212
+ ),
213
+ "expected_revision": source["revision"],
214
+ },
215
+ )
216
+ )
217
+ fetched = await client.post(
218
+ "/tool_call",
219
+ json={
220
+ "tool_name": "get_model_source",
221
+ "tool_params": {"model_id": model["model_id"]},
222
+ },
223
+ )
224
+ assert fetched.status_code == 200
225
+ assert "[36,20,8]" in fetched.json()["scad_code"]
226
+ conflict = await client.post(
227
+ "/tool_call",
228
+ json={
229
+ "tool_name": "modify_3d_model",
230
+ "tool_params": {
231
+ "model_id": model["model_id"],
232
+ "scad_code": "cube(100);",
233
+ "expected_revision": source["revision"],
234
+ },
235
+ },
236
+ )
237
+ assert conflict.status_code == 409
238
+ assert conflict.json()["current_revision"] == changed["revision"]
239
+ page = await client.get(model["preview_url"])
240
+ assert page.status_code == 200
241
+ assert "<script>" not in page.text
242
+ assert "&lt;script&gt;" in page.text
243
+ for path in model["previews"].values():
244
+ result = await client.get(path)
245
+ assert result.status_code == 200
246
+ assert result.headers["content-type"] == "image/png"
247
+ Image.open(io.BytesIO(result.content)).verify()
248
+ for format in ("stl", "scad", "csg", "3mf"):
249
+ response = await client.get(
250
+ f"/download/{model['model_id']}", params={"format": format}
251
+ )
252
+ assert response.status_code == 200, response.text
253
+ assert len(response.content) > 0
254
+ if format == "stl":
255
+ mesh = trimesh.load_mesh(
256
+ io.BytesIO(response.content), file_type="stl"
257
+ )
258
+ assert mesh.extents.tolist() == pytest.approx([36, 20, 8])
259
+ if format == "scad":
260
+ assert response.text.startswith("difference()")
261
+ invalid = await session.call_tool(
262
+ "export_model", {"model_id": model["model_id"], "format": "step"}
263
+ )
264
+ assert invalid.isError
265
+ response = await client.post(
266
+ "/tool_call",
267
+ json={
268
+ "tool_name": "create_3d_model",
269
+ "tool_params": {"description": "sphere radius 7 mm"},
270
+ },
271
+ )
272
+ assert response.status_code == 200, response.text
273
+ assert response.json()["parameters"]["radius"] == 7
274
+ for payload, status in [
275
+ ({"tool_name": "unknown"}, 404),
276
+ ({"tool_name": "modify_3d_model", "tool_params": {}}, 422),
277
+ (
278
+ {
279
+ "tool_name": "create_3d_model",
280
+ "tool_params": {"model_type": "cube", "unknown": 1},
281
+ },
282
+ 422,
283
+ ),
284
+ (
285
+ {
286
+ "tool_name": "create_model_from_scad",
287
+ "tool_params": {"scad_code": "bad !!!"},
288
+ },
289
+ 422,
290
+ ),
291
+ ({"tool_name": "get_model", "tool_params": {"model_id": "bad"}}, 404),
292
+ (
293
+ {"tool_name": "get_model_source", "tool_params": {"model_id": "bad"}},
294
+ 404,
295
+ ),
296
+ ]:
297
+ assert (await client.post("/tool_call", json=payload)).status_code == status
298
+ assert (
299
+ await client.post(
300
+ "/tool_call",
301
+ content="{broken",
302
+ headers={"Content-Type": "application/json"},
303
+ )
304
+ ).status_code == 422
305
+ assert (
306
+ await client.get("/", headers={"Host": "evil.example"})
307
+ ).status_code == 400
308
+ assert (
309
+ await client.post(
310
+ "/tool_call",
311
+ json={"tool_name": "get_capabilities"},
312
+ headers={"Origin": "https://evil.example"},
313
+ )
314
+ ).status_code == 403