Inspiration

Alexa+ can hold a conversation about your house. The moment it can also change something in your house, one question stops being academic: what exactly did I just agree to?

HomeOps Guardian is a Model Context Protocol server for household electricity. It reads circuit telemetry, finds load worth shifting out of a peak tariff window, and stages a plan. It will not touch a breaker until a person says yes — and the plan it shows you is the plan it is about to run, which turned out to be the hard part.

The honest part first: it does not run on an Echo

Nothing can, yet.

Amazon’s own Alexa+ add-on console — the portal’s "Create and manage your add-on" link — resolves to developer.amazon.com/alexa/console/ask/addons#/, and that page renders "Coming Soon". Checked first-hand, signed in, on 2026-09-14. On the official build session Amazon’s developer-relations team said it plainly: "right now you cannot take an SDK and control an Echo Show."

So this submission is the server, built to the plain MCP Streamable HTTP standard at spec version 2025-11-25, plus a web client standing in for Alexa+. The hosts explicitly endorsed demonstrating the track that way — it is what their own internal teams built. We would rather lead with this than have a judge discover it.

The stand-in for Alexa+ is a Bedrock agent, and it cannot approve its own plan

A typed sentence goes to an Amazon Bedrock Nova Pro agent (Converse API with tool use) that can only call the tools this MCP server lists. It reads the circuits and stages a plan; staging changes nothing. When it calls confirm_load_shift, the server asks the human through MCP elicitation and shows the exact staged plan. The model cannot answer for you: make the server trust the model’s own confirmed flag instead of the human’s answer, and 7 of 8 gate tests fail. Approve, and the final line comes from the server’s result — in the demo, "Done. 2 circuits changed; delivered 9.6 kW of the 9.6 kW promised." Decline, and it says nothing changed.

What it does

Four tools over one /mcp endpoint:

  • ping — health and server time.
  • get_circuit_telemetry — per-circuit draw, status, hourly cost, and the modelled time-of-use tariff. Every payload carries a dataSource block: liveRateFeed: false, liveMeter: false, the modelling basis, a reference URL, and an instruction not to present the figures to anyone as their bill.
  • stage_load_shift — returns a plan, not an action: PENDING_CONFIRMATION, a proposedActions array with each circuit, verb and kilowatt reduction, a projected total, and a savingsBasis string spelling out the arithmetic behind the money figure.
  • confirm_load_shift — only this one changes anything, and not on the caller’s word. The server asks the human through MCP elicitation, showing the staged plan, and executes only if the person accepts. The caller’s own confirmed: true is not enough, and a client that cannot ask a human fails closed. It reports back executedActions with each circuit’s draw before and after.

The protocol floor was false. Twice.

The rules set a minimum spec version, so we measured it instead of asserting it. ops/probe-protocol-version.mjs asks as six different clients and prints what the server actually negotiates.

First failure. A client asking for 2024-11-05 was answered 200 / 2024-11-05. The SDK negotiates down to whatever the client requests, so our README claimed a floor the server did not hold. Fixed by raising anything below the floor before the transport sees it; six tests pin one version each.

Second failure, a different shape. Two of our own ops/ scripts started the server with node dist/index.js, while npm start runs tsx src/index.ts. dist/ is gitignored, so it only changes when someone remembers to build — and after the floor fix it still held the old emit. The probe run against the script reported the pre-fix behaviour while every test stayed green, because the tests import the source. A judge following our own walkthrough would have been talking to a server this repository no longer contained.

Both scripts now run from source, and tests/ops-entrypoints.test.ts reads every .cmd in ops/ and fails the build if one launches node dist/. It found a second offender by itself: verify-live.cmd, whose entire job is verifying the live transport. It had been verifying a stale binary and reporting success.

The confirmation gate used to describe a plan it was not going to run

This is the defect worth reading about, because the gate is the whole product.

The card that asks you to approve a change had the plan typed into its HTML — two named appliances and their kilowatts — while the server’s response carries a proposedActions array. The totals fell back to || 9.6 and : '42.50', and the spoken confirmation invented "PAUSED until 9:00 PM" and "$4.61/hr" the same way.

The server had the same split: stageAction returned a hardcoded plan while executeAction separately hardcoded ev.powerKw = 0 and hvac.powerKw = 1.4. Two constants typed against the same default household, so they agreed — and our own verification script printed "promised 9.6 kW, delivered 9.60 kW" as proof of agreement between two numbers neither of which was derived from anything.

It all looked correct because the literals happened to match today’s server. Consent is to the plan as described, so a gate whose description and effect are independent constants is a gate in name only. Our own walkthrough said so, in writing, while the code did the opposite.

