Meet flua-mcp: flua as a tool your agent can call

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.

One line of setup

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"]}}}

No shell at all? The HTTP transport

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.

The loop, as tool calls

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.

Live probing, without keeping a shell alive

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.

Knowledge rides along

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.

Guardrails are defaults

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