@mgcrea/mcp-unifi-protect 0.0.0 → 0.2.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/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-iu_3JECB.js";
2
+ import { B as loadConfig, G as BUILD_INFO, W as setupInstructions, i as createServer, z as isConfigured } from "./server-B2-0Akro.js";
3
3
  import { ZodError } from "zod";
4
4
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
5
  //#region src/cli.ts
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-iu_3JECB.js";
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";
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 };
@@ -21,8 +21,8 @@ const pkg = readPackageJson();
21
21
  const BUILD_INFO = {
22
22
  name: pkg.name,
23
23
  version: pkg.version,
24
- gitCommit: "b7fe70a",
25
- gitCommitDate: "2026-08-30T10:17:29+02:00"
24
+ gitCommit: "e314041",
25
+ gitCommitDate: "2026-08-30T10:23:09+02:00"
26
26
  };
27
27
  //#endregion
28
28
  //#region src/config.ts
@@ -1740,4 +1740,4 @@ const createServer = (opts) => {
1740
1740
  //#endregion
1741
1741
  export { saveSession as A, loadConfig as B, summarizeSensor as C, staticSessionProvider as D, createSessionProvider as E, LOGIN_PATH as F, BUILD_INFO as G, resolveConfigPath as H, PRIVATE_API_PATH as I, UPDATES_WS_PATH as L, ProtectApiError as M, ProtectAuthError as N, clearSession as O, WritesDisabledError as P, expandTilde as R, summarizeNvr as S, summarizeViewer as T, resolveSessionPath as U, normalizeBaseUrl as V, setupInstructions as W, summarizeChime as _, registerTools as a, summarizeLight as b, ProtectClient as c, retryAfterMs as d, createDeviceCache as f, summarizeCamera as g, summarizeBootstrap as h, createServer as i, NotConfiguredError as j, loadSession as k, backoffMs as l, isoTime as m, SERVER_VERSION as n, assertSafePath as o, buildNameIndex as p, USER_AGENT as r, toEpochMs as s, SERVER_NAME as t, buildQuery as u, summarizeEach as v, summarizeUser as w, summarizeLiveview as x, summarizeEvent as y, isConfigured as z };
1742
1742
 
1743
- //# sourceMappingURL=server-iu_3JECB.js.map
1743
+ //# sourceMappingURL=server-B2-0Akro.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"server-iu_3JECB.js","names":["undiciFetch"],"sources":["../src/build-info.ts","../src/config.ts","../src/client/errors.ts","../src/client/session-store.ts","../src/client/auth.ts","../src/client/shape.ts","../src/client/device-cache.ts","../src/client/protect.ts","../src/client/tls.ts","../src/tools/util.ts","../src/tools/cameras.ts","../src/tools/devices.ts","../src/tools/events.ts","../src/tools/request.ts","../src/tools/status.ts","../src/tools/system.ts","../src/tools/index.ts","../src/server.ts"],"sourcesContent":["// Build-time / runtime identity for the running server. `name`/`version` are\n// read from package.json at startup (always accurate); `gitCommit` /\n// `gitCommitDate` are injected by tsdown's `define` substitution at build time\n// and fall back to \"unknown\" when running from source (e.g. vitest).\n\nimport { readFileSync } from \"node:fs\";\n\n// oxlint-disable no-underscore-dangle -- bundler-injected build-time constants.\ndeclare const __GIT_COMMIT__: string;\ndeclare const __GIT_COMMIT_DATE__: string;\n\ntype PackageJson = { name: string; version: string };\n\nconst readPackageJson = (): PackageJson => {\n try {\n const pkgUrl = new URL(\"../package.json\", import.meta.url);\n return JSON.parse(readFileSync(pkgUrl, \"utf8\")) as PackageJson;\n } catch {\n return { name: \"@mgcrea/mcp-unifi-protect\", version: \"0.0.0\" };\n }\n};\n\nconst pkg = readPackageJson();\n\nexport type BuildInfo = {\n name: string;\n version: string;\n gitCommit: string;\n gitCommitDate: string;\n};\n\nexport const BUILD_INFO: BuildInfo = {\n name: pkg.name,\n version: pkg.version,\n gitCommit: typeof __GIT_COMMIT__ === \"string\" ? __GIT_COMMIT__ : \"unknown\",\n gitCommitDate: typeof __GIT_COMMIT_DATE__ === \"string\" ? __GIT_COMMIT_DATE__ : \"unknown\",\n};\n","import { readFileSync, statSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\n\nimport { z } from \"zod\";\n\n/**\n * The private (undocumented) Protect API, mounted under UniFi OS's proxy. This\n * server deliberately wraps this rather than the official Integration API at\n * `/proxy/protect/integration/v1`: the official one has NO historical query\n * capability at all — its only query parameters in the entire published OpenAPI\n * spec are `channel`, `highQuality` and `qualities` — so \"what happened at the\n * front door last night\" is unanswerable through it.\n *\n * The trade is real and accepted: nothing below is contractual, and Ubiquiti\n * moves these endpoints between Protect releases. `unifi_protect_get_system_info`\n * reports the running version so a mismatch is visible, and\n * `unifi_protect_request` reaches anything that moved without a code change.\n */\nexport const PRIVATE_API_PATH = \"/proxy/protect/api\";\n\n/** UniFi OS's own auth surface, which is NOT under the Protect proxy path. */\nexport const LOGIN_PATH = \"/api/auth/login\";\n\n/** The realtime channel. Not used yet — see the WebSocket note in the README. */\nexport const UPDATES_WS_PATH = \"/proxy/protect/ws/updates\";\n\nconst ConfigSchema = z\n .object({\n /** Console origin, e.g. `https://192.168.1.1` or `https://10.0.0.1:8443`. */\n baseUrl: z.string().min(1).optional(),\n username: z.string().min(1).optional(),\n password: z.string().min(1).optional(),\n /**\n * A 2FA code is single-use and expires in ~30s, so it is only ever useful\n * for a one-shot `unifi_protect_auth_login`, never for an unattended start.\n */\n totp: z.string().min(1).optional(),\n /**\n * On by default. Turning it off is scoped to this server's own requests via\n * an undici dispatcher, not the whole process.\n *\n * Verifying takes two things together, and one alone does nothing: the\n * console's certificate is self-signed (so NODE_EXTRA_CA_CERTS must point at\n * it) AND it carries no IP SAN — `CN=unifi.local`, SANs for `unifi.local`,\n * `localhost` and `127.0.0.1` — so UNIFI_PROTECT_HOST must be a host name\n * that resolves to the console, never its IP address.\n */\n verifyTls: z.boolean().default(true),\n allowWrites: z.boolean().default(false),\n sessionFile: z.string().min(1),\n snapshotDir: z.string().min(1),\n maxRetries: z.number().int().nonnegative().max(10).default(3),\n /** Guards against an unexpectedly huge video export blowing up the process. */\n maxDownloadBytes: z.number().int().positive().default(200_000_000),\n /** How long a cached camera id→name index stays fresh, in seconds. */\n deviceCacheTtlSeconds: z.number().int().nonnegative().max(3600).default(60),\n })\n .strict()\n .superRefine((cfg, ctx) => {\n // Deliberately NOT an error when nothing is configured. An MCP server that\n // exits at startup shows up in the client as a bare \"MCP error -32000:\n // Connection closed\" with stderr swallowed — so the one message that would\n // have explained what to set never reaches anyone. The server stays up,\n // registers unifi_protect_auth_status, and reports the gap as data.\n //\n // A half-configured install IS worth flagging, though: a host with no\n // password is a typo, not a deliberate state, and saying so beats letting\n // every call fail with an auth error that reads like a wrong password.\n if (cfg.baseUrl && !cfg.username) {\n ctx.addIssue({\n code: \"custom\",\n path: [\"username\"],\n message:\n \"UNIFI_PROTECT_HOST is set but UNIFI_PROTECT_USERNAME is not. Both, plus \" +\n \"UNIFI_PROTECT_PASSWORD, are needed to reach the console.\",\n });\n }\n if (cfg.username && !cfg.password) {\n ctx.addIssue({\n code: \"custom\",\n path: [\"password\"],\n message: \"UNIFI_PROTECT_USERNAME is set but UNIFI_PROTECT_PASSWORD is not.\",\n });\n }\n });\n\nexport type Config = z.infer<typeof ConfigSchema>;\n\n/**\n * The on-disk config document. Keys are camelCase to mirror `Config` rather\n * than the env var names: this is a typed JSON file, not a shell.\n *\n * `.strict()` on purpose — a typo'd `userName` must be an error. Silently\n * ignoring an unknown key looks exactly like \"that setting had no effect\",\n * which is the worst way to learn your credentials came from somewhere else.\n */\nconst FileConfigSchema = z\n .object({\n host: z.string().min(1).optional(),\n username: z.string().min(1).optional(),\n password: z.string().min(1).optional(),\n verifyTls: z.boolean().optional(),\n allowWrites: z.boolean().optional(),\n sessionFile: z.string().min(1).optional(),\n snapshotDir: z.string().min(1).optional(),\n maxRetries: z.number().int().nonnegative().max(10).optional(),\n maxDownloadBytes: z.number().int().positive().optional(),\n deviceCacheTtlSeconds: z.number().int().nonnegative().max(3600).optional(),\n })\n .strict();\n\nexport type FileConfig = z.infer<typeof FileConfigSchema>;\n\n/**\n * Normalize whatever someone pasted into a console origin, preserving the port.\n *\n * \"192.168.1.1\" -> \"https://192.168.1.1\"\n * \"10.0.0.1:8443\" -> \"https://10.0.0.1:8443\"\n * \"https://udm.lan/protect/\" -> \"https://udm.lan\"\n *\n * A port must survive: consoles are commonly reached on a non-443 port, and the\n * normalizers in mcp-keycloak and mcp-shopify both drop it. Everything after\n * the origin is discarded — the API paths are this server's business, and a\n * pasted `/protect/dashboard` URL would otherwise be prefixed onto every call.\n *\n * `https` is forced: UniFi OS redirects plain HTTP, and following that redirect\n * would send the session cookie over cleartext on the first hop.\n */\nexport const normalizeBaseUrl = (raw: string): string => {\n const trimmedRaw = raw.trim().replace(/\\/+$/, \"\");\n const withScheme = /^https?:\\/\\//i.test(trimmedRaw) ? trimmedRaw : `https://${trimmedRaw}`;\n const url = new URL(withScheme);\n // `url.host` rather than `hostname` so an explicit :port is kept.\n return `https://${url.host}`;\n};\n\nconst parseBool = (value: string | undefined): boolean | undefined => {\n const t = trimmed(value);\n if (t === undefined) return undefined;\n return [\"1\", \"true\", \"yes\", \"on\"].includes(t.toLowerCase());\n};\n\nconst parseIntOpt = (value: string | undefined): number | undefined => {\n if (value === undefined || value.trim() === \"\") return undefined;\n const n = Number(value);\n return Number.isInteger(n) ? n : undefined;\n};\n\n/** Maps \"\" to undefined, so an empty env var means \"unset\" rather than \"empty\". */\nconst trimmed = (value: string | undefined): string | undefined => {\n const t = value?.trim();\n return t ? t : undefined;\n};\n\nconst message = (err: unknown): string => (err instanceof Error ? err.message : String(err));\n\n/** `readFileSync` does not expand `~`, but it is the natural thing to write in a config file. */\nexport const expandTilde = (path: string): string =>\n path === \"~\" || path.startsWith(\"~/\") ? join(homedir(), path.slice(1)) : path;\n\n/**\n * Where the config file lives, most specific first: an explicit override, then\n * the XDG location, then the conventional `~/.config`.\n */\nexport const resolveConfigPath = (env: NodeJS.ProcessEnv = process.env): string => {\n const explicit = trimmed(env.UNIFI_PROTECT_CONFIG);\n if (explicit) return expandTilde(explicit);\n const base = trimmed(env.XDG_CONFIG_HOME) ?? join(homedir(), \".config\");\n return join(expandTilde(base), \"unifi-protect\", \"config.json\");\n};\n\n/** The session file sits beside the config file unless told otherwise. */\nexport const resolveSessionPath = (env: NodeJS.ProcessEnv = process.env): string =>\n join(dirname(resolveConfigPath(env)), \"session.json\");\n\n/**\n * The session file holds a live console cookie, so being readable by other\n * users is worth saying out loud. A warning and not an error: refusing to start\n * would be a worse trade for someone on a single-user machine.\n */\nexport const warnIfGroupReadable = (path: string): void => {\n if (process.platform === \"win32\") return; // mode bits mean nothing here\n try {\n if (statSync(path).mode & 0o077) {\n process.stderr.write(\n `[unifi-protect] ${path} is readable by other users. Run: chmod 600 ${path}\\n`,\n );\n }\n } catch {\n // Not worth failing startup over.\n }\n};\n\n/**\n * Read the config file, treating \"absent\" as \"contributes nothing\". Every other\n * failure throws and names the path, so a malformed file is never mistaken for\n * a missing one — that confusion sends you hunting for credentials that were\n * sitting right there.\n */\nconst readConfigFile = (path: string): FileConfig => {\n let raw: string;\n try {\n raw = readFileSync(path, \"utf8\");\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") return {};\n throw new Error(`Could not read the config file (${path}): ${message(err)}`, { cause: err });\n }\n\n warnIfGroupReadable(path);\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (err) {\n throw new Error(`The config file (${path}) is not valid JSON: ${message(err)}`, { cause: err });\n }\n\n const result = FileConfigSchema.safeParse(parsed);\n if (!result.success) {\n const issues = result.error.issues\n .map((issue) => `${issue.path.join(\".\") || \"(root)\"}: ${issue.message}`)\n .join(\"; \");\n throw new Error(`The config file (${path}) is not valid: ${issues}`);\n }\n return result.data;\n};\n\n/**\n * Environment first, config file second, **per field** — not whole-source.\n * Docker and CI inject the environment and must keep working untouched, while a\n * one-off `UNIFI_PROTECT_ALLOW_WRITES=0` still has to override a file that says\n * `true`. Merging field by field is the only rule that gives both.\n */\nexport const loadConfig = (\n env: NodeJS.ProcessEnv = process.env,\n configPath: string = resolveConfigPath(env),\n): Config => {\n const file = readConfigFile(configPath);\n const host = trimmed(env.UNIFI_PROTECT_HOST) ?? file.host;\n const sessionFile =\n trimmed(env.UNIFI_PROTECT_SESSION_FILE) ?? file.sessionFile ?? resolveSessionPath(env);\n const snapshotDir =\n trimmed(env.UNIFI_PROTECT_SNAPSHOT_DIR) ??\n file.snapshotDir ??\n join(homedir(), \".cache\", \"unifi-protect\");\n\n return ConfigSchema.parse({\n baseUrl: host ? normalizeBaseUrl(host) : undefined,\n username: trimmed(env.UNIFI_PROTECT_USERNAME) ?? file.username,\n password: trimmed(env.UNIFI_PROTECT_PASSWORD) ?? file.password,\n totp: trimmed(env.UNIFI_PROTECT_TOTP),\n verifyTls: parseBool(env.UNIFI_PROTECT_VERIFY_TLS) ?? file.verifyTls,\n allowWrites: parseBool(env.UNIFI_PROTECT_ALLOW_WRITES) ?? file.allowWrites,\n sessionFile: expandTilde(sessionFile),\n snapshotDir: expandTilde(snapshotDir),\n maxRetries: parseIntOpt(env.UNIFI_PROTECT_MAX_RETRIES) ?? file.maxRetries,\n maxDownloadBytes: parseIntOpt(env.UNIFI_PROTECT_MAX_DOWNLOAD_BYTES) ?? file.maxDownloadBytes,\n deviceCacheTtlSeconds:\n parseIntOpt(env.UNIFI_PROTECT_DEVICE_CACHE_TTL) ?? file.deviceCacheTtlSeconds,\n });\n};\n\n/** True once the server has everything it needs to reach a console. */\nexport const isConfigured = (config: Config): boolean =>\n Boolean(config.baseUrl && config.username && config.password);\n\n/**\n * Returned by unifi_protect_auth_status and printed to stderr at startup. Prose\n * rather than a code, because this is the text someone acts on when nothing\n * works — and the server can no longer signal it by refusing to start.\n */\nexport const setupInstructions = (config: Config): string[] => {\n if (isConfigured(config)) return [];\n return [\n \"No UniFi Protect console is configured, so only unifi_protect_auth_status is registered.\",\n \"Set UNIFI_PROTECT_HOST to your console's address — the IP or hostname of the UDM Pro, \" +\n \"UNVR or Cloud Key, e.g. 192.168.1.1 (https:// is assumed, and a :port is preserved).\",\n \"Set UNIFI_PROTECT_USERNAME and UNIFI_PROTECT_PASSWORD to a console login.\",\n // The advice that actually matters. A Ubiquiti SSO account often cannot log\n // in locally at all, and reusing the owner account hands an agent the keys\n // to the whole console rather than to Protect.\n \"Create a dedicated LOCAL user for this rather than reusing your own: UniFi OS → Settings → \" +\n \"Admins & Users → Add User → Local Access Only, and give it Protect permissions only. A \" +\n \"Ubiquiti cloud (SSO) account may fail local login entirely, and a local account keeps the \" +\n \"blast radius to Protect.\",\n \"Give that user View-Only rights unless you intend to set UNIFI_PROTECT_ALLOW_WRITES=1 — \" +\n \"with writes off the mutating tools are not registered at all, so they cannot be called.\",\n \"If the account has 2FA, pass a current code once via unifi_protect_auth_login; the session \" +\n \"is then cached and reused. A code in UNIFI_PROTECT_TOTP expires in about 30 seconds, so \" +\n \"it is no use for an unattended start.\",\n \"Certificate verification is ON by default. A console reached by IP can never pass it: \" +\n \"the certificate is self-signed AND has no IP SAN. Either set UNIFI_PROTECT_HOST to a host \" +\n \"name that resolves to the console and point NODE_EXTRA_CA_CERTS at its certificate, or \" +\n \"set UNIFI_PROTECT_VERIFY_TLS=false, which is scoped to this server's requests only. \" +\n \"Set it to true only if you installed a certificate your machine already trusts.\",\n ];\n};\n","/** A non-2xx answer from the console. */\nexport class ProtectApiError extends Error {\n override readonly name = \"ProtectApiError\";\n readonly status: number;\n /** The request path, so an error names what failed without a stack trace. */\n readonly path: string | undefined;\n readonly errors: unknown;\n\n constructor(\n message: string,\n opts: { status: number; path?: string | undefined; errors?: unknown },\n ) {\n super(message);\n this.status = opts.status;\n this.path = opts.path;\n this.errors = opts.errors;\n }\n}\n\n/** The console rejected the credentials, or 2FA is required and was not supplied. */\nexport class ProtectAuthError extends Error {\n override readonly name = \"ProtectAuthError\";\n /** True when the console asked for a 2FA code rather than refusing the password. */\n readonly needsTwoFactor: boolean;\n\n constructor(message: string, opts: { needsTwoFactor?: boolean } = {}) {\n super(message);\n this.needsTwoFactor = opts.needsTwoFactor ?? false;\n }\n}\n\n/** Thrown when a write path is reached while UNIFI_PROTECT_ALLOW_WRITES is off. */\nexport class WritesDisabledError extends Error {\n override readonly name = \"WritesDisabledError\";\n\n constructor(what: string) {\n super(\n `${what} is a write operation, but writes are disabled. ` +\n `Set UNIFI_PROTECT_ALLOW_WRITES=1 to register the mutating tools.`,\n );\n }\n}\n\n/** Thrown when the server is asked to reach a console it has no credentials for. */\nexport class NotConfiguredError extends Error {\n override readonly name = \"NotConfiguredError\";\n\n constructor() {\n super(\n \"No UniFi Protect console is configured. Call unifi_protect_auth_status for the setup steps.\",\n );\n }\n}\n","import { chmod, mkdir, readFile, unlink, writeFile } from \"node:fs/promises\";\nimport { dirname } from \"node:path\";\n\n/**\n * A console session: the bare cookie value plus the CSRF token that was current\n * when it was issued. Both are needed on every request, and neither is derivable\n * from the other, so they are stored and invalidated as one unit.\n */\nexport type PersistedSession = {\n /** The `TOKEN=<jwt>` pair, attributes already stripped. */\n cookie: string;\n csrfToken: string;\n /** Which console this belongs to, so a changed host does not reuse a stale session. */\n baseUrl: string;\n /** Which account it was issued to, for the same reason. */\n username: string;\n savedAt: string;\n};\n\n/** Load a persisted session, or undefined if none / unreadable / not JSON. */\nexport const loadSession = async (path: string): Promise<PersistedSession | undefined> => {\n try {\n const raw = await readFile(path, \"utf8\");\n const parsed = JSON.parse(raw) as Partial<PersistedSession>;\n // A truncated or hand-edited file must read as \"no session\" rather than\n // producing a request with `cookie: undefined`, which fails as a 401 and\n // sends you looking at the password.\n if (!parsed.cookie || !parsed.csrfToken || !parsed.baseUrl || !parsed.username) {\n return undefined;\n }\n return parsed as PersistedSession;\n } catch {\n return undefined;\n }\n};\n\n/** Persist a session with owner-only permissions. */\nexport const saveSession = async (path: string, session: PersistedSession): Promise<void> => {\n await mkdir(dirname(path), { recursive: true });\n await writeFile(path, JSON.stringify(session, null, 2), { mode: 0o600 });\n // `mode` on writeFile only applies when the file is CREATED, so a session\n // rewritten over a file that already existed keeps whatever mode it had.\n // chmod every time rather than trusting the create bit.\n await chmod(path, 0o600);\n};\n\n/** Remove a persisted session. Absent is success — logout is idempotent. */\nexport const clearSession = async (path: string): Promise<void> => {\n try {\n await unlink(path);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== \"ENOENT\") throw err;\n }\n};\n","import { LOGIN_PATH, type Config } from \"../config.js\";\nimport { ProtectAuthError } from \"./errors.js\";\nimport { clearSession, loadSession, saveSession, type PersistedSession } from \"./session-store.js\";\n\nexport type Logger = {\n debug?(...args: unknown[]): void;\n warn?(...args: unknown[]): void;\n error?(...args: unknown[]): void;\n};\n\n/** The two headers that authenticate every request to a UniFi OS console. */\nexport type SessionHeaders = {\n cookie: string;\n \"x-csrf-token\": string;\n};\n\nexport type SessionStatus = {\n authenticated: boolean;\n /** Where the live session came from, for unifi_protect_auth_status. */\n source: \"none\" | \"restored\" | \"login\";\n username: string | undefined;\n savedAt: string | undefined;\n};\n\n/**\n * A pluggable source of console session headers. The client calls `headers()`\n * on every request and `invalidate()` on a 401 to force the next call to\n * re-run the handshake.\n */\nexport type SessionProvider = {\n headers(): Promise<SessionHeaders>;\n invalidate(): void;\n /** Force a fresh handshake now, optionally with a 2FA code. Used by auth_login. */\n login(totp?: string): Promise<SessionStatus>;\n /** Drop the session in memory and on disk. */\n logout(): Promise<void>;\n describe(): SessionStatus;\n};\n\nexport type SessionProviderOptions = {\n config: Config;\n fetch?: typeof fetch;\n logger?: Logger;\n now?: () => number;\n};\n\n/**\n * Read one header, collapsing undici's `string | string[] | undefined` shape.\n * Multi-value headers (the Set-Cookie shape) collapse to their first entry,\n * which is all the handshake needs — the session is a single cookie.\n */\nconst header = (res: Response, name: string): string | undefined =>\n res.headers.get(name) ?? undefined;\n\n/**\n * Keep only the `TOKEN=value` pair, dropping the cookie's attributes (Path,\n * Expires, HttpOnly, …). Sending those attributes back on a request header is\n * malformed and the console rejects the session.\n */\nconst bareCookie = (setCookie: string): string => {\n const semicolon = setCookie.indexOf(\";\");\n return semicolon === -1 ? setCookie : setCookie.slice(0, semicolon);\n};\n\n/**\n * Whether a login response is asking for a second factor rather than refusing\n * the password. Protect answers 499 for this; some firmwares use a 401 with a\n * body naming the requirement, so both are probed.\n */\nconst isTwoFactorChallenge = (status: number, body: string): boolean =>\n status === 499 || /2fa|two[- ]factor|mfa|otp/i.test(body);\n\nexport const createSessionProvider = (opts: SessionProviderOptions): SessionProvider => {\n const { config } = opts;\n const fetchImpl = opts.fetch ?? fetch;\n const logger = opts.logger;\n\n let session: PersistedSession | undefined;\n let source: SessionStatus[\"source\"] = \"none\";\n // A single in-flight promise coordinates concurrent callers. Unlike a locally\n // signed token, a console login is a network call that must not be issued\n // twice: the console rotates the CSRF token on every successful login, so two\n // races leave whichever finished last, which need not be the one whose token\n // the caller went on to use.\n let inflight: Promise<PersistedSession> | undefined;\n let restored = false;\n\n const requireCredentials = (): { baseUrl: string; username: string; password: string } => {\n if (!config.baseUrl || !config.username || !config.password) {\n throw new ProtectAuthError(\n \"No console credentials are configured. Call unifi_protect_auth_status for the setup steps.\",\n );\n }\n return { baseUrl: config.baseUrl, username: config.username, password: config.password };\n };\n\n /**\n * Fetch a CSRF token from the console root. UniFi OS gates login behind CSRF\n * protection, so a first login on a cold start has nothing to present until\n * this runs.\n */\n const fetchCsrfToken = async (baseUrl: string): Promise<string | undefined> => {\n try {\n const res = await fetchImpl(baseUrl, { method: \"GET\", redirect: \"manual\" });\n return header(res, \"x-csrf-token\");\n } catch (err) {\n logger?.debug?.(`could not prefetch a CSRF token: ${String(err)}`);\n return undefined;\n }\n };\n\n /** Run the UniFi OS handshake and return a complete session. */\n const handshake = async (totp?: string): Promise<PersistedSession> => {\n const { baseUrl, username, password } = requireCredentials();\n const url = `${baseUrl}${LOGIN_PATH}`;\n const code = totp ?? config.totp;\n\n const attempt = async (csrf: string | undefined): Promise<Response> =>\n fetchImpl(url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n ...(csrf ? { \"x-csrf-token\": csrf } : {}),\n },\n body: JSON.stringify({\n username,\n password,\n // `token` is what the console calls the 2FA code. Omitted entirely\n // rather than sent as null, which some firmwares reject outright.\n ...(code ? { token: code } : {}),\n rememberMe: true,\n }),\n redirect: \"manual\",\n });\n\n logger?.debug?.(`logging in to ${baseUrl} as ${username}`);\n let res = await attempt(session?.csrfToken);\n\n // Rejected with no CSRF token to offer: fetch one from the root document\n // and retry the login exactly once.\n if (!res.ok && !session?.csrfToken) {\n const csrf = await fetchCsrfToken(baseUrl);\n if (csrf) {\n logger?.debug?.(\"retrying login with a freshly fetched CSRF token\");\n res = await attempt(csrf);\n }\n }\n\n if (!res.ok) {\n const body = await res.text().catch(() => \"\");\n if (isTwoFactorChallenge(res.status, body)) {\n throw new ProtectAuthError(\n \"The console requires a two-factor code. Call unifi_protect_auth_login with a current \" +\n \"code from your authenticator app — the session is cached afterwards, so this is a \" +\n \"one-off.\",\n { needsTwoFactor: true },\n );\n }\n throw new ProtectAuthError(\n `Login to ${baseUrl} as ${username} failed: HTTP ${res.status} ${res.statusText}. ` +\n `Check the username and password, and note that a Ubiquiti cloud (SSO) account often ` +\n `cannot log in locally — create a Local Access Only user instead.`,\n );\n }\n\n // The console rotates the CSRF token on a successful login and returns the\n // new one in X-Updated-CSRF-Token. Preferring that over X-CSRF-Token matters:\n // taking the stale one makes the FIRST request succeed and every later one\n // fail, which reads like an expiry problem and is not one.\n const csrfToken = header(res, \"x-updated-csrf-token\") ?? header(res, \"x-csrf-token\");\n const setCookie = res.headers.getSetCookie()[0] ?? header(res, \"set-cookie\");\n\n if (!csrfToken || !setCookie) {\n throw new ProtectAuthError(\n `Login to ${baseUrl} succeeded (HTTP ${res.status}) but returned no ` +\n `${!setCookie ? \"session cookie\" : \"CSRF token\"}. This usually means the host is not a ` +\n `UniFi OS console — check UNIFI_PROTECT_HOST points at the console itself, not at a ` +\n `reverse proxy in front of it.`,\n );\n }\n\n const next: PersistedSession = {\n cookie: bareCookie(setCookie),\n csrfToken,\n baseUrl,\n username,\n savedAt: new Date(opts.now?.() ?? Date.now()).toISOString(),\n };\n await saveSession(config.sessionFile, next).catch((err: unknown) => {\n // A session that cannot be persisted is still usable in memory; failing\n // the login over it would be a worse trade.\n logger?.warn?.(`could not persist the session to ${config.sessionFile}: ${String(err)}`);\n });\n return next;\n };\n\n /**\n * Reuse a session from disk when it belongs to the console and account we are\n * configured for. A session for a different host or user is not merely\n * useless, it would authenticate as the wrong identity.\n */\n const restore = async (): Promise<PersistedSession | undefined> => {\n if (restored) return undefined;\n restored = true;\n const persisted = await loadSession(config.sessionFile);\n if (!persisted) return undefined;\n if (persisted.baseUrl !== config.baseUrl || persisted.username !== config.username) {\n logger?.debug?.(\"ignoring a persisted session issued for a different console or user\");\n return undefined;\n }\n logger?.debug?.(`restored a session from ${config.sessionFile}`);\n return persisted;\n };\n\n const ensure = async (totp?: string): Promise<PersistedSession> => {\n if (session) return session;\n\n // Try disk before the network, but only once per process: a restored\n // session that turns out to be expired comes back as a 401, which\n // invalidate() clears and the next call logs in properly.\n const fromDisk = await restore();\n if (fromDisk) {\n session = fromDisk;\n source = \"restored\";\n return session;\n }\n\n if (!inflight) {\n inflight = handshake(totp).finally(() => {\n inflight = undefined;\n });\n }\n session = await inflight;\n source = \"login\";\n return session;\n };\n\n return {\n async headers(): Promise<SessionHeaders> {\n const live = await ensure();\n return { cookie: live.cookie, \"x-csrf-token\": live.csrfToken };\n },\n\n invalidate(): void {\n session = undefined;\n // A restored-but-expired session must not be restored a second time, or\n // the 401 retry loop would keep replaying the same dead cookie until it\n // runs out of attempts and reports a credentials problem that isn't one.\n restored = true;\n },\n\n async login(totp?: string): Promise<SessionStatus> {\n session = undefined;\n restored = true;\n session = await handshake(totp);\n source = \"login\";\n return this.describe();\n },\n\n async logout(): Promise<void> {\n session = undefined;\n restored = true;\n source = \"none\";\n await clearSession(config.sessionFile);\n },\n\n describe(): SessionStatus {\n return {\n authenticated: session !== undefined,\n source,\n username: session?.username ?? config.username,\n savedAt: session?.savedAt,\n };\n },\n };\n};\n\n/** For tests: fixed headers, no network, no disk. */\nexport const staticSessionProvider = (\n headers: SessionHeaders = { cookie: \"TOKEN=test\", \"x-csrf-token\": \"csrf-test\" },\n): SessionProvider => ({\n headers: async () => headers,\n invalidate: () => {},\n login: async () => ({\n authenticated: true,\n source: \"login\",\n username: \"test\",\n savedAt: undefined,\n }),\n logout: async () => {},\n describe: () => ({ authenticated: true, source: \"login\", username: \"test\", savedAt: undefined }),\n});\n","// UniFi Protect's private API answers with the console's internal representation,\n// which is built for its own web app and not for a context window. One camera in\n// `bootstrap` runs to roughly 8-15 KB of JSON once `channels[]` (three encoder\n// profiles, ~20 fields each), `stats`, `featureFlags` (around 60 booleans),\n// `ispSettings`, `osdSettings`, `motionZones`, `smartDetectZones`, `privacyZones`\n// and `lenses` are counted. A ten-camera system is well over 100 KB from what\n// looks like a one-line \"list my cameras\".\n//\n// So list tools return an allowlist of the fields someone actually asks about,\n// and `get_*` tools return the full object — that is the point of a get.\n\ntype Rec = Record<string, unknown>;\n\nconst isRecord = (value: unknown): value is Rec =>\n typeof value === \"object\" && value !== null && !Array.isArray(value);\n\nconst str = (value: unknown): string | undefined =>\n typeof value === \"string\" && value.length > 0 ? value : undefined;\n\n/** Apply a summarizer across an array, passing non-arrays through untouched. */\nexport const summarizeEach = <T>(value: unknown, fn: (item: Rec) => T): unknown =>\n Array.isArray(value) ? value.filter(isRecord).map(fn) : value;\n\n/**\n * Protect timestamps are milliseconds since the Unix epoch. Rendering them as\n * ISO 8601 costs a few characters and saves the model from having to reason\n * about a bare 13-digit integer — which it does get wrong, usually by reading\n * it as seconds and landing in 1970.\n */\nexport const isoTime = (value: unknown): string | undefined => {\n if (typeof value !== \"number\" || !Number.isFinite(value) || value <= 0) return undefined;\n return new Date(value).toISOString();\n};\n\n/** An index from device id to display name, used to resolve event references. */\nexport type NameIndex = ReadonlyMap<string, string>;\n\nexport const buildNameIndex = (devices: unknown): NameIndex => {\n const index = new Map<string, string>();\n if (!Array.isArray(devices)) return index;\n for (const device of devices) {\n if (!isRecord(device)) continue;\n const id = str(device.id);\n const name = str(device.name);\n if (id && name) index.set(id, name);\n }\n return index;\n};\n\nexport const summarizeCamera = (camera: Rec): Rec => {\n const flags = isRecord(camera.featureFlags) ? camera.featureFlags : {};\n const recording = isRecord(camera.recordingSettings) ? camera.recordingSettings : {};\n const smart = isRecord(camera.smartDetectSettings) ? camera.smartDetectSettings : {};\n return {\n id: camera.id,\n name: camera.name,\n type: camera.type,\n mac: camera.mac,\n host: camera.host,\n isConnected: camera.isConnected,\n state: camera.state,\n isRecording: camera.isRecording,\n recordingMode: recording.mode,\n // What the camera can actually do, drawn out of ~60 feature flags. These\n // three are the ones that decide whether another tool call is even valid:\n // a PTZ tool against a fixed camera is a guaranteed error.\n hasPtz: flags.canOpticalZoom ?? flags.hasPtz ?? false,\n hasPackageCamera: flags.hasPackageCamera ?? false,\n hasSmartDetect: flags.hasSmartDetect ?? false,\n smartDetectTypes: smart.objectTypes ?? [],\n firmwareVersion: camera.firmwareVersion,\n isUpdating: camera.isUpdating,\n lastSeen: isoTime(camera.lastSeen),\n };\n};\n\n/**\n * Events reference their camera by id. Resolving that to a name server-side is\n * the single most useful thing this layer does: it removes a join the model\n * would otherwise have to perform against a separate camera list, and get\n * silently wrong. The id is kept too, since the write and snapshot tools need it.\n */\nexport const summarizeEvent = (event: Rec, cameras?: NameIndex): Rec => {\n const cameraId = str(event.camera);\n const metadata = isRecord(event.metadata) ? event.metadata : undefined;\n const plate =\n metadata && isRecord(metadata.licensePlate) ? str(metadata.licensePlate.name) : undefined;\n\n return {\n id: event.id,\n type: event.type,\n start: isoTime(event.start),\n end: isoTime(event.end),\n ...(cameraId ? { cameraId } : {}),\n ...(cameraId && cameras?.get(cameraId) ? { camera: cameras.get(cameraId) } : {}),\n ...(Array.isArray(event.smartDetectTypes) && event.smartDetectTypes.length > 0\n ? { smartDetectTypes: event.smartDetectTypes }\n : {}),\n // Only present on smart detections, and the reason someone searched.\n ...(plate ? { licensePlate: plate } : {}),\n ...(typeof event.score === \"number\" ? { score: event.score } : {}),\n // Whether a still frame exists, so the model knows\n // unifi_protect_get_event_thumbnail is worth calling. Deliberately a flag\n // and not an id: the console's own `thumbnail` field is `e-<eventId>`,\n // which belongs to a DIFFERENT endpoint (`thumbnails/<id>`) than the one\n // this server uses (`events/<eventId>/thumbnail`). Handing that value out\n // invites passing it to the wrong place, which 404s — as it did here.\n ...(str(event.thumbnail) ? { hasThumbnail: true } : {}),\n };\n};\n\nexport const summarizeLight = (light: Rec): Rec => {\n const settings = isRecord(light.lightDeviceSettings) ? light.lightDeviceSettings : {};\n return {\n id: light.id,\n name: light.name,\n isConnected: light.isConnected,\n isLightOn: light.isLightOn,\n isPirMotionDetected: light.isPirMotionDetected,\n ledLevel: settings.ledLevel,\n isIndicatorEnabled: settings.isIndicatorEnabled,\n firmwareVersion: light.firmwareVersion,\n lastSeen: isoTime(light.lastSeen),\n };\n};\n\nexport const summarizeSensor = (sensor: Rec): Rec => {\n const stats = isRecord(sensor.stats) ? sensor.stats : {};\n const reading = (key: string): unknown => {\n const block = stats[key];\n return isRecord(block) ? block.value : undefined;\n };\n return {\n id: sensor.id,\n name: sensor.name,\n type: sensor.type,\n isConnected: sensor.isConnected,\n mountType: sensor.mountType,\n batteryStatus: isRecord(sensor.batteryStatus) ? sensor.batteryStatus.percentage : undefined,\n isOpened: sensor.isOpened,\n isMotionDetected: sensor.isMotionDetected,\n // The readings are the point of a sensor, but they sit one level down\n // inside `stats.<metric>.value` alongside per-metric history arrays that\n // are far larger than the reading itself.\n temperature: reading(\"temperature\"),\n humidity: reading(\"humidity\"),\n light: reading(\"light\"),\n firmwareVersion: sensor.firmwareVersion,\n lastSeen: isoTime(sensor.lastSeen),\n };\n};\n\nexport const summarizeViewer = (viewer: Rec): Rec => ({\n id: viewer.id,\n name: viewer.name,\n isConnected: viewer.isConnected,\n liveview: viewer.liveview,\n streamLimit: viewer.streamLimit,\n firmwareVersion: viewer.firmwareVersion,\n lastSeen: isoTime(viewer.lastSeen),\n});\n\nexport const summarizeChime = (chime: Rec): Rec => ({\n id: chime.id,\n name: chime.name,\n isConnected: chime.isConnected,\n volume: chime.volume,\n cameraIds: chime.cameraIds,\n firmwareVersion: chime.firmwareVersion,\n lastSeen: isoTime(chime.lastSeen),\n});\n\nexport const summarizeLiveview = (liveview: Rec): Rec => ({\n id: liveview.id,\n name: liveview.name,\n isDefault: liveview.isDefault,\n owner: liveview.owner,\n // `slots` holds the full camera layout; the count is what someone asks about.\n slotCount: Array.isArray(liveview.slots) ? liveview.slots.length : undefined,\n});\n\nexport const summarizeUser = (user: Rec): Rec => ({\n id: user.id,\n name: user.name,\n firstName: user.firstName,\n lastName: user.lastName,\n email: user.email,\n role: user.role,\n isOwner: user.isOwner,\n lastLoginTime: isoTime(user.lastLoginTime),\n});\n\nconst num = (value: unknown): number | undefined =>\n typeof value === \"number\" && Number.isFinite(value) ? value : undefined;\n\nconst rec = (value: unknown): Rec | undefined => (isRecord(value) ? value : undefined);\n\n/**\n * Storage moved between Protect releases, so both layouts are read.\n *\n * Protect 6.x exposed `nvr.storageInfo` with `totalSize` / `totalSpaceUsed` and a\n * `devices[]` array carrying full SMART tables. By 7.2 that key is gone entirely:\n * the same numbers live under `nvr.systemInfo.storage` as `size` / `used` /\n * `available`, the per-disk detail moved to `systemInfo.ustorage.disks`, and a\n * separate `nvr.storageStats` carries utilization. Reading only one shape gives\n * an empty `storage: {}` on the other, which is how this was found.\n */\nconst summarizeStorage = (nvr: Rec): Rec => {\n const system = rec(nvr.systemInfo);\n const modern = rec(system?.storage);\n const legacy = rec(nvr.storageInfo);\n const stats = rec(nvr.storageStats);\n const ustorage = rec(system?.ustorage);\n const disks = Array.isArray(ustorage?.disks) ? ustorage.disks.filter(isRecord) : undefined;\n\n const total = num(modern?.size) ?? num(legacy?.totalSize);\n const used = num(modern?.used) ?? num(legacy?.totalSpaceUsed);\n const available =\n num(modern?.available) ??\n (total !== undefined && used !== undefined ? total - used : undefined);\n\n return {\n ...(total !== undefined ? { totalBytes: total } : {}),\n ...(used !== undefined ? { usedBytes: used } : {}),\n ...(available !== undefined ? { availableBytes: available } : {}),\n ...(num(stats?.utilization) !== undefined\n ? { utilizationPercent: Math.round((stats!.utilization as number) * 10) / 10 }\n : {}),\n ...(str(modern?.type) ? { type: modern!.type } : {}),\n ...(str(ustorage?.raid) ? { raid: ustorage!.raid } : {}),\n ...((str(modern?.capability) ?? str(nvr.hardDriveState))\n ? { health: modern?.capability ?? nvr.hardDriveState }\n : {}),\n ...(disks\n ? {\n disks: disks.length,\n // Naming the unhealthy count rather than dumping four SMART tables:\n // the tables are enormous and the only question anyone asks of them\n // is whether a disk is failing.\n disksUnhealthy: disks.filter((d) => d.healthy !== undefined && d.healthy !== \"good\")\n .length,\n }\n : {}),\n // An NVR at 99% with isRecycling: true is working AS DESIGNED — it overwrites\n // the oldest footage continuously. Saying so inline stops a full-looking disk\n // being reported as a problem, which is the obvious wrong reading.\n ...(nvr.isRecycling === true\n ? {\n isRecycling: true,\n note: \"Near-full with recycling on is normal: the console overwrites the oldest footage continuously rather than stopping.\",\n }\n : {}),\n };\n};\n\nexport const summarizeNvr = (nvr: Rec): Rec => {\n const system = rec(nvr.systemInfo);\n const memory = rec(system?.memory);\n const cpu = rec(system?.cpu);\n const upSince = num(nvr.upSince);\n return {\n id: nvr.id,\n name: nvr.name,\n host: nvr.host,\n type: nvr.type,\n // Pin this in the README: the private API is undocumented and moves between\n // releases, so the running version is the first thing to check when a\n // previously working tool starts returning 404.\n version: nvr.version,\n firmwareVersion: nvr.firmwareVersion,\n ...(str(nvr.marketName) ? { model: nvr.marketName } : {}),\n timezone: nvr.timezone,\n isRecordingDisabled: nvr.isRecordingDisabled,\n storage: summarizeStorage(nvr),\n ...(memory ? { memory: { available: memory.available, total: memory.total } } : {}),\n ...(cpu ? { cpu: { load: cpu.averageLoad, temperature: cpu.temperature } } : {}),\n // 7.x reports when the console came up rather than how long it has been up.\n ...(upSince !== undefined\n ? {\n upSince: new Date(upSince).toISOString(),\n uptimeSeconds: Math.round((Date.now() - upSince) / 1000),\n }\n : num(nvr.uptime) !== undefined\n ? { uptimeSeconds: Math.round((nvr.uptime as number) / 1000) }\n : {}),\n lastSeen: isoTime(nvr.lastSeen),\n };\n};\n\n/**\n * The bootstrap document is the entire console state and must never be returned\n * raw — it is the single largest context bomb this API offers. This reduces it\n * to the NVR summary plus per-type counts, which is what \"tell me about my\n * system\" actually wants.\n */\nexport const summarizeBootstrap = (bootstrap: Rec): Rec => {\n const count = (key: string): number =>\n Array.isArray(bootstrap[key]) ? (bootstrap[key] as unknown[]).length : 0;\n\n return {\n nvr: isRecord(bootstrap.nvr) ? summarizeNvr(bootstrap.nvr) : undefined,\n devices: {\n cameras: count(\"cameras\"),\n lights: count(\"lights\"),\n sensors: count(\"sensors\"),\n viewers: count(\"viewers\"),\n chimes: count(\"chimes\"),\n bridges: count(\"bridges\"),\n doorlocks: count(\"doorlocks\"),\n },\n liveviews: count(\"liveviews\"),\n users: count(\"users\"),\n lastUpdateId: bootstrap.lastUpdateId,\n };\n};\n","import type { ProtectClient } from \"./protect.js\";\nimport { buildNameIndex, type NameIndex } from \"./shape.js\";\n\nexport type DeviceCacheOptions = {\n client: ProtectClient;\n ttlSeconds: number;\n now?: () => number;\n};\n\n/**\n * A short-lived camera id→name index, used to resolve the `camera` reference on\n * every event into something readable.\n *\n * It is cached because event search is the hot path and every result set needs\n * the same index: fetching the camera list once per search rather than once per\n * event is the difference between one extra request and none. The TTL is short\n * because a renamed or newly adopted camera should appear without a restart,\n * and a stale name is only ever cosmetic — the id travels alongside it.\n */\nexport type DeviceCache = {\n cameras(): Promise<NameIndex>;\n invalidate(): void;\n};\n\nexport const createDeviceCache = (opts: DeviceCacheOptions): DeviceCache => {\n const now = opts.now ?? Date.now;\n const ttlMs = opts.ttlSeconds * 1000;\n let cached: { index: NameIndex; expiresAt: number } | undefined;\n // Concurrent event searches would otherwise each fetch the camera list.\n let inflight: Promise<NameIndex> | undefined;\n\n const fetchIndex = async (): Promise<NameIndex> => {\n const cameras = await opts.client.get<unknown>(\"cameras\");\n const index = buildNameIndex(cameras);\n cached = { index, expiresAt: now() + ttlMs };\n return index;\n };\n\n return {\n async cameras(): Promise<NameIndex> {\n if (cached && now() < cached.expiresAt) return cached.index;\n if (!inflight) {\n inflight = fetchIndex().finally(() => {\n inflight = undefined;\n });\n }\n try {\n return await inflight;\n } catch {\n // Name resolution is a nicety, not the answer. If the camera list is\n // unreachable, events still come back with their ids rather than the\n // whole search failing over a cosmetic lookup.\n return new Map<string, string>();\n }\n },\n invalidate(): void {\n cached = undefined;\n },\n };\n};\n","import { PRIVATE_API_PATH } from \"../config.js\";\nimport type { Logger, SessionProvider } from \"./auth.js\";\nimport { ProtectApiError } from \"./errors.js\";\n\n/** Array values become repeated params, which is how Protect expects `types`. */\nexport type Query = Record<string, string | number | boolean | string[] | undefined>;\n\nconst sleep = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms));\n\nexport const backoffMs = (attempt: number): number => Math.min(1000 * 2 ** attempt, 8000);\n\nexport const retryAfterMs = (res: Response): number | undefined => {\n const value = res.headers.get(\"Retry-After\");\n if (value === null) return undefined;\n const seconds = Number(value);\n return Number.isFinite(seconds) ? Math.max(seconds, 0) * 1000 : undefined;\n};\n\nconst safeJsonParse = (text: string): unknown => {\n try {\n return JSON.parse(text);\n } catch {\n return text;\n }\n};\n\nexport const buildQuery = (query: Query | undefined): string => {\n if (!query) return \"\";\n const params = new URLSearchParams();\n for (const [key, value] of Object.entries(query)) {\n if (value === undefined) continue;\n // Protect reads `types` and `smartDetectTypes` as REPEATED parameters\n // (?types=motion&types=ring), not as a comma-joined string. A joined value\n // matches no event type at all and returns an empty list rather than an error.\n if (Array.isArray(value)) {\n for (const item of value) params.append(key, item);\n } else {\n params.append(key, String(value));\n }\n }\n const qs = params.toString();\n return qs ? `?${qs}` : \"\";\n};\n\nexport type ProtectClientOptions = {\n baseUrl: string;\n session: SessionProvider;\n maxRetries: number;\n userAgent: string;\n maxDownloadBytes: number;\n fetch?: typeof fetch;\n logger?: Logger;\n};\n\nexport type BinaryResult = {\n bytes: Uint8Array;\n contentType: string;\n};\n\n/**\n * The private Protect API client. Every path passed in is relative to\n * `/proxy/protect/api` — callers write `cameras/abc123`, not the full path.\n */\nexport class ProtectClient {\n readonly baseUrl: string;\n private readonly session: SessionProvider;\n private readonly maxRetries: number;\n private readonly userAgent: string;\n private readonly maxDownloadBytes: number;\n private readonly fetchImpl: typeof fetch;\n private readonly logger: Logger | undefined;\n\n constructor(opts: ProtectClientOptions) {\n this.baseUrl = opts.baseUrl.replace(/\\/+$/, \"\");\n this.session = opts.session;\n this.maxRetries = opts.maxRetries;\n this.userAgent = opts.userAgent;\n this.maxDownloadBytes = opts.maxDownloadBytes;\n this.fetchImpl = opts.fetch ?? fetch;\n this.logger = opts.logger;\n }\n\n /** Absolute URL for a path relative to the private API root. */\n url(path: string, query?: Query): string {\n const clean = path.replace(/^\\/+/, \"\");\n return `${this.baseUrl}${PRIVATE_API_PATH}/${clean}${buildQuery(query)}`;\n }\n\n /**\n * Perform one authenticated request, retrying on 401 (re-login), 429 and 5xx.\n * Returns the raw Response so callers can decide between JSON and bytes.\n */\n private async send(\n method: string,\n url: string,\n opts: { body?: unknown; accept?: string } = {},\n ): Promise<Response> {\n const hasBody = opts.body !== undefined;\n const bodyText = hasBody ? JSON.stringify(opts.body) : undefined;\n let attempt = 0;\n\n for (;;) {\n const auth = await this.session.headers();\n this.logger?.debug?.(`${method} ${url} (attempt ${attempt + 1})`);\n const res = await this.fetchImpl(url, {\n method,\n headers: {\n ...auth,\n Accept: opts.accept ?? \"application/json\",\n \"User-Agent\": this.userAgent,\n ...(hasBody ? { \"Content-Type\": \"application/json\" } : {}),\n },\n ...(bodyText !== undefined ? { body: bodyText } : {}),\n // Never follow a redirect: UniFi OS bounces an expired session to a\n // login page, and following it would turn a 401 we can recover from\n // into a 200 carrying HTML that parses as neither JSON nor an image.\n redirect: \"manual\",\n });\n\n // 401 means the cookie died mid-session. Drop it and re-run the handshake.\n if (res.status === 401 && attempt < this.maxRetries) {\n this.logger?.warn?.(\"HTTP 401 — re-authenticating and retrying\");\n this.session.invalidate();\n attempt += 1;\n continue;\n }\n\n if ((res.status === 429 || res.status >= 500) && attempt < this.maxRetries) {\n const delay = retryAfterMs(res) ?? backoffMs(attempt);\n this.logger?.warn?.(`HTTP ${res.status} — retrying in ${delay}ms`);\n await sleep(delay);\n attempt += 1;\n continue;\n }\n\n return res;\n }\n }\n\n /** A JSON request against the private API. */\n async request<T = unknown>(\n method: string,\n path: string,\n opts: { query?: Query; body?: unknown } = {},\n ): Promise<T> {\n const url = this.url(path, opts.query);\n const res = await this.send(method, url, opts.body !== undefined ? { body: opts.body } : {});\n const text = await res.text();\n\n if (!res.ok) {\n throw new ProtectApiError(this.errorMessage(res, method, path, text), {\n status: res.status,\n path,\n errors: safeJsonParse(text),\n });\n }\n\n // Many PATCH endpoints answer 200 with an empty body rather than 204.\n if (res.status === 204 || text.trim() === \"\") return null as T;\n return safeJsonParse(text) as T;\n }\n\n get<T = unknown>(path: string, query?: Query): Promise<T> {\n return this.request<T>(\"GET\", path, query ? { query } : {});\n }\n\n post<T = unknown>(path: string, body?: unknown, query?: Query): Promise<T> {\n return this.request<T>(\"POST\", path, {\n ...(body !== undefined ? { body } : {}),\n ...(query ? { query } : {}),\n });\n }\n\n patch<T = unknown>(path: string, body?: unknown, query?: Query): Promise<T> {\n return this.request<T>(\"PATCH\", path, {\n ...(body !== undefined ? { body } : {}),\n ...(query ? { query } : {}),\n });\n }\n\n del<T = unknown>(path: string, query?: Query): Promise<T> {\n return this.request<T>(\"DELETE\", path, query ? { query } : {});\n }\n\n /**\n * Fetch a binary asset — a snapshot JPEG, an event thumbnail, an exported\n * MP4. Size is checked against `maxDownloadBytes` from Content-Length where\n * the console supplies one, and again after reading where it does not, so an\n * unexpectedly huge export fails with a clear message rather than by\n * exhausting the heap.\n */\n async requestBytes(\n path: string,\n opts: { query?: Query; accept?: string } = {},\n ): Promise<BinaryResult> {\n const url = this.url(path, opts.query);\n const res = await this.send(\"GET\", url, {\n accept: opts.accept ?? \"application/octet-stream\",\n });\n\n if (!res.ok) {\n const text = await res.text().catch(() => \"\");\n throw new ProtectApiError(this.errorMessage(res, \"GET\", path, text), {\n status: res.status,\n path,\n errors: safeJsonParse(text),\n });\n }\n\n const declared = Number(res.headers.get(\"Content-Length\"));\n if (Number.isFinite(declared) && declared > this.maxDownloadBytes) {\n throw new ProtectApiError(\n `${path} would return ${declared} bytes, over the ${this.maxDownloadBytes}-byte limit. ` +\n `Narrow the time range, or raise UNIFI_PROTECT_MAX_DOWNLOAD_BYTES.`,\n { status: res.status, path },\n );\n }\n\n const bytes = new Uint8Array(await res.arrayBuffer());\n if (bytes.byteLength > this.maxDownloadBytes) {\n throw new ProtectApiError(\n `${path} returned ${bytes.byteLength} bytes, over the ${this.maxDownloadBytes}-byte ` +\n `limit. Narrow the time range, or raise UNIFI_PROTECT_MAX_DOWNLOAD_BYTES.`,\n { status: res.status, path },\n );\n }\n\n return {\n bytes,\n contentType: res.headers.get(\"Content-Type\") ?? \"application/octet-stream\",\n };\n }\n\n /** Status-aware prose, because this is the text someone acts on. */\n private errorMessage(res: Response, method: string, path: string, body: string): string {\n const detail = safeJsonParse(body);\n const upstream =\n typeof detail === \"object\" && detail !== null && \"error\" in detail\n ? String((detail as { error: unknown }).error)\n : typeof detail === \"string\" && detail.length > 0 && detail.length < 200\n ? detail\n : undefined;\n\n const base =\n `Protect ${method} ${path} failed: HTTP ${res.status} ${res.statusText}`.trim() +\n (upstream ? ` — ${upstream}` : \"\");\n\n if (res.status === 401 || res.status === 403) {\n return (\n `${base}. The account may lack Protect permissions for this device, or may be ` +\n `view-only while this call needs write access.`\n );\n }\n if (res.status === 404) {\n return (\n `${base}. Check the id came from a list tool on THIS console. Note also that the ` +\n `private Protect API is undocumented and Ubiquiti moves endpoints between releases — ` +\n `unifi_protect_get_system_info reports the version actually running.`\n );\n }\n // A redirect reaching here means `redirect: \"manual\"` caught a bounce to\n // the login page, which is what an expired or rejected session looks like.\n if (res.status >= 300 && res.status < 400) {\n return `${base}. The console redirected the request, which usually means the session was rejected.`;\n }\n return base;\n }\n}\n","import { Agent, fetch as undiciFetch } from \"undici\";\n\nimport type { Logger } from \"./auth.js\";\n\n/**\n * The one place TLS is decided.\n *\n * UniFi consoles ship a self-signed certificate, and Node's native `fetch`\n * ignores a `node:https` Agent — the only scoped way to relax verification is an\n * undici dispatcher. That is why this server carries a third runtime dependency:\n * the alternative, NODE_TLS_REJECT_UNAUTHORIZED=0, is process-global and\n * disables verification for every other request the process ever makes.\n *\n * This replaced exactly that process-wide switch, which had been adopted on the\n * belief that undici could not be imported and a dispatcher therefore could not\n * be scoped. It can — see below.\n *\n * **The `fetch` here must be undici's own, not the global one.** Node's built-in\n * fetch is a *bundled copy* of undici, and it rejects a dispatcher constructed\n * from the separately-installed package with a bare `UND_ERR_INVALID_ARG`,\n * surfacing as an unexplained \"fetch failed\". Passing undici's own fetch keeps\n * both sides on one class. Verification is on by default, so the platform fetch\n * remains the common path.\n *\n * Note what a pinned certificate can and cannot fix. These consoles present\n * `CN=unifi.local` with SANs for `unifi.local`, `localhost` and `127.0.0.1` —\n * and **no IP SAN**. Reached by IP, verification fails on the host name however\n * the certificate is trusted. Measured with `curl --cacert`: by host name\n * `ssl_verify=0`, by IP `ssl_verify=1`. So verifying requires BOTH\n * NODE_EXTRA_CA_CERTS and a host name that resolves to the console.\n */\nexport const createHttpFetch = (opts: { insecureTls: boolean; logger?: Logger }): typeof fetch => {\n if (!opts.insecureTls) return fetch;\n\n opts.logger?.warn?.(\n \"TLS certificate verification is DISABLED for this server's requests \" +\n \"(UNIFI_PROTECT_VERIFY_TLS=false). To verify instead, address the console by a host name \" +\n \"that resolves to it — its certificate has no IP SAN — and point NODE_EXTRA_CA_CERTS at \" +\n \"the console's certificate.\",\n );\n\n const dispatcher = new Agent({ connect: { rejectUnauthorized: false } });\n // undici's fetch/Response are structurally the WHATWG ones the rest of the\n // client uses; the cast bridges the two nominal type declarations and is\n // confined to this file.\n return ((input: Parameters<typeof fetch>[0], init?: Parameters<typeof fetch>[1]) =>\n undiciFetch(input as string, { ...init, dispatcher } as never)) as unknown as typeof fetch;\n};\n","import { z } from \"zod\";\n\nimport {\n NotConfiguredError,\n ProtectApiError,\n ProtectAuthError,\n WritesDisabledError,\n} from \"../client/errors.js\";\n\nexport type ToolResult = {\n content: ({ type: \"text\"; text: string } | { type: \"image\"; data: string; mimeType: string })[];\n isError?: boolean;\n};\n\nexport const ok = (data: unknown): ToolResult => ({\n content: [{ type: \"text\", text: JSON.stringify(data ?? { ok: true }, null, 2) }],\n});\n\nexport const fail = (message: string, extra?: unknown): ToolResult => ({\n content: [\n {\n type: \"text\",\n text: JSON.stringify({ error: message, ...(extra ? { details: extra } : {}) }, null, 2),\n },\n ],\n isError: true,\n});\n\n/** Render a thrown value as a tool error, preserving upstream detail. */\nexport const toFailure = (err: unknown): ToolResult => {\n if (err instanceof ProtectApiError) {\n return fail(err.message, {\n status: err.status,\n ...(err.path ? { path: err.path } : {}),\n errors: err.errors,\n });\n }\n if (err instanceof ProtectAuthError) {\n return fail(err.message, err.needsTwoFactor ? { needsTwoFactor: true } : undefined);\n }\n if (err instanceof WritesDisabledError || err instanceof NotConfiguredError) {\n return fail(err.message);\n }\n if (err instanceof Error) return fail(err.message);\n return fail(\"Unknown error\", err);\n};\n\n/** Run a tool body, JSON-formatting the result and turning errors into a tool error. */\nexport const wrap = async <T>(fn: () => Promise<T>): Promise<ToolResult> => {\n try {\n return ok(await fn());\n } catch (err) {\n return toFailure(err);\n }\n};\n\n/** Like `wrap`, but the body chooses its own result shape (e.g. an image block). */\nexport const wrapResult = async (fn: () => Promise<ToolResult>): Promise<ToolResult> => {\n try {\n return await fn();\n } catch (err) {\n return toFailure(err);\n }\n};\n\n/** Drop undefined values so we never send `{\"name\": undefined}` to the console. */\nexport const compact = <T extends Record<string, unknown>>(obj: T): Partial<T> =>\n Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined)) as Partial<T>;\n\n/** `{}` is not a no-op PATCH body — send undefined instead. */\nexport const compactOrUndefined = <T extends Record<string, unknown>>(\n obj: T,\n): Partial<T> | undefined => {\n const out = compact(obj);\n return Object.keys(out).length > 0 ? out : undefined;\n};\n\nconst RELATIVE =\n /^(\\d+(?:\\.\\d+)?)\\s*(s|sec|secs|seconds?|m|min|mins|minutes?|h|hr|hrs|hours?|d|days?|w|weeks?)(?:\\s+ago)?$/i;\n\nconst UNIT_MS: Record<string, number> = {\n s: 1000,\n m: 60_000,\n h: 3_600_000,\n d: 86_400_000,\n w: 604_800_000,\n};\n\n/**\n * Convert a time expression to milliseconds since the Unix epoch.\n *\n * This exists because the console's `/events` endpoint takes JavaScript\n * millisecond timestamps — not ISO 8601, and NOT Unix seconds. A seconds value\n * is not rejected: it is interpreted as a moment in January 1970, so the query\n * succeeds and returns an empty list. That reads as \"nothing happened last\n * night\", which is the most expensive possible failure for this server.\n *\n * Accepts ISO 8601, a relative expression like \"2h ago\" or \"30m\", the literal\n * \"now\", or a raw millisecond number.\n */\nexport const toEpochMs = (value: string | number, now: number = Date.now()): number => {\n if (typeof value === \"number\") return Math.round(value);\n\n const text = value.trim();\n if (text === \"\" || text.toLowerCase() === \"now\") return now;\n\n const relative = RELATIVE.exec(text);\n if (relative) {\n const amount = Number(relative[1]);\n const unit = relative[2]!.toLowerCase()[0]!;\n const scale = UNIT_MS[unit];\n if (scale === undefined) {\n throw new Error(`Unrecognised time unit in \"${value}\".`);\n }\n return Math.round(now - amount * scale);\n }\n\n // A bare number in a string is milliseconds, but a 10-digit one is almost\n // certainly seconds that someone converted by hand. Catch it rather than\n // silently querying 1970.\n if (/^\\d+$/.test(text)) {\n const n = Number(text);\n if (text.length <= 10) {\n throw new Error(\n `\"${value}\" looks like Unix SECONDS. This API takes milliseconds — pass ${n * 1000}, ` +\n `or better, an ISO 8601 timestamp or a relative expression like \"2h ago\".`,\n );\n }\n return n;\n }\n\n const parsed = Date.parse(text);\n if (Number.isNaN(parsed)) {\n throw new Error(\n `Could not read \"${value}\" as a time. Use ISO 8601 (2026-08-29T22:00:00Z), a relative ` +\n `expression (\"2h ago\", \"30m\", \"7d\"), or \"now\".`,\n );\n }\n return parsed;\n};\n\nexport const confirmArg = z\n .literal(true)\n .describe(\"Must be true. Explicit acknowledgement that this changes the console's state.\");\n\nexport const cameraIdArg = z\n .string()\n .min(1)\n .describe(\n \"Camera id — the `id` from unifi_protect_list_cameras, a 24-character hex string. Not the \" +\n \"camera's name and not its MAC address.\",\n );\n\n/** The shared prose for every time argument, so the trap is stated everywhere it applies. */\nconst timeHelp = (role: string): string =>\n `${role} Accepts ISO 8601 (\"2026-08-29T22:00:00Z\"), a relative expression (\"2h ago\", ` +\n `\"30m\", \"7d\"), or \"now\". Converted to the millisecond epoch the console requires — do ` +\n `not pass Unix seconds, which would silently query 1970 and return nothing.`;\n\nexport const timeArg = (role: string) => z.string().optional().describe(timeHelp(role));\n\nexport const requiredTimeArg = (role: string) => z.string().min(1).describe(timeHelp(role));\n\nexport const limitArg = z\n .number()\n .int()\n .min(1)\n .max(500)\n .default(50)\n .describe(\n \"Maximum number of items to return (1-500). Defaults to 50. A busy system logs thousands \" +\n \"of motion events a day, so raise this deliberately.\",\n );\n","import { mkdir, writeFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\n\nimport type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { z } from \"zod\";\n\nimport type { ProtectClient } from \"../client/protect.js\";\nimport { summarizeCamera, summarizeEach } from \"../client/shape.js\";\nimport type { ToolContext } from \"./index.js\";\nimport { cameraIdArg, compactOrUndefined, confirmArg, ok, wrap, wrapResult } from \"./util.js\";\n\ntype Rec = Record<string, unknown>;\n\nconst RECORDING_MODES = [\"always\", \"never\", \"detections\", \"schedule\"] as const;\n\n/** Filesystem-safe stem for a snapshot file, derived from the camera name. */\nconst slug = (value: string): string =>\n value\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-|-$/g, \"\") || \"camera\";\n\nexport const registerCameraTools = (\n server: McpServer,\n client: ProtectClient,\n ctx: ToolContext,\n): void => {\n server.registerTool(\n \"unifi_protect_list_cameras\",\n {\n description:\n \"List every camera on the console with its id, name, connection state, recording mode, \" +\n \"firmware and what it can do (PTZ, package camera, smart detection, and which object \" +\n \"types it detects). Returns a summary rather than the console's full camera record, \" +\n \"which runs to thousands of fields across encoder profiles, zones and feature flags — \" +\n \"use unifi_protect_get_camera when you need all of it for one camera.\",\n inputSchema: {},\n annotations: { readOnlyHint: true },\n },\n async () => wrap(async () => summarizeEach(await client.get(\"cameras\"), summarizeCamera)),\n );\n\n server.registerTool(\n \"unifi_protect_get_camera\",\n {\n description:\n \"Get one camera's complete record — every setting the console holds, including encoder \" +\n \"channels, motion and smart-detection zones, privacy masks, OSD and LED settings, ISP \" +\n \"tuning and live statistics. This is large (roughly 8-15 KB of JSON). Prefer \" +\n \"unifi_protect_list_cameras unless you specifically need a field it does not carry.\",\n inputSchema: { cameraId: cameraIdArg },\n annotations: { readOnlyHint: true },\n },\n async ({ cameraId }) => wrap(() => client.get(`cameras/${encodeURIComponent(cameraId)}`)),\n );\n\n server.registerTool(\n \"unifi_protect_get_camera_snapshot\",\n {\n description:\n \"Capture a still frame from a camera as it looks right now. Writes the JPEG to disk and \" +\n 'returns its path, size and content type by default. Set output=\"image\" to get the ' +\n \"frame inline instead so a vision model can actually look at it — that costs roughly \" +\n \"300,000 to 700,000 characters of context per call, so choose it deliberately rather \" +\n \"than by default. A fresh capture is forced; without that the console can hand back a \" +\n \"cached frame that is minutes old.\",\n inputSchema: {\n cameraId: cameraIdArg,\n output: z\n .enum([\"file\", \"image\"])\n .default(\"file\")\n .describe(\n 'Where the frame goes. \"file\" writes it to disk and returns the path — cheap, and ' +\n 'you can read the file later if it turns out to matter. \"image\" returns it inline ' +\n \"for a model to look at, at a large cost in context.\",\n ),\n highQuality: z\n .boolean()\n .default(false)\n .describe(\n \"Request the camera's full resolution rather than a scaled frame. Larger and slower; \" +\n 'with output=\"image\" it multiplies an already expensive call.',\n ),\n savePath: z\n .string()\n .optional()\n .describe(\n \"Absolute path to write the JPEG to. Defaults to a timestamped file under \" +\n \"UNIFI_PROTECT_SNAPSHOT_DIR. Parent directories are created.\",\n ),\n },\n annotations: { readOnlyHint: true },\n },\n async ({ cameraId, output, highQuality, savePath }) =>\n wrapResult(async () => {\n const { bytes, contentType } = await client.requestBytes(\n `cameras/${encodeURIComponent(cameraId)}/snapshot`,\n {\n query: {\n // Without force=true the console may return the last frame it\n // happens to have cached, which can be minutes stale — and a\n // stale frame is indistinguishable from a current one.\n force: \"true\",\n ...(highQuality ? { highQuality: \"true\" } : {}),\n ts: Date.now(),\n },\n accept: \"image/jpeg\",\n },\n );\n\n if (output === \"image\") {\n return {\n content: [\n {\n type: \"image\" as const,\n data: Buffer.from(bytes).toString(\"base64\"),\n mimeType: contentType.startsWith(\"image/\") ? contentType : \"image/jpeg\",\n },\n ],\n };\n }\n\n const cameras = await ctx.devices.cameras();\n const name = cameras.get(cameraId) ?? cameraId;\n const stamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n const path = savePath ?? join(ctx.config.snapshotDir, `${slug(name)}-${stamp}.jpg`);\n await mkdir(join(path, \"..\"), { recursive: true });\n await writeFile(path, bytes);\n\n return ok({\n path,\n bytes: bytes.byteLength,\n contentType,\n camera: name,\n capturedAt: new Date().toISOString(),\n note: 'Call again with output=\"image\" if a model needs to see the frame itself.',\n });\n }),\n );\n\n server.registerTool(\n \"unifi_protect_list_ptz_presets\",\n {\n description:\n \"List a PTZ camera's saved preset positions, with the slot number each one lives at. \" +\n \"Only meaningful for cameras reporting hasPtz: true in unifi_protect_list_cameras. \" +\n \"There is no tool to MOVE a PTZ camera or run a patrol: those commands exist only on \" +\n \"Ubiquiti's official Integration API (a separate X-API-KEY auth this server does not \" +\n \"use), not on the private API this server wraps — presets are created and driven from \" +\n \"the Protect app itself.\",\n inputSchema: { cameraId: cameraIdArg },\n annotations: { readOnlyHint: true },\n },\n async ({ cameraId }) =>\n wrap(() => client.get(`cameras/${encodeURIComponent(cameraId)}/ptz/preset`)),\n );\n\n server.registerTool(\n \"unifi_protect_list_ptz_patrols\",\n {\n description:\n \"List a PTZ camera's saved patrol routes. See unifi_protect_list_ptz_presets for why \" +\n \"there is no tool to start or stop one.\",\n inputSchema: { cameraId: cameraIdArg },\n annotations: { readOnlyHint: true },\n },\n async ({ cameraId }) =>\n wrap(() => client.get(`cameras/${encodeURIComponent(cameraId)}/ptz/patrol`)),\n );\n\n if (!ctx.allowWrites) return;\n\n server.registerTool(\n \"unifi_protect_update_camera\",\n {\n description:\n \"Change a camera's settings in place. Only the fields you pass are sent, and the \" +\n \"console merges them — sibling settings inside the same block are preserved, so \" +\n \"setting osdDate alone does not clear osdName. Use \" +\n \"unifi_protect_set_camera_recording_mode for recording mode alone — it is the setting \" +\n \"people mean most often and it is easy to send wrongly here.\",\n inputSchema: {\n cameraId: cameraIdArg,\n name: z.string().min(1).optional().describe(\"Display name shown throughout Protect.\"),\n micVolume: z\n .number()\n .int()\n .min(0)\n .max(100)\n .optional()\n .describe(\"Microphone sensitivity, 0-100. 0 mutes the microphone.\"),\n isMicEnabled: z.boolean().optional().describe(\"Whether the microphone records at all.\"),\n // A camera's LED is a simple on/off (`ledSettings.isEnabled`). The\n // 0-6 `ledLevel` that looks like it belongs here is a FLOODLIGHT\n // field — see unifi_protect_update_light.\n statusLedEnabled: z\n .boolean()\n .optional()\n .describe(\"Whether the camera's status LED lights up at all.\"),\n osdName: z.boolean().optional().describe(\"Overlay the camera name on the video.\"),\n osdDate: z.boolean().optional().describe(\"Overlay the date and time on the video.\"),\n },\n annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true },\n },\n async ({ cameraId, name, micVolume, isMicEnabled, statusLedEnabled, osdName, osdDate }) =>\n wrap(async () => {\n const ledSettings = compactOrUndefined({ isEnabled: statusLedEnabled });\n const osdSettings = compactOrUndefined({ isNameEnabled: osdName, isDateEnabled: osdDate });\n const body = compactOrUndefined({\n name,\n micVolume,\n isMicEnabled,\n ledSettings,\n osdSettings,\n });\n if (!body) {\n throw new Error(\n \"Nothing to update — pass at least one of name, micVolume, isMicEnabled, \" +\n \"statusLedEnabled, osdName, osdDate.\",\n );\n }\n return summarizeCamera(\n await client.patch<Rec>(`cameras/${encodeURIComponent(cameraId)}`, body),\n );\n }),\n );\n\n server.registerTool(\n \"unifi_protect_set_camera_recording_mode\",\n {\n description:\n \"Set what a camera records. `never` stops recording entirely — the camera stays online \" +\n \"and streams live, but nothing is written, so there will be no footage to search later. \" +\n \"`detections` records only motion and smart detections; `always` records continuously; \" +\n \"`schedule` follows the schedule configured in Protect.\",\n inputSchema: {\n cameraId: cameraIdArg,\n mode: z\n .enum(RECORDING_MODES)\n .describe(\n \"Recording mode. `never` means no footage is kept from now on — this is the one \" +\n \"worth pausing over.\",\n ),\n },\n annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true },\n },\n async ({ cameraId, mode }) =>\n wrap(async () =>\n summarizeCamera(\n await client.patch<Rec>(`cameras/${encodeURIComponent(cameraId)}`, {\n recordingSettings: { mode },\n }),\n ),\n ),\n );\n\n server.registerTool(\n \"unifi_protect_reboot_camera\",\n {\n description:\n \"Reboot a camera. It stops recording and goes offline for roughly a minute, and any \" +\n \"footage during that window is lost. Useful for a camera that has stopped responding.\",\n inputSchema: { cameraId: cameraIdArg, confirm: confirmArg },\n annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false },\n },\n async ({ cameraId }) =>\n wrap(() => client.post(`cameras/${encodeURIComponent(cameraId)}/reboot`)),\n );\n};\n","import type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { z } from \"zod\";\n\nimport type { ProtectClient } from \"../client/protect.js\";\nimport {\n summarizeChime,\n summarizeEach,\n summarizeLight,\n summarizeSensor,\n summarizeViewer,\n} from \"../client/shape.js\";\nimport type { ToolContext } from \"./index.js\";\nimport { compactOrUndefined, wrap } from \"./util.js\";\n\ntype Rec = Record<string, unknown>;\n\nconst idArg = (kind: string, listTool: string) =>\n z.string().min(1).describe(`${kind} id — the \\`id\\` from ${listTool}.`);\n\nexport const registerDeviceTools = (\n server: McpServer,\n client: ProtectClient,\n ctx: ToolContext,\n): void => {\n server.registerTool(\n \"unifi_protect_list_lights\",\n {\n description:\n \"List UniFi Protect floodlights with their connection state, whether the light is \" +\n \"currently on, whether PIR motion is being detected, and brightness.\",\n inputSchema: {},\n annotations: { readOnlyHint: true },\n },\n async () => wrap(async () => summarizeEach(await client.get(\"lights\"), summarizeLight)),\n );\n\n server.registerTool(\n \"unifi_protect_list_sensors\",\n {\n description:\n \"List UniFi Protect sensors with their current readings — temperature, humidity, light \" +\n \"level — plus open/closed state, motion, and battery percentage. The readings are \" +\n \"lifted out of the console's per-metric history arrays, which are far larger than the \" +\n \"values themselves.\",\n inputSchema: {},\n annotations: { readOnlyHint: true },\n },\n async () => wrap(async () => summarizeEach(await client.get(\"sensors\"), summarizeSensor)),\n );\n\n server.registerTool(\n \"unifi_protect_list_viewers\",\n {\n description:\n \"List UniFi Protect Viewport devices and which live view each is currently displaying.\",\n inputSchema: {},\n annotations: { readOnlyHint: true },\n },\n async () => wrap(async () => summarizeEach(await client.get(\"viewers\"), summarizeViewer)),\n );\n\n server.registerTool(\n \"unifi_protect_list_chimes\",\n {\n description:\n \"List UniFi Protect chimes, their volume, and which doorbell cameras each is paired to.\",\n inputSchema: {},\n annotations: { readOnlyHint: true },\n },\n async () => wrap(async () => summarizeEach(await client.get(\"chimes\"), summarizeChime)),\n );\n\n if (!ctx.allowWrites) return;\n\n server.registerTool(\n \"unifi_protect_update_light\",\n {\n description:\n \"Change a floodlight's settings — brightness, whether the light is on, and the PIR \" +\n \"sensitivity that decides when it triggers. Only the fields you pass are sent, and the \" +\n \"console merges them, so the PIR duration and lux sensitivity you do not pass survive.\",\n inputSchema: {\n lightId: idArg(\"Light\", \"unifi_protect_list_lights\"),\n isLightOn: z.boolean().optional().describe(\"Turn the light on or off right now.\"),\n ledLevel: z\n .number()\n .int()\n .min(1)\n .max(6)\n .optional()\n .describe(\"Brightness, 1 (dimmest) to 6 (brightest).\"),\n pirSensitivity: z\n .number()\n .int()\n .min(0)\n .max(100)\n .optional()\n .describe(\"Motion sensitivity, 0-100. Higher triggers on smaller movement.\"),\n },\n annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true },\n },\n async ({ lightId, isLightOn, ledLevel, pirSensitivity }) =>\n wrap(async () => {\n const lightOnSettings = compactOrUndefined({ isLedForceOn: isLightOn });\n const lightDeviceSettings = compactOrUndefined({ ledLevel, pirSensitivity });\n const body = compactOrUndefined({ lightOnSettings, lightDeviceSettings });\n if (!body) {\n throw new Error(\n \"Nothing to update — pass at least one of isLightOn, ledLevel, pirSensitivity.\",\n );\n }\n return summarizeLight(\n await client.patch<Rec>(`lights/${encodeURIComponent(lightId)}`, body),\n );\n }),\n );\n\n server.registerTool(\n \"unifi_protect_update_sensor\",\n {\n description:\n \"Rename a sensor or change which of its capabilities are enabled. Only the fields you \" +\n \"pass are sent.\",\n inputSchema: {\n sensorId: idArg(\"Sensor\", \"unifi_protect_list_sensors\"),\n name: z.string().min(1).optional().describe(\"Display name for the sensor.\"),\n motionEnabled: z.boolean().optional().describe(\"Whether motion detection reports events.\"),\n temperatureEnabled: z.boolean().optional().describe(\"Whether temperature is reported.\"),\n humidityEnabled: z.boolean().optional().describe(\"Whether humidity is reported.\"),\n lightEnabled: z.boolean().optional().describe(\"Whether the light level is reported.\"),\n },\n annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true },\n },\n async ({ sensorId, name, motionEnabled, temperatureEnabled, humidityEnabled, lightEnabled }) =>\n wrap(async () => {\n const body = compactOrUndefined({\n name,\n motionSettings: compactOrUndefined({ isEnabled: motionEnabled }),\n temperatureSettings: compactOrUndefined({ isEnabled: temperatureEnabled }),\n humiditySettings: compactOrUndefined({ isEnabled: humidityEnabled }),\n lightSettings: compactOrUndefined({ isEnabled: lightEnabled }),\n });\n if (!body) {\n throw new Error(\n \"Nothing to update — pass at least one of name, motionEnabled, temperatureEnabled, \" +\n \"humidityEnabled, lightEnabled.\",\n );\n }\n return summarizeSensor(\n await client.patch<Rec>(`sensors/${encodeURIComponent(sensorId)}`, body),\n );\n }),\n );\n\n server.registerTool(\n \"unifi_protect_update_viewer\",\n {\n description:\n \"Put a saved live view on a Viewport screen, or rename the viewer. The liveview id \" +\n \"comes from unifi_protect_list_liveviews — this changes what is displayed on a physical \" +\n \"screen, so someone watching will see it switch.\",\n inputSchema: {\n viewerId: idArg(\"Viewer\", \"unifi_protect_list_viewers\"),\n liveview: z\n .string()\n .min(1)\n .optional()\n .describe(\"Live view id from unifi_protect_list_liveviews — the layout to display.\"),\n name: z.string().min(1).optional().describe(\"Display name for the viewer.\"),\n },\n annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true },\n },\n async ({ viewerId, liveview, name }) =>\n wrap(async () => {\n const body = compactOrUndefined({ liveview, name });\n if (!body) throw new Error(\"Nothing to update — pass liveview or name.\");\n return summarizeViewer(\n await client.patch<Rec>(`viewers/${encodeURIComponent(viewerId)}`, body),\n );\n }),\n );\n\n server.registerTool(\n \"unifi_protect_update_chime\",\n {\n description:\n \"Change a chime's volume or rename it. Volume 0 silences it, so a doorbell press will \" +\n \"make no sound.\",\n inputSchema: {\n chimeId: idArg(\"Chime\", \"unifi_protect_list_chimes\"),\n volume: z\n .number()\n .int()\n .min(0)\n .max(100)\n .optional()\n .describe(\"Volume, 0-100. 0 means the chime stays silent when the doorbell is pressed.\"),\n name: z.string().min(1).optional().describe(\"Display name for the chime.\"),\n },\n annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true },\n },\n async ({ chimeId, volume, name }) =>\n wrap(async () => {\n const body = compactOrUndefined({ volume, name });\n if (!body) throw new Error(\"Nothing to update — pass volume or name.\");\n return summarizeChime(\n await client.patch<Rec>(`chimes/${encodeURIComponent(chimeId)}`, body),\n );\n }),\n );\n};\n","import { mkdir, writeFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\n\nimport type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { z } from \"zod\";\n\nimport type { ProtectClient } from \"../client/protect.js\";\nimport { summarizeEvent } from \"../client/shape.js\";\nimport type { ToolContext } from \"./index.js\";\nimport {\n cameraIdArg,\n limitArg,\n ok,\n requiredTimeArg,\n timeArg,\n toEpochMs,\n wrap,\n wrapResult,\n} from \"./util.js\";\n\ntype Rec = Record<string, unknown>;\n\n/**\n * The event types worth searching. Protect emits far more — disk health,\n * firmware updates, adoption, connection churn — but they are noise in an\n * event search and swamp the interesting ones.\n */\nconst EVENT_TYPES = [\n \"motion\",\n \"smartDetectZone\",\n \"smartDetectLine\",\n \"smartAudioDetect\",\n \"ring\",\n \"doorAccess\",\n \"nfcCardScanned\",\n \"fingerprintIdentified\",\n \"disconnect\",\n \"cameraConnected\",\n \"cameraDisconnected\",\n \"recordingDeleted\",\n] as const;\n\n/**\n * The default set: what a person means by \"what happened\". Motion, anything the\n * camera classified, and doorbell rings — with the device-health chatter left out.\n */\nconst DEFAULT_EVENT_TYPES = [\n \"motion\",\n \"smartDetectZone\",\n \"smartDetectLine\",\n \"smartAudioDetect\",\n \"ring\",\n] as const;\n\nconst SMART_DETECT_TYPES = [\n \"person\",\n \"vehicle\",\n \"animal\",\n \"package\",\n \"licensePlate\",\n \"face\",\n \"alrmSmoke\",\n \"alrmCmonx\",\n \"alrmSiren\",\n \"alrmBabyCry\",\n \"alrmSpeak\",\n \"alrmBark\",\n \"alrmBurglar\",\n \"alrmCarHorn\",\n \"alrmGlassBreak\",\n] as const;\n\nexport const registerEventTools = (\n server: McpServer,\n client: ProtectClient,\n ctx: ToolContext,\n): void => {\n server.registerTool(\n \"unifi_protect_list_events\",\n {\n description:\n \"Search recorded events over any time range — motion, smart detections (person, \" +\n \"vehicle, animal, package, licence plate), doorbell rings, and camera connection \" +\n 'changes. This is the tool for questions like \"what happened at the front door last ' +\n \"night\\\". Each result carries its camera's NAME as well as its id, so no second lookup \" +\n \"is needed. Narrow with `types`, `smartDetectTypes` and `cameraId` wherever you can: a \" +\n \"busy system logs thousands of motion events a day, and an unfiltered query returns \" +\n \"the newest slice of that rather than the interesting part.\",\n inputSchema: {\n start: timeArg(\"Beginning of the search window.\"),\n end: timeArg(\"End of the search window.\"),\n types: z\n .array(z.enum(EVENT_TYPES))\n .optional()\n .describe(\n \"Event types to include. Defaults to motion, smart detections and rings. \" +\n \"`smartDetectZone` is the object-detection type — pair it with smartDetectTypes \" +\n \"to ask for people or vehicles specifically.\",\n ),\n smartDetectTypes: z\n .array(z.enum(SMART_DETECT_TYPES))\n .optional()\n .describe(\n 'What the camera classified, e.g. [\"person\"] or [\"vehicle\",\"licensePlate\"]. Only ' +\n \"meaningful for smartDetectZone / smartDetectLine events; the alrm* values are \" +\n \"audio detections. Cameras without smart detection never produce these.\",\n ),\n cameraId: cameraIdArg\n .optional()\n .describe(\n \"Restrict to one camera — the `id` from unifi_protect_list_cameras. Omit for all cameras.\",\n ),\n limit: limitArg,\n order: z\n .enum([\"newest\", \"oldest\"])\n .default(\"newest\")\n .describe(\"Which end of the window to return first.\"),\n },\n annotations: { readOnlyHint: true },\n },\n async ({ start, end, types, smartDetectTypes, cameraId, limit, order }) =>\n wrap(async () => {\n // Default to the last 24 hours rather than whatever the console decides,\n // which is undocumented and has changed between releases.\n const endMs = end === undefined ? Date.now() : toEpochMs(end);\n const startMs = start === undefined ? endMs - 86_400_000 : toEpochMs(start);\n\n if (startMs >= endMs) {\n throw new Error(\n `The window is empty: start (${new Date(startMs).toISOString()}) is not before end ` +\n `(${new Date(endMs).toISOString()}).`,\n );\n }\n\n // `types` is ALWAYS sent, even when the caller passed none. Omitting it\n // triggers a pagination bug in Protect where the console ignores the\n // window and returns the wrong slice — a silently wrong answer rather\n // than an error. Sending an explicit list is the documented workaround.\n const requestedTypes = types && types.length > 0 ? types : [...DEFAULT_EVENT_TYPES];\n\n const raw = await client.get<unknown>(\"events\", {\n start: startMs,\n end: endMs,\n limit,\n orderDirection: order === \"newest\" ? \"DESC\" : \"ASC\",\n types: [...requestedTypes],\n ...(smartDetectTypes && smartDetectTypes.length > 0\n ? { smartDetectTypes: [...smartDetectTypes] }\n : {}),\n // The console's own descriptions are long, redundant with the fields\n // already returned, and localised — pure token cost.\n withoutDescriptions: \"true\",\n });\n\n const cameras = await ctx.devices.cameras();\n const events = Array.isArray(raw) ? raw : [];\n const shaped = events\n .filter((e): e is Rec => typeof e === \"object\" && e !== null)\n // The API has no camera filter on this endpoint, so it is applied here.\n .filter((e) => cameraId === undefined || e.camera === cameraId)\n .map((e) => summarizeEvent(e, cameras));\n\n return {\n window: { start: new Date(startMs).toISOString(), end: new Date(endMs).toISOString() },\n types: requestedTypes,\n count: shaped.length,\n // Say so when the limit was the binding constraint, rather than\n // letting a truncated list read as a complete one.\n ...(events.length >= limit\n ? {\n truncated: true,\n note: `Returned the ${order === \"newest\" ? \"newest\" : \"oldest\"} ${limit} events; more exist in this window. Narrow the range or raise limit.`,\n }\n : {}),\n events: shaped,\n };\n }),\n );\n\n server.registerTool(\n \"unifi_protect_get_event\",\n {\n description:\n \"Get one event's full record, including detection metadata the search results leave \" +\n \"out — per-object tracking, detected zones, licence plate text and vehicle attributes \" +\n \"where the camera captured them. Use the `id` from unifi_protect_list_events.\",\n inputSchema: {\n eventId: z.string().min(1).describe(\"Event id — the `id` from unifi_protect_list_events.\"),\n },\n annotations: { readOnlyHint: true },\n },\n async ({ eventId }) => wrap(() => client.get(`events/${encodeURIComponent(eventId)}`)),\n );\n\n server.registerTool(\n \"unifi_protect_get_event_thumbnail\",\n {\n description:\n \"Fetch the still image Protect captured for an event — the frame that triggered the \" +\n 'detection. Writes it to disk and returns the path by default; set output=\"image\" to ' +\n \"return it inline for a vision model to look at, which costs a large amount of context. \" +\n \"Pass the event's `id` from unifi_protect_list_events; results showing \" +\n \"`hasThumbnail: true` have one.\",\n inputSchema: {\n eventId: z\n .string()\n .min(1)\n .describe(\n \"Event id — the `id` from unifi_protect_list_events. Results with \" +\n \"`hasThumbnail: true` have an image; others return 404. A raw `e-…` value from \" +\n \"the console's own payload is also accepted.\",\n ),\n output: z\n .enum([\"file\", \"image\"])\n .default(\"file\")\n .describe('\"file\" writes it to disk and returns the path; \"image\" returns it inline.'),\n savePath: z\n .string()\n .optional()\n .describe(\n \"Absolute path to write the JPEG to. Defaults to a file under \" +\n \"UNIFI_PROTECT_SNAPSHOT_DIR. Parent directories are created.\",\n ),\n },\n annotations: { readOnlyHint: true },\n },\n async ({ eventId, output, savePath }) =>\n wrapResult(async () => {\n // The console reports an event's thumbnail as `e-<eventId>`, but that\n // form belongs to `thumbnails/<id>`; `events/<id>/thumbnail` wants the\n // bare event id. Strip the prefix so either value works rather than\n // making the caller know which endpoint this happens to use.\n const id = eventId.startsWith(\"e-\") ? eventId.slice(2) : eventId;\n const { bytes, contentType } = await client.requestBytes(\n `events/${encodeURIComponent(id)}/thumbnail`,\n { accept: \"image/jpeg\" },\n );\n\n if (output === \"image\") {\n return {\n content: [\n {\n type: \"image\" as const,\n data: Buffer.from(bytes).toString(\"base64\"),\n mimeType: contentType.startsWith(\"image/\") ? contentType : \"image/jpeg\",\n },\n ],\n };\n }\n\n const path = savePath ?? join(ctx.config.snapshotDir, `event-${slugId(id)}.jpg`);\n await mkdir(join(path, \"..\"), { recursive: true });\n await writeFile(path, bytes);\n return ok({ path, bytes: bytes.byteLength, contentType });\n }),\n );\n\n server.registerTool(\n \"unifi_protect_export_video\",\n {\n description:\n \"Export recorded footage from one camera over a time range as an MP4 file on disk. \" +\n \"Always writes to a file and returns the path — video is never returned inline. Size \" +\n \"grows quickly with the window: expect tens of megabytes per minute at full quality, \" +\n \"and the call fails rather than exhausting memory if the export exceeds \" +\n \"UNIFI_PROTECT_MAX_DOWNLOAD_BYTES. Footage only exists if the camera was recording at \" +\n \"the time, so check the recording mode before concluding that nothing happened.\",\n inputSchema: {\n cameraId: cameraIdArg,\n start: requiredTimeArg(\"Beginning of the footage to export.\"),\n end: requiredTimeArg(\"End of the footage to export.\"),\n savePath: z\n .string()\n .optional()\n .describe(\n \"Absolute path to write the MP4 to. Defaults to a timestamped file under \" +\n \"UNIFI_PROTECT_SNAPSHOT_DIR. Parent directories are created.\",\n ),\n channel: z\n .number()\n .int()\n .min(0)\n .max(3)\n .default(0)\n .describe(\n \"Encoder channel: 0 is the high-quality stream, higher numbers are progressively \" +\n \"lower bitrate. Use a higher channel to keep a long export manageable.\",\n ),\n },\n annotations: { readOnlyHint: true },\n },\n async ({ cameraId, start, end, savePath, channel }) =>\n wrap(async () => {\n const startMs = toEpochMs(start);\n const endMs = toEpochMs(end);\n if (startMs >= endMs) {\n throw new Error(\n `The window is empty: start (${new Date(startMs).toISOString()}) is not before end ` +\n `(${new Date(endMs).toISOString()}).`,\n );\n }\n\n const { bytes, contentType } = await client.requestBytes(\"video/export\", {\n query: { camera: cameraId, start: startMs, end: endMs, channel },\n accept: \"video/mp4\",\n });\n\n const cameras = await ctx.devices.cameras();\n const name = cameras.get(cameraId) ?? cameraId;\n const stamp = new Date(startMs).toISOString().replace(/[:.]/g, \"-\");\n const path = savePath ?? join(ctx.config.snapshotDir, `${slugId(name)}-${stamp}.mp4`);\n await mkdir(join(path, \"..\"), { recursive: true });\n await writeFile(path, bytes);\n\n return {\n path,\n bytes: bytes.byteLength,\n contentType,\n camera: name,\n window: { start: new Date(startMs).toISOString(), end: new Date(endMs).toISOString() },\n };\n }),\n );\n};\n\nconst slugId = (value: string): string =>\n value\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-|-$/g, \"\") || \"export\";\n","import type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { z } from \"zod\";\n\nimport { WritesDisabledError } from \"../client/errors.js\";\nimport type { ProtectClient } from \"../client/protect.js\";\nimport { wrap } from \"./util.js\";\n\n/**\n * Reject anything that is not a plain relative path. The server decides the\n * host and the `/proxy/protect/api` prefix; letting a caller supply either\n * would send the console session somewhere it does not belong.\n */\nexport const assertSafePath = (path: string): void => {\n if (/^[a-z][a-z0-9+.-]*:\\/\\//i.test(path)) {\n throw new Error(\"`path` must be a path, not an absolute URL — the server sets the host.\");\n }\n if (path.split(\"/\").includes(\"..\")) {\n throw new Error(\"`path` must not contain `..` segments.\");\n }\n};\n\nexport const registerRequestTool = (\n server: McpServer,\n client: ProtectClient,\n allowWrites: boolean,\n): void => {\n const methods = allowWrites ? ([\"GET\", \"POST\", \"PATCH\", \"DELETE\"] as const) : ([\"GET\"] as const);\n\n server.registerTool(\n \"unifi_protect_request\",\n {\n description:\n \"Escape hatch: call any private Protect API endpoint directly, relative to \" +\n \"/proxy/protect/api. This exists because the private API is undocumented and Ubiquiti \" +\n \"moves endpoints between Protect releases — when a wrapped tool starts returning 404, \" +\n \"this reaches the replacement without waiting for a new version of this server. \" +\n \"Responses are returned RAW and unshaped, so a broad endpoint like `bootstrap` can \" +\n \"return hundreds of kilobytes; prefer the wrapped tools, which summarize. \" +\n (allowWrites\n ? \"Writes are ENABLED, so POST, PATCH and DELETE are permitted — there is no \" +\n \"confirmation step here, so check the path before you call it.\"\n : \"Writes are DISABLED: only GET is permitted. Set UNIFI_PROTECT_ALLOW_WRITES=1 to \" +\n \"allow mutations.\"),\n inputSchema: {\n path: z\n .string()\n .min(1)\n .describe(\n 'Path relative to /proxy/protect/api, without a leading slash, e.g. \"cameras\", ' +\n '\"nvr\", \"events/abc123\". Not an absolute URL.',\n ),\n method: z.enum(methods).default(\"GET\").describe(\"HTTP method.\"),\n query: z\n .record(z.string(), z.string())\n .optional()\n .describe(\n 'Query parameters as a flat string map, e.g. {\"start\":\"1756500000000\"}. Remember ' +\n \"that Protect times are milliseconds since the epoch.\",\n ),\n body: z\n .record(z.string(), z.unknown())\n .optional()\n .describe(\"JSON request body, for POST and PATCH.\"),\n },\n annotations: { readOnlyHint: !allowWrites, destructiveHint: allowWrites },\n },\n async ({ path, method, query, body }) =>\n wrap(async () => {\n // Belt and braces: the enum already excludes writes, but a client could\n // hand-roll a request that skips schema validation entirely.\n if (!allowWrites && method !== \"GET\") {\n throw new WritesDisabledError(`unifi_protect_request with method ${method}`);\n }\n assertSafePath(path);\n return client.request(method, path, {\n ...(query ? { query } : {}),\n ...(body ? { body } : {}),\n });\n }),\n );\n};\n","import { statSync } from \"node:fs\";\n\nimport type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { z } from \"zod\";\n\nimport type { ProtectClient } from \"../client/protect.js\";\nimport { isConfigured, setupInstructions } from \"../config.js\";\nimport type { ToolContext } from \"./index.js\";\nimport { confirmArg, wrap } from \"./util.js\";\n\nconst fileMode = (path: string): number | undefined => {\n try {\n return statSync(path).mode & 0o777;\n } catch {\n return undefined;\n }\n};\n\nexport const registerStatusTools = (\n server: McpServer,\n client: ProtectClient,\n ctx: ToolContext,\n): void => {\n server.registerTool(\n \"unifi_protect_auth_status\",\n {\n description:\n \"Check whether this server can actually reach your UniFi Protect console. By default it \" +\n \"logs in and makes a real call, so the answer reflects the console rather than cached \" +\n \"state — this is the tool to run when something is not working. Reports the host, the \" +\n \"account, the Protect version, whether TLS is verified, and whether writes are enabled; \" +\n \"when nothing is configured it returns the exact setup steps instead. Call this first \" +\n \"when a tool you expected is not listed: an absent tool means missing configuration or \" +\n \"writes being off, not a bug.\",\n inputSchema: {\n probe: z\n .boolean()\n .default(true)\n .describe(\n \"Actually contact the console (logging in if needed) rather than only reporting \" +\n \"what is already cached. Set false for a fast, purely local answer.\",\n ),\n },\n annotations: { readOnlyHint: true },\n },\n async ({ probe }) =>\n wrap(async () => {\n const configured = isConfigured(ctx.config);\n if (!configured) {\n return {\n configured: false,\n host: ctx.config.baseUrl ?? null,\n available_without_credentials: [\"unifi_protect_auth_status\"],\n setup: setupInstructions(ctx.config),\n };\n }\n\n // Reporting only the cached session state answers the wrong question: a\n // freshly started server has no session yet and would report\n // \"authenticated: false\" while being perfectly able to reach the\n // console. So by default this actually logs in and calls the API, and\n // reports what happened.\n let reachable: boolean | undefined;\n let version: unknown;\n let failure: string | undefined;\n if (probe) {\n try {\n const bootstrap = await client.get<Record<string, unknown>>(\"bootstrap\");\n const nvr = bootstrap?.nvr;\n version =\n typeof nvr === \"object\" && nvr !== null\n ? (nvr as Record<string, unknown>).version\n : undefined;\n reachable = true;\n } catch (err) {\n reachable = false;\n failure = err instanceof Error ? err.message : String(err);\n }\n }\n\n const status = ctx.session.describe();\n const mode = fileMode(ctx.config.sessionFile);\n return {\n configured: true,\n host: ctx.config.baseUrl,\n username: status.username ?? null,\n ...(probe\n ? {\n reachable,\n ...(version !== undefined ? { protectVersion: version } : {}),\n ...(failure ? { failure } : {}),\n }\n : {}),\n session: {\n authenticated: status.authenticated,\n // \"restored\" means it came from disk and has not been proven yet —\n // the first real call is what confirms it, via a 401 and re-login\n // if the cookie has expired.\n source: status.source,\n established_at: status.savedAt ?? null,\n file: ctx.config.sessionFile,\n file_mode: mode === undefined ? \"absent\" : `0${mode.toString(8)}`,\n ...(mode !== undefined && (mode & 0o077) !== 0\n ? { warning: `Readable by other users. Run: chmod 600 ${ctx.config.sessionFile}` }\n : {}),\n },\n tls: ctx.config.verifyTls\n ? \"verified\"\n : \"UNVERIFIED — certificate checks are off for this server's requests only. \" +\n \"Verifying needs both NODE_EXTRA_CA_CERTS pointing at the console certificate and \" +\n \"UNIFI_PROTECT_HOST set to a host name: the certificate carries no IP SAN.\",\n writes: ctx.allowWrites ? \"enabled\" : \"disabled\",\n api:\n \"private (undocumented). Ubiquiti moves these endpoints between Protect releases, so \" +\n \"the Protect version reported above is the first thing to check if a tool that used \" +\n \"to work starts returning 404.\",\n };\n }),\n );\n\n // The login tools are pointless without a console to log in to, so they do not\n // exist at all until one is configured.\n if (!isConfigured(ctx.config)) return;\n\n server.registerTool(\n \"unifi_protect_auth_login\",\n {\n description:\n \"Force a fresh login to the console, replacing any cached session. Normally unnecessary \" +\n \"— the server logs in on demand and re-authenticates automatically on a 401. Use it to \" +\n \"supply a two-factor code, which cannot be done unattended: the code is single-use and \" +\n \"expires in about 30 seconds, so it is passed here once and the resulting session is \" +\n \"then cached and reused.\",\n inputSchema: {\n totp: z\n .string()\n .regex(/^\\d{6,8}$/, \"A two-factor code is 6 to 8 digits.\")\n .optional()\n .describe(\"Current code from your authenticator app. Omit if the account has no 2FA.\"),\n },\n annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },\n },\n async ({ totp }) =>\n wrap(async () => {\n const status = await ctx.session.login(totp);\n return {\n authenticated: status.authenticated,\n username: status.username,\n established_at: status.savedAt,\n session_file: ctx.config.sessionFile,\n note: \"The session is stored with mode 600 and reused until the console rejects it.\",\n };\n }),\n );\n\n server.registerTool(\n \"unifi_protect_auth_logout\",\n {\n description:\n \"Drop the cached session and delete the session file. The next call logs in again from \" +\n \"the configured username and password, so this does not lock anything out — use it to \" +\n \"clear a session after changing accounts, or to remove the cookie from disk.\",\n inputSchema: { confirm: confirmArg },\n annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true },\n },\n async () =>\n wrap(async () => {\n await ctx.session.logout();\n return {\n logged_out: true,\n note: \"Credentials are unchanged; the next tool call will log in again.\",\n };\n }),\n );\n};\n","import type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { z } from \"zod\";\n\nimport type { ProtectClient } from \"../client/protect.js\";\nimport {\n summarizeBootstrap,\n summarizeEach,\n summarizeLiveview,\n summarizeUser,\n} from \"../client/shape.js\";\nimport type { ToolContext } from \"./index.js\";\nimport { compactOrUndefined, confirmArg, wrap } from \"./util.js\";\n\ntype Rec = Record<string, unknown>;\n\nexport const registerSystemTools = (\n server: McpServer,\n client: ProtectClient,\n ctx: ToolContext,\n): void => {\n server.registerTool(\n \"unifi_protect_get_system_info\",\n {\n description:\n \"Overview of the console: model, Protect version, firmware, timezone, uptime, storage \" +\n \"use and how many devices of each type are adopted. Worth calling first on an unfamiliar \" +\n \"system. The reported Protect version matters: this server talks to Protect's private \" +\n \"API, which Ubiquiti changes between releases, so a version that differs from the one \" +\n \"in the README is the first thing to check if a tool starts returning 404.\",\n inputSchema: {},\n annotations: { readOnlyHint: true },\n },\n async () => wrap(async () => summarizeBootstrap(await client.get<Rec>(\"bootstrap\"))),\n );\n\n server.registerTool(\n \"unifi_protect_list_users\",\n {\n description:\n \"List the accounts that can sign in to Protect, with their role and last login. Useful \" +\n \"for auditing who has access to the cameras.\",\n inputSchema: {},\n annotations: { readOnlyHint: true },\n },\n async () => wrap(async () => summarizeEach(await client.get(\"users\"), summarizeUser)),\n );\n\n server.registerTool(\n \"unifi_protect_list_liveviews\",\n {\n description:\n \"List the saved live views — the named camera grid layouts shown on viewers and in the \" +\n \"Protect app. The returned id is what unifi_protect_update_viewer needs to put a layout \" +\n \"on a screen.\",\n inputSchema: {},\n annotations: { readOnlyHint: true },\n },\n async () => wrap(async () => summarizeEach(await client.get(\"liveviews\"), summarizeLiveview)),\n );\n\n if (!ctx.allowWrites) return;\n\n server.registerTool(\n \"unifi_protect_update_nvr_settings\",\n {\n description:\n \"Change console-wide settings. `isRecordingDisabled` is the significant one: turning it \" +\n \"on stops recording on EVERY camera at once, so nothing is written until it is turned \" +\n \"back off. Only the fields you pass are sent.\",\n inputSchema: {\n name: z.string().min(1).optional().describe(\"Display name for the console.\"),\n timezone: z\n .string()\n .min(1)\n .optional()\n .describe('IANA timezone, e.g. \"Europe/Paris\". Affects event timestamps and schedules.'),\n isRecordingDisabled: z\n .boolean()\n .optional()\n .describe(\n \"Disable recording across every camera. True means no footage is kept, system-wide.\",\n ),\n },\n annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true },\n },\n async ({ name, timezone, isRecordingDisabled }) =>\n wrap(async () => {\n const body = compactOrUndefined({ name, timezone, isRecordingDisabled });\n if (!body) {\n throw new Error(\n \"Nothing to update — pass at least one of name, timezone, isRecordingDisabled.\",\n );\n }\n return client.patch(\"nvr\", body);\n }),\n );\n\n server.registerTool(\n \"unifi_protect_reboot_nvr\",\n {\n description:\n \"REBOOT THE CONSOLE. Every camera stops recording for the two to five minutes it takes \" +\n \"to come back, and footage from that window is lost permanently. This also drops the \" +\n \"network if the console is your router (a UDM or UDM Pro), taking down everything behind \" +\n \"it. Reboot a single unresponsive camera with unifi_protect_reboot_camera instead \" +\n \"wherever that would do.\",\n inputSchema: { confirm: confirmArg },\n annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false },\n },\n async () => wrap(() => client.post(\"nvr/reboot\")),\n );\n};\n","import type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\n\nimport type { SessionProvider } from \"../client/auth.js\";\nimport type { DeviceCache } from \"../client/device-cache.js\";\nimport type { ProtectClient } from \"../client/protect.js\";\nimport { isConfigured, type Config } from \"../config.js\";\nimport { registerCameraTools } from \"./cameras.js\";\nimport { registerDeviceTools } from \"./devices.js\";\nimport { registerEventTools } from \"./events.js\";\nimport { registerRequestTool } from \"./request.js\";\nimport { registerStatusTools } from \"./status.js\";\nimport { registerSystemTools } from \"./system.js\";\n\nexport type ToolContext = {\n config: Config;\n /** Register the mutating tools too. Off by default — see UNIFI_PROTECT_ALLOW_WRITES. */\n allowWrites: boolean;\n session: SessionProvider;\n devices: DeviceCache;\n};\n\n/**\n * Register the UniFi Protect tools.\n *\n * unifi_protect_auth_status comes first and unconditionally, so a server with no\n * console configured is still a useful one — it can say what to set — rather\n * than a connection that closes with its own error message swallowed.\n *\n * Read tools are then always registered; the write tools only when\n * `allowWrites` is set, so with the flag off they are not merely refused — they\n * are absent from tools/list and cannot be called at all. A refusal still lets a\n * model try, retry, and reason about how to get around it; a tool that does not\n * exist ends the conversation.\n */\nexport const registerTools = (server: McpServer, client: ProtectClient, ctx: ToolContext): void => {\n registerStatusTools(server, client, ctx);\n if (!isConfigured(ctx.config)) return;\n\n registerSystemTools(server, client, ctx);\n registerCameraTools(server, client, ctx);\n registerEventTools(server, client, ctx);\n registerDeviceTools(server, client, ctx);\n registerRequestTool(server, client, ctx.allowWrites);\n};\n","import { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\n\nimport { BUILD_INFO } from \"./build-info.js\";\nimport { createSessionProvider, type Logger, type SessionProvider } from \"./client/auth.js\";\nimport { createDeviceCache, type DeviceCache } from \"./client/device-cache.js\";\nimport { ProtectClient } from \"./client/protect.js\";\nimport { createHttpFetch } from \"./client/tls.js\";\nimport type { Config } from \"./config.js\";\nimport { registerTools } from \"./tools/index.js\";\n\nexport const SERVER_NAME = BUILD_INFO.name;\nexport const SERVER_VERSION = BUILD_INFO.version;\nexport const USER_AGENT = `mcp-unifi-protect-js/${BUILD_INFO.version}`;\n\nexport type CreateServerOptions = {\n config: Config;\n fetch?: typeof fetch;\n logger?: Logger;\n /** Override the session provider (tests, and the interactive login flow). */\n session?: SessionProvider;\n};\n\nexport type CreatedServer = {\n server: McpServer;\n client: ProtectClient;\n session: SessionProvider;\n devices: DeviceCache;\n};\n\nexport const createServer = (opts: CreateServerOptions): CreatedServer => {\n const { config } = opts;\n const server = new McpServer({ name: SERVER_NAME, version: SERVER_VERSION });\n\n // Built once and shared by the session provider and the client, so the TLS\n // policy is a property of this server's requests rather than of the process.\n // Tests inject `opts.fetch` and never reach createHttpFetch.\n const fetchImpl =\n opts.fetch ??\n createHttpFetch({\n insecureTls: !config.verifyTls,\n ...(opts.logger ? { logger: opts.logger } : {}),\n });\n\n const session =\n opts.session ??\n createSessionProvider({\n config,\n fetch: fetchImpl,\n ...(opts.logger ? { logger: opts.logger } : {}),\n });\n\n const client = new ProtectClient({\n // An unconfigured server still constructs a client, so createServer stays\n // total. No tool that could use it is registered, and the session provider\n // throws a message naming the fix if something ever reaches it anyway.\n baseUrl: config.baseUrl ?? \"https://unconfigured.invalid\",\n session,\n maxRetries: config.maxRetries,\n userAgent: USER_AGENT,\n maxDownloadBytes: config.maxDownloadBytes,\n fetch: fetchImpl,\n ...(opts.logger ? { logger: opts.logger } : {}),\n });\n\n const devices = createDeviceCache({ client, ttlSeconds: config.deviceCacheTtlSeconds });\n\n registerTools(server, client, {\n config,\n allowWrites: config.allowWrites,\n session,\n devices,\n });\n\n return { server, client, session, devices };\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"server-B2-0Akro.js","names":["undiciFetch"],"sources":["../src/build-info.ts","../src/config.ts","../src/client/errors.ts","../src/client/session-store.ts","../src/client/auth.ts","../src/client/shape.ts","../src/client/device-cache.ts","../src/client/protect.ts","../src/client/tls.ts","../src/tools/util.ts","../src/tools/cameras.ts","../src/tools/devices.ts","../src/tools/events.ts","../src/tools/request.ts","../src/tools/status.ts","../src/tools/system.ts","../src/tools/index.ts","../src/server.ts"],"sourcesContent":["// Build-time / runtime identity for the running server. `name`/`version` are\n// read from package.json at startup (always accurate); `gitCommit` /\n// `gitCommitDate` are injected by tsdown's `define` substitution at build time\n// and fall back to \"unknown\" when running from source (e.g. vitest).\n\nimport { readFileSync } from \"node:fs\";\n\n// oxlint-disable no-underscore-dangle -- bundler-injected build-time constants.\ndeclare const __GIT_COMMIT__: string;\ndeclare const __GIT_COMMIT_DATE__: string;\n\ntype PackageJson = { name: string; version: string };\n\nconst readPackageJson = (): PackageJson => {\n try {\n const pkgUrl = new URL(\"../package.json\", import.meta.url);\n return JSON.parse(readFileSync(pkgUrl, \"utf8\")) as PackageJson;\n } catch {\n return { name: \"@mgcrea/mcp-unifi-protect\", version: \"0.0.0\" };\n }\n};\n\nconst pkg = readPackageJson();\n\nexport type BuildInfo = {\n name: string;\n version: string;\n gitCommit: string;\n gitCommitDate: string;\n};\n\nexport const BUILD_INFO: BuildInfo = {\n name: pkg.name,\n version: pkg.version,\n gitCommit: typeof __GIT_COMMIT__ === \"string\" ? __GIT_COMMIT__ : \"unknown\",\n gitCommitDate: typeof __GIT_COMMIT_DATE__ === \"string\" ? __GIT_COMMIT_DATE__ : \"unknown\",\n};\n","import { readFileSync, statSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\n\nimport { z } from \"zod\";\n\n/**\n * The private (undocumented) Protect API, mounted under UniFi OS's proxy. This\n * server deliberately wraps this rather than the official Integration API at\n * `/proxy/protect/integration/v1`: the official one has NO historical query\n * capability at all — its only query parameters in the entire published OpenAPI\n * spec are `channel`, `highQuality` and `qualities` — so \"what happened at the\n * front door last night\" is unanswerable through it.\n *\n * The trade is real and accepted: nothing below is contractual, and Ubiquiti\n * moves these endpoints between Protect releases. `unifi_protect_get_system_info`\n * reports the running version so a mismatch is visible, and\n * `unifi_protect_request` reaches anything that moved without a code change.\n */\nexport const PRIVATE_API_PATH = \"/proxy/protect/api\";\n\n/** UniFi OS's own auth surface, which is NOT under the Protect proxy path. */\nexport const LOGIN_PATH = \"/api/auth/login\";\n\n/** The realtime channel. Not used yet — see the WebSocket note in the README. */\nexport const UPDATES_WS_PATH = \"/proxy/protect/ws/updates\";\n\nconst ConfigSchema = z\n .object({\n /** Console origin, e.g. `https://192.168.1.1` or `https://10.0.0.1:8443`. */\n baseUrl: z.string().min(1).optional(),\n username: z.string().min(1).optional(),\n password: z.string().min(1).optional(),\n /**\n * A 2FA code is single-use and expires in ~30s, so it is only ever useful\n * for a one-shot `unifi_protect_auth_login`, never for an unattended start.\n */\n totp: z.string().min(1).optional(),\n /**\n * On by default. Turning it off is scoped to this server's own requests via\n * an undici dispatcher, not the whole process.\n *\n * Verifying takes two things together, and one alone does nothing: the\n * console's certificate is self-signed (so NODE_EXTRA_CA_CERTS must point at\n * it) AND it carries no IP SAN — `CN=unifi.local`, SANs for `unifi.local`,\n * `localhost` and `127.0.0.1` — so UNIFI_PROTECT_HOST must be a host name\n * that resolves to the console, never its IP address.\n */\n verifyTls: z.boolean().default(true),\n allowWrites: z.boolean().default(false),\n sessionFile: z.string().min(1),\n snapshotDir: z.string().min(1),\n maxRetries: z.number().int().nonnegative().max(10).default(3),\n /** Guards against an unexpectedly huge video export blowing up the process. */\n maxDownloadBytes: z.number().int().positive().default(200_000_000),\n /** How long a cached camera id→name index stays fresh, in seconds. */\n deviceCacheTtlSeconds: z.number().int().nonnegative().max(3600).default(60),\n })\n .strict()\n .superRefine((cfg, ctx) => {\n // Deliberately NOT an error when nothing is configured. An MCP server that\n // exits at startup shows up in the client as a bare \"MCP error -32000:\n // Connection closed\" with stderr swallowed — so the one message that would\n // have explained what to set never reaches anyone. The server stays up,\n // registers unifi_protect_auth_status, and reports the gap as data.\n //\n // A half-configured install IS worth flagging, though: a host with no\n // password is a typo, not a deliberate state, and saying so beats letting\n // every call fail with an auth error that reads like a wrong password.\n if (cfg.baseUrl && !cfg.username) {\n ctx.addIssue({\n code: \"custom\",\n path: [\"username\"],\n message:\n \"UNIFI_PROTECT_HOST is set but UNIFI_PROTECT_USERNAME is not. Both, plus \" +\n \"UNIFI_PROTECT_PASSWORD, are needed to reach the console.\",\n });\n }\n if (cfg.username && !cfg.password) {\n ctx.addIssue({\n code: \"custom\",\n path: [\"password\"],\n message: \"UNIFI_PROTECT_USERNAME is set but UNIFI_PROTECT_PASSWORD is not.\",\n });\n }\n });\n\nexport type Config = z.infer<typeof ConfigSchema>;\n\n/**\n * The on-disk config document. Keys are camelCase to mirror `Config` rather\n * than the env var names: this is a typed JSON file, not a shell.\n *\n * `.strict()` on purpose — a typo'd `userName` must be an error. Silently\n * ignoring an unknown key looks exactly like \"that setting had no effect\",\n * which is the worst way to learn your credentials came from somewhere else.\n */\nconst FileConfigSchema = z\n .object({\n host: z.string().min(1).optional(),\n username: z.string().min(1).optional(),\n password: z.string().min(1).optional(),\n verifyTls: z.boolean().optional(),\n allowWrites: z.boolean().optional(),\n sessionFile: z.string().min(1).optional(),\n snapshotDir: z.string().min(1).optional(),\n maxRetries: z.number().int().nonnegative().max(10).optional(),\n maxDownloadBytes: z.number().int().positive().optional(),\n deviceCacheTtlSeconds: z.number().int().nonnegative().max(3600).optional(),\n })\n .strict();\n\nexport type FileConfig = z.infer<typeof FileConfigSchema>;\n\n/**\n * Normalize whatever someone pasted into a console origin, preserving the port.\n *\n * \"192.168.1.1\" -> \"https://192.168.1.1\"\n * \"10.0.0.1:8443\" -> \"https://10.0.0.1:8443\"\n * \"https://udm.lan/protect/\" -> \"https://udm.lan\"\n *\n * A port must survive: consoles are commonly reached on a non-443 port, and the\n * normalizers in mcp-keycloak and mcp-shopify both drop it. Everything after\n * the origin is discarded — the API paths are this server's business, and a\n * pasted `/protect/dashboard` URL would otherwise be prefixed onto every call.\n *\n * `https` is forced: UniFi OS redirects plain HTTP, and following that redirect\n * would send the session cookie over cleartext on the first hop.\n */\nexport const normalizeBaseUrl = (raw: string): string => {\n const trimmedRaw = raw.trim().replace(/\\/+$/, \"\");\n const withScheme = /^https?:\\/\\//i.test(trimmedRaw) ? trimmedRaw : `https://${trimmedRaw}`;\n const url = new URL(withScheme);\n // `url.host` rather than `hostname` so an explicit :port is kept.\n return `https://${url.host}`;\n};\n\nconst parseBool = (value: string | undefined): boolean | undefined => {\n const t = trimmed(value);\n if (t === undefined) return undefined;\n return [\"1\", \"true\", \"yes\", \"on\"].includes(t.toLowerCase());\n};\n\nconst parseIntOpt = (value: string | undefined): number | undefined => {\n if (value === undefined || value.trim() === \"\") return undefined;\n const n = Number(value);\n return Number.isInteger(n) ? n : undefined;\n};\n\n/** Maps \"\" to undefined, so an empty env var means \"unset\" rather than \"empty\". */\nconst trimmed = (value: string | undefined): string | undefined => {\n const t = value?.trim();\n return t ? t : undefined;\n};\n\nconst message = (err: unknown): string => (err instanceof Error ? err.message : String(err));\n\n/** `readFileSync` does not expand `~`, but it is the natural thing to write in a config file. */\nexport const expandTilde = (path: string): string =>\n path === \"~\" || path.startsWith(\"~/\") ? join(homedir(), path.slice(1)) : path;\n\n/**\n * Where the config file lives, most specific first: an explicit override, then\n * the XDG location, then the conventional `~/.config`.\n */\nexport const resolveConfigPath = (env: NodeJS.ProcessEnv = process.env): string => {\n const explicit = trimmed(env.UNIFI_PROTECT_CONFIG);\n if (explicit) return expandTilde(explicit);\n const base = trimmed(env.XDG_CONFIG_HOME) ?? join(homedir(), \".config\");\n return join(expandTilde(base), \"unifi-protect\", \"config.json\");\n};\n\n/** The session file sits beside the config file unless told otherwise. */\nexport const resolveSessionPath = (env: NodeJS.ProcessEnv = process.env): string =>\n join(dirname(resolveConfigPath(env)), \"session.json\");\n\n/**\n * The session file holds a live console cookie, so being readable by other\n * users is worth saying out loud. A warning and not an error: refusing to start\n * would be a worse trade for someone on a single-user machine.\n */\nexport const warnIfGroupReadable = (path: string): void => {\n if (process.platform === \"win32\") return; // mode bits mean nothing here\n try {\n if (statSync(path).mode & 0o077) {\n process.stderr.write(\n `[unifi-protect] ${path} is readable by other users. Run: chmod 600 ${path}\\n`,\n );\n }\n } catch {\n // Not worth failing startup over.\n }\n};\n\n/**\n * Read the config file, treating \"absent\" as \"contributes nothing\". Every other\n * failure throws and names the path, so a malformed file is never mistaken for\n * a missing one — that confusion sends you hunting for credentials that were\n * sitting right there.\n */\nconst readConfigFile = (path: string): FileConfig => {\n let raw: string;\n try {\n raw = readFileSync(path, \"utf8\");\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") return {};\n throw new Error(`Could not read the config file (${path}): ${message(err)}`, { cause: err });\n }\n\n warnIfGroupReadable(path);\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (err) {\n throw new Error(`The config file (${path}) is not valid JSON: ${message(err)}`, { cause: err });\n }\n\n const result = FileConfigSchema.safeParse(parsed);\n if (!result.success) {\n const issues = result.error.issues\n .map((issue) => `${issue.path.join(\".\") || \"(root)\"}: ${issue.message}`)\n .join(\"; \");\n throw new Error(`The config file (${path}) is not valid: ${issues}`);\n }\n return result.data;\n};\n\n/**\n * Environment first, config file second, **per field** — not whole-source.\n * Docker and CI inject the environment and must keep working untouched, while a\n * one-off `UNIFI_PROTECT_ALLOW_WRITES=0` still has to override a file that says\n * `true`. Merging field by field is the only rule that gives both.\n */\nexport const loadConfig = (\n env: NodeJS.ProcessEnv = process.env,\n configPath: string = resolveConfigPath(env),\n): Config => {\n const file = readConfigFile(configPath);\n const host = trimmed(env.UNIFI_PROTECT_HOST) ?? file.host;\n const sessionFile =\n trimmed(env.UNIFI_PROTECT_SESSION_FILE) ?? file.sessionFile ?? resolveSessionPath(env);\n const snapshotDir =\n trimmed(env.UNIFI_PROTECT_SNAPSHOT_DIR) ??\n file.snapshotDir ??\n join(homedir(), \".cache\", \"unifi-protect\");\n\n return ConfigSchema.parse({\n baseUrl: host ? normalizeBaseUrl(host) : undefined,\n username: trimmed(env.UNIFI_PROTECT_USERNAME) ?? file.username,\n password: trimmed(env.UNIFI_PROTECT_PASSWORD) ?? file.password,\n totp: trimmed(env.UNIFI_PROTECT_TOTP),\n verifyTls: parseBool(env.UNIFI_PROTECT_VERIFY_TLS) ?? file.verifyTls,\n allowWrites: parseBool(env.UNIFI_PROTECT_ALLOW_WRITES) ?? file.allowWrites,\n sessionFile: expandTilde(sessionFile),\n snapshotDir: expandTilde(snapshotDir),\n maxRetries: parseIntOpt(env.UNIFI_PROTECT_MAX_RETRIES) ?? file.maxRetries,\n maxDownloadBytes: parseIntOpt(env.UNIFI_PROTECT_MAX_DOWNLOAD_BYTES) ?? file.maxDownloadBytes,\n deviceCacheTtlSeconds:\n parseIntOpt(env.UNIFI_PROTECT_DEVICE_CACHE_TTL) ?? file.deviceCacheTtlSeconds,\n });\n};\n\n/** True once the server has everything it needs to reach a console. */\nexport const isConfigured = (config: Config): boolean =>\n Boolean(config.baseUrl && config.username && config.password);\n\n/**\n * Returned by unifi_protect_auth_status and printed to stderr at startup. Prose\n * rather than a code, because this is the text someone acts on when nothing\n * works — and the server can no longer signal it by refusing to start.\n */\nexport const setupInstructions = (config: Config): string[] => {\n if (isConfigured(config)) return [];\n return [\n \"No UniFi Protect console is configured, so only unifi_protect_auth_status is registered.\",\n \"Set UNIFI_PROTECT_HOST to your console's address — the IP or hostname of the UDM Pro, \" +\n \"UNVR or Cloud Key, e.g. 192.168.1.1 (https:// is assumed, and a :port is preserved).\",\n \"Set UNIFI_PROTECT_USERNAME and UNIFI_PROTECT_PASSWORD to a console login.\",\n // The advice that actually matters. A Ubiquiti SSO account often cannot log\n // in locally at all, and reusing the owner account hands an agent the keys\n // to the whole console rather than to Protect.\n \"Create a dedicated LOCAL user for this rather than reusing your own: UniFi OS → Settings → \" +\n \"Admins & Users → Add User → Local Access Only, and give it Protect permissions only. A \" +\n \"Ubiquiti cloud (SSO) account may fail local login entirely, and a local account keeps the \" +\n \"blast radius to Protect.\",\n \"Give that user View-Only rights unless you intend to set UNIFI_PROTECT_ALLOW_WRITES=1 — \" +\n \"with writes off the mutating tools are not registered at all, so they cannot be called.\",\n \"If the account has 2FA, pass a current code once via unifi_protect_auth_login; the session \" +\n \"is then cached and reused. A code in UNIFI_PROTECT_TOTP expires in about 30 seconds, so \" +\n \"it is no use for an unattended start.\",\n \"Certificate verification is ON by default. A console reached by IP can never pass it: \" +\n \"the certificate is self-signed AND has no IP SAN. Either set UNIFI_PROTECT_HOST to a host \" +\n \"name that resolves to the console and point NODE_EXTRA_CA_CERTS at its certificate, or \" +\n \"set UNIFI_PROTECT_VERIFY_TLS=false, which is scoped to this server's requests only. \" +\n \"Set it to true only if you installed a certificate your machine already trusts.\",\n ];\n};\n","/** A non-2xx answer from the console. */\nexport class ProtectApiError extends Error {\n override readonly name = \"ProtectApiError\";\n readonly status: number;\n /** The request path, so an error names what failed without a stack trace. */\n readonly path: string | undefined;\n readonly errors: unknown;\n\n constructor(\n message: string,\n opts: { status: number; path?: string | undefined; errors?: unknown },\n ) {\n super(message);\n this.status = opts.status;\n this.path = opts.path;\n this.errors = opts.errors;\n }\n}\n\n/** The console rejected the credentials, or 2FA is required and was not supplied. */\nexport class ProtectAuthError extends Error {\n override readonly name = \"ProtectAuthError\";\n /** True when the console asked for a 2FA code rather than refusing the password. */\n readonly needsTwoFactor: boolean;\n\n constructor(message: string, opts: { needsTwoFactor?: boolean } = {}) {\n super(message);\n this.needsTwoFactor = opts.needsTwoFactor ?? false;\n }\n}\n\n/** Thrown when a write path is reached while UNIFI_PROTECT_ALLOW_WRITES is off. */\nexport class WritesDisabledError extends Error {\n override readonly name = \"WritesDisabledError\";\n\n constructor(what: string) {\n super(\n `${what} is a write operation, but writes are disabled. ` +\n `Set UNIFI_PROTECT_ALLOW_WRITES=1 to register the mutating tools.`,\n );\n }\n}\n\n/** Thrown when the server is asked to reach a console it has no credentials for. */\nexport class NotConfiguredError extends Error {\n override readonly name = \"NotConfiguredError\";\n\n constructor() {\n super(\n \"No UniFi Protect console is configured. Call unifi_protect_auth_status for the setup steps.\",\n );\n }\n}\n","import { chmod, mkdir, readFile, unlink, writeFile } from \"node:fs/promises\";\nimport { dirname } from \"node:path\";\n\n/**\n * A console session: the bare cookie value plus the CSRF token that was current\n * when it was issued. Both are needed on every request, and neither is derivable\n * from the other, so they are stored and invalidated as one unit.\n */\nexport type PersistedSession = {\n /** The `TOKEN=<jwt>` pair, attributes already stripped. */\n cookie: string;\n csrfToken: string;\n /** Which console this belongs to, so a changed host does not reuse a stale session. */\n baseUrl: string;\n /** Which account it was issued to, for the same reason. */\n username: string;\n savedAt: string;\n};\n\n/** Load a persisted session, or undefined if none / unreadable / not JSON. */\nexport const loadSession = async (path: string): Promise<PersistedSession | undefined> => {\n try {\n const raw = await readFile(path, \"utf8\");\n const parsed = JSON.parse(raw) as Partial<PersistedSession>;\n // A truncated or hand-edited file must read as \"no session\" rather than\n // producing a request with `cookie: undefined`, which fails as a 401 and\n // sends you looking at the password.\n if (!parsed.cookie || !parsed.csrfToken || !parsed.baseUrl || !parsed.username) {\n return undefined;\n }\n return parsed as PersistedSession;\n } catch {\n return undefined;\n }\n};\n\n/** Persist a session with owner-only permissions. */\nexport const saveSession = async (path: string, session: PersistedSession): Promise<void> => {\n await mkdir(dirname(path), { recursive: true });\n await writeFile(path, JSON.stringify(session, null, 2), { mode: 0o600 });\n // `mode` on writeFile only applies when the file is CREATED, so a session\n // rewritten over a file that already existed keeps whatever mode it had.\n // chmod every time rather than trusting the create bit.\n await chmod(path, 0o600);\n};\n\n/** Remove a persisted session. Absent is success — logout is idempotent. */\nexport const clearSession = async (path: string): Promise<void> => {\n try {\n await unlink(path);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== \"ENOENT\") throw err;\n }\n};\n","import { LOGIN_PATH, type Config } from \"../config.js\";\nimport { ProtectAuthError } from \"./errors.js\";\nimport { clearSession, loadSession, saveSession, type PersistedSession } from \"./session-store.js\";\n\nexport type Logger = {\n debug?(...args: unknown[]): void;\n warn?(...args: unknown[]): void;\n error?(...args: unknown[]): void;\n};\n\n/** The two headers that authenticate every request to a UniFi OS console. */\nexport type SessionHeaders = {\n cookie: string;\n \"x-csrf-token\": string;\n};\n\nexport type SessionStatus = {\n authenticated: boolean;\n /** Where the live session came from, for unifi_protect_auth_status. */\n source: \"none\" | \"restored\" | \"login\";\n username: string | undefined;\n savedAt: string | undefined;\n};\n\n/**\n * A pluggable source of console session headers. The client calls `headers()`\n * on every request and `invalidate()` on a 401 to force the next call to\n * re-run the handshake.\n */\nexport type SessionProvider = {\n headers(): Promise<SessionHeaders>;\n invalidate(): void;\n /** Force a fresh handshake now, optionally with a 2FA code. Used by auth_login. */\n login(totp?: string): Promise<SessionStatus>;\n /** Drop the session in memory and on disk. */\n logout(): Promise<void>;\n describe(): SessionStatus;\n};\n\nexport type SessionProviderOptions = {\n config: Config;\n fetch?: typeof fetch;\n logger?: Logger;\n now?: () => number;\n};\n\n/**\n * Read one header, collapsing undici's `string | string[] | undefined` shape.\n * Multi-value headers (the Set-Cookie shape) collapse to their first entry,\n * which is all the handshake needs — the session is a single cookie.\n */\nconst header = (res: Response, name: string): string | undefined =>\n res.headers.get(name) ?? undefined;\n\n/**\n * Keep only the `TOKEN=value` pair, dropping the cookie's attributes (Path,\n * Expires, HttpOnly, …). Sending those attributes back on a request header is\n * malformed and the console rejects the session.\n */\nconst bareCookie = (setCookie: string): string => {\n const semicolon = setCookie.indexOf(\";\");\n return semicolon === -1 ? setCookie : setCookie.slice(0, semicolon);\n};\n\n/**\n * Whether a login response is asking for a second factor rather than refusing\n * the password. Protect answers 499 for this; some firmwares use a 401 with a\n * body naming the requirement, so both are probed.\n */\nconst isTwoFactorChallenge = (status: number, body: string): boolean =>\n status === 499 || /2fa|two[- ]factor|mfa|otp/i.test(body);\n\nexport const createSessionProvider = (opts: SessionProviderOptions): SessionProvider => {\n const { config } = opts;\n const fetchImpl = opts.fetch ?? fetch;\n const logger = opts.logger;\n\n let session: PersistedSession | undefined;\n let source: SessionStatus[\"source\"] = \"none\";\n // A single in-flight promise coordinates concurrent callers. Unlike a locally\n // signed token, a console login is a network call that must not be issued\n // twice: the console rotates the CSRF token on every successful login, so two\n // races leave whichever finished last, which need not be the one whose token\n // the caller went on to use.\n let inflight: Promise<PersistedSession> | undefined;\n let restored = false;\n\n const requireCredentials = (): { baseUrl: string; username: string; password: string } => {\n if (!config.baseUrl || !config.username || !config.password) {\n throw new ProtectAuthError(\n \"No console credentials are configured. Call unifi_protect_auth_status for the setup steps.\",\n );\n }\n return { baseUrl: config.baseUrl, username: config.username, password: config.password };\n };\n\n /**\n * Fetch a CSRF token from the console root. UniFi OS gates login behind CSRF\n * protection, so a first login on a cold start has nothing to present until\n * this runs.\n */\n const fetchCsrfToken = async (baseUrl: string): Promise<string | undefined> => {\n try {\n const res = await fetchImpl(baseUrl, { method: \"GET\", redirect: \"manual\" });\n return header(res, \"x-csrf-token\");\n } catch (err) {\n logger?.debug?.(`could not prefetch a CSRF token: ${String(err)}`);\n return undefined;\n }\n };\n\n /** Run the UniFi OS handshake and return a complete session. */\n const handshake = async (totp?: string): Promise<PersistedSession> => {\n const { baseUrl, username, password } = requireCredentials();\n const url = `${baseUrl}${LOGIN_PATH}`;\n const code = totp ?? config.totp;\n\n const attempt = async (csrf: string | undefined): Promise<Response> =>\n fetchImpl(url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n ...(csrf ? { \"x-csrf-token\": csrf } : {}),\n },\n body: JSON.stringify({\n username,\n password,\n // `token` is what the console calls the 2FA code. Omitted entirely\n // rather than sent as null, which some firmwares reject outright.\n ...(code ? { token: code } : {}),\n rememberMe: true,\n }),\n redirect: \"manual\",\n });\n\n logger?.debug?.(`logging in to ${baseUrl} as ${username}`);\n let res = await attempt(session?.csrfToken);\n\n // Rejected with no CSRF token to offer: fetch one from the root document\n // and retry the login exactly once.\n if (!res.ok && !session?.csrfToken) {\n const csrf = await fetchCsrfToken(baseUrl);\n if (csrf) {\n logger?.debug?.(\"retrying login with a freshly fetched CSRF token\");\n res = await attempt(csrf);\n }\n }\n\n if (!res.ok) {\n const body = await res.text().catch(() => \"\");\n if (isTwoFactorChallenge(res.status, body)) {\n throw new ProtectAuthError(\n \"The console requires a two-factor code. Call unifi_protect_auth_login with a current \" +\n \"code from your authenticator app — the session is cached afterwards, so this is a \" +\n \"one-off.\",\n { needsTwoFactor: true },\n );\n }\n throw new ProtectAuthError(\n `Login to ${baseUrl} as ${username} failed: HTTP ${res.status} ${res.statusText}. ` +\n `Check the username and password, and note that a Ubiquiti cloud (SSO) account often ` +\n `cannot log in locally — create a Local Access Only user instead.`,\n );\n }\n\n // The console rotates the CSRF token on a successful login and returns the\n // new one in X-Updated-CSRF-Token. Preferring that over X-CSRF-Token matters:\n // taking the stale one makes the FIRST request succeed and every later one\n // fail, which reads like an expiry problem and is not one.\n const csrfToken = header(res, \"x-updated-csrf-token\") ?? header(res, \"x-csrf-token\");\n const setCookie = res.headers.getSetCookie()[0] ?? header(res, \"set-cookie\");\n\n if (!csrfToken || !setCookie) {\n throw new ProtectAuthError(\n `Login to ${baseUrl} succeeded (HTTP ${res.status}) but returned no ` +\n `${!setCookie ? \"session cookie\" : \"CSRF token\"}. This usually means the host is not a ` +\n `UniFi OS console — check UNIFI_PROTECT_HOST points at the console itself, not at a ` +\n `reverse proxy in front of it.`,\n );\n }\n\n const next: PersistedSession = {\n cookie: bareCookie(setCookie),\n csrfToken,\n baseUrl,\n username,\n savedAt: new Date(opts.now?.() ?? Date.now()).toISOString(),\n };\n await saveSession(config.sessionFile, next).catch((err: unknown) => {\n // A session that cannot be persisted is still usable in memory; failing\n // the login over it would be a worse trade.\n logger?.warn?.(`could not persist the session to ${config.sessionFile}: ${String(err)}`);\n });\n return next;\n };\n\n /**\n * Reuse a session from disk when it belongs to the console and account we are\n * configured for. A session for a different host or user is not merely\n * useless, it would authenticate as the wrong identity.\n */\n const restore = async (): Promise<PersistedSession | undefined> => {\n if (restored) return undefined;\n restored = true;\n const persisted = await loadSession(config.sessionFile);\n if (!persisted) return undefined;\n if (persisted.baseUrl !== config.baseUrl || persisted.username !== config.username) {\n logger?.debug?.(\"ignoring a persisted session issued for a different console or user\");\n return undefined;\n }\n logger?.debug?.(`restored a session from ${config.sessionFile}`);\n return persisted;\n };\n\n const ensure = async (totp?: string): Promise<PersistedSession> => {\n if (session) return session;\n\n // Try disk before the network, but only once per process: a restored\n // session that turns out to be expired comes back as a 401, which\n // invalidate() clears and the next call logs in properly.\n const fromDisk = await restore();\n if (fromDisk) {\n session = fromDisk;\n source = \"restored\";\n return session;\n }\n\n if (!inflight) {\n inflight = handshake(totp).finally(() => {\n inflight = undefined;\n });\n }\n session = await inflight;\n source = \"login\";\n return session;\n };\n\n return {\n async headers(): Promise<SessionHeaders> {\n const live = await ensure();\n return { cookie: live.cookie, \"x-csrf-token\": live.csrfToken };\n },\n\n invalidate(): void {\n session = undefined;\n // A restored-but-expired session must not be restored a second time, or\n // the 401 retry loop would keep replaying the same dead cookie until it\n // runs out of attempts and reports a credentials problem that isn't one.\n restored = true;\n },\n\n async login(totp?: string): Promise<SessionStatus> {\n session = undefined;\n restored = true;\n session = await handshake(totp);\n source = \"login\";\n return this.describe();\n },\n\n async logout(): Promise<void> {\n session = undefined;\n restored = true;\n source = \"none\";\n await clearSession(config.sessionFile);\n },\n\n describe(): SessionStatus {\n return {\n authenticated: session !== undefined,\n source,\n username: session?.username ?? config.username,\n savedAt: session?.savedAt,\n };\n },\n };\n};\n\n/** For tests: fixed headers, no network, no disk. */\nexport const staticSessionProvider = (\n headers: SessionHeaders = { cookie: \"TOKEN=test\", \"x-csrf-token\": \"csrf-test\" },\n): SessionProvider => ({\n headers: async () => headers,\n invalidate: () => {},\n login: async () => ({\n authenticated: true,\n source: \"login\",\n username: \"test\",\n savedAt: undefined,\n }),\n logout: async () => {},\n describe: () => ({ authenticated: true, source: \"login\", username: \"test\", savedAt: undefined }),\n});\n","// UniFi Protect's private API answers with the console's internal representation,\n// which is built for its own web app and not for a context window. One camera in\n// `bootstrap` runs to roughly 8-15 KB of JSON once `channels[]` (three encoder\n// profiles, ~20 fields each), `stats`, `featureFlags` (around 60 booleans),\n// `ispSettings`, `osdSettings`, `motionZones`, `smartDetectZones`, `privacyZones`\n// and `lenses` are counted. A ten-camera system is well over 100 KB from what\n// looks like a one-line \"list my cameras\".\n//\n// So list tools return an allowlist of the fields someone actually asks about,\n// and `get_*` tools return the full object — that is the point of a get.\n\ntype Rec = Record<string, unknown>;\n\nconst isRecord = (value: unknown): value is Rec =>\n typeof value === \"object\" && value !== null && !Array.isArray(value);\n\nconst str = (value: unknown): string | undefined =>\n typeof value === \"string\" && value.length > 0 ? value : undefined;\n\n/** Apply a summarizer across an array, passing non-arrays through untouched. */\nexport const summarizeEach = <T>(value: unknown, fn: (item: Rec) => T): unknown =>\n Array.isArray(value) ? value.filter(isRecord).map(fn) : value;\n\n/**\n * Protect timestamps are milliseconds since the Unix epoch. Rendering them as\n * ISO 8601 costs a few characters and saves the model from having to reason\n * about a bare 13-digit integer — which it does get wrong, usually by reading\n * it as seconds and landing in 1970.\n */\nexport const isoTime = (value: unknown): string | undefined => {\n if (typeof value !== \"number\" || !Number.isFinite(value) || value <= 0) return undefined;\n return new Date(value).toISOString();\n};\n\n/** An index from device id to display name, used to resolve event references. */\nexport type NameIndex = ReadonlyMap<string, string>;\n\nexport const buildNameIndex = (devices: unknown): NameIndex => {\n const index = new Map<string, string>();\n if (!Array.isArray(devices)) return index;\n for (const device of devices) {\n if (!isRecord(device)) continue;\n const id = str(device.id);\n const name = str(device.name);\n if (id && name) index.set(id, name);\n }\n return index;\n};\n\nexport const summarizeCamera = (camera: Rec): Rec => {\n const flags = isRecord(camera.featureFlags) ? camera.featureFlags : {};\n const recording = isRecord(camera.recordingSettings) ? camera.recordingSettings : {};\n const smart = isRecord(camera.smartDetectSettings) ? camera.smartDetectSettings : {};\n return {\n id: camera.id,\n name: camera.name,\n type: camera.type,\n mac: camera.mac,\n host: camera.host,\n isConnected: camera.isConnected,\n state: camera.state,\n isRecording: camera.isRecording,\n recordingMode: recording.mode,\n // What the camera can actually do, drawn out of ~60 feature flags. These\n // three are the ones that decide whether another tool call is even valid:\n // a PTZ tool against a fixed camera is a guaranteed error.\n hasPtz: flags.canOpticalZoom ?? flags.hasPtz ?? false,\n hasPackageCamera: flags.hasPackageCamera ?? false,\n hasSmartDetect: flags.hasSmartDetect ?? false,\n smartDetectTypes: smart.objectTypes ?? [],\n firmwareVersion: camera.firmwareVersion,\n isUpdating: camera.isUpdating,\n lastSeen: isoTime(camera.lastSeen),\n };\n};\n\n/**\n * Events reference their camera by id. Resolving that to a name server-side is\n * the single most useful thing this layer does: it removes a join the model\n * would otherwise have to perform against a separate camera list, and get\n * silently wrong. The id is kept too, since the write and snapshot tools need it.\n */\nexport const summarizeEvent = (event: Rec, cameras?: NameIndex): Rec => {\n const cameraId = str(event.camera);\n const metadata = isRecord(event.metadata) ? event.metadata : undefined;\n const plate =\n metadata && isRecord(metadata.licensePlate) ? str(metadata.licensePlate.name) : undefined;\n\n return {\n id: event.id,\n type: event.type,\n start: isoTime(event.start),\n end: isoTime(event.end),\n ...(cameraId ? { cameraId } : {}),\n ...(cameraId && cameras?.get(cameraId) ? { camera: cameras.get(cameraId) } : {}),\n ...(Array.isArray(event.smartDetectTypes) && event.smartDetectTypes.length > 0\n ? { smartDetectTypes: event.smartDetectTypes }\n : {}),\n // Only present on smart detections, and the reason someone searched.\n ...(plate ? { licensePlate: plate } : {}),\n ...(typeof event.score === \"number\" ? { score: event.score } : {}),\n // Whether a still frame exists, so the model knows\n // unifi_protect_get_event_thumbnail is worth calling. Deliberately a flag\n // and not an id: the console's own `thumbnail` field is `e-<eventId>`,\n // which belongs to a DIFFERENT endpoint (`thumbnails/<id>`) than the one\n // this server uses (`events/<eventId>/thumbnail`). Handing that value out\n // invites passing it to the wrong place, which 404s — as it did here.\n ...(str(event.thumbnail) ? { hasThumbnail: true } : {}),\n };\n};\n\nexport const summarizeLight = (light: Rec): Rec => {\n const settings = isRecord(light.lightDeviceSettings) ? light.lightDeviceSettings : {};\n return {\n id: light.id,\n name: light.name,\n isConnected: light.isConnected,\n isLightOn: light.isLightOn,\n isPirMotionDetected: light.isPirMotionDetected,\n ledLevel: settings.ledLevel,\n isIndicatorEnabled: settings.isIndicatorEnabled,\n firmwareVersion: light.firmwareVersion,\n lastSeen: isoTime(light.lastSeen),\n };\n};\n\nexport const summarizeSensor = (sensor: Rec): Rec => {\n const stats = isRecord(sensor.stats) ? sensor.stats : {};\n const reading = (key: string): unknown => {\n const block = stats[key];\n return isRecord(block) ? block.value : undefined;\n };\n return {\n id: sensor.id,\n name: sensor.name,\n type: sensor.type,\n isConnected: sensor.isConnected,\n mountType: sensor.mountType,\n batteryStatus: isRecord(sensor.batteryStatus) ? sensor.batteryStatus.percentage : undefined,\n isOpened: sensor.isOpened,\n isMotionDetected: sensor.isMotionDetected,\n // The readings are the point of a sensor, but they sit one level down\n // inside `stats.<metric>.value` alongside per-metric history arrays that\n // are far larger than the reading itself.\n temperature: reading(\"temperature\"),\n humidity: reading(\"humidity\"),\n light: reading(\"light\"),\n firmwareVersion: sensor.firmwareVersion,\n lastSeen: isoTime(sensor.lastSeen),\n };\n};\n\nexport const summarizeViewer = (viewer: Rec): Rec => ({\n id: viewer.id,\n name: viewer.name,\n isConnected: viewer.isConnected,\n liveview: viewer.liveview,\n streamLimit: viewer.streamLimit,\n firmwareVersion: viewer.firmwareVersion,\n lastSeen: isoTime(viewer.lastSeen),\n});\n\nexport const summarizeChime = (chime: Rec): Rec => ({\n id: chime.id,\n name: chime.name,\n isConnected: chime.isConnected,\n volume: chime.volume,\n cameraIds: chime.cameraIds,\n firmwareVersion: chime.firmwareVersion,\n lastSeen: isoTime(chime.lastSeen),\n});\n\nexport const summarizeLiveview = (liveview: Rec): Rec => ({\n id: liveview.id,\n name: liveview.name,\n isDefault: liveview.isDefault,\n owner: liveview.owner,\n // `slots` holds the full camera layout; the count is what someone asks about.\n slotCount: Array.isArray(liveview.slots) ? liveview.slots.length : undefined,\n});\n\nexport const summarizeUser = (user: Rec): Rec => ({\n id: user.id,\n name: user.name,\n firstName: user.firstName,\n lastName: user.lastName,\n email: user.email,\n role: user.role,\n isOwner: user.isOwner,\n lastLoginTime: isoTime(user.lastLoginTime),\n});\n\nconst num = (value: unknown): number | undefined =>\n typeof value === \"number\" && Number.isFinite(value) ? value : undefined;\n\nconst rec = (value: unknown): Rec | undefined => (isRecord(value) ? value : undefined);\n\n/**\n * Storage moved between Protect releases, so both layouts are read.\n *\n * Protect 6.x exposed `nvr.storageInfo` with `totalSize` / `totalSpaceUsed` and a\n * `devices[]` array carrying full SMART tables. By 7.2 that key is gone entirely:\n * the same numbers live under `nvr.systemInfo.storage` as `size` / `used` /\n * `available`, the per-disk detail moved to `systemInfo.ustorage.disks`, and a\n * separate `nvr.storageStats` carries utilization. Reading only one shape gives\n * an empty `storage: {}` on the other, which is how this was found.\n */\nconst summarizeStorage = (nvr: Rec): Rec => {\n const system = rec(nvr.systemInfo);\n const modern = rec(system?.storage);\n const legacy = rec(nvr.storageInfo);\n const stats = rec(nvr.storageStats);\n const ustorage = rec(system?.ustorage);\n const disks = Array.isArray(ustorage?.disks) ? ustorage.disks.filter(isRecord) : undefined;\n\n const total = num(modern?.size) ?? num(legacy?.totalSize);\n const used = num(modern?.used) ?? num(legacy?.totalSpaceUsed);\n const available =\n num(modern?.available) ??\n (total !== undefined && used !== undefined ? total - used : undefined);\n\n return {\n ...(total !== undefined ? { totalBytes: total } : {}),\n ...(used !== undefined ? { usedBytes: used } : {}),\n ...(available !== undefined ? { availableBytes: available } : {}),\n ...(num(stats?.utilization) !== undefined\n ? { utilizationPercent: Math.round((stats!.utilization as number) * 10) / 10 }\n : {}),\n ...(str(modern?.type) ? { type: modern!.type } : {}),\n ...(str(ustorage?.raid) ? { raid: ustorage!.raid } : {}),\n ...((str(modern?.capability) ?? str(nvr.hardDriveState))\n ? { health: modern?.capability ?? nvr.hardDriveState }\n : {}),\n ...(disks\n ? {\n disks: disks.length,\n // Naming the unhealthy count rather than dumping four SMART tables:\n // the tables are enormous and the only question anyone asks of them\n // is whether a disk is failing.\n disksUnhealthy: disks.filter((d) => d.healthy !== undefined && d.healthy !== \"good\")\n .length,\n }\n : {}),\n // An NVR at 99% with isRecycling: true is working AS DESIGNED — it overwrites\n // the oldest footage continuously. Saying so inline stops a full-looking disk\n // being reported as a problem, which is the obvious wrong reading.\n ...(nvr.isRecycling === true\n ? {\n isRecycling: true,\n note: \"Near-full with recycling on is normal: the console overwrites the oldest footage continuously rather than stopping.\",\n }\n : {}),\n };\n};\n\nexport const summarizeNvr = (nvr: Rec): Rec => {\n const system = rec(nvr.systemInfo);\n const memory = rec(system?.memory);\n const cpu = rec(system?.cpu);\n const upSince = num(nvr.upSince);\n return {\n id: nvr.id,\n name: nvr.name,\n host: nvr.host,\n type: nvr.type,\n // Pin this in the README: the private API is undocumented and moves between\n // releases, so the running version is the first thing to check when a\n // previously working tool starts returning 404.\n version: nvr.version,\n firmwareVersion: nvr.firmwareVersion,\n ...(str(nvr.marketName) ? { model: nvr.marketName } : {}),\n timezone: nvr.timezone,\n isRecordingDisabled: nvr.isRecordingDisabled,\n storage: summarizeStorage(nvr),\n ...(memory ? { memory: { available: memory.available, total: memory.total } } : {}),\n ...(cpu ? { cpu: { load: cpu.averageLoad, temperature: cpu.temperature } } : {}),\n // 7.x reports when the console came up rather than how long it has been up.\n ...(upSince !== undefined\n ? {\n upSince: new Date(upSince).toISOString(),\n uptimeSeconds: Math.round((Date.now() - upSince) / 1000),\n }\n : num(nvr.uptime) !== undefined\n ? { uptimeSeconds: Math.round((nvr.uptime as number) / 1000) }\n : {}),\n lastSeen: isoTime(nvr.lastSeen),\n };\n};\n\n/**\n * The bootstrap document is the entire console state and must never be returned\n * raw — it is the single largest context bomb this API offers. This reduces it\n * to the NVR summary plus per-type counts, which is what \"tell me about my\n * system\" actually wants.\n */\nexport const summarizeBootstrap = (bootstrap: Rec): Rec => {\n const count = (key: string): number =>\n Array.isArray(bootstrap[key]) ? (bootstrap[key] as unknown[]).length : 0;\n\n return {\n nvr: isRecord(bootstrap.nvr) ? summarizeNvr(bootstrap.nvr) : undefined,\n devices: {\n cameras: count(\"cameras\"),\n lights: count(\"lights\"),\n sensors: count(\"sensors\"),\n viewers: count(\"viewers\"),\n chimes: count(\"chimes\"),\n bridges: count(\"bridges\"),\n doorlocks: count(\"doorlocks\"),\n },\n liveviews: count(\"liveviews\"),\n users: count(\"users\"),\n lastUpdateId: bootstrap.lastUpdateId,\n };\n};\n","import type { ProtectClient } from \"./protect.js\";\nimport { buildNameIndex, type NameIndex } from \"./shape.js\";\n\nexport type DeviceCacheOptions = {\n client: ProtectClient;\n ttlSeconds: number;\n now?: () => number;\n};\n\n/**\n * A short-lived camera id→name index, used to resolve the `camera` reference on\n * every event into something readable.\n *\n * It is cached because event search is the hot path and every result set needs\n * the same index: fetching the camera list once per search rather than once per\n * event is the difference between one extra request and none. The TTL is short\n * because a renamed or newly adopted camera should appear without a restart,\n * and a stale name is only ever cosmetic — the id travels alongside it.\n */\nexport type DeviceCache = {\n cameras(): Promise<NameIndex>;\n invalidate(): void;\n};\n\nexport const createDeviceCache = (opts: DeviceCacheOptions): DeviceCache => {\n const now = opts.now ?? Date.now;\n const ttlMs = opts.ttlSeconds * 1000;\n let cached: { index: NameIndex; expiresAt: number } | undefined;\n // Concurrent event searches would otherwise each fetch the camera list.\n let inflight: Promise<NameIndex> | undefined;\n\n const fetchIndex = async (): Promise<NameIndex> => {\n const cameras = await opts.client.get<unknown>(\"cameras\");\n const index = buildNameIndex(cameras);\n cached = { index, expiresAt: now() + ttlMs };\n return index;\n };\n\n return {\n async cameras(): Promise<NameIndex> {\n if (cached && now() < cached.expiresAt) return cached.index;\n if (!inflight) {\n inflight = fetchIndex().finally(() => {\n inflight = undefined;\n });\n }\n try {\n return await inflight;\n } catch {\n // Name resolution is a nicety, not the answer. If the camera list is\n // unreachable, events still come back with their ids rather than the\n // whole search failing over a cosmetic lookup.\n return new Map<string, string>();\n }\n },\n invalidate(): void {\n cached = undefined;\n },\n };\n};\n","import { PRIVATE_API_PATH } from \"../config.js\";\nimport type { Logger, SessionProvider } from \"./auth.js\";\nimport { ProtectApiError } from \"./errors.js\";\n\n/** Array values become repeated params, which is how Protect expects `types`. */\nexport type Query = Record<string, string | number | boolean | string[] | undefined>;\n\nconst sleep = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms));\n\nexport const backoffMs = (attempt: number): number => Math.min(1000 * 2 ** attempt, 8000);\n\nexport const retryAfterMs = (res: Response): number | undefined => {\n const value = res.headers.get(\"Retry-After\");\n if (value === null) return undefined;\n const seconds = Number(value);\n return Number.isFinite(seconds) ? Math.max(seconds, 0) * 1000 : undefined;\n};\n\nconst safeJsonParse = (text: string): unknown => {\n try {\n return JSON.parse(text);\n } catch {\n return text;\n }\n};\n\nexport const buildQuery = (query: Query | undefined): string => {\n if (!query) return \"\";\n const params = new URLSearchParams();\n for (const [key, value] of Object.entries(query)) {\n if (value === undefined) continue;\n // Protect reads `types` and `smartDetectTypes` as REPEATED parameters\n // (?types=motion&types=ring), not as a comma-joined string. A joined value\n // matches no event type at all and returns an empty list rather than an error.\n if (Array.isArray(value)) {\n for (const item of value) params.append(key, item);\n } else {\n params.append(key, String(value));\n }\n }\n const qs = params.toString();\n return qs ? `?${qs}` : \"\";\n};\n\nexport type ProtectClientOptions = {\n baseUrl: string;\n session: SessionProvider;\n maxRetries: number;\n userAgent: string;\n maxDownloadBytes: number;\n fetch?: typeof fetch;\n logger?: Logger;\n};\n\nexport type BinaryResult = {\n bytes: Uint8Array;\n contentType: string;\n};\n\n/**\n * The private Protect API client. Every path passed in is relative to\n * `/proxy/protect/api` — callers write `cameras/abc123`, not the full path.\n */\nexport class ProtectClient {\n readonly baseUrl: string;\n private readonly session: SessionProvider;\n private readonly maxRetries: number;\n private readonly userAgent: string;\n private readonly maxDownloadBytes: number;\n private readonly fetchImpl: typeof fetch;\n private readonly logger: Logger | undefined;\n\n constructor(opts: ProtectClientOptions) {\n this.baseUrl = opts.baseUrl.replace(/\\/+$/, \"\");\n this.session = opts.session;\n this.maxRetries = opts.maxRetries;\n this.userAgent = opts.userAgent;\n this.maxDownloadBytes = opts.maxDownloadBytes;\n this.fetchImpl = opts.fetch ?? fetch;\n this.logger = opts.logger;\n }\n\n /** Absolute URL for a path relative to the private API root. */\n url(path: string, query?: Query): string {\n const clean = path.replace(/^\\/+/, \"\");\n return `${this.baseUrl}${PRIVATE_API_PATH}/${clean}${buildQuery(query)}`;\n }\n\n /**\n * Perform one authenticated request, retrying on 401 (re-login), 429 and 5xx.\n * Returns the raw Response so callers can decide between JSON and bytes.\n */\n private async send(\n method: string,\n url: string,\n opts: { body?: unknown; accept?: string } = {},\n ): Promise<Response> {\n const hasBody = opts.body !== undefined;\n const bodyText = hasBody ? JSON.stringify(opts.body) : undefined;\n let attempt = 0;\n\n for (;;) {\n const auth = await this.session.headers();\n this.logger?.debug?.(`${method} ${url} (attempt ${attempt + 1})`);\n const res = await this.fetchImpl(url, {\n method,\n headers: {\n ...auth,\n Accept: opts.accept ?? \"application/json\",\n \"User-Agent\": this.userAgent,\n ...(hasBody ? { \"Content-Type\": \"application/json\" } : {}),\n },\n ...(bodyText !== undefined ? { body: bodyText } : {}),\n // Never follow a redirect: UniFi OS bounces an expired session to a\n // login page, and following it would turn a 401 we can recover from\n // into a 200 carrying HTML that parses as neither JSON nor an image.\n redirect: \"manual\",\n });\n\n // 401 means the cookie died mid-session. Drop it and re-run the handshake.\n if (res.status === 401 && attempt < this.maxRetries) {\n this.logger?.warn?.(\"HTTP 401 — re-authenticating and retrying\");\n this.session.invalidate();\n attempt += 1;\n continue;\n }\n\n if ((res.status === 429 || res.status >= 500) && attempt < this.maxRetries) {\n const delay = retryAfterMs(res) ?? backoffMs(attempt);\n this.logger?.warn?.(`HTTP ${res.status} — retrying in ${delay}ms`);\n await sleep(delay);\n attempt += 1;\n continue;\n }\n\n return res;\n }\n }\n\n /** A JSON request against the private API. */\n async request<T = unknown>(\n method: string,\n path: string,\n opts: { query?: Query; body?: unknown } = {},\n ): Promise<T> {\n const url = this.url(path, opts.query);\n const res = await this.send(method, url, opts.body !== undefined ? { body: opts.body } : {});\n const text = await res.text();\n\n if (!res.ok) {\n throw new ProtectApiError(this.errorMessage(res, method, path, text), {\n status: res.status,\n path,\n errors: safeJsonParse(text),\n });\n }\n\n // Many PATCH endpoints answer 200 with an empty body rather than 204.\n if (res.status === 204 || text.trim() === \"\") return null as T;\n return safeJsonParse(text) as T;\n }\n\n get<T = unknown>(path: string, query?: Query): Promise<T> {\n return this.request<T>(\"GET\", path, query ? { query } : {});\n }\n\n post<T = unknown>(path: string, body?: unknown, query?: Query): Promise<T> {\n return this.request<T>(\"POST\", path, {\n ...(body !== undefined ? { body } : {}),\n ...(query ? { query } : {}),\n });\n }\n\n patch<T = unknown>(path: string, body?: unknown, query?: Query): Promise<T> {\n return this.request<T>(\"PATCH\", path, {\n ...(body !== undefined ? { body } : {}),\n ...(query ? { query } : {}),\n });\n }\n\n del<T = unknown>(path: string, query?: Query): Promise<T> {\n return this.request<T>(\"DELETE\", path, query ? { query } : {});\n }\n\n /**\n * Fetch a binary asset — a snapshot JPEG, an event thumbnail, an exported\n * MP4. Size is checked against `maxDownloadBytes` from Content-Length where\n * the console supplies one, and again after reading where it does not, so an\n * unexpectedly huge export fails with a clear message rather than by\n * exhausting the heap.\n */\n async requestBytes(\n path: string,\n opts: { query?: Query; accept?: string } = {},\n ): Promise<BinaryResult> {\n const url = this.url(path, opts.query);\n const res = await this.send(\"GET\", url, {\n accept: opts.accept ?? \"application/octet-stream\",\n });\n\n if (!res.ok) {\n const text = await res.text().catch(() => \"\");\n throw new ProtectApiError(this.errorMessage(res, \"GET\", path, text), {\n status: res.status,\n path,\n errors: safeJsonParse(text),\n });\n }\n\n const declared = Number(res.headers.get(\"Content-Length\"));\n if (Number.isFinite(declared) && declared > this.maxDownloadBytes) {\n throw new ProtectApiError(\n `${path} would return ${declared} bytes, over the ${this.maxDownloadBytes}-byte limit. ` +\n `Narrow the time range, or raise UNIFI_PROTECT_MAX_DOWNLOAD_BYTES.`,\n { status: res.status, path },\n );\n }\n\n const bytes = new Uint8Array(await res.arrayBuffer());\n if (bytes.byteLength > this.maxDownloadBytes) {\n throw new ProtectApiError(\n `${path} returned ${bytes.byteLength} bytes, over the ${this.maxDownloadBytes}-byte ` +\n `limit. Narrow the time range, or raise UNIFI_PROTECT_MAX_DOWNLOAD_BYTES.`,\n { status: res.status, path },\n );\n }\n\n return {\n bytes,\n contentType: res.headers.get(\"Content-Type\") ?? \"application/octet-stream\",\n };\n }\n\n /** Status-aware prose, because this is the text someone acts on. */\n private errorMessage(res: Response, method: string, path: string, body: string): string {\n const detail = safeJsonParse(body);\n const upstream =\n typeof detail === \"object\" && detail !== null && \"error\" in detail\n ? String((detail as { error: unknown }).error)\n : typeof detail === \"string\" && detail.length > 0 && detail.length < 200\n ? detail\n : undefined;\n\n const base =\n `Protect ${method} ${path} failed: HTTP ${res.status} ${res.statusText}`.trim() +\n (upstream ? ` — ${upstream}` : \"\");\n\n if (res.status === 401 || res.status === 403) {\n return (\n `${base}. The account may lack Protect permissions for this device, or may be ` +\n `view-only while this call needs write access.`\n );\n }\n if (res.status === 404) {\n return (\n `${base}. Check the id came from a list tool on THIS console. Note also that the ` +\n `private Protect API is undocumented and Ubiquiti moves endpoints between releases — ` +\n `unifi_protect_get_system_info reports the version actually running.`\n );\n }\n // A redirect reaching here means `redirect: \"manual\"` caught a bounce to\n // the login page, which is what an expired or rejected session looks like.\n if (res.status >= 300 && res.status < 400) {\n return `${base}. The console redirected the request, which usually means the session was rejected.`;\n }\n return base;\n }\n}\n","import { Agent, fetch as undiciFetch } from \"undici\";\n\nimport type { Logger } from \"./auth.js\";\n\n/**\n * The one place TLS is decided.\n *\n * UniFi consoles ship a self-signed certificate, and Node's native `fetch`\n * ignores a `node:https` Agent — the only scoped way to relax verification is an\n * undici dispatcher. That is why this server carries a third runtime dependency:\n * the alternative, NODE_TLS_REJECT_UNAUTHORIZED=0, is process-global and\n * disables verification for every other request the process ever makes.\n *\n * This replaced exactly that process-wide switch, which had been adopted on the\n * belief that undici could not be imported and a dispatcher therefore could not\n * be scoped. It can — see below.\n *\n * **The `fetch` here must be undici's own, not the global one.** Node's built-in\n * fetch is a *bundled copy* of undici, and it rejects a dispatcher constructed\n * from the separately-installed package with a bare `UND_ERR_INVALID_ARG`,\n * surfacing as an unexplained \"fetch failed\". Passing undici's own fetch keeps\n * both sides on one class. Verification is on by default, so the platform fetch\n * remains the common path.\n *\n * Note what a pinned certificate can and cannot fix. These consoles present\n * `CN=unifi.local` with SANs for `unifi.local`, `localhost` and `127.0.0.1` —\n * and **no IP SAN**. Reached by IP, verification fails on the host name however\n * the certificate is trusted. Measured with `curl --cacert`: by host name\n * `ssl_verify=0`, by IP `ssl_verify=1`. So verifying requires BOTH\n * NODE_EXTRA_CA_CERTS and a host name that resolves to the console.\n */\nexport const createHttpFetch = (opts: { insecureTls: boolean; logger?: Logger }): typeof fetch => {\n if (!opts.insecureTls) return fetch;\n\n opts.logger?.warn?.(\n \"TLS certificate verification is DISABLED for this server's requests \" +\n \"(UNIFI_PROTECT_VERIFY_TLS=false). To verify instead, address the console by a host name \" +\n \"that resolves to it — its certificate has no IP SAN — and point NODE_EXTRA_CA_CERTS at \" +\n \"the console's certificate.\",\n );\n\n const dispatcher = new Agent({ connect: { rejectUnauthorized: false } });\n // undici's fetch/Response are structurally the WHATWG ones the rest of the\n // client uses; the cast bridges the two nominal type declarations and is\n // confined to this file.\n return ((input: Parameters<typeof fetch>[0], init?: Parameters<typeof fetch>[1]) =>\n undiciFetch(input as string, { ...init, dispatcher } as never)) as unknown as typeof fetch;\n};\n","import { z } from \"zod\";\n\nimport {\n NotConfiguredError,\n ProtectApiError,\n ProtectAuthError,\n WritesDisabledError,\n} from \"../client/errors.js\";\n\nexport type ToolResult = {\n content: ({ type: \"text\"; text: string } | { type: \"image\"; data: string; mimeType: string })[];\n isError?: boolean;\n};\n\nexport const ok = (data: unknown): ToolResult => ({\n content: [{ type: \"text\", text: JSON.stringify(data ?? { ok: true }, null, 2) }],\n});\n\nexport const fail = (message: string, extra?: unknown): ToolResult => ({\n content: [\n {\n type: \"text\",\n text: JSON.stringify({ error: message, ...(extra ? { details: extra } : {}) }, null, 2),\n },\n ],\n isError: true,\n});\n\n/** Render a thrown value as a tool error, preserving upstream detail. */\nexport const toFailure = (err: unknown): ToolResult => {\n if (err instanceof ProtectApiError) {\n return fail(err.message, {\n status: err.status,\n ...(err.path ? { path: err.path } : {}),\n errors: err.errors,\n });\n }\n if (err instanceof ProtectAuthError) {\n return fail(err.message, err.needsTwoFactor ? { needsTwoFactor: true } : undefined);\n }\n if (err instanceof WritesDisabledError || err instanceof NotConfiguredError) {\n return fail(err.message);\n }\n if (err instanceof Error) return fail(err.message);\n return fail(\"Unknown error\", err);\n};\n\n/** Run a tool body, JSON-formatting the result and turning errors into a tool error. */\nexport const wrap = async <T>(fn: () => Promise<T>): Promise<ToolResult> => {\n try {\n return ok(await fn());\n } catch (err) {\n return toFailure(err);\n }\n};\n\n/** Like `wrap`, but the body chooses its own result shape (e.g. an image block). */\nexport const wrapResult = async (fn: () => Promise<ToolResult>): Promise<ToolResult> => {\n try {\n return await fn();\n } catch (err) {\n return toFailure(err);\n }\n};\n\n/** Drop undefined values so we never send `{\"name\": undefined}` to the console. */\nexport const compact = <T extends Record<string, unknown>>(obj: T): Partial<T> =>\n Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined)) as Partial<T>;\n\n/** `{}` is not a no-op PATCH body — send undefined instead. */\nexport const compactOrUndefined = <T extends Record<string, unknown>>(\n obj: T,\n): Partial<T> | undefined => {\n const out = compact(obj);\n return Object.keys(out).length > 0 ? out : undefined;\n};\n\nconst RELATIVE =\n /^(\\d+(?:\\.\\d+)?)\\s*(s|sec|secs|seconds?|m|min|mins|minutes?|h|hr|hrs|hours?|d|days?|w|weeks?)(?:\\s+ago)?$/i;\n\nconst UNIT_MS: Record<string, number> = {\n s: 1000,\n m: 60_000,\n h: 3_600_000,\n d: 86_400_000,\n w: 604_800_000,\n};\n\n/**\n * Convert a time expression to milliseconds since the Unix epoch.\n *\n * This exists because the console's `/events` endpoint takes JavaScript\n * millisecond timestamps — not ISO 8601, and NOT Unix seconds. A seconds value\n * is not rejected: it is interpreted as a moment in January 1970, so the query\n * succeeds and returns an empty list. That reads as \"nothing happened last\n * night\", which is the most expensive possible failure for this server.\n *\n * Accepts ISO 8601, a relative expression like \"2h ago\" or \"30m\", the literal\n * \"now\", or a raw millisecond number.\n */\nexport const toEpochMs = (value: string | number, now: number = Date.now()): number => {\n if (typeof value === \"number\") return Math.round(value);\n\n const text = value.trim();\n if (text === \"\" || text.toLowerCase() === \"now\") return now;\n\n const relative = RELATIVE.exec(text);\n if (relative) {\n const amount = Number(relative[1]);\n const unit = relative[2]!.toLowerCase()[0]!;\n const scale = UNIT_MS[unit];\n if (scale === undefined) {\n throw new Error(`Unrecognised time unit in \"${value}\".`);\n }\n return Math.round(now - amount * scale);\n }\n\n // A bare number in a string is milliseconds, but a 10-digit one is almost\n // certainly seconds that someone converted by hand. Catch it rather than\n // silently querying 1970.\n if (/^\\d+$/.test(text)) {\n const n = Number(text);\n if (text.length <= 10) {\n throw new Error(\n `\"${value}\" looks like Unix SECONDS. This API takes milliseconds — pass ${n * 1000}, ` +\n `or better, an ISO 8601 timestamp or a relative expression like \"2h ago\".`,\n );\n }\n return n;\n }\n\n const parsed = Date.parse(text);\n if (Number.isNaN(parsed)) {\n throw new Error(\n `Could not read \"${value}\" as a time. Use ISO 8601 (2026-08-29T22:00:00Z), a relative ` +\n `expression (\"2h ago\", \"30m\", \"7d\"), or \"now\".`,\n );\n }\n return parsed;\n};\n\nexport const confirmArg = z\n .literal(true)\n .describe(\"Must be true. Explicit acknowledgement that this changes the console's state.\");\n\nexport const cameraIdArg = z\n .string()\n .min(1)\n .describe(\n \"Camera id — the `id` from unifi_protect_list_cameras, a 24-character hex string. Not the \" +\n \"camera's name and not its MAC address.\",\n );\n\n/** The shared prose for every time argument, so the trap is stated everywhere it applies. */\nconst timeHelp = (role: string): string =>\n `${role} Accepts ISO 8601 (\"2026-08-29T22:00:00Z\"), a relative expression (\"2h ago\", ` +\n `\"30m\", \"7d\"), or \"now\". Converted to the millisecond epoch the console requires — do ` +\n `not pass Unix seconds, which would silently query 1970 and return nothing.`;\n\nexport const timeArg = (role: string) => z.string().optional().describe(timeHelp(role));\n\nexport const requiredTimeArg = (role: string) => z.string().min(1).describe(timeHelp(role));\n\nexport const limitArg = z\n .number()\n .int()\n .min(1)\n .max(500)\n .default(50)\n .describe(\n \"Maximum number of items to return (1-500). Defaults to 50. A busy system logs thousands \" +\n \"of motion events a day, so raise this deliberately.\",\n );\n","import { mkdir, writeFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\n\nimport type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { z } from \"zod\";\n\nimport type { ProtectClient } from \"../client/protect.js\";\nimport { summarizeCamera, summarizeEach } from \"../client/shape.js\";\nimport type { ToolContext } from \"./index.js\";\nimport { cameraIdArg, compactOrUndefined, confirmArg, ok, wrap, wrapResult } from \"./util.js\";\n\ntype Rec = Record<string, unknown>;\n\nconst RECORDING_MODES = [\"always\", \"never\", \"detections\", \"schedule\"] as const;\n\n/** Filesystem-safe stem for a snapshot file, derived from the camera name. */\nconst slug = (value: string): string =>\n value\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-|-$/g, \"\") || \"camera\";\n\nexport const registerCameraTools = (\n server: McpServer,\n client: ProtectClient,\n ctx: ToolContext,\n): void => {\n server.registerTool(\n \"unifi_protect_list_cameras\",\n {\n description:\n \"List every camera on the console with its id, name, connection state, recording mode, \" +\n \"firmware and what it can do (PTZ, package camera, smart detection, and which object \" +\n \"types it detects). Returns a summary rather than the console's full camera record, \" +\n \"which runs to thousands of fields across encoder profiles, zones and feature flags — \" +\n \"use unifi_protect_get_camera when you need all of it for one camera.\",\n inputSchema: {},\n annotations: { readOnlyHint: true },\n },\n async () => wrap(async () => summarizeEach(await client.get(\"cameras\"), summarizeCamera)),\n );\n\n server.registerTool(\n \"unifi_protect_get_camera\",\n {\n description:\n \"Get one camera's complete record — every setting the console holds, including encoder \" +\n \"channels, motion and smart-detection zones, privacy masks, OSD and LED settings, ISP \" +\n \"tuning and live statistics. This is large (roughly 8-15 KB of JSON). Prefer \" +\n \"unifi_protect_list_cameras unless you specifically need a field it does not carry.\",\n inputSchema: { cameraId: cameraIdArg },\n annotations: { readOnlyHint: true },\n },\n async ({ cameraId }) => wrap(() => client.get(`cameras/${encodeURIComponent(cameraId)}`)),\n );\n\n server.registerTool(\n \"unifi_protect_get_camera_snapshot\",\n {\n description:\n \"Capture a still frame from a camera as it looks right now. Writes the JPEG to disk and \" +\n 'returns its path, size and content type by default. Set output=\"image\" to get the ' +\n \"frame inline instead so a vision model can actually look at it — that costs roughly \" +\n \"300,000 to 700,000 characters of context per call, so choose it deliberately rather \" +\n \"than by default. A fresh capture is forced; without that the console can hand back a \" +\n \"cached frame that is minutes old.\",\n inputSchema: {\n cameraId: cameraIdArg,\n output: z\n .enum([\"file\", \"image\"])\n .default(\"file\")\n .describe(\n 'Where the frame goes. \"file\" writes it to disk and returns the path — cheap, and ' +\n 'you can read the file later if it turns out to matter. \"image\" returns it inline ' +\n \"for a model to look at, at a large cost in context.\",\n ),\n highQuality: z\n .boolean()\n .default(false)\n .describe(\n \"Request the camera's full resolution rather than a scaled frame. Larger and slower; \" +\n 'with output=\"image\" it multiplies an already expensive call.',\n ),\n savePath: z\n .string()\n .optional()\n .describe(\n \"Absolute path to write the JPEG to. Defaults to a timestamped file under \" +\n \"UNIFI_PROTECT_SNAPSHOT_DIR. Parent directories are created.\",\n ),\n },\n annotations: { readOnlyHint: true },\n },\n async ({ cameraId, output, highQuality, savePath }) =>\n wrapResult(async () => {\n const { bytes, contentType } = await client.requestBytes(\n `cameras/${encodeURIComponent(cameraId)}/snapshot`,\n {\n query: {\n // Without force=true the console may return the last frame it\n // happens to have cached, which can be minutes stale — and a\n // stale frame is indistinguishable from a current one.\n force: \"true\",\n ...(highQuality ? { highQuality: \"true\" } : {}),\n ts: Date.now(),\n },\n accept: \"image/jpeg\",\n },\n );\n\n if (output === \"image\") {\n return {\n content: [\n {\n type: \"image\" as const,\n data: Buffer.from(bytes).toString(\"base64\"),\n mimeType: contentType.startsWith(\"image/\") ? contentType : \"image/jpeg\",\n },\n ],\n };\n }\n\n const cameras = await ctx.devices.cameras();\n const name = cameras.get(cameraId) ?? cameraId;\n const stamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n const path = savePath ?? join(ctx.config.snapshotDir, `${slug(name)}-${stamp}.jpg`);\n await mkdir(join(path, \"..\"), { recursive: true });\n await writeFile(path, bytes);\n\n return ok({\n path,\n bytes: bytes.byteLength,\n contentType,\n camera: name,\n capturedAt: new Date().toISOString(),\n note: 'Call again with output=\"image\" if a model needs to see the frame itself.',\n });\n }),\n );\n\n server.registerTool(\n \"unifi_protect_list_ptz_presets\",\n {\n description:\n \"List a PTZ camera's saved preset positions, with the slot number each one lives at. \" +\n \"Only meaningful for cameras reporting hasPtz: true in unifi_protect_list_cameras. \" +\n \"There is no tool to MOVE a PTZ camera or run a patrol: those commands exist only on \" +\n \"Ubiquiti's official Integration API (a separate X-API-KEY auth this server does not \" +\n \"use), not on the private API this server wraps — presets are created and driven from \" +\n \"the Protect app itself.\",\n inputSchema: { cameraId: cameraIdArg },\n annotations: { readOnlyHint: true },\n },\n async ({ cameraId }) =>\n wrap(() => client.get(`cameras/${encodeURIComponent(cameraId)}/ptz/preset`)),\n );\n\n server.registerTool(\n \"unifi_protect_list_ptz_patrols\",\n {\n description:\n \"List a PTZ camera's saved patrol routes. See unifi_protect_list_ptz_presets for why \" +\n \"there is no tool to start or stop one.\",\n inputSchema: { cameraId: cameraIdArg },\n annotations: { readOnlyHint: true },\n },\n async ({ cameraId }) =>\n wrap(() => client.get(`cameras/${encodeURIComponent(cameraId)}/ptz/patrol`)),\n );\n\n if (!ctx.allowWrites) return;\n\n server.registerTool(\n \"unifi_protect_update_camera\",\n {\n description:\n \"Change a camera's settings in place. Only the fields you pass are sent, and the \" +\n \"console merges them — sibling settings inside the same block are preserved, so \" +\n \"setting osdDate alone does not clear osdName. Use \" +\n \"unifi_protect_set_camera_recording_mode for recording mode alone — it is the setting \" +\n \"people mean most often and it is easy to send wrongly here.\",\n inputSchema: {\n cameraId: cameraIdArg,\n name: z.string().min(1).optional().describe(\"Display name shown throughout Protect.\"),\n micVolume: z\n .number()\n .int()\n .min(0)\n .max(100)\n .optional()\n .describe(\"Microphone sensitivity, 0-100. 0 mutes the microphone.\"),\n isMicEnabled: z.boolean().optional().describe(\"Whether the microphone records at all.\"),\n // A camera's LED is a simple on/off (`ledSettings.isEnabled`). The\n // 0-6 `ledLevel` that looks like it belongs here is a FLOODLIGHT\n // field — see unifi_protect_update_light.\n statusLedEnabled: z\n .boolean()\n .optional()\n .describe(\"Whether the camera's status LED lights up at all.\"),\n osdName: z.boolean().optional().describe(\"Overlay the camera name on the video.\"),\n osdDate: z.boolean().optional().describe(\"Overlay the date and time on the video.\"),\n },\n annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true },\n },\n async ({ cameraId, name, micVolume, isMicEnabled, statusLedEnabled, osdName, osdDate }) =>\n wrap(async () => {\n const ledSettings = compactOrUndefined({ isEnabled: statusLedEnabled });\n const osdSettings = compactOrUndefined({ isNameEnabled: osdName, isDateEnabled: osdDate });\n const body = compactOrUndefined({\n name,\n micVolume,\n isMicEnabled,\n ledSettings,\n osdSettings,\n });\n if (!body) {\n throw new Error(\n \"Nothing to update — pass at least one of name, micVolume, isMicEnabled, \" +\n \"statusLedEnabled, osdName, osdDate.\",\n );\n }\n return summarizeCamera(\n await client.patch<Rec>(`cameras/${encodeURIComponent(cameraId)}`, body),\n );\n }),\n );\n\n server.registerTool(\n \"unifi_protect_set_camera_recording_mode\",\n {\n description:\n \"Set what a camera records. `never` stops recording entirely — the camera stays online \" +\n \"and streams live, but nothing is written, so there will be no footage to search later. \" +\n \"`detections` records only motion and smart detections; `always` records continuously; \" +\n \"`schedule` follows the schedule configured in Protect.\",\n inputSchema: {\n cameraId: cameraIdArg,\n mode: z\n .enum(RECORDING_MODES)\n .describe(\n \"Recording mode. `never` means no footage is kept from now on — this is the one \" +\n \"worth pausing over.\",\n ),\n },\n annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true },\n },\n async ({ cameraId, mode }) =>\n wrap(async () =>\n summarizeCamera(\n await client.patch<Rec>(`cameras/${encodeURIComponent(cameraId)}`, {\n recordingSettings: { mode },\n }),\n ),\n ),\n );\n\n server.registerTool(\n \"unifi_protect_reboot_camera\",\n {\n description:\n \"Reboot a camera. It stops recording and goes offline for roughly a minute, and any \" +\n \"footage during that window is lost. Useful for a camera that has stopped responding.\",\n inputSchema: { cameraId: cameraIdArg, confirm: confirmArg },\n annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false },\n },\n async ({ cameraId }) =>\n wrap(() => client.post(`cameras/${encodeURIComponent(cameraId)}/reboot`)),\n );\n};\n","import type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { z } from \"zod\";\n\nimport type { ProtectClient } from \"../client/protect.js\";\nimport {\n summarizeChime,\n summarizeEach,\n summarizeLight,\n summarizeSensor,\n summarizeViewer,\n} from \"../client/shape.js\";\nimport type { ToolContext } from \"./index.js\";\nimport { compactOrUndefined, wrap } from \"./util.js\";\n\ntype Rec = Record<string, unknown>;\n\nconst idArg = (kind: string, listTool: string) =>\n z.string().min(1).describe(`${kind} id — the \\`id\\` from ${listTool}.`);\n\nexport const registerDeviceTools = (\n server: McpServer,\n client: ProtectClient,\n ctx: ToolContext,\n): void => {\n server.registerTool(\n \"unifi_protect_list_lights\",\n {\n description:\n \"List UniFi Protect floodlights with their connection state, whether the light is \" +\n \"currently on, whether PIR motion is being detected, and brightness.\",\n inputSchema: {},\n annotations: { readOnlyHint: true },\n },\n async () => wrap(async () => summarizeEach(await client.get(\"lights\"), summarizeLight)),\n );\n\n server.registerTool(\n \"unifi_protect_list_sensors\",\n {\n description:\n \"List UniFi Protect sensors with their current readings — temperature, humidity, light \" +\n \"level — plus open/closed state, motion, and battery percentage. The readings are \" +\n \"lifted out of the console's per-metric history arrays, which are far larger than the \" +\n \"values themselves.\",\n inputSchema: {},\n annotations: { readOnlyHint: true },\n },\n async () => wrap(async () => summarizeEach(await client.get(\"sensors\"), summarizeSensor)),\n );\n\n server.registerTool(\n \"unifi_protect_list_viewers\",\n {\n description:\n \"List UniFi Protect Viewport devices and which live view each is currently displaying.\",\n inputSchema: {},\n annotations: { readOnlyHint: true },\n },\n async () => wrap(async () => summarizeEach(await client.get(\"viewers\"), summarizeViewer)),\n );\n\n server.registerTool(\n \"unifi_protect_list_chimes\",\n {\n description:\n \"List UniFi Protect chimes, their volume, and which doorbell cameras each is paired to.\",\n inputSchema: {},\n annotations: { readOnlyHint: true },\n },\n async () => wrap(async () => summarizeEach(await client.get(\"chimes\"), summarizeChime)),\n );\n\n if (!ctx.allowWrites) return;\n\n server.registerTool(\n \"unifi_protect_update_light\",\n {\n description:\n \"Change a floodlight's settings — brightness, whether the light is on, and the PIR \" +\n \"sensitivity that decides when it triggers. Only the fields you pass are sent, and the \" +\n \"console merges them, so the PIR duration and lux sensitivity you do not pass survive.\",\n inputSchema: {\n lightId: idArg(\"Light\", \"unifi_protect_list_lights\"),\n isLightOn: z.boolean().optional().describe(\"Turn the light on or off right now.\"),\n ledLevel: z\n .number()\n .int()\n .min(1)\n .max(6)\n .optional()\n .describe(\"Brightness, 1 (dimmest) to 6 (brightest).\"),\n pirSensitivity: z\n .number()\n .int()\n .min(0)\n .max(100)\n .optional()\n .describe(\"Motion sensitivity, 0-100. Higher triggers on smaller movement.\"),\n },\n annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true },\n },\n async ({ lightId, isLightOn, ledLevel, pirSensitivity }) =>\n wrap(async () => {\n const lightOnSettings = compactOrUndefined({ isLedForceOn: isLightOn });\n const lightDeviceSettings = compactOrUndefined({ ledLevel, pirSensitivity });\n const body = compactOrUndefined({ lightOnSettings, lightDeviceSettings });\n if (!body) {\n throw new Error(\n \"Nothing to update — pass at least one of isLightOn, ledLevel, pirSensitivity.\",\n );\n }\n return summarizeLight(\n await client.patch<Rec>(`lights/${encodeURIComponent(lightId)}`, body),\n );\n }),\n );\n\n server.registerTool(\n \"unifi_protect_update_sensor\",\n {\n description:\n \"Rename a sensor or change which of its capabilities are enabled. Only the fields you \" +\n \"pass are sent.\",\n inputSchema: {\n sensorId: idArg(\"Sensor\", \"unifi_protect_list_sensors\"),\n name: z.string().min(1).optional().describe(\"Display name for the sensor.\"),\n motionEnabled: z.boolean().optional().describe(\"Whether motion detection reports events.\"),\n temperatureEnabled: z.boolean().optional().describe(\"Whether temperature is reported.\"),\n humidityEnabled: z.boolean().optional().describe(\"Whether humidity is reported.\"),\n lightEnabled: z.boolean().optional().describe(\"Whether the light level is reported.\"),\n },\n annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true },\n },\n async ({ sensorId, name, motionEnabled, temperatureEnabled, humidityEnabled, lightEnabled }) =>\n wrap(async () => {\n const body = compactOrUndefined({\n name,\n motionSettings: compactOrUndefined({ isEnabled: motionEnabled }),\n temperatureSettings: compactOrUndefined({ isEnabled: temperatureEnabled }),\n humiditySettings: compactOrUndefined({ isEnabled: humidityEnabled }),\n lightSettings: compactOrUndefined({ isEnabled: lightEnabled }),\n });\n if (!body) {\n throw new Error(\n \"Nothing to update — pass at least one of name, motionEnabled, temperatureEnabled, \" +\n \"humidityEnabled, lightEnabled.\",\n );\n }\n return summarizeSensor(\n await client.patch<Rec>(`sensors/${encodeURIComponent(sensorId)}`, body),\n );\n }),\n );\n\n server.registerTool(\n \"unifi_protect_update_viewer\",\n {\n description:\n \"Put a saved live view on a Viewport screen, or rename the viewer. The liveview id \" +\n \"comes from unifi_protect_list_liveviews — this changes what is displayed on a physical \" +\n \"screen, so someone watching will see it switch.\",\n inputSchema: {\n viewerId: idArg(\"Viewer\", \"unifi_protect_list_viewers\"),\n liveview: z\n .string()\n .min(1)\n .optional()\n .describe(\"Live view id from unifi_protect_list_liveviews — the layout to display.\"),\n name: z.string().min(1).optional().describe(\"Display name for the viewer.\"),\n },\n annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true },\n },\n async ({ viewerId, liveview, name }) =>\n wrap(async () => {\n const body = compactOrUndefined({ liveview, name });\n if (!body) throw new Error(\"Nothing to update — pass liveview or name.\");\n return summarizeViewer(\n await client.patch<Rec>(`viewers/${encodeURIComponent(viewerId)}`, body),\n );\n }),\n );\n\n server.registerTool(\n \"unifi_protect_update_chime\",\n {\n description:\n \"Change a chime's volume or rename it. Volume 0 silences it, so a doorbell press will \" +\n \"make no sound.\",\n inputSchema: {\n chimeId: idArg(\"Chime\", \"unifi_protect_list_chimes\"),\n volume: z\n .number()\n .int()\n .min(0)\n .max(100)\n .optional()\n .describe(\"Volume, 0-100. 0 means the chime stays silent when the doorbell is pressed.\"),\n name: z.string().min(1).optional().describe(\"Display name for the chime.\"),\n },\n annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true },\n },\n async ({ chimeId, volume, name }) =>\n wrap(async () => {\n const body = compactOrUndefined({ volume, name });\n if (!body) throw new Error(\"Nothing to update — pass volume or name.\");\n return summarizeChime(\n await client.patch<Rec>(`chimes/${encodeURIComponent(chimeId)}`, body),\n );\n }),\n );\n};\n","import { mkdir, writeFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\n\nimport type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { z } from \"zod\";\n\nimport type { ProtectClient } from \"../client/protect.js\";\nimport { summarizeEvent } from \"../client/shape.js\";\nimport type { ToolContext } from \"./index.js\";\nimport {\n cameraIdArg,\n limitArg,\n ok,\n requiredTimeArg,\n timeArg,\n toEpochMs,\n wrap,\n wrapResult,\n} from \"./util.js\";\n\ntype Rec = Record<string, unknown>;\n\n/**\n * The event types worth searching. Protect emits far more — disk health,\n * firmware updates, adoption, connection churn — but they are noise in an\n * event search and swamp the interesting ones.\n */\nconst EVENT_TYPES = [\n \"motion\",\n \"smartDetectZone\",\n \"smartDetectLine\",\n \"smartAudioDetect\",\n \"ring\",\n \"doorAccess\",\n \"nfcCardScanned\",\n \"fingerprintIdentified\",\n \"disconnect\",\n \"cameraConnected\",\n \"cameraDisconnected\",\n \"recordingDeleted\",\n] as const;\n\n/**\n * The default set: what a person means by \"what happened\". Motion, anything the\n * camera classified, and doorbell rings — with the device-health chatter left out.\n */\nconst DEFAULT_EVENT_TYPES = [\n \"motion\",\n \"smartDetectZone\",\n \"smartDetectLine\",\n \"smartAudioDetect\",\n \"ring\",\n] as const;\n\nconst SMART_DETECT_TYPES = [\n \"person\",\n \"vehicle\",\n \"animal\",\n \"package\",\n \"licensePlate\",\n \"face\",\n \"alrmSmoke\",\n \"alrmCmonx\",\n \"alrmSiren\",\n \"alrmBabyCry\",\n \"alrmSpeak\",\n \"alrmBark\",\n \"alrmBurglar\",\n \"alrmCarHorn\",\n \"alrmGlassBreak\",\n] as const;\n\nexport const registerEventTools = (\n server: McpServer,\n client: ProtectClient,\n ctx: ToolContext,\n): void => {\n server.registerTool(\n \"unifi_protect_list_events\",\n {\n description:\n \"Search recorded events over any time range — motion, smart detections (person, \" +\n \"vehicle, animal, package, licence plate), doorbell rings, and camera connection \" +\n 'changes. This is the tool for questions like \"what happened at the front door last ' +\n \"night\\\". Each result carries its camera's NAME as well as its id, so no second lookup \" +\n \"is needed. Narrow with `types`, `smartDetectTypes` and `cameraId` wherever you can: a \" +\n \"busy system logs thousands of motion events a day, and an unfiltered query returns \" +\n \"the newest slice of that rather than the interesting part.\",\n inputSchema: {\n start: timeArg(\"Beginning of the search window.\"),\n end: timeArg(\"End of the search window.\"),\n types: z\n .array(z.enum(EVENT_TYPES))\n .optional()\n .describe(\n \"Event types to include. Defaults to motion, smart detections and rings. \" +\n \"`smartDetectZone` is the object-detection type — pair it with smartDetectTypes \" +\n \"to ask for people or vehicles specifically.\",\n ),\n smartDetectTypes: z\n .array(z.enum(SMART_DETECT_TYPES))\n .optional()\n .describe(\n 'What the camera classified, e.g. [\"person\"] or [\"vehicle\",\"licensePlate\"]. Only ' +\n \"meaningful for smartDetectZone / smartDetectLine events; the alrm* values are \" +\n \"audio detections. Cameras without smart detection never produce these.\",\n ),\n cameraId: cameraIdArg\n .optional()\n .describe(\n \"Restrict to one camera — the `id` from unifi_protect_list_cameras. Omit for all cameras.\",\n ),\n limit: limitArg,\n order: z\n .enum([\"newest\", \"oldest\"])\n .default(\"newest\")\n .describe(\"Which end of the window to return first.\"),\n },\n annotations: { readOnlyHint: true },\n },\n async ({ start, end, types, smartDetectTypes, cameraId, limit, order }) =>\n wrap(async () => {\n // Default to the last 24 hours rather than whatever the console decides,\n // which is undocumented and has changed between releases.\n const endMs = end === undefined ? Date.now() : toEpochMs(end);\n const startMs = start === undefined ? endMs - 86_400_000 : toEpochMs(start);\n\n if (startMs >= endMs) {\n throw new Error(\n `The window is empty: start (${new Date(startMs).toISOString()}) is not before end ` +\n `(${new Date(endMs).toISOString()}).`,\n );\n }\n\n // `types` is ALWAYS sent, even when the caller passed none. Omitting it\n // triggers a pagination bug in Protect where the console ignores the\n // window and returns the wrong slice — a silently wrong answer rather\n // than an error. Sending an explicit list is the documented workaround.\n const requestedTypes = types && types.length > 0 ? types : [...DEFAULT_EVENT_TYPES];\n\n const raw = await client.get<unknown>(\"events\", {\n start: startMs,\n end: endMs,\n limit,\n orderDirection: order === \"newest\" ? \"DESC\" : \"ASC\",\n types: [...requestedTypes],\n ...(smartDetectTypes && smartDetectTypes.length > 0\n ? { smartDetectTypes: [...smartDetectTypes] }\n : {}),\n // The console's own descriptions are long, redundant with the fields\n // already returned, and localised — pure token cost.\n withoutDescriptions: \"true\",\n });\n\n const cameras = await ctx.devices.cameras();\n const events = Array.isArray(raw) ? raw : [];\n const shaped = events\n .filter((e): e is Rec => typeof e === \"object\" && e !== null)\n // The API has no camera filter on this endpoint, so it is applied here.\n .filter((e) => cameraId === undefined || e.camera === cameraId)\n .map((e) => summarizeEvent(e, cameras));\n\n return {\n window: { start: new Date(startMs).toISOString(), end: new Date(endMs).toISOString() },\n types: requestedTypes,\n count: shaped.length,\n // Say so when the limit was the binding constraint, rather than\n // letting a truncated list read as a complete one.\n ...(events.length >= limit\n ? {\n truncated: true,\n note: `Returned the ${order === \"newest\" ? \"newest\" : \"oldest\"} ${limit} events; more exist in this window. Narrow the range or raise limit.`,\n }\n : {}),\n events: shaped,\n };\n }),\n );\n\n server.registerTool(\n \"unifi_protect_get_event\",\n {\n description:\n \"Get one event's full record, including detection metadata the search results leave \" +\n \"out — per-object tracking, detected zones, licence plate text and vehicle attributes \" +\n \"where the camera captured them. Use the `id` from unifi_protect_list_events.\",\n inputSchema: {\n eventId: z.string().min(1).describe(\"Event id — the `id` from unifi_protect_list_events.\"),\n },\n annotations: { readOnlyHint: true },\n },\n async ({ eventId }) => wrap(() => client.get(`events/${encodeURIComponent(eventId)}`)),\n );\n\n server.registerTool(\n \"unifi_protect_get_event_thumbnail\",\n {\n description:\n \"Fetch the still image Protect captured for an event — the frame that triggered the \" +\n 'detection. Writes it to disk and returns the path by default; set output=\"image\" to ' +\n \"return it inline for a vision model to look at, which costs a large amount of context. \" +\n \"Pass the event's `id` from unifi_protect_list_events; results showing \" +\n \"`hasThumbnail: true` have one.\",\n inputSchema: {\n eventId: z\n .string()\n .min(1)\n .describe(\n \"Event id — the `id` from unifi_protect_list_events. Results with \" +\n \"`hasThumbnail: true` have an image; others return 404. A raw `e-…` value from \" +\n \"the console's own payload is also accepted.\",\n ),\n output: z\n .enum([\"file\", \"image\"])\n .default(\"file\")\n .describe('\"file\" writes it to disk and returns the path; \"image\" returns it inline.'),\n savePath: z\n .string()\n .optional()\n .describe(\n \"Absolute path to write the JPEG to. Defaults to a file under \" +\n \"UNIFI_PROTECT_SNAPSHOT_DIR. Parent directories are created.\",\n ),\n },\n annotations: { readOnlyHint: true },\n },\n async ({ eventId, output, savePath }) =>\n wrapResult(async () => {\n // The console reports an event's thumbnail as `e-<eventId>`, but that\n // form belongs to `thumbnails/<id>`; `events/<id>/thumbnail` wants the\n // bare event id. Strip the prefix so either value works rather than\n // making the caller know which endpoint this happens to use.\n const id = eventId.startsWith(\"e-\") ? eventId.slice(2) : eventId;\n const { bytes, contentType } = await client.requestBytes(\n `events/${encodeURIComponent(id)}/thumbnail`,\n { accept: \"image/jpeg\" },\n );\n\n if (output === \"image\") {\n return {\n content: [\n {\n type: \"image\" as const,\n data: Buffer.from(bytes).toString(\"base64\"),\n mimeType: contentType.startsWith(\"image/\") ? contentType : \"image/jpeg\",\n },\n ],\n };\n }\n\n const path = savePath ?? join(ctx.config.snapshotDir, `event-${slugId(id)}.jpg`);\n await mkdir(join(path, \"..\"), { recursive: true });\n await writeFile(path, bytes);\n return ok({ path, bytes: bytes.byteLength, contentType });\n }),\n );\n\n server.registerTool(\n \"unifi_protect_export_video\",\n {\n description:\n \"Export recorded footage from one camera over a time range as an MP4 file on disk. \" +\n \"Always writes to a file and returns the path — video is never returned inline. Size \" +\n \"grows quickly with the window: expect tens of megabytes per minute at full quality, \" +\n \"and the call fails rather than exhausting memory if the export exceeds \" +\n \"UNIFI_PROTECT_MAX_DOWNLOAD_BYTES. Footage only exists if the camera was recording at \" +\n \"the time, so check the recording mode before concluding that nothing happened.\",\n inputSchema: {\n cameraId: cameraIdArg,\n start: requiredTimeArg(\"Beginning of the footage to export.\"),\n end: requiredTimeArg(\"End of the footage to export.\"),\n savePath: z\n .string()\n .optional()\n .describe(\n \"Absolute path to write the MP4 to. Defaults to a timestamped file under \" +\n \"UNIFI_PROTECT_SNAPSHOT_DIR. Parent directories are created.\",\n ),\n channel: z\n .number()\n .int()\n .min(0)\n .max(3)\n .default(0)\n .describe(\n \"Encoder channel: 0 is the high-quality stream, higher numbers are progressively \" +\n \"lower bitrate. Use a higher channel to keep a long export manageable.\",\n ),\n },\n annotations: { readOnlyHint: true },\n },\n async ({ cameraId, start, end, savePath, channel }) =>\n wrap(async () => {\n const startMs = toEpochMs(start);\n const endMs = toEpochMs(end);\n if (startMs >= endMs) {\n throw new Error(\n `The window is empty: start (${new Date(startMs).toISOString()}) is not before end ` +\n `(${new Date(endMs).toISOString()}).`,\n );\n }\n\n const { bytes, contentType } = await client.requestBytes(\"video/export\", {\n query: { camera: cameraId, start: startMs, end: endMs, channel },\n accept: \"video/mp4\",\n });\n\n const cameras = await ctx.devices.cameras();\n const name = cameras.get(cameraId) ?? cameraId;\n const stamp = new Date(startMs).toISOString().replace(/[:.]/g, \"-\");\n const path = savePath ?? join(ctx.config.snapshotDir, `${slugId(name)}-${stamp}.mp4`);\n await mkdir(join(path, \"..\"), { recursive: true });\n await writeFile(path, bytes);\n\n return {\n path,\n bytes: bytes.byteLength,\n contentType,\n camera: name,\n window: { start: new Date(startMs).toISOString(), end: new Date(endMs).toISOString() },\n };\n }),\n );\n};\n\nconst slugId = (value: string): string =>\n value\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-|-$/g, \"\") || \"export\";\n","import type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { z } from \"zod\";\n\nimport { WritesDisabledError } from \"../client/errors.js\";\nimport type { ProtectClient } from \"../client/protect.js\";\nimport { wrap } from \"./util.js\";\n\n/**\n * Reject anything that is not a plain relative path. The server decides the\n * host and the `/proxy/protect/api` prefix; letting a caller supply either\n * would send the console session somewhere it does not belong.\n */\nexport const assertSafePath = (path: string): void => {\n if (/^[a-z][a-z0-9+.-]*:\\/\\//i.test(path)) {\n throw new Error(\"`path` must be a path, not an absolute URL — the server sets the host.\");\n }\n if (path.split(\"/\").includes(\"..\")) {\n throw new Error(\"`path` must not contain `..` segments.\");\n }\n};\n\nexport const registerRequestTool = (\n server: McpServer,\n client: ProtectClient,\n allowWrites: boolean,\n): void => {\n const methods = allowWrites ? ([\"GET\", \"POST\", \"PATCH\", \"DELETE\"] as const) : ([\"GET\"] as const);\n\n server.registerTool(\n \"unifi_protect_request\",\n {\n description:\n \"Escape hatch: call any private Protect API endpoint directly, relative to \" +\n \"/proxy/protect/api. This exists because the private API is undocumented and Ubiquiti \" +\n \"moves endpoints between Protect releases — when a wrapped tool starts returning 404, \" +\n \"this reaches the replacement without waiting for a new version of this server. \" +\n \"Responses are returned RAW and unshaped, so a broad endpoint like `bootstrap` can \" +\n \"return hundreds of kilobytes; prefer the wrapped tools, which summarize. \" +\n (allowWrites\n ? \"Writes are ENABLED, so POST, PATCH and DELETE are permitted — there is no \" +\n \"confirmation step here, so check the path before you call it.\"\n : \"Writes are DISABLED: only GET is permitted. Set UNIFI_PROTECT_ALLOW_WRITES=1 to \" +\n \"allow mutations.\"),\n inputSchema: {\n path: z\n .string()\n .min(1)\n .describe(\n 'Path relative to /proxy/protect/api, without a leading slash, e.g. \"cameras\", ' +\n '\"nvr\", \"events/abc123\". Not an absolute URL.',\n ),\n method: z.enum(methods).default(\"GET\").describe(\"HTTP method.\"),\n query: z\n .record(z.string(), z.string())\n .optional()\n .describe(\n 'Query parameters as a flat string map, e.g. {\"start\":\"1756500000000\"}. Remember ' +\n \"that Protect times are milliseconds since the epoch.\",\n ),\n body: z\n .record(z.string(), z.unknown())\n .optional()\n .describe(\"JSON request body, for POST and PATCH.\"),\n },\n annotations: { readOnlyHint: !allowWrites, destructiveHint: allowWrites },\n },\n async ({ path, method, query, body }) =>\n wrap(async () => {\n // Belt and braces: the enum already excludes writes, but a client could\n // hand-roll a request that skips schema validation entirely.\n if (!allowWrites && method !== \"GET\") {\n throw new WritesDisabledError(`unifi_protect_request with method ${method}`);\n }\n assertSafePath(path);\n return client.request(method, path, {\n ...(query ? { query } : {}),\n ...(body ? { body } : {}),\n });\n }),\n );\n};\n","import { statSync } from \"node:fs\";\n\nimport type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { z } from \"zod\";\n\nimport type { ProtectClient } from \"../client/protect.js\";\nimport { isConfigured, setupInstructions } from \"../config.js\";\nimport type { ToolContext } from \"./index.js\";\nimport { confirmArg, wrap } from \"./util.js\";\n\nconst fileMode = (path: string): number | undefined => {\n try {\n return statSync(path).mode & 0o777;\n } catch {\n return undefined;\n }\n};\n\nexport const registerStatusTools = (\n server: McpServer,\n client: ProtectClient,\n ctx: ToolContext,\n): void => {\n server.registerTool(\n \"unifi_protect_auth_status\",\n {\n description:\n \"Check whether this server can actually reach your UniFi Protect console. By default it \" +\n \"logs in and makes a real call, so the answer reflects the console rather than cached \" +\n \"state — this is the tool to run when something is not working. Reports the host, the \" +\n \"account, the Protect version, whether TLS is verified, and whether writes are enabled; \" +\n \"when nothing is configured it returns the exact setup steps instead. Call this first \" +\n \"when a tool you expected is not listed: an absent tool means missing configuration or \" +\n \"writes being off, not a bug.\",\n inputSchema: {\n probe: z\n .boolean()\n .default(true)\n .describe(\n \"Actually contact the console (logging in if needed) rather than only reporting \" +\n \"what is already cached. Set false for a fast, purely local answer.\",\n ),\n },\n annotations: { readOnlyHint: true },\n },\n async ({ probe }) =>\n wrap(async () => {\n const configured = isConfigured(ctx.config);\n if (!configured) {\n return {\n configured: false,\n host: ctx.config.baseUrl ?? null,\n available_without_credentials: [\"unifi_protect_auth_status\"],\n setup: setupInstructions(ctx.config),\n };\n }\n\n // Reporting only the cached session state answers the wrong question: a\n // freshly started server has no session yet and would report\n // \"authenticated: false\" while being perfectly able to reach the\n // console. So by default this actually logs in and calls the API, and\n // reports what happened.\n let reachable: boolean | undefined;\n let version: unknown;\n let failure: string | undefined;\n if (probe) {\n try {\n const bootstrap = await client.get<Record<string, unknown>>(\"bootstrap\");\n const nvr = bootstrap?.nvr;\n version =\n typeof nvr === \"object\" && nvr !== null\n ? (nvr as Record<string, unknown>).version\n : undefined;\n reachable = true;\n } catch (err) {\n reachable = false;\n failure = err instanceof Error ? err.message : String(err);\n }\n }\n\n const status = ctx.session.describe();\n const mode = fileMode(ctx.config.sessionFile);\n return {\n configured: true,\n host: ctx.config.baseUrl,\n username: status.username ?? null,\n ...(probe\n ? {\n reachable,\n ...(version !== undefined ? { protectVersion: version } : {}),\n ...(failure ? { failure } : {}),\n }\n : {}),\n session: {\n authenticated: status.authenticated,\n // \"restored\" means it came from disk and has not been proven yet —\n // the first real call is what confirms it, via a 401 and re-login\n // if the cookie has expired.\n source: status.source,\n established_at: status.savedAt ?? null,\n file: ctx.config.sessionFile,\n file_mode: mode === undefined ? \"absent\" : `0${mode.toString(8)}`,\n ...(mode !== undefined && (mode & 0o077) !== 0\n ? { warning: `Readable by other users. Run: chmod 600 ${ctx.config.sessionFile}` }\n : {}),\n },\n tls: ctx.config.verifyTls\n ? \"verified\"\n : \"UNVERIFIED — certificate checks are off for this server's requests only. \" +\n \"Verifying needs both NODE_EXTRA_CA_CERTS pointing at the console certificate and \" +\n \"UNIFI_PROTECT_HOST set to a host name: the certificate carries no IP SAN.\",\n writes: ctx.allowWrites ? \"enabled\" : \"disabled\",\n api:\n \"private (undocumented). Ubiquiti moves these endpoints between Protect releases, so \" +\n \"the Protect version reported above is the first thing to check if a tool that used \" +\n \"to work starts returning 404.\",\n };\n }),\n );\n\n // The login tools are pointless without a console to log in to, so they do not\n // exist at all until one is configured.\n if (!isConfigured(ctx.config)) return;\n\n server.registerTool(\n \"unifi_protect_auth_login\",\n {\n description:\n \"Force a fresh login to the console, replacing any cached session. Normally unnecessary \" +\n \"— the server logs in on demand and re-authenticates automatically on a 401. Use it to \" +\n \"supply a two-factor code, which cannot be done unattended: the code is single-use and \" +\n \"expires in about 30 seconds, so it is passed here once and the resulting session is \" +\n \"then cached and reused.\",\n inputSchema: {\n totp: z\n .string()\n .regex(/^\\d{6,8}$/, \"A two-factor code is 6 to 8 digits.\")\n .optional()\n .describe(\"Current code from your authenticator app. Omit if the account has no 2FA.\"),\n },\n annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },\n },\n async ({ totp }) =>\n wrap(async () => {\n const status = await ctx.session.login(totp);\n return {\n authenticated: status.authenticated,\n username: status.username,\n established_at: status.savedAt,\n session_file: ctx.config.sessionFile,\n note: \"The session is stored with mode 600 and reused until the console rejects it.\",\n };\n }),\n );\n\n server.registerTool(\n \"unifi_protect_auth_logout\",\n {\n description:\n \"Drop the cached session and delete the session file. The next call logs in again from \" +\n \"the configured username and password, so this does not lock anything out — use it to \" +\n \"clear a session after changing accounts, or to remove the cookie from disk.\",\n inputSchema: { confirm: confirmArg },\n annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true },\n },\n async () =>\n wrap(async () => {\n await ctx.session.logout();\n return {\n logged_out: true,\n note: \"Credentials are unchanged; the next tool call will log in again.\",\n };\n }),\n );\n};\n","import type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { z } from \"zod\";\n\nimport type { ProtectClient } from \"../client/protect.js\";\nimport {\n summarizeBootstrap,\n summarizeEach,\n summarizeLiveview,\n summarizeUser,\n} from \"../client/shape.js\";\nimport type { ToolContext } from \"./index.js\";\nimport { compactOrUndefined, confirmArg, wrap } from \"./util.js\";\n\ntype Rec = Record<string, unknown>;\n\nexport const registerSystemTools = (\n server: McpServer,\n client: ProtectClient,\n ctx: ToolContext,\n): void => {\n server.registerTool(\n \"unifi_protect_get_system_info\",\n {\n description:\n \"Overview of the console: model, Protect version, firmware, timezone, uptime, storage \" +\n \"use and how many devices of each type are adopted. Worth calling first on an unfamiliar \" +\n \"system. The reported Protect version matters: this server talks to Protect's private \" +\n \"API, which Ubiquiti changes between releases, so a version that differs from the one \" +\n \"in the README is the first thing to check if a tool starts returning 404.\",\n inputSchema: {},\n annotations: { readOnlyHint: true },\n },\n async () => wrap(async () => summarizeBootstrap(await client.get<Rec>(\"bootstrap\"))),\n );\n\n server.registerTool(\n \"unifi_protect_list_users\",\n {\n description:\n \"List the accounts that can sign in to Protect, with their role and last login. Useful \" +\n \"for auditing who has access to the cameras.\",\n inputSchema: {},\n annotations: { readOnlyHint: true },\n },\n async () => wrap(async () => summarizeEach(await client.get(\"users\"), summarizeUser)),\n );\n\n server.registerTool(\n \"unifi_protect_list_liveviews\",\n {\n description:\n \"List the saved live views — the named camera grid layouts shown on viewers and in the \" +\n \"Protect app. The returned id is what unifi_protect_update_viewer needs to put a layout \" +\n \"on a screen.\",\n inputSchema: {},\n annotations: { readOnlyHint: true },\n },\n async () => wrap(async () => summarizeEach(await client.get(\"liveviews\"), summarizeLiveview)),\n );\n\n if (!ctx.allowWrites) return;\n\n server.registerTool(\n \"unifi_protect_update_nvr_settings\",\n {\n description:\n \"Change console-wide settings. `isRecordingDisabled` is the significant one: turning it \" +\n \"on stops recording on EVERY camera at once, so nothing is written until it is turned \" +\n \"back off. Only the fields you pass are sent.\",\n inputSchema: {\n name: z.string().min(1).optional().describe(\"Display name for the console.\"),\n timezone: z\n .string()\n .min(1)\n .optional()\n .describe('IANA timezone, e.g. \"Europe/Paris\". Affects event timestamps and schedules.'),\n isRecordingDisabled: z\n .boolean()\n .optional()\n .describe(\n \"Disable recording across every camera. True means no footage is kept, system-wide.\",\n ),\n },\n annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true },\n },\n async ({ name, timezone, isRecordingDisabled }) =>\n wrap(async () => {\n const body = compactOrUndefined({ name, timezone, isRecordingDisabled });\n if (!body) {\n throw new Error(\n \"Nothing to update — pass at least one of name, timezone, isRecordingDisabled.\",\n );\n }\n return client.patch(\"nvr\", body);\n }),\n );\n\n server.registerTool(\n \"unifi_protect_reboot_nvr\",\n {\n description:\n \"REBOOT THE CONSOLE. Every camera stops recording for the two to five minutes it takes \" +\n \"to come back, and footage from that window is lost permanently. This also drops the \" +\n \"network if the console is your router (a UDM or UDM Pro), taking down everything behind \" +\n \"it. Reboot a single unresponsive camera with unifi_protect_reboot_camera instead \" +\n \"wherever that would do.\",\n inputSchema: { confirm: confirmArg },\n annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false },\n },\n async () => wrap(() => client.post(\"nvr/reboot\")),\n );\n};\n","import type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\n\nimport type { SessionProvider } from \"../client/auth.js\";\nimport type { DeviceCache } from \"../client/device-cache.js\";\nimport type { ProtectClient } from \"../client/protect.js\";\nimport { isConfigured, type Config } from \"../config.js\";\nimport { registerCameraTools } from \"./cameras.js\";\nimport { registerDeviceTools } from \"./devices.js\";\nimport { registerEventTools } from \"./events.js\";\nimport { registerRequestTool } from \"./request.js\";\nimport { registerStatusTools } from \"./status.js\";\nimport { registerSystemTools } from \"./system.js\";\n\nexport type ToolContext = {\n config: Config;\n /** Register the mutating tools too. Off by default — see UNIFI_PROTECT_ALLOW_WRITES. */\n allowWrites: boolean;\n session: SessionProvider;\n devices: DeviceCache;\n};\n\n/**\n * Register the UniFi Protect tools.\n *\n * unifi_protect_auth_status comes first and unconditionally, so a server with no\n * console configured is still a useful one — it can say what to set — rather\n * than a connection that closes with its own error message swallowed.\n *\n * Read tools are then always registered; the write tools only when\n * `allowWrites` is set, so with the flag off they are not merely refused — they\n * are absent from tools/list and cannot be called at all. A refusal still lets a\n * model try, retry, and reason about how to get around it; a tool that does not\n * exist ends the conversation.\n */\nexport const registerTools = (server: McpServer, client: ProtectClient, ctx: ToolContext): void => {\n registerStatusTools(server, client, ctx);\n if (!isConfigured(ctx.config)) return;\n\n registerSystemTools(server, client, ctx);\n registerCameraTools(server, client, ctx);\n registerEventTools(server, client, ctx);\n registerDeviceTools(server, client, ctx);\n registerRequestTool(server, client, ctx.allowWrites);\n};\n","import { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\n\nimport { BUILD_INFO } from \"./build-info.js\";\nimport { createSessionProvider, type Logger, type SessionProvider } from \"./client/auth.js\";\nimport { createDeviceCache, type DeviceCache } from \"./client/device-cache.js\";\nimport { ProtectClient } from \"./client/protect.js\";\nimport { createHttpFetch } from \"./client/tls.js\";\nimport type { Config } from \"./config.js\";\nimport { registerTools } from \"./tools/index.js\";\n\nexport const SERVER_NAME = BUILD_INFO.name;\nexport const SERVER_VERSION = BUILD_INFO.version;\nexport const USER_AGENT = `mcp-unifi-protect-js/${BUILD_INFO.version}`;\n\nexport type CreateServerOptions = {\n config: Config;\n fetch?: typeof fetch;\n logger?: Logger;\n /** Override the session provider (tests, and the interactive login flow). */\n session?: SessionProvider;\n};\n\nexport type CreatedServer = {\n server: McpServer;\n client: ProtectClient;\n session: SessionProvider;\n devices: DeviceCache;\n};\n\nexport const createServer = (opts: CreateServerOptions): CreatedServer => {\n const { config } = opts;\n const server = new McpServer({ name: SERVER_NAME, version: SERVER_VERSION });\n\n // Built once and shared by the session provider and the client, so the TLS\n // policy is a property of this server's requests rather than of the process.\n // Tests inject `opts.fetch` and never reach createHttpFetch.\n const fetchImpl =\n opts.fetch ??\n createHttpFetch({\n insecureTls: !config.verifyTls,\n ...(opts.logger ? { logger: opts.logger } : {}),\n });\n\n const session =\n opts.session ??\n createSessionProvider({\n config,\n fetch: fetchImpl,\n ...(opts.logger ? { logger: opts.logger } : {}),\n });\n\n const client = new ProtectClient({\n // An unconfigured server still constructs a client, so createServer stays\n // total. No tool that could use it is registered, and the session provider\n // throws a message naming the fix if something ever reaches it anyway.\n baseUrl: config.baseUrl ?? \"https://unconfigured.invalid\",\n session,\n maxRetries: config.maxRetries,\n userAgent: USER_AGENT,\n maxDownloadBytes: config.maxDownloadBytes,\n fetch: fetchImpl,\n ...(opts.logger ? { logger: opts.logger } : {}),\n });\n\n const devices = createDeviceCache({ client, ttlSeconds: config.deviceCacheTtlSeconds });\n\n registerTools(server, client, {\n config,\n allowWrites: config.allowWrites,\n session,\n devices,\n });\n\n return { server, client, session, devices };\n};\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mgcrea/mcp-unifi-protect",
3
- "version": "0.0.0",
3
+ "version": "0.2.0",
4
4
  "description": "Model Context Protocol server for UniFi Protect",
5
5
  "keywords": [
6
6
  "mcp",
@@ -68,4 +68,4 @@
68
68
  "node": ">=22"
69
69
  },
70
70
  "packageManager": "pnpm@11.13.1"
71
- }
71
+ }