What replaced it:

  • One SHIFT_TARGETS definition. Staging derives each reduction from the circuit’s current draw; execution walks the staged plan and records executedActions with before/after per circuit; the delivered reduction is summed from those.
  • One TARIFF constant feeding both the telemetry payload and the savings model, so a rate cannot be stated in one place and assumed in another.
  • The $42.50 monthly saving was a constant with no stated basis. It is now derived — reduction × 5 peak hours × 21.7 weekdays × the $0.14 peak-to-off-peak differential — and the payload carries the savingsBasis string that says so. The figure is now $145.82, which is the first version of it that means anything.
  • The client renders every figure from the payload and says "not reported by the server" where a field is absent, never a plausible substitute.

Two more inventions fell out of the guard test on its own: an off-peak rate of "$0.34/kWh (29% cheaper)" that no tool result contained, and "your EV charger and HVAC make up 80%+ of this load". Both now come from the payload — the two actual largest circuits and their real 82%.

The test that would have caught it

tests/gate-invariants.test.ts measures relationships rather than values, so it keeps holding when the household changes:

  • the staged total equals the sum of the staged items
  • execution itemises exactly the circuits the plan named
  • promised equals delivered, computed from opposite directions
  • the delivered figure equals the drop in the household total
  • each item’s own before-minus-after equals its claimed reduction
  • cancelling reports no executed actions and moves nothing

And the one that matters most: stage a second plan after the shift has run and it must propose less, because the charger is already paused. A hardcoded plan would cheerfully offer the same 9.6 kW again. We proved the test has teeth by pinning projectedReductionKw = 9.6, watching it fail with Expected: < 9.6, Received: 9.6, and reverting.

How we built it

services/mcp-server — TypeScript, Express, @modelcontextprotocol/sdk. One /mcp endpoint handling POST, GET and DELETE, with MCP-Session-Id issuance and validation, Origin checking for the spec’s DNS-rebinding guard, and the 406/400/404/403/204 negative paths each asserted by a test. 48 tests across 5 suites.

services/agent — the stand-in for Alexa+: an Amazon Bedrock Converse tool-use loop on amazon.nova-pro-v1:0, capped at 8 steps. Its tools are exactly what the MCP server lists, so the model can only act through the server. After a confirmation, its final sentence is written from the server’s result (circuits changed, kilowatts delivered against promised), never from the model’s own words.

apps/simulator — a Vite web client that shows the conversation, the agent’s trace with each Bedrock request id, every MCP JSON-RPC frame, and the approval card the server’s elicitation asks for. It replaced a keyword router on 2026-09-23.

ops/verify-gate.mjs — drives the live server over MCP without a browser and proves the gate by readings rather than screenshots: staging leaves every circuit untouched (13.4 kW, ev_charger CHARGING), approval moves them (3.8 kW, PAUSED), and the 9.6 kW promised equals the 9.60 kW delivered.

What is real and what is modelled

Real and tested The Bedrock Nova Pro agent, the MCP elicitation gate, the MCP server, the Streamable HTTP transport and its session handling, the JSON-RPC handshake, the tool schemas, the protocol floor, the confirmation gate’s logic and state transitions, and the client’s HTTP conversation with the server.
Simulated The household. Circuits, power draws, power factors, hourly costs and the time-of-use tariff are modelled in software. There is no smart panel, no CT clamp, no meter, no utility feed. Every payload carrying a number carries a dataSource block that says so.
Not possible for anyone yet Deploying to an Echo device. See the top of this page.

We shipped a version where the tariff did not say so, and it read as a live rate quote from a named utility beside a live timestamp — which an assistant would then have spoken to someone as their electricity bill. Finding and fixing that is friction-log entry 5, and it is the most useful thing we learned building this.

Accomplishments we’re proud of

  • A confirmation gate that derives what it promises and reports what it delivered, with the two computed from opposite directions so a disagreement would be visible.
  • 57 server tests in 6 suites plus 9 agent tests, up from 22 — including two suites that read our own ops/ scripts and our own client source to stop retired defects returning.
  • A protocol claim that is measured on camera from live stdout, not asserted in a README.
  • Every figure in the product traceable to a payload, and a UI that says "not reported by the server" rather than filling a gap.

What we learned

Two constants that happen to match are not a match. Every serious defect in this project — the negotiated version, the gate’s plan, the savings figure, the off-peak rate, the 80% claim — was a value that looked verified because something else nearby agreed with it by coincidence. The rule we came out with: if a number reaches a user, something must compute it from the thing it describes, and a test must fail when it stops.

What’s next for HomeOps Guardian

  • An actual Alexa+ add-on, the day add-ons stop rendering "Coming Soon".
  • Real telemetry from a smart panel or CT clamps, so the dataSource block can start saying liveMeter: true truthfully.
  • A live tariff feed, so the savings figure stops being modelled.
  • Multi-household and scheduling, where the interesting safety question becomes consent over time rather than consent per action.

Code (MIT): https://github.com/AtchayamG/homeops-guardian · Narration in the demo is synthesized with Microsoft Edge Neural TTS, and the closing card says so.

Built With

Share this project:

Updates

Submission history