Case study · Agents and control systems
An agent could read the building. Writing to it was the hard part.
A language model can be given a control system to read without much ceremony. Writing is a different question: a station has no undo, a mistyped set point is a real room full of real people, and the protocol will accept a bad write as cheerfully as a good one. The interesting engineering was never the model. It was the fence.
- oBIX
- Model Context Protocol
- Niagara 4
- Read-only by default
- 162 tests
- MIT
The systems involved
What had to talk to what
| The control system | A Niagara 4 station: field devices on BACnet and Modbus normalised into one point tree, with histories, alarms and writable set points, served over the station's own oBIX servlet. |
|---|---|
| The agent | Any Model Context Protocol client. The tool list it is handed is the entire interface it gets — it cannot discover anything that is not in that list. |
| The bridge between them | A server in standard-library Python: no dependencies, MIT licensed, and small enough that the whole trust argument can be read in an afternoon. |
The difficulty
What made them hard to connect
A control system has no transaction log and no undo. Every other integration problem on this page is recoverable; this one ends with plant in a state nobody asked for, and the person who finds out is an occupant rather than an engineer.
The station and its documentation disagree about the protocol. The export branch is mounted as continuousControl rather than export — that string is the literal return value of BExportLobbyAgent.getLobbyName() in the shipped jar — so a client written from the documentation walks to an address that is not there. HTTP Basic is not enabled on a station by default either, which turns the first 401 into an hour of looking in the wrong place.
An agent that is told a capability exists will plan around it. So “read-only by default” had to be a mechanism rather than a sentence in a readme: four independent ones, each of which has to be defeated before a single value moves.
The work
What we built
Read, discoverable; write, deliberate
10 read tools — the lobby, points, folders, histories, open alarms and live values through server-side watches — and one write tool that is absent from the tool list unless the bridge was started with writes enabled and a named subtree to write to. An argument error, not a permissive default, is what you get for enabling writes without naming a prefix.
Protocol facts read out of the jars
Every protocol decision was decoded from one Niagara 4.15 install with javap and written into an evidence file of 819 lines that cites the class and method behind each one — and says plainly where the install answers nothing. Documentation was the thing being checked, not the source.
A test per claim
162 tests, which is what it takes to assert a negative: that the write tool is not advertised, that dispatch refuses it by name, that an href outside the allowlist is rejected even once writes are on.
Now
What it does today
An engineer can point an agent at a station and ask what is alarming, what a point has done for the last day, or which devices stopped reporting — and get an answer traced to ORDs rather than a guess. Turning that into write access takes two deliberate arguments and a named subtree, and the tool's own description says so, because the common assumption is the other way round.
| Read tools | 10 |
|---|---|
| Write tools | 1, off unless two arguments are passed |
| Tests | 162 |
| Evidence file | 819 lines, class and method cited per finding |
| Dependencies | 0 — Python standard library |
| Licence | MIT |
The stack
What it is made of
| Protocol | oBIX 1.1 and its REST binding, over the station's HTTP servlet |
|---|---|
| Agent interface | Model Context Protocol over JSON-RPC 2.0, stdio transport |
| Language | Python, standard library only, no dependencies |
| Target | Niagara 4 station, AX-era export agent included |
Who did what
All of it ours, agent-assisted, and the repository's commit history says so rather than this page. The protocol facts belong to the framework, not to us: they were read out of a vendor's shipped jars, which is a measurement, not a relationship. Nothing here was built with, for, or at the request of the framework's vendor.
Check it
The evidence, not a description of it
Every link here opens the thing itself — a repository, a running demo, a note with the method in it.
Next step
Tell us the version, the hardware, and what it has to do.
You will get a written scope and a fixed price against it. If the honest answer is that you do not need us, you will get that instead.