AI-native building automation. Connect Claude to your BACnet, Modbus, Haystack, MQTT, and SNMP systems — read trends, diagnose faults, read and write schedules, and keep production safe by default.
MCP4BAS is a Model Context Protocol server that gives AI assistants like Claude direct, structured access to building systems. Instead of copy-pasting values out of a BAS workstation, you ask Claude and it talks to the protocol layer directly.
| Why it matters | |
|---|---|
| Building data is scattered across tools and vendors | MCP4BAS unifies it into one conversation |
| Troubleshooting is slow when context is fragmented | Claude reads trends, properties, and schedules in one pass |
| Schedule and setpoint changes carry real risk | Every write goes through enforced safety gates |
| Protocol | Capability |
|---|---|
| BACnet/IP | Discovery, property read/write, trend logs, weekly/exception schedules, device inventory, IP adapter MAC lookup, foreign device (BBMD) registration, routed MS/TP access |
| Modbus TCP | Holding/input/coil register reads, guarded single-register writes |
| Haystack | Point discovery and metadata fetch with tag quality scoring |
| MQTT | Telemetry ingest/query, controlled publish with audit envelope |
| SNMP | Read-only get, walk, and device health summary |
| Tool | Description |
|---|---|
who_is |
Broadcast discovery and I-Am collection |
bacnet_list_discovered_devices |
List persisted discovery inventory |
bacnet_get_discovered_device |
Load one device record by key |
bacnet_tag_discovered_device |
Add operator tags and notes |
bacnet_refresh_discovered_device_identity |
Enrich with serial / model / firmware |
read_property |
Read any BACnet object property |
write_property |
Write a property at a BACnet priority (safety-gated) |
bacnet_get_trend |
Pull trend-log entries with adaptive ReadRange strategy |
bacnet_get_schedule |
Read weekly + exception schedules with human-readable output |
bacnet_set_weekly_schedule |
Write one day of a weekly schedule (value-type auto-inferred) |
bacnet_get_ip_adapter_mac |
Resolve MAC address for a BACnet/IP peer |
| Tool | Description |
|---|---|
modbus_read_registers |
Read holding, input, or coil registers |
modbus_write |
Write a single register or coil (safety-gated) |
| Tool | Description |
|---|---|
haystack_discover_points |
Discover and score tagged points |
haystack_get_point_metadata |
Return full metadata for a point |
| Tool | Description |
|---|---|
mqtt_ingest_message |
Ingest a raw MQTT payload |
mqtt_get_latest_points |
Query latest values across topics |
mqtt_publish_message |
Publish to an allowlisted topic |
| Tool | Description |
|---|---|
snmp_get |
Get a single OID value |
snmp_walk |
Walk a subtree |
snmp_device_health_summary |
Summarize device health from standard OIDs |
python -m venv .venv
.venv\Scripts\activate # Windows PowerShell
pip install -r dev-requirements.txt
python -m mcp4bas.server --transport stdioOn first run, if .env does not exist, MCP4BAS creates one from .env.example and attempts to auto-detect a usable BACNET_LOCAL_ADDRESS. It will never overwrite an existing .env.
Windows + Claude Desktop? See CLAUDE_DESKTOP_SETUP.md for a full beginner-friendly walkthrough including the Microsoft Store build path.
These capabilities have been tested against real hardware, not just mocked data:
| Capability | Device / notes |
|---|---|
| BACnet discovery | Trane Symbio 210 (device 50, 192.168.0.62) |
| Property read/write with priority array | Trane Symbio 210 |
Trend log retrieval (ReadRange) |
Trane Symbio 210 — adaptive small-count strategy handles 480-byte no-segmentation APDUs |
| Schedule read — weekly + exception | Routed MS/TP controller (device 242631, address 2001:10) |
| Schedule write — weekly day block | Same routed controller; value type auto-inferred from schedule-default |
| Routed BACnet (MS/TP behind router) | DNET:MAC address format (2001:10) |
| Foreign device (BBMD) registration | Env-var wired; cross-subnet BACnet/IP |
Schedule reads return human-readable times and unwrapped values — no BACnet encoding noise:
{
"status": "ok",
"target": "2001:10",
"weekly_schedule": [
{
"day": "monday",
"events": [
{ "time": "06:00:00", "value": 1 },
{ "time": "18:00:00", "value": 0 }
]
}
],
"effective_period": { "start": "any", "end": "any" }
}The connector reads schedule-default from the device to determine whether values should be encoded as enumerated, boolean, real, or another type. You pass 0 or 1 — the right encoding is automatic.
Default state is read-only. Nothing writes until you explicitly enable it.
BAS_OPERATION_MODE=read-only # enforced globally
BACNET_WRITE_ENABLED=false # secondary per-protocol gate
BAS_DRY_RUN=true # validate + audit without sending| Gate | What it does |
|---|---|
BAS_OPERATION_MODE |
Global read-only / write-enabled switch |
BACNET_WRITE_ENABLED |
Per-protocol secondary gate |
BAS_DRY_RUN |
Validate, build payload, audit — without executing |
BACNET_WRITE_ALLOWLIST |
Allowlist of object_id:property pairs that may be written |
| Priority bounds | Writes validated to BACnet priority 1–16 |
Every write response includes a full audit envelope: mode, dry_run, allowed, reason, target, request, timestamp.
Do not write when:
- Device identity is uncertain or conflicts with expected site/model context
- The point is not in the approved allowlist
- Operations staff have not validated the current mode
- Dry-run output has not been reviewed
- The change is not recorded in change-control notes
Discovered devices are automatically persisted to a local JSON file.
- Override path:
BACNET_DISCOVERY_INVENTORY_PATH - Disable:
BACNET_DISCOVERY_INVENTORY_ENABLED=false - Supports operator tags, notes, and enriched identity fields (serial / model / firmware) from
bacnet_refresh_discovered_device_identity
If your PC is on a different IP subnet than the BACnet network, set the BBMD address:
BACNET_FOREIGN_BBMD=10.0.1.1 # IP or IP:port of the BBMD
BACNET_FOREIGN_TTL=30 # registration lifetime in secondsLeave BACNET_FOREIGN_BBMD blank for same-subnet operation. This is separate from routed MS/TP — field bus devices accessed via DNET:MAC addresses do not require BBMD registration.
If a controller does not reliably serve ReadRange, collect local trend samples by polling:
mcp4bas-historian \
--target-address 192.168.0.62 \
--object-id analog-input,1 \
--property present-value \
--interval-seconds 60 \
--output trend_samples.csv--samples N and --duration-seconds S stop conditions available. CSV includes timestamp, value, numeric value, and per-sample error text.
Discover → Inventory → Identify → Diagnose → Write (change window only)
- Discover —
who_isto find reachable devices; confirm sources before reading - Inventory — work from persisted records; add tags/notes for human context
- Identify — refresh serial/model/firmware; treat enrichment as best-effort
- Diagnose — read properties, trends, schedules; keep prompts specific
- Write — enable intentionally, dry-run first, allowlist required, review audit
Run who_is, summarize discovered device instances and sources, then stop.
List persisted devices, pick the record for AHU-1, and return its device_key.
Read present-value for these object IDs on device X and return a table with timestamps.
Read the weekly schedule for schedule,1 at address 2001:10 and show which days are programmed.
Set Monday on schedule,1 at 2001:10 to occupied 06:00–18:00, dry-run first, then execute if approved.
Before any write, confirm mode and allowlist, run dry-run only — do not execute a live write.
If identity is ambiguous, allowlist is missing, or dry-run is unapproved, block the write and explain why.
| Check | Status |
|---|---|
Test suite (pytest) |
✅ 100 passing |
Type checking (mypy) |
✅ Clean (18 source files) |
Linting (ruff) |
✅ Clean |
| CI workflow | ✅ Passing |
| Live BACnet reads | ✅ Validated on real hardware |
| Live BACnet writes | ✅ Validated (priority array, schedule day blocks) |
| Haystack / MQTT | ✅ Integration-tested with local datasets |
| SNMP | ✅ Unit and integration tested |
If local environment artifacts were committed previously, clean them up once:
git rm -r --cached .venv .pytest_cache .mypy_cache .ruff_cache
git rm --cached .env bacnet_discovery_inventory.json
git commit -m "chore: stop tracking local environment artifacts".env.example stays tracked as the template. Discovery inventory files are .gitignored.
MQTT — Quality depends on consistent topic and payload conventions. MCP4BAS validates completeness and surfaces confidence/caveat metadata when context is weak.
Haystack / Niagara — Workflows depend on tagging quality. Missing or inconsistent tags reduce confidence and are surfaced in tool responses with remediation hints.
- Expand live rollout coverage for Haystack and MQTT environments
- Extend SNMP validation against live devices
- Continue publish-batch hardening for public release cadence
- ⭐ Star the project to follow progress
- 💡 Open an issue with ideas or use cases
- 🔧 Contribute with PRs as the roadmap grows