@mgcrea/mcp-unifi-protect 0.4.0 → 1.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/README.md CHANGED
@@ -21,14 +21,14 @@ at all unless you ask for them.
21
21
 
22
22
  ## Two ways to connect
23
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 |
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 — pinned on first use | a real certificate, nothing to do |
30
+ | Works off-LAN | no | yes |
31
+ | Session state on disk | yes, mode `600` | none |
32
32
 
33
33
  **Both modes expose exactly the same tools**, because both speak the same private
34
34
  Protect API — the connector forwards the whole `/proxy/protect/...` tree, the private
@@ -51,7 +51,7 @@ UNIFI_PROTECT_CONSOLE_ID=… # curl -H "X-API-KEY: $KEY" https://api.ui.com/
51
51
  UNIFI_PROTECT_HOST=192.168.1.1
52
52
  UNIFI_PROTECT_USERNAME=mcp
53
53
  UNIFI_PROTECT_PASSWORD=…
54
- UNIFI_PROTECT_VERIFY_TLS=false
54
+ # TLS needs nothing: the console's certificate is pinned on the first request.
55
55
  ```
56
56
 
57
57
  `UNIFI_PROTECT_MODE` is inferred as `cloud` when an API key and a console id are both
@@ -112,27 +112,40 @@ Integration API — see [Not implemented](#not-implemented).
112
112
 
113
113
  ## Security
114
114
 
115
- **Supply chain.** Three runtime dependencies: the MCP SDK, zod, and `undici`. Retry and backoff
116
- are hand-rolled; there is no HTTP client wrapper, no logger, no crypto library. `undici` earns its
117
- place by being the only way to scope the TLS exception below to this server's own requests — see
118
- the note there. Published from CI with provenance via OIDC trusted publishing; the container
115
+ **Supply chain.** Three runtime dependencies: the MCP SDK, zod, and
116
+ [`@mgcrea/unifi-protect`](https://github.com/mgcrea/unifi-protect-client) the console client,
117
+ which is ours and whose own dependencies are `undici`, `ws` and zod. There is no HTTP client
118
+ wrapper, no logger, no crypto library. That client used to be a copy inside this repo; the copy
119
+ and the original had already begun to drift, which is a bad way to hold knowledge about an
120
+ undocumented API. Published from CI with provenance via OIDC trusted publishing; the container
119
121
  image is multi-arch, carries an SBOM, and is signed with cosign.
120
122
 
121
123
  **Your credentials.** The username and password come from the environment or a config file, and
122
124
  never leave this process except in the login request to your console. The resulting session
123
125
  cookie is cached at `~/.config/unifi-protect/session.json` with mode `600`.
124
126
 
125
- **Certificate verification is ON by default**, and disabling it is scoped to this server's own
127
+ **Certificate verification is ON by default, and now needs no setup.** The console's certificate
128
+ is _pinned_: on the first request the server reads it, records its SHA-256 fingerprint and PEM in
129
+ `~/.config/unifi-protect/trust.json` (mode `600`), and every connection afterwards verifies against
130
+ that one certificate as its own anchor. The host name check is replaced — not skipped — by a
131
+ fingerprint comparison, so addressing the console by IP is fine and a swapped certificate fails
132
+ hard.
133
+
134
+ This is what changed. The certificate is issued to `unifi.local` with **no IP SAN**, so reached by
135
+ an IP address, host name verification could never pass however the certificate was trusted; the
136
+ old advice was to set `NODE_EXTRA_CA_CERTS` _and_ address the console by a resolvable name, or
137
+ give up and disable verification. Pinning removes both requirements.
138
+
139
+ Pinning is trust-on-first-use, and it is only as good as that first moment. The fingerprint is
140
+ logged when it is learned and reported by `unifi_protect_auth_status`; set
141
+ `UNIFI_PROTECT_FINGERPRINT` to make the trust explicit instead, and a console that presents
142
+ anything else is then refused rather than adopted. If the console legitimately reissues its
143
+ certificate, delete the trust file.
144
+
145
+ `UNIFI_PROTECT_VERIFY_TLS=false` still turns checking off entirely, scoped to this server's own
126
146
  requests through an undici dispatcher — it is not `NODE_TLS_REJECT_UNAUTHORIZED`, so nothing else
127
- in the process is affected. (It previously _was_ process-wide, on the belief that a dispatcher
128
- could not be scoped without a dependency. It can, and `undici` is now that dependency.)
129
-
130
- Verifying takes two things together, and either alone achieves nothing: the certificate is
131
- self-signed, so `NODE_EXTRA_CA_CERTS` must point at it; **and** it is issued to `unifi.local` with
132
- no IP SAN, so `UNIFI_PROTECT_HOST` must be a host name that resolves to the console rather than its
133
- IP address. Reached by IP, verification fails on the host name however the certificate is trusted.
134
- See `.env.example` for the two commands. If the console has no name on your network, set
135
- `UNIFI_PROTECT_VERIFY_TLS=false`; the startup banner then prints `tls=UNVERIFIED` on every run.
147
+ in the process is affected. There should no longer be a reason to use it; the startup banner
148
+ prints `tls=UNVERIFIED` on every run when you do.
136
149
 
137
150
  **Blast radius.** With the defaults, the worst an agent can do is read your cameras and write
138
151
  image files into the snapshot directory. With `UNIFI_PROTECT_ALLOW_WRITES=1` it can additionally
@@ -147,7 +160,9 @@ Local-Access-Only account with View Only rights, and leave writes off unless you
147
160
  | `UNIFI_PROTECT_USERNAME` | yes | — | Console login |
148
161
  | `UNIFI_PROTECT_PASSWORD` | yes | — | Its password |
149
162
  | `UNIFI_PROTECT_TOTP` | no | — | 2FA code. Expires in ~30s — prefer `unifi_protect_auth_login` |
150
- | `UNIFI_PROTECT_VERIFY_TLS` | no | `true` | Verify the console's certificate (needs a host name, not an IP) |
163
+ | `UNIFI_PROTECT_VERIFY_TLS` | no | `true` | Verify the console's certificate. Off disables pinning entirely |
164
+ | `UNIFI_PROTECT_FINGERPRINT` | no | — | Pin this SHA-256 up front instead of learning it on first use |
165
+ | `UNIFI_PROTECT_TRUST_FILE` | no | `~/.config/unifi-protect/trust.json` | Where the pinned certificate is remembered, mode 600 |
151
166
  | `UNIFI_PROTECT_ALLOW_WRITES` | no | `false` | Register the 12 mutating tools |
152
167
  | `UNIFI_PROTECT_SESSION_FILE` | no | `~/.config/unifi-protect/session.json` | Cached session, mode 600 |
153
168
  | `UNIFI_PROTECT_SNAPSHOT_DIR` | no | `~/.cache/unifi-protect` | Where images and exports are written |
@@ -387,7 +402,7 @@ returns the setup steps as data.
387
402
  `UNIFI_PROTECT_ALLOW_WRITES=1`. That is the design, not a bug — an absent tool cannot be called,
388
403
  whereas a refused one invites an agent to keep trying.
389
404
 
390
- **`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`
405
+ **`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
391
406
  unless you have installed a trusted certificate on the console.
