npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@mgcrea/mcp-unifi-protect

v1.0.1

Published

Model Context Protocol server for UniFi Protect

Readme

@mgcrea/mcp-unifi-protect

Model Context Protocol server for a self-hosted UniFi Protect console — cameras, recorded events and smart detections, snapshots, footage export, and the lights, sensors, viewers and chimes attached to it. Read-only by default: the tools that change anything are not registered at all unless you ask for them.

Features

  • Search recorded events over any time range — motion, person / vehicle / animal / package / licence-plate detections, doorbell rings — with each result already carrying its camera's name, not just an id.
  • Snapshots and footage — capture a frame now, pull an event's thumbnail, export an MP4. All written to disk by default, so a still frame does not silently cost you a context window.
  • Devices — cameras, lights, sensors (with their temperature, humidity and light readings), viewers, chimes, live views and users.
  • Shaped responses. A console camera record is 8-15 KB of JSON; a list of ten is over 100 KB. List tools return the fifteen fields anyone actually asks about. get_* returns everything.
  • Stays up with no credentials, reporting what to configure through unifi_protect_auth_status rather than exiting and showing in your client as a bare Connection closed.

Two ways to connect

| | local (default) | cloud | | --------------------- | ------------------------------------------------ | --------------------------------------- | | Reaches the console | directly on your LAN | via api.ui.com Site Manager connector | | Credentials | host + username + password | API key + console id | | Auth mechanism | UniFi OS login → session cookie + CSRF token | X-API-KEY header | | TLS | console's self-signed cert — pinned on first use | a real certificate, nothing to do | | Works off-LAN | no | yes | | Session state on disk | yes, mode 600 | none |

Both modes expose exactly the same tools, because both speak the same private Protect API — the connector forwards the whole /proxy/protect/... tree, the private API included. That is not obvious and is worth stating plainly: Ubiquiti's official Integration API has no historical query capability whatsoever, so if the connector only carried that, cloud mode could not answer a single question about the past. It carries the private API too, verified against a live console — bootstrap, events, cameras and binary snapshots all answer 200.

So cloud mode is a full alternative, not a reduced one, and it removes the local account, the password, the session file, the CSRF handshake and the self-signed certificate problem in one go.

# cloud — no local account at all
UNIFI_PROTECT_API_KEY=…        # unifi.ui.com → Settings → API Keys
UNIFI_PROTECT_CONSOLE_ID=…     # curl -H "X-API-KEY: $KEY" https://api.ui.com/v1/hosts

# local — on the LAN
UNIFI_PROTECT_HOST=192.168.1.1
UNIFI_PROTECT_USERNAME=mcp
UNIFI_PROTECT_PASSWORD=…
# TLS needs nothing: the console's certificate is pinned on the first request.

UNIFI_PROTECT_MODE is inferred as cloud when an API key and a console id are both set, so it usually needs no setting. console/unifios/lan and remote/site-manager/connector are accepted as synonyms, and an unrecognised value is reported through unifi_protect_auth_status rather than killing the server.

Two traps in cloud mode. A key that works for /v1/hosts can still return 403 user cannot access host in the organization for a console outside the organization it was issued in — valid key, wrong org, and the message reads nothing like a credentials problem. And API keys are per-console: a key created on your Network gateway or a UNAS does not authenticate against the NVR running Protect, and the NVR rejects it exactly as it rejects a made-up key.

Why local mode needs a username and password

An API key looks like it ought to work here, and it is the obvious thing to reach for. It does not, and the reason is worth writing down so nobody spends an afternoon on it.

A key created on the console itself (UniFi OS → Control Plane → Integrations) is recognised — but only by Ubiquiti's official Integration API. It is refused by the private API this server depends on. Tested against a UNVR on Protect 7.2.105 with a key issued on that console:

| Endpoint | With a console API key | | ----------------------------------------- | ---------------------- | | /proxy/protect/integration/v1/meta/info | 200 | | /proxy/protect/integration/v1/cameras | 200 | | /proxy/protect/integration/v1/nvrs | 200 | | /proxy/protect/api/nvr | 401 | | /proxy/protect/api/cameras | 401 | | /proxy/protect/api/events | 401 | | /proxy/protect/api/bootstrap | 500 |

A fabricated key returns 401 on the official API too, so the 200s above confirm the key really was valid — the private API simply does not accept key auth.

