Skip to main content
Glama

Closed Loop

Daily check-ins that close the loop. A self-hosted MCP server for an Alexa+ add-on that asks an older adult who lives alone a few check-in questions their family wrote, records the answers honestly, follows up later, and emails a family member only when a loop stays open.

Simulated client. Simulated Alexa+ client. Not Amazon software. Alexa+ add-on tooling is partner-only, so this project implements the MCP server side and demonstrates it through a self-built MCP client, as the hackathon FAQ allows.

Scope. Reminders and check-ins only. Not an emergency alert system. No answer does not mean an emergency.

Voice turns. Check-in conversations are started by the person, a household routine, or the simulator. MCP add-ons have no documented way to speak first.

Demo video: link added at submission · Live demo: URL shared with judges (access code required)


What it does

A reminder that says "take care of your morning routine" never learns whether that happened. Families improvise around this (a reminder plus a follow-up call, or a lamp switched off as a sign of life). Amazon ended Alexa Together in 2025. Closed Loop does the part that comes after the question:

  1. Record what was said, honestly. "I think so, maybe after lunch" is stored as said later, never as done. A hedged "yes" becomes said they would, not sure. The ledger only ever says reported by the person, said later, no response, needs human contact, and never "confirmed".

  2. Follow up. It asks again at the time the person chose ("after lunch" → 13:00), or after the caregiver's re-ask interval, at most a set number of times. It never re-asks in the same turn.

  3. Tell a human when the loop stays open. When a window closes with no report, the family member gets a plain email: "We did not hear back about your morning routine today. This is not an emergency alert." One click acknowledges it and stops further emails. If nobody acknowledges for a second day, the second contact is told.

Confusion or distress ("I want to go home, where is everyone?") gets the family's own calming line, no further questions, and a neutral note to the family: "Margaret seemed confused or upset during a check-in. A call may help." Emergency wording gets one fixed line: "If this is an emergency, please call 911 now." Self-harm wording adds the 988 Suicide and Crisis Lifeline.

Related MCP server: Daybreak

Try it locally (no AWS account needed)

Requires Node.js 20 or later.

npm install
npm run dev

Then open:

Local run mode uses an in-memory store, the same simulated clock, the rule-based planner, and a fake email sender that writes each email as an HTML file to ./out/emails/.

A two-minute walkthrough

  1. In the simulator, press Start a fresh demo day at 7:55 am, then +1 hour.

  2. Press Simulate morning routine start. The assistant asks Margaret's morning question, worded by her daughter.

  3. Reply "I think so, maybe after lunch". Watch the MCP traffic pane: the planner picks defer_checkin, and the browser calls the real MCP server. The card shows said later · ask again at 13:00.

  4. Press +5 hours, then the routine button again. The follow-up comes first. Reply "Yes, done." The loop closes: reported by the person.

  5. Press +1 day. Yesterday's unanswered lunch and evening windows close; an email to Dana appears in the Family email panel (and in ./out/emails/). Open the console and press I've been in touch to acknowledge.

  6. Try "I want to go home. Where is everyone?" and "I fell and I can't get up."

Check the MCP server with other clients

npx @modelcontextprotocol/inspector@2.11.0 --cli http://localhost:5173/mcp --transport http --header "Authorization: Bearer local-dev-mcp" --method tools/list
npm run second-client -- http://localhost:5173/mcp local-dev-mcp
spikes/handshake.sh http://localhost:5173 local-dev-mcp

Architecture

