@mgcrea/mcp-unifi-protect 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -19,6 +19,97 @@ at all unless you ask for them.
19
19
  - **Stays up with no credentials**, reporting what to configure through `unifi_protect_auth_status`
20
20
  rather than exiting and showing in your client as a bare `Connection closed`.
21
21
 
22
+ ## Two ways to connect
23
+
24
+ | | `local` (default) | `cloud` |
25
+ | --------------------- | -------------------------------------------- | --------------------------------------- |
26
+ | Reaches the console | directly on your LAN | via `api.ui.com` Site Manager connector |
27
+ | Credentials | host + username + password | API key + console id |
28
+ | Auth mechanism | UniFi OS login → session cookie + CSRF token | `X-API-KEY` header |
29
+ | TLS | console's self-signed cert — needs setup | a real certificate, nothing to do |
30
+ | Works off-LAN | no | yes |
31
+ | Session state on disk | yes, mode `600` | none |
32
+
33
+ **Both modes expose exactly the same tools**, because both speak the same private
34
+ Protect API — the connector forwards the whole `/proxy/protect/...` tree, the private
35
+ API included. That is not obvious and is worth stating plainly: Ubiquiti's _official_
36
+ Integration API has no historical query capability whatsoever, so if the connector only
37
+ carried that, cloud mode could not answer a single question about the past. It carries
38
+ the private API too, verified against a live console — `bootstrap`, `events`, `cameras`
39
+ and binary snapshots all answer `200`.
40
+
41
+ So cloud mode is a full alternative, not a reduced one, and it removes the local
42
+ account, the password, the session file, the CSRF handshake and the self-signed
43
+ certificate problem in one go.
44
+
45
+ ```bash
46
+ # cloud — no local account at all
47
+ UNIFI_PROTECT_API_KEY=… # unifi.ui.com → Settings → API Keys
48
+ UNIFI_PROTECT_CONSOLE_ID=… # curl -H "X-API-KEY: $KEY" https://api.ui.com/v1/hosts
49
+
50
+ # local — on the LAN
51
+ UNIFI_PROTECT_HOST=192.168.1.1
52
+ UNIFI_PROTECT_USERNAME=mcp
53
+ UNIFI_PROTECT_PASSWORD=…
54
+ UNIFI_PROTECT_VERIFY_TLS=false
55
+ ```
56
+
57
+ `UNIFI_PROTECT_MODE` is inferred as `cloud` when an API key and a console id are both
58
+ set, so it usually needs no setting. `console`/`unifios`/`lan` and
59
+ `remote`/`site-manager`/`connector` are accepted as synonyms, and an unrecognised value
60
+ is reported through `unifi_protect_auth_status` rather than killing the server.
61
+
62
+ **Two traps in cloud mode.** A key that works for `/v1/hosts` can still return
63
+ `403 user cannot access host in the organization` for a console outside the
64
+ organization it was issued in — valid key, wrong org, and the message reads nothing like
65
+ a credentials problem. And API keys are **per-console**: a key created on your Network
66
+ gateway or a UNAS does not authenticate against the NVR running Protect, and the NVR
67
+ rejects it exactly as it rejects a made-up key.
68
+
69
+ ### Why local mode needs a username and password
70
+
71
+ An API key looks like it ought to work here, and it is the obvious thing to reach for.
72
+ It does not, and the reason is worth writing down so nobody spends an afternoon on it.
73
+
74
+ A key created **on the console itself** (UniFi OS → Control Plane → Integrations) is
75
+ recognised — but only by Ubiquiti's _official_ Integration API. It is refused by the
76
+ private API this server depends on. Tested against a UNVR on Protect 7.2.105 with a key
77
+ issued on that console:
78
+
79
+ | Endpoint | With a console API key |
80
+ | ----------------------------------------- | ---------------------- |
81
+ | `/proxy/protect/integration/v1/meta/info` | `200` |
82
+ | `/proxy/protect/integration/v1/cameras` | `200` |
83
+ | `/proxy/protect/integration/v1/nvrs` | `200` |
84
+ | `/proxy/protect/api/nvr` | **`401`** |
85
+ | `/proxy/protect/api/cameras` | **`401`** |
86
+ | `/proxy/protect/api/events` | **`401`** |
87
+ | `/proxy/protect/api/bootstrap` | **`500`** |
88
+
89
+ A fabricated key returns `401` on the official API too, so the `200`s above confirm the
90
+ key really was valid — the private API simply does not accept key auth.
91
+
92
+ Putting all three paths together:
93
+
94
+ | Path | Authenticates with | Private API (event history, snapshots) |
95
+ | ----------------------------- | --------------------- | -------------------------------------- |
96
+ | `local` + username / password | session cookie + CSRF | ✅ |
97
+ | `local` + API key | `X-API-KEY` | ❌ `401` |
98
+ | `cloud` + API key | `X-API-KEY` | ✅ |
99
+
100
+ The asymmetry is not arbitrary. Over the connector, `api.ui.com` authenticates _you_ by
101
+ key and then reaches the console over its own trusted channel, so the console is never
102
+ asked to accept a key on a private path. On the LAN there is no such intermediary, and
103
+ the private API only knows the session the web app itself uses.
104
+
105
+ **So a local-only deployment needs a username and password.** That is a property of
106
+ Protect, not a shortcut taken here. Use a dedicated Local-Access-Only account with
107
+ View Only rights, as described below, and the credential's blast radius stays small.
108
+
109
+ The one thing a console API key _would_ unlock is the PTZ move commands
110
+ (`ptz/goto`, `ptz/patrol/start`, `ptz/patrol/stop`), which exist only on the official
111
+ Integration API — see [Not implemented](#not-implemented).
112
+
22
113
  ## Security
23
114
 
24
115
  **Supply chain.** Three runtime dependencies: the MCP SDK, zod, and `undici`. Retry and backoff
@@ -117,7 +208,7 @@ printf '%s\n' \
117
208
 
118
209
  ## Tools
119
210
 
120
- 20 read tools, plus 9 more when writes are enabled.
211
+ 22 read tools, plus 10 more when writes are enabled.
121
212
 
122
213
  | Tool | What it does | Writes |
123
214
  | ----------------------------------------- | --------------------------------------------------------------------------------------------------- | ---------------------- |
@@ -130,9 +221,11 @@ printf '%s\n' \
130
221
  | `unifi_protect_get_camera_snapshot` | Capture a frame now, to a file or inline | — |
131
222
  | `unifi_protect_list_ptz_presets` | A PTZ camera's saved preset slots | — |
132
223
  | `unifi_protect_list_ptz_patrols` | A PTZ camera's saved patrol routes | — |
224
+ | `unifi_protect_check_settings` | **Audit every camera for inconsistent or self-defeating settings** | — |
133
225
  | `unifi_protect_list_events` | **Search recorded events over any time range** | — |
134
226
  | `unifi_protect_get_event` | One event's full detection metadata | — |
135
227
  | `unifi_protect_get_event_thumbnail` | The frame that triggered a detection | — |
228
+ | `unifi_protect_get_event_thumbnails` | Up to 6 frames at once, inline — how you tell a person from a branch | — |
136
229
  | `unifi_protect_export_video` | Export footage as an MP4 on disk | — |
137
230
  | `unifi_protect_list_lights` | Floodlights, with state and brightness | — |
138
231
  | `unifi_protect_list_sensors` | Sensors, with temperature / humidity / light readings | — |
@@ -142,6 +235,7 @@ printf '%s\n' \
142
235
  | `unifi_protect_list_users` | Who can sign in to Protect | — |
143
236
  | `unifi_protect_request` | Escape hatch: call any private endpoint directly | GET only unless writes |
144
237
  | `unifi_protect_update_camera` | Name, mic, status LED, OSD overlays | ✅ |
238
+ | `unifi_protect_set_camera_detections` | Which objects and sounds a camera detects — the gate below | ✅ |
145
239
  | `unifi_protect_set_camera_recording_mode` | `always` / `never` / `detections` / `schedule` | ✅ |
146
240
  | `unifi_protect_reboot_camera` | Reboot one camera | ✅ confirm |
147
241
  | `unifi_protect_update_light` | Brightness, on/off, PIR sensitivity | ✅ |
@@ -151,6 +245,24 @@ printf '%s\n' \
151
245
  | `unifi_protect_update_nvr_settings` | Console name, timezone, global recording | ✅ |
152
246
  | `unifi_protect_reboot_nvr` | Reboot the console | ✅ confirm |
153
247
 
248
+ ## Resources and prompts
249
+
250
+ Three resources carry the standing facts a question needs before a tool is chosen, so a client
251
+ can attach them once instead of spending a call per question:
252
+
253
+ | Resource | Why it exists |
254
+ | --------------------------- | ---------------------------------------------------------------------------------------- |
255
+ | `unifi-protect://console` | The console's **time zone**, so "1am" is read as the local clock rather than UTC |
256
+ | `unifi-protect://cameras` | What each camera will **actually** detect, what its zones ask for, and where they differ |
257
+ | `unifi-protect://locations` | Named groups of cameras, so a question about a _place_ resolves to ids |
258
+
259
+ Two prompts carry the procedure, which is the part a tool list cannot express:
260
+
261
+ - **`check_camera_settings`** — run the audit and interpret it, changing nothing. Several
262
+ findings have two valid opposite fixes, and which is right depends on what the camera is for.
263
+ - **`who_passed`** — find who was present in a window, and **fall back to motion frames on any
264
+ camera whose detector is off** rather than reporting a zero count as an absence.
265
+
154
266
  ## Worked example: what happened at the front door last night
155
267
 
156
268
  ```jsonc
@@ -205,12 +317,46 @@ console, and both now handled in either form:
205
317
  results report `hasThumbnail: true` rather than an id, and `unifi_protect_get_event_thumbnail`
206
318
  takes the event's `id` (though it tolerates an `e-…` value too).
207
319
 
320
+ **Smart detection is gated in two places, and only one of them is obvious.**
321
+ `smartDetectSettings.objectTypes` on the device is the master switch;
322
+ `smartDetectZones[].objectTypes` says what each zone asks for. A zone can ask for `person` while
323
+ the device list omits it, and the console then reports nothing at all — no error, no warning,
324
+ just an empty result forever. On the console this was built against, a doorbell had
325
+ `zone: [person, vehicle, animal]` against `device: [animal]`, so a person search returned zero
326
+ across seven days while people walked past nightly.
327
+
328
+ Zero results are therefore never reported bare. `unifi_protect_list_events` cross-checks the
329
+ requested detection types against each camera's device list and returns a `warnings` array
330
+ saying the detector was off — the difference between "nobody was there" and "nothing was
331
+ looking". `unifi_protect_check_settings` finds the same misconfiguration across the whole
332
+ system, and `unifi_protect_set_camera_detections` fixes it, keeping the zones in step.
333
+
334
+ One limit worth knowing: the check reflects the camera's setting **now**, so a historical search
335
+ over a period when the detector was off but has since been enabled gets no warning.
336
+
337
+ **Some settings are reported on read but refused on write.** `smartDetectSettings.audioTypes`
338
+ comes back containing `smoke_cmonx`, and a PATCH containing it fails with
339
+ `400 The smart detection feature is not enabled for: smoke_cmonx`. Any read-modify-write that
340
+ echoes the list back therefore breaks. `unifi_protect_set_camera_detections` filters against
341
+ `featureFlags.smartDetectAudioTypes` and reports what it dropped.
342
+
343
+ **Camera filtering happens on the console, and the parameter must be repeated.** `/events`
344
+ accepts `cameras=<id>`, repeated once per camera. A comma-separated list is accepted and
345
+ silently matches nothing. This mattered more than it looks: filtering client-side instead
346
+ fetches the newest `limit` events across _all_ cameras and discards the rest, so a quiet camera
347
+ over a long window came back empty while reporting a successful search.
348
+
208
349
  **Times are milliseconds, and getting it wrong fails silently.** The console takes JavaScript
209
350
  millisecond timestamps. A Unix _seconds_ value is not rejected — it is read as a moment in 1970,
210
351
  so the query succeeds and returns an empty list, which reads as "nothing happened". Every time
211
352
  argument here accepts ISO 8601, a relative expression (`"2h ago"`, `"30m"`, `"7d"`) or `"now"`,
212
353
  and a ten-digit number is refused with the corrected value in the error.
213
354
 
355
+ Local forms are also accepted — `"1am"`, `"01:30"`, `"2026-08-30 01:00"` — and read in the
356
+ **console's own time zone**, because a question about last night is a question about the clock
357
+ where the cameras are. A bare time of day resolves to its most recent occurrence, and `start`
358
+ anchors to the window's end, so "1am to 6am" stays one coherent night however late it is asked.
359
+
214
360
  **Event search is always filtered by type.** Omitting `types` entirely triggers a pagination bug
215
361
  in Protect where the console ignores the window and returns the wrong slice. `unifi_protect_list_events`
216
362
  always sends an explicit list, defaulting to motion, smart detections and rings.
@@ -240,6 +386,22 @@ whereas a refused one invites an agent to keep trying.
240
386
  **`self-signed certificate` errors.** Verification is on by default and cannot pass against an IP address. Either address the console by name with `NODE_EXTRA_CA_CERTS` set, or `UNIFI_PROTECT_VERIFY_TLS=false`
241
387
  unless you have installed a trusted certificate on the console.
242
388
 
389
+ **Cloud mode returns 403 `user cannot access host in the organization`.** The key is
390
+ valid but was issued in an organization that does not contain that console. Check the
391
+ console appears in `curl -H "X-API-KEY: $KEY" https://api.ui.com/v1/hosts`; if the web
392
+ dashboard shows it but that call does not, they are different organizations.
393
+
394
+ **I set an API key for local mode and everything returns 401.** Local mode cannot use an
395
+ API key — see [Why local mode needs a username and password](#why-local-mode-needs-a-username-and-password).
396
+ Set `UNIFI_PROTECT_USERNAME` and `UNIFI_PROTECT_PASSWORD`, or switch to `cloud` mode,
397
+ where a key is all you need.
398
+
399
+ **A local API key returns 401 on everything.** API keys are per-console. A key created
400
+ on your Network gateway is not valid on the NVR running Protect — and the NVR rejects an
401
+ unknown key with exactly the same `401` it gives a fabricated one, so the message cannot
402
+ distinguish "wrong console" from "wrong key". Create the key on the console you are
403
+ addressing, or use cloud mode.
404
+
243
405
  **Everything returns 401.** Check the account is a local one, and that it has Protect
244
406
  permissions. `unifi_protect_auth_status` distinguishes "cannot log in" from "logged in but
245
407
  forbidden".
@@ -255,7 +417,12 @@ so inline so it does not read as a fault.
255
417
 
256
418
  ## What has been verified against real hardware
257
419
 
258
- Built and exercised end-to-end against a live **UNVR4 on Protect 7.2.105** with 12 cameras, one
420
+ **Cloud mode** was verified end-to-end against a live console over the Site Manager
421
+ connector: `auth_status` reachable, camera list, and event search returning real
422
+ detections with their camera names resolved — all authenticated by an API key alone,
423
+ with no local account anywhere in the picture.
424
+
425
+ **Local mode** was built and exercised end-to-end against a live **UNVR4 on Protect 7.2.105** with 12 cameras, one
259
426
  floodlight and one chime. Every read tool was run; the write tools were exercised with _no-op_
260
427
  writes — each value set to the value it already held — and the device state read back unchanged
261
428
  afterwards. That run is also what caught three bugs this README's earlier drafts described
@@ -291,7 +458,7 @@ pnpm lint && pnpm format:check && pnpm typecheck && pnpm test && pnpm build
291
458
  Publish:
292
459
 
293
460
  ```bash
294
- pnpm dlx release-it # bump, commit, tag
461
+ pnpm release minor # bump, commit, tag (patch|minor|major)
295
462
  git push --follow-tags # CI publishes to npm + GHCR from the tag
296
463
  ```
297
464
 
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { B as loadConfig, G as BUILD_INFO, W as setupInstructions, i as createServer, z as isConfigured } from "./server-B2-0Akro.js";
2
+ import { G as BUILD_INFO, L as setupInstructions, M as isConfigured, N as loadConfig, i as createServer } from "./server-B1wSHfi7.js";
3
3
  import { ZodError } from "zod";
4
4
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
5
  //#region src/cli.ts
@@ -27,7 +27,8 @@ const main = async () => {
27
27
  });
28
28
  const transport = new StdioServerTransport();
29
29
  await server.connect(transport);
30
- stderrLogger.warn(`unifi-protect-mcp connected (host=${config.baseUrl ?? "MISSING"}, user=${config.username ?? "MISSING"}, writes=${config.allowWrites ? "ENABLED" : "disabled"}, tls=${config.verifyTls ? "verified" : "UNVERIFIED"})`);
30
+ stderrLogger.warn(`unifi-protect-mcp connected (mode=${config.mode}, ` + (config.mode === "cloud" ? `console=${config.consoleId ?? "MISSING"}, auth=${config.apiKey ? "api-key" : "MISSING"}, ` : `host=${config.baseUrl ?? "MISSING"}, user=${config.username ?? "MISSING"}, `) + `writes=${config.allowWrites ? "ENABLED" : "disabled"}, tls=${config.mode === "cloud" || config.verifyTls ? "verified" : "UNVERIFIED"})`);
31
+ for (const issue of config.issues) stderrLogger.warn(` ${issue}`);
31
32
  if (!isConfigured(config)) {
32
33
  stderrLogger.warn(" not configured — only unifi_protect_auth_status is available:");
33
34
  for (const line of setupInstructions(config)) stderrLogger.warn(` ${line}`);
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.js","names":[],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { ZodError } from \"zod\";\n\nimport { BUILD_INFO } from \"./build-info.js\";\nimport { isConfigured, loadConfig, setupInstructions } from \"./config.js\";\nimport { createServer } from \"./server.js\";\n\n// Everything goes to stderr: stdout is the MCP protocol channel, and a stray\n// log line there corrupts the JSON-RPC stream — usually failing the client's\n// next parse, far from the cause.\n// oxlint-disable no-console -- this is the process entry point; stderr is the log channel.\nconst stderrLogger = {\n debug: (...args: unknown[]) => {\n if (process.env.UNIFI_PROTECT_DEBUG) console.error(\"[unifi-protect-mcp]\", ...args);\n },\n warn: (...args: unknown[]) => console.error(\"[unifi-protect-mcp]\", ...args),\n error: (...args: unknown[]) => console.error(\"[unifi-protect-mcp]\", ...args),\n};\n\n/** Show a config mistake as its field messages, not 40 frames of zod internals. */\nconst describeFatal = (err: unknown): string => {\n if (err instanceof ZodError) {\n return err.issues\n .map((issue) => {\n const path = issue.path.join(\".\");\n return path ? `${path}: ${issue.message}` : issue.message;\n })\n .join(\"\\n\");\n }\n return err instanceof Error ? err.message : String(err);\n};\n\nconst main = async (): Promise<void> => {\n stderrLogger.warn(\n `${BUILD_INFO.name}@${BUILD_INFO.version} (git ${BUILD_INFO.gitCommit} ${BUILD_INFO.gitCommitDate}, node ${process.version})`,\n );\n\n const config = loadConfig();\n // Before anything can open a socket.\n\n const { server } = createServer({ config, logger: stderrLogger });\n const transport = new StdioServerTransport();\n await server.connect(transport);\n\n stderrLogger.warn(\n `unifi-protect-mcp connected (host=${config.baseUrl ?? \"MISSING\"}, ` +\n `user=${config.username ?? \"MISSING\"}, ` +\n `writes=${config.allowWrites ? \"ENABLED\" : \"disabled\"}, ` +\n `tls=${config.verifyTls ? \"verified\" : \"UNVERIFIED\"})`,\n );\n\n // Connecting successfully but exposing one tool is confusing unless we say\n // why. The server no longer refuses to start over this, so the banner and\n // unifi_protect_auth_status are the only channels left.\n if (!isConfigured(config)) {\n stderrLogger.warn(\" not configured — only unifi_protect_auth_status is available:\");\n for (const line of setupInstructions(config)) stderrLogger.warn(` ${line}`);\n stderrLogger.warn(\" Call unifi_protect_auth_status for this same guidance in your client.\");\n }\n\n const shutdown = (signal: string): void => {\n stderrLogger.warn(`received ${signal}, shutting down`);\n process.exit(0);\n };\n process.on(\"SIGINT\", () => shutdown(\"SIGINT\"));\n process.on(\"SIGTERM\", () => shutdown(\"SIGTERM\"));\n};\n\nmain().catch((err: unknown) => {\n console.error(`[unifi-protect-mcp] fatal: ${describeFatal(err)}`);\n if (process.env.UNIFI_PROTECT_DEBUG && err instanceof Error) console.error(err.stack);\n process.exit(1);\n});\n"],"mappings":";;;;;AAYA,MAAM,eAAe;CACnB,QAAQ,GAAG,SAAoB;EAC7B,IAAI,QAAQ,IAAI,qBAAqB,QAAQ,MAAM,uBAAuB,GAAG,IAAI;CACnF;CACA,OAAO,GAAG,SAAoB,QAAQ,MAAM,uBAAuB,GAAG,IAAI;CAC1E,QAAQ,GAAG,SAAoB,QAAQ,MAAM,uBAAuB,GAAG,IAAI;AAC7E;;AAGA,MAAM,iBAAiB,QAAyB;CAC9C,IAAI,eAAe,UACjB,OAAO,IAAI,OACR,KAAK,UAAU;EACd,MAAM,OAAO,MAAM,KAAK,KAAK,GAAG;EAChC,OAAO,OAAO,GAAG,KAAK,IAAI,MAAM,YAAY,MAAM;CACpD,CAAC,CAAC,CACD,KAAK,IAAI;CAEd,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AACxD;AAEA,MAAM,OAAO,YAA2B;CACtC,aAAa,KACX,GAAG,WAAW,KAAK,GAAG,WAAW,QAAQ,QAAQ,WAAW,UAAU,GAAG,WAAW,cAAc,SAAS,QAAQ,QAAQ,EAC7H;CAEA,MAAM,SAAS,WAAW;CAG1B,MAAM,EAAE,WAAW,aAAa;EAAE;EAAQ,QAAQ;CAAa,CAAC;CAChE,MAAM,YAAY,IAAI,qBAAqB;CAC3C,MAAM,OAAO,QAAQ,SAAS;CAE9B,aAAa,KACX,qCAAqC,OAAO,WAAW,UAAU,SACvD,OAAO,YAAY,UAAU,WAC3B,OAAO,cAAc,YAAY,WAAW,QAC/C,OAAO,YAAY,aAAa,aAAa,EACxD;CAKA,IAAI,CAAC,aAAa,MAAM,GAAG;EACzB,aAAa,KAAK,iEAAiE;EACnF,KAAK,MAAM,QAAQ,kBAAkB,MAAM,GAAG,aAAa,KAAK,KAAK,MAAM;EAC3E,aAAa,KAAK,yEAAyE;CAC7F;CAEA,MAAM,YAAY,WAAyB;EACzC,aAAa,KAAK,YAAY,OAAO,gBAAgB;EACrD,QAAQ,KAAK,CAAC;CAChB;CACA,QAAQ,GAAG,gBAAgB,SAAS,QAAQ,CAAC;CAC7C,QAAQ,GAAG,iBAAiB,SAAS,SAAS,CAAC;AACjD;AAEA,KAAK,CAAC,CAAC,OAAO,QAAiB;CAC7B,QAAQ,MAAM,8BAA8B,cAAc,GAAG,GAAG;CAChE,IAAI,QAAQ,IAAI,uBAAuB,eAAe,OAAO,QAAQ,MAAM,IAAI,KAAK;CACpF,QAAQ,KAAK,CAAC;AAChB,CAAC"}
1
+ {"version":3,"file":"cli.js","names":[],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { ZodError } from \"zod\";\n\nimport { BUILD_INFO } from \"#/build-info\";\nimport { isConfigured, loadConfig, setupInstructions } from \"#/config\";\nimport { createServer } from \"#/server\";\n\n// Everything goes to stderr: stdout is the MCP protocol channel, and a stray\n// log line there corrupts the JSON-RPC stream — usually failing the client's\n// next parse, far from the cause.\n// oxlint-disable no-console -- this is the process entry point; stderr is the log channel.\nconst stderrLogger = {\n debug: (...args: unknown[]) => {\n if (process.env.UNIFI_PROTECT_DEBUG) console.error(\"[unifi-protect-mcp]\", ...args);\n },\n warn: (...args: unknown[]) => console.error(\"[unifi-protect-mcp]\", ...args),\n error: (...args: unknown[]) => console.error(\"[unifi-protect-mcp]\", ...args),\n};\n\n/** Show a config mistake as its field messages, not 40 frames of zod internals. */\nconst describeFatal = (err: unknown): string => {\n if (err instanceof ZodError) {\n return err.issues\n .map((issue) => {\n const path = issue.path.join(\".\");\n return path ? `${path}: ${issue.message}` : issue.message;\n })\n .join(\"\\n\");\n }\n return err instanceof Error ? err.message : String(err);\n};\n\nconst main = async (): Promise<void> => {\n stderrLogger.warn(\n `${BUILD_INFO.name}@${BUILD_INFO.version} (git ${BUILD_INFO.gitCommit} ${BUILD_INFO.gitCommitDate}, node ${process.version})`,\n );\n\n const config = loadConfig();\n // Before anything can open a socket.\n\n const { server } = createServer({ config, logger: stderrLogger });\n const transport = new StdioServerTransport();\n await server.connect(transport);\n\n stderrLogger.warn(\n `unifi-protect-mcp connected (mode=${config.mode}, ` +\n (config.mode === \"cloud\"\n ? `console=${config.consoleId ?? \"MISSING\"}, auth=${config.apiKey ? \"api-key\" : \"MISSING\"}, `\n : `host=${config.baseUrl ?? \"MISSING\"}, user=${config.username ?? \"MISSING\"}, `) +\n `writes=${config.allowWrites ? \"ENABLED\" : \"disabled\"}, ` +\n `tls=${config.mode === \"cloud\" || config.verifyTls ? \"verified\" : \"UNVERIFIED\"})`,\n );\n\n // Connecting successfully but exposing one tool is confusing unless we say\n // why. The server no longer refuses to start over this, so the banner and\n // unifi_protect_auth_status are the only channels left.\n for (const issue of config.issues) stderrLogger.warn(` ${issue}`);\n\n if (!isConfigured(config)) {\n stderrLogger.warn(\" not configured — only unifi_protect_auth_status is available:\");\n for (const line of setupInstructions(config)) stderrLogger.warn(` ${line}`);\n stderrLogger.warn(\" Call unifi_protect_auth_status for this same guidance in your client.\");\n }\n\n const shutdown = (signal: string): void => {\n stderrLogger.warn(`received ${signal}, shutting down`);\n process.exit(0);\n };\n process.on(\"SIGINT\", () => shutdown(\"SIGINT\"));\n process.on(\"SIGTERM\", () => shutdown(\"SIGTERM\"));\n};\n\nmain().catch((err: unknown) => {\n console.error(`[unifi-protect-mcp] fatal: ${describeFatal(err)}`);\n if (process.env.UNIFI_PROTECT_DEBUG && err instanceof Error) console.error(err.stack);\n process.exit(1);\n});\n"],"mappings":";;;;;AAYA,MAAM,eAAe;CACnB,QAAQ,GAAG,SAAoB;EAC7B,IAAI,QAAQ,IAAI,qBAAqB,QAAQ,MAAM,uBAAuB,GAAG,IAAI;CACnF;CACA,OAAO,GAAG,SAAoB,QAAQ,MAAM,uBAAuB,GAAG,IAAI;CAC1E,QAAQ,GAAG,SAAoB,QAAQ,MAAM,uBAAuB,GAAG,IAAI;AAC7E;;AAGA,MAAM,iBAAiB,QAAyB;CAC9C,IAAI,eAAe,UACjB,OAAO,IAAI,OACR,KAAK,UAAU;EACd,MAAM,OAAO,MAAM,KAAK,KAAK,GAAG;EAChC,OAAO,OAAO,GAAG,KAAK,IAAI,MAAM,YAAY,MAAM;CACpD,CAAC,CAAC,CACD,KAAK,IAAI;CAEd,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AACxD;AAEA,MAAM,OAAO,YAA2B;CACtC,aAAa,KACX,GAAG,WAAW,KAAK,GAAG,WAAW,QAAQ,QAAQ,WAAW,UAAU,GAAG,WAAW,cAAc,SAAS,QAAQ,QAAQ,EAC7H;CAEA,MAAM,SAAS,WAAW;CAG1B,MAAM,EAAE,WAAW,aAAa;EAAE;EAAQ,QAAQ;CAAa,CAAC;CAChE,MAAM,YAAY,IAAI,qBAAqB;CAC3C,MAAM,OAAO,QAAQ,SAAS;CAE9B,aAAa,KACX,qCAAqC,OAAO,KAAK,OAC9C,OAAO,SAAS,UACb,WAAW,OAAO,aAAa,UAAU,SAAS,OAAO,SAAS,YAAY,UAAU,MACxF,QAAQ,OAAO,WAAW,UAAU,SAAS,OAAO,YAAY,UAAU,OAC9E,UAAU,OAAO,cAAc,YAAY,WAAW,QAC/C,OAAO,SAAS,WAAW,OAAO,YAAY,aAAa,aAAa,EACnF;CAKA,KAAK,MAAM,SAAS,OAAO,QAAQ,aAAa,KAAK,KAAK,OAAO;CAEjE,IAAI,CAAC,aAAa,MAAM,GAAG;EACzB,aAAa,KAAK,iEAAiE;EACnF,KAAK,MAAM,QAAQ,kBAAkB,MAAM,GAAG,aAAa,KAAK,KAAK,MAAM;EAC3E,aAAa,KAAK,yEAAyE;CAC7F;CAEA,MAAM,YAAY,WAAyB;EACzC,aAAa,KAAK,YAAY,OAAO,gBAAgB;EACrD,QAAQ,KAAK,CAAC;CAChB;CACA,QAAQ,GAAG,gBAAgB,SAAS,QAAQ,CAAC;CAC7C,QAAQ,GAAG,iBAAiB,SAAS,SAAS,CAAC;AACjD;AAEA,KAAK,CAAC,CAAC,OAAO,QAAiB;CAC7B,QAAQ,MAAM,8BAA8B,cAAc,GAAG,GAAG;CAChE,IAAI,QAAQ,IAAI,uBAAuB,eAAe,OAAO,QAAQ,MAAM,IAAI,KAAK;CACpF,QAAQ,KAAK,CAAC;AAChB,CAAC"}
package/dist/index.d.ts CHANGED
@@ -20,7 +20,19 @@ declare const LOGIN_PATH = "/api/auth/login";
20
20
  /** The realtime channel. Not used yet — see the WebSocket note in the README. */
