mesh-prep-mcp
by Horuspokus
README.md
# mesh-prep-mcp
**An MCP server that lets an LLM agent turn a raw 3D asset into an experience-ready one** — inspect, repair, decimate, rescale and export triangle meshes through the [Model Context Protocol](https://modelcontextprotocol.io), with a small C++17 core for the numbers the agent bases its decisions on.
[](https://github.com/Horuspokus/mesh-prep-mcp/actions/workflows/ci.yml)
## Why
Getting a scan or CAD export into a real-time scene is a chain of small judgement calls: *is it watertight? are these millimetres? how far can I decimate before the silhouette suffers?* Those calls are usually made by hand. This project exposes the underlying operators to an agent as MCP tools, returns **before/after statistics** for every step, and lets the agent decide — and explain — what it did. It is deliberately small: a rehearsal of agent-driven 3D data preparation, not a product.
## What the agent gets
| Tool | What it does |
|---|---|
| `load_mesh(path)` | Loads OBJ / STL / PLY / GLB / glTF / OFF / 3MF …, registers it, returns a first inspection |
| `inspect_mesh(mesh_id)` | Bounds, extent, area, volume, face/vertex counts, degenerate faces, boundary & non-manifold edges, duplicate vertices, plus an **issue list with the operator that fixes each one** |
| `repair_mesh(mesh_id)` | Welds duplicates, drops degenerate/duplicate faces and fold-overs, fixes winding, fills small holes → new id, before/after |
| `decimate_mesh(mesh_id, target_faces \| ratio)` | Quadric-error decimation to a face budget → new id, reduction %, surface-area drift |
| `scale_mesh(mesh_id, factor)` | Unit conversion (e.g. mm → m) → new id |
| `export_mesh(mesh_id, path, format?)` | OBJ / GLB / glTF / STL / PLY / OFF |
| `list_meshes()` | Everything in the session with full provenance |
| resource `mesh://{id}/report` | Markdown report of one mesh |
| prompt `prepare_for_realtime` | A guided full preparation pass |
Every operator returns a **new** `mesh_id`; earlier ids stay valid, so the agent can compare, branch or roll back without any undo logic.
## Demo (no LLM needed)

```text
$ python scripts/make_sample_mesh.py # 82k-face noisy "scan" in millimetres, with holes and degenerate faces
$ python scripts/demo_client.py # talks to the server over stdio, like Claude Desktop does
tools: load_mesh, inspect_mesh, repair_mesh, decimate_mesh, scale_mesh, export_mesh, list_meshes
[load_mesh] m1: 81,920 faces, 40,962 verts, extent (502, 501, 502), backend=cpp
issue: 3 degenerate (zero-area or collapsed) faces -> repair_mesh
issue: open surface: 9 boundary edges (not watertight) -> repair_mesh fills small holes; ...
[repair_mesh] m1: 81,920 faces, 40,962 verts, watertight=False, issues=2
-> m2: 81,920 faces, 40,962 verts, watertight=True, issues=0
[decimate_mesh] m2: 81,920 faces, 40,962 verts, watertight=True, issues=0
-> m3: 20,000 faces, 10,002 verts, watertight=False, issues=1
75.6% fewer faces, surface area -0.49%
issue: 1 non-manifold edges (shared by 3+ faces)
[repair_mesh] verify after decimation
-> m4: 19,998 faces, 10,001 verts, watertight=True, issues=0
[scale_mesh] x0.001 (mm -> m): extent (0.501, 0.501, 0.502)
[export_mesh] examples/output/sample_scan_ready.glb (360,752 bytes)
```
Note the step after decimation: quadric collapse can leave a fold-over, so the client inspects the result and repairs once more before exporting — the agent gets numbers, not "done", precisely so it can make that call. With Claude Desktop the same run is one sentence: *"Prepare `examples/sample_scan.obj` for a real-time scene with a 20k-face budget and explain what you changed."*
## Architecture
```text
Claude Desktop / Claude Code
│ MCP (stdio, JSON-RPC)
▼
src/mesh_prep_mcp/server.py tools · resource · prompt (mcp SDK, 1.x and 2.x)
│
src/mesh_prep_mcp/ops.py load / inspect / repair / decimate / scale / export (trimesh, fast-simplification)
│
cpp/ meshstats C++17: bounds, area, signed volume, degenerate faces,
└─ bindings.cpp boundary & non-manifold edges, duplicate vertices (pybind11)
```
The C++ backend is optional: if the extension is not built, `ops.inspect` falls back to a NumPy implementation and reports `backend: "python"`. The C++ library has no Python dependency and its own tests (`ctest`).
## Quick start
```bash
git clone https://github.com/Horuspokus/mesh-prep-mcp && cd mesh-prep-mcp
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
# C++ core + pybind11 module (optional but recommended)
cmake -S cpp -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build
ctest --test-dir build --output-on-failure
pytest -q # Python tests
python scripts/make_sample_mesh.py && python scripts/demo_client.py
```
> **If `import mesh_prep_mcp` fails after the editable install** (seen once on macOS with Python 3.14 in an iCloud-synced folder), install it non-editable instead: `python -m pip install --force-reinstall --no-deps .` — and re-run that after editing files under `src/`.
### Claude Desktop
Copy `examples/claude_desktop_config.json` into your `claude_desktop_config.json` and replace the two absolute paths. `MESH_PREP_ROOT` sandboxes file access to one directory (recommended).
### Claude Code
```bash
claude mcp add mesh-prep -e MESH_PREP_ROOT=$PWD/examples -- $PWD/.venv/bin/mesh-prep-mcp
```
## Design notes
- **Immutable results.** Operators never mutate a registered mesh; they add a new one with its provenance (`history`). Cheap, and it makes "compare budgets A and B" or "go back to the repaired version" trivial for the agent.
- **Numbers, not adjectives.** Every tool returns measured before/after values (face count, surface-area drift, watertightness) so the agent's explanation to the user is grounded.
- **Issue → operator mapping.** `inspect_mesh` does the triage once, in code; the agent only has to follow the recommendation or argue with it.
- **Safety.** `MESH_PREP_ROOT` rejects paths outside one directory; exports never overwrite the input by default naming.
- **C++ where it pays.** The statistics pass is the one thing that runs on every step and on every mesh size, so it lives in C++ with the GIL released; everything else stays in Python for readability.
## Roadmap
- IFC input through IfcOpenShell (element-level preparation, properties kept as metadata)
- glTF LOD chains (`decimate_chain(budgets=[200k, 50k, 10k])`)
- Material / texture checks (missing UVs, oversized textures)
- Scene assembly: place several prepared assets into one glTF scene
- Streamable-HTTP transport for use behind a web app
## License
MIT — Gökhan Dönmez, 2026.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues