Skip to content

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 systemA 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 agentAny 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 themA 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 tools10
Write tools1, off unless two arguments are passed
Tests162
Evidence file819 lines, class and method cited per finding
Dependencies0 — Python standard library
LicenceMIT

The stack

What it is made of

ProtocoloBIX 1.1 and its REST binding, over the station's HTTP servlet
Agent interfaceModel Context Protocol over JSON-RPC 2.0, stdio transport
LanguagePython, standard library only, no dependencies
TargetNiagara 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.