@mgcrea/mcp-unifi-protect 0.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Olivier Louvignes
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,300 @@
1
+ # @mgcrea/mcp-unifi-protect
2
+
3
+ Model Context Protocol server for a self-hosted **UniFi Protect** console — cameras, recorded
4
+ events and smart detections, snapshots, footage export, and the lights, sensors, viewers and
5
+ chimes attached to it. Read-only by default: the tools that change anything are not registered
6
+ at all unless you ask for them.
7
+
8
+ ## Features
9
+
10
+ - **Search recorded events over any time range** — motion, person / vehicle / animal / package /
11
+ licence-plate detections, doorbell rings — with each result already carrying its camera's
12
+ **name**, not just an id.
13
+ - **Snapshots and footage** — capture a frame now, pull an event's thumbnail, export an MP4.
14
+ All written to disk by default, so a still frame does not silently cost you a context window.
15
+ - **Devices** — cameras, lights, sensors (with their temperature, humidity and light readings),
16
+ viewers, chimes, live views and users.
17
+ - **Shaped responses.** A console camera record is 8-15 KB of JSON; a list of ten is over 100 KB.
18
+ List tools return the fifteen fields anyone actually asks about. `get_*` returns everything.
19
+ - **Stays up with no credentials**, reporting what to configure through `unifi_protect_auth_status`
20
+ rather than exiting and showing in your client as a bare `Connection closed`.
21
+
22
+ ## Security
23
+
24
+ **Supply chain.** Three runtime dependencies: the MCP SDK, zod, and `undici`. Retry and backoff
25
+ are hand-rolled; there is no HTTP client wrapper, no logger, no crypto library. `undici` earns its
26
+ place by being the only way to scope the TLS exception below to this server's own requests — see
27
+ the note there. Published from CI with provenance via OIDC trusted publishing; the container
28
+ image is multi-arch, carries an SBOM, and is signed with cosign.
29
+
30
+ **Your credentials.** The username and password come from the environment or a config file, and
31
+ never leave this process except in the login request to your console. The resulting session
32
+ cookie is cached at `~/.config/unifi-protect/session.json` with mode `600`.
33
+
34
+ **Certificate verification is ON by default**, and disabling it is scoped to this server's own
35
+ requests through an undici dispatcher — it is not `NODE_TLS_REJECT_UNAUTHORIZED`, so nothing else
36
+ in the process is affected. (It previously _was_ process-wide, on the belief that a dispatcher
37
+ could not be scoped without a dependency. It can, and `undici` is now that dependency.)
38
+
39
+ Verifying takes two things together, and either alone achieves nothing: the certificate is
40
+ self-signed, so `NODE_EXTRA_CA_CERTS` must point at it; **and** it is issued to `unifi.local` with
41
+ no IP SAN, so `UNIFI_PROTECT_HOST` must be a host name that resolves to the console rather than its
42
+ IP address. Reached by IP, verification fails on the host name however the certificate is trusted.
43
+ See `.env.example` for the two commands. If the console has no name on your network, set
44
+ `UNIFI_PROTECT_VERIFY_TLS=false`; the startup banner then prints `tls=UNVERIFIED` on every run.
45
+
46
+ **Blast radius.** With the defaults, the worst an agent can do is read your cameras and write
47
+ image files into the snapshot directory. With `UNIFI_PROTECT_ALLOW_WRITES=1` it can additionally
48
+ reconfigure devices, stop a camera recording, and reboot a camera or the whole console. Use a
49
+ Local-Access-Only account with View Only rights, and leave writes off unless you need them.
50
+
51
+ ## Configure
52
+
53
+ | Variable | Required | Default | What it does |
54
+ | ---------------------------------- | -------- | -------------------------------------- | --------------------------------------------------------------- |
55
+ | `UNIFI_PROTECT_HOST` | yes | — | Console IP or hostname. `https://` assumed, `:port` preserved |
56
+ | `UNIFI_PROTECT_USERNAME` | yes | — | Console login |
57
+ | `UNIFI_PROTECT_PASSWORD` | yes | — | Its password |
58
+ | `UNIFI_PROTECT_TOTP` | no | — | 2FA code. Expires in ~30s — prefer `unifi_protect_auth_login` |
59
+ | `UNIFI_PROTECT_VERIFY_TLS` | no | `true` | Verify the console's certificate (needs a host name, not an IP) |
60
+ | `UNIFI_PROTECT_ALLOW_WRITES` | no | `false` | Register the 12 mutating tools |
61
+ | `UNIFI_PROTECT_SESSION_FILE` | no | `~/.config/unifi-protect/session.json` | Cached session, mode 600 |
62
+ | `UNIFI_PROTECT_SNAPSHOT_DIR` | no | `~/.cache/unifi-protect` | Where images and exports are written |
63
+ | `UNIFI_PROTECT_CONFIG` | no | `~/.config/unifi-protect/config.json` | Config file location |
64
+ | `UNIFI_PROTECT_MAX_RETRIES` | no | `3` | Retries on 401 / 429 / 5xx |
65
+ | `UNIFI_PROTECT_MAX_DOWNLOAD_BYTES` | no | `200000000` | Refuse a download larger than this |
66
+ | `UNIFI_PROTECT_DEVICE_CACHE_TTL` | no | `60` | Camera id→name cache lifetime, seconds |
67
+ | `UNIFI_PROTECT_DEBUG` | no | — | Verbose request logging to stderr |
68
+
69
+ The config file mirrors these as camelCase JSON (`host`, `username`, `verifyTls`, …). It is
70
+ strict: an unknown key is an error rather than a silent no-op. **Environment variables win over
71
+ the file, field by field**, so a one-off `UNIFI_PROTECT_ALLOW_WRITES=0` still beats a file that
72
+ says `true`.
73
+
74
+ ### Create an account for it
75
+
76
+ UniFi OS → **Settings → Admins & Users → Add User → Local Access Only**, with Protect
77
+ permissions and **View Only** unless you plan to enable writes.
78
+
79
+ Use a local account rather than your Ubiquiti (SSO) one. Cloud accounts frequently cannot log in
80
+ locally at all, and a scoped local account keeps this server away from the rest of the console.
81
+
82
+ ## Quick start
83
+
84
+ **A. npx**
85
+
86
+ ```bash
87
+ UNIFI_PROTECT_HOST=192.168.1.1 UNIFI_PROTECT_USERNAME=mcp UNIFI_PROTECT_PASSWORD=… \
88
+ npx -y @mgcrea/mcp-unifi-protect
89
+ ```
90
+
91
+ **B. Docker (stdio)**
92
+
93
+ ```bash
94
+ docker run --rm -i \
95
+ -e UNIFI_PROTECT_HOST=192.168.1.1 \
96
+ -e UNIFI_PROTECT_USERNAME=mcp \
97
+ -e UNIFI_PROTECT_PASSWORD=… \
98
+ ghcr.io/mgcrea/mcp-unifi-protect
99
+ ```
100
+
101
+ **C. From source**
102
+
103
+ ```bash
104
+ pnpm install && pnpm build
105
+ node dist/cli.js
106
+ ```
107
+
108
+ ### Inspect the tools
109
+
110
+ ```bash
111
+ printf '%s\n' \
112
+ '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"cli","version":"0"}}}' \
113
+ '{"jsonrpc":"2.0","method":"notifications/initialized"}' \
114
+ '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' \
115
+ | node dist/cli.js 2>/dev/null | jq -r '.result.tools[]?.name'
116
+ ```
117
+
118
+ ## Tools
119
+
120
+ 20 read tools, plus 9 more when writes are enabled.
121
+
122
+ | Tool | What it does | Writes |
123
+ | ----------------------------------------- | --------------------------------------------------------------------------------------------------- | ---------------------- |
124
+ | `unifi_protect_auth_status` | Log in and make a real call, reporting whether the console is reachable and on what Protect version | — |
125
+ | `unifi_protect_auth_login` | Force a fresh login; the only way to supply a 2FA code | — |
126
+ | `unifi_protect_auth_logout` | Drop the cached session and delete the session file | confirm |
127
+ | `unifi_protect_get_system_info` | Console model, Protect version, storage, device counts | — |
128
+ | `unifi_protect_list_cameras` | Every camera, summarized | — |
129
+ | `unifi_protect_get_camera` | One camera's complete record (large) | — |
130
+ | `unifi_protect_get_camera_snapshot` | Capture a frame now, to a file or inline | — |
131
+ | `unifi_protect_list_ptz_presets` | A PTZ camera's saved preset slots | — |
132
+ | `unifi_protect_list_ptz_patrols` | A PTZ camera's saved patrol routes | — |
133
+ | `unifi_protect_list_events` | **Search recorded events over any time range** | — |
134
+ | `unifi_protect_get_event` | One event's full detection metadata | — |
135
+ | `unifi_protect_get_event_thumbnail` | The frame that triggered a detection | — |
136
+ | `unifi_protect_export_video` | Export footage as an MP4 on disk | — |
137
+ | `unifi_protect_list_lights` | Floodlights, with state and brightness | — |
138
+ | `unifi_protect_list_sensors` | Sensors, with temperature / humidity / light readings | — |
139
+ | `unifi_protect_list_viewers` | Viewport devices and what each displays | — |
140
+ | `unifi_protect_list_chimes` | Chimes, volume, paired doorbells | — |
141
+ | `unifi_protect_list_liveviews` | Saved camera grid layouts | — |
142
+ | `unifi_protect_list_users` | Who can sign in to Protect | — |
143
+ | `unifi_protect_request` | Escape hatch: call any private endpoint directly | GET only unless writes |
144
+ | `unifi_protect_update_camera` | Name, mic, status LED, OSD overlays | ✅ |
145
+ | `unifi_protect_set_camera_recording_mode` | `always` / `never` / `detections` / `schedule` | ✅ |
146
+ | `unifi_protect_reboot_camera` | Reboot one camera | ✅ confirm |
147
+ | `unifi_protect_update_light` | Brightness, on/off, PIR sensitivity | ✅ |
148
+ | `unifi_protect_update_sensor` | Name, which capabilities report | ✅ |
149
+ | `unifi_protect_update_viewer` | Put a live view on a screen | ✅ |
150
+ | `unifi_protect_update_chime` | Volume, name | ✅ |
151
+ | `unifi_protect_update_nvr_settings` | Console name, timezone, global recording | ✅ |
152
+ | `unifi_protect_reboot_nvr` | Reboot the console | ✅ confirm |
153
+
154
+ ## Worked example: what happened at the front door last night
155
+
156
+ ```jsonc
157
+ // 1. Which cameras are there?
158
+ {"name": "unifi_protect_list_cameras", "arguments": {}}
159
+ // → [{ "id": "661a…", "name": "Front Door", "hasSmartDetect": true,
160
+ // "smartDetectTypes": ["person","package"], "recordingMode": "detections", … }]
161
+
162
+ // 2. People seen overnight. Note the camera NAME comes back resolved.
163
+ {"name": "unifi_protect_list_events", "arguments": {
164
+ "start": "2026-08-29T22:00:00Z", "end": "2026-08-30T07:00:00Z",
165
+ "types": ["smartDetectZone"], "smartDetectTypes": ["person"]}}
166
+ // → { "count": 3, "events": [
167
+ // { "id": "9f3c1a02-…", "start": "2026-08-30T02:14:07.000Z", "camera": "Front Door",
168
+ // "smartDetectTypes": ["person"], "score": 94, "hasThumbnail": true }, … ] }
169
+
170
+ // 3. Look at the one at 02:14 — pass the event's own id.
171
+ {"name": "unifi_protect_get_event_thumbnail",
172
+ "arguments": {"eventId": "9f3c1a02-…", "output": "image"}}
173
+
174
+ // 4. Pull the footage around it.
175
+ {"name": "unifi_protect_export_video", "arguments": {
176
+ "cameraId": "661a…", "start": "2026-08-30T02:13:30Z", "end": "2026-08-30T02:15:00Z"}}
177
+ // → { "path": "/Users/you/.cache/unifi-protect/front-door-….mp4", "bytes": 18432000 }
178
+ ```
179
+
180
+ ## Traps worth knowing
181
+
182
+ **This wraps Protect's private API, not the official one.** Ubiquiti publishes an Integration
183
+ API at `/proxy/protect/integration/v1` with an OpenAPI spec and an `X-API-KEY` header. It is not
184
+ used here, because it has **no historical query capability at all** — the only query parameters
185
+ in its entire spec are `channel`, `highQuality` and `qualities`, and events exist solely as a
186
+ live WebSocket. "What happened last night" is unanswerable through it. The private API answers
187
+ that, at the cost of being undocumented and liable to change between Protect releases. This was
188
+ built and verified end-to-end against a live **UNVR running Protect 7.2.105**.
189
+ `unifi_protect_get_system_info` reports the version you are actually running, and
190
+ `unifi_protect_request` reaches any endpoint that moves.
191
+
192
+ Two shapes already changed between 6.x and 7.x, both found by running this against a real
193
+ console, and both now handled in either form:
194
+
195
+ - **Storage moved.** 6.x had `nvr.storageInfo` with `totalSize` / `totalSpaceUsed`. By 7.2 that
196
+ key is gone; the numbers live under `nvr.systemInfo.storage` and `nvr.storageStats`, with
197
+ per-disk health in `systemInfo.ustorage.disks`.
198
+ - **A camera has no `ledLevel`.** The 0-6 brightness that looks like it belongs there is a
199
+ _floodlight_ field; a camera's LED is the on/off `ledSettings.isEnabled`. Sub-objects also
200
+ deep-merge on PATCH, so setting one OSD overlay preserves the others — verified by writing to
201
+ a live camera and reading it back.
202
+ - **An event's `thumbnail` field is not a thumbnail id you can use here.** It reads `e-<eventId>`
203
+ and belongs to the `thumbnails/<id>` endpoint; `events/<eventId>/thumbnail` — the one this
204
+ server calls — wants the bare event id. Passing the console's own value returns 404. So list
205
+ results report `hasThumbnail: true` rather than an id, and `unifi_protect_get_event_thumbnail`
206
+ takes the event's `id` (though it tolerates an `e-…` value too).
207
+
208
+ **Times are milliseconds, and getting it wrong fails silently.** The console takes JavaScript
209
+ millisecond timestamps. A Unix _seconds_ value is not rejected — it is read as a moment in 1970,
210
+ so the query succeeds and returns an empty list, which reads as "nothing happened". Every time
211
+ argument here accepts ISO 8601, a relative expression (`"2h ago"`, `"30m"`, `"7d"`) or `"now"`,
212
+ and a ten-digit number is refused with the corrected value in the error.
213
+
214
+ **Event search is always filtered by type.** Omitting `types` entirely triggers a pagination bug
215
+ in Protect where the console ignores the window and returns the wrong slice. `unifi_protect_list_events`
216
+ always sends an explicit list, defaulting to motion, smart detections and rings.
217
+
218
+ **Footage only exists if the camera was recording.** An empty event search may mean the camera's
219
+ recording mode is `never`, not that nothing happened. `unifi_protect_list_cameras` shows the mode.
220
+
221
+ **Snapshots are forced.** Without that the console can return a cached frame minutes old, which
222
+ is indistinguishable from a current one.
223
+
224
+ **A cloud account may not work.** Ubiquiti SSO accounts frequently cannot log in locally. Create
225
+ a Local Access Only user.
226
+
227
+ ## Troubleshooting
228
+
229
+ **The server does not appear, or shows `Connection closed`.** It should never exit on missing
230
+ credentials — run it by hand with the same environment and read stderr. Everything it logs goes
231
+ to stderr, because stdout is the protocol channel.
232
+
233
+ **Only `unifi_protect_auth_status` is listed.** No console is configured. Call that tool; it
234
+ returns the setup steps as data.
235
+
236
+ **A tool I expected is missing.** The write tools are not registered unless
237
+ `UNIFI_PROTECT_ALLOW_WRITES=1`. That is the design, not a bug — an absent tool cannot be called,
238
+ whereas a refused one invites an agent to keep trying.
239
+
240
+ **`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
+ unless you have installed a trusted certificate on the console.
242
+
243
+ **Everything returns 401.** Check the account is a local one, and that it has Protect
244
+ permissions. `unifi_protect_auth_status` distinguishes "cannot log in" from "logged in but
245
+ forbidden".
246
+
247
+ **A tool that used to work now returns 404.** Compare the Protect version from
248
+ `unifi_protect_get_system_info` against 7.2.105 above; an upgrade may have moved the endpoint.
249
+ `unifi_protect_request` is the workaround while it is fixed — it reaches any path under
250
+ `/proxy/protect/api` directly, which is how both of the 6.x→7.x changes above were pinned down.
251
+
252
+ **Storage shows as nearly full.** That is normal on an NVR: `isRecycling: true` means the
253
+ console continuously overwrites the oldest footage rather than stopping. `get_system_info` says
254
+ so inline so it does not read as a fault.
255
+
256
+ ## What has been verified against real hardware
257
+
258
+ Built and exercised end-to-end against a live **UNVR4 on Protect 7.2.105** with 12 cameras, one
259
+ floodlight and one chime. Every read tool was run; the write tools were exercised with _no-op_
260
+ writes — each value set to the value it already held — and the device state read back unchanged
261
+ afterwards. That run is also what caught three bugs this README's earlier drafts described
262
+ wrongly: the storage layout, the event-thumbnail id, and two camera fields that do not exist.
263
+
264
+ Two tools remain **unverified for want of hardware**, and are marked here rather than left to look
265
+ tested:
266
+
267
+ - `unifi_protect_update_sensor` — no UP Sense device on the test console (`sensors` is empty).
268
+ Note that a Protect **floodlight has its own built-in PIR**, reported as `isPirMotionDetected`
269
+ and tuned via `pirSensitivity`; that is part of the light, so it is `unifi_protect_update_light`
270
+ that controls it, not this tool. A garden lamp is not a sensor device.
271
+ - `unifi_protect_update_viewer` — no Viewport device on the test console.
272
+
273
+ Both follow the same PATCH shape as the tools that were verified, so they are likely correct, but
274
+ "likely" is the honest word until someone runs them.
275
+
276
+ ## Not implemented
277
+
278
+ The realtime WebSocket at `/proxy/protect/ws/updates` is not wired up. It is a binary framed
279
+ protocol, and because this server wraps the private API, event _history_ is already available
280
+ over REST through `unifi_protect_list_events` — which is what the WebSocket would have been
281
+ needed for. Node's global `WebSocket` follows the WHATWG signature and ignores a headers option,
282
+ so attaching the session cookie would mean adding `ws` as a dependency.
283
+
284
+ ## Develop
285
+
286
+ ```bash
287
+ pnpm install
288
+ pnpm lint && pnpm format:check && pnpm typecheck && pnpm test && pnpm build
289
+ ```
290
+
291
+ Publish:
292
+
293
+ ```bash
294
+ pnpm dlx release-it # bump, commit, tag
295
+ git push --follow-tags # CI publishes to npm + GHCR from the tag
296
+ ```
297
+
298
+ ## License
299
+
300
+ MIT
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ export {}
package/dist/cli.js ADDED
@@ -0,0 +1,51 @@
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-iu_3JECB.js";
3
+ import { ZodError } from "zod";
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+ //#region src/cli.ts
6
+ const stderrLogger = {
7
+ debug: (...args) => {
8
+ if (process.env.UNIFI_PROTECT_DEBUG) console.error("[unifi-protect-mcp]", ...args);
9
+ },
10
+ warn: (...args) => console.error("[unifi-protect-mcp]", ...args),
11
+ error: (...args) => console.error("[unifi-protect-mcp]", ...args)
12
+ };
13
+ /** Show a config mistake as its field messages, not 40 frames of zod internals. */
14
+ const describeFatal = (err) => {
15
+ if (err instanceof ZodError) return err.issues.map((issue) => {
16
+ const path = issue.path.join(".");
17
+ return path ? `${path}: ${issue.message}` : issue.message;
18
+ }).join("\n");
19
+ return err instanceof Error ? err.message : String(err);
20
+ };
21
+ const main = async () => {
22
+ stderrLogger.warn(`${BUILD_INFO.name}@${BUILD_INFO.version} (git ${BUILD_INFO.gitCommit} ${BUILD_INFO.gitCommitDate}, node ${process.version})`);
23
+ const config = loadConfig();
24
+ const { server } = createServer({
25
+ config,
26
+ logger: stderrLogger
27
+ });
28
+ const transport = new StdioServerTransport();
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"})`);
31
+ if (!isConfigured(config)) {
32
+ stderrLogger.warn(" not configured — only unifi_protect_auth_status is available:");
33
+ for (const line of setupInstructions(config)) stderrLogger.warn(` ${line}`);
34
+ stderrLogger.warn(" Call unifi_protect_auth_status for this same guidance in your client.");
35
+ }
36
+ const shutdown = (signal) => {
37
+ stderrLogger.warn(`received ${signal}, shutting down`);
38
+ process.exit(0);
39
+ };
40
+ process.on("SIGINT", () => shutdown("SIGINT"));
41
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
42
+ };
43
+ main().catch((err) => {
44
+ console.error(`[unifi-protect-mcp] fatal: ${describeFatal(err)}`);
45
+ if (process.env.UNIFI_PROTECT_DEBUG && err instanceof Error) console.error(err.stack);
46
+ process.exit(1);
47
+ });
48
+ //#endregion
49
+ export {};
50
+
51
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +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"}