Custodian
Serves as an Alexa+ add-on/MCP server, enabling hands-free voice control of a household inventory: adding owned items, checking CPSC/NHTSA/FDA recalls, getting household briefings, and tracking due maintenance, with recall cards, timelines and inventory views rendered on Echo Show screens.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Custodianis anything in my house recalled?"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Custodian
A household inventory that keeps watch.
Tell it what you own — a car seat, a stroller, the minivan, the smoke detectors — and Custodian continuously cross-references the public CPSC, NHTSA and FDA recall feeds, explains in plain language what is wrong and what the manufacturer will do about it, and keeps track of the small things that keep a home safe: smoke-detector batteries, water and air filters, car-seat expiry, vehicle service, warranties.
It is delivered as a self-hosted MCP server, so it works from any MCP host — Claude, an Echo Show running Alexa+, or a script — and it ships with an MCP App so hosts with a screen can show the recall notice, the affected models and a one-tap "I've handled it".
"I just bought a Graco 4Ever car seat."
→ Got it, I've added Graco 4Ever car seat as a car seat. I'll keep an eye on recalls and let you know if anything comes up.
"Is anything in my house recalled?"
→ Your Joolz Aer2 car seat adapter is affected by a recall: the adapters can fail to attach to the stroller,
which may allow the car seat to fall. The maker is offering a refund. There is one more on screen.
"Anything I should know about?"
→ Heads up: there's a new recall on your 2019 Honda Odyssey … Also, your Kidde smoke detector needs
new batteries, 71 days overdue, and your Graco 4Ever car seat expires in 19 days.Contents: Why · What it does · How it works · Design notes · Running it · Configuration · Testing · Deploying · Alexa+ · Project layout · Limitations · Data sources
Why it exists
Product recalls have a poor remedy rate, and not because people don't care: the notice arrives as an email from a brand you forgot you bought from, weeks after the fact, at an address you no longer read. Meanwhile the water filter is nine months old, the smoke-detector batteries date from last year, and the car seat expired quietly on a date printed under the cushion.
The assistant that is in the room when you strap a car seat in, or change a filter, is the one that should know about these things. Custodian gives that assistant a memory of what the household owns and a background process that keeps checking it — so the answer to "is anything in my house recalled?" is one sentence, spoken, with the remedy included.
Related MCP server: HomeOps MCP
What it does
Tools
Custodian exposes ten MCP tools. Each is one customer intent; the model (Claude, Alexa, …) decides when to call them.
Tool | You say | It does | Screen |
| "I just bought a …", "we have a 2019 Honda Odyssey" | Records the item, infers brand/category, creates default maintenance reminders, and schedules a recall sweep for it | — |
| "is my car seat recalled?", "any recalls?" | Precomputed recall matches for the whole household or one item, with severity and a plain-language remedy | recall cards |
| "tell me more", "who do I call?" | The full notice: hazard, remedy, affected models, contact, link | detail page |
| "I requested the refund", "we threw it out" | Marks a match as | — |
| "anything I should know?" | Everything new since the last briefing: new recall matches plus maintenance due in the next 30 days; marks recalls seen | briefing dashboard |
| "what needs doing at home?" | Overdue and upcoming maintenance within a horizon (default 30 days) | timeline |
| "I changed the smoke detector batteries" | Rolls an interval reminder forward (or clears a one-off one) | — |
| "remind me to descale the espresso machine every 3 months" | Adds a custom recurring or one-off reminder to an item | — |
| "what are you tracking?" | Items grouped by category with per-item recall badges | inventory |
| "we sold the stroller" | Stops tracking an item and its matches and reminders | — |
Every tool returns two things: one or two spoken sentences in content[0].text (no URLs, no identifiers — the model reads this aloud) and the full payload in structuredContent, validated against a declared outputSchema that the MCP App consumes. Ambiguity ("remove the car seat" when there are three) is handled by returning candidates so the assistant can ask, rather than by elicitation, which not every host supports. Failures are tool-execution errors with a spoken explanation, never an empty result.
Screens
One MCP App bundle renders five views, chosen from the calling tool: recall cards, a recall detail page, the household briefing, the maintenance timeline, and the inventory. Buttons on the screens call tools back (acknowledge_recall, log_maintenance, get_recall_details) and push a one-line note into the model's context ("On screen, the customer marked the recall … as remedy requested") so the next spoken turn is coherent.
Rendered with the reference MCP Apps basic-host against live recall data.
How it works
flowchart LR
subgraph Host["MCP host (Claude, Alexa+, …)"]
LLM[assistant] --> UI[MCP App]
end
LLM -- "tools/call over Streamable HTTP + bearer" --> S[Custodian MCP server]
UI -- "ui:// resource · callServerTool" --> S
S --> ST[(Store: JSON file or DynamoDB)]
SW[Sweeper: hourly + on add_item] --> CPSC & NHTSA & FDA
SW --> IDX[MiniSearch index · explainable matcher]
IDX --> ST
SW -. "ambiguous matches, item normalization" .-> BR[Claude on Bedrock · optional]
AS[OAuth 2.1 AS · /authorize /token /.well-known] --> SThe life of an item
You say "I just bought a Graco 4Ever car seat." The host calls
add_item. The handler normalises the name, infers the brand (Graco) and category (car_seat) from keyword tables, merges into an existing item if you already own one (quantity goes up), stores it, and answers — in a few milliseconds, without touching the network.After the answer is sent, an
onItemAddedhook runs: default maintenance reminders are created for the category (a car seat gets an expiry six years from its manufacture date), and the sweeper does a targeted sweep for just this item — a CPSC product search, an NHTSA lookup if it is a vehicle, an openFDA search if it is food, medicine or a device — then scores the item against every candidate recall and writesMatchrows. If Bedrock is enabled, the model first normalises the spoken description into a structured record (brand, model, aliases) so matching has more to work with.Every hour the full sweep runs: it pulls new CPSC records since its cursor (one year of backfill on first run), crawls another slice of NHTSA's child-seat catalogue, refreshes per-item lookups, re-scores every item in every household, and retires open matches that no longer qualify. It never touches matches the customer has acted on.
You ask "is anything recalled?"
check_recallsreads the precomputed matches from the store, sorts by severity then confidence, and speaks the top one with its remedy. The MCP App gets the same payload and renders the cards.
Matching
Matching is deterministic and every match carries a written reason. For a durable good:
Signal | Weight |
Item brand appears in the recall's keywords | 0.45 |
Item model (or a model name from the recall's product list) | 0.35 |
Share of the item's distinctive words found in the recall | up to 0.30 |
No brand or model agreement and weak word overlap | capped at 0.30 |
Recall category incompatible with the item's | × 0.6 |
Recall published more than a year before the item was bought | × 0.5 |
Vehicles: exact make + model + year against an NHTSA campaign | 1.0 (anything else is 0) |
Food and medication: each distinctive word in the FDA record | 0.55 for the first, +0.25 after |
A score of 0.5 records a match ("may affect yours"); 0.8 is spoken as a definite ("is affected"). Candidates come from a MiniSearch index over titles, products and keywords, so a household of a few dozen items scores against a corpus of thousands of recalls in milliseconds.
Recall keywords come from the record body (products, description, manufacturer, UPCs), and title words count only when the body corroborates them — because the CPSC feed contains at least one record whose title belongs to a different recall (see docs/platform-notes.md). When the optional Bedrock adjudicator is on, matches in the 0.5–0.8 band are shown to Claude with the item and the recall text; a "no" verdict drops the match, a "yes" promotes it, and the verdict is cached on the match so it is never re-asked.
Maintenance reminders
Adding an item creates category defaults; you can add your own with set_maintenance_reminder.
Category | Default reminders |
Smoke / CO detector | Replace batteries every 6 months; replace the unit 10 years after manufacture |
Water filter | Replace every 6 months |
HVAC / furnace filter | Replace every 90 days |
Car seat | Expires 6 years after the manufacture date |
Vehicle | Oil change and service every 6 months |
Heater | Inspect yearly before the heating season |
Appliance, electronics | Warranty ends one year after purchase (when a purchase date is known) |
Interval reminders computed from an old purchase date start from today rather than beginning life "663 days overdue". Logging a task rolls it forward by its interval; one-off reminders (expiry, warranty) are cleared.
Authentication
Custodian includes a small OAuth 2.1 authorization server rather than delegating to a third party, because voice assistants need a specific shape that generic MCP auth libraries do not produce. There are two tiers of access:
Grant | Scope | Used for | Token |
|
|
| 1 h access token, no refresh token |
|
|
| 1 h access token + rotating 180-day refresh |
The code flow shows a login page where a household picks a name and passphrase (created on first use, scrypt-hashed; the name becomes the household id). Access tokens are ES256 JWTs with iss = PUBLIC_URL, aud = PUBLIC_URL/mcp and sub = <household>; the bearer middleware maps the scope to the JSON-RPC method (tools/call needs mcp:tools) and rejects with a JSON 401/403 — deliberately without a WWW-Authenticate challenge, which some assistant clients cannot process. Discovery documents live at /.well-known/oauth-authorization-server, /.well-known/oauth-protected-resource and /.well-known/jwks.json. AUTH_MODE=dev additionally accepts a static token so local tooling can skip the flow.
Protocol details
Built on the MCP TypeScript SDK v2. Requests are served statelessly — a fresh
McpServerper request — so both the 2025-erainitializehandshake (2025-03-26through2025-11-25) and the 2026-07-28 envelope work from the same code.Tools declare
outputSchema, behaviourannotations(readOnlyHint,destructiveHint,idempotentHint) and inline SVGicons.Screen-worthy tools carry
_meta.ui.resourceUripointing at a content-hashedui://custodian/app-<hash>.htmlresource served withmimeType: text/html;profile=mcp-appand a CSP that allows images only from the recall feeds' hosts. When no bundle is built the same tools run voice-only.Requests with a browser
Originthat is not the configured public host are refused with403./metrics.jsonexposes per-tool p50/p95/max latency and error counts. Locally every tool answers in well under 10 ms.
Storage
A Store interface with two implementations, both covered by the same contract tests:
FileStore — an in-memory map persisted to one JSON file with debounced atomic writes. The default for development;
:memory:for tests.DynamoStore — a single-table design (
PK/SK):H#<household>partitions hold the household record, items, matches and reminders; a sharedRECALLpartition holds the corpus;AUTHrows carry a DynamoDB TTL so codes, refresh tokens and pending logins expire on their own.
Design notes
Nothing slow on the voice path. Alexa+ allows 500 ms per tool call. No tool handler performs network I/O or calls a model; all of that lives in the sweeper, hourly and immediately after
add_itemreturns.Speech and screen are different payloads.
contentis what gets read aloud;structuredContentis what gets drawn. Neither is derived from the other at render time.Explain every match. A written reason per match beats a better black-box score when a family is deciding whether to stop using a crib.
Hardened against the feeds. Normalisers are null-safe, keywords are body-corroborated, a normaliser version stamp resets the persisted corpus when the rules change, and stale open matches are retired on the next sweep.
Household, not user. Inventory is shared by everyone in the home, which is how a kitchen assistant is actually used.
Running it
Requirements: Node 22+ (24 recommended), npm 10+. No API keys — the recall feeds are public.
npm install
npm run build:ui # builds the MCP App bundle (packages/ui/dist/app.html)
cp .env.example .env # AUTH_MODE=dev, JSON file store
npm run dev # http://localhost:3001/mcp (restarts on change)Then, in another terminal:
npm run seed # adds a realistic inventory; real recalls appear within seconds
npm run smoke # a scripted, voice-shaped conversation with latency numbers
npm run oauth:conformance # the account-linking checks against a running server
npm run check # typecheck · lint · format · tests · UI build (what CI runs)Connecting a host
Claude Desktop / claude.ai — expose the server (
npx cloudflared tunnel --url http://localhost:3001), setPUBLIC_URLto the tunnel origin andAUTH_MODE=oauth, and add<tunnel>/mcpas a custom connector. Claude walks the OAuth flow; the login page creates your household on first use.MCP Apps basic-host (renders the screens) — the reference host cannot send credentials, so run
npx tsx scripts/dev-proxy.ts, which adds the dev bearer on the way through, then in ext-apps/examples/basic-host:SERVERS='["http://localhost:3099/mcp"]' npm startand open http://localhost:8080.Any MCP client —
POST /mcpwithAuthorization: Bearer dev-token(or an OAuth access token) and standard JSON-RPC.
HTTP endpoints
Path | Purpose |
| MCP Streamable HTTP endpoint (bearer required) |
| Liveness; per-tool latency |
| OAuth 2.1 authorization server |
| Authorization-server metadata, protected-resource metadata, JWKS |
| Policy pages (linked from the Alexa+ listing) |
Configuration
Everything is an environment variable (.env is read in development).
Variable | Default | Meaning |
|
| Listen port |
|
| Public origin: OAuth issuer, JWT audience ( |
|
|
|
|
| File store location ( |
|
| DynamoDB store |
|
|
|
|
| Static token for local tooling (dev mode only) |
|
| The single pre-registered confidential client |
| — | Comma-separated allow-list of redirect URIs |
| generated | PEM ES256 key; generated and persisted in the store when unset |
|
| Full-sweep schedule; whether to sweep at start-up |
|
| Optional Claude-on-Bedrock enrichment (needs AWS credentials) |
|
| Where the built MCP App bundle is read from |
Testing
npm test runs the vitest suite against recorded fixtures from the live feeds, so it is deterministic and offline:
source normalisers for CPSC, NHTSA (vehicles and child seats) and openFDA, including the mis-titled CPSC record
the matcher's precision cases (true positives, near-misses, generic-word traps, purchase-date recency)
the sweeper with fake sources: incremental pulls, sliced crawling, targeted sweeps, retiring stale matches, Bedrock adjudication with a fake client
the OAuth server end to end: discovery shape, both grants, PKCE failure, single-use codes, refresh rotation, scope gating
every tool through the real HTTP stack, including spoken phrasing and the latency budget
the MCP Apps wiring (resource, CSP,
_meta.ui) and theStorecontract against the file store and DynamoDB (via dynalite)
CI runs typecheck (including the tests), ESLint, Prettier, the suite, the UI build and a CDK synth on every push.
Deploying to AWS
infra/ is an AWS CDK stack: one App Runner service (built from the repo's Dockerfile, HTTPS out of the box), a DynamoDB table, a generated OAuth client secret in Secrets Manager, and an instance role with DynamoDB and Bedrock access.
npm run infra:deploy # prints ServiceUrl, McpEndpoint, TableName, OAuthClientSecretArn
npm run infra:deploy -- -c publicUrl=https://<ServiceUrl> -c redirectUris=<comma-separated redirect URIs>The second deploy pins PUBLIC_URL (issuer and audience) once the App Runner domain exists. The stack runs with AUTH_MODE=oauth, STORE=dynamo and BEDROCK_ENABLED=true. One always-on instance keeps the hourly sweep alive.
Alexa+ integration
Custodian was designed as an Alexa+ add-on (the Alexa+ MCP Toolkit connects an MCP server to Alexa devices). The requirements that shaped it — Streamable HTTP, MCP 2025-11-25, the two-tier OAuth model, a 500 ms tool budget, and MCP Apps for Echo Show — are all met by this server; the add-on manifest and the deploy sequence are in addon-package/, and docs/platform-notes.md records what was not obvious from the toolkit's documentation. Deploying the add-on itself requires an Amazon developer account and the toolkit's CLI.
Project layout
packages/shared/src
schemas.ts zod schemas: Item, RecallRecord, Match, MaintenanceRule, Household, and every tool's output
text.ts, dates.ts tokenising, speech helpers, calendar arithmetic
packages/server/src
index.ts boot: config → store → auth runtime → sweeper → HTTP
http/app.ts Express app, /mcp handler, origin guard
auth/ OAuth 2.1 AS (oauth.ts), bearer middleware, JWT keys, login/policy pages
mcp/ McpServer factory, defineTool wrapper, tool implementations (tools/*.ts), views, icons
sources/ CPSC, NHTSA vehicle, NHTSA child seat, openFDA normalisers
match/ MiniSearch index and the scoring function
jobs/sweep.ts the background sweeper
maintenance/ default reminder rules and scheduling
enrich/ category/brand inference; optional Bedrock enricher
store/ Store interface, FileStore, DynamoStore
ui.ts loads the built MCP App bundle
packages/server/test tests + recorded fixtures
packages/ui/src Preact MCP App: host bridge, views, styles
infra CDK stack (App Runner, DynamoDB, Secrets Manager, Bedrock IAM)
scripts smoke, oauth-conformance, seed, gen-assets, dev-proxy, setup.sh
addon-package Alexa+ add-on manifest and deploy notes
docs platform notes, screenshotsLimitations and known gaps
No proactive notifications. The sweeper finds new recalls in the background, but you hear about them on your next briefing; MCP has no channel for a server to start a conversation.
Household names are a global namespace. Two homes that pick the same name share a login (protected by the passphrase). A real deployment would link to an identity provider.
Single-instance sweeper. The hourly job runs in the web process; scaling out would need a lock or a separate worker.
US recall feeds only, and matching is tuned for English product names.
Recall data can be delayed, incomplete or wrong at the source. Custodian is a prompt to check, not a substitute for the manufacturer's notice.
Data sources
Public, keyless, and attributed on every match: CPSC recalls, NHTSA recalls (vehicle campaigns and the child-seat catalogue), and openFDA enforcement reports for food, drugs and devices. Always confirm with the manufacturer or the issuing agency before acting.
This server cannot be deployed
Maintenance
Related MCP Connectors
Is it recalled or fake? Recall lookup vs live US gov feeds + transparent counterfeit risk signal.
Household-aware cooking brain: pantry, meal suggestions, dietary safety, recipes, shopping lists.
- DomusOAuthapp.getdomus
Homeowner's AI: view & update appliances, warranties, documents & maintenance for your home.
Check US recalls for a car, a baby product, a food or a medicine in the official recall databases.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceEnables AI-powered household management including inventory tracking, restock predictions, meal planning from available ingredients, and baby supply monitoring through natural language commands.Apache 2.0
- AlicenseNot gradedqualityBmaintenanceEnables voice-first household maintenance tracking, turning spoken updates into durable records, service history, and due-date reminders for a concise maintenance brief.MIT
- AlicenseNot gradedqualityCmaintenanceEnables voice-first shared object memory for Alexa+, letting users record where items were last reported, retrieve authorized locations, correct stale records, check shared items in and out, run guided Lost Mode searches, and manage privacy-aware access.MIT
- AlicenseNot gradedqualityBmaintenanceEnables households to manage appliance inventories, manuals, warranties, and maintenance through natural-language voice interactions, with grounded troubleshooting and consumable reordering.MIT