392
407
 
393
408
  **Cloud mode returns 403 `user cannot access host in the organization`.** The key is
@@ -432,6 +447,15 @@ writes — each value set to the value it already held — and the device state
432
447
  afterwards. That run is also what caught three bugs this README's earlier drafts described
433
448
  wrongly: the storage layout, the event-thumbnail id, and two camera fields that do not exist.
434
449
 
450
+ **Certificate pinning** was verified against that same console on 2026-08-31, addressed by IP
451
+ (`192.168.6.3`) with verification ON — the case that was impossible before. Confirmed in one run:
452
+ the certificate is captured and its fingerprint logged on first contact; the trust file is written
453
+ mode `600`; a restart reuses the pin without re-capturing; a correct `UNIFI_PROTECT_FINGERPRINT`
454
+ (colons and all) is accepted; and a wrong one is refused with a message naming both fingerprints,
455
+ surfacing as a failed tool call rather than a server that never starts. `auth_status` reported
456
+ `reachable: true` on Protect 7.2.105 throughout, and a session cached by the previous release was
457
+ restored unchanged — the on-disk format did not move.
458
+
435
459
  Two tools remain **unverified for want of hardware**, and are marked here rather than left to look
436
460
  tested:
437
461
 
@@ -446,11 +470,17 @@ Both follow the same PATCH shape as the tools that were verified, so they are li
446
470
 
447
471
  ## Not implemented
448
472
 
449
- The realtime WebSocket at `/proxy/protect/ws/updates` is not wired up. It is a binary framed
450
- protocol, and because this server wraps the private API, event _history_ is already available
451
- over REST through `unifi_protect_list_events` which is what the WebSocket would have been
452
- needed for. Node's global `WebSocket` follows the WHATWG signature and ignores a headers option,
453
- so attaching the session cookie would mean adding `ws` as a dependency.
473
+ The realtime WebSocket at `/proxy/protect/ws/updates` is not wired up here, though
474
+ `@mgcrea/unifi-protect` now implements it `connectEventStream` and `ProtectStore` are a
475
+ decoded frame stream and a live device cache, and this server deliberately uses neither.
476
+
477
+ The reason is that they are the wrong shape for an MCP server. Both want a long-lived process
478
+ that bootstraps at start-up and stays subscribed; this server must start with no credentials and
479
+ no connectivity at all, answering `unifi_protect_auth_status` until it is configured. It would
480
+ also gain little: because this wraps the private API, event _history_ is already available over
481
+ REST through `unifi_protect_list_events`, which is what a subscription would have been for.
482
+ Adopting it would mean deferring the bootstrap until credentials appear — worth doing if a tool
483
+ ever needs push rather than poll, and not before.
454
484
 
455
485
  ## Develop
456
486
 
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { G as BUILD_INFO, L as setupInstructions, M as isConfigured, N as loadConfig, i as createServer } from "./server-9inOsyBi.js";
2
+ import { A as isConfigured, H as BUILD_INFO, I as setupInstructions, i as createServer, j as loadConfig } from "./server-DE3vnfF5.js";
3
3
  import { ZodError } from "zod";