flowchart LR
  subgraph Browser
    SIM[Simulated Alexa+ client<br/>real MCP over HTTP]
    CON[Caregiver console]
  end
  subgraph AWS["AWS (us-east-1, SAM)"]
    WEB["Web Lambda · Function URL<br/>/mcp · console API · ack links · pages"]
    DDB[(DynamoDB<br/>single table, TTL, Streams)]
    SCH[EventBridge Scheduler<br/>tick every minute · keep-warm every 5 min]
    TICK[Tick Lambda<br/>windows · ladder · caps]
    SES[Amazon SES]
    CLS[Classifier Lambda<br/>second opinion]
    BR[Amazon Bedrock<br/>Converse API]
  end
  SIM -- "initialize · tools/list · tools/call" --> WEB
  SIM -- planner --> WEB
  CON --> WEB
  WEB --> DDB
  WEB -. "planner (off the hot path)" .-> BR
  SCH --> TICK
  SCH -. keep-warm .-> WEB
  TICK --> DDB
  TICK --> SES
  DDB -- "Streams: new utterances" --> CLS
  CLS -.-> BR
  CLS --> DDB
  • MCP tool calls touch DynamoDB only. No LLM and no network call on the hot path. Warm round trips measured 9 ms p50 / 33 ms p95 server-side (Alexa+ requires under 500 ms).

  • The tick cannot speak to anyone in the home. It closes windows, decides who to tell, and sends email. Every alert is claimed with a conditional write before sending, so overlapping ticks never double-send.

  • The planner stands in for Alexa+'s own LLM. The browser asks it which tool to call, then makes the MCP call itself, so the traffic you see is real. It uses Bedrock (Converse with the live tools/list as tool definitions) and falls back to rules, saying so in the traffic pane.

  • Results come with a card. An MCP App (ui:// resource) renders each result visually wherever the client supports MCP Apps.

  • A second opinion runs after the fact. New utterances flow through DynamoDB Streams to a classifier that asks Bedrock to re-read them. It can only move a record toward caution: distress it hears escalates to needs human contact; a "done" it doubts gets flagged in the console.

More detail: docs/architecture.md.

The MCP server

Self-hosted, Streamable HTTP, stateless (no Mcp-Session-Id), JSON responses, built on @modelcontextprotocol/sdk 1.32.1 (pinned to the 1.x line).

Tool

Input

What it does

get_due_checkins

none

Open check-ins right now, in the family's wording. Marks them asked; re-serves follow-ups when due.

record_response

occurrenceId, response (done · later · not_yet · no · unsure · needs_help), utterance (≤200 chars, optional)

Runs the safety layer, stores what was said honestly, returns the wording to read back and the next step.

defer_checkin

occurrenceId, until (in_30_min · in_1_hour · after_lunch · this_afternoon · this_evening)

Moves the next ask to a concrete time inside the window.

get_my_status

none

Today in plain words, and who gets emailed, so the person knows what is shared.

pause_checkins

scope (today · 24_hours)

Always honored, no questions asked.

Every tool returns structuredContent plus a short text fallback. Signatures are fixed, strings are bounded, choices are enums (Alexa+ refreshes tool definitions only on deployment).

MCP App card. The server also publishes ui://closed-loop/checkin-card (text/html;profile=mcp-app, the MCP Apps extension, SEP-1865): one self-contained card that renders any of the five results (open loops, recorded as…, ask again at 13:00, today's summary, paused). Tools reference it in their definitions (_meta.ui.resourceUri, as the MCP Apps spec places it) and in their results (_meta.ui with resourceUri, invoking, invoked, as Alexa+'s lifecycle docs show). The card makes no network requests, so it runs under the spec's restrictive default CSP. The simulator advertises io.modelcontextprotocol/ui, reads the resource, and renders the card in a sandboxed iframe through the ui/initialize → tool-input → tool-result handshake. (Simplification: one sandboxed frame with an opaque origin, rather than the spec's two-level sandbox proxy on a separate origin.) Clients without UI support simply read the text.

Protocol behaviors, tested (spikes/handshake.sh, test/mcp-protocol.test.ts):

  • initialize accepts both 2025-03-26 (Alexa's documented lifecycle example) and 2025-11-25 (the version its overview links), and answers with the one the client asked for.

  • Origin is validated: same origin and listed origins allowed, requests with no Origin allowed, anything else 403.

  • No or wrong bearer token → 401 with no WWW-Authenticate header (Alexa+'s authentication page lists that header as not supported). OAuth discovery is served at /.well-known/oauth-protected-resource (RFC 9728) and /.well-known/oauth-authorization-server (S256 PKCE, client_credentials). The OAuth endpoints themselves are stubs; this build uses a static bearer token.

  • GET /mcp → 405. Oversize input and enum violations are rejected. tools/list is byte-identical across calls and protocol versions (snapshot test).

  • A missing or partial Accept header is completed before the SDK sees it (the SDK returns 406 otherwise; Alexa's client headers are undocumented).

Safety and honesty, by design

  • Rule-based safety layer on the hot path (no LLM): hedges ("I think so", "maybe", "just about to") turn done into unsure; anything without clear affirmative wording fails closed to unsure, never done. A 20-utterance eval assumes the worst case (the planner claims done for everything) and passes with zero unsafe results.

  • Distress branch: calming line only, no re-quiz, and no check-ins asked for 60 minutes. Crisis branch: fixed 911 line (plus 988 for self-harm wording), with no promise that anyone is told immediately.

  • The ledger never claims more than happened. "No response" says whether the person was ever asked: no response (not reached) versus no response (asked, no answer).

  • Caregiver wording lint: check-in text is read aloud, so drug names, doses, URLs, and long text are rejected with plain advice ("Use generic wording like 'your morning routine'").

  • Consent and control: check-ins run only with a recorded consent that names the person and who recorded it. Revoking it stops questions and emails immediately. Plans expire (default 14 days) so a forgotten plan cannot keep emailing. At most 3 emails per day; quiet hours hold emails overnight. The person can pause any time.

  • Data minimization: utterances are cut to 200 characters and expire after 30 days (DynamoDB TTL); a delete-all-data button removes everything.

  • Acknowledge links are signed and expire. Opening one only shows a button, so email link scanners cannot acknowledge on someone's behalf.

Accessibility

The console is designed for large default text (18 px), full keyboard use, visible focus, labelled controls, errors tied to their fields, and status that never relies on color alone (each check-in's ring changes shape: open, closed, dotted, or marked). Body text uses Atkinson Hyperlegible. axe-core reports zero violations on every page in light and dark themes (docs/accessibility.md). This is a design choice; it is not a claim that blind or low-vision users asked for this product.

Deploy to AWS

Requires the AWS CLI, AWS SAM CLI, and a verified SES identity (the SES sandbox only sends to verified addresses).

cp .env.example .env    # fill in tokens, SES addresses, optional Bedrock model ID
npm run deploy          # builds the Lambdas with esbuild and runs sam deploy

The stack creates one DynamoDB table (on-demand, TTL, Streams), three arm64 Lambda functions (web, tick, classifier), two EventBridge Scheduler schedules, least-privilege IAM for SES and Bedrock, and 7-day log retention. It stays inside the free tier for a demo. Run the end-to-end check against it:

npm run e2e -- https://<function-url-id>.lambda-url.us-east-1.on.aws "$ADMIN_TOKEN" "$MCP_BEARER_TOKEN"

Tests

npm test            # 82 tests: state machine, ladder, safety eval, stores, MCP protocol, classifier
npm run typecheck
npm run e2e         # replays the demo against a running deployment and checks the ledger

Set DDB_TEST_TABLE to run the store contract tests against a real DynamoDB table as well.

Known limits (stated plainly)

  • No proactive speech. No documented way exists for an MCP add-on to start a conversation, set a native reminder, or be invoked by a routine. That is the single most important missing primitive for this use case (see FRICTION_LOG.md #4 and the feature request in PRODUCT_FEEDBACK.md).

  • Not tested against real Alexa+. Add-on tooling is partner-only. The server follows Alexa's published requirements and is exercised by three different MCP clients.

  • Bedrock on this account. The new AWS account used for the hackathon has a Bedrock daily token quota of 0 (FRICTION_LOG #7). The Bedrock planner and classifier are wired, deployed, and pass Bedrock's request validation; until the quota is raised they fall back to rules and say so.

  • Auth. A static bearer token stands in for OAuth 2.1 with PKCE. Discovery metadata is served; the authorization server is not implemented.

  • Cold starts of the full web function are about 0.9 s (0.61 s init plus a first request that creates the day's records); warm calls take 4–28 ms server-side. A keep-warm schedule keeps one instance ready because new accounts cannot use provisioned concurrency (FRICTION_LOG #8).

Repository layout

packages/core/          state machine, ledger wording, safety rules, lint, clock, stores (pure TS, tested)
packages/mcp-server/    MCP handler on SDK 1.x, five tools, auth, Origin, OAuth metadata, adapters
packages/api/           console API, ack links, admin clock, planners, web + Lambda entry
packages/tick/          tick Lambda, SES and file mailers
packages/classifier/    DynamoDB Streams second-opinion Lambda
packages/store-dynamo/  DynamoDB implementation of the store
apps/                   simulator, console, landing page (static, no build step)
infra/template.yaml     AWS SAM
scripts/                dev server, build, deploy, e2e replay, second MCP client
spikes/                 Phase 0 handshake and latency spikes
test/                   unit, protocol, and contract tests
docs/                   architecture, demo script, accessibility

mcp-handshake-check: the zero-dependency checker built alongside this project, which runs the same wire-level checks against any stateless Streamable HTTP MCP server.

License

MIT

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables agentic voice assistants to run daily check-ins for someone living alone, capturing wellbeing, medication, and appointments, while keeping family informed via summaries and escalating when needed.
    MIT
  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables voice-driven morning check-ins for older adults living alone through Alexa+, capturing sleep, mood and pain, logging medication doses and appointments, and answering medication questions from official FDA labels. Keeps family members informed with household status updates and automatic alerts when a check-in or dose is missed.
    5 npm
    MIT
  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables families to coordinate caregiving through a self-hosted MCP server, tracking medication doses, daily check-ins, appointments, handoff notes, and family alerts via Alexa+ or any MCP host.
    15 npm
    MIT
  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables an older adult to talk with Alexa+ about medicines and daily routine while a caregiver sees a calm, consent-gated dashboard and push alerts only when a genuine pattern (e.g. an unusually late first activity) appears. Includes elicitation-based consent, emergency bypass, resumable SSE streams, and an inline MCP App view.
    MIT