@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,1456 @@
1
+ import os
2
+ import logging
3
+ import uuid
4
+ import json
5
+ from typing import Dict, Any, List, Optional, Tuple
6
+ from fastapi import FastAPI, Request, Response, HTTPException
7
+ from fastapi.responses import JSONResponse, FileResponse
8
+ from fastapi.middleware.cors import CORSMiddleware
9
+ from fastapi.staticfiles import StaticFiles
10
+ from fastapi.templating import Jinja2Templates
11
+ import uvicorn
12
+ from mcp import MCPServer, MCPTool, MCPToolCall, MCPToolCallResult
13
+
14
+ # Import configuration
15
+ from src.config import *
16
+
17
+ # Import components
18
+ from src.nlp.parameter_extractor import ParameterExtractor
19
+ from src.models.code_generator import CodeGenerator
20
+ from src.openscad_wrapper.wrapper import OpenSCADWrapper
21
+ from src.utils.cad_exporter import CADExporter
22
+ from src.visualization.headless_renderer import HeadlessRenderer
23
+ from src.printer_discovery.printer_discovery import PrinterDiscovery, PrinterInterface
24
+ from src.ai.venice_api import VeniceImageGenerator
25
+ from src.ai.sam_segmentation import SAMSegmenter
26
+
27
+ # Configure logging
28
+ logging.basicConfig(
29
+ level=logging.INFO,
30
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
31
+ )
32
+ logger = logging.getLogger(__name__)
33
+
34
+ # Create FastAPI app
35
+ app = FastAPI(title="OpenSCAD MCP Server")
36
+
37
+ # Add CORS middleware
38
+ app.add_middleware(
39
+ CORSMiddleware,
40
+ allow_origins=["*"],
41
+ allow_credentials=True,
42
+ allow_methods=["*"],
43
+ allow_headers=["*"],
44
+ )
45
+
46
+ # Create directories
47
+ os.makedirs("scad", exist_ok=True)
48
+ os.makedirs("output", exist_ok=True)
49
+ os.makedirs("output/models", exist_ok=True)
50
+ os.makedirs("output/preview", exist_ok=True)
51
+ os.makedirs("templates", exist_ok=True)
52
+ os.makedirs("static", exist_ok=True)
53
+
54
+ # Initialize components
55
+ parameter_extractor = ParameterExtractor()
56
+ code_generator = CodeGenerator("scad", "output")
57
+ openscad_wrapper = OpenSCADWrapper("scad", "output")
58
+ cad_exporter = CADExporter()
59
+ headless_renderer = HeadlessRenderer()
60
+ printer_discovery = PrinterDiscovery()
61
+
62
+ # Initialize AI components
63
+ venice_generator = VeniceImageGenerator(VENICE_API_KEY, IMAGES_DIR)
64
+ gemini_generator = GeminiImageGenerator(GEMINI_API_KEY, IMAGES_DIR)
65
+ cuda_mvs = CUDAMultiViewStereo(CUDA_MVS_PATH, MODELS_DIR, use_gpu=CUDA_MVS_USE_GPU)
66
+ image_approval = ImageApprovalTool(APPROVED_IMAGES_DIR)
67
+
68
+ # Initialize remote processing components if enabled
69
+ remote_connection_manager = None
70
+ if REMOTE_CUDA_MVS["ENABLED"]:
71
+ logger.info("Initializing remote CUDA MVS connection manager")
72
+ remote_connection_manager = CUDAMVSConnectionManager(
73
+ api_key=REMOTE_CUDA_MVS["API_KEY"],
74
+ discovery_port=REMOTE_CUDA_MVS["DISCOVERY_PORT"],
75
+ use_lan_discovery=REMOTE_CUDA_MVS["USE_LAN_DISCOVERY"],
76
+ server_url=REMOTE_CUDA_MVS["SERVER_URL"] if REMOTE_CUDA_MVS["SERVER_URL"] else None
77
+ )
78
+
79
+ # Initialize workflow pipeline
80
+ multi_view_pipeline = MultiViewToModelPipeline(
81
+ gemini_generator=gemini_generator,
82
+ cuda_mvs=cuda_mvs,
83
+ approval_tool=image_approval,
84
+ output_dir=OUTPUT_DIR
85
+ )
86
+
87
+ # SAM2 segmenter will be initialized on first use to avoid loading the model unnecessarily
88
+ sam_segmenter = None
89
+
90
+ def get_sam_segmenter():
91
+ """
92
+ Get or initialize the SAM2 segmenter.
93
+
94
+ Returns:
95
+ SAMSegmenter instance
96
+ """
97
+ global sam_segmenter
98
+ if sam_segmenter is None:
99
+ logger.info("Initializing SAM2 segmenter")
100
+ sam_segmenter = SAMSegmenter(
101
+ model_type=SAM2_MODEL_TYPE,
102
+ checkpoint_path=SAM2_CHECKPOINT_PATH,
103
+ use_gpu=SAM2_USE_GPU,
104
+ output_dir=MASKS_DIR
105
+ )
106
+ return sam_segmenter
107
+
108
+ # Store models in memory
109
+ models = {}
110
+ printers = {}
111
+ approved_images = {}
112
+ remote_jobs = {}
113
+
114
+ # Create MCP server
115
+ mcp_server = MCPServer()
116
+
117
+ # Mount static files
118
+ app.mount("/static", StaticFiles(directory="static"), name="static")
119
+
120
+ # Create Jinja2 templates
121
+ templates = Jinja2Templates(directory="templates")
122
+
123
+ # Create model preview template
124
+ with open("templates/preview.html", "w") as f:
125
+ f.write("""
126
+ <!DOCTYPE html>
127
+ <html lang="en">
128
+ <head>
129
+ <meta charset="UTF-8">
130
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
131
+ <title>OpenSCAD Model Preview</title>
132
+ <style>
133
+ body {
134
+ font-family: Arial, sans-serif;
135
+ margin: 0;
136
+ padding: 0;
137
+ background-color: #f5f5f5;
138
+ }
139
+ .container {
140
+ max-width: 1200px;
141
+ margin: 0 auto;
142
+ padding: 20px;
143
+ }
144
+ h1 {
145
+ color: #333;
146
+ margin-bottom: 20px;
147
+ }
148
+ .preview-container {
149
+ display: flex;
150
+ flex-wrap: wrap;
151
+ gap: 20px;
152
+ margin-top: 20px;
153
+ }
154
+ .preview-image {
155
+ border: 1px solid #ddd;
156
+ border-radius: 5px;
157
+ padding: 10px;
158
+ background-color: white;
159
+ }
160
+ .preview-image img {
161
+ max-width: 100%;
162
+ height: auto;
163
+ }
164
+ .preview-image h3 {
165
+ margin-top: 10px;
166
+ margin-bottom: 5px;
167
+ color: #555;
168
+ }
169
+ .parameters {
170
+ margin-top: 20px;
171
+ background-color: #f9f9f9;
172
+ padding: 15px;
173
+ border-radius: 5px;
174
+ border: 1px solid #eee;
175
+ }
176
+ .parameters h2 {
177
+ margin-top: 0;
178
+ color: #333;
179
+ }
180
+ .parameters table {
181
+ width: 100%;
182
+ border-collapse: collapse;
183
+ }
184
+ .parameters table th, .parameters table td {
185
+ padding: 8px;
186
+ text-align: left;
187
+ border-bottom: 1px solid #ddd;
188
+ }
189
+ .parameters table th {
190
+ background-color: #f2f2f2;
191
+ }
192
+ .actions {
193
+ margin-top: 20px;
194
+ display: flex;
195
+ gap: 10px;
196
+ }
197
+ .actions button {
198
+ padding: 10px 15px;
199
+ background-color: #4CAF50;
200
+ color: white;
201
+ border: none;
202
+ border-radius: 4px;
203
+ cursor: pointer;
204
+ }
205
+ .actions button:hover {
206
+ background-color: #45a049;
207
+ }
208
+ </style>
209
+ </head>
210
+ <body>
211
+ <div class="container">
212
+ <h1>OpenSCAD Model Preview: {{ model_id }}</h1>
213
+
214
+ <div class="parameters">
215
+ <h2>Parameters</h2>
216
+ <table>
217
+ <tr>
218
+ <th>Parameter</th>
219
+ <th>Value</th>
220
+ </tr>
221
+ {% for key, value in parameters.items() %}
222
+ <tr>
223
+ <td>{{ key }}</td>
224
+ <td>{{ value }}</td>
225
+ </tr>
226
+ {% endfor %}
227
+ </table>
228
+ </div>
229
+
230
+ <div class="preview-container">
231
+ {% for view, image_path in previews.items() %}
232
+ <div class="preview-image">
233
+ <h3>{{ view|title }} View</h3>
234
+ <img src="{{ image_path }}" alt="{{ view }} view">
235
+ </div>
236
+ {% endfor %}
237
+ </div>
238
+
239
+ <div class="actions">
240
+ <button onclick="window.location.href='/download/{{ model_id }}'">Download Model</button>
241
+ </div>
242
+ </div>
243
+ </body>
244
+ </html>
245
+ """)
246
+
247
+ # Define MCP tools
248
+ @mcp_server.tool
249
+ def create_3d_model(description: str) -> Dict[str, Any]:
250
+ """
251
+ Create a 3D model from a natural language description.
252
+
253
+ Args:
254
+ description: Natural language description of the 3D model
255
+
256
+ Returns:
257
+ Dictionary with model information
258
+ """
259
+ # Extract parameters from description
260
+ model_type, parameters = parameter_extractor.extract_parameters(description)
261
+
262
+ # Generate a unique model ID
263
+ model_id = str(uuid.uuid4())
264
+
265
+ # Generate OpenSCAD code
266
+ scad_code = code_generator.generate_code(model_type, parameters)
267
+
268
+ # Save the SCAD file
269
+ scad_file = openscad_wrapper.generate_scad(scad_code, model_id)
270
+
271
+ # Generate preview images
272
+ previews = openscad_wrapper.generate_multi_angle_previews(scad_file, parameters)
273
+
274
+ # Export to parametric format (CSG by default)
275
+ success, model_file, error = cad_exporter.export_model(
276
+ scad_file,
277
+ "csg",
278
+ parameters,
279
+ metadata={
280
+ "description": description,
281
+ "model_type": model_type,
282
+ }
283
+ )
284
+
285
+ # Store model information
286
+ models[model_id] = {
287
+ "id": model_id,
288
+ "type": model_type,
289
+ "parameters": parameters,
290
+ "description": description,
291
+ "scad_file": scad_file,
292
+ "model_file": model_file if success else None,
293
+ "previews": previews,
294
+ "format": "csg"
295
+ }
296
+
297
+ # Create response
298
+ response = {
299
+ "model_id": model_id,
300
+ "model_type": model_type,
301
+ "parameters": parameters,
302
+ "preview_url": f"/ui/preview/{model_id}",
303
+ "supported_formats": cad_exporter.get_supported_formats()
304
+ }
305
+
306
+ return response
307
+
308
+ @mcp_server.tool
309
+ def modify_3d_model(model_id: str, modifications: str) -> Dict[str, Any]:
310
+ """
311
+ Modify an existing 3D model.
312
+
313
+ Args:
314
+ model_id: ID of the model to modify
315
+ modifications: Natural language description of the modifications
316
+
317
+ Returns:
318
+ Dictionary with updated model information
319
+ """
320
+ # Check if model exists
321
+ if model_id not in models:
322
+ raise ValueError(f"Model with ID {model_id} not found")
323
+
324
+ # Get existing model information
325
+ model_info = models[model_id]
326
+
327
+ # Extract parameters from modifications
328
+ _, new_parameters = parameter_extractor.extract_parameters(
329
+ modifications,
330
+ model_type=model_info["type"],
331
+ existing_parameters=model_info["parameters"]
332
+ )
333
+
334
+ # Generate OpenSCAD code with updated parameters
335
+ scad_code = code_generator.generate_code(model_info["type"], new_parameters)
336
+
337
+ # Save the SCAD file
338
+ scad_file = openscad_wrapper.generate_scad(scad_code, model_id)
339
+
340
+ # Generate preview images
341
+ previews = openscad_wrapper.generate_multi_angle_previews(scad_file, new_parameters)
342
+
343
+ # Export to parametric format (same as original)
344
+ success, model_file, error = cad_exporter.export_model(
345
+ scad_file,
346
+ model_info["format"],
347
+ new_parameters,
348
+ metadata={
349
+ "description": model_info["description"] + " | " + modifications,
350
+ "model_type": model_info["type"],
351
+ }
352
+ )
353
+
354
+ # Update model information
355
+ models[model_id] = {
356
+ "id": model_id,
357
+ "type": model_info["type"],
358
+ "parameters": new_parameters,
359
+ "description": model_info["description"] + " | " + modifications,
360
+ "scad_file": scad_file,
361
+ "model_file": model_file if success else None,
362
+ "previews": previews,
363
+ "format": model_info["format"]
364
+ }
365
+
366
+ # Create response
367
+ response = {
368
+ "model_id": model_id,
369
+ "model_type": model_info["type"],
370
+ "parameters": new_parameters,
371
+ "preview_url": f"/ui/preview/{model_id}",
372
+ "supported_formats": cad_exporter.get_supported_formats()
373
+ }
374
+
375
+ return response
376
+
377
+ @mcp_server.tool
378
+ def export_model(model_id: str, format: str = "csg") -> Dict[str, Any]:
379
+ """
380
+ Export a 3D model to a specific format.
381
+
382
+ Args:
383
+ model_id: ID of the model to export
384
+ format: Format to export to (csg, stl, obj, etc.)
385
+
386
+ Returns:
387
+ Dictionary with export information
388
+ """
389
+ # Check if model exists
390
+ if model_id not in models:
391
+ raise ValueError(f"Model with ID {model_id} not found")
392
+
393
+ # Get model information
394
+ model_info = models[model_id]
395
+
396
+ # Check if format is supported
397
+ supported_formats = cad_exporter.get_supported_formats()
398
+ if format not in supported_formats:
399
+ raise ValueError(f"Format {format} not supported. Supported formats: {', '.join(supported_formats)}")
400
+
401
+ # Export model
402
+ success, model_file, error = cad_exporter.export_model(
403
+ model_info["scad_file"],
404
+ format,
405
+ model_info["parameters"],
406
+ metadata={
407
+ "description": model_info["description"],
408
+ "model_type": model_info["type"],
409
+ }
410
+ )
411
+
412
+ if not success:
413
+ raise ValueError(f"Failed to export model: {error}")
414
+
415
+ # Update model information
416
+ models[model_id]["model_file"] = model_file
417
+ models[model_id]["format"] = format
418
+
419
+ # Create response
420
+ response = {
421
+ "model_id": model_id,
422
+ "format": format,
423
+ "model_file": model_file,
424
+ "download_url": f"/download/{model_id}"
425
+ }
426
+
427
+ return response
428
+
429
+ @mcp_server.tool
430
+ def discover_printers() -> Dict[str, Any]:
431
+ """
432
+ Discover 3D printers on the network.
433
+
434
+ Returns:
435
+ Dictionary with discovered printers
436
+ """
437
+ # Discover printers
438
+ discovered_printers = printer_discovery.discover_printers()
439
+
440
+ # Store printers
441
+ for printer in discovered_printers:
442
+ printers[printer["id"]] = printer
443
+
444
+ # Create response
445
+ response = {
446
+ "printers": discovered_printers
447
+ }
448
+
449
+ return response
450
+
451
+ @mcp_server.tool
452
+ def connect_to_printer(printer_id: str) -> Dict[str, Any]:
453
+ """
454
+ Connect to a 3D printer.
455
+
456
+ Args:
457
+ printer_id: ID of the printer to connect to
458
+
459
+ Returns:
460
+ Dictionary with connection information
461
+ """
462
+ # Check if printer exists
463
+ if printer_id not in printers:
464
+ raise ValueError(f"Printer with ID {printer_id} not found")
465
+
466
+ # Get printer information
467
+ printer_info = printers[printer_id]
468
+
469
+ # Connect to printer
470
+ printer_interface = PrinterInterface(printer_info)
471
+ success, error = printer_interface.connect()
472
+
473
+ if not success:
474
+ raise ValueError(f"Failed to connect to printer: {error}")
475
+
476
+ # Update printer information
477
+ printers[printer_id]["connected"] = True
478
+ printers[printer_id]["interface"] = printer_interface
479
+
480
+ # Create response
481
+ response = {
482
+ "printer_id": printer_id,
483
+ "connected": True,
484
+ "printer_info": printer_info
485
+ }
486
+
487
+ return response
488
+
489
+ @mcp_server.tool
490
+ def print_model(model_id: str, printer_id: str) -> Dict[str, Any]:
491
+ """
492
+ Print a 3D model on a connected printer.
493
+
494
+ Args:
495
+ model_id: ID of the model to print
496
+ printer_id: ID of the printer to print on
497
+
498
+ Returns:
499
+ Dictionary with print job information
500
+ """
501
+ # Check if model exists
502
+ if model_id not in models:
503
+ raise ValueError(f"Model with ID {model_id} not found")
504
+
505
+ # Check if printer exists
506
+ if printer_id not in printers:
507
+ raise ValueError(f"Printer with ID {printer_id} not found")
508
+
509
+ # Check if printer is connected
510
+ if not printers[printer_id].get("connected", False):
511
+ raise ValueError(f"Printer with ID {printer_id} is not connected")
512
+
513
+ # Get model and printer information
514
+ model_info = models[model_id]
515
+ printer_info = printers[printer_id]
516
+
517
+ # Check if model has been exported to a printable format
518
+ if not model_info.get("model_file"):
519
+ raise ValueError(f"Model with ID {model_id} has not been exported")
520
+
521
+ # Print model
522
+ printer_interface = printer_info["interface"]
523
+ job_id, error = printer_interface.print_model(model_info["model_file"])
524
+
525
+ if not job_id:
526
+ raise ValueError(f"Failed to print model: {error}")
527
+
528
+ # Create response
529
+ response = {
530
+ "model_id": model_id,
531
+ "printer_id": printer_id,
532
+ "job_id": job_id,
533
+ "status": "printing"
534
+ }
535
+
536
+ return response
537
+
538
+ @mcp_server.tool
539
+ def get_printer_status(printer_id: str) -> Dict[str, Any]:
540
+ """
541
+ Get the status of a printer.
542
+
543
+ Args:
544
+ printer_id: ID of the printer to get status for
545
+
546
+ Returns:
547
+ Dictionary with printer status
548
+ """
549
+ # Check if printer exists
550
+ if printer_id not in printers:
551
+ raise ValueError(f"Printer with ID {printer_id} not found")
552
+
553
+ # Check if printer is connected
554
+ if not printers[printer_id].get("connected", False):
555
+ raise ValueError(f"Printer with ID {printer_id} is not connected")
556
+
557
+ # Get printer information
558
+ printer_info = printers[printer_id]
559
+
560
+ # Get printer status
561
+ printer_interface = printer_info["interface"]
562
+ status = printer_interface.get_status()
563
+
564
+ # Create response
565
+ response = {
566
+ "printer_id": printer_id,
567
+ "status": status
568
+ }
569
+
570
+ return response
571
+
572
+ @mcp_server.tool
573
+ def cancel_print_job(printer_id: str, job_id: str) -> Dict[str, Any]:
574
+ """
575
+ Cancel a print job.
576
+
577
+ Args:
578
+ printer_id: ID of the printer
579
+ job_id: ID of the print job to cancel
580
+
581
+ Returns:
582
+ Dictionary with cancellation information
583
+ """
584
+ # Check if printer exists
585
+ if printer_id not in printers:
586
+ raise ValueError(f"Printer with ID {printer_id} not found")
587
+
588
+ # Check if printer is connected
589
+ if not printers[printer_id].get("connected", False):
590
+ raise ValueError(f"Printer with ID {printer_id} is not connected")
591
+
592
+ # Get printer information
593
+ printer_info = printers[printer_id]
594
+
595
+ # Cancel print job
596
+ printer_interface = printer_info["interface"]
597
+ success, error = printer_interface.cancel_job(job_id)
598
+
599
+ if not success:
600
+ raise ValueError(f"Failed to cancel print job: {error}")
601
+
602
+ # Create response
603
+ response = {
604
+ "printer_id": printer_id,
605
+ "job_id": job_id,
606
+ "status": "cancelled"
607
+ }
608
+
609
+ return response
610
+
611
+ # Add Venice.ai image generation tool
612
+ @mcp_server.tool
613
+ def generate_image(prompt: str, model: str = "fluently-xl") -> Dict[str, Any]:
614
+ """
615
+ Generate an image using Venice.ai's image generation models.
616
+
617
+ Args:
618
+ prompt: Text description for image generation
619
+ model: Model to use (default: fluently-xl). Options include:
620
+ - "fluently-xl" (fastest, 2.30s): Quick generation with good quality
621
+ - "flux-dev" (high quality): Detailed, premium image quality
622
+ - "flux-dev-uncensored": Uncensored version of flux-dev model
623
+ - "stable-diffusion-3.5": Standard stable diffusion model
624
+ - "pony-realism": Specialized for realistic outputs
625
+ - "lustify-sdxl": Artistic stylization model
626
+
627
+ You can also use natural language like:
628
+ - "fastest model", "quick generation", "efficient"
629
+ - "high quality", "detailed", "premium quality"
630
+ - "realistic", "photorealistic"
631
+ - "artistic", "stylized", "creative"
632
+
633
+ Returns:
634
+ Dictionary with image information
635
+ """
636
+ # Generate a unique image ID
637
+ image_id = str(uuid.uuid4())
638
+
639
+ # Generate image
640
+ result = venice_generator.generate_image(prompt, model)
641
+
642
+ # Create response
643
+ response = {
644
+ "image_id": image_id,
645
+ "prompt": prompt,
646
+ "model": model,
647
+ "image_path": result.get("local_path"),
648
+ "image_url": result.get("image_url")
649
+ }
650
+
651
+ return response
652
+
653
+ # Add SAM2 segmentation tool
654
+ @mcp_server.tool
655
+ def segment_image(image_path: str, points: Optional[List[Tuple[int, int]]] = None) -> Dict[str, Any]:
656
+ """
657
+ Segment objects in an image using SAM2 (Segment Anything Model 2).
658
+
659
+ Args:
660
+ image_path: Path to the input image
661
+ points: Optional list of (x, y) points to guide segmentation
662
+ If not provided, automatic segmentation will be used
663
+
664
+ Returns:
665
+ Dictionary with segmentation masks and metadata
666
+ """
667
+ # Get or initialize SAM2 segmenter
668
+ sam_segmenter = get_sam_segmenter()
669
+
670
+ # Generate a unique segmentation ID
671
+ segmentation_id = str(uuid.uuid4())
672
+
673
+ try:
674
+ # Perform segmentation
675
+ if points:
676
+ result = sam_segmenter.segment_image(image_path, points)
677
+ else:
678
+ result = sam_segmenter.segment_with_auto_points(image_path)
679
+
680
+ # Create response
681
+ response = {
682
+ "segmentation_id": segmentation_id,
683
+ "image_path": image_path,
684
+ "mask_paths": result.get("mask_paths", []),
685
+ "num_masks": result.get("num_masks", 0),
686
+ "points_used": points if points else result.get("points", [])
687
+ }
688
+
689
+ return response
690
+ except Exception as e:
691
+ logger.error(f"Error segmenting image: {str(e)}")
692
+ raise HTTPException(status_code=500, detail=f"Error segmenting image: {str(e)}")
693
+
694
+
695
+ # Add Google Gemini image generation tool
696
+ @mcp_server.tool
697
+ def generate_image_gemini(prompt: str, model: str = GEMINI_MODEL) -> Dict[str, Any]:
698
+ """
699
+ Generate an image using Google Gemini's image generation models.
700
+
701
+ Args:
702
+ prompt: Text description for image generation
703
+ model: Model to use (default: gemini-2.0-flash-exp-image-generation)
704
+
705
+ Returns:
706
+ Dictionary with image information
707
+ """
708
+ # Generate a unique image ID
709
+ image_id = str(uuid.uuid4())
710
+
711
+ # Generate image
712
+ result = gemini_generator.generate_image(prompt, model)
713
+
714
+ # Create response
715
+ response = {
716
+ "image_id": image_id,
717
+ "prompt": prompt,
718
+ "model": model,
719
+ "image_path": result.get("local_path"),
720
+ "image_url": f"/images/{os.path.basename(result.get('local_path', ''))}"
721
+ }
722
+
723
+ return response
724
+
725
+
726
+ # Add multi-view image generation tool
727
+ @mcp_server.tool
728
+ def generate_multi_view_images(prompt: str, num_views: int = 4) -> Dict[str, Any]:
729
+ """
730
+ Generate multiple views of the same 3D object using Google Gemini.
731
+
732
+ Args:
733
+ prompt: Text description of the 3D object
734
+ num_views: Number of views to generate (default: 4)
735
+
736
+ Returns:
737
+ Dictionary with multi-view image information
738
+ """
739
+ # Validate number of views
740
+ if num_views < MULTI_VIEW_PIPELINE["MIN_NUM_VIEWS"]:
741
+ raise ValueError(f"Number of views must be at least {MULTI_VIEW_PIPELINE['MIN_NUM_VIEWS']}")
742
+
743
+ if num_views > MULTI_VIEW_PIPELINE["MAX_NUM_VIEWS"]:
744
+ raise ValueError(f"Number of views cannot exceed {MULTI_VIEW_PIPELINE['MAX_NUM_VIEWS']}")
745
+
746
+ # Generate a unique multi-view ID
747
+ multi_view_id = str(uuid.uuid4())
748
+
749
+ # Generate multi-view images
750
+ results = gemini_generator.generate_multiple_views(prompt, num_views)
751
+
752
+ # Create response
753
+ response = {
754
+ "multi_view_id": multi_view_id,
755
+ "prompt": prompt,
756
+ "num_views": num_views,
757
+ "views": [
758
+ {
759
+ "view_id": result.get("view_id", f"view_{i+1}"),
760
+ "view_index": result.get("view_index", i+1),
761
+ "view_direction": result.get("view_direction", ""),
762
+ "image_path": result.get("local_path"),
763
+ "image_url": f"/images/{os.path.basename(result.get('local_path', ''))}"
764
+ }
765
+ for i, result in enumerate(results)
766
+ ],
767
+ "approval_required": IMAGE_APPROVAL["ENABLED"] and not IMAGE_APPROVAL["AUTO_APPROVE"]
768
+ }
769
+
770
+ # Store multi-view information for approval
771
+ if IMAGE_APPROVAL["ENABLED"]:
772
+ approved_images[multi_view_id] = {
773
+ "multi_view_id": multi_view_id,
774
+ "prompt": prompt,
775
+ "num_views": num_views,
776
+ "views": response["views"],
777
+ "approved_views": [] if not IMAGE_APPROVAL["AUTO_APPROVE"] else [view["view_id"] for view in response["views"]],
778
+ "rejected_views": [],
779
+ "approval_complete": IMAGE_APPROVAL["AUTO_APPROVE"]
780
+ }
781
+
782
+ return response
783
+
784
+
785
+ # Add image approval tool
786
+ @mcp_server.tool
787
+ def approve_image(multi_view_id: str, view_id: str) -> Dict[str, Any]:
788
+ """
789
+ Approve an image for 3D model generation.
790
+
791
+ Args:
792
+ multi_view_id: ID of the multi-view set
793
+ view_id: ID of the view to approve
794
+
795
+ Returns:
796
+ Dictionary with approval information
797
+ """
798
+ # Check if multi-view ID exists
799
+ if multi_view_id not in approved_images:
800
+ raise ValueError(f"Multi-view set with ID {multi_view_id} not found")
801
+
802
+ # Get multi-view information
803
+ multi_view_info = approved_images[multi_view_id]
804
+
805
+ # Check if view ID exists
806
+ view_exists = False
807
+ for view in multi_view_info["views"]:
808
+ if view["view_id"] == view_id:
809
+ view_exists = True
810
+ break
811
+
812
+ if not view_exists:
813
+ raise ValueError(f"View with ID {view_id} not found in multi-view set {multi_view_id}")
814
+
815
+ # Check if view is already approved
816
+ if view_id in multi_view_info["approved_views"]:
817
+ return {
818
+ "multi_view_id": multi_view_id,
819
+ "view_id": view_id,
820
+ "status": "already_approved",
821
+ "approved_views": multi_view_info["approved_views"],
822
+ "rejected_views": multi_view_info["rejected_views"],
823
+ "approval_complete": multi_view_info["approval_complete"]
824
+ }
825
+
826
+ # Remove from rejected views if present
827
+ if view_id in multi_view_info["rejected_views"]:
828
+ multi_view_info["rejected_views"].remove(view_id)
829
+
830
+ # Add to approved views
831
+ multi_view_info["approved_views"].append(view_id)
832
+
833
+ # Check if approval is complete
834
+ if len(multi_view_info["approved_views"]) >= IMAGE_APPROVAL["MIN_APPROVED_IMAGES"]:
835
+ multi_view_info["approval_complete"] = True
836
+
837
+ # Create response
838
+ response = {
839
+ "multi_view_id": multi_view_id,
840
+ "view_id": view_id,
841
+ "status": "approved",
842
+ "approved_views": multi_view_info["approved_views"],
843
+ "rejected_views": multi_view_info["rejected_views"],
844
+ "approval_complete": multi_view_info["approval_complete"]
845
+ }
846
+
847
+ return response
848
+
849
+
850
+ # Add image rejection tool
851
+ @mcp_server.tool
852
+ def reject_image(multi_view_id: str, view_id: str) -> Dict[str, Any]:
853
+ """
854
+ Reject an image for 3D model generation.
855
+
856
+ Args:
857
+ multi_view_id: ID of the multi-view set
858
+ view_id: ID of the view to reject
859
+
860
+ Returns:
861
+ Dictionary with rejection information
862
+ """
863
+ # Check if multi-view ID exists
864
+ if multi_view_id not in approved_images:
865
+ raise ValueError(f"Multi-view set with ID {multi_view_id} not found")
866
+
867
+ # Get multi-view information
868
+ multi_view_info = approved_images[multi_view_id]
869
+
870
+ # Check if view ID exists
871
+ view_exists = False
872
+ for view in multi_view_info["views"]:
873
+ if view["view_id"] == view_id:
874
+ view_exists = True
875
+ break
876
+
877
+ if not view_exists:
878
+ raise ValueError(f"View with ID {view_id} not found in multi-view set {multi_view_id}")
879
+
880
+ # Check if view is already rejected
881
+ if view_id in multi_view_info["rejected_views"]:
882
+ return {
883
+ "multi_view_id": multi_view_id,
884
+ "view_id": view_id,
885
+ "status": "already_rejected",
886
+ "approved_views": multi_view_info["approved_views"],
887
+ "rejected_views": multi_view_info["rejected_views"],
888
+ "approval_complete": multi_view_info["approval_complete"]
889
+ }
890
+
891
+ # Remove from approved views if present
892
+ if view_id in multi_view_info["approved_views"]:
893
+ multi_view_info["approved_views"].remove(view_id)
894
+
895
+ # Add to rejected views
896
+ multi_view_info["rejected_views"].append(view_id)
897
+
898
+ # Check if approval is complete
899
+ if len(multi_view_info["approved_views"]) >= IMAGE_APPROVAL["MIN_APPROVED_IMAGES"]:
900
+ multi_view_info["approval_complete"] = True
901
+ else:
902
+ multi_view_info["approval_complete"] = False
903
+
904
+ # Create response
905
+ response = {
906
+ "multi_view_id": multi_view_id,
907
+ "view_id": view_id,
908
+ "status": "rejected",
909
+ "approved_views": multi_view_info["approved_views"],
910
+ "rejected_views": multi_view_info["rejected_views"],
911
+ "approval_complete": multi_view_info["approval_complete"]
912
+ }
913
+
914
+ return response
915
+
916
+
917
+ # Add 3D model generation from approved images tool
918
+ @mcp_server.tool
919
+ def create_3d_model_from_images(multi_view_id: str, output_name: Optional[str] = None) -> Dict[str, Any]:
920
+ """
921
+ Create a 3D model from approved multi-view images.
922
+
923
+ Args:
924
+ multi_view_id: ID of the multi-view set
925
+ output_name: Optional name for the output model
926
+
927
+ Returns:
928
+ Dictionary with model information
929
+ """
930
+ # Check if multi-view ID exists
931
+ if multi_view_id not in approved_images:
932
+ raise ValueError(f"Multi-view set with ID {multi_view_id} not found")
933
+
934
+ # Get multi-view information
935
+ multi_view_info = approved_images[multi_view_id]
936
+
937
+ # Check if approval is complete
938
+ if not multi_view_info["approval_complete"]:
939
+ raise ValueError(f"Approval for multi-view set {multi_view_id} is not complete")
940
+
941
+ # Check if there are enough approved images
942
+ if len(multi_view_info["approved_views"]) < IMAGE_APPROVAL["MIN_APPROVED_IMAGES"]:
943
+ raise ValueError(f"Not enough approved images. Need at least {IMAGE_APPROVAL['MIN_APPROVED_IMAGES']}, but only have {len(multi_view_info['approved_views'])}")
944
+
945
+ # Get approved image paths
946
+ approved_image_paths = []
947
+ for view in multi_view_info["views"]:
948
+ if view["view_id"] in multi_view_info["approved_views"]:
949
+ approved_image_paths.append(view["image_path"])
950
+
951
+ # Generate a unique model ID
952
+ model_id = str(uuid.uuid4())
953
+
954
+ # Set output name if not provided
955
+ if not output_name:
956
+ output_name = f"model_{model_id[:8]}"
957
+
958
+ # Create 3D model
959
+ if REMOTE_CUDA_MVS["ENABLED"] and remote_connection_manager:
960
+ # Use remote CUDA MVS processing
961
+ servers = discover_remote_servers()
962
+
963
+ if not servers:
964
+ raise ValueError("No remote CUDA MVS servers found")
965
+
966
+ # Use the first available server
967
+ server_id = servers[0]["id"]
968
+
969
+ # Upload images
970
+ upload_result = upload_images_to_server(server_id, approved_image_paths)
971
+
972
+ if not upload_result or "job_id" not in upload_result:
973
+ raise ValueError("Failed to upload images to remote server")
974
+
975
+ job_id = upload_result["job_id"]
976
+
977
+ # Process images
978
+ process_result = process_images_remotely(
979
+ server_id,
980
+ job_id,
981
+ {
982
+ "quality": REMOTE_CUDA_MVS["DEFAULT_RECONSTRUCTION_QUALITY"],
983
+ "output_format": REMOTE_CUDA_MVS["DEFAULT_OUTPUT_FORMAT"]
984
+ }
985
+ )
986
+
987
+ if not process_result:
988
+ raise ValueError(f"Failed to process images for job {job_id}")
989
+
990
+ # Wait for completion if requested
991
+ if REMOTE_CUDA_MVS["WAIT_FOR_COMPLETION"]:
992
+ import time
993
+
994
+ while True:
995
+ status = get_job_status(job_id)
996
+
997
+ if not status:
998
+ raise ValueError(f"Failed to get status for job {job_id}")
999
+
1000
+ if status["status"] in ["completed", "failed", "cancelled"]:
1001
+ break
1002
+
1003
+ time.sleep(REMOTE_CUDA_MVS["POLL_INTERVAL"])
1004
+
1005
+ if status["status"] == "completed":
1006
+ # Download model
1007
+ download_result = download_remote_model(job_id)
1008
+
1009
+ if not download_result:
1010
+ raise ValueError(f"Failed to download model for job {job_id}")
1011
+
1012
+ # Store model information
1013
+ models[model_id] = {
1014
+ "id": model_id,
1015
+ "type": "cuda_mvs_remote",
1016
+ "parameters": {
1017
+ "multi_view_id": multi_view_id,
1018
+ "prompt": multi_view_info["prompt"],
1019
+ "num_views": len(approved_image_paths),
1020
+ "quality": REMOTE_CUDA_MVS["DEFAULT_RECONSTRUCTION_QUALITY"],
1021
+ "output_format": REMOTE_CUDA_MVS["DEFAULT_OUTPUT_FORMAT"]
1022
+ },
1023
+ "description": f"3D model generated from {len(approved_image_paths)} views of '{multi_view_info['prompt']}'",
1024
+ "model_file": download_result.get("model_path"),
1025
+ "point_cloud_file": download_result.get("point_cloud_path"),
1026
+ "previews": {}, # Will be generated later
1027
+ "format": REMOTE_CUDA_MVS["DEFAULT_OUTPUT_FORMAT"],
1028
+ "remote_job_id": job_id
1029
+ }
1030
+
1031
+ # Create response
1032
+ response = {
1033
+ "model_id": model_id,
1034
+ "multi_view_id": multi_view_id,
1035
+ "status": "completed",
1036
+ "model_path": download_result.get("model_path"),
1037
+ "point_cloud_path": download_result.get("point_cloud_path"),
1038
+ "format": REMOTE_CUDA_MVS["DEFAULT_OUTPUT_FORMAT"]
1039
+ }
1040
+ else:
1041
+ # Store job information
1042
+ remote_jobs[job_id] = {
1043
+ "model_id": model_id,
1044
+ "multi_view_id": multi_view_id,
1045
+ "server_id": server_id,
1046
+ "job_id": job_id,
1047
+ "status": status["status"],
1048
+ "message": status.get("message", "")
1049
+ }
1050
+
1051
+ # Create response
1052
+ response = {
1053
+ "model_id": model_id,
1054
+ "multi_view_id": multi_view_id,
1055
+ "status": status["status"],
1056
+ "message": status.get("message", ""),
1057
+ "job_id": job_id
1058
+ }
1059
+ else:
1060
+ # Store job information
1061
+ remote_jobs[job_id] = {
1062
+ "model_id": model_id,
1063
+ "multi_view_id": multi_view_id,
1064
+ "server_id": server_id,
1065
+ "job_id": job_id,
1066
+ "status": "processing"
1067
+ }
1068
+
1069
+ # Create response
1070
+ response = {
1071
+ "model_id": model_id,
1072
+ "multi_view_id": multi_view_id,
1073
+ "status": "processing",
1074
+ "job_id": job_id,
1075
+ "server_id": server_id
1076
+ }
1077
+ else:
1078
+ # Use local CUDA MVS processing
1079
+ result = cuda_mvs.process_images(
1080
+ approved_image_paths,
1081
+ output_name=output_name,
1082
+ quality=REMOTE_CUDA_MVS["DEFAULT_RECONSTRUCTION_QUALITY"],
1083
+ output_format=REMOTE_CUDA_MVS["DEFAULT_OUTPUT_FORMAT"]
1084
+ )
1085
+
1086
+ # Store model information
1087
+ models[model_id] = {
1088
+ "id": model_id,
1089
+ "type": "cuda_mvs_local",
1090
+ "parameters": {
1091
+ "multi_view_id": multi_view_id,
1092
+ "prompt": multi_view_info["prompt"],
1093
+ "num_views": len(approved_image_paths),
1094
+ "quality": REMOTE_CUDA_MVS["DEFAULT_RECONSTRUCTION_QUALITY"],
1095
+ "output_format": REMOTE_CUDA_MVS["DEFAULT_OUTPUT_FORMAT"]
1096
+ },
1097
+ "description": f"3D model generated from {len(approved_image_paths)} views of '{multi_view_info['prompt']}'",
1098
+ "model_file": result.get("model_path"),
1099
+ "point_cloud_file": result.get("point_cloud_path"),
1100
+ "previews": {}, # Will be generated later
1101
+ "format": REMOTE_CUDA_MVS["DEFAULT_OUTPUT_FORMAT"]
1102
+ }
1103
+
1104
+ # Create response
1105
+ response = {
1106
+ "model_id": model_id,
1107
+ "multi_view_id": multi_view_id,
1108
+ "status": "completed",
1109
+ "model_path": result.get("model_path"),
1110
+ "point_cloud_path": result.get("point_cloud_path"),
1111
+ "format": REMOTE_CUDA_MVS["DEFAULT_OUTPUT_FORMAT"]
1112
+ }
1113
+
1114
+ return response
1115
+
1116
+
1117
+ # Add complete pipeline tool (text to 3D model)
1118
+ @mcp_server.tool
1119
+ def create_3d_model_from_text(prompt: str, num_views: int = 4, wait_for_completion: bool = True) -> Dict[str, Any]:
1120
+ """
1121
+ Create a 3D model from a text description using the complete pipeline.
1122
+
1123
+ Args:
1124
+ prompt: Text description of the 3D object
1125
+ num_views: Number of views to generate (default: 4)
1126
+ wait_for_completion: Whether to wait for remote processing to complete
1127
+
1128
+ Returns:
1129
+ Dictionary with model information
1130
+ """
1131
+ # Generate multi-view images
1132
+ multi_view_result = generate_multi_view_images(prompt, num_views)
1133
+
1134
+ multi_view_id = multi_view_result["multi_view_id"]
1135
+
1136
+ # Auto-approve all images if enabled
1137
+ if IMAGE_APPROVAL["AUTO_APPROVE"]:
1138
+ for view in multi_view_result["views"]:
1139
+ approve_image(multi_view_id, view["view_id"])
1140
+ else:
1141
+ # Return multi-view result for manual approval
1142
+ return {
1143
+ "status": "awaiting_approval",
1144
+ "message": "Please approve or reject each image before proceeding",
1145
+ "multi_view_id": multi_view_id,
1146
+ "views": multi_view_result["views"]
1147
+ }
1148
+
1149
+ # Create 3D model from approved images
1150
+ model_result = create_3d_model_from_images(multi_view_id)
1151
+
1152
+ # If remote processing is not waiting for completion, return job information
1153
+ if not wait_for_completion and model_result.get("status") == "processing":
1154
+ return model_result
1155
+
1156
+ # Return model information
1157
+ return model_result
1158
+
1159
+
1160
+ # Add remote CUDA MVS server discovery tool
1161
+ @mcp_server.tool
1162
+ def discover_remote_cuda_mvs_servers() -> Dict[str, Any]:
1163
+ """
1164
+ Discover remote CUDA MVS servers on the network.
1165
+
1166
+ Returns:
1167
+ Dictionary with discovered servers
1168
+ """
1169
+ if not REMOTE_CUDA_MVS["ENABLED"]:
1170
+ raise ValueError("Remote CUDA MVS processing is not enabled")
1171
+
1172
+ if not remote_connection_manager:
1173
+ raise ValueError("Remote CUDA MVS connection manager is not initialized")
1174
+
1175
+ servers = discover_remote_servers()
1176
+
1177
+ return {
1178
+ "servers": servers,
1179
+ "count": len(servers)
1180
+ }
1181
+
1182
+
1183
+ # Add remote job status tool
1184
+ @mcp_server.tool
1185
+ def get_remote_job_status(job_id: str) -> Dict[str, Any]:
1186
+ """
1187
+ Get the status of a remote CUDA MVS processing job.
1188
+
1189
+ Args:
1190
+ job_id: ID of the job to get status for
1191
+
1192
+ Returns:
1193
+ Dictionary with job status
1194
+ """
1195
+ if not REMOTE_CUDA_MVS["ENABLED"]:
1196
+ raise ValueError("Remote CUDA MVS processing is not enabled")
1197
+
1198
+ if not remote_connection_manager:
1199
+ raise ValueError("Remote CUDA MVS connection manager is not initialized")
1200
+
1201
+ # Check if job exists
1202
+ if job_id not in remote_jobs:
1203
+ raise ValueError(f"Job with ID {job_id} not found")
1204
+
1205
+ # Get job information
1206
+ job_info = remote_jobs[job_id]
1207
+
1208
+ # Get status from server
1209
+ status = get_job_status(job_id)
1210
+
1211
+ if not status:
1212
+ raise ValueError(f"Failed to get status for job with ID {job_id}")
1213
+
1214
+ # Update job information
1215
+ job_info["status"] = status.get("status", job_info["status"])
1216
+ job_info["progress"] = status.get("progress", 0)
1217
+ job_info["message"] = status.get("message", "")
1218
+
1219
+ return job_info
1220
+
1221
+
1222
+ # Add remote model download tool
1223
+ @mcp_server.tool
1224
+ def download_remote_model_result(job_id: str) -> Dict[str, Any]:
1225
+ """
1226
+ Download a processed model from a remote CUDA MVS server.
1227
+
1228
+ Args:
1229
+ job_id: ID of the job to download model for
1230
+
1231
+ Returns:
1232
+ Dictionary with model information
1233
+ """
1234
+ if not REMOTE_CUDA_MVS["ENABLED"]:
1235
+ raise ValueError("Remote CUDA MVS processing is not enabled")
1236
+
1237
+ if not remote_connection_manager:
1238
+ raise ValueError("Remote CUDA MVS connection manager is not initialized")
1239
+
1240
+ # Check if job exists
1241
+ if job_id not in remote_jobs:
1242
+ raise ValueError(f"Job with ID {job_id} not found")
1243
+
1244
+ # Get job information
1245
+ job_info = remote_jobs[job_id]
1246
+
1247
+ # Check if job is completed
1248
+ if job_info["status"] != "completed":
1249
+ raise ValueError(f"Job with ID {job_id} is not completed (status: {job_info['status']})")
1250
+
1251
+ # Download model
1252
+ result = download_remote_model(job_id)
1253
+
1254
+ if not result:
1255
+ raise ValueError(f"Failed to download model for job with ID {job_id}")
1256
+
1257
+ # Update job information
1258
+ job_info["model_path"] = result.get("model_path")
1259
+ job_info["point_cloud_path"] = result.get("point_cloud_path")
1260
+ job_info["downloaded"] = True
1261
+
1262
+ # Update model information if available
1263
+ if "model_id" in job_info and job_info["model_id"] in models:
1264
+ model_id = job_info["model_id"]
1265
+ models[model_id]["model_file"] = result.get("model_path")
1266
+ models[model_id]["point_cloud_file"] = result.get("point_cloud_path")
1267
+
1268
+ return {
1269
+ "job_id": job_id,
1270
+ "model_path": result.get("model_path"),
1271
+ "point_cloud_path": result.get("point_cloud_path"),
1272
+ "format": REMOTE_CUDA_MVS["DEFAULT_OUTPUT_FORMAT"]
1273
+ }
1274
+
1275
+
1276
+ # Add remote job cancellation tool
1277
+ @mcp_server.tool
1278
+ def cancel_remote_job(job_id: str) -> Dict[str, Any]:
1279
+ """
1280
+ Cancel a remote CUDA MVS processing job.
1281
+
1282
+ Args:
1283
+ job_id: ID of the job to cancel
1284
+
1285
+ Returns:
1286
+ Dictionary with cancellation result
1287
+ """
1288
+ if not REMOTE_CUDA_MVS["ENABLED"]:
1289
+ raise ValueError("Remote CUDA MVS processing is not enabled")
1290
+
1291
+ if not remote_connection_manager:
1292
+ raise ValueError("Remote CUDA MVS connection manager is not initialized")
1293
+
1294
+ # Check if job exists
1295
+ if job_id not in remote_jobs:
1296
+ raise ValueError(f"Job with ID {job_id} not found")
1297
+
1298
+ # Get job information
1299
+ job_info = remote_jobs[job_id]
1300
+
1301
+ # Cancel job
1302
+ result = cancel_job(job_id)
1303
+
1304
+ if not result:
1305
+ raise ValueError(f"Failed to cancel job with ID {job_id}")
1306
+
1307
+ # Update job information
1308
+ if result.get("cancelled", False):
1309
+ job_info["status"] = "cancelled"
1310
+ job_info["message"] = "Job cancelled by user"
1311
+
1312
+ return {
1313
+ "job_id": job_id,
1314
+ "cancelled": result.get("cancelled", False),
1315
+ "status": job_info["status"],
1316
+ "message": job_info.get("message", "")
1317
+ }
1318
+
1319
+
1320
+ # FastAPI routes
1321
+ @app.post("/tool_call")
1322
+ async def handle_tool_call(request: Request) -> JSONResponse:
1323
+ """
1324
+ Handle a tool call from a client.
1325
+
1326
+ Args:
1327
+ request: FastAPI request object
1328
+
1329
+ Returns:
1330
+ JSON response with tool call result
1331
+ """
1332
+ # Parse request
1333
+ data = await request.json()
1334
+
1335
+ # Check if tool name is provided
1336
+ if "tool_name" not in data:
1337
+ raise HTTPException(status_code=400, detail="Tool name is required")
1338
+
1339
+ # Check if tool exists
1340
+ tool_name = data["tool_name"]
1341
+ if tool_name not in mcp_server.tools:
1342
+ raise HTTPException(status_code=404, detail=f"Tool {tool_name} not found")
1343
+
1344
+ # Get tool parameters
1345
+ tool_params = data.get("tool_params", {})
1346
+
1347
+ # Call tool
1348
+ try:
1349
+ result = mcp_server.tools[tool_name](**tool_params)
1350
+ return JSONResponse(content=result)
1351
+ except Exception as e:
1352
+ logger.error(f"Error calling tool {tool_name}: {str(e)}")
1353
+ raise HTTPException(status_code=500, detail=str(e))
1354
+
1355
+ @app.get("/ui/preview/{model_id}")
1356
+ async def preview_model(request: Request, model_id: str) -> Response:
1357
+ """
1358
+ Render a preview page for a model.
1359
+
1360
+ Args:
1361
+ request: FastAPI request object
1362
+ model_id: ID of the model to preview
1363
+
1364
+ Returns:
1365
+ HTML response with model preview
1366
+ """
1367
+ # Check if model exists
1368
+ if model_id not in models:
1369
+ raise HTTPException(status_code=404, detail=f"Model with ID {model_id} not found")
1370
+
1371
+ # Get model information
1372
+ model_info = models[model_id]
1373
+
1374
+ # Render template
1375
+ return templates.TemplateResponse(
1376
+ "preview.html",
1377
+ {
1378
+ "request": request,
1379
+ "model_id": model_id,
1380
+ "parameters": model_info["parameters"],
1381
+ "previews": model_info["previews"]
1382
+ }
1383
+ )
1384
+
1385
+ @app.get("/preview/{view}/{model_id}")
1386
+ async def get_preview(view: str, model_id: str) -> FileResponse:
1387
+ """
1388
+ Get a preview image for a model.
1389
+
1390
+ Args:
1391
+ view: View to get preview for
1392
+ model_id: ID of the model
1393
+
1394
+ Returns:
1395
+ Image file response
1396
+ """
1397
+ # Check if model exists
1398
+ if model_id not in models:
1399
+ raise HTTPException(status_code=404, detail=f"Model with ID {model_id} not found")
1400
+
1401
+ # Get model information
1402
+ model_info = models[model_id]
1403
+
1404
+ # Check if preview exists
1405
+ if view not in model_info["previews"]:
1406
+ raise HTTPException(status_code=404, detail=f"Preview for view {view} not found")
1407
+
1408
+ # Return preview image
1409
+ return FileResponse(model_info["previews"][view])
1410
+
1411
+ @app.get("/download/{model_id}")
1412
+ async def download_model(model_id: str) -> FileResponse:
1413
+ """
1414
+ Download a model file.
1415
+
1416
+ Args:
1417
+ model_id: ID of the model to download
1418
+
1419
+ Returns:
1420
+ Model file response
1421
+ """
1422
+ # Check if model exists
1423
+ if model_id not in models:
1424
+ raise HTTPException(status_code=404, detail=f"Model with ID {model_id} not found")
1425
+
1426
+ # Get model information
1427
+ model_info = models[model_id]
1428
+
1429
+ # Check if model file exists
1430
+ if not model_info.get("model_file"):
1431
+ raise HTTPException(status_code=404, detail=f"Model file for model with ID {model_id} not found")
1432
+
1433
+ # Return model file
1434
+ return FileResponse(
1435
+ model_info["model_file"],
1436
+ filename=f"{model_id}.{model_info['format']}"
1437
+ )
1438
+
1439
+ @app.get("/")
1440
+ async def root() -> Dict[str, Any]:
1441
+ """
1442
+ Root endpoint.
1443
+
1444
+ Returns:
1445
+ Dictionary with server information
1446
+ """
1447
+ return {
1448
+ "name": "OpenSCAD MCP Server",
1449
+ "version": "1.0.0",
1450
+ "description": "MCP server for OpenSCAD",
1451
+ "tools": list(mcp_server.tools.keys())
1452
+ }
1453
+
1454
+ # Run server
1455
+ if __name__ == "__main__":
1456
+ uvicorn.run("src.main:app", host="0.0.0.0", port=8000, reload=True)