21
21
  declare const UPDATES_WS_PATH = "/proxy/protect/ws/updates";
22
22
  declare const ConfigSchema: z.ZodObject<{
23
+ mode: z.ZodDefault<z.ZodEnum<{
24
+ cloud: "cloud";
25
+ local: "local";
26
+ }>>;
27
+ modeSource: z.ZodDefault<z.ZodEnum<{
28
+ default: "default";
29
+ explicit: "explicit";
30
+ inferred: "inferred";
31
+ invalid: "invalid";
32
+ }>>;
23
33
  baseUrl: z.ZodOptional<z.ZodString>;
34
+ consoleId: z.ZodOptional<z.ZodString>;
35
+ apiKey: z.ZodOptional<z.ZodString>;
24
36
  username: z.ZodOptional<z.ZodString>;
25
37
  password: z.ZodOptional<z.ZodString>;
26
38
  totp: z.ZodOptional<z.ZodString>;
@@ -31,6 +43,9 @@ declare const ConfigSchema: z.ZodObject<{
31
43
  maxRetries: z.ZodDefault<z.ZodNumber>;
32
44
  maxDownloadBytes: z.ZodDefault<z.ZodNumber>;
33
45
  deviceCacheTtlSeconds: z.ZodDefault<z.ZodNumber>;
46
+ timeZone: z.ZodOptional<z.ZodString>;
47
+ locations: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>;
48
+ issues: z.ZodDefault<z.ZodArray<z.ZodString>>;
34
49
  }, z.core.$strict>;
35
50
  type Config = z.infer<typeof ConfigSchema>;
36
51
  /**
@@ -42,7 +57,13 @@ type Config = z.infer<typeof ConfigSchema>;
42
57
  * which is the worst way to learn your credentials came from somewhere else.
43
58
  */
44
59
  declare const FileConfigSchema: z.ZodObject<{
60
+ mode: z.ZodOptional<z.ZodEnum<{
61
+ cloud: "cloud";
62
+ local: "local";
63
+ }>>;
45
64
  host: z.ZodOptional<z.ZodString>;
65
+ consoleId: z.ZodOptional<z.ZodString>;
66
+ apiKey: z.ZodOptional<z.ZodString>;
46
67
  username: z.ZodOptional<z.ZodString>;
47
68
  password: z.ZodOptional<z.ZodString>;
48
69
  verifyTls: z.ZodOptional<z.ZodBoolean>;
@@ -52,6 +73,8 @@ declare const FileConfigSchema: z.ZodObject<{
52
73
  maxRetries: z.ZodOptional<z.ZodNumber>;
53
74
  maxDownloadBytes: z.ZodOptional<z.ZodNumber>;
54
75
  deviceCacheTtlSeconds: z.ZodOptional<z.ZodNumber>;
76
+ timeZone: z.ZodOptional<z.ZodString>;
77
+ locations: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>;
55
78
  }, z.core.$strict>;
56
79
  type FileConfig = z.infer<typeof FileConfigSchema>;
57
80
  /**
@@ -79,12 +102,6 @@ declare const expandTilde: (path: string) => string;
79
102
  declare const resolveConfigPath: (env?: NodeJS.ProcessEnv) => string;
80
103
  /** The session file sits beside the config file unless told otherwise. */
81
104
  declare const resolveSessionPath: (env?: NodeJS.ProcessEnv) => string;
82
- /**
83
- * Environment first, config file second, **per field** — not whole-source.
84
- * Docker and CI inject the environment and must keep working untouched, while a
85
- * one-off `UNIFI_PROTECT_ALLOW_WRITES=0` still has to override a file that says
86
- * `true`. Merging field by field is the only rule that gives both.
87
- */
88
105
  declare const loadConfig: (env?: NodeJS.ProcessEnv, configPath?: string) => Config;
89
106
  /** True once the server has everything it needs to reach a console. */
90
107
  declare const isConfigured: (config: Config) => boolean;
@@ -102,14 +119,17 @@ type Logger = {
102
119
  error?(...args: unknown[]): void;
103
120
  };
104
121
  /** The two headers that authenticate every request to a UniFi OS console. */
105
- type SessionHeaders = {
106
- cookie: string;
107
- "x-csrf-token": string;
108
- };
122
+ /**
123
+ * The headers that authenticate one request. Deliberately an open record rather
124
+ * than the cookie/CSRF pair it used to be: local mode sends
125
+ * `cookie` + `x-csrf-token`, cloud mode sends a single `x-api-key`, and the
126
+ * client should not have to know which it is holding.
127
+ */
128
+ type SessionHeaders = Record<string, string>;
109
129
  type SessionStatus = {
110
130
  authenticated: boolean;
111
131
  /** Where the live session came from, for unifi_protect_auth_status. */
112
- source: "none" | "restored" | "login";
132
+ source: "none" | "restored" | "login" | "api-key";
113
133
  username: string | undefined;
114
134
  savedAt: string | undefined;
115
135
  };
@@ -137,6 +157,28 @@ declare const createSessionProvider: (opts: SessionProviderOptions) => SessionPr
137
157
  /** For tests: fixed headers, no network, no disk. */
138
158
  declare const staticSessionProvider: (headers?: SessionHeaders) => SessionProvider;
139
159
  //#endregion
160
+ //#region src/client/detection.d.ts
161
+ /** Everything about one camera that decides whether a detection could occur. */
162
+ type CameraFacts = {
163
+ id: string;
164
+ name: string;
165
+ /** The master switch: `smartDetectSettings.objectTypes`. */
166
+ enabled: string[];
167
+ /** The union of what the zones ask for. */
168
+ zone: string[];
169
+ /** What the hardware supports: `featureFlags.smartDetectTypes`. */
170
+ capable: string[];
171
+ audioEnabled: string[];
172
+ audioCapable: string[];
173
+ hasSmartDetect: boolean;
174
+ isConnected: boolean;
175
+ recordingMode: string | undefined;
176
+ motionDetectionEnabled: boolean | undefined;
177
+ /** Types a zone asks for that the device list blocks — the silent failure. */
178
+ blocked: string[];
179
+ };
180
+ type CameraIndex = ReadonlyMap<string, CameraFacts>;
181
+ //#endregion
140
182
  //#region src/client/protect.d.ts
141
183
  /** Array values become repeated params, which is how Protect expects `types`. */
142
184
  type Query = Record<string, string | number | boolean | string[] | undefined>;
@@ -221,7 +263,7 @@ declare const summarizeCamera: (camera: Rec) => Rec;
221
263
  * would otherwise have to perform against a separate camera list, and get
222
264
  * silently wrong. The id is kept too, since the write and snapshot tools need it.
223
265
  */
224
- declare const summarizeEvent: (event: Rec, cameras?: NameIndex) => Rec;
266
+ declare const summarizeEvent: (event: Rec, cameras?: NameIndex, timeZone?: string) => Rec;
225
267
  declare const summarizeLight: (light: Rec) => Rec;
226
268
  declare const summarizeSensor: (sensor: Rec) => Rec;
227
269
  declare const summarizeViewer: (viewer: Rec) => Rec;
@@ -244,17 +286,32 @@ type DeviceCacheOptions = {
244
286
  now?: () => number;
245
287
  };
246
288
  /**
247
- * A short-lived camera id→name index, used to resolve the `camera` reference on
248
- * every event into something readable.
289
+ * A short-lived index of the cameras, used to resolve the `camera` reference on
290
+ * every event into something readable, and to explain why a detection search
291
+ * could not have matched.
249
292
  *
250
293
  * It is cached because event search is the hot path and every result set needs
251
294
  * the same index: fetching the camera list once per search rather than once per
252
295
  * event is the difference between one extra request and none. The TTL is short
253
296
  * because a renamed or newly adopted camera should appear without a restart,
254
297
  * and a stale name is only ever cosmetic — the id travels alongside it.
298
+ *
299
+ * Names and detection facts come from ONE fetch. They were two calls until the
300
+ * gate cross-check needed both on the same path, and a second round-trip per
301
+ * search to re-read the list we already had is pure waste.
255
302
  */
256
303
  type DeviceCache = {
304
+ /** id → display name, for resolving event references. */
257
305
  cameras(): Promise<NameIndex>;
306
+ /** id → everything that decides whether a detection could occur. */
307
+ facts(): Promise<CameraIndex>;
308
+ /**
309
+ * The console's IANA time zone, or undefined if it cannot be read.
310
+ *
311
+ * Needed to interpret "1am" as the person asking meant it. Cached far longer
312
+ * than the camera list: a console's zone changes when someone moves house.
313
+ */
314
+ timeZone(): Promise<string | undefined>;
258
315
  invalidate(): void;
259
316
  };
260
317
  declare const createDeviceCache: (opts: DeviceCacheOptions) => DeviceCache;
@@ -367,6 +424,21 @@ declare const registerTools: (server: McpServer, client: ProtectClient, ctx: Too
367
424
  declare const assertSafePath: (path: string) => void;
368
425
  //#endregion
369
426
  //#region src/tools/util.d.ts
427
+ type TimeOptions = {
428
+ now?: number;
429
+ /**
430
+ * The console's time zone. A bare "1am" means 1am WHERE THE CAMERAS ARE, not
431
+ * wherever this process happens to run, and an hour's error silently returns
432
+ * the wrong night's footage.
433
+ */
434
+ timeZone?: string;
435
+ /**
436
+ * A bare time-of-day resolves to its latest occurrence at or before this.
437
+ * Event search passes the window's end here, so "1am to 6am" asked at 03:00
438
+ * still resolves to one coherent night rather than an inverted window.
439
+ */
440
+ before?: number;
441
+ };
370
442
  /**
371
443
  * Convert a time expression to milliseconds since the Unix epoch.
372
444
  *
@@ -377,9 +449,12 @@ declare const assertSafePath: (path: string) => void;
377
449
  * night", which is the most expensive possible failure for this server.
378
450
  *
379
451
  * Accepts ISO 8601, a relative expression like "2h ago" or "30m", the literal
380
- * "now", or a raw millisecond number.
452
+ * "now", a raw millisecond number, and — when a `timeZone` is supplied — naive
453
+ * local forms: "2026-08-30 01:00", "2026-08-30", "01:00" and "1am". Naive forms
454
+ * are read in the console's zone, because that is the zone the person asking is
455
+ * thinking in.
381
456
  */
382
- declare const toEpochMs: (value: string | number, now?: number) => number;
457
+ declare const toEpochMs: (value: string | number, opts?: number | TimeOptions) => number;
383
458
  //#endregion
384
459
  export { type BinaryResult, type Config, type CreateServerOptions, type CreatedServer, type DeviceCache, type FileConfig, LOGIN_PATH, type Logger, type NameIndex, NotConfiguredError, PRIVATE_API_PATH, type PersistedSession, ProtectApiError, ProtectAuthError, ProtectClient, type ProtectClientOptions, type Query, SERVER_NAME, SERVER_VERSION, type SessionHeaders, type SessionProvider, type SessionStatus, type ToolContext, UPDATES_WS_PATH, USER_AGENT, WritesDisabledError, assertSafePath, backoffMs, buildNameIndex, buildQuery, clearSession, createDeviceCache, createServer, createSessionProvider, expandTilde, isConfigured, isoTime, loadConfig, loadSession, normalizeBaseUrl, registerTools, resolveConfigPath, resolveSessionPath, retryAfterMs, saveSession, setupInstructions, staticSessionProvider, summarizeBootstrap, summarizeCamera, summarizeChime, summarizeEach, summarizeEvent, summarizeLight, summarizeLiveview, summarizeNvr, summarizeSensor, summarizeUser, summarizeViewer, toEpochMs };
385
460
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../src/config.ts","../src/client/auth.ts","../src/client/protect.ts","../src/client/shape.ts","../src/client/device-cache.ts","../src/server.ts","../src/client/session-store.ts","../src/client/errors.ts","../src/tools/index.ts","../src/tools/request.ts","../src/tools/util.ts"],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/config.ts","../src/client/auth.ts","../src/client/detection.ts","../src/client/protect.ts","../src/client/shape.ts","../src/client/device-cache.ts","../src/server.ts","../src/client/session-store.ts","../src/client/errors.ts","../src/tools/index.ts","../src/tools/request.ts","../src/tools/util.ts"],"mappings":";;;;;;;;;;;;;;;;cAmBa;;cAGA;;cAGA;cAgDP,cAAY,EAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8Ed,EAAA,KAAA;KAEQ,SAAS,EAAE,aAAa;;;;;;;;;cAU9B,kBAAgB,EAAA;;;;;;;;;;;;;;;;;;;GAkBX,EAAA,KAAA;KAEC,aAAa,EAAE,aAAa;;;;;;;;;;;;;;;;cAiB3B,mBAAgB;;cA6BhB,cAAW;;;;;cAOX,oBAAiB,MAAS,OAAO;;cAQjC,qBAAkB,MAAS,OAAO;cA0FlC,aAAU,MAChB,OAAO,YAAU,wBAErB;;cA8HU,eAAY,QAAY;;;;;;cAUxB,oBAAiB,QAAY;;;KChd9B;EACV,UAAU;EACV,SAAS;EACT,UAAU;;;;;;;;;KAUA,iBAAiB;KAEjB;EACV;;EAEA;EACA;EACA;;;;;;;KAQU;EACV,WAAW,QAAQ;EACnB;;EAEA,MAAM,gBAAgB,QAAQ;;EAE9B,UAAU;EACV,YAAY;;KAGF;EACV,QAAQ;EACR,eAAe;EACf,SAAS;EACT;;cA6BW,wBAAqB,MAAU,2BAAyB;;cAqOxD,wBAAqB,UACvB,mBACR;;;;KChQS;EACV;EACA;;EAEA;;EAEA;;EAEA;EACA;EACA;EACA;EACA;EACA;EACA;;EAEA;;KA+BU,cAAc,oBAAoB;;;;KCjGlC,QAAQ;cAIP,YAAS;cAET,eAAY,KAAS;cAerB,aAAU,OAAW;KAkBtB;EACV;EACA,SAAS;EACT;EACA;EACA;EACA,eAAe;EACf,SAAS;;KAGC;EACV,OAAO;EACP;;;;;;cAOW;WACF;mBACQ;mBACA;mBACA;mBACA;mBACA;mBACA;EAEjB,YAAY,MAAM;;EAWlB,IAAI,cAAc,QAAQ;;;;;UASZ;;EAgDR,QAAQ,aACZ,gBACA,cACA;IAAQ,QAAQ;IAAO;MACtB,QAAQ;EAkBX,IAAI,aAAa,cAAc,QAAQ,QAAQ,QAAQ;EAIvD,KAAK,aAAa,cAAc,gBAAgB,QAAQ,QAAQ,QAAQ;EAOxE,MAAM,aAAa,cAAc,gBAAgB,QAAQ,QAAQ,QAAQ;EAOzE,IAAI,aAAa,cAAc,QAAQ,QAAQ,QAAQ;;;;;;;;EAWjD,aACJ,cACA;IAAQ,QAAQ;IAAO;MACtB,QAAQ;;UAwCH;;;;KC/NL,MAAM;;cASE,gBAAiB,GAAC,gBAAgB,KAAO,MAAM,QAAQ;;;;;;;cASvD,UAAO;;KA+BR,YAAY;cAEX,iBAAc,qBAAuB;cAYrC,kBAAe,QAAY,QAAM;;;;;;;cAiCjC,iBAAc,OAAW,KAAG,UAAY,WAAS,sBAAsB;cAmCvE,iBAAc,OAAW,QAAM;cAe/B,kBAAe,QAAY,QAAM;cA0BjC,kBAAe,QAAY,QAAM;cAUjC,iBAAc,OAAW,QAAM;cAU/B,oBAAiB,UAAc,QAAM;cASrC,gBAAa,MAAU,QAAM;cA0E7B,eAAY,KAAS,QAAM;;;;;;;cAwC3B,qBAAkB,WAAe,QAAM;;;KClUxC;EACV,QAAQ;EACR;EACA;;;;;;;;;;;;;;;;;KAkBU;;EAEV,WAAW,QAAQ;;EAEnB,SAAS,QAAQ;;;;;;;EAOjB,YAAY;EACZ;;cAKW,oBAAiB,MAAU,uBAAqB;;;cCzBhD;cACA;cACA;KAED;EACV,QAAQ;EACR,eAAe;EACf,SAAS;;EAET,UAAU;;KAGA;EACV,QAAQ;EACR,QAAQ;EACR,SAAS;EACT,SAAS;;cAGE,eAAY,MAAU,wBAAsB;;;;;;;;KC5B7C;;EAEV;EACA;;EAEA;;EAEA;EACA;;;cAIW,cAAW,iBAAyB,QAAQ;;cAiB5C,cAAW,cAAsB,SAAW,qBAAmB;;cAU/D,eAAY,iBAAyB;;;;cC9CrC,wBAAwB;WACjB;WACT;;WAEA;WACA;EAET,YACE,iBACA;IAAQ;IAAgB;IAA2B;;;;cAU1C,yBAAyB;WAClB;;WAET;EAET,YAAY,iBAAiB;IAAQ;;;;cAO1B,4BAA4B;WACrB;EAElB,YAAY;;;cASD,2BAA2B;WACpB;EAElB;;;;KCjCU;EACV,QAAQ;;EAER;EACA,SAAS;EACT,SAAS;;;;;;;;;;;;;;;cAgBE,gBAAa,QAAY,WAAS,QAAU,eAAa,KAAO;;;;;;;;cCvBhE,iBAAc;;;KC6If;EACV;;;;;;EAMA;;;;;;EAMA;;;;;;;;;;;;;;;;;cAkBW,YAAS,wBAA0B,gBAAiB"}
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- import { A as saveSession, B as loadConfig, C as summarizeSensor, D as staticSessionProvider, E as createSessionProvider, F as LOGIN_PATH, H as resolveConfigPath, I as PRIVATE_API_PATH, L as UPDATES_WS_PATH, M as ProtectApiError, N as ProtectAuthError, O as clearSession, P as WritesDisabledError, R as expandTilde, S as summarizeNvr, T as summarizeViewer, U as resolveSessionPath, V as normalizeBaseUrl, W as setupInstructions, _ as summarizeChime, a as registerTools, b as summarizeLight, c as ProtectClient, d as retryAfterMs, f as createDeviceCache, g as summarizeCamera, h as summarizeBootstrap, i as createServer, j as NotConfiguredError, k as loadSession, l as backoffMs, m as isoTime, n as SERVER_VERSION, o as assertSafePath, p as buildNameIndex, r as USER_AGENT, s as toEpochMs, t as SERVER_NAME, u as buildQuery, v as summarizeEach, w as summarizeUser, x as summarizeLiveview, y as summarizeEvent, z as isConfigured } from "./server-B2-0Akro.js";
1
+ import { A as UPDATES_WS_PATH, B as saveSession, C as buildQuery, D as staticSessionProvider, E as createSessionProvider, F as resolveConfigPath, H as ProtectApiError, I as resolveSessionPath, L as setupInstructions, M as isConfigured, N as loadConfig, O as LOGIN_PATH, P as normalizeBaseUrl, R as clearSession, S as backoffMs, T as createDeviceCache, U as ProtectAuthError, V as NotConfiguredError, W as WritesDisabledError, _ as summarizeNvr, a as registerTools, b as summarizeViewer, c as buildNameIndex, d as summarizeCamera, f as summarizeChime, g as summarizeLiveview, h as summarizeLight, i as createServer, j as expandTilde, k as PRIVATE_API_PATH, l as isoTime, m as summarizeEvent, n as SERVER_VERSION, o as assertSafePath, p as summarizeEach, r as USER_AGENT, s as toEpochMs, t as SERVER_NAME, u as summarizeBootstrap, v as summarizeSensor, w as retryAfterMs, x as ProtectClient, y as summarizeUser, z as loadSession } from "./server-B1wSHfi7.js";
2
2
  export { LOGIN_PATH, NotConfiguredError, PRIVATE_API_PATH, ProtectApiError, ProtectAuthError, ProtectClient, SERVER_NAME, SERVER_VERSION, UPDATES_WS_PATH, USER_AGENT, WritesDisabledError, assertSafePath, backoffMs, buildNameIndex, buildQuery, clearSession, createDeviceCache, createServer, createSessionProvider, expandTilde, isConfigured, isoTime, loadConfig, loadSession, normalizeBaseUrl, registerTools, resolveConfigPath, resolveSessionPath, retryAfterMs, saveSession, setupInstructions, staticSessionProvider, summarizeBootstrap, summarizeCamera, summarizeChime, summarizeEach, summarizeEvent, summarizeLight, summarizeLiveview, summarizeNvr, summarizeSensor, summarizeUser, summarizeViewer, toEpochMs };