-
-
System diagram: two processes, one gate
-
Propose-confirm-execute sequence
-
tools/list against the live MCP server on AWS EC2 returns all eight tools.
-
An approve call without the owner's key: HTTP 401, nothing minted.
-
The server's test suite passing: 7 suites, 47 tests.
-
The agent proposes unlocking the front door. The owner declines; the lock's tile still read Locked, and the log holds one entry, the light.
-
The owner clicks Confirm: execute_action runs with the one-time token, and the light's tile moves to 50%.
-
The agent proposes dimming the living room light to 50% and stops at Confirmation needed; nothing has changed yet.
-
execute_action called directly with no confirmation_id: isError true, and the door stays locked.
Source code (MIT): github.com/guptachetan1995/alexa-plus. Live MCP server: http://16.176.3.215:3000/mcp on AWS EC2. It speaks MCP over Streamable HTTP, so a browser tab shows HTTP 400; connect an MCP client instead, for example npx -y @modelcontextprotocol/inspector --cli http://16.176.3.215:3000/mcp --method tools/list (details).
Inspiration
An agent that can unlock your front door can unlock it by mistake, or because something it read told it to. MCP gives hosts two ways to be careful: tool annotations, which are hints a host may act on, and elicitation, which is a way to ask the user. Neither one stops a call the host decides to make anyway, and a confirmation inside the agent's own loop is something the agent's side of the wire can skip.
We wanted approval to be enforcement instead: something the agent's side of the wire cannot produce, whatever the model decides. The people who need that are homes where more than one person has a say over the same devices: households running several brands of device, renters and landlords sharing a smart lock, and caregivers who need a record of who approved what.
What it does
Smart Home Agent is a self-hosted MCP server that exposes a smart home through one interface, plus a simulated Alexa+ web client that drives it. The agent can discover devices, read their state, check the home's automation policy and compose multi-device scenes. It cannot change anything on its own.
Every change goes through propose → confirm → execute. The agent calls propose_action (or compose_scene), which records the proposal with its expected outcome, rationale and policy check, and changes nothing. The conversation stops on a "Confirmation needed" panel. Only the owner's Confirm mints a one-time confirmation token: exactly one function mints it, on an approve route that no MCP tool reaches and that requires the owner's key. execute_action accepts that token once, for that exact device, action and parameters, re-checks the automation policy, and only then changes the device and writes an audit entry naming the person as the actor.
In the demo the agent dims the living room light (the owner confirms, and the light's tile moves to 50%) and then proposes unlocking the front door (the owner declines, the door stays locked, and the audit log holds one entry: the light).
Try to skip the owner and the call is refused with a narrated reason, and nothing changes: no token, an unknown token, an unapproved proposal, a replayed token, a token approved for a different action, a policy that denies the action at execute time, or an approve call without the owner's key. Each of those has its own test.
How we built it
- MCP server (
server/) — Express 5 and the official@modelcontextprotocol/sdk, speaking Streamable HTTP at spec revision 2025-11-25 on/mcp(POST, GET, DELETE, with session-id issuance, reuse and termination). Eight tools: four read-only (list_devices,get_device_state,check_automation_policy,read_audit_log) and two propose/execute pairs (propose_action/execute_action,compose_scene/execute_scene). Each carries MCP tool annotations for hosts that prompt on them; the gate does not depend on them. - The approval boundary —
ProposalStore.approve()is the only code that ever sets a confirmation token. Its only caller isPOST /proposals/:id/approve, which requires the owner key (Authorization: Bearer, compared as digests withtimingSafeEqual) and answers browsers only from allowlisted origins. No MCP tool wraps it, so "only the owner can authorize a change" is a property of the architecture, not a promise in a prompt. - Simulated Alexa+ client (
client/) — a React conversation UI with no bundler and no build step, a separate project that imports nothing fromserver/and reaches it only over HTTP. Device tiles above the conversation are rebuilt from the real tool results, so a Confirm visibly changes a device and a Decline visibly does not. - Deterministic by default — the scripted planner runs the demo and the tests with no LLM and no API key;
PLANNER=bedrockswaps in Amazon Bedrock. - Tests — 47 server tests (Jest and Supertest) and 25 client tests (
node:test), including integration tests that spawn the real server as a separate process.
Challenges we ran into
- The MCP SDK's
createMcpExpressApp()ships no CORS handling, so a browser client on another origin fails with a bare "Failed to fetch" whose real cause (a preflight 404 and a missingAccess-Control-Expose-Headers: Mcp-Session-Id) is invisible to a Node integration test. - AWS App Runner could not be used: building the service from its GitHub source needed an AWS Organizations "all features" migration the account had not made, and
us-east-1was blocked by a service control policy. The server went to EC2 inap-southeast-2instead, which is why a US tester sees 570–700 ms end to end. @aws-sdk/client-bedrock-runtimein a zero-build browser page failed with a minifiedTypeError: AC is not a function; the real cause was the Node-only credential chain. The Bedrock call moved server-side, where credentials belong anyway.- The planner's first default model, Claude 3.5 Sonnet, had left Bedrock's catalog; we switched to Nova Micro, in-Region in
ap-southeast-2.
All seven friction points are written up in the friction log.
AWS services used
- Amazon Bedrock (Converse API, tool-use loop, model
amazon.nova-micro-v1:0) is an optional planner (PLANNER=bedrock) that replaces the scripted turn sequence with real model reasoning. It is called from Node (client/server.js, orclient/bedrock-cli.jsin a terminal), never from the browser. Every tool call still goes through the same MCP client, and every change still needs the owner's Confirm: the model has no path to mint a confirmation token. Proven with a live Converse call from AWS CloudShell that returned a real tool-use decision (HTTP 200). A full run of the loop against the MCP server with a real model has not been recorded yet. - AWS EC2 hosts the MCP server at
http://16.176.3.215:3000/mcp(t3.micro, Amazon Linux 2023,ap-southeast-2). - AWS App Runner was the planned host; its container artifacts are in the repository, and the account restriction above kept it from being used.
This entry is also in the Open Source mini-challenge: a new, MIT-licensed project created inside the submission window.
What's next
- An HTTPS endpoint (a route that needs no new account is written up in docs/deploy.md) and the OAuth 2.1 + PKCE onboarding a real Alexa+ add-on needs, which would also replace the single owner key with per-person identity in the audit log.
- A real Alexa+ surface in the loop (the Alexa+ MCP Toolkit or an Agent Skill) in place of the simulated client.
- A recorded
PLANNER=bedrockrun against the live server. - Persistence for devices, proposals and the audit log, which live in memory today.
Built With
- alexa
- amazon-bedrock
- amazon-ec2
- amazon-web-services
- css
- eslint
- express.js
- html
- javascript
- jest
- json-rpc
- mcp
- model-context-protocol
- node.js
- npm
- react
- streamable-http
- supertest
- zod
Log in or sign up for Devpost to join the conversation.