Post 13 put an agent in the loop: it writes the QA, runs flua, reads
the PASS/FAIL lines, iterates. That loop needed a shell and the commands.
flua 0.1.45 removes even that: flua-mcp ships in the same
fibaro-flua wheel, so any MCP host — Claude Desktop,
Copilot, Cursor — can call flua directly as a tool. No repo checkout, no
shell commands, no parsing: the agent asks check this or run
this and gets JSON back.
Upgrade flua, then register the server in your host's config. That is the whole install — the server is already on disk:
pip install -U fibaro-flua
Claude Desktop (claude_desktop_config.json):
{"mcpServers": {"flua": {"command": "flua-mcp"}}}
Copilot and other .mcp.json hosts (uses the interpreter
flua runs on):
{"mcpServers": {"flua": {"command": "python", "args": ["-m", "flua.mcp.server"]}}}
stdio is perfect for hosts on your laptop. Some hosts cannot spawn a local process — a cloud agent, a browser-based client, or one flua-mcp shared by several machines. For those, the same server speaks Streamable HTTP: one endpoint, the same tools, no session headers, no SSE. Opt-in, and safe by default:
flua-mcp --http # 127.0.0.1:8091, loopback only FLUA_MCP_TOKEN=secret \ flua-mcp --http 0.0.0.0:8091 # LAN: every request needs the token
The server refuses non-loopback binds without a token — the guard is built in, not documented. Register it as a remote server:
{"mcpServers": {"flua": {"url": "http://127.0.0.1:8091/mcp"}}}
That is the piece that lets a cloud agent drive flua through a tunnel
while the HC3 stays behind your router — and mode: "remote"
is still explicit, so nothing reaches the controller uninvited.
Take the self-asserting QA from post 6, now with flua's built-in test library (the same file still runs on a real HC3 — there the assertions become no-ops):
--%%name:hello_mcp
--%%file:$flua-test.lua,flua_test
local Test = require("flua-test")
local t = Test.new()
function QuickApp:onInit()
t:expect(api.get("/devices/" .. self.id).id == self.id, "device registered")
self:updateProperty("value", true)
t:assert_eq(self.properties.value, true, "value flips on")
setTimeout(function()
self:updateProperty("value", false)
t:assert_eq(self.properties.value, false, "lamp off after 10 virtual minutes")
t:finish()
end, 10 * 60 * 1000)
end
The agent calls qa_check for the static layer (syntax,
unknown --%% directives, deprecated HC3 APIs) and then
qa_run — offline, instant so the ten virtual
minutes collapse to nothing. What it gets back is the answer, not log
prose:
{"exitCode": 0,
"summary": {"levels": {"INFO": 1, "DEBUG": 4}, "pass": 3, "fail": 0, "failures": 0},
"logs": [...], "verdict": []}
exitCode and summary.failures are the verdict;
summary.pass/fail count the assertions; logs are
structured {time, level, tag, qaid, msg} lines. When the agent
only wants the failures it filters server-side —
{"filter": {"levels": ["ERROR"]}} — instead of shelling out
to jq. qa_ui returns a QA's controls as structured data the
same way.
Some bugs need a running QA, not a finished run. The session tier does
that: qa_start boots an engine and returns a
sessionId, qa_probe reads devices, QA status and
only the log lines since the last probe, qa_action
dispatches a device action into the running QA, and
qa_stop/qa_sessions manage it. Sessions idle out
on their own and die with the server — an agent cannot leak engines.
The agent also gets flua's documentation without a repo checkout:
resources/read flua://docs/USAGE.md serves the whole docs
tree that ships in the wheel — the tutorial chapters, the architecture
guide, the HC3 API reference. And two prompts seed the thinking:
qa_debug turns a pasted error into the debugging workflow
(static layer → offline reproduction → reading the error shape →
shrinking → the pinned contracts), and qa_skeleton emits a
runnable self-testing template with the deferred-onInit pattern — the demo
above follows it. Both are pinned by the test suite, so the guidance never
drifts from what flua actually does.
Every run is offline unless mode: "remote" is explicit —
the worst the agent can do is fail an assertion. Controller credentials
never travel in tool arguments; remote runs use the server's environment,
the same .env chain flua itself uses. Runs are bounded by a
timeout, and a bad request is a clean JSON-RPC error, never a dead server.
The protocol is current (2026-07-28) with the legacy handshake still
served, so old and new hosts both work.
Post 13's loop, industrialized: the agent asks for the work as typed tool calls, proves it against the simulated house, and you review the diff — the controller still only hears from you.
Next time: proxy mode — flua mirrors a QA onto your real HC3, so it runs offline but behaves as a device on the controller. The deepest offline-online blend yet.
flua-mcp: docs/mcp.md · pip: fibaro-flua 0.1.45 · flua: https://github.com/jangabrielsson/flua