Skip to main content
Glama
README.md
# HandoffHome

HandoffHome is a self-hosted MCP server for handing over a fictional household’s evening routine. Jordan leaves early, Sam takes over, and Alex marks a task done from another client session. The server detects calendar clashes, asks one clarification, proposes a revision, and changes ownership only after confirmation. A stale proposal must be re-planned. “What changed and why?” is answered from the change log.

This is a **simulated Alexa+ experience** built for the Amazon Build, Ship, Shape Alexa+ track. The MCP server and its HTTP clients are real. There is no connection to Alexa+, an Alexa device, the Alexa developer console, MCP Toolkit or Web Simulator. The understanding layer is the **deterministic intent parser (no AI model)** in `src/assistant`; it recognizes a limited vocabulary rather than using a hosted LLM. No user research has been conducted; this demo makes no user-adoption or impact claims.

## Demo and review

The [submission package](docs/submission-package/) includes the 2:20 narrated demo, captions, friction log and validation evidence. The final application checkpoint passed 365 automated tests, type checking, a production build and two real-browser tests. See [test evidence](docs/evidence/demo-release/).

This public repository preserves 64 genuine development commits and their authors, dates, messages and merge structure. Three planning documents about unrelated private projects were removed from every historical tree in this release copy; that changes the affected commit hashes. The original private archive is preserved. This release documentation is a subsequent, genuine commit. Historical review and release-gate documents describe the state when written; current submission readiness is tracked in the final package.

## Run locally

Requirements: **Node 24** (`.nvmrc`; developed with 24.22.0 and npm 11.19) and **Google Chrome** for the intended browser experience and the Playwright Chrome-channel tests. No API keys, accounts, cloud services or paid integrations are needed for the application. Installing packages and running the optional conformance/Inspector tools requires network access.

```sh
npm ci && npm run demo
```

Open **http://127.0.0.1:8787**. The MCP endpoint is **http://127.0.0.1:8787/mcp**; `GET /healthz` reports the protocol and mode. Stop the server with Ctrl-C.

`demo` builds first, enables the demo-only reset tool, and starts with `--fresh`: **each launch resets the local demo state**. The fixture is fictional Rivera-Tan household data, in `Asia/Singapore`. The default clock starts on 21 October 2026 at 5:05 PM and advances with elapsed wall time. Ordinary server starts load the durable JSON state at `data/state.json`. To keep that state between launches:

```sh
npm run build
node dist/server/main.js --serve-web
```

<!-- WEB_STATUS -->
`npm run demo` serves the browser client at http://127.0.0.1:8787. The speaker UI uses the deterministic intent parser from `src/assistant` through the official SDK client. Alex's phone is a second MCP session.
<!-- /WEB_STATUS -->

## Try the handoff

Choose Jordan in the simulated “Speaking as” selector and type “I need to leave early; hand over the evening routine to Sam.” Choose the offered time after football. Use Alex’s phone panel to mark the recycling bins done before committing. Confirm the updated plan, use the replay button, then speak as Sam and ask “What changed and why?” The phone is a separate MCP session, not a direct state edit. Typed input is the reproducible path; voice input is optional.

After a handoff is committed, click **Show card** to open its optional MCP App. Scroll the speaker card region to reach Refresh; Close stays available above the card and the composer remains below it. The card displays the supplied handoff, commit ID and available change-log citations. It is read-only and runs in this page's own MCP host; compatibility with third-party hosts is unverified.

For a headless reproduction, `npm run scenario` starts its own real server on a free loopback port, uses a temporary state file, runs the exact golden scenario through official SDK clients, and rewrites [the transcript](docs/evidence/scenario/transcript.md). It leaves your `data/state.json` alone and shuts down its own clients and server. It asserts the exact question, both plan summaries, IDs, stale reason, confirmation, replay and explanation citations.

## Architecture and protocol

```mermaid
flowchart LR
  A[Speaker UI + deterministic parser] -->|SDK Client /mcp| S[MCP server]
  B[Alex's phone: second SDK session] -->|Streamable HTTP| S
  I[Inspector / headless SDK clients] -->|initialize, tools, resources| S
  S --> D[Pure domain + serialized mutations]
  D --> J[Atomic local JSON + hash-chained log]
  S -->|elicitation/create + resource notifications| A
```

