Inspiration

Alexa+ integrations run on MCP, which means a voice assistant will soon call tools that move money and open doors. Reading the MCP spec, we noticed that everything about how a tool is called is standardised, but nothing about whether it should be — previewing, confirming and recording an action are left to each server. Most demo servers skip all three. We wanted to build the household server we would actually let an assistant talk to, and make the safety layer the product rather than a checkbox.

What it does

  • 31 MCP tools over Streamable HTTP for one household: bills, chores, shopping, reminders, budget, simulated smart-home devices (a lock, a thermostat) and routines, members, policies, pending actions and the audit log.
  • One guard on every write. A mutating call returns executed (low risk, already done and audited) or needs_confirmation (a preview of every concrete change, an action_id, an expiry, and a spoken sentence telling the user what to say). confirm_action runs it exactly once; a repeat replays the stored result; reject_action declines it; unapproved actions expire in ten minutes. dry_run: true on any mutating tool returns the preview and writes nothing.
  • Policies with a floor. Adds and updates execute immediately; money-moving and destructive actions need a yes; unlocking the door for a child, or changing the policies themselves, needs a person in the console — an assistant cannot lower the guard and then approve its own change.
  • A tamper-evident audit log. Every proposal, execution, rejection and expiry appends a row whose SHA-256 covers the previous row's hash. verify_audit_chain recomputes the whole chain; the console shows "Chain intact · N rows".
  • A console that is the human half of the guard. /pending shows the same preview the assistant saw, with Approve and Reject; the CRUD pages go through the same runTool path as the MCP endpoint. Server-rendered, phone-friendly, works with JavaScript off.
  • An MCP App. The ui://housewarden/pending resource (MCP Apps extension, on by default) renders the same approval card inside hosts that support it; hosts that do not see ordinary tools with a spoken text fallback.
  • Zero-setup self-hosting. npm run dev generates the secrets, runs an embedded Postgres (PGlite), loads a demo family and starts. Any Node 20+ host runs it with npm run build && npm run start; managed Postgres is one environment variable away.

How we built it

  • Next.js 16 (App Router, TypeScript, Tailwind) hosts both the console and the MCP endpoint at app/api/mcp/route.ts.
  • mcp-handler 2 turns an MCP SDK v2 McpServer into a web-standard request handler; it serves the 2026-07-28 revision natively and falls back to stateless 2025-era Streamable HTTP, so the same endpoint works for Alexa+ (2025-11-25) and newer hosts. A 30-line wrapper adds the bearer-token check (constant time), the Origin allow-list and the never-fall-open 503.
  • @modelcontextprotocol/server 2 registers each tool with a zod v4 input schema, an output schema (structuredContent) and voice-oriented annotations and descriptions; lib/contracts.ts is the single source of DTOs, the tool catalogue and default risks. registerResource publishes the MCP App document, and the guard tools carry _meta.ui.resourceUri.
  • The guard (lib/guard) is the only module that executes a mutation plan: propose → plan (pure, read-only) → policy → execute or queue; confirm does an atomic pending → confirmed claim under a Postgres advisory lock, re-plans, compares previews (STALE_PREVIEW on drift) and executes once. Audit rows are hashed over a canonical JSON serialisation with published test vectors.
  • Storage is one Db interface with two adapters — PGlite (embedded, default) and pg — on one Postgres dialect and idempotent SQL migrations.
  • Verification: 93 vitest tests on in-memory PGlite (core, tools, MCP App), and npm run e2e, which starts a server and drives it with the real @modelcontextprotocol/client 2 over Streamable HTTP: no-token → 503, 2025 handshake and 2026 negotiation, tools/list = 31 with schemas, read, low-risk mutation and idempotent replay, dry run, confirm-risk mutation with proof nothing was written, exactly-once confirm, reject, chain verification, and the raw 403 / 401 / 405 / 400 answers — 23 checks. The demo video itself is driven by the same kind of client (demo/record.mjs --check replays the storyline as 26 more assertions).

Challenges we ran into

  • Making "exactly once" true under concurrency without sessions or timers: the answer was to make the pending action the session, claim it atomically, and sweep expiries on access.
  • Re-planning on confirmation so an approval never executes something the approver did not see.
  • Keeping one write path honest across two surfaces: the console has no privileged bypass, which forced the confirmation card to be a first-class UI pattern rather than an afterthought.
  • Tooling: Next 16's Turbopack refuses a node_modules symlink outside the project root, which broke next dev in our parallel-worktree build setup until we found --webpack — and webpack dev mode then raced on its own manifests under concurrent on-demand compiles (F1, F5 in the friction log). mcp-handler builds a fresh McpServer per request, so we had to keep every schema a module-level constant to make that cheap (F6).

Accomplishments we are proud of

  • A safety model that is explainable in one sequence diagram and provable with one command (npm run e2e prints a 23-line pass/fail table).
  • Spoken-first tool design: every tool returns a sentence fit for text-to-speech alongside structured output, and the server's instructions tell the assistant exactly how to handle a needs_confirmation.
  • Real product completeness: empty states, error states, a named visual identity, a demo household, a narrated demo client, an MCP App, and zero-setup self-hosting.

What we learned

MCP SDK v2's output schemas plus a client that validates structuredContent make a server's promises checkable, which changes how you design tools: the envelope (executed / needs_confirmation / dry_run) became a typed contract rather than prose. We also learned that a voice host needs the server to carry the confirmation state, because the assistant may not support elicitation and the person may want to approve on a screen instead.

What's next

  • Real device back-ends behind the same set_device_state contract.
  • Per-member PINs for voice approvals, and household-level notification of pending actions.
  • A host-capability check for the MCP App once a stateful transport makes client capabilities visible at registration time.

Built With

Share this project:

Updates

Submission history