bimq
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@bimq Find fire-rated doors on level 3"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
bimq
A read-only BIM query server for agents. Point it at an IFC or gbXML model and it answers structured questions — fire-rated doors on level 3, elements with no material assigned, spaces below the minimum daylight area — bounded by a policy file, with deterministic results and a citation back to the GlobalId and source line behind every row.
Zero dependencies. Python 3.11+. MCP server over stdio, plus a CLI that answers the same questions so you can check a policy before you trust an agent to it.
bimq query elements_by_property model.ifc \
type=IfcDoor storey="Level 3" property=FireRating op=existsid type name tag storey_name source match
---------------------- ------- -------- ---- ----------- -------------- -------------------------------
0XBbD$nZDLuRru91_CQ_xe IfcDoor Door-302 D302 Level 3 office.ifc:314 Pset_DoorCommon.FireRating=EI60
31kamnSrrNbf3eF0_vhXrJ IfcDoor Door-301 D301 Level 3 office.ifc:304 Pset_DoorCommon.FireRating=EI60
2 row(s)
digest: sha256:06321f331735417dd149d649b8e26de71a63ff2bb26a31c38cbf66a4f0314b77Then check it, because a citation you cannot follow is just a confident-looking string:
bimq cite model.ifc 31kamnSrrNbf3eF0_vhXrJ31kamnSrrNbf3eF0_vhXrJ (IfcGloballyUniqueId)
office.ifc:304 #297
#297= IFCDOOR('31kamnSrrNbf3eF0_vhXrJ',#5,'Door-301',$,$,$,$,'D301',2100.0,900.0,.DOOR.,.SINGLE_SWING_LEFT.,$);Why
The current instinct is to dump IFC text into a context window. That fails immediately at real model sizes, and it fails quietly: a 300 MB model is roughly 95% geometry, so what fits in the window is a truncated arbitrary slice, and the model answers from it anyway. The failure looks like a fluent paragraph about a door that does not exist.
bimq inverts it. The model stays on disk. Queries are structured, the answers are small, and every row carries the id and line it came from — so a claim can be checked against the file instead of trusted.
Three properties hold for every answer:
Bounded. A TOML policy file says what is readable — which files, which queries, which types, which storeys, which properties. The engine reduces the model to the visible set before the query runs, so a query primitive cannot reach what the policy hides even by accident.
Deterministic. Same model, same query, same bytes. Every answer carries a
digest you can pin in a test. Element order, group order and float rounding are
all fixed; the read block size and the file's name do not change a finding.
Cited. Every row carries {id, id_kind, source, ref, line}. bimq cite
resolves it back to the original statement. The test-suite re-reads the recorded
line for every element of every fixture and fails if the id is not there.
Related MCP server: artagon-vault-mcp
Install
pip install bimqOr run it from a clone with no install at all — there is nothing to build:
python -m bimq describe tests/fixtures/office.ifcUse it as an MCP server
{
"mcpServers": {
"bimq": {
"command": "bimq",
"args": ["serve", "/srv/bim/tower.ifc", "-p", "/srv/bim/policy.toml"]
}
}
}Every query primitive becomes a bim_* tool, all annotated readOnlyHint, plus
bim_cite. Omit the model path to let each call name its own file — then
allow_sources is what stands between a path argument and your filesystem.
The server tells the agent how to behave on initialize: call bim_model_summary
first, quote a GlobalId for anything you assert, treat truncated as "there are
more", and read notes — because no results and no data recorded are
different findings and only the notes distinguish them.
A policy refusal comes back as a successful tool result carrying
policy_denied and the rule that fired, not as a protocol error. An agent that
receives a protocol error retries; an agent told "this policy does not expose
costs" reports the limit and moves on.
Query primitives
Primitive | Answers |
| schema, units, storeys, entity types, property-set names |
| project → site → building → storey → space |
| elements of a type, subtypes included |
| property comparison; the fire-door workhorse |
| data completeness: who has no value for this field |
| no material through any of IFC's five ways of saying so |
| rooms inside an area range, always in m² |
| distinct values with counts — run this before guessing names |
| totals grouped by type, storey or PredefinedType |
| expand specific GlobalIds to every pset and quantity |
bimq queries prints their parameters. List queries return compact rows on
purpose; element_detail is the drill-down, and keeping those separate is what
stops a query from becoming the context dump it replaced.
Aggregates report their own coverage. A roll-up over 200 walls where 160 carry no
quantity says so in summary and notes, because a total over 40 of 200 is not
a total.
Policy
name = "consultant-readonly"
[allow_sources]
roots = ["/srv/bim"]
max_bytes = 536870912
[allow_queries]
queries = ["model_summary", "spatial_tree", "elements_by_type", "elements_by_property"]
[scope_storeys]
names = ["Level 2", "Level 3"]
include_unplaced = false
[allow_types]
types = ["IfcBuiltElement", "IfcSpace", "IfcBuildingStorey"]
[deny_properties]
properties = ["*Cost*", "Pset_Tender.*"]
[redact_properties]
properties = ["*.Owner*", "*SerialNumber*"]
placeholder = "[redacted]"
[max_results]
limit = 200bimq policy check policy.toml # validate before shipping
bimq rules # every rule, with an exampleNotes on the design:
An unknown table is a hard error, not a warning. A file whose job is to withhold data must not fail open because of a typo.
denyandredactare different tools. A denied property is gone; a redacted one is present with a placeholder. The distinction matters to an agent: redaction says this exists and you are not being shown it, so the agent reports a gap instead of concluding nobody entered the data.Denial covers the query side too. You cannot filter on a denied property, because
op=gt value=1000repeated a few times reconstructs it.Withholding is reported, never silent. Answers carry
policy.elements_withheldand a note. Truncation setstruncated: true.Every answer is capped even with no policy at all. "Unlimited" is not a sane default for something feeding a context window.
Source formats
Format | Notes |
IFC-SPF ( | IFC2X3 / IFC4 / IFC4X3, streaming reader, no dependencies |
gbXML ( | energy models; ids are stamped |
Wanted, one per PR: Revit export (pyRevit/Dynamo JSON), Speckle stream, IFC-JSON, COBie. See CONTRIBUTING.md.
How the IFC reader stays small
bimq/sources/spf.py is a complete ISO 10303-21 reader in under 400 lines. The
parts that matter:
The file is scanned in 4 MB blocks, so a 300 MB model is never one string. A block boundary can land inside a string literal, so the scanner explicitly matches unterminated literals and carries them forward. Tested at block sizes down to one byte, where the result must still be byte-identical.
A
;inside'a;b'does not end a statement,''is an escaped quote, and\X2\...\X0\decodes to UTF-16 — soPhòng họpsurvives the round trip.Comments appear between statements, inside parameter lists, and around section markers. All three are handled; the reported line still points at the entity.
Geometry is never loaded. An instance is kept only if its first attribute is a syntactically valid GlobalId — making it an
IfcRootsubtype — or if it is one of ~30 unrooted carriers of property, quantity, material or unit data. The test is applied to the raw text before tokenising, which is where the parse time on a real file actually goes.
Check the throughput claim yourself without needing a model of your own — this writes a file shaped like a real export (a modest element count buried in geometry), parses it, and reports:
$ bimq bench --synthetic 20000
synthetic model: 20000 elements among 820006 instances
tmp6l05ix9x.ifc: 32.2 MiB, 20001 elements
parse: 1.71 s · 18.8 MiB/s · 11,680 elements/s
peak rss: 107 MiB (3.3x file size)820,006 instances go in; 20,001 elements stay resident. That ratio is the whole argument — resident size tracks how many things the building has, not how many points were needed to draw them.
Model files are treated as untrusted input. The gbXML reader refuses entity
declarations outright, so a file cannot carry a billion-laughs expansion or an
external entity pointing at /etc/passwd.
Units
Every number bimq returns is SI: metres, m², m³. A model authored in millimetres
with areas in square metres (what Revit exports) and one authored in feet with
areas in square feet both answer spaces_by_area max_m2=8 correctly.
IfcConversionBasedUnit chains are resolved, not guessed.
Try it
The fixtures are synthetic — stated plainly, because a fixture pretending to be a
real project is one nobody can check. What makes them useful is that the defects
are deliberate and enumerated: a wall with no material, a fire door with no
rating, a room below 8 m², a room with no area quantity at all, a door whose
rating is inherited from its type, and a Vietnamese room name written with \X2\
escapes.
python scripts/make_fixture.py tests/fixtures
bimq describe tests/fixtures/office.ifc
bimq query elements_missing_material tests/fixtures/office.ifc type=IfcWall
bimq query spaces_by_area tests/fixtures/office.ifc max_m2=8
bimq query property_values tests/fixtures/office.ifc type=IfcDoor property=FireRating
bimq query spaces_by_area tests/fixtures/legacy-imperial.ifc max_m2=8 # authored in feet
bimq query spaces_by_area tests/fixtures/clinic.gbxml max_m2=8 # gbXML, same primitiveDevelopment
make test # the suite
make fixtures # regenerate fixtures (byte-identical; CI checks this)
make bench # parse throughput on a fixtureLicense
MIT
Available Tools
11 toolsbim_citeQuote the source for a GlobalIdARead-only
Return the original file text behind a GlobalId: the STEP line that declares it, verbatim. Use it to verify a claim before repeating it, and when a reviewer asks where a number came from.
Inputs: id (required), max_lines.
bim_element_detailElement detail by GlobalIdARead-only
The drill-down half of the loop: list queries return compact rows, and this expands named GlobalIds into every property set, quantity and material.
Ids that are not returned are reported in not_found, which covers both "no
such element" and "hidden by policy" — deliberately without distinguishing
them, so the response does not leak the shape of what is filtered out.
Inputs: ids (required), max_ids.
bim_elements_by_propertyElements by property valueARead-only
Find elements whose property matches a condition, e.g. fire-rated doors on level 3: type=IfcDoor, storey=Level 3, property=FireRating, op=exists.
pset is optional and usually best left out: the same field lands in
Pset_DoorCommon on one exporter and a bespoke set on another, so by default
every property set on the element is searched. The matched set and value are
returned alongside each hit, so the answer says where it came from.
Inputs: property (required), op, pset, type, +5 more.
bim_elements_by_typeElements by typeARead-only
List elements of an IFC type, optionally narrowed by storey, name or
PredefinedType. Subtypes are included by default, so IfcBuiltElement returns
walls, slabs, doors and the rest. Returns one compact row per element with its
GlobalId citation.
Inputs: type, storey, name_contains, predefined_type, +1 more.
bim_elements_missing_materialElements with no material assignedARead-only
Elements that no IfcRelAssociatesMaterial reaches — through a plain material, a
material list, a layer set, a profile set or a constituent set, all of which are
resolved before the check. Restrict with type to ask the question that
actually matters, e.g. only IfcWall and IfcSlab.
Inputs: type, storey, name_contains, predefined_type, +1 more.
bim_elements_missing_propertyElements missing a required propertyARead-only
The inverse of elements_by_property, and the one most model-audit questions reduce to: which elements of this type have no value for this field? An empty string counts as missing by default, because exporters write one for "not filled in" as often as they omit the property entirely.
Inputs: property (required), pset, type, storey, +4 more.
bim_model_summaryModel summaryARead-only
Schema, authoring application, units, storeys, and the element counts by type — of the visible model, after policy. Cheap, bounded, and the right first call: it tells an agent which types and property sets exist before it starts guessing names at them.
Inputs: top_psets, top_types.
bim_property_valuesDistinct property valuesARead-only
What values does this field actually take? Run this before elements_by_property rather than guessing: it is how you learn that the model spells fire ratings "EI60", "60 min" and "60" in three different property sets, which no amount of schema documentation will tell you.
Returns each distinct value with a count and up to three example GlobalIds.
Inputs: property (required), pset, type, storey, +4 more.
bim_quantity_rollupQuantity roll-upARead-only
Total a named quantity (NetSideArea, GrossVolume, Length, ...) over a selection, grouped by type, storey or PredefinedType. Lengths, areas and volumes are already converted to m, m2 and m3.
Every group reports without_quantity next to count. A total computed over
40 of 200 walls is not a total, and the caller is told so rather than left to
assume the model was complete.
Inputs: quantity (required), type, storey, group_by, +3 more.
bim_spaces_by_areaSpaces by floor areaARead-only
Spaces whose floor area falls inside a range, e.g. "rooms below the 8 m2 minimum". Areas are reported in square metres after unit conversion, so the threshold means the same thing on a model authored in millimetres and one authored in feet.
Quantity names are tried in order: the explicit quantity if given, then
NetFloorArea, GrossFloorArea, Area. Spaces carrying none of them are reported
in notes rather than silently dropped — "no answer" and "no data" are
different findings.
Inputs: type, max_m2, min_m2, storey, +4 more.
bim_spatial_treeSpatial structureARead-only
The containment hierarchy, which is the map an agent needs before it can ask anything storey-shaped: it turns "level 3" into a GlobalId. Element counts are counts of visible elements, so a policy that scopes the model to two storeys shows zero elsewhere rather than hinting at what it is hiding.
Inputs: max_depth, with_element_ids.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 11 tools
Each tool maps to a distinct IFC query or audit operation: model discovery, typed/property selection, missing-data filters, detail expansion, aggregation, spatial lookup, and raw citation. The element-query family shares a resource but the filters and inverses are clearly separated, so misselection risk is low.
All 11 names use a uniform snake_case `bim_` prefix and predictable resource/action phrasing such as `elements_by_*`, `missing_*`, and `*_rollup`. The singular `bim_element_detail` versus plural `bim_elements_*` reads naturally rather than inconsistently.
11 tools fit the scope of an IFC/BIM inspection server: a small discovery set, a coherent element-query family, and specialized aggregation, spatial, and citation tools. There is no obvious filler or missing basic operation that would require extra tools.
The surface covers discovery, filtering, missing-data audits, quantity aggregation, spatial containment, and source citation, which is strong for a read-only BIM query server. Minor gaps remain around reverse material queries and richer relationship/containment queries, but most audit workflows are covered or workable through detail expansion.
Maintenance
Related MCP Connectors
Convert Revit files to XKT, IFC, or DWG and query BIM data via natural language.
Read-only MCP server for The Quiet Protocol's engines, benchmarks, proof, and business data.
Design buildings as IFC4 BIM models from a brief: an AI architect that plans, builds and edits.
Read-only Wikidata tools and identity evidence. Local optional Google KG; bounded public remote.
Related MCP Servers
- AlicenseAqualityDmaintenanceEnables AI agents to load, query, and analyze IFC building model files, including spatial structures, elements, properties, materials, and geometry.201MIT
- FlicenseNot gradedqualityCmaintenanceRead-only MCP server for querying an evidence-aware knowledge vault with temporal and provenance-aware data, supporting agent memory and semantic graph projections.-
- AlicenseAqualityDmaintenanceAn MCP server that lets users query, validate, convert, and compare IFC/BIM files using natural language. Runs fully locally and integrates with AI agents like Claude.8MIT
- AlicenseBqualityBmaintenanceOpen-source MCP server for Autodesk Revit 2022 to 2027: read, audit and edit live models with 89 tools, one Undo entry per change, and a workstation read-only lock.8916MIT