""" OpenSCAD MCP Server Ermöglicht Claude, OpenSCAD-Code zu rendern und STL-Dateien zu erzeugen. """ import subprocess import tempfile import os import base64 from pathlib import Path from mcp.server.fastmcp import FastMCP # Pfad zur OpenSCAD-Executable (Windows-Standard) OPENSCAD_PATH = os.environ.get( "OPENSCAD_PATH", r"C:\Program Files\OpenSCAD\openscad.exe" ) # Output-Verzeichnis für STL-Dateien OUTPUT_DIR = Path(os.environ.get("OPENSCAD_OUTPUT_DIR", Path.home() / "3d-models")) OUTPUT_DIR.mkdir(parents=True, exist_ok=True) mcp = FastMCP("openscad") def _run_openscad(scad_code: str, output_path: Path, extra_args: list = None) -> tuple[bool, str]: """Führt OpenSCAD mit dem gegebenen Code aus. Gibt (success, output) zurück.""" with tempfile.NamedTemporaryFile(suffix=".scad", mode="w", delete=False, encoding="utf-8") as f: f.write(scad_code) scad_file = f.name try: cmd = [OPENSCAD_PATH, "-o", str(output_path)] + (extra_args or []) + [scad_file] result = subprocess.run(cmd, capture_output=True, text=True, timeout=60) output = (result.stdout + result.stderr).strip() success = result.returncode == 0 and output_path.exists() return success, output finally: os.unlink(scad_file) @mcp.tool() def render_stl(code: str, filename: str = "model.stl") -> str: """ Rendert OpenSCAD-Code als STL-Datei für den 3D-Drucker. Args: code: Gültiger OpenSCAD-Code filename: Dateiname für die STL-Ausgabe (z.B. "halter.stl") Returns: Pfad zur erzeugten STL-Datei oder Fehlermeldung """ if not filename.endswith(".stl"): filename += ".stl" output_path = OUTPUT_DIR / filename success, output = _run_openscad(code, output_path) if success: size_kb = output_path.stat().st_size // 1024 return f"STL erfolgreich erstellt: {output_path}\nDateigröße: {size_kb} KB\nOpenSCAD-Output: {output or '(kein Output)'}" else: return f"Fehler beim Rendern:\n{output}\n\nBitte OpenSCAD-Code prüfen." @mcp.tool() def preview_png(code: str, filename: str = "preview.png") -> str: """ Rendert eine PNG-Vorschau des OpenSCAD-Modells. Args: code: Gültiger OpenSCAD-Code filename: Dateiname für die PNG-Ausgabe Returns: Base64-kodiertes PNG-Bild oder Fehlermeldung """ if not filename.endswith(".png"): filename += ".png" output_path = OUTPUT_DIR / filename # --camera=0,0,0,55,0,25,140 = isometrische Ansicht extra = ["--camera=0,0,0,55,0,25,140", "--imgsize=800,600", "--colorscheme=DeepOcean"] success, output = _run_openscad(code, output_path, extra_args=extra) if success: with open(output_path, "rb") as f: img_b64 = base64.b64encode(f.read()).decode() return f"data:image/png;base64,{img_b64}" else: return f"Fehler beim Rendern der Vorschau:\n{output}" @mcp.tool() def list_models() -> str: """Listet alle bisher erstellten 3D-Modelle im Output-Verzeichnis.""" files = list(OUTPUT_DIR.glob("*.stl")) if not files: return f"Keine STL-Dateien in {OUTPUT_DIR}" lines = [f"Modelle in {OUTPUT_DIR}:"] for f in sorted(files): size_kb = f.stat().st_size // 1024 lines.append(f" {f.name} ({size_kb} KB)") return "\n".join(lines) if __name__ == "__main__": mcp.run()