Putting all three paths together:

| Path | Authenticates with | Private API (event history, snapshots) | | ----------------------------- | --------------------- | -------------------------------------- | | local + username / password | session cookie + CSRF | ✅ | | local + API key | X-API-KEY | ❌ 401 | | cloud + API key | X-API-KEY | ✅ |

The asymmetry is not arbitrary. Over the connector, api.ui.com authenticates you by key and then reaches the console over its own trusted channel, so the console is never asked to accept a key on a private path. On the LAN there is no such intermediary, and the private API only knows the session the web app itself uses.

So a local-only deployment needs a username and password. That is a property of Protect, not a shortcut taken here. Use a dedicated Local-Access-Only account with View Only rights, as described below, and the credential's blast radius stays small.

The one thing a console API key would unlock is the PTZ move commands (ptz/goto, ptz/patrol/start, ptz/patrol/stop), which exist only on the official Integration API — see Not implemented.

Security

Supply chain. Three runtime dependencies: the MCP SDK, zod, and @mgcrea/unifi-protect — the console client, which is ours and whose own dependencies are undici, ws and zod. There is no HTTP client wrapper, no logger, no crypto library. That client used to be a copy inside this repo; the copy and the original had already begun to drift, which is a bad way to hold knowledge about an undocumented API. Published from CI with provenance via OIDC trusted publishing; the container image is multi-arch, carries an SBOM, and is signed with cosign.

Your credentials. The username and password come from the environment or a config file, and never leave this process except in the login request to your console. The resulting session cookie is cached at ~/.config/unifi-protect/session.json with mode 600.

Certificate verification is ON by default, and now needs no setup. The console's certificate is pinned: on the first request the server reads it, records its SHA-256 fingerprint and PEM in ~/.config/unifi-protect/trust.json (mode 600), and every connection afterwards verifies against that one certificate as its own anchor. The host name check is replaced — not skipped — by a fingerprint comparison, so addressing the console by IP is fine and a swapped certificate fails hard.

This is what changed. The certificate is issued to unifi.local with no IP SAN, so reached by an IP address, host name verification could never pass however the certificate was trusted; the old advice was to set NODE_EXTRA_CA_CERTS and address the console by a resolvable name, or give up and disable verification. Pinning removes both requirements.

Pinning is trust-on-first-use, and it is only as good as that first moment. The fingerprint is logged when it is learned and reported by unifi_protect_auth_status; set UNIFI_PROTECT_FINGERPRINT to make the trust explicit instead, and a console that presents anything else is then refused rather than adopted. If the console legitimately reissues its certificate, delete the trust file.

UNIFI_PROTECT_VERIFY_TLS=false still turns checking off entirely, scoped to this server's own requests through an undici dispatcher — it is not NODE_TLS_REJECT_UNAUTHORIZED, so nothing else in the process is affected. There should no longer be a reason to use it; the startup banner prints tls=UNVERIFIED on every run when you do.

Blast radius. With the defaults, the worst an agent can do is read your cameras and write image files into the snapshot directory. With UNIFI_PROTECT_ALLOW_WRITES=1 it can additionally reconfigure devices, stop a camera recording, and reboot a camera or the whole console. Use a Local-Access-Only account with View Only rights, and leave writes off unless you need them.

Configure

| Variable | Required | Default | What it does | | ---------------------------------- | -------- | -------------------------------------- | --------------------------------------------------------------- | | UNIFI_PROTECT_HOST | yes | — | Console IP or hostname. https:// assumed, :port preserved | | UNIFI_PROTECT_USERNAME | yes | — | Console login | | UNIFI_PROTECT_PASSWORD | yes | — | Its password | | UNIFI_PROTECT_TOTP | no | — | 2FA code. Expires in ~30s — prefer unifi_protect_auth_login | | UNIFI_PROTECT_VERIFY_TLS | no | true | Verify the console's certificate. Off disables pinning entirely | | UNIFI_PROTECT_FINGERPRINT | no | — | Pin this SHA-256 up front instead of learning it on first use | | UNIFI_PROTECT_TRUST_FILE | no | ~/.config/unifi-protect/trust.json | Where the pinned certificate is remembered, mode 600 | | UNIFI_PROTECT_ALLOW_WRITES | no | false | Register the 12 mutating tools | | UNIFI_PROTECT_SESSION_FILE | no | ~/.config/unifi-protect/session.json | Cached session, mode 600 | | UNIFI_PROTECT_SNAPSHOT_DIR | no | ~/.cache/unifi-protect | Where images and exports are written | | UNIFI_PROTECT_CONFIG | no | ~/.config/unifi-protect/config.json | Config file location | | UNIFI_PROTECT_MAX_RETRIES | no | 3 | Retries on 401 / 429 / 5xx | | UNIFI_PROTECT_MAX_DOWNLOAD_BYTES | no | 200000000 | Refuse a download larger than this | | UNIFI_PROTECT_DEVICE_CACHE_TTL | no | 60 | Camera id→name cache lifetime, seconds | | UNIFI_PROTECT_DEBUG | no | — | Verbose request logging to stderr |

