360 Secure SkillHub
360 安全技能中心

simplisafe-mcp

在 shell 中以 curl+jq 调用 SimpliSafe 服务端 API:一次浏览器登录换取 OAuth 刷新令牌后,可读取报警状态、传感器、门锁、事件与设置,并执行布防/撤防、上锁/解锁。写操作作用于真实安防系统,需先与用户确认并二次读取校验。

chrischall
chrischall
热度 63.46
最近更新 2026-10-10 07:30:01
分类:自动化依赖:代码执行 / 读写文件 / 第三方 API应用场景:自动化脚本 / 工作流 / API 集成

安装技能

复制下面的话术发给你的 Agent,它会通过 secure-skills 检索并安装该技能。

执行命令安装此 skill: npx secure-skills-cli add simplisafe-mcp
Metadata
name
simplisafe-api
description
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.

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 URL>"

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 <METHOD> <PATH> [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 at location.system.version, not the top-level systemVersion (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/<serial>/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_COUNT while the exit delay runs, settling to AWAY / HOME. That is success in progress, not failure.
  • Do not diff whole objects — alarmStateTimestamp, stateUpdated and lastUpdated advance 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 cleanly unlocked.
  • 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=true makes the base station re-poll its devices — slower and harder on the hardware. Leave it false unless freshness matters.
  • Lock state encoding is counter-intuitive: lockState 1 = locked, 2 = unlocked, and lockJamState overrides both.
  • Device type ids 21, 23 and 24 appear 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.