The server imports and runs `McpServer` and `StreamableHTTPServerTransport` from **`@modelcontextprotocol/sdk` 1.32.1**, negotiating **MCP 2025-11-25**. It serves POST/GET/DELETE `/mcp`, session IDs, version headers and SSE. Clients send `Mcp-Session-Id` and `MCP-Protocol-Version` after initialization; DELETE terminates a session. Unknown sessions return 404; a missing ID or unsupported version header returns 400.

The web client uses the official SDK `Client` and `StreamableHTTPClientTransport`. Its protocol inspector shows JSON-RPC messages and HTTP metadata. This in-page inspector is distinct from the independent **MCP Inspector CLI** check. The browser dialogue manager converts deterministic intents to MCP tool calls; the server owns all planning, revisions, confirmation, persistence and explanations. The UI chooses the simulated speaker explicitly; there is no speaker recognition.

Each tool has strict input validation, an output schema and `structuredContent` on success, plus short speech. Domain errors use `isError: true` and `_meta["handoffhome/error"]`, without `structuredContent`. Every mutation requires an idempotency key. A retry with the same key cannot create a second commit; reusing a key with different arguments is rejected. An outstanding confirmation returns `idempotency_in_progress` on same-key retries.

The domain serializes mutations, persists before returning, and re-checks the proposal’s task/event basis at confirmation. It releases its mutation lock while waiting for elicitation. Ownership changes only after `elicitation/create` returns explicit acceptance with `confirm: true`, or after `confirm_handoff` validates the fallback token. The token is single-use, expires after ten minutes, and only its SHA-256 is persisted. Reissuing or invalidating the proposal invalidates the old token. A pending token response is transient; once resolved, the originating commit key replays the final outcome.

## Tools, resources and prompt

Annotation columns below mean `readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`. A dash means omitted, rather than asserted false. Annotations are descriptive hints; server guards enforce the actual behavior.

| Tool | Purpose | Read-only | Destructive | Idempotent | Open world |
|---|---|---|---|---|---|
| `get_household_overview` | Members, tasks, events, active handoffs | true | — | — | false |
| `get_handoff` | Question, current revision, earlier revisions, commit IDs | true | — | — | false |
| `explain_changes` | Changes for a member, with log citations | true | — | — | false |
| `start_handoff` | Start a draft; detect a clash and ask one question | false | false | true | false |
| `answer_clarification` | Apply an offered option and propose a revision | false | false | true | false |
| `replan_handoff` | Rebuild from current state with an expected revision | false | false | true | false |
| `commit_handoff` | Request confirmation; commit only after acceptance | false | false | true | false |
| `confirm_handoff` | Explicit token-based fallback confirmation/cancellation | false | false | true | false |
| `cancel_handoff` | Cancel an uncommitted handoff | false | false | true | false |
| `update_task_status` | Record a fixture task update; invalidate dependent plans | false | false | true | false |
| `update_calendar_event` | Move a fixture event; invalidate dependent plans | false | false | true | false |
| `revise_handoff` | Create a separately confirmed correction to a committed handoff | false | false | true | false |
| `reset_demo_household` | Reset fictional state; only with `HANDOFFHOME_DEMO_TOOLS=1` | false | true | true | false |

Resources:

- `handoffhome://household/state`: overview without speech.
- `handoffhome://household/changelog`: entries and head hash.
- `handoffhome://handoffs/{handoff_id}`: handoff resource template.
- `handoffhome://tasks/{task_id}`: task and its history template.
- `ui://handoffhome/handoff-card.html`: self-contained MCP App UI, MIME `text/html;profile=mcp-app` (build first).

The App uses `@modelcontextprotocol/ext-apps` 1.7.5 with the official App/AppBridge protocol. Only get_handoff advertises the UI resource. The iframe sandbox is exactly `allow-scripts`; its host forwards only get_handoff/explain_changes and rejects write-tool requests before server traffic. An unavailable card leaves the speaker flow usable.

State, changelog and existing handoff resources support subscriptions. Mutations notify subscribed sessions with `notifications/resources/updated`; entity creation also emits `notifications/resources/list_changed`. Task resources are readable but do not support subscriptions. The `hand_over_routine` prompt accepts `from_member` and `to_member`; it guides a client through the server workflow.

## Commands

Run these from the repository root. Run builds and tests one at a time; `--pool=forks --maxWorkers=2` limits Vitest parallelism.