4
4
  import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
5
5
  //#region src/cli.ts
@@ -7,6 +7,7 @@ const stderrLogger = {
7
7
  debug: (...args) => {
8
8
  if (process.env.UNIFI_PROTECT_DEBUG) console.error("[unifi-protect-mcp]", ...args);
9
9
  },
10
+ info: (...args) => console.error("[unifi-protect-mcp]", ...args),
10
11
  warn: (...args) => console.error("[unifi-protect-mcp]", ...args),
11
12
  error: (...args) => console.error("[unifi-protect-mcp]", ...args)
12
13
  };
@@ -18,16 +19,29 @@ const describeFatal = (err) => {
18
19
  }).join("\n");
19
20
  return err instanceof Error ? err.message : String(err);
20
21
  };
22
+ /**
23
+ * How TLS will be handled, said once at startup.
24
+ *
25
+ * "pinned" is a promise about the first request rather than a report of one:
26
+ * the certificate is read lazily, because nothing here may open a socket — a
27
+ * console that is asleep has to surface as a failed tool call, not as a server
28
+ * that never starts.
29
+ */
30
+ const tlsBanner = (config) => {
31
+ if (config.mode === "cloud") return "verified";
32
+ if (!config.verifyTls) return "UNVERIFIED";
33
+ return config.fingerprint ? "pinned (fingerprint configured)" : "pinned on first use";
34
+ };
21
35
  const main = async () => {
22
36
  stderrLogger.warn(`${BUILD_INFO.name}@${BUILD_INFO.version} (git ${BUILD_INFO.gitCommit} ${BUILD_INFO.gitCommitDate}, node ${process.version})`);
23
37
  const config = loadConfig();
24
- const { server } = createServer({
38
+ const { server, close } = createServer({
25
39
  config,
26
40
  logger: stderrLogger
27
41
  });
28
42
  const transport = new StdioServerTransport();
29
43
  await server.connect(transport);
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"})`);
44
+ 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=${tlsBanner(config)})`);
31
45
  for (const issue of config.issues) stderrLogger.warn(` ${issue}`);
32
46
  if (!isConfigured(config)) {
33
47
  stderrLogger.warn(" not configured — only unifi_protect_auth_status is available:");
@@ -36,6 +50,7 @@ const main = async () => {
36
50
  }
37
51
  const shutdown = (signal) => {
38
52
  stderrLogger.warn(`received ${signal}, shutting down`);
53
+ close().catch(() => void 0);
39
54
  process.exit(0);
40
55
  };
41
56
  process.on("SIGINT", () => shutdown("SIGINT"));
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/server/stdio\";\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"}
1
+ {"version":3,"file":"cli.js","names":[],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { StdioServerTransport } from \"@modelcontextprotocol/server/stdio\";\nimport { ZodError } from \"zod\";\n\nimport { BUILD_INFO } from \"#/build-info\";\nimport { isConfigured, loadConfig, setupInstructions, type Config } 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 // Not optional despite nothing here calling it: @mgcrea/unifi-protect reports\n // a newly learned certificate fingerprint at info, and that is the one line\n // someone needs in order to pin it explicitly afterwards. Omitting `info`\n // drops it silently, because the client's Logger has every level optional.\n info: (...args: unknown[]) => console.error(\"[unifi-protect-mcp]\", ...args),\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\n/**\n * How TLS will be handled, said once at startup.\n *\n * \"pinned\" is a promise about the first request rather than a report of one:\n * the certificate is read lazily, because nothing here may open a socket — a\n * console that is asleep has to surface as a failed tool call, not as a server\n * that never starts.\n */\nconst tlsBanner = (config: Config): string => {\n if (config.mode === \"cloud\") return \"verified\";\n if (!config.verifyTls) return \"UNVERIFIED\";\n return config.fingerprint ? \"pinned (fingerprint configured)\" : \"pinned on first use\";\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\n const { server, close } = 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=${tlsBanner(config)})`,\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 // Releases the dispatcher's keep-alive sockets. Best-effort: the exit below\n // is what actually ends the process, and a hung close must not delay it.\n void close().catch(() => undefined);\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;CAKA,OAAO,GAAG,SAAoB,QAAQ,MAAM,uBAAuB,GAAG,IAAI;CAC1E,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;;;;;;;;;AAUA,MAAM,aAAa,WAA2B;CAC5C,IAAI,OAAO,SAAS,SAAS,OAAO;CACpC,IAAI,CAAC,OAAO,WAAW,OAAO;CAC9B,OAAO,OAAO,cAAc,oCAAoC;AAClE;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;CAE1B,MAAM,EAAE,QAAQ,UAAU,aAAa;EAAE;EAAQ,QAAQ;CAAa,CAAC;CACvE,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,UAAU,MAAM,EAAE,EAC7B;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;EAGrD,MAAW,CAAC,CAAC,YAAY,KAAA,CAAS;EAClC,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"}