A Niagara 4 custom module that exposes read-only station data through the Model Context Protocol (MCP).
MCP-compatible AI clients (Claude Desktop, VS Code Copilot, etc.) can discover and call tools that introspect the station safely and without mutation.
Test status: successfully validated end-to-end with Claude (via MCP proxy).
- β Live Niagara station integration verified
- β Claude MCP workflow verified
- β Secure read-only tool surface
- β SCRAM auth handled by local proxy
The correct v0.1 is not the most powerful version. The correct v0.1 is the safest useful version.
v0.1 is read-only. No point writes, overrides, alarm acknowledgements, schedule edits, or station configuration changes are implemented or planned for this release.
Niagara Station
βββ BMcpService (BAbstractService)
βββ McpHttpServlet β HTTP POST /mcp
βββ McpJsonRpcHandler β JSON-RPC 2.0 dispatcher
βββ McpToolRegistry β tool name β handler map
βββ NiagaraSecurity β allowlist & sensitive-slot enforcement
βββ NiagaraComponentTools β station.info, component.read/children/slots
βββ NiagaraBqlTools β bql.query (SELECT only)
βββ NiagaraAlarmTools β alarm.query
βββ NiagaraHistoryTools β history.list
βββ NiagaraBacnetTools β bacnet.devices
βββ NiagaraJson β zero-dependency JSON parser/builder
niagaraMCP/
βββ build.gradle Gradle build (stubs + main + test source sets)
βββ module.xml Niagara module descriptor
βββ settings.gradle
βββ README.md
βββ src/
βββ stubs/java/ Niagara API stubs (compile-time only; replaced by SDK at runtime)
β βββ javax/baja/sys/
β βββ javax/baja/web/
βββ main/java/com/makeitworkok/niagaramcp/
β βββ BMcpService.java
β βββ McpHttpServlet.java
β βββ McpJsonRpcHandler.java
β βββ McpToolRegistry.java
β βββ McpTool.java
β βββ McpToolResult.java
β βββ McpErrors.java
β βββ NiagaraSecurity.java
β βββ NiagaraComponentTools.java
β βββ NiagaraBqlTools.java
β βββ NiagaraAlarmTools.java
β βββ NiagaraHistoryTools.java
β βββ NiagaraBacnetTools.java
β βββ NiagaraJson.java
βββ test/java/com/makeitworkok/niagaramcp/
βββ McpJsonRpcHandlerTest.java
βββ McpToolRegistryTest.java
βββ NiagaraSecurityTest.java
βββ NiagaraJsonTest.java
The project compiles against Niagara API stubs so it can be built without a Niagara SDK installation. You need Java 11+ and Gradle.
# Compile only
gradle compileJava
# Run unit tests (59 tests, no Niagara runtime required)
gradle test
# Build JAR
gradle jarReplace compileOnly project(':stubs') in build.gradle with the actual Niagara
JAR paths:
compileOnly fileTree(dir: System.getenv('NIAGARA_HOME') + '/lib', include: '*.jar')- Build the module (
gradle jar) to producebuild/libs/niagaraMcp-0.1.0.jar. - Package it as a Niagara module (
.nmodule) following the Tridium module-packaging guide for your Niagara version. - Install the module in the station using Niagara Workbench.
- Navigate to Config β Services in the station tree.
- Drag BMcpService from the
niagaraMcppalette entry into Services. - Start the service. The MCP endpoint is now live at
http://<station-host>:<port>/mcp.
The current implementation has been validated on a live Niagara station and returns working MCP JSON-RPC responses.
Niagara 4.15 uses SCRAM-based web auth, so the easiest client integration path is to run the included local HTTP proxy.
- Ensure BMcpService is running in the station.
- Start the proxy:
python mcp_proxy.py \
--niagara-url http://localhost \
--niagara-user mcp \
--niagara-pass <your-password> \
--token <your-client-token> \
--port 8765Notes:
--backend-mcp-tokenis optional. If omitted, the proxy auto-discovers it fromGET /mcpafter login.- By default, the proxy listens on
127.0.0.1:8765.
Configure Claude Desktop (or any HTTP MCP client) to call:
- URL:
http://127.0.0.1:8765/mcp - Header:
X-MCP-Token: <your-client-token>
Quick verification call:
curl -X POST http://127.0.0.1:8765/mcp \
-H "X-MCP-Token: <your-client-token>" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list",
"params": {}
}'| Property | Default | Description |
|---|---|---|
enabled |
true |
Enable / disable the MCP endpoint |
endpointPath |
/mcp |
HTTP path |
readOnly |
true |
Must remain true for v0.1 |
allowBql |
true |
Permit BQL SELECT queries |
maxResults |
500 |
Maximum rows / items per call |
allowlistedRoots |
see below | Comma-separated ORD prefixes |
Default allowlisted roots:
station:|slot:/Driversstation:|slot:/Servicesstation:|slot:/Config
Any ORD that does not start with one of these roots is immediately rejected.
Returns basic station metadata.
{}Response:
{
"stationName": "MySupervisor",
"hostId": "host-abc123",
"niagaraVersion": "4.12",
"moduleVersion": "0.1.0",
"readOnly": true
}Reads a component by ORD.
{ "ord": "station:|slot:/Drivers" }Lists immediate children of an ORD.
{ "ord": "station:|slot:/Drivers", "limit": 100 }Lists all slots on a component. Sensitive slots (password, token, key, β¦)
are masked as ***.
{ "ord": "station:|slot:/Drivers/BacnetNetwork" }Runs a read-only BQL SELECT query. Mutation keywords are rejected.
{ "query": "SELECT * FROM control:NumericPoint", "limit": 100 }Returns recent alarm records.
{ "limit": 100 }Lists available history identifiers.
{ "limit": 100 }Lists BACnet devices under a network ORD.
{ "networkOrd": "station:|slot:/Drivers/BacnetNetwork", "limit": 100 }- Read-only by default β write operations are explicitly absent from v0.1.
- Allowlisted roots β every ORD is checked against configured prefixes.
- Sensitive-slot masking β slots whose name contains
password,secret,token,key,credential,auth, etc. return***instead of the real value. - BQL SELECT-only β mutation keywords (
SET,DELETE,INSERT,UPDATE, β¦) cause immediate rejection. - Result caps β all queries honour
maxResults. - Fail-closed β if a security check is uncertain, access is denied.
Resources are exposed with the URI scheme niagara://station/slot/<path>.
niagara://station/slot/Drivers β station:|slot:/Drivers
niagara://station/slot/Services β station:|slot:/Services
niagara://station/slot/Config β station:|slot:/Config
resources/list returns the allowlisted root ORDs.
resources/read delegates to niagara.component.read.
# List available tools
curl -X POST http://127.0.0.1:8765/mcp \
-H "X-MCP-Token: <your-client-token>" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list",
"params": {}
}'
# Get station info
curl -X POST http://127.0.0.1:8765/mcp \
-H "X-MCP-Token: <your-client-token>" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "niagara.station.info",
"arguments": {}
}
}'
# List children under /Drivers
curl -X POST http://127.0.0.1:8765/mcp \
-H "X-MCP-Token: <your-client-token>" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "niagara.component.children",
"arguments": {
"ord": "station:|slot:/Drivers",
"limit": 20
}
}
}'
# Run a BQL SELECT query
curl -X POST http://127.0.0.1:8765/mcp \
-H "X-MCP-Token: <your-client-token>" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 4,
"method": "tools/call",
"params": {
"name": "niagara.bql.query",
"arguments": {
"query": "SELECT * FROM control:NumericPoint",
"limit": 50
}
}
}'| Phase | Status | Scope |
|---|---|---|
| 1 β Read-Only Core | β Done | Module loads, service starts/stops, HTTP endpoint, JSON-RPC, tools/list, station.info, component.children |
| 2 β Component Introspection | β Done | component.read, component.slots, slot masking, ORD validation, allowlist enforcement |
| 3 β BQL | β Done | bql.query, SELECT-only validation, result limiting |
| 4 β Niagara Subsystems | β Done | alarm.query, history.list, bacnet.devices |
| 5 β MCP Client Compatibility | β Done | Validated end-to-end with Claude via local auth proxy |
| 6 β Write Support | β Out of scope | Not in v0.1 |
To keep the repository professional and reproducible, follow these guidelines.
Commit these:
- Source code under
src/ - Gradle wrapper files and build scripts
- Module metadata (
module.xml,module.palette) - Tests and documentation
- Public certificate material only (for example
.cer), if needed
Do not commit these:
- Local secrets (
.p12, keystores, private keys, passwords, tokens) - Runtime/debug artifacts (
*.log,cookies.txt, ad-hoc payload files) - Temporary inspection/research files (
tmpinspect/) - Build outputs (
build/,.gradle/, generated binaries) - Local tool settings (
.claude/, IDE-local state)
Working rule:
- If a file is machine-local, environment-specific, or sensitive, keep it out of git.
Before pushing:
- Run
git statusand check for unexpected files. - Confirm no secrets appear in staged changes.
- Keep commits focused by purpose (code, docs, cleanup, etc.).
The following will not be implemented in v0.1:
niagara.point.writeniagara.point.overrideniagara.component.invokeActionniagara.alarm.ackniagara.schedule.writeniagara.component.create/.deleteniagara.station.restartniagara.driver.discoverAndAdd