The config file mirrors these as camelCase JSON (host, username, verifyTls, …). It is strict: an unknown key is an error rather than a silent no-op. Environment variables win over the file, field by field, so a one-off UNIFI_PROTECT_ALLOW_WRITES=0 still beats a file that says true.

Create an account for it

UniFi OS → Settings → Admins & Users → Add User → Local Access Only, with Protect permissions and View Only unless you plan to enable writes.

Use a local account rather than your Ubiquiti (SSO) one. Cloud accounts frequently cannot log in locally at all, and a scoped local account keeps this server away from the rest of the console.

Quick start

A. npx

UNIFI_PROTECT_HOST=192.168.1.1 UNIFI_PROTECT_USERNAME=mcp UNIFI_PROTECT_PASSWORD=… \
  npx -y @mgcrea/mcp-unifi-protect

B. Docker (stdio)

docker run --rm -i \
  -e UNIFI_PROTECT_HOST=192.168.1.1 \
  -e UNIFI_PROTECT_USERNAME=mcp \
  -e UNIFI_PROTECT_PASSWORD=… \
  ghcr.io/mgcrea/mcp-unifi-protect

C. From source

pnpm install && pnpm build
node dist/cli.js

Inspect the tools

printf '%s\n' \
  '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"cli","version":"0"}}}' \
  '{"jsonrpc":"2.0","method":"notifications/initialized"}' \
  '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' \
| node dist/cli.js 2>/dev/null | jq -r '.result.tools[]?.name'

Tools

22 read tools, plus 10 more when writes are enabled.

| Tool | What it does | Writes | | ----------------------------------------- | --------------------------------------------------------------------------------------------------- | ---------------------- | | unifi_protect_auth_status | Log in and make a real call, reporting whether the console is reachable and on what Protect version | — | | unifi_protect_auth_login | Force a fresh login; the only way to supply a 2FA code | — | | unifi_protect_auth_logout | Drop the cached session and delete the session file | confirm | | unifi_protect_get_system_info | Console model, Protect version, storage, device counts | — | | unifi_protect_list_cameras | Every camera, summarized | — | | unifi_protect_get_camera | One camera's complete record (large) | — | | unifi_protect_get_camera_snapshot | Capture a frame now, to a file or inline | — | | unifi_protect_list_ptz_presets | A PTZ camera's saved preset slots | — | | unifi_protect_list_ptz_patrols | A PTZ camera's saved patrol routes | — | | unifi_protect_check_settings | Audit every camera for inconsistent or self-defeating settings | — | | unifi_protect_list_events | Search recorded events over any time range | — | | unifi_protect_get_event | One event's full detection metadata | — | | unifi_protect_get_event_thumbnail | The frame that triggered a detection | — | | unifi_protect_get_event_thumbnails | Up to 6 frames at once, inline — how you tell a person from a branch | — | | unifi_protect_export_video | Export footage as an MP4 on disk | — | | unifi_protect_list_lights | Floodlights, with state and brightness | — | | unifi_protect_list_sensors | Sensors, with temperature / humidity / light readings | — | | unifi_protect_list_viewers | Viewport devices and what each displays | — | | unifi_protect_list_chimes | Chimes, volume, paired doorbells | — | | unifi_protect_list_liveviews | Saved camera grid layouts | — | | unifi_protect_list_users | Who can sign in to Protect | — | | unifi_protect_request | Escape hatch: call any private endpoint directly | GET only unless writes | | unifi_protect_update_camera | Name, mic, status LED, OSD overlays | ✅ | | unifi_protect_set_camera_detections | Which objects and sounds a camera detects — the gate below | ✅ | | unifi_protect_set_camera_recording_mode | always / never / detections / schedule | ✅ | | unifi_protect_reboot_camera | Reboot one camera | ✅ confirm | | unifi_protect_update_light | Brightness, on/off, PIR sensitivity | ✅ | | unifi_protect_update_sensor | Name, which capabilities report | ✅ | | unifi_protect_update_viewer | Put a live view on a screen | ✅ | | unifi_protect_update_chime | Volume, name | ✅ | | unifi_protect_update_nvr_settings | Console name, timezone, global recording | ✅ | | unifi_protect_reboot_nvr | Reboot the console | ✅ confirm |

