Tools reference¶
Bonsai MCP exposes fourteen tools, split into two categories:
| Category | Behaviour | Tools |
|---|---|---|
| QUERY | Read-only. Safe to call without confirmation. | get_scene_info, get_selected_objects, list_elements, get_psets, get_viewport_screenshot, get_ifc_project_info, get_spatial_structure, get_quantities |
| EDIT | Mutates Blender state, the IFC model, or the filesystem. | execute_ifc_code, execute_blender_code, save_ifc_file, refresh_view, refresh_geometry, reload_project |
The category is encoded in two places:
- A
[QUERY]or[EDIT]prefix at the start of every tool description. - The standard MCP
Tool.annotationshints (readOnlyHint,destructiveHint,idempotentHint,openWorldHint), which MCP clients can read to render the distinction natively.
Every tool also declares an outputSchema and returns
structuredContent alongside the human-readable JSON text, so clients
that understand structured tool output get typed results for free. Input
schemas are generated from the same Pydantic models the server validates
with, so schema and behaviour cannot drift apart.
List-shaped results are paged: pass limit/offset where offered and
watch the total/truncated flags instead of requesting everything at
once.
Errors follow one convention: every failure states what went wrong and
what to do next (for example, the read-only refusal names the exact
Blender panel toggle to flip, and an invalid selector query returns a
syntax cheat sheet). Clients and models should follow the instruction in
the error instead of retrying the same call.
Two code execution tools live in the EDIT category: execute_ifc_code
(preferred, IFC-only, bpy blocked) and execute_blender_code (full bpy
access). See Safety.
get_scene_info (QUERY)¶
Returns a scene-level snapshot. When query is supplied, the response also
includes an objects array filtered by the query.
For structured element listings, prefer the dedicated
list_elements tool; the query parameter here
remains for compatibility.
Inputs (all optional):
{
"query": "walls",
"ifc_class": null,
"name": null,
"global_id": null,
"limit": 200,
"offset": 0
}
Supported query values:
| Query | Notes |
|---|---|
| (omitted) | Scene summary only; no objects field. |
all |
All objects in the scene. |
selected |
Currently selected objects. |
walls, doors, windows, spaces, slabs, columns, beams, roofs, stairs |
Filtered by IFC class. |
by_class |
Requires ifc_class, e.g. "IfcCovering". |
by_name |
Requires name (exact Blender object name). |
by_global_id |
Requires global_id (IFC GlobalId). |
Returns:
{
"scene_name": "Scene",
"object_count": 42,
"selected_count": 1,
"selected_objects": ["IfcWall/MyWall"],
"collections": ["IfcSite", "IfcBuilding"],
"object_type_counts": {"MESH": 40, "EMPTY": 2},
"ifc_available": true,
"blender_version": "4.2.0",
"objects": [
{
"name": "IfcWall/MyWall",
"type": "MESH",
"location": [0.0, 0.0, 0.0],
"dimensions": [5.0, 0.2, 3.0],
"ifc_class": "IfcWall",
"global_id": "2O2Fr$t4X7Zf8NOew3FK6X"
}
],
"objects_total": 28,
"objects_truncated": false,
"objects_offset": 0,
"objects_limit": 200
}
The objects field (and its paging metadata) is omitted entirely when no
query is supplied. On huge scenes, page with limit/offset instead of
raising the limit.
get_selected_objects (QUERY)¶
Inputs (optional): limit (1-1000, default 200), because a box-select can
grab thousands of objects.
Returns:
{
"objects": [
{
"name": "IfcWall/MyWall",
"type": "MESH",
"location": [0.0, 0.0, 0.0],
"dimensions": [5.0, 0.2, 3.0],
"ifc_class": "IfcWall",
"global_id": "2O2Fr$t4X7Zf8NOew3FK6X"
}
],
"total": 1,
"truncated": false
}
ifc_class and global_id are null if no IFC data is associated.
list_elements (QUERY)¶
Lists IFC-backed elements (objects without an IFC entity are skipped) with
structured filters, replacing most uses of get_scene_info queries.
Inputs (all optional):
{
"ifc_class": "IfcWall",
"name_contains": "kitchen",
"storey": "Level 1",
"selector": "IfcWall, Pset_WallCommon.FireRating=F30",
"limit": 200,
"offset": 0
}
| Input | Notes |
|---|---|
ifc_class |
Inheritance-aware: IfcWall also matches IfcWallStandardCase. |
name_contains |
Case-insensitive substring match on the Blender object name. |
storey |
Name or GlobalId of an IfcBuildingStorey. Elements inside the storey's spaces count as in the storey. |
selector |
Optional IfcOpenShell selector query, applied on top of the other filters. Examples: IfcWall, material=concrete, IfcWall, Pset_WallCommon.FireRating=F30, IfcElement, Name=/W.*1/ (regex). Uses ifcopenshell.util.selector syntax; an invalid query returns an error with a short cheat sheet. |
limit / offset |
Paging, 1-1000 per page (default 200). |
Returns:
{
"elements": [
{
"name": "IfcWall/MyWall",
"type": "MESH",
"location": [0.0, 0.0, 0.0],
"dimensions": [5.0, 0.2, 3.0],
"ifc_class": "IfcWall",
"global_id": "2O2Fr$t4X7Zf8NOew3FK6X"
}
],
"total": 28,
"truncated": false,
"offset": 0,
"limit": 200
}
get_spatial_structure (QUERY)¶
Returns the project's spatial hierarchy as a tree: IfcProject ->
IfcSite -> IfcBuilding -> IfcBuildingStorey -> IfcSpace, with
storey elevations and (by default) counts of contained elements grouped by
IFC class. Answers "what is in this building, storey by storey" without
any code execution.
Inputs (optional): include_element_counts (default true).
Returns:
{
"schema": "IFC4",
"tree": {
"name": "My Project",
"ifc_class": "IfcProject",
"global_id": "0YvctVUKr0kugbFTf53O9L",
"children": [
{
"name": "Site",
"ifc_class": "IfcSite",
"global_id": "...",
"children": [
{
"name": "Building",
"ifc_class": "IfcBuilding",
"global_id": "...",
"children": [
{
"name": "Level 1",
"ifc_class": "IfcBuildingStorey",
"global_id": "...",
"elevation": 0.0,
"element_counts": {"IfcDoor": 9, "IfcWall": 28},
"element_total": 37,
"children": [
{"name": "Kitchen", "ifc_class": "IfcSpace", "global_id": "..."}
]
}
]
}
]
}
]
}
}
get_quantities (QUERY)¶
Quantity takeoff without code execution: aggregates every numeric value
found in elements' IFC quantity sets (base quantities such as walls'
NetSideArea or slabs' GrossVolume), grouped by IFC class and
optionally per building storey.
Inputs (all optional):
{
"ifc_classes": ["IfcWall", "IfcSlab"],
"by_storey": false
}
ifc_classes defaults to the common building element classes (walls,
slabs, columns, beams, doors, windows, roofs, stairs, coverings, spaces);
matching is inheritance-aware.
Returns:
{
"classes": {
"IfcWall": {
"count": 28,
"elements_without_quantities": 2,
"quantities": {
"NetSideArea": {"sum": 412.6, "elements": 26},
"Length": {"sum": 148.2, "elements": 26}
}
}
},
"units": {"LENGTHUNIT": "millimetre", "AREAUNIT": "square metre"},
"by_storey": {
"Level 1": {
"IfcWall": {"count": 12, "quantities": {"NetSideArea": {"sum": 180.1, "elements": 12}}}
}
}
}
elements_without_quantities is a model-quality signal: elements of that
class carrying no numeric quantities at all. units is a best-effort read
of the project's unit assignment. by_storey appears only when requested.
get_psets (QUERY)¶
Returns IFC property sets and quantity sets for one or more objects.
Accepts any mix of GlobalIds and Blender object names. Large batches are
paged: limit targets (default and maximum 100) are processed per call,
starting at offset (GlobalIds first, then names); the response reports
targets_total, truncated, and next_offset so clients can continue.
Inputs (at least one entry required between the two lists):
{
"global_ids": ["2O2Fr$t4X7Zf8NOew3FK6X"],
"names": ["IfcWall/MyWall"],
"limit": 100,
"offset": 0
}
Returns an ordered results list that mirrors the input order
(GlobalIds first, then names). Each entry records the original
request, plus either the pset payload or an error field:
{
"results": [
{
"request": {"global_id": "2O2Fr$t4X7Zf8NOew3FK6X"},
"object": {
"name": "IfcWall/MyWall",
"global_id": "2O2Fr$t4X7Zf8NOew3FK6X",
"ifc_class": "IfcWall"
},
"property_sets": {
"Pset_WallCommon": {
"IsExternal": true,
"LoadBearing": false,
"FireRating": "F30"
}
},
"quantity_sets": {
"Qto_WallBaseQuantities": {
"Length": 5.0,
"Height": 3.0,
"NetArea": 14.5
}
}
},
{
"request": {"name": "IfcDoor/Missing"},
"error": "not found"
}
]
}
Each property_sets / quantity_sets map passes through IfcOpenShell's native
ifcopenshell.util.element.get_psets shape, so besides the real property names
each set also carries an integer id key (the STEP id of the underlying
IfcPropertySet / IfcElementQuantity); clients can ignore it.
Returns a clear error if no IFC project is loaded. Per-target lookup
failures (missing GlobalId, missing object name, object without IFC link)
appear as error entries in the results list rather than aborting the
batch.
get_viewport_screenshot (QUERY)¶
Captures the 3D viewport, optionally aiming and framing it first. The
render is downscaled so its longest edge fits max_size and encoded as
JPEG by default, keeping the response safely inside MCP size caps (some
clients enforce 1 MB per tool result). Scene render settings are restored
after the capture; the viewport orientation persists.
Inputs (all optional):
{
"max_size": 800,
"format": "jpeg",
"quality": 85,
"view": "iso",
"fit": "all",
"azimuth": null,
"elevation": null,
"storey": null,
"shading": null,
"show_overlays": false,
"include_objects": false,
"max_objects": 50
}
| Input | Values | Notes |
|---|---|---|
max_size |
64-2048, default 800 | Longest edge in px. Downscales only, never upscales, and is additionally capped by the native viewport resolution. |
format |
jpeg (default), png |
JPEG is much smaller; PNG is lossless. A PNG that is estimated to exceed the response size cap is auto-downgraded to JPEG before rendering, with a note in the response. |
quality |
1-100, default 85 | JPEG only. |
view |
top, bottom, front, back, left, right, iso, camera |
Aims the viewport first. Axis names give orthographic views, iso a perspective isometric, camera the scene camera. Omit to keep the current orientation. |
azimuth / elevation |
degrees | Arbitrary view direction instead of view (mutually exclusive with it). Azimuth 0 = front, counter-clockwise seen from above; elevation 0 = horizontal, 90 = bird's eye (defaults to 30 when only azimuth is given). iso equals azimuth 45, elevation 30. |
fit |
all, selected |
Frames everything or the current selection. Framing is direction-aware: after the initial fit, the zoom is tightened to the content's projected 2D extent, so elevations fill the frame instead of the bounding sphere. |
storey |
storey Name or GlobalId | Isolates one IfcBuildingStorey for the shot: everything else is hidden and restored afterwards. storey + view='top' + fit='all' is a floor plan. |
shading |
wireframe, solid, material, rendered, class_colors |
Viewport shading for the capture (restored afterwards). class_colors renders solid with one flat color per IFC class and returns a legend. |
show_overlays |
boolean, default false | Overlays (grid, axes, gizmos) are hidden by default; they are noise for image analysis. Set true to keep them. |
include_objects |
boolean, default false | Adds screen-space 2D bounding boxes and view depth keyed by GlobalId to the text output. |
max_objects |
1-200, default 50 | Cap for include_objects. Selection is stratified across IFC classes (a few walls, doors, windows, ...) so ground slabs cannot crowd out everything else; truncation is flagged. |
Returns, in order:
- An MCP image content block (
image/jpegorimage/png). The image comes first because some MCP clients mishandle mixed content. - A text block with the image dimensions and attached base64 length
(so a client-side image drop is diagnosable from text), any
auto-downgrade note, the
class_colorslegend when requested, and structured viewport state: rotation quaternion, perspective mode (PERSP/ORTHO/CAMERA),is_orthographic_side_view, view distance, pivot location, and (when used) the appliedazimuth/elevationandstorey.
With include_objects=true, the viewport state also lists in-frame objects:
{
"objects_in_view": [
{
"name": "IfcWall/MyWall",
"ifc_class": "IfcWall",
"global_id": "2O2Fr$t4X7Zf8NOew3FK6X",
"box": [0.329, 0.55, 0.712, 0.563],
"depth": 24.6
}
],
"objects_in_view_total": 1250,
"objects_truncated": true
}
box is [x_min, y_min, x_max, y_max], normalized 0-1 with the origin at
the image's top-left (smaller y is higher on screen). depth is the
view-space distance to the object's bounding-box centre in model units, so
near/far ordering is available from text alone. Boxes are approximate
(full object extent, ignoring occlusion) but preserve relative spatial
layout, which lets a text-only agent reason about containment,
left/right/above/below relations, relative sizes, and depth ordering even
when its client does not deliver tool-result images.
If Blender is running in --background mode or has no open 3D viewport,
the tool returns a clear error (a visible VIEW_3D area is required).
When several 3D viewports are open, the largest one is used.
get_ifc_project_info (QUERY)¶
Inputs: none.
Returns:
{
"schema": "IFC4",
"project_name": "My Project",
"project_global_id": "0YvctVUKr0kugbFTf53O9L",
"entity_counts": {
"IfcSite": 1,
"IfcBuilding": 1,
"IfcBuildingStorey": 3,
"IfcWall": 28,
"IfcDoor": 9,
"IfcWindow": 14,
"IfcSlab": 6,
"IfcSpace": 22,
"IfcColumn": 0,
"IfcBeam": 0,
"IfcRoof": 0,
"IfcStair": 0
},
"materials": {
"count": 14,
"names": ["Concrete", "Glass", "Steel", "Timber"],
"truncated": false
},
"classifications": {
"count": 1,
"systems": [
{"name": "Uniclass 2015", "source": "NBS", "edition": "v1.20"}
],
"truncated": false
}
}
materials.names is sorted and de-duplicated; materials.count is the raw
IfcMaterial count. Both lists are capped at 100 entries; truncated: true
means more entities exist than were returned.
If no IFC project is loaded, returns a clear error.
execute_ifc_code (EDIT)¶
Preferred code execution tool. Runs IfcOpenShell / Bonsai API code with
bpy access blocked. Use this for all IFC/BIM data operations.
Pre-injected namespace (no imports needed):
| Variable | What it is |
|---|---|
ifc |
The currently loaded IFC file (ifcopenshell.file or None) |
ifcopenshell |
The ifcopenshell module |
ifc_api |
ifcopenshell.api (high-level create/edit operations) |
element_util |
ifcopenshell.util.element (psets, qtos, traversal) |
tool |
bonsai.tool module (or None if unavailable) |
get_ifc_file() |
Returns the loaded IFC file, raising a clear error if none is open |
get_default_container() |
Returns the active spatial container (e.g. the active storey) |
save_and_load_ifc(path=None) |
Legacy helper (save then full reload); prefer the refresh tools |
Viewport sync: edits made here change the in-memory IFC model but do
not appear in the Blender viewport until refreshed. Pick the cheapest
tier for what the edit touched: refresh_view after
data-only edits (names, psets, classifications),
refresh_geometry after moving elements or
changing representations, reload_project after
creating or deleting elements. Do not save just to make edits visible;
saving is a separate, durability-only step.
MCP edits are outside Blender's undo stack: Ctrl+Z will not revert them.
Inputs:
{ "code": "walls = ifc.by_type('IfcWall')\nfor w in walls:\n print(w.Name, w.GlobalId)" }
Returns:
{
"success": true,
"stdout": "MyWall 2O2Fr$t4X7Zf8NOew3FK6X\n",
"stderr": "",
"stdout_truncated": false,
"stderr_truncated": false,
"stdout_bytes": 38,
"stderr_bytes": 0,
"error": null,
"traceback": null,
"ifc_available": true,
"namespace_keys": ["ifc", "ifcopenshell", "ifc_api", "element_util", "tool",
"get_ifc_file", "get_default_container", "save_and_load_ifc"]
}
If the code contains bpy imports or references, the tool returns an error
directing you to use execute_blender_code instead.
execute_blender_code (EDIT)¶
Fallback for operations that genuinely require bpy (viewport manipulation,
rendering, object transforms, modifiers). For IFC/BIM data work, always
prefer execute_ifc_code.
The same three helper functions as execute_ifc_code are pre-injected:
get_ifc_file(), get_default_container(), and save_and_load_ifc(path=None).
Inputs:
{ "code": "import bpy; print(len(bpy.context.scene.objects))" }
Returns:
{
"success": true,
"stdout": "42\n",
"stderr": "",
"stdout_truncated": false,
"stderr_truncated": false,
"stdout_bytes": 3,
"stderr_bytes": 0,
"error": null,
"traceback": null
}
Errors are reported with success: false, a stringified error, and the full
traceback.
stdout/stderr are each capped at 256 KB to keep one MCP message bounded;
when truncation occurs, the corresponding _truncated flag is true and
_bytes reports the original size before trimming.
refresh_view (EDIT)¶
Syncs the Blender scene after data-only IFC edits: names, descriptions, psets, quantities, classifications. Sub-millisecond per element even on million-entity models. Does not write to disk. Does not rebuild geometry.
Inputs:
{ "global_ids": ["2O2Fr$t4X7Zf8NOew3FK6X"] }
Returns per-element results (refreshed counts successes; elements that
were deleted or have no Blender object yet get an error entry pointing at
reload_project):
{
"refreshed": 1,
"results": [
{ "global_id": "2O2Fr$t4X7Zf8NOew3FK6X", "object": "IfcWall/NewName", "renamed": true }
],
"note": "Data-only sync (names). Nothing was written to disk; ..."
}
refresh_geometry (EDIT)¶
Rebuilds Blender geometry and placement for specific elements after
geometric IFC edits: moved elements (changed ObjectPlacement) or changed
representations. Fast and targeted; does not write to disk; the rest of
the scene (selection, visibility, camera) is untouched.
Not covered: newly created and deleted elements; use
reload_project for those.
Inputs:
{ "global_ids": ["2O2Fr$t4X7Zf8NOew3FK6X"] }
Returns per-element results with placement_synced flags and a
representations_reloaded summary flag.
reload_project (EDIT)¶
Full scene rebuild from the in-memory IFC model. The model is written
to a temporary file and reloaded from it; the project file on disk is not
modified, and the project path is restored afterwards so saving (Ctrl+S or
save_ifc_file) still targets the user's file.
Slow: seconds to minutes on large models, and it resets selection, visibility, and camera. Use only when targeted refresh is insufficient: after creating or deleting elements, or when the scene has genuinely diverged.
Takes no inputs. Returns:
{ "reloaded": true, "project_path": "C:/models/house.ifc", "path_restored": true }
save_ifc_file (EDIT)¶
Writes the IFC model to disk. Durability only: call it when the user asks to save. It does not refresh the viewport; use the refresh tools for visibility. The user pressing Ctrl+S in Blender is equivalent to the in-place mode.
Inputs (all optional):
{ "output_path": "/abs/path/to/output.ifc", "overwrite": false, "reload": false }
Two modes:
- In-place save (
output_pathomitted): saves the project back to its own file, like Bonsai's File > Save IFC. There is no overwrite guard here, since writing the project's own file is the point. Fails with a clear error if the project has never been saved (no path yet). - Save-as (
output_pathgiven): writes to the new path. Refuses to overwrite an existing file unlessoverwrite=true; the parent directory must already exist. The project keeps pointing at its original file.
reload=true is a legacy flag that reloads the project from the saved file
afterwards; prefer reload_project, which does not require saving first.
Returns:
{
"saved": true,
"output_path": "/abs/path/to/output.ifc",
"in_place": false,
"method": "bonsai.bim.export_ifc.IfcExporter",
"reloaded": false
}
method reports which writer ran: "bonsai.bim.export_ifc.IfcExporter"
(preferred; Bonsai's exporter first syncs pending Blender-side edits into
the IFC model) or "ifcopenshell.write" (fallback when Bonsai is not
available).
Resources and prompts¶
Beyond tools, the server exposes read-only state as MCP resources (clients can pin them into context without a tool round trip):
| URI | Content |
|---|---|
bonsai://project |
The get_ifc_project_info payload. |
bonsai://scene |
The scene summary (no object query). |
bonsai://element/{global_id}/psets |
Resource template: psets/qtos for one element. |
All resources return application/json.
Two MCP prompts encode the workflows the server instructions describe in prose:
| Prompt | Arguments | What it does |
|---|---|---|
model-audit |
focus (optional) |
Walk the model with the query tools (project info, spatial tree, quantities, pset spot-checks, screenshots) and produce a quality report. |
visual-verify |
what_changed (optional) |
Reload if needed, take overview and detail screenshots, compare against the intended edit. |
Long operations also emit coarse MCP progress notifications when the
client supplies a progressToken.