Skip to main content
Glama

Guardian MCP

The Alexa+ MCP server for Guardian — an AI household safety agent that reasons about events, context, and policy before acting, rather than just forwarding alerts.

Architecture

Alexa+  ──(MCP tools/call)──►  src/server.ts  ──►  src/tools.ts  ──►  decide()  ──►  store
Ring   ──(EventBridge)────►  src/lambda/ringEventHandler.ts  ──►  decide()  ──►  store
Dashboard (browser)  ──(REST poll)──►  src/server.ts /api/*  ──►  store

Two independent entry points — the synchronous MCP path (Alexa+ voice turns) and the asynchronous event path (Ring → EventBridge → Lambda) — both call the exact same decide() function and read/write the exact same store. That's deliberate: the reasoning and the state are the product; MCP and EventBridge are just two doors into it.

Related MCP server: a207-router-mcp

What's in this repo

  • src/types/domain.ts — household/event/policy/incident data model

  • src/store/ — storage layer, swappable:

    • types.tsIGuardianStore interface everything else codes against

    • memoryStore.ts — in-memory, seeded, zero AWS dependency (default)

    • dynamoStore.ts — real DynamoDB implementation

    • index.ts — picks the backend via STORE_BACKEND env var

    • seedData.ts — the demo household, shared by both backends

  • src/decisionEngine.ts — rule-based decision engine (deterministic, no AWS), including pattern-of-events escalation (Scenario D: no single event is alarming, but a cluster of unusual events in a short window is)

  • src/bedrockDecisionEngine.ts — Bedrock-backed decision engine, with automatic fallback to the rule engine if the Bedrock call fails or times out

  • src/decide.ts — picks the engine via DECISION_ENGINE env var

  • src/policyCompiler.ts — Guardian Rules: turns a plain-language rule ("if Mom doesn't respond after two attempts, notify me") into a structured Policy via Bedrock — no rule-based fallback here, since guessing a wrong structured policy is worse than reporting it couldn't parse the rule

  • src/tools.ts — the 13 MCP tools Alexa+ calls

  • src/app.ts — the Express app (MCP endpoint, dashboard REST API, dashboard static files) — no listen() call, shared by local dev and Lambda

  • src/server.ts — local-dev entry point (app.listen())

  • src/lambda/mcpHandler.ts — wraps the same app for Lambda + API Gateway via serverless-http — this is what Alexa+ actually talks to once deployed

  • src/lambda/ringEventHandler.ts — EventBridge-triggered Lambda for the Ring ingestion path

  • src/scripts/seedDynamo.ts — populates DynamoDB tables with the demo household

  • public/index.html — the dashboard (house status, active incidents, Contact/Escalate buttons) — this is your Fire TV screen; cast the browser tab if you don't build a native app

  • template.yaml — AWS SAM: both Lambdas, all 6 DynamoDB tables, the EventBridge rule, least-privilege IAM, and a CloudWatch dashboard

  • addon-package/addon.json — the Alexa+ add-on manifest, built to Amazon's real MCP Toolkit schema, with placeholders for what only you can provide

  • src/__tests__/decisionEngine.test.ts — automated tests for all four demo scenarios (A/B/C/D) plus the fallback path and two regression guards for real bugs the tests themselves caught (see below)

  • src/__tests__/store.test.ts — regression test for a bug in addEvent's timestamp handling, found via live testing rather than the existing suite (see below)

  • .github/workflows/ci.yml — typecheck + build + test on every push/PR

  • infra/*.sh — raw AWS CLI fallback if you need to provision outside of SAM

Run it locally (zero AWS dependency)

npm install
npm run build
npm start
# Guardian MCP server listening on http://localhost:3000/mcp
  • Dashboard: open http://localhost:3000/

  • Health check: GET /health

This runs entirely on MemoryStore + the rule-based decision engine — no AWS credentials needed to develop against it.

Try the reasoning pipeline directly

curl -s -X POST http://localhost:3000/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{
    "jsonrpc": "2.0", "id": 1, "method": "tools/call",
    "params": {
      "name": "report_event",
      "arguments": { "source": "ring", "type": "person_detected", "location": "front_door" }
    }
  }'

This is the "unknown visitor at the door" scenario from the demo script. With the seeded household (Mary home and marked vulnerable), it returns a tier: "ask" decision asking whether Mary needs anything — Scenario C from the design doc — and the reasoning always includes "I won't unlock the door without your confirmation" no matter which tier resolves the event. No Alexa+ or real Ring device required to see it work.

Call initialize then tools/list first if you want the full tool catalog and schemas (any MCP client, or curl with an Accept: application/json, text/event-stream header, works).

Turning on Bedrock

export DECISION_ENGINE=bedrock
export AWS_REGION=us-east-1
export BEDROCK_MODEL_ID=anthropic.claude-3-5-sonnet-20241022-v2:0
npm start

If the Bedrock call fails for any reason (permissions, model not enabled in-region, network blip mid-demo), it automatically falls back to the rule engine and logs why — the demo doesn't crash, it just gets slightly less nuanced for that one decision.

Teaching Guardian a new rule (Guardian Rules)

curl -s -X POST http://localhost:3000/mcp -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \
  -d '{
    "jsonrpc": "2.0", "id": 1, "method": "tools/call",
    "params": {
      "name": "add_policy",
      "arguments": { "rule": "If someone knocks after 10pm, always wake me even if it seems minor" }
    }
  }'

Requires DECISION_ENGINE=bedrock (or just Bedrock credentials present — the compiler always uses Bedrock, there's no rule-based fallback for this one). Without credentials it fails loudly and explains why, rather than crashing or silently guessing.

Bugs and gaps found along the way

Worth documenting honestly rather than glossing over:

  1. The hard constraint was too broad. p-never-auto-unlock matched any person_detected event, so a scheduled visitor (e.g. the cleaner, arriving in their normal Monday window) would incorrectly trigger the same "I won't unlock the door" refusal as a genuinely unknown visitor — Scenario A from the design doc was never actually achievable. Fixed by adding an expected-visitor context check that runs first. (Found by writing tests.)

  2. A catch-all policy was silently matching everything. p-quiet-hours had an empty appliesTo, which the original matches() function treated as "matches any event" — meaning it won before the intended hardcoded default ever ran, making that default dead code. Fixed by requiring policies to specify at least one criterion to auto-match. (Found by writing tests.)

  3. Scenarios B and C didn't exist, and the "refusal" demo was tier-mislabeled. The original build only ever implemented Scenario A (expected visitor) and D (pattern escalation) — B (owner away → simple notify) and C (vulnerable member home → ask if assistance is needed) were never coded. Worse: the "AI refuses an unsafe action" demo moment was hardcoded to tier: "escalate" at 95% confidence, which doesn't match the design doc's own transcript for that exact situation ("I'll notify you and continue monitoring" — restrained, not urgent) and contradicts the Bedrock engine's own system prompt, which explicitly says to use escalate sparingly. Fixed by implementing checkVulnerableMemberHome (C) and checkOwnerAway (B) as real context checks, and folding the never-auto-unlock constraint into normal policy resolution so its tier follows context (inform/ask) while the refusal language itself stays constant across every tier. (Found by a direct question — "did we only have A and D, is there B and C" — not by the test suite. The lesson: passing tests only prove the code does what the tests assume it should; they don't catch a scenario that was never implemented or a demo that was confidently mislabeled.)

All three are now regression-tested in decisionEngine.test.ts.

  1. Adding the timestamp override itself introduced a new bug. tools.ts's { source, type, location, timestamp } object literal always creates an own timestamp property — undefined when the caller omits it, but present nonetheless. addEvent's object spread was ordered { timestamp: computedDefault, ...input }, so that explicit undefined silently overwrote the computed default — every event created without an explicit timestamp ended up with no timestamp at all, which broke Scenario D's pattern detection (it filters by timestamp). Caught by live-testing all four scenarios end to end after the B/C fix, not by the existing test suite — every existing test happened to always pass an explicit timestamp. Fixed by reordering the spread so id/timestamp are always assigned after the spread, and added a targeted regression test (store.test.ts) that reproduces the exact input shape tools.ts produces — verified to fail against the old code before confirming the fix.

Deploying (Lambda + API Gateway + DynamoDB + EventBridge)

Everything is wired in template.yaml (AWS SAM). One-time setup: install the SAM CLI.

npm run build
sam build
sam deploy --guided

sam deploy --guided will prompt for a stack name and region, then create:

  • McpFunction — the MCP server (Lambda + HTTP API), the public HTTPS endpoint Alexa+ talks to

  • RingEventFunction — the Ring ingestion Lambda, subscribed to an EventBridge rule matching source: "guardian.ring"

  • 6 DynamoDB tables (members, visitors, policies, events, incidents, config)

  • A CloudWatch dashboard (guardian-agent) with invocation/error/latency widgets — including p50/p99 latency against Alexa+'s 500ms requirement

  • IAM roles scoped to exactly what each function needs (DynamoDB CRUD on its own tables, bedrock:InvokeModel scoped to Anthropic models — not bedrock:*)

Start with DecisionEngine=rules (the default parameter) to confirm the whole pipeline works with zero Bedrock dependency, then redeploy with DecisionEngine=bedrock once you've confirmed model access:

sam deploy --parameter-overrides DecisionEngine=bedrock

After deploy, seed the tables (same demo household as local dev):

npm run seed:dynamo

The stack output McpEndpoint is the URL you need for the next step.

Alexa+ track compliance

The Devpost rules for the Alexa+ track require: "a working Agent Skill or a self-hosted MCP server, implementing MCP spec version (minimum acceptable version is 2025-11-25)." Guardian satisfies this on the MCP server branch — it negotiates 2025-11-25 correctly and is deployed at a public HTTPS endpoint. No additional registration step is required to meet the track requirement.

To verify spec compliance against the running server:

npx @modelcontextprotocol/inspector
# point it at https://h14vepqrzj.execute-api.us-east-1.amazonaws.com/mcp
# or http://localhost:3000/mcp for local dev

Optional — Alexa+ MCP Toolkit (Private Preview): if you have Private Preview access, addon-package/addon.json is already built to Amazon's real add-on schema with all URLs filled in. Register and deploy with:

npm install -g @alexa-ai/cli
alexa-ai configure                # LWA OAuth, one-time
cd addon-package && alexa-ai deploy

This is an enhancement on top of an already-compliant submission, not a requirement.

Wiring in real Ring events

The EventBridge rule and Lambda subscription are already created by sam deploy (see above) — nothing further to provision. Point your Ring webhook/poller at a small adapter that calls EventBridge PutEvents with Source: "guardian.ring", DetailType: "RingEvent", and a Detail of { eventType, location }. Keep Ring as the observation source only — all reasoning stays in the decision engine, per the product's core design principle (Ring produces observations, Guardian produces decisions).

For quick manual testing without a real Ring device:

aws events put-events --entries '[{
  "Source": "guardian.ring",
  "DetailType": "RingEvent",
  "Detail": "{\"eventType\":\"person_detected\",\"location\":\"front_door\"}"
}]'

infra/dynamodb-tables.sh and infra/eventbridge-rule.sh are kept as a raw-CLI fallback if you ever need to provision outside of SAM (e.g. debugging a single resource) — normal path is sam deploy.

Docs

  • SUBMISSION.md — judge-facing writeup: problem, differentiation, architecture, AWS usage

  • DEMO_SCRIPT.md — both demos, timed, mapped to actual tool calls

  • CONTRIBUTING.md — the extension points that make this reusable beyond Guardian specifically

  • FRICTION_LOG.md — template + one real entry, for the Open Source and general judging bonus

  • LICENSE — MIT

Store listing assets

addon-package/media/ has a generated icon set (all 6 required sizes) and a carousel image, styled to match the dashboard — real files, not placeholders, ready to upload to wherever you host static assets (S3+CloudFront, GitHub Pages, etc.). Regenerate them with python3 brand-assets/generate_icons.py and python3 brand-assets/generate_carousel.py if you want to tweak the look (both are plain PIL, no network dependency).

addon-package/legal/privacy-policy.md and terms-of-use.md are drafts scoped to exactly what this codebase actually collects and does — not generic boilerplate. Replace the bracketed placeholders, get them reviewed, host them, then point addon.json's privacyPolicyUrl / termsOfUseUrl at the hosted versions.

Build order status

  1. ✅ MCP server + rule-based decision engine (zero AWS deps, tested)

  2. ✅ Bedrock decision engine with automatic fallback

  3. ✅ DynamoDB store, swappable via STORE_BACKEND env var

  4. ✅ Ring → EventBridge → Lambda ingestion path

  5. ✅ Dashboard (Fire TV screen)

  6. ✅ Pattern-of-events escalation (Scenario D)

  7. ✅ NL policy compiler (Guardian Rules)

  8. ✅ SAM deployment (Lambda + API Gateway + DynamoDB + EventBridge + CloudWatch)

  9. addon.json built to Amazon's real MCP Toolkit schema

  10. ✅ Open-source packaging: LICENSE, CONTRIBUTING.md (real extension points, not boilerplate), FRICTION_LOG.md

  11. ✅ Demo script, timed and mapped to tested tool calls, with a backup-video plan

  12. ✅ Store listing assets (generated icon set + carousel image) and privacy policy / terms drafts scoped to what the code actually does

  13. ✅ Judge-facing submission writeup (SUBMISSION.md)

  14. You: record your demo video (MCP Inspector or curl against the deployed endpoint fully satisfies the track requirement — a real voice interaction via the bridge tool or Alexa+ Private Preview is a bonus, not a requirement), fill in the friction log and the AWS-feedback section of SUBMISSION.md as you go

Related MCP Connectors

Related MCP Servers

  • F
    license
    B
    quality
    B
    maintenance
    Provides deterministic intent routing and safety gating for multi-agent systems, enforcing P0 critical word detection, mutually exclusive hard rules (MX-1/2/3), and a permission matrix as pure functions via four MCP tools.
    4
    -
  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables Alexa+ agents to maintain auditable operational continuity across shifts by turning speech into verifiable state, persisting unresolved work, refusing unverified actions, and requiring human approval before executing and confirming high-risk tasks.
    MIT