| Command | Action |
|---|---|
| `npm ci` | Install the pinned lockfile |
| `npm run typecheck` | Check application, scripts and tests with TypeScript |
| `npm test` | Vitest: domain, store, assistant, server, scenario and web unit suites present in the checkout |
| `npm test -- --pool=forks --maxWorkers=2` | Full suite with at most two Vitest processes |
| `npx vitest run tests/scenario/http.test.ts --pool=forks --maxWorkers=2` | Only the HTTP scenario/race suite |
| `npx vitest run tests/domain tests/store --pool=forks --maxWorkers=2` | Domain/store, including the 200-run invariant property test |
| `npx vitest run tests/assistant --pool=forks --maxWorkers=2` | Deterministic parser/dialogue tests |
| `npx vitest run tests/server --pool=forks --maxWorkers=2` | SDK HTTP protocol/security tests |
| `npm run test:e2e` | Playwright using installed Chrome; starts `demo:test`; no browser download needed |
| `npm run build` | Self-contained MCP App card, Vite browser build and emitted TypeScript server |
| `npm run demo` | Build, enable reset tool, reset state, serve web and MCP on port 8787 |
| `npm run demo:test` | Demo with temporary state and port from `E2E_PORT` (used by Playwright) |
| `npm run dev` | Watched server plus Vite at http://127.0.0.1:5173 |
| `npm run reset` | Replace persisted demo state from the fixture (build server first; stop it before resetting) |
| `npm run scenario` | Own ephemeral server; assert golden A; write committed transcript |
| `npm run conformance` | Run all 32 official server scenarios and record pass/fail/N/A evidence |
| `npm run inspector:check` | Independent MCP Inspector CLI list/call check |
| `npm run licenses` | Regenerate `THIRD_PARTY_NOTICES.md` from installed packages |
| `npm run check` | Typecheck, full Vitest suite, then production build |

To run `check` with at most two Vitest processes, use `npm run typecheck && npm test -- --pool=forks --maxWorkers=2 && npm run build`.

Server flags: `--serve-web`, `--fresh`, `--reset-only`, `--port <n>`. Configuration is read from `HANDOFFHOME_HOST`, `HANDOFFHOME_PORT`, `HANDOFFHOME_STATE_FILE`, `HANDOFFHOME_ALLOWED_ORIGINS` (comma-separated), `HANDOFFHOME_ALLOW_REMOTE`, `HANDOFFHOME_DEMO_TOOLS`, `HANDOFFHOME_CLOCK` (`fixed` or `real`) and `HANDOFFHOME_LOG` (`info` or `debug`). The default fixture clock is intentional; real wall time may put all fixture tasks in the past. The web client uses `?server=` when supplied, otherwise same-origin `/mcp` in production and `http://127.0.0.1:8787/mcp` under Vite. A custom browser origin must be explicitly allowed.

## Verification and evidence

<!-- VERIFICATION_STATUS -->
**Local fixture verified — 18 Oct 2026.** A4 integration was checked on `c5f732972fcfd35f5b2f8a9dd449f7d8efe88591`; final evidence commit `17ed46e` and main merge `fc9293c` preserve that application/test tree.

| Check | Result and tested revision |
|---|---|
| Capped Vitest | **364 passed / 25 files**, zero failures/skips, c5f7329 |
| Install, typecheck and production build | Pass, c5f7329; card JavaScript 331,058 bytes |
| Focused MCP App Chrome tests, real / mock | **3/3 + 3/3**, 61fdde0 (application/test tree identical to c5f7329); includes normal 1280×800 wheel reachability |
| Earlier full Chrome suites, real / mock | **22/22 + 22/22**, d46e04f, before the viewport fix; an additional 22/22 real run corrected the evidence capture |
| Fresh official conformance 0.1.16 | **8 pass / 0 fail / 24 N/A**, c5f7329; all 32 scenarios executed, reference-fixture failures retained |
| Fresh MCP Inspector CLI 1.0.2 | **2/2**, c5f7329; get_handoff alone advertises both UI URI metadata keys |
| Earlier clean-clone demo | d4622ff: install, 336 tests, demo HTML/health/initialize and foreign Origin rejection passed before A4 |

The integration unit run started without the card build and checked the explicit missing-build error. Its later successful build is not a new built-resource HTTP test. Slot3's prior post-build resource tests and focused real browser run exercised the actual self-contained HTML, bridge and UI on the reviewed source. Served HTML measured **332,825 bytes** against the 400,000-byte budget. No full browser suite was rerun for the viewport-only correction; the focused regression and exact tree equality are recorded separately.

