alexa-plus
Integrates with Amazon Bedrock (and the Alexa+ smart-home ecosystem): an optional Bedrock planner drives the tool-use loop via Bedrock's Converse API (default model amazon.nova-micro-v1:0), letting a real model decide which smart-home tools to call and in what order. Requests are proxied through the client's own Node process with BedrockRuntimeClient so AWS credentials are never exposed to browser code, and every model-chosen action still passes through the human Confirm/Decline gate before it can execute.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@alexa-plusPropose dimming the living room light to 50% and wait for my approval."
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
alexa-plus: Smart Home Agent
Alexa+ MCP server that exposes smart home capabilities through a unified interface with human-in-the-loop approval gates for all device actions, plus a simulated Alexa+ web client that talks to it over the wire.
The server (entries/alexa-plus/server/) exposes all eight tools from SPEC.md
section 3 over MCP's Streamable HTTP transport (spec revision 2025-11-25), backed by a
seeded 5-device registry (server/data/devices.json):
Read-only:
list_devices,get_device_state,check_automation_policy,read_audit_log.Propose/confirm/execute pairs:
propose_action/execute_actionandcompose_scene/execute_scene. Proposing never changes state. Executing requires a one-timeconfirmation_id— a token minted ONLY by a person approving the proposal through the two owner-only REST routes below (never through an MCP tool call, and never by the agent itself).execute_action/execute_scenerefuse — with a narrated reason, never a silent no-op — a call with no token, a token that matches no pending proposal, an already-used token, or a token approved for a different action.Owner-only REST routes (not MCP tools):
POST /proposals/:id/approvemints the token;POST /proposals/:id/rejectdeclines without ever minting one;GET /proposals/:idreads a proposal's current status. These stand in for SPEC.md's owner-only CLI verbs — this entry has a web client instead of a CLI, so the client's Confirm/Decline buttons call these routes directly.
The client (entries/alexa-plus/client/) is a separate project — its own
package.json, its own npm install/npm start/npm test — that imports nothing from
server/ and reaches it only over HTTP. It renders an Alexa+-style conversation and
walks the demo script from SPEC.md section 5 with a scripted planner (no LLM
required): every tool call it makes is a live fetch() to the running server, and a
proposed action pauses the conversation until you click Confirm or Decline right there
in the page — the demo runs one confirmed action (the living room light) and one
declined action (the kitchen plug), matching SPEC.md section 5's issue #36 amendment.
Setup
npm installRelated MCP server: homekit-mcp
Run
npm startStarts the MCP server on http://127.0.0.1:3000/mcp (override the port with PORT=<n>).
The endpoint accepts POST, GET, and DELETE per the Streamable HTTP transport spec.
Test
npm testRuns the conformance suite in server/test/: initialize handshake, tools/list,
tools/call (including the unknown-device and unknown-tool error paths, and a
no-mutation check against the seeded registry), session lifecycle (session id
issuance, reuse, DELETE termination, and the 400-vs-404 distinction between a missing
and a terminated/unknown session id), the propose→approve/reject→execute lifecycle for
both single actions and scenes, and — the DoD this goal is graded on — every way an
execute_action/execute_scene call can be refused (no confirmation_id, a token that
matches no pending proposal, a proposal that was never approved, a replayed
already-used token, a token approved for a different action, and the owner-only
approve/reject routes themselves refusing an unknown or already-decided proposal).
Lint
npm run lintVerify
bash verify.shSimulated Alexa+ client
In a second terminal, with the server already running from the steps above:
cd client && npm install && npm startThen open http://127.0.0.1:5173 in a browser (override the client's own port with
PORT=<n>; it defaults to talking to the server at http://127.0.0.1:3000/mcp, editable
in the page). Click "Start demo conversation" to run the SPEC.md section 5 script. It
will pause twice with a "Confirmation needed" panel — click Confirm the first time
(dimming the living room light — this one actually executes and its new state shows up
in the tool panel below) and Decline the second time (turning off the kitchen plug
— the agent narrates that it left it alone, and the plug's state never changes).
client/ still has no bundler or build step — React loads from a CDN import map in
index.html. It does have one real npm dependency now, @aws-sdk/client-bedrock-runtime
(#122, see "PLANNER=bedrock" below), so npm install there does real work; "one command
each" (npm install && npm start) still works identically for server and client.
To run the client's own integration test (spawns the real server as a separate process and drives the full script against it over HTTP, including a scripted confirm and a scripted decline):
cd client && npm testPLANNER=bedrock — the Bedrock tool-use-loop planner (#122)
The scripted planner above is the default and needs no LLM. An alternative planner lets a real model (Amazon Bedrock's Converse API, tool-use loop) choose which tools to call and in what order, instead of walking a fixed script — set it when starting the client:
PLANNER=bedrock AWS_REGION=ap-southeast-2 BEDROCK_MODEL_ID=amazon.nova-micro-v1:0 npm startAll three env vars are optional (defaults shown above — AWS_REGION defaults to
ap-southeast-2, BEDROCK_MODEL_ID to amazon.nova-micro-v1:0, the cheapest Bedrock
model confirmed to support Converse tool-use as of 2026-09-10 — see SPEC.md section 10).
client/server.js reads all three once at startup, but only PLANNER and
BEDROCK_MODEL_ID get templated into window.__ALEXA_PLUS_CONFIG__ in the served HTML
(app.js reads that global and dynamically imports client/src/bedrock-planner.js only
when planner: 'bedrock'). AWS_REGION is deliberately not templated (#130) — it only
matters to the real BedrockRuntimeClient server.js constructs for itself, and the
browser no longer constructs one at all.
What does not change: no AWS credentials are ever read, embedded, or reachable by
browser-served code. The browser never constructs an AWS SDK client at all — it POSTs
the Converse request to this same origin's POST /bedrock/converse (client/server.js),
which is the one place BedrockRuntimeClient is actually constructed, under Node, where
the AWS SDK's standard credential chain (environment variables, ~/.aws/credentials,
IMDS) can resolve safely. Every tool call the model makes still goes through the exact
same mcpClient.callTool() chokepoint the scripted planner and the UI use, and every
proposed action still has to pass through the exact same human Confirm/Decline gate to
get a confirmation_id — the model can reason about anything, but it can never mint its
own token, redirect an execution to a different device/action than what was actually
proposed and confirmed, replay a token, or clobber one pending proposal with another when
two are in flight at once (see client/src/bedrock-planner.js's header comment for
exactly how that's enforced). npm test needs no LLM key and makes no network call to
AWS: client/test/bedrock-planner.test.js and client/test/bedrock-proxy-route.test.js
both drive the loop and the proxy route respectively against a mocked bedrockClient
returning scripted Converse-shaped responses.
Live browser execution works (#130, corrected 2026-09-11). An earlier version of
this section documented the opposite as a "known, deliberate gap" — that was true at the
time (the browser tried to construct BedrockRuntimeClient itself, which cannot resolve
credentials and cannot even resolve the SDK's import without a bundler) but was never
actually verified live until #130 opened this client in a real browser tab and watched it
fail exactly that way. The fix moved the AWS call server-side; a real click-through now
gets as far as a real AWS SDK error (e.g. Could not load credentials from any providers, in this environment, which has no local AWS credentials configured)
delivered cleanly through the UI's normal error path, not a browser crash. Getting an
actual model response additionally needs the machine running npm start in client/ to
have real AWS credentials available to the Node process (local ~/.aws/credentials /
env vars in dev, an IAM role if this process is ever deployed) — nothing about that is
new to this goal, it is just where the existing "no AWS credentials in this repo" limit
now actually lives.
Inspecting with an off-the-shelf MCP client
With the server running (npm start in one terminal), use the reference MCP Inspector
CLI from another terminal to list and call the tools directly — no custom client code
needed:
npx -y @modelcontextprotocol/inspector --cli http://127.0.0.1:3000/mcp --method tools/list
npx -y @modelcontextprotocol/inspector --cli http://127.0.0.1:3000/mcp --method tools/call --tool-name list_devices --tool-arg room=kitchen
npx -y @modelcontextprotocol/inspector --cli http://127.0.0.1:3000/mcp --method tools/call --tool-name get_device_state --tool-arg device_id=dev_thermostat_1tools/list against a live server returns all eight tools:
- list_devices
- get_device_state
- check_automation_policy
- read_audit_log
- propose_action
- execute_action
- compose_scene
- execute_scene
TOTAL: 8Demo walkthrough
The SPEC.md section 5 script, as it actually runs. Below is a real transcript captured by
driving client/src/planner.js against a freshly started server, confirming the first
proposal and declining the second — the same code path the browser UI uses, with the
button clicks scripted instead of clicked. Abridged only by truncating long JSON
payloads; nothing is paraphrased.
[1] USER: Turn on the living room light and set it to 50% brightness, then check the thermostat.
[2] AGENT: Let me see what devices are in the home.
tool: list_devices args={}
-> {"devices":[{"device_id":"dev_living_room_light_1","name":"Living Room Overhead", …5 devices…]}
[4] AGENT: Checking home automation policy allows dimming this light.
tool: check_automation_policy
-> {"allowed":true,"rule":"no_restrictions","reason":""}
[5] AGENT: Proposing: dim Living Room Overhead to 50% brightness.
tool: propose_action args={"device_id":"dev_living_room_light_1","action":"set_brightness","params":{"brightness":50}}
-> {"proposal_id":"prop_47a8311a-…","status":"awaiting_approval","confirmation_token":null}
[6] SYSTEM confirm gate -> proposal prop_47a8311a-… (set_brightness) status=awaiting_approval
person clicks: CONFIRM
[7] AGENT: Executing the confirmed action.
tool: execute_action args={…,"confirmation_id":"confirm_030ea540-…"}
-> {"device_id":"dev_living_room_light_1","new_state":{"power":"on","brightness":50,"color_temp_k":4000},"result":"success","audit_entry_id":"audit_e351d597-…"}
[9] USER: Also turn off the kitchen coffee maker plug.
[11] AGENT: Proposing: turn off the Kitchen Coffee Maker Plug.
tool: propose_action -> {"proposal_id":"prop_bbbdddf3-…","status":"awaiting_approval"}
[12] SYSTEM confirm gate -> proposal prop_bbbdddf3-… (turn_off) status=awaiting_approval
person clicks: DECLINE
[13] AGENT: Understood — leaving the Kitchen Coffee Maker Plug on, nothing changed.
[14] AGENT: Here is the audit log for everything that actually executed this session.
tool: read_audit_log args={}
-> {"entries":[{"entry_id":"audit_e351d597-…","device_id":"dev_living_room_light_1",
"action":"set_brightness","actor":"user","reason":"User approved via prop_47a8311a-…",
"result":"success","new_state":{"power":"on","brightness":50,"color_temp_k":4000}}]}Two things to notice. The declined action produced no audit entry — the log ends with exactly one — and it left the plug untouched. And nothing between the proposal and the person's click changed any state.
The other half of the gate is what happens when a caller skips the person entirely. Against the same running server:
BEFORE {"device_id":"dev_kitchen_plug_1","state":{"power":"off"},"last_updated":"2026-09-08T08:00:00Z"}
REFUSAL isError=true
execute_action requires a confirmation_id minted by the person confirming a
pending proposal in the client. Call propose_action first and wait for their
decision — this call was refused, nothing changed.
AFTER {"device_id":"dev_kitchen_plug_1","state":{"power":"off"},"last_updated":"2026-09-08T08:00:00Z"}
AUDIT entries=1Same state, same last_updated, same audit count. The refusal is a true no-op, not a
warning printed beside a change that happened anyway.
Known limitations
Stated plainly, because a submission that hides these is worse than one that names them.
Deployed (#124). Live at
http://16.176.3.215:3000/mcp, an AWS EC2 instance (t3.micro, Amazon Linux 2023,ap-southeast-2) — the region aus-east-1/ App Runner Organizations restriction on this AWS account ruled out. Verified with a realinitializehandshake:HTTP 200, correctprotocolVersion/capabilities/serverInfo. Measured end-to-end latency from a US-based test origin is 570–700 ms, overSPEC.md§11's 500 ms line — curl's own timing breakdown shows this is 100% network distance toap-southeast-2(TCP connect alone is ~290 ms, one full round trip), not server processing: the loopbacktools/callround-trip is still 0.62–1.17 ms, unchanged. A judge testing from within Australia/APAC would see this comfortably under 500 ms; a US/EU tester will see the same geography this measurement did. Deployment artifacts and the runbook remain at docs/deploy.md.No authentication. OAuth 2.1 with PKCE is not implemented (
SPEC.mdsection 7 recordsauth.jsas deferred). The server requires no credentials, so nothing is gated behind an auth path that does not exist — but a real Alexa+ add-on would need it.Not the Alexa+ MCP Toolkit. This is a self-hosted MCP server plus a simulated Alexa+ client. Every tool call in that client is a real call to the real server, but no Alexa device is in the loop.
All state is in memory. Devices, proposals and audit entries live for the life of the server process; restarting re-reads
server/data/devices.jsonand forgets everything else. Nothing is written back to disk. This is deliberate (SPEC.mdsection 4), not an unfinished persistence layer.The planner is scripted, not reasoning. It walks a fixed conversation so the demo and tests run identically with no LLM and no API key.
SPEC.mdsection 10 leaves an LLM planner as optional future work; it would still have to pass through the same confirm gate.execute_scenedoes not roll back. If a later action in a scene fails, the earlier ones stay applied; the tool stops and reports exactly how far it got. Its description says so.The demo's coffee-maker line is looser than the data. The plug is seeded
off, so "leaving it on" is about the proposal being declined, not about an appliance that was running.docs/video-script.mdflags this and tells the narrator not to embellish it.
Privacy and security notes
No personal data, anywhere. The five devices in
server/data/devices.jsonare fictional, in a fictional house. No real address, account, network identifier or person appears in any source file, test, or fixture.No credentials, with one scoped exception. The MCP server, the scripted planner, and every device/proposal/audit tool read no API key, token, or password — there is no
.env, no secret store, no authentication path for any of that. The exception isPLANNER=bedrock(#130):client/server.js'sPOST /bedrock/converseresolves an AWS credential the normal Node way (local~/.aws/credentials/ env vars in dev, an IAM role if ever deployed) to call Bedrock. That credential is held only in that Node process's own environment, is never logged, written to disk, or echoed in any response, and — the property that actually matters here — is structurally unreachable from browser-served code: the browser only ever POSTs a Converse request and reads back its JSON result, the same origin, no credential in either direction./bedrock/conversehas no origin check, auth, or rate limit of its own (matching every other route on this server, none of which needed one for a same-machine demo) — the one difference is that this route is a real, metered AWS call, so that tradeoff is worth naming rather than leaving implicit if this process is ever reachable from anywhere but localhost.No outbound network calls, with the same exception. Every tool resolves against the in-memory registry; the server contacts no device, vendor API, or third-party service. The browser's only external fetch is loading React from a CDN via the import map in
client/index.html. UnderPLANNER=bedrock,client/server.jsadditionally makes one outbound call per conversation turn to Amazon Bedrock — the one deliberate exception, and never anything else.Authorization is structural, not advisory.
ProposalStore.approve()is the only code that ever mints aconfirmation_token, and its only caller is the owner-onlyPOST /proposals/:id/approveroute. No MCP tool wraps it, so no agent tool call can reach it. Tokens are single-use:markExecutednulls the token, so a replay is refused.Tokens are not echoed back.
GET /proposals/:idstripsconfirmation_tokenbefore responding — the token is returned exactly once, to the caller that minted it.Policy is re-checked at execute time, never trusted from an earlier
check_automation_policycall, so an approval cannot be used to smuggle through an action the policy would now deny.The audit log is append-only. There is no delete or redact path in
audit.js;read_audit_logis its only reader.Host-header validation is off, deliberately (#124). The SDK's DNS-rebinding protection defaults to rejecting any request whose
Hostis notlocalhost/127.0.0.1— true until this demo was actually deployed to a real address, at which point that check refused every request from its own public IP withHTTP 403 Invalid Host. Disabled viaenableDnsRebindingProtection: false, the same tradeoff already made for CORS below: with no auth and no origin allowlist, this one check wasn't real protection, just an accident of the SDK's localhost-only default.CORS is wide open (
Access-Control-Allow-Origin: *) because the demo runs two local processes on two ports. That is appropriate for a localhost demo and would need tightening to an allowlist before any production deployment.The client's static server serves only
client/, from a fixed extension allowlist, and rejects any resolved path outside its own directory.
License
MIT — see LICENSE at this entry's root. The repository root carries the same MIT license, which is the one GitHub reads for the repository's About section.
Documentation
Document | What it is |
The pinned contract: tools, data model, demo script, stack, and the hackathon's submission checklist. | |
Architecture diagrams (Mermaid + exported SVG), the approval-gate sequence, the refusal matrix, and how to onboard this server to Alexa+. | |
The Devpost write-up: text description, Built With, every form field, and the submission checklist ticked line by line with evidence. | |
Product feedback on all ten tools/APIs/SDKs used. | |
Four friction-log entries in the hackathon's requested format. | |
Demo video script, shot list, and timing budget. |
See SPEC.md for the full specification, demo script, and submission checklist.
This server cannot be deployed
Maintenance
Related MCP Connectors
Human-in-the-loop review and approval for AI agents. Audit trail, approval policies, native MCP.
Zero-secret MCP gateway for AI agents: risk-scored, audited calls with human-in-the-loop approval.
Human-in-the-loop for AI agents over MCP: durable approvals with a hosted review page & audit trail
Deterministic IT asset registry operated by AI agents over MCP. Agents propose, humans approve.
Related MCP Servers
- FlicenseBqualityNot gradedmaintenanceEnables control and monitoring of Home Assistant smart home devices through MCP, allowing users to list entities, check device states, and call services to control lights, switches, sensors, and other connected devices.4-
- AlicenseNot gradedqualityDmaintenanceEnables AI agents to control Apple Home devices, scenes, and automations through MCP.11 npm64MIT
- AlicenseNot gradedqualityBmaintenanceCentral MCP gateway for smart home automation, enabling agents to safely control Home Assistant and Node-RED with identity-based access, human confirmation for writes, and a WebUI for governance.1MIT
- AlicenseBqualityBmaintenanceProvides a unified MCP interface for smart home automation, enabling device discovery, state management, energy optimization, and policy-aware plan validation across multiple home automation protocols.20Mozilla Public 2.0