From 77eb5379d25ba52b3c394581b015169890efd4a3 Mon Sep 17 00:00:00 2001 From: Thorsten Date: Thu, 12 Mar 2026 16:00:24 +0100 Subject: [PATCH] Initial commit: OpenSCAD MCP server for Claude Code MCP server that lets Claude generate and render OpenSCAD models to STL files. Includes setup documentation and installation guide. Co-Authored-By: Claude Sonnet 4.6 --- .gitignore | 4 + README.md | 66 ++++++++++++ SETUP.md | 277 ++++++++++++++++++++++++++++++++++++++++++++++++++ server.py | 109 ++++++++++++++++++++ trichter.scad | 92 +++++++++++++++++ 5 files changed, 548 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 SETUP.md create mode 100644 server.py create mode 100644 trichter.scad diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fc5a810 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.venv/ +__pycache__/ +*.pyc +*.pyo diff --git a/README.md b/README.md new file mode 100644 index 0000000..03d6aaa --- /dev/null +++ b/README.md @@ -0,0 +1,66 @@ +# OpenSCAD MCP Server + +LLM-gestützte 3D-Modell-Erstellung für den 3D-Drucker via Claude Code. + +## Setup + +### 1. OpenSCAD installieren +https://openscad.org/downloads.html → Windows-Installer + +### 2. Python-Abhängigkeiten +```bash +pip install mcp +``` + +### 3. MCP-Server in Claude Code registrieren + +In Claude Code: **Settings → MCP Servers → Add** (oder direkt in `~/.claude/claude_desktop_config.json`): + +```json +{ + "mcpServers": { + "openscad": { + "command": "python", + "args": ["C:/Users/thors/Documents/Claude/openscad-mcp/server.py"] + } + } +} +``` + +### Umgebungsvariablen (optional) +| Variable | Standard | Beschreibung | +|----------|----------|--------------| +| `OPENSCAD_PATH` | `C:\Program Files\OpenSCAD\openscad.exe` | Pfad zur OpenSCAD-Executable | +| `OPENSCAD_OUTPUT_DIR` | `~/3d-models` | Ausgabeverzeichnis für STL-Dateien | + +## Verfügbare Tools + +| Tool | Beschreibung | +|------|--------------| +| `render_stl(code, filename)` | OpenSCAD-Code → STL-Datei | +| `preview_png(code, filename)` | OpenSCAD-Code → PNG-Vorschau | +| `list_models()` | Alle STL-Dateien auflisten | + +## Beispiel-Prompt für Claude + +``` +Erstelle einen Wandhalter für einen 32mm Rohrdurchmesser, +Wandstärke 3mm, mit 2 Schraubenlöchern (M4), Gesamtbreite 60mm. +Rendere es als "rohrhalter.stl". +``` + +## OpenSCAD Tipps für LLMs + +Claude generiert typisch solchen Code: +```openscad +difference() { + union() { + cylinder(h=20, d=40); + translate([0,0,10]) cube([60,10,20], center=true); + } + cylinder(h=22, d=32, center=true); // Loch für Rohr + // Schraubenlöcher + translate([20,0,0]) cylinder(h=22, d=4.5, center=true); + translate([-20,0,0]) cylinder(h=22, d=4.5, center=true); +} +``` diff --git a/SETUP.md b/SETUP.md new file mode 100644 index 0000000..85b6976 --- /dev/null +++ b/SETUP.md @@ -0,0 +1,277 @@ +# OpenSCAD MCP Server — Installationsanleitung + +LLM-gestützte 3D-Modell-Erstellung: Claude generiert OpenSCAD-Code und rendert ihn +direkt zu STL-Dateien für den 3D-Drucker. + +--- + +## Voraussetzungen + +- Windows 10/11 +- Claude Code Desktop App installiert +- Python (Microsoft Store) — bereits installiert auf diesem System + +--- + +## Schritt 1: OpenSCAD installieren + +1. https://openscad.org/downloads.html aufrufen +2. **Windows Installer** herunterladen und ausführen +3. Standardpfad bestätigen: `C:\Program Files\OpenSCAD\` + +Verifizieren: +``` +"C:\Program Files\OpenSCAD\openscad.exe" --version +``` +Erwartete Ausgabe: `OpenSCAD version 2021.01` (oder neuer) + +--- + +## Schritt 2: Projektverzeichnis anlegen + +``` +mkdir C:\Users\\Documents\Claude\openscad-mcp +``` + +--- + +## Schritt 3: Python Virtual Environment erstellen + +> **Wichtig:** Den Microsoft Store Python verwenden (`python3.13.exe` in WindowsApps). +> Das `python.exe` aus `C:\Users\...\AppData\Local\Programs\Python\Python313\` ist ein +> eingebettetes Python **ohne pip** und funktioniert nicht. +> Die `WindowsApps\python3.13.exe` ist ein App Execution Alias — funktioniert nur in +> interaktiven Shells, **nicht** als Subprocess von Claude Code. +> Lösung: Virtual Environment erstellen — das `.venv\Scripts\python.exe` ist eine +> echte Executable ohne diese Einschränkung. + +``` +cd C:\Users\\Documents\Claude\openscad-mcp + +"C:\Users\\AppData\Local\Microsoft\WindowsApps\python3.13.exe" -m venv .venv + +.venv\Scripts\pip install mcp +``` + +Verifizieren: +``` +.venv\Scripts\python.exe -c "from mcp.server.fastmcp import FastMCP; print('OK')" +``` + +--- + +## Schritt 4: MCP-Server erstellen + +Datei `C:\Users\\Documents\Claude\openscad-mcp\server.py`: + +```python +""" +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 + +OPENSCAD_PATH = os.environ.get( + "OPENSCAD_PATH", + r"C:\Program Files\OpenSCAD\openscad.exe" +) + +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]: + 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.""" + 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}" + + +@mcp.tool() +def preview_png(code: str, filename: str = "preview.png") -> str: + """Rendert eine PNG-Vorschau des OpenSCAD-Modells.""" + if not filename.endswith(".png"): + filename += ".png" + output_path = OUTPUT_DIR / filename + 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() +``` + +--- + +## Schritt 5: MCP-Server testen + +``` +echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' | .venv\Scripts\python.exe server.py +``` + +Erwartete Ausgabe: JSON-Antwort mit `"name":"openscad"` — Server funktioniert. + +--- + +## Schritt 6: Claude Code Desktop konfigurieren + +> **Wichtig:** Es gibt zwei Settings-Dateien. Nur eine davon wird von der Desktop App gelesen: +> +> | Datei | Gelesen von | +> |-------|------------| +> | `C:\Users\\.claude\settings.json` | Claude Code **CLI** (Terminal) | +> | `C:\Users\\AppData\Roaming\Claude\claude_desktop_config.json` | Claude Code **Desktop App** | +> +> Da die Desktop App verwendet wird, muss der Eintrag in `claude_desktop_config.json`. + +Datei öffnen: `C:\Users\\AppData\Roaming\Claude\claude_desktop_config.json` + +Den `mcpServers`-Block hinzufügen (**Backslashes** im Pfad beachten): + +```json +{ + "mcpServers": { + "openscad": { + "command": "C:\\Users\\\\Documents\\Claude\\openscad-mcp\\.venv\\Scripts\\python.exe", + "args": ["C:\\Users\\\\Documents\\Claude\\openscad-mcp\\server.py"] + } + }, + "preferences": { + ...vorhandene Einstellungen beibehalten... + } +} +``` + +--- + +## Schritt 7: Claude Code neu starten + +**Vollständig beenden** — nicht nur neues Chat-Fenster öffnen: +- Rechtsklick auf Claude-Icon im **Systemtray** → **Quit/Beenden** +- Alternativ: Task-Manager → `claude.exe` beenden + +Dann Claude Code neu starten und einen neuen Chat öffnen. + +--- + +## Verifikation + +In einem neuen Chat eingeben: +> "Erstelle einen einfachen Würfel 20x20x20mm als test.stl" + +Claude sollte automatisch das Tool `render_stl` aufrufen und eine STL-Datei in +`C:\Users\\3d-models\test.stl` erzeugen. + +--- + +## Beispiel-Prompts + +``` +Erstelle einen Filamentrollenhalter für 200mm Außendurchmesser, 30mm Achse, als filamenthalter.stl +``` +``` +Erstelle einen Wandhalter für ein 32mm Rohr, Wandstärke 3mm, 2 Schraubenlöcher M4, als rohrhalter.stl +``` +``` +Erstelle eine Box 80x50x30mm mit Deckel und Scharnier, Wandstärke 2mm, als box.stl +``` + +--- + +## Troubleshooting + +### Server wird nicht geladen (Tools nicht sichtbar) +- Prüfen ob Eintrag in `claude_desktop_config.json` ist (nicht in `settings.json`) +- Claude Code **vollständig** neu starten (Systemtray → Quit) +- Debug-Log prüfen: `C:\Users\\.claude\debug\` — neueste Datei nach `openscad` durchsuchen + +### STL-Render schlägt fehl +- OpenSCAD-Pfad prüfen: `"C:\Program Files\OpenSCAD\openscad.exe" --version` +- Output-Verzeichnis prüfen: `C:\Users\\3d-models\` muss existieren (wird automatisch erstellt) + +### Python/mcp Import-Fehler +- Virtual Environment neu erstellen (Schritt 3) +- Prüfen: `.venv\Scripts\python.exe -c "from mcp.server.fastmcp import FastMCP; print('OK')"` + +--- + +## Dateistruktur + +``` +C:\Users\\ +├── AppData\Roaming\Claude\ +│ └── claude_desktop_config.json ← MCP-Server Registrierung +├── .claude\ +│ └── settings.json ← Claude Code CLI-Settings (nicht relevant) +├── Documents\Claude\openscad-mcp\ +│ ├── .venv\ ← Python Virtual Environment +│ │ └── Scripts\python.exe ← Echter Python-Interpreter +│ └── server.py ← MCP-Server Code +└── 3d-models\ ← Ausgabeverzeichnis für STL-Dateien + ├── filamenthalter.stl + └── ... +``` + +--- + +## Hintergrund: Warum so kompliziert? + +1. **Zwei Config-Dateien**: Claude Code Desktop liest `claude_desktop_config.json`, + nicht `~/.claude/settings.json`. Letzteres ist nur für den CLI-Modus. + +2. **Windows App Execution Aliases**: `WindowsApps\python3.13.exe` ist ein Stub + der nur aus interaktiven Shells funktioniert. Als Subprocess von Claude Code + schlägt er ohne Fehlermeldung fehl. Ein Virtual Environment erstellt eine + echte `python.exe` ohne diese Einschränkung. + +3. **Eingebettetes Python**: `C:\...\Programs\Python\Python313\python.exe` (das + vom `py`-Launcher verwendete) hat kein `pip` und kann nicht für MCP verwendet + werden. diff --git a/server.py b/server.py new file mode 100644 index 0000000..5d00503 --- /dev/null +++ b/server.py @@ -0,0 +1,109 @@ +""" +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() diff --git a/trichter.scad b/trichter.scad new file mode 100644 index 0000000..9381b18 --- /dev/null +++ b/trichter.scad @@ -0,0 +1,92 @@ +// Trichter +$fn = 64; + +// Parameter +top_r = 40; // Radius oben +cone_h = 50; // Höhe des Kegelabschnitts +spout_r = 6; // Radius des Rohrs +spout_h = 25; // Länge des geraden Rohrs +wall = 2; // Wandstärke +ext_h = 20; // Länge der abgewinkelten Verlängerung +angle = 135; // Winkel der Verlängerung in Grad +spout_h2 = 25; // Länge des zweiten Ausgussrohrs +angle2 = 45; // Winkel von Knie 2 + +difference() { + // --- Außenhülle --- + union() { + // Kegelabschnitt + cylinder(h=cone_h, r1=spout_r, r2=top_r); + + // Gerades Ausgussrohr + translate([0, 0, -spout_h]) + cylinder(h=spout_h, r=spout_r); + + // Knie 1 + hull() { + translate([0, 0, -spout_h]) + cylinder(h=0.01, r=spout_r); + translate([0, 0, -spout_h]) + rotate([angle, 0, 0]) + cylinder(h=0.01, r=spout_r); + } + + // Abgewinkelte Verlängerung + translate([0, 0, -spout_h]) + rotate([angle, 0, 0]) + cylinder(h=ext_h, r=spout_r); + + // Knie 2 + zweites Ausgussrohr + translate([0, 0, -spout_h]) + rotate([angle, 0, 0]) + translate([0, 0, ext_h]) { + hull() { + cylinder(h=0.01, r=spout_r); + rotate([angle2, 0, 0]) + cylinder(h=0.01, r=spout_r); + } + rotate([angle2, 0, 0]) + cylinder(h=spout_h2, r=spout_r); + } + } + + // --- Innenkanal --- + union() { + // Kegel innen + translate([0, 0, wall]) + cylinder(h=cone_h, r1=spout_r - wall, r2=top_r - wall); + + // Gerades Rohr innen + translate([0, 0, -(spout_h + 1)]) + cylinder(h=spout_h + 1 + wall, r=spout_r - wall); + + // Knie 1 innen + hull() { + translate([0, 0, -spout_h]) + cylinder(h=0.01, r=spout_r - wall); + translate([0, 0, -spout_h]) + rotate([angle, 0, 0]) + cylinder(h=0.01, r=spout_r - wall); + } + + // Verlängerung innen + translate([0, 0, -spout_h]) + rotate([angle, 0, 0]) + translate([0, 0, -1]) + cylinder(h=ext_h + 2, r=spout_r - wall); + + // Knie 2 innen + zweites Rohr innen + translate([0, 0, -spout_h]) + rotate([angle, 0, 0]) + translate([0, 0, ext_h]) { + hull() { + cylinder(h=0.01, r=spout_r - wall); + rotate([angle2, 0, 0]) + cylinder(h=0.01, r=spout_r - wall); + } + rotate([angle2, 0, 0]) + translate([0, 0, -1]) + cylinder(h=spout_h2 + 2, r=spout_r - wall); + } + } +}