Resources and prompts

Three resources carry the standing facts a question needs before a tool is chosen, so a client can attach them once instead of spending a call per question:

| Resource | Why it exists | | --------------------------- | ---------------------------------------------------------------------------------------- | | unifi-protect://console | The console's time zone, so "1am" is read as the local clock rather than UTC | | unifi-protect://cameras | What each camera will actually detect, what its zones ask for, and where they differ | | unifi-protect://locations | Named groups of cameras, so a question about a place resolves to ids |

Two prompts carry the procedure, which is the part a tool list cannot express:

  • check_camera_settings — run the audit and interpret it, changing nothing. Several findings have two valid opposite fixes, and which is right depends on what the camera is for.
  • who_passed — find who was present in a window, and fall back to motion frames on any camera whose detector is off rather than reporting a zero count as an absence. It takes the question as one free-text argument, so quote it: slash-command arguments are split shell-style and mapped positionally, so who_passed in front of the house last night? arrives as just "in", while who_passed "in front of the house last night?" arrives whole. A single-word question is treated as that truncation and refused rather than answered.

Worked example: what happened at the front door last night

// 1. Which cameras are there?
{"name": "unifi_protect_list_cameras", "arguments": {}}
// → [{ "id": "661a…", "name": "Front Door", "hasSmartDetect": true,
//      "smartDetectTypes": ["person","package"], "recordingMode": "detections", … }]

// 2. People seen overnight. Note the camera NAME comes back resolved.
{"name": "unifi_protect_list_events", "arguments": {
   "start": "2026-08-29T22:00:00Z", "end": "2026-08-30T07:00:00Z",
   "types": ["smartDetectZone"], "smartDetectTypes": ["person"]}}
// → { "count": 3, "events": [
//     { "id": "9f3c1a02-…", "start": "2026-08-30T02:14:07.000Z", "camera": "Front Door",
//       "smartDetectTypes": ["person"], "score": 94, "hasThumbnail": true }, … ] }

// 3. Look at the one at 02:14 — pass the event's own id.
{"name": "unifi_protect_get_event_thumbnail",
 "arguments": {"eventId": "9f3c1a02-…", "output": "image"}}

// 4. Pull the footage around it.
{"name": "unifi_protect_export_video", "arguments": {
   "cameraId": "661a…", "start": "2026-08-30T02:13:30Z", "end": "2026-08-30T02:15:00Z"}}
// → { "path": "/Users/you/.cache/unifi-protect/front-door-….mp4", "bytes": 18432000 }

Traps worth knowing

This wraps Protect's private API, not the official one. Ubiquiti publishes an Integration API at /proxy/protect/integration/v1 with an OpenAPI spec and an X-API-KEY header. It is not used here, because it has no historical query capability at all — the only query parameters in its entire spec are channel, highQuality and qualities, and events exist solely as a live WebSocket. "What happened last night" is unanswerable through it. The private API answers that, at the cost of being undocumented and liable to change between Protect releases. This was built and verified end-to-end against a live UNVR running Protect 7.2.105. unifi_protect_get_system_info reports the version you are actually running, and unifi_protect_request reaches any endpoint that moves.

