bonsai-mcp
# bonsai-mcp
[](https://github.com/nhantruong96/bonsai-mcp/actions/workflows/tests.yml)
[](pyproject.toml)
[](https://ifcopenshell.org/)
[](https://bonsaibim.org/)
[](https://modelcontextprotocol.io/)
[](LICENSE)
MCP server to read, view and edit IFC models with IfcOpenShell and Bonsai (Blender).
Query a model, look at it through Blender's viewport, and edit it with IFC
semantics — profiles, placements, property sets — never raw mesh operations.
The read tools run with Blender closed.
**Status:** Stage 3 — read tools, Blender/Bonsai viewport, and IFC-semantic editing.
---
## Table of contents
- [1. Requirements](#1-requirements)
- [2. Install](#2-install)
- [3. Install the Blender add-on](#3-install-the-blender-add-on)
- [4. Selfcheck](#4-selfcheck)
- [5. Claude Desktop config](#5-claude-desktop-config)
- [6. Claude Code config](#6-claude-code-config)
- [7. Usage procedure](#7-usage-procedure)
- [8. Tool reference](#8-tool-reference)
- [9. Safe editing procedure](#9-safe-editing-procedure)
- [10. Unit conventions](#10-unit-conventions)
- [11. Environment variables](#11-environment-variables)
- [12. Troubleshooting](#12-troubleshooting)
- [13. Development](#13-development)
---
## 1. Requirements
| | |
|---|---|
| Python | >= 3.11 (3.13 recommended) |
| `uv` | >= 0.5 |
| OS | Windows 11 (primary), macOS, Linux |
| Blender | >= 4.2, only needed for the `bl_*` tools |
| Bonsai | the build matching Blender's Python (Blender 5.x → `bonsai_py313`) |
The `ifc_*` tools run with Blender closed. The `bl_*` tools need Blender + Bonsai + the add-on.
---
## 2. Install
```bash
git clone https://github.com/nhantruong96/bonsai-mcp.git
cd bonsai-mcp
uv sync
```
To pin Python 3.13:
```bash
uv sync --python 3.13
```
**Sample model.** `--selfcheck` and pytest use KIT's FZK-Haus model. It is **not committed** — it is not this project's file — and is **downloaded automatically** on first run. Nothing to do.
Fetch it manually if the machine is offline at run time:
```bash
curl -L -o tests/fixtures/AC20-FZK-Haus.ifc https://www.ifcwiki.org/images/e/e3/AC20-FZK-Haus.ifc
```
Or point the selfcheck at any IFC file:
```bash
uv run python -m bonsai_mcp --selfcheck --file "D:\path\to\your.ifc"
```
---
## 3. Install the Blender add-on
Only needed for the `bl_*` tools. Skip this if you only use the read tools.
**3.1. Install Bonsai** (if it is not installed yet)
Blender 5.x runs Python 3.13 → take the `bonsai_py313` build. Blender 4.2 runs Python 3.11 → `bonsai_py311`.
Blender → `Edit > Preferences > Get Extensions` → the ˅ button top right → `Install from Disk...` → pick the Bonsai zip.
**3.2. Build the add-on zip**
```powershell
Compress-Archive -Path C:\Tools\bonsai-mcp\addon\bonsai_mcp_addon -DestinationPath C:\Tools\bonsai-mcp\bonsai_mcp_addon.zip -Force
```
**3.3. Install the add-on**
Blender → `Edit > Preferences > Get Extensions` → the ˅ button top right → `Install from Disk...` → pick `bonsai_mcp_addon.zip`.
**3.4. Point the add-on at `src`**
`Edit > Preferences > Add-ons` → `Bonsai MCP Bridge` → expand → fill in **bonsai-mcp src**:
```
C:\Tools\bonsai-mcp\src
```
This is what lets the `ifc_*` tools read the model Blender has open. Without it the `bl_*` tools still work but `ifc_*` cannot read through Blender.
Alternatively set the `BONSAI_MCP_SRC` environment variable, which takes precedence.
**3.5. Run**
1. Open an IFC file: `Bonsai > Project > Load Project`.
2. In the 3D viewport press `N` → **Bonsai MCP** tab → **Start Server**.
3. The panel shows `Running`, the address `127.0.0.1:9876`, request/error counts and the last 20 log lines.
Change the port under `Edit > Preferences > Add-ons > Bonsai MCP Bridge > Port`, then set `BONSAI_MCP_BLENDER_PORT` to the same value on the MCP side.
---
## 4. Selfcheck
```bash
uv run python -m bonsai_mcp --selfcheck
```
```bash
uv run python -m bonsai_mcp --list-tools
```
```bash
uv run python -m pytest
```
Selfcheck exits `0` on success. All logging goes to stderr.
The Blender integration tests skip themselves when no bridge is running. To run them, Start Server in Blender then:
```bash
uv run python -m pytest tests/test_blender_integration.py -v
```
```bash
uv run python -m pytest tests/test_blender_edit_integration.py -v
```
---
## 5. Claude Desktop config
File: `%APPDATA%\Claude\claude_desktop_config.json` (Windows) ·
`~/Library/Application Support/Claude/claude_desktop_config.json` (macOS)
```json
{
"mcpServers": {
"bonsai-mcp": {
"command": "uv",
"args": [
"--directory",
"C:\\Tools\\bonsai-mcp",
"run",
"python",
"-m",
"bonsai_mcp"
],
"env": {
"BONSAI_MCP_BLENDER_HOST": "127.0.0.1",
"BONSAI_MCP_BLENDER_PORT": "9876",
"BONSAI_MCP_LOG_LEVEL": "INFO"
}
}
}
}
```
The `env` block is optional; leaving it out uses exactly those defaults.
Restart Claude Desktop after editing the file.
---
## 6. Claude Code config
```bash
claude mcp add bonsai-mcp -- uv --directory C:\Tools\bonsai-mcp run python -m bonsai_mcp
```
Verify:
```bash
claude mcp list
```
---
## 7. Usage procedure
### 7.1. Without Blender
1. `ifc_open` — open the file; check schema, units and element counts.
2. `ifc_list_property_names` — **always run this before writing any property query.**
3. `ifc_model_info` or `ifc_spatial_tree` — get the storey structure and per-class counts.
4. `ifc_list_entities` / `ifc_select` — filter to the elements you need; page with `limit` + `offset`.
5. `ifc_get_element` — full detail for one element.
6. `ifc_geometry_info` — geometry, profile and absolute placement of that element.
7. `ifc_representation_stats` / `ifc_validate` — whole-file QC.
### 7.2. With Blender
1. Load Project in Bonsai, then Start Server in the **Bonsai MCP** panel.
2. `bl_status` — confirm the file, the versions, and that the session switched to `blender` mode.
3. From here every `ifc_*` tool reads the model Blender holds. **Do not call `ifc_open`** — it is refused by design.
4. `bl_set_view` for an overview, `bl_focus_element(guid)` for one element.
5. For anything internal pass `isolate=true`, or use `bl_isolate`, then `bl_isolate(restore=true)`.
---
## 8. Tool reference
### 8.1. IFC read tools
| Tool | Parameters | Returns |
|---|---|---|
| `ifc_open` | `path` | schema, exporter, creation dates, units, entity/element totals, `ifcopenshell.version` |
| `ifc_model_info` | `class_limit=60`, `storey_limit=200` | the above plus a per-class histogram and the storey list |
| `ifc_list_entities` | `ifc_class`, `limit=50`, `offset=0` | real total plus one page: guid, name, class, storey |
| `ifc_get_element` | `guid` \| `step_id` | attributes, psets, quantities, material, type, container |
| `ifc_select` | `query`, `limit=50`, `offset=0` | `ifcopenshell.util.selector` results |
| `ifc_spatial_tree` | `max_depth=4`, `class_limit=10` | Project → Site → Building → Storey with element counts per node |
| `ifc_list_property_names` | `limit=200`, `properties_per_set=60` | every pset/property name that actually exists, with occurrence counts |
| `ifc_geometry_info` | `guid` \| `step_id`, `include_metrics=true` | representations, items, profiles, absolute 4×4 matrix, bbox, volume, area; plus flat `profiles[]` and `extrusions[]` |
| `ifc_representation_stats` | `limit=60`, `shared_map_limit=20`, `map_scan_limit=2000` | breakdown by `RepresentationIdentifier/Type` and item class, parametric/tessellated share, most-shared maps |
| `ifc_validate` | `limit=50`, `express_rules=false` | condensed `ifcopenshell.validate` report |
Query syntax for `ifc_select`:
```
IfcWall
IfcWall, IfcSlab
IfcBeam, Name=/B-.*/
IfcSpace, Pset_SpaceCommon.IsExternal=TRUE
```
### 8.2. Blender tools
| Tool | Parameters | Returns |
|---|---|---|
| `bl_status` | `timeout=30` | whether Blender is reachable, Bonsai version, the IFC file it has open, `ifcopenshell` comparison of both sides, session state |
| `bl_screenshot` | `width=1280`, `timeout=30` | viewport PNG plus its real dimensions |
| `bl_focus_element` | `guid`, `isolate=false`, `width=1280`, `timeout=30` | selects and zooms to the element, returns the image and what was focused |
| `bl_isolate` | `guids[]`, `restore=false`, `width=1280`, `timeout=30` | hides everything else, returns the image and hidden/kept counts |
| `bl_set_view` | `direction`, `frame_all=true`, `width=1280`, `timeout=30` | sets the viewpoint, returns the image |
| `bl_reload` | `guids[]` (empty = all), `timeout=60` | resyncs the viewport from the in-memory IFC data |
`direction`: `front`, `back`, `left`, `right`, `top`, `bottom`, `axo`.
**`width` bounds the image's longest side, not literally its width.** The viewport aspect ratio is preserved. Clamped to 320–2048.
`bl_reload` does **not** reload from disk; it rebuilds the viewport from the in-memory IFC data.
### 8.3. Edit tools, group A: attributes
No geometry is touched. Each takes **`guid`** for one element or **`query`** for many.
| Tool | Parameters |
|---|---|
| `ifc_set_attribute` | `attribute`, `value`, `guid` \| `query`, `max_elements=100` |
| `ifc_set_pset_value` | `pset`, `property`, `value`, `guid` \| `query`, `max_elements=100` |
| `ifc_add_pset` | `pset`, `properties{}`, `guid` \| `query`, `max_elements=100` |
| `ifc_remove_pset` | `pset`, `guid` \| `query`, `max_elements=100` |
### 8.4. Edit tools, group B: placement
| Tool | Parameters |
|---|---|
| `ifc_move_element` | `dx`, `dy`, `dz` (metres), `guid` \| `query`, `max_elements=100` |
| `ifc_rotate_element` | `axis` (x/y/z), `degrees`, `guid` \| `query`, `max_elements=100` |
| `ifc_set_placement` | `guid`, `matrix` 4×4 (translation in metres) |
`ifc_rotate_element` spins the element about **its own origin**, not the project origin.
### 8.5. Edit tools, group C: parametric geometry
| Tool | Parameters |
|---|---|
| `ifc_edit_profile` | `guid`, `params{}`, `apply_to_all=false` |
| `ifc_set_extrusion_depth` | `guid`, `depth` (metres), `apply_to_all=false` |
| `ifc_patch` | `recipe`, `args{}`, `allow_file_replacement=false` |
Recipes: `ResetAbsoluteCoordinates`, `OffsetObjectPlacements`, `SetWorldCoordinateSystem`, `ConvertLengthUnit`, `Optimise`.
`args` uses the recipe's own parameter names; a wrong name is answered with the accepted list and their defaults.
### 8.6. Saving
| Tool | Parameters |
|---|---|
| `ifc_save` | `path`, `overwrite=false`, `validate=true` |
| `ifc_revert` | — |
---
## 9. Safe editing procedure
1. Run `ifc_geometry_info` before any geometry edit — check the real parameter names and whether the element shares a representation map.
2. Edit. The session's first edit automatically writes `<name>.backup-<timestamp>.ifc` beside the original.
3. Read the `before`/`after` values in the result. Every edit tool returns both.
4. In blender mode, use `bl_focus_element` or `bl_screenshot` to look at the result. The viewport is already resynced.
5. `ifc_save` to a **new path**. Overwriting requires an explicit `overwrite=true`.
6. If it went wrong, `ifc_revert` returns to the state before the first edit.
### Guardrails
| Guardrail | Behaviour |
|---|---|
| Automatic backup | Once per session, taken just before the first edit |
| Never overwrite the original | `ifc_save` requires a new path; overwriting needs `overwrite=true` |
| Shared geometry | Group C **refuses** when a map or profile is shared, reporting how many elements would be affected; pass `apply_to_all=true` to change all of them |
| No tessellation | Editing tessellated geometry is refused, and no tool creates a `Tessellation` |
| Bulk ceiling | A `query` matching more than `max_elements` (default 100) is refused without changing anything |
| Before and after | Every edit tool returns `before`, `after`, `changed` — never `{"status": "ok"}` |
| Viewport sync | In blender mode, Bonsai rebuilds exactly the affected elements after each edit |
**Note:** Blender's own undo (`Ctrl+Z`) does **not** cover MCP edits. Use `ifc_revert`.
---
## 10. Unit conventions
| Quantity | Unit returned | Note |
|---|---|---|
| Coordinates, dimensions | **metres** (`*_m`, `origin_m`, `bounding_box_m`) | already multiplied by `length_unit_scale_to_metre` |
| Profile parameters | both `parameters_file_units` and `parameters_metres` | |
| Extrusion depth | `extrusion_depth_file_units` and `extrusion_depth_m` | |
| Volume / area | `volume_m3`, `surface_area_m2` | from the IfcOpenShell kernel (SI) |
| Storey elevation | `elevation_m` | |
| Angles | degrees | |
| 4×4 matrix | `matrix_4x4_metres` (translation column converted to metres) and `matrix_4x4_file_units` | row-major, absolute |
| Values passed **into** edit tools | **metres** and **degrees** | `dx/dy/dz`, `depth`, `matrix`, `degrees` |
| `params` of `ifc_edit_profile` | **the file's own units** | use the numbers shown in `parameters_file_units` |
The placement matrix is **absolute**: the whole nested `IfcLocalPlacement` chain is multiplied through.
---
## 11. Environment variables
| Variable | Set where | Default | Effect |
|---|---|---|---|
| `BONSAI_MCP_LOG_LEVEL` | MCP side | `INFO` | `DEBUG`, `INFO`, `WARNING`, `ERROR` |
| `BONSAI_MCP_BLENDER_HOST` | MCP side | `127.0.0.1` | add-on address |
| `BONSAI_MCP_BLENDER_PORT` | MCP side | `9876` | add-on port; must match the preference set in Blender |
| `BONSAI_MCP_SRC` | Blender side | — | path to `src`; takes precedence over the add-on preference |
---
## 12. Troubleshooting
| Symptom | Fix |
|---|---|
| `No IFC model is open. Call ifc_open(path) first.` | Call `ifc_open` first |
| `IFC file not found: ...` | Use an absolute path; escape `\\` in JSON on Windows |
| `ifc_select` returns `total: 0` when the data is definitely there | Run `ifc_list_property_names`; real pset names often differ from the standard (`BaseQuantities` ≠ `Qto_WallBaseQuantities`) |
| `'IfcXxx' is not usable with by_type` | Wrong class name; see the suggestions in the error, or `ifc_model_info` |
| Result looks truncated | Read `total`, `has_more`, `next_offset` and call again with `offset` |
| Coordinates are in the wrong place | Use `origin_m` / `matrix_4x4_metres`, not the raw `IfcLocalPlacement` |
| `metrics.available: false` | The element has no geometry the kernel could build; read `metrics.error` |
| No profile found under `representations` | Clipping booleans nest; use the flat `profiles[]` and `extrusions[]` |
| `profile.parametric: false` | It is an `IfcArbitraryClosedProfileDef` — an explicit curve with no parameters to edit |
| `representation_maps.scan_note` is not `null` | The map scan was capped; raise `map_scan_limit` |
| `ModuleNotFoundError: mcp.server.fastmcp` | `mcp` moved to 2.x — re-run `uv sync` (`pyproject.toml` pins `<2`) |
| `Could not download the sample model` | Offline. Fetch it manually with the `curl` command in [section 2](#2-install), or pass `--file` |
| `returned N bytes that do not start with 'ISO-10303-21'` | A captive portal or error page answered instead of the file; nothing was written |
| `sample model sha256 is ..., expected ...` | The upstream file changed; tests asserting exact counts may now fail |
| pytest reports many `s` (skipped) | Blender integration tests, or a missing sample model; see the reasons with `-rs` |
| Server does not appear in Claude | Wrong `--directory` path, or Claude was not restarted |
| No log output anywhere | Logging goes to **stderr**, not stdout |
### Blender
| Symptom | Fix |
|---|---|
| `Blender is not running, or the Bonsai MCP add-on has not been started` | In Blender: 3D viewport → `N` → Bonsai MCP tab → Start Server |
| The `Bonsai MCP` panel is not there | The add-on is not enabled: `Edit > Preferences > Add-ons` → tick `Bonsai MCP Bridge` |
| `Could not bind 127.0.0.1:9876` | The port is taken; change Port in the preferences and set `BONSAI_MCP_BLENDER_PORT` to match |
| `VERSION MISMATCH` in `bl_status` | Pin them together: set `ifcopenshell==<Bonsai's build>` in `pyproject.toml` and re-run `uv sync` |
| `query_modules_loaded: false` | The add-on has no path to `src`; see [section 3.4](#3-install-the-blender-add-on) |
| `No IFC project is loaded in Blender` | Load one first with `Bonsai > Project > Load Project` |
| `ifc_open` refused with `ModelLockedError` | By design — Blender holds the model; load the other file in Blender instead |
| `bl_focus_element` returns an image without the element in it | It is behind other geometry; call again with `isolate=true` |
| The image is very tall or very narrow | The image follows the viewport aspect ratio; make the Blender window landscape |
| `No 3D viewport is available` | Blender is headless, or the workspace has no VIEW_3D area |
| `main thread is busy` | Blender has a dialog open or a modal operator running; close it, or raise `timeout` |
| Screenshots show the old geometry after an edit | Call `bl_reload` |
### Editing
| Symptom | Fix |
|---|---|
| `EditRefused: ... apply to N element(s), not one` | Shared geometry. Check with `ifc_geometry_info`; if that is what you want, pass `apply_to_all=true` |
| `EditRefused: ... will not tessellate` | The element has no parameters left; re-export it upstream as a swept solid |
| `matches N elements, over the max_elements ceiling` | Narrow the `query`, or raise `max_elements` after checking the count with `ifc_select` |
| `matched nothing, so there was nothing to edit` | Run `ifc_list_property_names`; real pset names often differ from the standard |
| `has no property set named '...'` | Create it first with `ifc_add_pset` |
| `has no parameter(s) [...]` | The error already lists the profile's real parameter names |
| `FileExistsError: Refusing to overwrite` | Save to a different path, or pass `overwrite=true` |
| `NoBackupError` | Nothing has been edited yet, so no backup exists |
| `rebuilds the model into a new file object` | `ConvertLengthUnit` / `Optimise`: standalone needs `allow_file_replacement=true`; blender mode needs the project closed in Blender first |
| `BackupError: Could not write the safety copy` | The folder holding the original is not writable; nothing was changed |
| `Ctrl+Z` in Blender does not undo an edit | Blender's undo does not cover MCP edits; use `ifc_revert` |
---
## 13. Development
```bash
uv sync
uv run python -m pytest -v
uv run python -m bonsai_mcp --selfcheck
```
Conventions:
- `uv.lock` is committed; `.venv/` never is.
- No `print()`. stdout is the JSON-RPC channel; log through `config.py` to stderr.
- Nothing in `src/` may import `bpy` — the server must run with Blender closed. A test enforces it.
- `addon/bonsai_mcp_addon/protocol.py` runs on the socket thread and must never touch `bpy`; all `bpy` calls live in `handlers.py` and run only from the main-thread timer. `bpy` is not thread safe — a call from the wrong thread crashes Blender dozens of commands later, somewhere unrelated. An AST test enforces it.
- Failing tools return the full traceback in the `traceback` field; Blender-side failures carry Blender's own traceback too.
- Every write goes through `ifcopenshell.api`, and through `Session.write`; no tool touches the model directly.
- There are no raw-mesh tools. Editing a mesh freely and writing it back turns a `SweptSolid` into a `Tessellation` — parameters gone, file inflated, recipient unable to edit. A test enforces it.
- The guardrail fixture is generated by `tests/steel_fixture.py`: 4 beams sharing one `IfcRepresentationMap`, 1 column with its own profile, 1 tessellated slab.
License: MIT — see [LICENSE](LICENSE).
TDQS
Scored across 28 tools
The bl_/ifc_ prefixes and read-vs-write verbs separate most tools cleanly, and each tool has a specific resource or action. A few adjacent reading tools and geometry-edit tools (e.g. ifc_model_info vs ifc_representation_stats, ifc_edit_profile vs ifc_set_extrusion_depth) could be confused, but their descriptions disambiguate them well.
All names are snake_case and consistently use bl_/ifc_ prefixes, which makes the set predictable. However, the verb convention is not uniform: mutation tools use verb_object patterns like ifc_set_attribute and ifc_move_element, while several read tools are noun-like (ifc_model_info, ifc_spatial_tree, ifc_geometry_info) rather than get_*.
At 28 tools, the set is above the 25-tool threshold where an agent's tool-selection burden becomes steep. Most tools are individually purposeful, but several read/reporting and viewport tools could plausibly be consolidated without losing capability.
The set covers a substantial IFC workflow: open, inspect, edit properties and pset values, transform geometry, save, validate, and revert. Notable gaps remain, however, including no creation or deletion of IFC elements, no material/type assignment tools, and no spatial-structure editing, so full modelling workflows are incomplete.