bonsai-mcp
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 IFCsemantics — 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
- 2. Install
- 3. Install the Blender add-on
- 4. Selfcheck
- 5. Claude Desktop config
- 6. Claude Code config
- 7. Usage procedure
- 8. Tool reference
- 9. Safe editing procedure
- 10. Unit conventions
- 11. Environment variables
- 12. Troubleshooting
- 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
git clone https://github.com/nhantruong96/bonsai-mcp.git
cd bonsai-mcp
uv sync
To pin Python 3.13:
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:
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:
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
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
- Open an IFC file:
Bonsai > Project > Load Project. - In the 3D viewport press
N→ Bonsai MCP tab → Start Server. - The panel shows
Running, the address127.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
uv run python -m bonsai_mcp --selfcheck
uv run python -m bonsai_mcp --list-tools
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:
uv run python -m pytest tests/test_blender_integration.py -v
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)
{
"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
claude mcp add bonsai-mcp -- uv --directory C:\Tools\bonsai-mcp run python -m bonsai_mcp
Verify:
claude mcp list
7. Usage procedure
7.1. Without Blender
ifc_open— open the file; check schema, units and element counts.ifc_list_property_names— always run this before writing any property query.ifc_model_infoorifc_spatial_tree— get the storey structure and per-class counts.ifc_list_entities/ifc_select— filter to the elements you need; page withlimit+offset.ifc_get_element— full detail for one element.ifc_geometry_info— geometry, profile and absolute placement of that element.ifc_representation_stats/ifc_validate— whole-file QC.
7.2. With Blender
- Load Project in Bonsai, then Start Server in the Bonsai MCP panel.
bl_status— confirm the file, the versions, and that the session switched toblendermode.- From here every
ifc_*tool reads the model Blender holds. Do not callifc_open— it is refused by design. bl_set_viewfor an overview,bl_focus_element(guid)for one element.- For anything internal pass
isolate=true, or usebl_isolate, thenbl_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
- Run
ifc_geometry_infobefore any geometry edit — check the real parameter names and whether the element shares a representation map. - Edit. The session's first edit automatically writes
<name>.backup-<timestamp>.ifcbeside the original. - Read the
before/aftervalues in the result. Every edit tool returns both. - In blender mode, use
bl_focus_elementorbl_screenshotto look at the result. The viewport is already resynced. ifc_saveto a new path. Overwriting requires an explicitoverwrite=true.- If it went wrong,
ifc_revertreturns 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, 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 |
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
uv sync
uv run python -m pytest -v
uv run python -m bonsai_mcp --selfcheck
Conventions:
uv.lockis committed;.venv/never is.- No
print(). stdout is the JSON-RPC channel; log throughconfig.pyto stderr. - Nothing in
src/may importbpy— the server must run with Blender closed. A test enforces it. addon/bonsai_mcp_addon/protocol.pyruns on the socket thread and must never touchbpy; allbpycalls live inhandlers.pyand run only from the main-thread timer.bpyis 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
tracebackfield; Blender-side failures carry Blender's own traceback too. - Every write goes through
ifcopenshell.api, and throughSession.write; no tool touches the model directly. - There are no raw-mesh tools. Editing a mesh freely and writing it back turns a
SweptSolidinto aTessellation— 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 oneIfcRepresentationMap, 1 column with its own profile, 1 tessellated slab.
License: MIT — see LICENSE.