latchkey
Provides integration with Ring cameras, learning recurring activity patterns from Ring event history and alerting when an expected event does not occur.
Click on "Install 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., "@latchkeyAlert me if the front door camera doesn't detect motion by 8am"
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.
Latchkey
Every camera app is event-driven, so none of them can tell you about the event that did not happen. A webhook fires when motion is detected. Nothing fires when a person who leaves the house every weekday morning does not leave.
Latchkey learns each recurring pattern from Ring's event history, derives a time window with a deadline, and asks one decidable question when the deadline passes: did a matching event land inside it? If it did, the event is absorbed and never shown. If it did not, and the camera was demonstrably up, one card reaches a human.
On the demo household that ratio is:
183 events observed · 183 absorbed · 4 expectations due · 1 escalatedThe escalation is not one of the 183. It is the arrival that never came. That is the whole product, and it is the one thing a camera feed structurally cannot produce.
A card reads:
Front Door · motion.human · weekdays 07:53–08:33 · seen 20 of last 20 weekdays
· nothing by 08:33 · last event 20:08 (12h 25m ago)Every number on it is checkable. Opening the evidence drawer shows the event ids behind the support count, the exact window in epoch milliseconds, and the paths of the Ring documents that define each field the claim rests on — fetched live from Ring's own documentation server while you look at them. No card ever says "AI detected".
Run it
No Ring account. No token. No network. No install step.
node --version # 22.18 or newer, for native TypeScript
npm startOpen http://127.0.0.1:8787. A 28-day household loads from fixtures, six expectations are learned, and the incident day replays in about eighteen seconds — one full day of synthetic Ring notifications, each one HMAC-signed and POSTed over real HTTP to the real webhook route. Watch 08:33 pass.
npm test # 85 tests, node:test, no framework
npm run doctor # check our assumptions against Ring's live documentation
npm run fixtures # regenerate the household from its seednpm run doctor is the only command that needs a network connection. It reaches
Ring's MCP knowledge server, which needs no credentials, and fails if any field
this codebase parses has stopped being documented.
Related MCP server: OBSBOT Camera MCP Server
What was exercised against what
This project was built without a Ring Playground token. Obtaining one requires an interactive browser sign-in, and the build was unattended. Rather than pretend otherwise, here is the exact state of every endpoint.
Endpoint | Shape taken from | Exercised against fixtures | Exercised against live Ring |
|
| yes | no |
|
| yes | no |
|
| yes, including pagination and filters | no |
|
| yes, 303 and 416 | no |
|
| yes, including HEVC rejection | no |
| same | yes | no |
Webhook |
| yes — real HMAC over real HTTP | no |
Ring MCP knowledge server | first-party, no credentials | n/a | yes, on every |
Amazon Bedrock | AWS SigV4 documentation | no | no — no AWS account |
Two rows are worth reading twice.
The webhook row is not fixtures. Offline mode signs its synthetic notifications with a genuine HMAC-SHA256 over the exact bytes on the wire and POSTs them to the same route Ring would. The receiver verifies against the raw body before parsing. Forged and re-serialised payloads are rejected with 401. The one security-critical path in this project is never bypassed, not even offline.
The api.amazonvision.com rows have been reached, but never authenticated.
Running the server with an invalid RING_ACCESS_TOKEN sends the real
GET /v1/devices?include=status,capabilities over the network and Ring answers
401. That proves the host, the path and the transport are real and that this
client is the code that talks to them. It proves nothing about the response
bodies, which is why every cell in the live column still says no.
The MCP row is live, first-party Ring, from any machine. npm run doctor
opens a real connection to Ring infrastructure with no account, no token and no
device, and checks forty-five documented field names across seven documents against what this code
parses.
When someone does sign in, this flips itself:
RING_ACCESS_TOKEN=... npm run verify:liveThat replays the same assertions against api.amazonvision.com and writes a
dated report into verification/. The table above is updated from that file, not
from memory. Until then every "live" column says no.
How it works
src/ring/client.ts Partner API client. Real URLs, headers, pagination,
redirect policy, JSON:API parsing, error mapping.
Takes a `transport`; defaults to global fetch.
src/ring/fixtures.ts The offline wire. Speaks HTTP, reproduces the traps.
src/ring/subtype.ts Recovers the motion subtype history does not return.
src/ring/whep.ts SDP: H.265 injection, recvonly validation.
src/webhook.ts HMAC verification over raw bytes; idempotency.
src/expect/learn.ts Event stream → expectations. The claim.
src/expect/watch.ts Deadlines. satisfied / missed / unverifiable.
src/decide.ts Absorb or escalate, with the evidence bundle.
src/phrase.ts Deterministic sentence; Bedrock may re-render it.
src/mcp.ts Ring's documentation server, over JSON-RPC.
src/doctor.ts Spec-drift canary.
src/app.ts Boot, learn, arm, ingest, tick.
src/server.ts node:http. Console, SSE, /webhooks/ring, WHEP, media.
web/index.html One page.Node 22, TypeScript run natively. Zero runtime dependencies — no framework,
no HTTP library, no AWS SDK, no build step, no node_modules.
Offline mode is a transport swap, not a mode flag
const client = new RingClient({
token: LIVE ? TOKEN : 'offline-fixture-transport',
transport: LIVE ? undefined : createFixtureTransport({ now: () => clock.ms }),
});That is the entire difference between the two modes, and it is the only place in
the process that branches on it. undefined means global fetch. Request
construction, pagination, redirect handling, error mapping and parsing are the
same code either way, so a bug in that code is a bug in both.
The fixtures reproduce the traps, not the happy path
A fixture that only returns success proves nothing. src/ring/fixtures.ts
reproduces, deliberately:
links.nexton every history page including the last, and including empty ones — the documented infinite-loop trap;links.nextcarrying onlypage[key], so a filtered query silently widens on page two unless the client re-applies its filter (it does);history responses that withhold the motion subtype, which is why
src/ring/subtype.tsexists;416 MEDIA_NOT_FOUNDwhen no media exists at an instant;400 UNSUPPORTED_HEVC_SDP_OFFERfrom an HEVC-only camera given an H.264 offer;503from a camera that is offline.
Everything in fixtures/ is synthesised from a seed by scripts/gen-fixtures.ts
and carries a _provenance block saying so. Nothing was recorded from a Ring
account. No real Ring response is reproduced anywhere in this repository.
The learner
Grouped by (device, event type, motion subtype). Three day granularities are
tried — every day, weekdays/weekends, each specific weekday — because real
households live at all three. Within a group the pooled minutes-past-midnight are
split wherever there is a gap wider than 90 minutes, so the 08:15 departure and
the 11:40 mail carrier on the same camera do not collapse into one bimodal blob
whose median is 10:00, a time nothing ever happens.
A candidate must survive four rules: at least 4 supporting days, a hit rate of at least 0.8, a mode no wider than 135 minutes, and a fitted window no wider than 90 minutes. Windows are centred on the median and sized by the MAD, so one morning she left at 06:40 does not drag the claim with it.
The mode-width rule is the one that stops a busy camera. Bounding only the fitted window is not enough: on a camera that sees something every hour, the 90-minute split never fires and the whole day becomes one mode, so the per-day sample degenerates into "the earliest arrival of the day" — an order statistic far tighter than the traffic underneath it. The MAD then collapses and pure street noise is claimed as a routine. Bounding the mode is what catches that.
Support is then recounted inside the fitted window and both gates re-applied, because "seen 20 of last 20 weekdays" is a claim about the window a reader is looking at. Counted across the mode instead, twenty 08:00 departures and eight 09:30 ones read as 28 of 28 for a window that only ever held twenty.
Overlapping survivors are resolved in favour of the more general reading, and the 0.8 threshold does the discriminating: a weekday habit scores 5/7 = 0.71 as a daily one and is rejected, while a genuinely daily habit absorbs its own weekday and per-weekday readings instead of shattering into seven patterns.
The watcher has three answers, not two
satisfied | Something matched. An arrival before the window but inside a bounded grace lookback counts, flagged |
missed | The window closed empty and the camera was up the whole time. The only outcome that reaches a human. |
unverifiable | The camera was offline for any part of the window. We cannot distinguish "she did not leave" from "we were not watching", so we say so. A five-minute blind spot is enough to hide a departure. |
A pattern that misses three days running is retired rather than escalated again. Three days of silence is a changed routine, not three emergencies, and repeating the same alarm daily is how a monitoring product teaches people to ignore it.
The demo day exercises all three: the front door departure is missed, the back door camera drops off the network across its own window and comes back unverifiable, and the mail carrier and dog walker are absorbed.
Time is an argument
There are no timers in the decision path. watcher.tick(nowMs) resolves every
deadline that has passed, which is the same call a production loop would make on
an interval. That is what lets a month of household routine and a fourteen-hour
absence be replayed in eighteen seconds — and what makes the boundary cases in
tests/watch.test.ts testable at all.
Tests
85, node:test, no framework, no mocking library. They are weighted towards the
decision logic, because that is the part that is more than a camera feed. The
adversarial cases are the point: an arrival one millisecond past the deadline, a
late arrival that must not retroactively unfire an alert, a camera blind for five
minutes of a thirty-minute window, a retried webhook, a routine that has moved
rather than stopped, a re-serialised payload whose signature must fail.
tests/e2e.test.ts runs the real server, replays a full day over real HTTP with
real signatures, and asserts that exactly one thing reaches a human.
Amazon Bedrock
LATCHKEY_BEDROCK=1 lets Bedrock re-word an already-decided card into one plain
sentence. The model renders, it never decides. The decision, the evidence and
every number are produced by src/decide.ts before the call is made, and if the
call is slow, unconfigured or wrong, the deterministic template is what ships.
Offline mode is unaffected.
The scope is deliberately tiny and deliberately the wrong shape. The obvious thing to do with a model here is have it read the events and judge whether something is wrong — which would make every card unauditable. A caregiver cannot check a hunch, and neither can a judge.
SigV4 is signed by hand in src/aws/sigv4.ts (about forty lines of node:crypto)
rather than pulling in an SDK for one POST. This path has never made a
successful call — no AWS account was available. What is verified without one is
the key-derivation chain, which matches the expected value AWS publishes in its
own Signature Version 4 documentation; see tests/aws.test.ts. The request
assembly and response parsing around it are unverified, and one known
uncertainty is flagged in the code: path segments are encoded once rather than
twice.
Product feedback
Required by the hackathon, and written properly in FRICTION_LOG.md — fourteen entries with reproductions, workarounds and suggested fixes. The summary:
Ring Partner API (event history, device discovery, image download, WHEP)
Used for. Everything. Event history is the learner's entire input; device discovery gates streaming and supplies codecs; image download corroborates a missed-event card; WHEP opens live view from that card.
Worked. The documentation is unusually complete — request and response
examples for every endpoint, error codes with their exact strings, and working
code in three languages. The links.next infinite-loop trap is documented,
which most vendors would leave you to discover. The HEVC workaround ships with
the JavaScript to implement it. JSON:API compound documents are consistent and
easy to join.
Needs work. (1) Event history returns event_type but never the motion
subtype, which exists only as a query filter — recovering it costs n+1 requests
per device. (2) subType on webhooks is camelCase, sits beside attributes
rather than inside it, and drops the motion. prefix the history filter
requires: three inconsistencies in one field, all of which fail silently. (3) The
documented links.next carries only page[key], so a filtered query appears to
work on page one and widens on page two. (4) Device capabilities do not publish a
power source, which is the attribute that decides whether a WHEP session lasts 30
seconds or 60. (5) 416 is the wrong status for "no media exists here", which is
often a useful answer rather than an error.
Onboarding. Reading the documentation: excellent, maybe two hours to a confident mental model. Making a single request: impossible for this build. Every route to a token, Playground included, ends at an interactive browser sign-in. There is no device-code flow, no read-only sandbox key, and no archive of sample responses. The last of those would cost Ring nothing and would have removed most of the uncertainty in this repository.
Would I build with it again. Yes, for anything observational. It is read-only for cameras — no arm, no siren, no talk, no snapshot trigger — and that constraint is spread across the endpoint reference rather than stated once near the top, which cost a day of design work on ideas that were dead on arrival. Say it in the first paragraph and every partner designs better.
Ring Appstore MCP knowledge server
Used for. npm run doctor — checking our assumed field names against Ring's
live specification — and the live doc citations in the console's evidence drawer.
Worked. It needs no credentials, which is the single most useful property
any part of this platform has for an unattended build. Plain streamable HTTP,
initialize → tools/list → tools/call in about sixty lines with no SDK.
ring___get_doc returns whole documents, not snippets. It caught a genuine error
in my own assumptions on its first run.
Needs work. initialize advertises resources and prompts capabilities
that return nothing useful. search_docs types max_results and
min_confidence as strings when they are numbers, which a strict MCP client will
reject. Search relevance is uneven — a webhook-signature query ranks a deployment
architecture document above the notifications document that defines it.
Onboarding. Ten minutes with curl. The best onboarding experience in the
stack, and it is barely mentioned in the developer documentation.
Would I build with it again. Yes, and this is the recommendation I would make loudest: link it from the first page. A credential-free documentation endpoint that an agent can query at runtime is a genuinely new thing and Ring is burying it.
Amazon Bedrock
Used for. Optional one-sentence rendering of an already-decided card.
Worked. Nothing — see above. No AWS account was available and the path has never run.
Needs work. The one piece of friction I can report honestly is documentation
rather than product: the SigV4 specification says non-S3 services double-encode
path segments, while the SDKs single-encode for bedrock-runtime. Bedrock model
ids contain a colon, so the two disagree, and there is no authoritative statement
of which is right for this service.
Would I build with it again. Unknown, and I am not going to pretend otherwise.
Node 22 native TypeScript, node:test, node:crypto, node:http
Used for. All of it. Zero runtime dependencies.
Worked. Type stripping meant no build step and no dist/. node:test is
fast and its TAP output is readable. node:crypto covers HMAC, SigV4 and the
CRC32/zlib needed to synthesise a PNG frame in the fixture transport.
Needs work. Type stripping rejects parameter properties
(constructor(private x: T)) at import time rather than at type-check time, so a
small refactor surfaces as a runtime crash in an unrelated module. node --test
given a bare directory tries to import the directory and fails with
MODULE_NOT_FOUND, pointing nowhere near the cause.
Would I build with it again. Yes, without hesitation, for anything this size.
What this is not
It is not a security system. It observes and infers; the Partner API is read-only for cameras and Latchkey cannot arm, disarm, sound, or speak.
It is not a medical or emergency service. A missed expectation means an expected event did not occur, and nothing more than that.
It does not do video analysis. Every claim on every card comes from event timestamps and event types.
It will not find a routine buried in a camera's own background traffic. Where arrivals never pause for 90 minutes there is no separable mode to fit, and the learner declines rather than guesses. A doorbell or a door camera has the quiet gaps; a camera pointed at a public street does not.
The households in
fixtures/are synthetic. No real person's routine, and no real Ring account, is represented anywhere in this repository.
Configuration
Variable | Default | Effect |
|
| HTTP port |
| unset | When set, every request goes to |
| random per process | Ring's HMAC signing key. Generated and never persisted when unset. |
|
| Wall-clock duration of the one-day replay. |
| unset |
|
No secret is committed to this repository, and none is written to disk at runtime.
Licence
MIT. See LICENSE.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections. Dates show when Glama detected each change.
No tool schema history has been recorded yet.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Connectors
Bzigo smart trap: device status, detections, health, firmware and simulations via AI.
Connect your Oura Ring account and enable access to your wellness data in apps and automations. In…
Monitor MCP servers, API contracts and AI outputs for schema drift. Alerts on breaking changes.
Monitor public pages and JSON endpoints after agents disconnect, with polling and webhooks.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceEnables AI assistants to access and control network cameras to capture images and perform analysis including brightness detection, color distribution, and edge detection.MIT
- AlicenseBqualityDmaintenanceEnables PTZ camera control with gimbal positioning, snapshots, and AI visual analysis for OBSBOT and UVC cameras. Supports autonomous scanning patterns and integrates with vision-language models for real-time camera analysis.71MIT
- FlicenseNot gradedqualityDmaintenanceEnables control of Ring home security devices, including doorbells, cameras, lights, and alarm systems, through MCP-compatible clients like Claude Desktop.4-
- AlicenseAqualityBmaintenanceEnables MCP clients to control Bosch Smart Home Cameras via natural language, including snapshots, motion events, privacy mode, and pan/tilt, using a reverse-engineered cloud API.70MIT
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/dud8/latchkey'
If you have feedback or need assistance with the MCP directory API, please join our Discord server