Physical-device control safety gate — never blindly toggle cloud devices; check state, surface risk, require explicit go-ahead before risky actions.
Browser
simplisafe-mcp
Try itQuery and control a SimpliSafe alarm system from the shell with curl — read system state, sensors, locks, events and settings, and arm/disarm or lock/unlock. Use when the user asks about their SimpliSafe alarm, house sensors, door locks, or whether the system is armed. Requires a one-time browser login to mint a refresh token.
What it does
Query and control a SimpliSafe alarm system from the shell with curl — read system state, sensors, locks, events and settings, and arm/disarm or lock/unlock. Use when the user asks about their SimpliSafe alarm, house sensors, door locks, or whether the system is armed. Requires a one-time browser login to mint a refresh token.
The skill document
SimpliSafe from the shell
SimpliSafe's API is reachable server-side — no browser bridge, no extension,
no signed-in tab. Auth is a bearer token minted from a long-lived OAuth refresh
token, so everything here is plain curl + jq.
Requires curl and jq.
One-time setup
SimpliSafe issues no API keys. The credential is an OAuth refresh token, minted by a browser login you do once:
node ~/git/simplisafe-mcp/scripts/bootstrap-auth.mjs # prints an authorize URL
# sign in, then copy the com.simplisafe.mobile://... URL the browser fails to open
node ~/git/simplisafe-mcp/scripts/bootstrap-auth.mjs ""
That writes SIMPLISAFE_REFRESH_TOKEN to ~/git/simplisafe-mcp/.env (mode 0600).
SimpliSafe does not rotate refresh tokens, so this survives indefinitely —
until you sign out of all devices in the SimpliSafe app, which revokes it.
To capture the code from the browser: open DevTools → Network → tick Preserve
log before signing in, then find the failed navigation to
com.simplisafe.mobile://…?code=… and copy its link address. The code is
single-use and expires in ~2 minutes.
Core pattern
source ~/git/simplisafe-mcp/skills/simplisafe-api/references/ss-helpers.sh
ss_api GET /api/authCheck | jq # who am I
SID=$(ss_sid) # the single active system id
ss_api GET "/ss3/subscriptions/$SID/sensors?forceUpdate=false" | jq
ss_api [JSON_BODY] attaches auth, prints the body on stdout,
and on a non-2xx prints to stderr and returns 1 — so a failure never looks
like an empty result. Access tokens are cached in $TMPDIR (0600) and re-minted
only when stale; the helpers never write to the repo's .env.
Resolve first
Almost every path needs a system id (sid), and getting one takes two calls
(authCheck → userId, then the subscriptions route). ss_sid does both, and
fails loudly if the account has more than one system rather than guessing —
pass the sid explicitly in that case.
Note two routing traps:
- The system version that decides
ss3/routing is atlocation.system.version, not the top-levelsystemVersion(a different number entirely). - Events and doorlock control are NOT under the
ss3/prefix. Everything else is.
Reads
# Is it armed? -> OFF | HOME | AWAY | HOME_COUNT | AWAY_COUNT | ALARM
ss_api GET "/users/$(ss_api GET /api/authCheck | jq -r .userId)/subscriptions?activeOnly=true" \
| jq -r '.subscriptions[0].location.system.alarmState'
# Anything wrong with a sensor?
ss_api GET "/ss3/subscriptions/$SID/sensors?forceUpdate=false" \
| jq '[.sensors[] | select(.flags.offline or .flags.lowBattery) | {name, type, flags}]'
# Recent activity
ss_api GET "/subscriptions/$SID/events?numEvents=10" \
| jq -r '.events[] | "\(.eventTimestamp|todate) \(.info)"'
More recipes, including lock state and base-station health: references/recipes.md.
Writes — read this before running one
These commands act on a physical security system. Disarming leaves a house unmonitored; arming can trip a siren and a monitoring-center dispatch; unlocking opens a real door. Confirm intent with the user before running any of them, and never run one speculatively.
ss_api POST "/ss3/subscriptions/$SID/state/away" # or /home, /off — no body
ss_api POST "/doorlock/$SID//state" '{"state":"lock"}' # or "unlock"
A 2xx is not proof it worked. Re-read and check one field:
sleep 3
ss_api GET "/users/$(ss_api GET /api/authCheck | jq -r .userId)/subscriptions?activeOnly=true" \
| jq -r '.subscriptions[0].location.system.alarmState'
Four things that make naive verification lie:
- Arming reports
AWAY_COUNT/HOME_COUNTwhile the exit delay runs, settling toAWAY/HOME. That is success in progress, not failure. - Do not diff whole objects —
alarmStateTimestamp,stateUpdatedandlastUpdatedadvance on their own, so any call would look successful. - Re-read locks with
forceUpdate=true. The cached payload lags by minutes and has been seen reporting a jammed lock as cleanlyunlocked. - A lock takes ~5 s to report, so a 3-second check calls a successful unlock a failure. Poll, don't sleep once.
Arming also LOCKS your doors. Each lock's setting has home/away flags
that auto-lock on arm, and homeToOff/awayToOff controlling whether disarming
unlocks them. With homeToOff: 0 (common), arm-then-disarm leaves the doors
locked — so it is not a safe way to test arming. Check with:
ss_api GET "/ss3/subscriptions/$SID/sensors?forceUpdate=false" \
| jq -r '.sensors[] | select(.type==16) | "\(.name)\t\(.setting)"'
A lock reporting lockJamState: 1 will not respond to commands at all: the POST
still returns 200, and nothing moves. That is a hardware fault, not a bad
request.
PINs are cleartext
/ss3/subscriptions/$SID/settings/normal returns settings.pins — the master,
duress and named-user alarm codes, in cleartext, in the same payload as the
harmless settings. Project .settings.normal unless the user explicitly wants
the codes:
ss_api GET "/ss3/subscriptions/$SID/settings/normal?forceUpdate=false" | jq '.settings.normal'
Notes
forceUpdate=truemakes the base station re-poll its devices — slower and harder on the hardware. Leave itfalseunless freshness matters.- Lock state encoding is counter-intuitive:
lockState1 = locked, 2 = unlocked, andlockJamStateoverrides both. - Device type ids
21,23and24appear on real systems but aren't in any public enum — handle unknown types gracefully. - Avoid tight polling; the API rate-limits.
Full verified endpoint reference: ~/git/simplisafe-mcp/docs/SIMPLISAFE-API.md.
Related skills
Yingmi MCP (qieman.com). Use this skill for ANY Yingmi MCP request — searching and reading data. Whenever a task involves Yingmi MCP, use this skill instead of calling the API directly.
Query and command a Kia vehicle directly with curl against the Kia Owners API (api.owners.kia.com), without running the MCP server. Use when the user wants a one-off read of their Kia's status, location, or EV charge state, or to lock/unlock/start climate from the shell — "check my Kia", "is the car locked", "what's the EV9 charge", "lock the car from the terminal". Requires KIA_USERNAME/KIA_PASSWORD and a one-time SMS/email MFA bootstrap.
Read and manage your Skylight Calendar family hub — calendar events, chores and reward stars, shared lists (grocery/to-do), and meal plans. Triggers on phrases like "check Skylight", "what's on the family calendar", "add an event to Skylight", "what chores does [kid] have", "mark [chore] done", "add milk to the grocery list", "what's on our shopping list", "what's for dinner", "what's on the meal plan", "who's on the Skylight frame", or any request involving the Skylight frame, family calendar, chores, rewards, shared lists, or meals. Works against your own signed-in Skylight account via email + password.
Monitor and control an OpenSprinkler irrigation controller. Enables checking status, running manual stations/programs, toggling rain delays, viewing logs, and pausing the queue via AI-friendly JSON formatting and 1-based station indexing—with no dangerous system commands.
Read a SimplePractice Client Portal through the simplepractice-mcp server — upcoming appointments, invoices/statements/superbills/receipts, balance and saved cards, paperwork waiting to be signed, and practice announcements. Use when the user asks about their therapy or healthcare appointments, what they owe a practice, a superbill for insurance, or forms their provider has sent them.