Two shapes already changed between 6.x and 7.x, both found by running this against a real console, and both now handled in either form:

  • Storage moved. 6.x had nvr.storageInfo with totalSize / totalSpaceUsed. By 7.2 that key is gone; the numbers live under nvr.systemInfo.storage and nvr.storageStats, with per-disk health in systemInfo.ustorage.disks.
  • A camera has no ledLevel. The 0-6 brightness that looks like it belongs there is a floodlight field; a camera's LED is the on/off ledSettings.isEnabled. Sub-objects also deep-merge on PATCH, so setting one OSD overlay preserves the others — verified by writing to a live camera and reading it back.
  • An event's thumbnail field is not a thumbnail id you can use here. It reads e-<eventId> and belongs to the thumbnails/<id> endpoint; events/<eventId>/thumbnail — the one this server calls — wants the bare event id. Passing the console's own value returns 404. So list results report hasThumbnail: true rather than an id, and unifi_protect_get_event_thumbnail takes the event's id (though it tolerates an e-… value too).

Smart detection is gated in two places, and only one of them is obvious. smartDetectSettings.objectTypes on the device is the master switch; smartDetectZones[].objectTypes says what each zone asks for. A zone can ask for person while the device list omits it, and the console then reports nothing at all — no error, no warning, just an empty result forever. On the console this was built against, a doorbell had zone: [person, vehicle, animal] against device: [animal], so a person search returned zero across seven days while people walked past nightly.

Zero results are therefore never reported bare. unifi_protect_list_events cross-checks the requested detection types against each camera's device list and returns a warnings array saying the detector was off — the difference between "nobody was there" and "nothing was looking". unifi_protect_check_settings finds the same misconfiguration across the whole system, and unifi_protect_set_camera_detections fixes it, keeping the zones in step.

One limit worth knowing: the check reflects the camera's setting now, so a historical search over a period when the detector was off but has since been enabled gets no warning.

Some settings are reported on read but refused on write. smartDetectSettings.audioTypes comes back containing smoke_cmonx, and a PATCH containing it fails with 400 The smart detection feature is not enabled for: smoke_cmonx. Any read-modify-write that echoes the list back therefore breaks. unifi_protect_set_camera_detections filters against featureFlags.smartDetectAudioTypes and reports what it dropped.

Camera filtering happens on the console, and the parameter must be repeated. /events accepts cameras=<id>, repeated once per camera. A comma-separated list is accepted and silently matches nothing. This mattered more than it looks: filtering client-side instead fetches the newest limit events across all cameras and discards the rest, so a quiet camera over a long window came back empty while reporting a successful search.

Times are milliseconds, and getting it wrong fails silently. The console takes JavaScript millisecond timestamps. A Unix seconds value is not rejected — it is read as a moment in 1970, so the query succeeds and returns an empty list, which reads as "nothing happened". Every time argument here accepts ISO 8601, a relative expression ("2h ago", "30m", "7d") or "now", and a ten-digit number is refused with the corrected value in the error.

Local forms are also accepted — "1am", "01:30", "2026-08-30 01:00" — and read in the console's own time zone, because a question about last night is a question about the clock where the cameras are. A bare time of day resolves to its most recent occurrence, and start anchors to the window's end, so "1am to 6am" stays one coherent night however late it is asked.

Event search is always filtered by type. Omitting types entirely triggers a pagination bug in Protect where the console ignores the window and returns the wrong slice. unifi_protect_list_events always sends an explicit list, defaulting to motion, smart detections and rings.

Footage only exists if the camera was recording. An empty event search may mean the camera's recording mode is never, not that nothing happened. unifi_protect_list_cameras shows the mode.

Snapshots are forced. Without that the console can return a cached frame minutes old, which is indistinguishable from a current one.

A cloud account may not work. Ubiquiti SSO accounts frequently cannot log in locally. Create a Local Access Only user.

Troubleshooting

The server does not appear, or shows Connection closed. It should never exit on missing credentials — run it by hand with the same environment and read stderr. Everything it logs goes to stderr, because stdout is the protocol channel.

Only unifi_protect_auth_status is listed. No console is configured. Call that tool; it returns the setup steps as data.

A tool I expected is missing. The write tools are not registered unless UNIFI_PROTECT_ALLOW_WRITES=1. That is the design, not a bug — an absent tool cannot be called, whereas a refused one invites an agent to keep trying.

self-signed certificate errors. These should no longer happen: the certificate is pinned on the first request, which works against an IP address. If one appears, the console is presenting a different certificate than the one pinned — delete ~/.config/unifi-protect/trust.json if it was legitimately reissued, and investigate if it was not unless you have installed a trusted certificate on the console.

