OpenSCAD MCP Server
A Model Context Protocol (MCP) server that gives AI assistants the ability to render, export, and analyze 3D models using OpenSCAD. Built with FastMCP for Python.
Prerequisites
Installation
Claude Code
--refresh-package openscad-mcp makes uv re-resolve the package from GitHubon every start, so a new release is picked up automatically the next time theserver launches. Drop the flag to pin to whatever uv has cached, or add@v0.6.0 after the URL to pin a release.
Add the server with a single command:
claude mcp add openscad --transport stdio -- \
uv run --refresh-package openscad-mcp --with git+https://github.com/quellant/openscad-mcp.git openscad-mcp
Or, if OpenSCAD is not on your PATH:
claude mcp add openscad --transport stdio \
--env OPENSCAD_PATH=/path/to/openscad -- \
uv run --refresh-package openscad-mcp --with git+https://github.com/quellant/openscad-mcp.git openscad-mcp
Use the --scope flag to control where the configuration is saved:
| Scope | Flag | Effect |
|---|---|---|
| Local (default) | --scope local |
Available only to you in the current project |
| Project | --scope project |
Shared with the team via .mcp.json |
| User | --scope user |
Available to you across all projects |
Claude Desktop
Add to your configuration file:
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json
{
"mcpServers": {
"openscad": {
"command": "uv",
"args": [
"run",
"--refresh-package", "openscad-mcp",
"--with", "git+https://github.com/quellant/openscad-mcp.git",
"openscad-mcp"
],
"env": {
"OPENSCAD_PATH": "/usr/bin/openscad"
}
}
}
}
Then restart Claude Desktop.
Cursor / Windsurf / VS Code
Add a .mcp.json file to your project root:
{
"mcpServers": {
"openscad": {
"command": "uv",
"args": [
"run",
"--refresh-package", "openscad-mcp",
"--with", "git+https://github.com/quellant/openscad-mcp.git",
"openscad-mcp"
]
}
}
}
Manual / Standalone
# Run directly from GitHub (no install required)
uv run --refresh-package openscad-mcp --with git+https://github.com/quellant/openscad-mcp.git openscad-mcp
# Or clone and run locally
git clone https://github.com/quellant/openscad-mcp.git
cd openscad-mcp
uv run openscad-mcp
Available Tools
Every tool response carries errors, warnings and hints parsed fromOpenSCAD's output. Check them: OpenSCAD exits 0 on a failed assert() or anunknown module and draws a blank scene.
Rendering
| Tool | Description |
|---|---|
render |
Images with a text digest before each one (camera, view direction, scale, bbox). mode=views (one image per view, or a custom camera), mode=section (exact cross-section with a scale bar), mode=parts (each part in its own colour, isolate ghosts the rest), mode=compare (before/after). grounded=true gives an orthographic view with a stated mm/px scale; annotate=true adds a scale bar, axis triad and bbox dimensions |
Assemblies
| Tool | Description |
|---|---|
check |
Relations between named parts, exported separately and never unioned: mode=interference (clear / contact / interference with penetration depth and a witness point), clearance (exact minimum distance with closest points), contact (area, normal, plane; `kind=static |
Parts are given inline as parts=[{name, code, place?, frame?, ghost?, mass_g?, motion?}] or in a check file (frames, quality, parts, checks, model). openscad-mcp check <file.yaml> runs a check file from the shell with a meaningful exit code, so make check is one call.
Export & Model Management
| Tool | Description |
|---|---|
export_model |
Export to STL, 3MF, AMF, OFF, NEF3, DXF, SVG, PDF or CSG. With parts=[...] every part is exported in its assembly position and bundled into one 3MF with named objects (or a directory of STLs) |
model |
`action=create |
Measurement & Validation
| Tool | Description |
|---|---|
measure |
Exact numbers from the geometry: model (bbox, volume, area, components, watertight, mesh_health), parts, section (contours; the offset may be an expression in the model's scope), mass (grams; with parts= and about_axis= the assembly mass, centre of mass and inertia about an axis, with mass_g overrides for purchased parts), probe (solid/air and which part at points; ray crossings; line of sight along a polyline), features (holes from the CSG tree: axis, diameter, depth, through/blind, undersize at $fn, fit names), printability (overhang patches with unsupported reach, thickness distribution vs nozzle, islands, support estimate; facts only), orientation (candidate orientations, no winner chosen), anchors (BOSL2 anchor frames in the assembly frame). Accepts an existing STL/SVG via mesh |
validate |
mode=syntax, geometry, predicates (with sweep={variable, values} reporting the crossing), includes (references resolved or not, plus the BOSL2 lint: a module from a used file placed by attach() is silently put at CENTER; autofix=true applies the rewrite when it is safe), printability (rules from the design-rule reference over measured facts) |
scad_eval |
Evaluate expressions in a model's variable scope and get typed values (number, vector, string, bool, range, undef) |
reference |
Sourced engineering data with confidence labels: fits (also bidirectional: diameter_mm=3.3 names the hole, shaft_mm+bore_mm names the fit), metric fasteners, heat-set inserts, bearings, magnets, joints, a purchased-parts catalog with BOSL2 modules and clearance masks, FDM design rules, materials, OpenSCAD cheatsheet, conventions |
get_libraries |
Discover installed OpenSCAD libraries |
check_openscad |
Verify OpenSCAD installation, version and capabilities |
Project Support
| Tool | Description |
|---|---|
get_project_files |
List .scad files and their references; mode=trace follows a constant through the project (what depends on it, what it depends on) |
clear_cache |
Clear the render cache |
Usage Examples
Once connected, ask your AI assistant:
- "Render a cube with rounded edges"
- "Show me the front and top of this model with a scale bar"
- "What is the volume and are there any cavities?"
- "Cut a section through the lid at z = 12 and tell me the wall thickness"
- "Colour the body and lid differently and ghost the body"
- "What clearance should I use for an M3 screw and a press-fit 608 bearing?"
- "Compare the model before and after changing the radius to 15"
- "Export my gear model to STL"
The server also publishes MCP resources (openscad://conventions,openscad://cheatsheet, openscad://reference/{topic}) and serverinstructions with the coordinate and assembly conventions it expects. AClaude Code skill lives in skills/openscad-design/SKILL.md and the repo canbe installed as a Claude Code plugin (.claude-plugin/).
Tool Parameters
render
| Parameter | Type | Default | Description |
|---|---|---|---|
scad_content |
string | — | OpenSCAD code to render* |
scad_file |
string | — | Path to .scad file* |
mode |
string | views |
views, section, parts, compare |
views |
list | ["isometric"] |
Any of front, back, left, right, top, bottom, isometric, dimetric |
camera_position / camera_target / camera_up |
list/string | — | Custom camera (used when views is omitted) |
grounded |
bool | false |
Measure the model, then render orthographically with a stated mm/px scale |
annotate |
bool | false |
Scale bar, axis triad, bbox dimensions (implies grounded) |
section_axis / section_offset |
string / number | z / 0 |
Cut plane for mode=section |
parts / isolate |
list / string | — | [{"name": "lid", "code": "lid();"}] for mode=parts |
variables_after / scad_content_after |
dict / string | — | The "after" side for mode=compare |
image_size |
list/string | [800,600] |
Output dimensions, clamped to 1568 px |
color_scheme |
string | Cornfield |
OpenSCAD color scheme |
variables |
dict | {} |
OpenSCAD variables |
quality |
string | — | draft, normal, or high |
include_paths |
list | — | Extra include directories (via OPENSCADPATH) |
Each image costs roughly 640 vision tokens at 800x600; ask for the views thatanswer a question rather than all of them. Auto-fit renders (grounded=false)have no recoverable absolute scale, which the digest states.
*Exactly one of scad_content or scad_file must be provided.
All parameter parsers accept multiple input formats (JSON strings, lists, dicts, CSV) for AI assistant compatibility.
Configuration
Environment Variables
| Variable | Description | Default |
|---|---|---|
OPENSCAD_PATH |
Path to OpenSCAD executable | Auto-detected |
MCP_TEMP_DIR |
Temporary file directory | /tmp/openscad-mcp |
MCP_TRANSPORT |
Transport type: stdio, http, sse |
stdio |
MCP_HOST |
Host for HTTP/SSE transport | localhost |
MCP_PORT |
Port for HTTP/SSE transport | 8000 |
MCP_MAX_CONCURRENT_RENDERS |
Max parallel renders | 5 |
MCP_RENDER_TIMEOUT |
Render timeout in seconds | 300 |
MCP_CACHE_ENABLED |
Enable render caching | true |
MCP_CACHE_SIZE_MB |
Max cache size in MB | 500 |
MCP_CACHE_TTL_HOURS |
Cache TTL in hours | 24 |
MCP_LOG_LEVEL |
Logging level | INFO |
MCP_MAX_FILE_SIZE_MB |
Max SCAD file size | 10 |
MCP_ALLOWED_PATHS |
Directories scripts may read from (os.pathsep-separated) |
unset = no validation |
MCP_MAX_MEMORY_MB |
Address-space limit per OpenSCAD process (POSIX), 0 disables |
4096 |
MCP_MAX_IMAGE_WIDTH / MCP_MAX_IMAGE_HEIGHT |
Render size clamp (aspect preserved) | 1568 |
MCP_HARD_WARNINGS |
Pass --hardwarnings to OpenSCAD (see Security) |
false |
YAML Configuration
Create a config.yaml for advanced configuration:
server:
name: "OpenSCAD MCP Server"
version: "0.1.0"
transport: stdio
rendering:
max_concurrent: 5
timeout_seconds: 300
default_color_scheme: Cornfield
cache:
enabled: true
max_size_mb: 500
ttl_hours: 24
security:
rate_limit: 60
max_file_size_mb: 10
allowed_paths: # null = no path validation at all (a warning is logged)
- /home/me/projects/parts
max_memory_mb: 4096
Security
Threat model
The server runs OpenSCAD on source it is handed. OpenSCAD can read any filethe process can read, through include <>, use <>, import() andsurface(), and can return what it read as echo output or as geometry. Theguarantees below hold only when allowed_paths is configured. Out of thebox it is unset, no path validation is performed, and the server logs awarning at startup saying so.
What is enforced:
- Path validation on arguments:
scad_file,include_paths(in everytool) and exportoutput_pathmust lie insideallowed_paths. Containmentuses resolved paths, so symlinks and..cannot escape. - Path validation on the dependency closure: every file OpenSCAD actuallyread is recorded with
-dand checked after the run. If any lies outsideallowed_paths, the standard library directories, or the server temp dir,the output (image, mesh, echo text) is withheld and the call fails. Thiscloses theinclude <...>-as-data andsurface(file=...)channels. - Memory ceiling: each OpenSCAD process runs under
RLIMIT_AS(max_memory_mb, default 4 GB) on POSIX hosts. OpenSCAD has no ceiling ofits own; a smallminkowski()can otherwise consume all host memory. - Timeout:
timeout_seconds, default 300 s; partial stderr is kept. - Echo channel bounds:
echo_outputis capped (200 lines, 2000 charsper line) and labelled as untrusted content from the rendered file. - File size limits, variable name validation(
^\$?[a-zA-Z_][a-zA-Z0-9_]*$) and model name validation (no pathtraversal) as before.
What is not enforced: no OS-level sandbox (no network isolation, nofilesystem namespace). For untrusted input run the server inside acontainer or under Landlock/bubblewrap with only the project directorymounted.
Why --hardwarnings is off
--hardwarnings stops OpenSCAD at the first warning but still exits 0, soit produced blank renders and silently truncated echo_output with noindication. Warnings now reach the assistant through the structuredwarnings, errors and hints fields on every tool response instead.Set MCP_HARD_WARNINGS=true to restore the flag.
Development
# Clone the repo
git clone https://github.com/quellant/openscad-mcp.git
cd openscad-mcp
# Install dependencies
uv sync --dev
# Run the server
uv run openscad-mcp
# Run tests
uv run pytest
# Lint & format
uv run ruff check src/ tests/
uv run black --check src/ tests/
# Type check
uv run mypy src/
Project Structure
openscad-mcp/
├── src/openscad_mcp/
│ ├── __init__.py # Package exports
│ ├── server.py # FastMCP server, all MCP tools and helpers
│ ├── types.py # Pydantic models and enums
│ └── utils/
│ └── config.py # Configuration with env/YAML/dotenv support
├── tests/ # 300 tests, 80%+ coverage
├── pyproject.toml
└── README.md
Testing
# Run all tests with coverage
uv run pytest
# Run specific markers
uv run pytest -m unit
uv run pytest -m performance
# Run a single file
uv run pytest tests/test_helpers.py -v
Tests mock the OpenSCAD subprocess — no OpenSCAD installation required to run them. Coverage target: 80% minimum.
Troubleshooting
OpenSCAD Not Found
# Check if OpenSCAD is installed
which openscad # Linux/macOS
where openscad.exe # Windows
# Set the path explicitly
export OPENSCAD_PATH=/path/to/openscad
Server Not Connecting
# Verify the server starts correctly
uv run --refresh-package openscad-mcp --with git+https://github.com/quellant/openscad-mcp.git openscad-mcp
# In Claude Code, check MCP status
/mcp
Render Timeout
Increase the timeout:
export MCP_RENDER_TIMEOUT=600
Contributing
- Fork the repository
- Create a feature branch (
git checkout -b feature/my-feature) - Make your changes with tests
- Ensure tests pass (
uv run pytest) - Open a Pull Request
Commit style: feat:, fix:, docs:, refactor:, chore:
License
MIT — see LICENSE
Acknowledgments
- FastMCP — Python MCP framework
- OpenSCAD — Programmable CAD software
- Model Context Protocol — The MCP specification