aec-platform

bimq

Community aec-platform
Updated

Read-only BIM query server for agents: structured queries over IFC/gbXML with a policy file, deterministic results, and GlobalId citations. Zero dependencies, MCP over stdio.

bimq

A read-only BIM query server for agents. Point it at an IFC or gbXML model andit answers structured questions — fire-rated doors on level 3, elements with nomaterial assigned, spaces below the minimum daylight area — bounded by a policyfile, with deterministic results and a citation back to the GlobalId and sourceline behind every row.

Zero dependencies. Python 3.11+. MCP server over stdio, plus a CLI that answersthe 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=exists
id                      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:06321f331735417dd149d649b8e26de71a63ff2bb26a31c38cbf66a4f0314b77

Then check it, because a citation you cannot follow is just a confident-looking string:

bimq cite model.ifc 31kamnSrrNbf3eF0_vhXrJ
31kamnSrrNbf3eF0_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 failsimmediately at real model sizes, and it fails quietly: a 300 MB model is roughly95% geometry, so what fits in the window is a truncated arbitrary slice, and themodel answers from it anyway. The failure looks like a fluent paragraph about adoor that does not exist.

bimq inverts it. The model stays on disk. Queries are structured, the answers aresmall, and every row carries the id and line it came from — so a claim can bechecked against the file instead of trusted.

Three properties hold for every answer:

Bounded. A TOML policy file says what is readable — which files, whichqueries, which types, which storeys, which properties. The engine reduces themodel to the visible set before the query runs, so a query primitive cannotreach what the policy hides even by accident.

Deterministic. Same model, same query, same bytes. Every answer carries adigest you can pin in a test. Element order, group order and float rounding areall 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 citeresolves it back to the original statement. The test-suite re-reads the recordedline for every element of every fixture and fails if the id is not there.

Install

pip install bimq

Or run it from a clone with no install at all — there is nothing to build:

python -m bimq describe tests/fixtures/office.ifc

Use 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, plusbim_cite. Omit the model path to let each call name its own file — thenallow_sources is what stands between a path argument and your filesystem.

The server tells the agent how to behave on initialize: call bim_model_summaryfirst, quote a GlobalId for anything you assert, treat truncated as "there aremore", and read notes — because no results and no data recorded aredifferent findings and only the notes distinguish them.

A policy refusal comes back as a successful tool result carryingpolicy_denied and the rule that fired, not as a protocol error. An agent thatreceives a protocol error retries; an agent told "this policy does not exposecosts" reports the limit and moves on.

Query primitives

Primitive Answers
model_summary schema, units, storeys, entity types, property-set names
spatial_tree project → site → building → storey → space
elements_by_type elements of a type, subtypes included
elements_by_property property comparison; the fire-door workhorse
elements_missing_property data completeness: who has no value for this field
elements_missing_material no material through any of IFC's five ways of saying so
spaces_by_area rooms inside an area range, always in m²
property_values distinct values with counts — run this before guessing names
quantity_rollup totals grouped by type, storey or PredefinedType
element_detail expand specific GlobalIds to every pset and quantity

bimq queries prints their parameters. List queries return compact rows onpurpose; element_detail is the drill-down, and keeping those separate is whatstops a query from becoming the context dump it replaced.

Aggregates report their own coverage. A roll-up over 200 walls where 160 carry noquantity says so in summary and notes, because a total over 40 of 200 is nota 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 = 200
bimq policy check policy.toml   # validate before shipping
bimq rules                      # every rule, with an example

Notes on the design:

  • An unknown table is a hard error, not a warning. A file whose job is towithhold data must not fail open because of a typo.
  • deny and redact are different tools. A denied property is gone; aredacted one is present with a placeholder. The distinction matters to anagent: redaction says this exists and you are not being shown it, so theagent 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=1000 repeated a few times reconstructs it.
  • Withholding is reported, never silent. Answers carrypolicy.elements_withheld and a note. Truncation sets truncated: true.
  • Every answer is capped even with no policy at all. "Unlimited" is not asane default for something feeding a context window.

Source formats

Format Notes
IFC-SPF (.ifc, .ifczip) IFC2X3 / IFC4 / IFC4X3, streaming reader, no dependencies
gbXML (.gbxml) energy models; ids are stamped gbXMLId, never confused with GlobalIds

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. Theparts that matter:

  • The file is scanned in 4 MB blocks, so a 300 MB model is never one string. Ablock boundary can land inside a string literal, so the scanner explicitlymatches unterminated literals and carries them forward. Tested at block sizesdown 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 — so Phòng họp survives the round trip.
  • Comments appear between statements, inside parameter lists, and around sectionmarkers. All three are handled; the reported line still points at the entity.
  • Geometry is never loaded. An instance is kept only if its first attributeis a syntactically valid GlobalId — making it an IfcRoot subtype — or if itis 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 theparse time on a real file actually goes.

Check the throughput claim yourself without needing a model of your own — thiswrites a file shaped like a real export (a modest element count buried ingeometry), 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 wholeargument — resident size tracks how many things the building has, not how manypoints were needed to draw them.

Model files are treated as untrusted input. The gbXML reader refuses entitydeclarations outright, so a file cannot carry a billion-laughs expansion or anexternal entity pointing at /etc/passwd.

Units

Every number bimq returns is SI: metres, m², m³. A model authored in millimetreswith areas in square metres (what Revit exports) and one authored in feet withareas 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 areal project is one nobody can check. What makes them useful is that the defectsare deliberate and enumerated: a wall with no material, a fire door with norating, a room below 8 m², a room with no area quantity at all, a door whoserating 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 primitive

Development

make test        # the suite
make fixtures    # regenerate fixtures (byte-identical; CI checks this)
make bench       # parse throughput on a fixture

License

MIT

MCP Server · Populars

MCP Server · New

    DROOdotFOO

    Raxol

    Write one app, render it to a terminal, a browser, or as agent tools. The terminal for your Gundam.

    Community DROOdotFOO
    morluto

    REA: Reverse Engineer Anything

    Reverse engineer anything with agents, from app behavior down to native binaries.

    Community morluto
    nedlir

    MCPwner

    Model Context Protocol server for autonomous vulnerability discovery

    Community nedlir
    codegraph-ai

    CodeGraph

    CodeGraph builds a semantic graph of your codebase — functions, classes, imports, call chains — and exposes it through 42 MCP tools, 38 languages, a VS Code extension, and a persistent memory layer. AI agents get structured code understanding instead of grepping through files.

    Community codegraph-ai
    getArbor-dev

    Arbor

    Graph-native code intelligence that replaces embedding-based RAG with deterministic program understanding.

    Community getArbor-dev