Skip to main content
Glama
Horuspokus

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.

[![CI](https://github.com/Horuspokus/mesh-prep-mcp/actions/workflows/ci.yml/badge.svg)](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)

![demo: scripts/demo_client.py preparing a noisy 82k-face scan](docs/demo.gif)

```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.