Cloud mode returns 403 user cannot access host in the organization. The key is valid but was issued in an organization that does not contain that console. Check the console appears in curl -H "X-API-KEY: $KEY" https://api.ui.com/v1/hosts; if the web dashboard shows it but that call does not, they are different organizations.

I set an API key for local mode and everything returns 401. Local mode cannot use an API key — see Why local mode needs a username and password. Set UNIFI_PROTECT_USERNAME and UNIFI_PROTECT_PASSWORD, or switch to cloud mode, where a key is all you need.

A local API key returns 401 on everything. API keys are per-console. A key created on your Network gateway is not valid on the NVR running Protect — and the NVR rejects an unknown key with exactly the same 401 it gives a fabricated one, so the message cannot distinguish "wrong console" from "wrong key". Create the key on the console you are addressing, or use cloud mode.

Everything returns 401. Check the account is a local one, and that it has Protect permissions. unifi_protect_auth_status distinguishes "cannot log in" from "logged in but forbidden".

A tool that used to work now returns 404. Compare the Protect version from unifi_protect_get_system_info against 7.2.105 above; an upgrade may have moved the endpoint. unifi_protect_request is the workaround while it is fixed — it reaches any path under /proxy/protect/api directly, which is how both of the 6.x→7.x changes above were pinned down.

Storage shows as nearly full. That is normal on an NVR: isRecycling: true means the console continuously overwrites the oldest footage rather than stopping. get_system_info says so inline so it does not read as a fault.

What has been verified against real hardware

Cloud mode was verified end-to-end against a live console over the Site Manager connector: auth_status reachable, camera list, and event search returning real detections with their camera names resolved — all authenticated by an API key alone, with no local account anywhere in the picture.

Local mode was built and exercised end-to-end against a live UNVR4 on Protect 7.2.105 with 12 cameras, one floodlight and one chime. Every read tool was run; the write tools were exercised with no-op writes — each value set to the value it already held — and the device state read back unchanged afterwards. That run is also what caught three bugs this README's earlier drafts described wrongly: the storage layout, the event-thumbnail id, and two camera fields that do not exist.

Certificate pinning was verified against that same console on 2026-08-31, addressed by IP (192.168.6.3) with verification ON — the case that was impossible before. Confirmed in one run: the certificate is captured and its fingerprint logged on first contact; the trust file is written mode 600; a restart reuses the pin without re-capturing; a correct UNIFI_PROTECT_FINGERPRINT (colons and all) is accepted; and a wrong one is refused with a message naming both fingerprints, surfacing as a failed tool call rather than a server that never starts. auth_status reported reachable: true on Protect 7.2.105 throughout, and a session cached by the previous release was restored unchanged — the on-disk format did not move.

Two tools remain unverified for want of hardware, and are marked here rather than left to look tested:

  • unifi_protect_update_sensor — no UP Sense device on the test console (sensors is empty). Note that a Protect floodlight has its own built-in PIR, reported as isPirMotionDetected and tuned via pirSensitivity; that is part of the light, so it is unifi_protect_update_light that controls it, not this tool. A garden lamp is not a sensor device.
  • unifi_protect_update_viewer — no Viewport device on the test console.

Both follow the same PATCH shape as the tools that were verified, so they are likely correct, but "likely" is the honest word until someone runs them.

Not implemented

The realtime WebSocket at /proxy/protect/ws/updates is not wired up here, though @mgcrea/unifi-protect now implements it — connectEventStream and ProtectStore are a decoded frame stream and a live device cache, and this server deliberately uses neither.

The reason is that they are the wrong shape for an MCP server. Both want a long-lived process that bootstraps at start-up and stays subscribed; this server must start with no credentials and no connectivity at all, answering unifi_protect_auth_status until it is configured. It would also gain little: because this wraps the private API, event history is already available over REST through unifi_protect_list_events, which is what a subscription would have been for. Adopting it would mean deferring the bootstrap until credentials appear — worth doing if a tool ever needs push rather than poll, and not before.

Develop

pnpm install
pnpm lint && pnpm format:check && pnpm typecheck && pnpm test && pnpm build

Publish:

pnpm release minor             # bump, commit, tag (patch|minor|major)
git push --follow-tags         # CI publishes to npm + GHCR from the tag

License

MIT