See [A4 source review](docs/reviews/review-slot3-5.md), [integration review](docs/reviews/review-slot1-6.md) and [protocol evidence](docs/evidence/protocol-notes.md). Chrome tests stub TTS; audible quality and speech recognition remain manual checks. Third-party MCP hosts and Alexa+ itself remain unverified.
<!-- /VERIFICATION_STATUS -->

The domain property test runs **200 sequences with seed 20261017**, checking I1–I10 after each operation. The HTTP scenario suite covers golden A with elicitation and without it, ten simultaneous same-key commits, a second session interrupting an open elicitation, Origin/version rejection, restart replay and speech limits. All application mutations and household-state observations use the actual HTTP MCP surface; fixture initialization and the clock are injected. The token-path test also reads the persisted file to check that it contains no raw token.

Raw reference-fixture failures remain visible in the conformance evidence; N/A does not claim those assertions passed. Inspector CLI 1.0.2 verified `tools/list` and `tools/call get_household_overview` against the real service.

- [Scenario transcript](docs/evidence/scenario/transcript.md): real server, two official SDK client sessions, exact golden strings and replay.
- [Conformance summary](docs/evidence/conformance/summary.md): each scenario and its reason; raw checks retained.
- [Inspector evidence](docs/evidence/inspector/summary.md): independent protocol client, not an AI assistant.
- [Protocol notes](docs/evidence/protocol-notes.md): installed SDK behavior and wire details.
- [Browser screenshots](docs/evidence/screenshots/): the committed `real-*.png` files come from the real server; `mock-*.png` files use the mock server.
- [Assistant client examples](examples/assistant-clients/README.md): **implemented but unverified**, pending a manual client/version/transcript record.

Status labels: the checks above are **local fixture verified**. Browser TTS tests use a stub in Chrome e2e; they do not verify audible output quality. Speech recognition needs **a manual check (not automated)**. Alexa+ itself and AWS deployment are not verified integrations; any future deployment is a **final release gate**, including a deployment cost review. A clean-clone demo run, contest publication and recording remain [release gates](docs/release-gates.md).

## Local security and privacy

The server binds to loopback by default and validates Host and Origin before allocating a session. No-Origin local CLI clients are allowed. The default browser allowlist is localhost/127.0.0.1 on ports 8787 and 5173. There is **no authentication or OAuth**: any trusted local process able to reach the port can use the tools. Session IDs and idempotency keys are not access control. Remote binding requires an explicit override, but does not add authentication; this is a local demo, with no public deployment supported.

All household data is **FICTIONAL DEMO DATA**, loaded from [the fixture](fixtures/household.rivera-tan.json). No real calendars, messages or contacts are accessed. The server makes no outbound network calls. State persists locally; a corrupted file is rejected without overwriting it and the error directs you to `npm run reset`. Writes are serialized and atomic; the log’s hash chain detects corruption at load. It is a local audit trail, not an independently trusted ledger.

The app has no telemetry, analytics or third-party fonts/CDNs. Typed input and household data stay local in the default demo. Optional browser speech recognition requires microphone permission: **“Voice input uses your browser's speech service. In Chrome this may send audio to Google. Typing keeps everything on this computer.”** Browser TTS uses an available system/browser voice. Automated tests stub TTS and do not record microphone audio. No recognition provider behavior or retention policy has been independently verified.

## Scope, limitations and licence

The demo handles a single English-language fictional household and ordinary chores. It has no free-text task creation; medical, medication/dosing, emergency, safety-monitoring, child-monitoring and elder-monitoring requests are out of scope. There are no accounts, multiple households, localization, real calendar adapters, messaging, SMS, email or push notifications. There is no voice biometrics, Amazon/Alexa branding, wake-word sound or Alexa voice. The optional local-model adapter is not built; the default requires no model service. No Lambda, Bedrock, AWS hosting or other deployment has been built.

The limited parser will ask for supported phrasing rather than understand arbitrary conversation. Confirmation can be dismissed or declined; an interrupted proposal must be re-planned. JSON persistence is appropriate for this local demo, not a multi-process service. Browser speech capability varies; typed input remains available.

Source is licensed under [MIT](LICENSE). Pinned dependency licence notices are in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md). See [the frozen implementation plan](docs/implementation-plan.md), [product feedback](docs/product-feedback.md), [friction log](docs/friction-log.md) and [demo script](docs/demo-script.md) for the build’s scope, evidence and contest handoff.

Maintenance

ActivityMaintained
ResponsivenessNo issues