@owine/unifi-protect-mcp 2.14.2 → 2.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # UniFi Protect MCP Server
2
2
 
3
- An MCP (Model Context Protocol) server that exposes UniFi Protect's Integration REST API as tools for Claude Code and other MCP clients. Aligned with UniFi Protect API **7.3.47** — 74 tools covering cameras, lights, sensors, chimes, viewers, sirens, fobs, relays, speakers, bridges, link stations, alarm hubs, arm profiles, live views, files, users, point-of-sale events, NVR status, and WebSocket subscriptions.
3
+ An MCP (Model Context Protocol) server that exposes UniFi Protect's Integration REST API as tools for Claude Code and other MCP clients. Aligned with UniFi Protect API **7.3.70** — 74 tools covering cameras, lights, sensors, chimes, viewers, sirens, fobs, relays, speakers, bridges, link stations, alarm hubs, arm profiles, live views, files, users, point-of-sale events, NVR status, and WebSocket subscriptions.
4
4
 
5
5
  ## Prerequisites
6
6
 
@@ -243,8 +243,8 @@ server logs a warning and talks to that host directly.
243
243
  | Tool | Description |
244
244
  |---|---|
245
245
  | `protect_trigger_alarm_webhook` | Trigger an alarm webhook (fires external alarm action) |
246
- | `protect_list_files` | List files by type |
247
- | `protect_upload_file` | Upload a file (base64-encoded) |
246
+ | `protect_list_files` | List device asset files (`animations`) |
247
+ | `protect_upload_file` | Upload a device asset file (base64 in, sent as multipart/form-data) |
248
248
 
249
249
  ### Users (4)
250
250
  | Tool | Description |
package/dist/client.d.ts CHANGED
@@ -14,6 +14,12 @@ export declare class ProtectClient {
14
14
  data: Buffer;
15
15
  mimeType: string;
16
16
  }>;
17
- postBinary(path: string, data: Buffer, contentType: string): Promise<unknown>;
17
+ /**
18
+ * Upload a file as multipart/form-data (the documented encoding for
19
+ * POST /files/{fileType}). fetch sets the Content-Type with its boundary, so
20
+ * none is set here. The part name `file` is not in the published docs but
21
+ * was verified against a live 7.3.70 console.
22
+ */
23
+ postFile(path: string, data: Buffer, contentType: string, fileName: string): Promise<unknown>;
18
24
  connectWebSocket(path: string): WebSocket;
19
25
  }
package/dist/client.js CHANGED
@@ -81,6 +81,12 @@ class ProtectClient {
81
81
  if (!response.ok) {
82
82
  throw new Error(`HTTP ${response.status}: ${await readErrorBody(response, MAX_JSON_BYTES)}`);
83
83
  }
84
+ // 204 No Content (action endpoints: siren/relay/speaker triggers, arm
85
+ // profile enable/disable, PTZ, …) has no body by definition — don't let a
86
+ // stray JSON content-type turn a success into an "empty body" error.
87
+ if (response.status === 204) {
88
+ return "";
89
+ }
84
90
  enforceSizeLimit(response, MAX_JSON_BYTES);
85
91
  const contentType = response.headers.get("content-type") ?? "";
86
92
  const text = await response.text();
@@ -122,15 +128,20 @@ class ProtectClient {
122
128
  const arrayBuffer = await response.arrayBuffer();
123
129
  return { data: Buffer.from(arrayBuffer), mimeType };
124
130
  }
125
- async postBinary(path, data, contentType) {
131
+ /**
132
+ * Upload a file as multipart/form-data (the documented encoding for
133
+ * POST /files/{fileType}). fetch sets the Content-Type with its boundary, so
134
+ * none is set here. The part name `file` is not in the published docs but
135
+ * was verified against a live 7.3.70 console.
136
+ */
137
+ async postFile(path, data, contentType, fileName) {
126
138
  const url = `${this.baseUrl}${path}`;
139
+ const form = new FormData();
140
+ form.append("file", new Blob([new Uint8Array(data)], { type: contentType }), fileName);
127
141
  const response = await fetch(url, {
128
142
  method: "POST",
129
- headers: {
130
- "X-API-KEY": this.headers["X-API-KEY"],
131
- "Content-Type": contentType,
132
- },
133
- body: new Uint8Array(data),
143
+ headers: { "X-API-KEY": this.headers["X-API-KEY"] },
144
+ body: form,
134
145
  redirect: "error",
135
146
  signal: AbortSignal.timeout(BINARY_TIMEOUT_MS),
136
147
  });
@@ -3,7 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.talkbackSessionOutputSchema = exports.rtspStreamOutputSchema = exports.rtspStreamSchema = exports.cameraListOutputSchema = exports.cameraOutputSchema = exports.cameraSchema = void 0;
4
4
  const common_js_1 = require("./common.js");
5
5
  /**
6
- * Verified against live UniFi Protect Integration API 7.3.47 (2026-09-14).
6
+ * Verified against live UniFi Protect Integration API 7.3.47 (2026-09-14) and
7
+ * re-verified against live 7.3.70 (2026-10-12).
7
8
  * The Integration API camera object is far smaller than the internal Protect
8
9
  * app's camera model — no isRecording/lastMotion/channels/firmwareVersion/etc.
9
10
  * `list` and `get-by-id` return the same field set (confirmed live).
@@ -94,7 +94,6 @@ export declare const fobSchema: import("zod").ZodObject<{
94
94
  awayState: import("zod").ZodOptional<import("zod").ZodNullable<import("zod").ZodString>>;
95
95
  buttonLabels: import("zod").ZodOptional<import("zod").ZodNullable<import("zod").ZodString>>;
96
96
  featureFlags: import("zod").ZodOptional<import("zod").ZodUnknown>;
97
- hasKeypad: import("zod").ZodOptional<import("zod").ZodUnknown>;
98
97
  armControlSettings: import("zod").ZodOptional<import("zod").ZodUnknown>;
99
98
  keypadSettings: import("zod").ZodOptional<import("zod").ZodUnknown>;
100
99
  wirelessConnectionState: import("zod").ZodOptional<import("zod").ZodUnknown>;
@@ -150,7 +149,6 @@ export declare const linkStationSchema: import("zod").ZodObject<{
150
149
  ledSettings: import("zod").ZodOptional<import("zod").ZodUnknown>;
151
150
  lastEvent: import("zod").ZodOptional<import("zod").ZodUnknown>;
152
151
  alarmHub: import("zod").ZodOptional<import("zod").ZodUnknown>;
153
- deviceTamperStatus: import("zod").ZodOptional<import("zod").ZodNullable<import("zod").ZodString>>;
154
152
  threadState: import("zod").ZodOptional<import("zod").ZodUnknown>;
155
153
  }, import("zod/v4/core").$loose>;
156
154
  export declare const alarmHubSchema: import("zod").ZodObject<{
@@ -165,7 +163,6 @@ export declare const alarmHubSchema: import("zod").ZodObject<{
165
163
  ledSettings: import("zod").ZodOptional<import("zod").ZodUnknown>;
166
164
  lastEvent: import("zod").ZodOptional<import("zod").ZodUnknown>;
167
165
  alarmHub: import("zod").ZodOptional<import("zod").ZodUnknown>;
168
- deviceTamperStatus: import("zod").ZodOptional<import("zod").ZodNullable<import("zod").ZodString>>;
169
166
  threadState: import("zod").ZodOptional<import("zod").ZodUnknown>;
170
167
  }, import("zod/v4/core").$loose>;
171
168
  export declare const DEVICE_SCHEMAS: {
@@ -265,7 +262,6 @@ export declare const DEVICE_SCHEMAS: {
265
262
  awayState: import("zod").ZodOptional<import("zod").ZodNullable<import("zod").ZodString>>;
266
263
  buttonLabels: import("zod").ZodOptional<import("zod").ZodNullable<import("zod").ZodString>>;
267
264
  featureFlags: import("zod").ZodOptional<import("zod").ZodUnknown>;
268
- hasKeypad: import("zod").ZodOptional<import("zod").ZodUnknown>;
269
265
  armControlSettings: import("zod").ZodOptional<import("zod").ZodUnknown>;
270
266
  keypadSettings: import("zod").ZodOptional<import("zod").ZodUnknown>;
271
267
  wirelessConnectionState: import("zod").ZodOptional<import("zod").ZodUnknown>;
@@ -321,7 +317,6 @@ export declare const DEVICE_SCHEMAS: {
321
317
  ledSettings: import("zod").ZodOptional<import("zod").ZodUnknown>;
322
318
  lastEvent: import("zod").ZodOptional<import("zod").ZodUnknown>;
323
319
  alarmHub: import("zod").ZodOptional<import("zod").ZodUnknown>;
324
- deviceTamperStatus: import("zod").ZodOptional<import("zod").ZodNullable<import("zod").ZodString>>;
325
320
  threadState: import("zod").ZodOptional<import("zod").ZodUnknown>;
326
321
  }, import("zod/v4/core").$loose>;
327
322
  readonly alarm_hub: import("zod").ZodObject<{
@@ -336,7 +331,6 @@ export declare const DEVICE_SCHEMAS: {
336
331
  ledSettings: import("zod").ZodOptional<import("zod").ZodUnknown>;
337
332
  lastEvent: import("zod").ZodOptional<import("zod").ZodUnknown>;
338
333
  alarmHub: import("zod").ZodOptional<import("zod").ZodUnknown>;
339
- deviceTamperStatus: import("zod").ZodOptional<import("zod").ZodNullable<import("zod").ZodString>>;
340
334
  threadState: import("zod").ZodOptional<import("zod").ZodUnknown>;
341
335
  }, import("zod/v4/core").$loose>;
342
336
  };
@@ -431,7 +425,6 @@ export declare function itemOutputShape(key: keyof typeof DEVICE_SCHEMAS): {
431
425
  awayState: import("zod").ZodOptional<import("zod").ZodNullable<import("zod").ZodString>>;
432
426
  buttonLabels: import("zod").ZodOptional<import("zod").ZodNullable<import("zod").ZodString>>;
433
427
  featureFlags: import("zod").ZodOptional<import("zod").ZodUnknown>;
434
- hasKeypad: import("zod").ZodOptional<import("zod").ZodUnknown>;
435
428
  armControlSettings: import("zod").ZodOptional<import("zod").ZodUnknown>;
436
429
  keypadSettings: import("zod").ZodOptional<import("zod").ZodUnknown>;
437
430
  wirelessConnectionState: import("zod").ZodOptional<import("zod").ZodUnknown>;
@@ -483,7 +476,6 @@ export declare function itemOutputShape(key: keyof typeof DEVICE_SCHEMAS): {
483
476
  ledSettings: import("zod").ZodOptional<import("zod").ZodUnknown>;
484
477
  lastEvent: import("zod").ZodOptional<import("zod").ZodUnknown>;
485
478
  alarmHub: import("zod").ZodOptional<import("zod").ZodUnknown>;
486
- deviceTamperStatus: import("zod").ZodOptional<import("zod").ZodNullable<import("zod").ZodString>>;
487
479
  threadState: import("zod").ZodOptional<import("zod").ZodUnknown>;
488
480
  };
489
481
  export declare function listOutputShape(key: keyof typeof DEVICE_SCHEMAS): {
@@ -578,7 +570,6 @@ export declare function listOutputShape(key: keyof typeof DEVICE_SCHEMAS): {
578
570
  awayState: import("zod").ZodOptional<import("zod").ZodNullable<import("zod").ZodString>>;
579
571
  buttonLabels: import("zod").ZodOptional<import("zod").ZodNullable<import("zod").ZodString>>;
580
572
  featureFlags: import("zod").ZodOptional<import("zod").ZodUnknown>;
581
- hasKeypad: import("zod").ZodOptional<import("zod").ZodUnknown>;
582
573
  armControlSettings: import("zod").ZodOptional<import("zod").ZodUnknown>;
583
574
  keypadSettings: import("zod").ZodOptional<import("zod").ZodUnknown>;
584
575
  wirelessConnectionState: import("zod").ZodOptional<import("zod").ZodUnknown>;
@@ -630,7 +621,6 @@ export declare function listOutputShape(key: keyof typeof DEVICE_SCHEMAS): {
630
621
  ledSettings: import("zod").ZodOptional<import("zod").ZodUnknown>;
631
622
  lastEvent: import("zod").ZodOptional<import("zod").ZodUnknown>;
632
623
  alarmHub: import("zod").ZodOptional<import("zod").ZodUnknown>;
633
- deviceTamperStatus: import("zod").ZodOptional<import("zod").ZodNullable<import("zod").ZodString>>;
634
624
  threadState: import("zod").ZodOptional<import("zod").ZodUnknown>;
635
625
  }, import("zod/v4/core").$loose>>;
636
626
  };
@@ -7,11 +7,11 @@ const common_js_1 = require("./common.js");
7
7
  /**
8
8
  * Device schemas for the config-driven Protect device types.
9
9
  *
10
- * Field sets are derived from the UniFi Protect Integration API 7.3.47 docs
10
+ * Field sets are derived from the UniFi Protect Integration API 7.3.70 docs
11
11
  * (per-type response samples). Chimes were additionally verified against live
12
- * console responses (2026-09-14); the rest have no instances on the available
13
- * console, so their TOP-LEVEL fields are typed from the published docs while
14
- * every nested object/array is left as `unknownField()` (z.unknown()) — naming
12
+ * console responses (7.3.47 on 2026-09-14, 7.3.70 on 2026-10-12); the rest
13
+ * have no instances on the available console, so their TOP-LEVEL fields are
14
+ * typed from the published docs while every nested object/array is left as `unknownField()` (z.unknown()) — naming
15
15
  * the field for the LLM without risking SDK output-validation failures on a
16
16
  * shape we have not observed live. `.passthrough()` lets any extra field flow.
17
17
  *
@@ -45,16 +45,16 @@ exports.sensorSchema = (0, common_js_1.passthroughObject)({
45
45
  featureFlags: (0, common_js_1.unknownField)("Sensor capability flags, added in 7.3.x (object: temperature, humidity, " +
46
46
  "light, motion, waterLeak, open, tamper, smoke — each { channelCount })"),
47
47
  batteryStatus: (0, common_js_1.unknownField)("Battery status (object: percentage, isLow)"),
48
- stats: (0, common_js_1.unknownField)("Environmental stats (object: light, humidity, temperature)"),
49
- lightSettings: (0, common_js_1.unknownField)("Light threshold settings (object)"),
50
- humiditySettings: (0, common_js_1.unknownField)("Humidity threshold settings (object)"),
51
- temperatureSettings: (0, common_js_1.unknownField)("Temperature threshold settings (object)"),
48
+ stats: (0, common_js_1.unknownField)("Environmental stats (object: light, humidity, temperature — each { value, status })"),
49
+ lightSettings: (0, common_js_1.unknownField)("Light threshold settings (object: isEnabled, margin, lowThreshold, highThreshold)"),
50
+ humiditySettings: (0, common_js_1.unknownField)("Humidity threshold settings (object: isEnabled, margin, lowThreshold, highThreshold)"),
51
+ temperatureSettings: (0, common_js_1.unknownField)("Temperature threshold settings (object: isEnabled, margin, lowThreshold, highThreshold)"),
52
52
  isOpened: (0, common_js_1.unknownField)("Open/close contact state (boolean)"),
53
53
  openStatusChangedAt: (0, common_js_1.unknownField)("Open-status change timestamp in epoch ms (number)"),
54
54
  isMotionDetected: (0, common_js_1.unknownField)("Motion currently detected (boolean)"),
55
55
  motionDetectedAt: (0, common_js_1.unknownField)("Last motion timestamp in epoch ms (number)"),
56
- motionSettings: (0, common_js_1.unknownField)("Motion detection settings (object)"),
57
- glassBreakSettings: (0, common_js_1.unknownField)("Glass-break detection settings (object)"),
56
+ motionSettings: (0, common_js_1.unknownField)("Motion detection settings (object: isEnabled, sensitivity, sensitivityWhenArmed)"),
57
+ glassBreakSettings: (0, common_js_1.unknownField)("Glass-break detection settings (object: isEnabled, sensitivity, sensitivityWhenArmed)"),
58
58
  scheduleMode: (0, common_js_1.nullableString)('Schedule mode: "always" | "when_armed"'),
59
59
  armProfileIds: (0, common_js_1.unknownField)("Arm profile IDs this sensor belongs to (array of strings)"),
60
60
  hasCustomSensitivityWhenArmed: (0, common_js_1.unknownField)("Custom armed sensitivity enabled (boolean)"),
@@ -62,12 +62,13 @@ exports.sensorSchema = (0, common_js_1.passthroughObject)({
62
62
  alarmSettings: (0, common_js_1.unknownField)("Alarm settings (object: isEnabled)"),
63
63
  leakDetectedAt: (0, common_js_1.unknownField)("Last leak timestamp in epoch ms (number)"),
64
64
  externalLeakDetectedAt: (0, common_js_1.unknownField)("Last external-leak timestamp in epoch ms (number)"),
65
- leakSettings: (0, common_js_1.unknownField)("Leak detection settings (object)"),
65
+ leakSettings: (0, common_js_1.unknownField)("Leak detection settings, read-only (object: isInternalEnabled, isExternalEnabled)"),
66
66
  tamperingDetectedAt: (0, common_js_1.unknownField)("Last tampering timestamp in epoch ms (number)"),
67
- wirelessConnectionState: (0, common_js_1.unknownField)("Wireless link state (object: signalState, batteryStatus, bridge)"),
67
+ wirelessConnectionState: (0, common_js_1.unknownField)("Wireless link state (object: signalState { signalQuality, signalStrength }, " +
68
+ "batteryStatus { percentage, isLow }, bridge)"),
68
69
  });
69
- // Verified live (7.1.83): chimes return cameraIds[] and ringSettings[] whose
70
- // items are { cameraId, volume, ringtoneId, repeatTimes }.
70
+ // Verified live (7.1.83, re-verified 7.3.70): chimes return cameraIds[] and
71
+ // ringSettings[] whose items are { cameraId, volume, ringtoneId, repeatTimes }.
71
72
  exports.chimeSchema = (0, common_js_1.passthroughObject)({
72
73
  ...deviceIdentity(),
73
74
  cameraIds: (0, common_js_1.unknownField)("Paired camera IDs (array of strings)"),
@@ -84,25 +85,29 @@ exports.sirenSchema = (0, common_js_1.passthroughObject)({
84
85
  ledSettings: (0, common_js_1.unknownField)("LED settings (object: isEnabled)"),
85
86
  sirenStatus: (0, common_js_1.unknownField)("Current siren status (object: isActive, activatedAt, duration)"),
86
87
  connectionType: (0, common_js_1.nullableString)('Connection type, e.g. "lora"'),
87
- wirelessConnectionState: (0, common_js_1.unknownField)("Wireless link state (object)"),
88
+ wirelessConnectionState: (0, common_js_1.unknownField)("Wireless link state (object: signalState { signalQuality, signalStrength }, " +
89
+ "batteryStatus { percentage, isLow }, bridge)"),
88
90
  });
89
91
  exports.fobSchema = (0, common_js_1.passthroughObject)({
90
92
  ...deviceIdentity(),
91
93
  awayState: (0, common_js_1.nullableString)('Away state, e.g. "ONLINE"'),
92
94
  buttonLabels: (0, common_js_1.nullableString)('Button label preset, e.g. "securityActions"'),
93
- featureFlags: (0, common_js_1.unknownField)("Feature flags (object: buttons[])"),
94
- hasKeypad: (0, common_js_1.unknownField)("Whether this fob has a keypad, added in 7.3.x (boolean)"),
95
+ featureFlags: (0, common_js_1.unknownField)("Feature flags (object: buttons[], hasKeypad — hasKeypad added in 7.3.x)"),
95
96
  armControlSettings: (0, common_js_1.unknownField)("Arm-control config, added in 7.3.x (object: enabled, armProfileId, nightProfileId)"),
96
- keypadSettings: (0, common_js_1.unknownField)("Keypad config, added in 7.3.x (object: beepEnabled, beepVolume, " +
97
- "backlightEnabled, backlightBrightness)"),
98
- wirelessConnectionState: (0, common_js_1.unknownField)("Wireless link state (object)"),
97
+ keypadSettings: (0, common_js_1.unknownField)("Keypad config, added in 7.3.x (object: beepEnabled, beepVolume). The " +
98
+ "backlightEnabled/backlightBrightness fields were dropped in 7.3.70."),
99
+ wirelessConnectionState: (0, common_js_1.unknownField)("Wireless link state (object: signalState { signalQuality, signalStrength }, " +
100
+ "batteryStatus { percentage, isLow }, bridge)"),
99
101
  });
100
102
  exports.relaySchema = (0, common_js_1.passthroughObject)({
101
103
  ...deviceIdentity(),
102
104
  ledSettings: (0, common_js_1.unknownField)("LED settings (object: isEnabled)"),
103
- outputs: (0, common_js_1.unknownField)("Output channels (array of objects)"),
104
- inputs: (0, common_js_1.unknownField)("Input channels (array of objects)"),
105
- wirelessConnectionState: (0, common_js_1.unknownField)("Wireless link state (object)"),
105
+ outputs: (0, common_js_1.unknownField)("Output channels (array of objects: id, name, type, delay, pulseDuration, " +
106
+ "state, rebootState). `id` is the outputId for protect_activate_relay_output"),
107
+ inputs: (0, common_js_1.unknownField)("Input channels (array of objects: id, name, state, actionTrigger, " +
108
+ "actionType, actionOutputId)"),
109
+ wirelessConnectionState: (0, common_js_1.unknownField)("Wireless link state (object: signalState { signalQuality, signalStrength }, " +
110
+ "batteryStatus { percentage, isLow }, bridge)"),
106
111
  });
107
112
  exports.speakerSchema = (0, common_js_1.passthroughObject)({
108
113
  ...deviceIdentity(),
@@ -125,8 +130,12 @@ const linkStationLikeSchema = () => (0, common_js_1.passthroughObject)({
125
130
  isAlarmHub: (0, common_js_1.unknownField)("Whether this device is an alarm hub (boolean)"),
126
131
  ledSettings: (0, common_js_1.unknownField)("LED settings (object: isEnabled)"),
127
132
  lastEvent: (0, common_js_1.unknownField)("Last event timestamp in epoch ms (number)"),
128
- alarmHub: (0, common_js_1.unknownField)("Alarm hub status (object: armed, battery, connector, cover, output, input, …)"),
129
- deviceTamperStatus: (0, common_js_1.nullableString)('Tamper status, added in 7.3.x, e.g. "tampered"'),
133
+ alarmHub: (0, common_js_1.unknownField)("Alarm hub status (object: armed, battery, buckboost, connector, cover, " +
134
+ 'deviceTamperStatus (e.g. "tampered", added in 7.3.x), ' +
135
+ "currentMeterChannelStatus, currentMeterStatus, inputPower, poeout, " +
136
+ "powerMeter, output and input (maps keyed by channel), " +
137
+ "inputTerminalStatus, outputTerminalStatus, emergencyTerminalStatus, " +
138
+ "auxiliaryPowerTerminalStatus)"),
130
139
  threadState: (0, common_js_1.unknownField)("Thread radio state, added in 7.3.x (object: network — status, role, " +
131
140
  "networkName, channel, panId, extendedPanId, joinedDeviceCount, " +
132
141
  "errorReason, lastUpdatedAt)"),
@@ -1,9 +1,9 @@
1
1
  import { z } from "zod";
2
2
  /**
3
3
  * Schemas below are verified against live UniFi Protect Integration API 7.3.47
4
- * responses (Melrose NVR Pro console, 2026-09-14) where instances exist, and
5
- * against the 7.3.47 published docs otherwise (arm profiles, files — no
6
- * instances on the console). The Integration API surface is much thinner than
4
+ * responses (Melrose NVR Pro console, 2026-09-14) where instances exist —
5
+ * re-verified live on 7.3.70 (2026-10-12) — and against the 7.3.70 published
6
+ * docs otherwise (arm profiles, files — no instances on the console). The Integration API surface is much thinner than
7
7
  * the internal Protect app API — only observed/documented fields are typed;
8
8
  * `.passthrough()` still allows anything extra a firmware/hardware mix adds.
9
9
  */
@@ -17,13 +17,13 @@ export declare const nvrSchema: z.ZodObject<{
17
17
  modelKey: z.ZodOptional<z.ZodNullable<z.ZodString>>;
18
18
  name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
19
19
  mac: z.ZodOptional<z.ZodNullable<z.ZodString>>;
20
- doorbellSettings: z.ZodOptional<z.ZodObject<{
20
+ doorbellSettings: z.ZodOptional<z.ZodNullable<z.ZodObject<{
21
21
  defaultMessageText: z.ZodOptional<z.ZodNullable<z.ZodString>>;
22
22
  defaultMessageResetTimeoutMs: z.ZodOptional<z.ZodUnknown>;
23
23
  customMessages: z.ZodOptional<z.ZodUnknown>;
24
24
  customImages: z.ZodOptional<z.ZodUnknown>;
25
- }, z.core.$loose>>;
26
- armMode: z.ZodOptional<z.ZodObject<{
25
+ }, z.core.$loose>>>;
26
+ armMode: z.ZodOptional<z.ZodNullable<z.ZodObject<{
27
27
  status: z.ZodOptional<z.ZodNullable<z.ZodString>>;
28
28
  armProfileId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
29
29
  armedAt: z.ZodOptional<z.ZodUnknown>;
@@ -32,7 +32,7 @@ export declare const nvrSchema: z.ZodObject<{
32
32
  breachEventCount: z.ZodOptional<z.ZodUnknown>;
33
33
  breachTriggerEventId: z.ZodOptional<z.ZodUnknown>;
34
34
  breachEventId: z.ZodOptional<z.ZodUnknown>;
35
- }, z.core.$loose>>;
35
+ }, z.core.$loose>>>;
36
36
  }, z.core.$loose>;
37
37
  export declare const nvrOutputSchema: {
38
38
  type: z.ZodOptional<z.ZodNullable<z.ZodString>>;
@@ -41,13 +41,13 @@ export declare const nvrOutputSchema: {
41
41
  modelKey: z.ZodOptional<z.ZodNullable<z.ZodString>>;
42
42
  name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
43
43
  mac: z.ZodOptional<z.ZodNullable<z.ZodString>>;
44
- doorbellSettings: z.ZodOptional<z.ZodObject<{
44
+ doorbellSettings: z.ZodOptional<z.ZodNullable<z.ZodObject<{
45
45
  defaultMessageText: z.ZodOptional<z.ZodNullable<z.ZodString>>;
46
46
  defaultMessageResetTimeoutMs: z.ZodOptional<z.ZodUnknown>;
47
47
  customMessages: z.ZodOptional<z.ZodUnknown>;
48
48
  customImages: z.ZodOptional<z.ZodUnknown>;
49
- }, z.core.$loose>>;
50
- armMode: z.ZodOptional<z.ZodObject<{
49
+ }, z.core.$loose>>>;
50
+ armMode: z.ZodOptional<z.ZodNullable<z.ZodObject<{
51
51
  status: z.ZodOptional<z.ZodNullable<z.ZodString>>;
52
52
  armProfileId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
53
53
  armedAt: z.ZodOptional<z.ZodUnknown>;
@@ -56,7 +56,7 @@ export declare const nvrOutputSchema: {
56
56
  breachEventCount: z.ZodOptional<z.ZodUnknown>;
57
57
  breachTriggerEventId: z.ZodOptional<z.ZodUnknown>;
58
58
  breachEventId: z.ZodOptional<z.ZodUnknown>;
59
- }, z.core.$loose>>;
59
+ }, z.core.$loose>>>;
60
60
  };
61
61
  export declare const userSchema: z.ZodObject<{
62
62
  id: z.ZodOptional<z.ZodString>;
@@ -124,11 +124,11 @@ export declare const liveviewSchema: z.ZodObject<{
124
124
  isGlobal: z.ZodOptional<z.ZodUnknown>;
125
125
  layout: z.ZodOptional<z.ZodUnknown>;
126
126
  owner: z.ZodOptional<z.ZodNullable<z.ZodString>>;
127
- slots: z.ZodOptional<z.ZodArray<z.ZodObject<{
127
+ slots: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodObject<{
128
128
  cameras: z.ZodOptional<z.ZodUnknown>;
129
129
  cycleMode: z.ZodOptional<z.ZodNullable<z.ZodString>>;
130
130
  cycleInterval: z.ZodOptional<z.ZodUnknown>;
131
- }, z.core.$loose>>>;
131
+ }, z.core.$loose>>>>;
132
132
  }, z.core.$loose>;
133
133
  export declare const liveviewOutputSchema: {
134
134
  id: z.ZodOptional<z.ZodString>;
@@ -138,11 +138,11 @@ export declare const liveviewOutputSchema: {
138
138
  isGlobal: z.ZodOptional<z.ZodUnknown>;
139
139
  layout: z.ZodOptional<z.ZodUnknown>;
140
140
  owner: z.ZodOptional<z.ZodNullable<z.ZodString>>;
141
- slots: z.ZodOptional<z.ZodArray<z.ZodObject<{
141
+ slots: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodObject<{
142
142
  cameras: z.ZodOptional<z.ZodUnknown>;
143
143
  cycleMode: z.ZodOptional<z.ZodNullable<z.ZodString>>;
144
144
  cycleInterval: z.ZodOptional<z.ZodUnknown>;
145
- }, z.core.$loose>>>;
145
+ }, z.core.$loose>>>>;
146
146
  };
147
147
  export declare const liveviewListOutputSchema: {
148
148
  result: z.ZodArray<z.ZodObject<{
@@ -153,11 +153,11 @@ export declare const liveviewListOutputSchema: {
153
153
  isGlobal: z.ZodOptional<z.ZodUnknown>;
154
154
  layout: z.ZodOptional<z.ZodUnknown>;
155
155
  owner: z.ZodOptional<z.ZodNullable<z.ZodString>>;
156
- slots: z.ZodOptional<z.ZodArray<z.ZodObject<{
156
+ slots: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodObject<{
157
157
  cameras: z.ZodOptional<z.ZodUnknown>;
158
158
  cycleMode: z.ZodOptional<z.ZodNullable<z.ZodString>>;
159
159
  cycleInterval: z.ZodOptional<z.ZodUnknown>;
160
- }, z.core.$loose>>>;
160
+ }, z.core.$loose>>>>;
161
161
  }, z.core.$loose>>;
162
162
  };
163
163
  export declare const armProfileSchema: z.ZodObject<{
@@ -196,19 +196,40 @@ export declare const armProfileOutputSchema: {
196
196
  updatedAt: z.ZodOptional<z.ZodUnknown>;
197
197
  };
198
198
  export declare const fileSchema: z.ZodObject<{
199
+ id: z.ZodOptional<z.ZodString>;
199
200
  name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
200
201
  type: z.ZodOptional<z.ZodNullable<z.ZodString>>;
201
202
  originalName: z.ZodOptional<z.ZodNullable<z.ZodString>>;
202
203
  path: z.ZodOptional<z.ZodNullable<z.ZodString>>;
204
+ size: z.ZodOptional<z.ZodUnknown>;
205
+ createdAt: z.ZodOptional<z.ZodNullable<z.ZodString>>;
206
+ updatedAt: z.ZodOptional<z.ZodNullable<z.ZodString>>;
207
+ metadata: z.ZodOptional<z.ZodUnknown>;
203
208
  }, z.core.$loose>;
204
209
  export declare const fileListOutputSchema: {
205
210
  result: z.ZodArray<z.ZodObject<{
211
+ id: z.ZodOptional<z.ZodString>;
206
212
  name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
207
213
  type: z.ZodOptional<z.ZodNullable<z.ZodString>>;
208
214
  originalName: z.ZodOptional<z.ZodNullable<z.ZodString>>;
209
215
  path: z.ZodOptional<z.ZodNullable<z.ZodString>>;
216
+ size: z.ZodOptional<z.ZodUnknown>;
217
+ createdAt: z.ZodOptional<z.ZodNullable<z.ZodString>>;
218
+ updatedAt: z.ZodOptional<z.ZodNullable<z.ZodString>>;
219
+ metadata: z.ZodOptional<z.ZodUnknown>;
210
220
  }, z.core.$loose>>;
211
221
  };
222
+ export declare const fileOutputSchema: {
223
+ id: z.ZodOptional<z.ZodString>;
224
+ name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
225
+ type: z.ZodOptional<z.ZodNullable<z.ZodString>>;
226
+ originalName: z.ZodOptional<z.ZodNullable<z.ZodString>>;
227
+ path: z.ZodOptional<z.ZodNullable<z.ZodString>>;
228
+ size: z.ZodOptional<z.ZodUnknown>;
229
+ createdAt: z.ZodOptional<z.ZodNullable<z.ZodString>>;
230
+ updatedAt: z.ZodOptional<z.ZodNullable<z.ZodString>>;
231
+ metadata: z.ZodOptional<z.ZodUnknown>;
232
+ };
212
233
  export declare const subscriptionOutputSchema: {
213
234
  messages: z.ZodOptional<z.ZodArray<z.ZodUnknown>>;
214
235
  duration: z.ZodOptional<z.ZodUnknown>;
@@ -1,13 +1,13 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.subscriptionOutputSchema = exports.fileListOutputSchema = exports.fileSchema = exports.armProfileOutputSchema = exports.armProfileListOutputSchema = exports.armProfileSchema = exports.liveviewListOutputSchema = exports.liveviewOutputSchema = exports.liveviewSchema = exports.ulpUserListOutputSchema = exports.ulpUserOutputSchema = exports.ulpUserSchema = exports.userListOutputSchema = exports.userOutputSchema = exports.userSchema = exports.nvrOutputSchema = exports.nvrSchema = exports.infoOutputSchema = void 0;
3
+ exports.subscriptionOutputSchema = exports.fileOutputSchema = exports.fileListOutputSchema = exports.fileSchema = exports.armProfileOutputSchema = exports.armProfileListOutputSchema = exports.armProfileSchema = exports.liveviewListOutputSchema = exports.liveviewOutputSchema = exports.liveviewSchema = exports.ulpUserListOutputSchema = exports.ulpUserOutputSchema = exports.ulpUserSchema = exports.userListOutputSchema = exports.userOutputSchema = exports.userSchema = exports.nvrOutputSchema = exports.nvrSchema = exports.infoOutputSchema = void 0;
4
4
  const zod_1 = require("zod");
5
5
  const common_js_1 = require("./common.js");
6
6
  /**
7
7
  * Schemas below are verified against live UniFi Protect Integration API 7.3.47
8
- * responses (Melrose NVR Pro console, 2026-09-14) where instances exist, and
9
- * against the 7.3.47 published docs otherwise (arm profiles, files — no
10
- * instances on the console). The Integration API surface is much thinner than
8
+ * responses (Melrose NVR Pro console, 2026-09-14) where instances exist —
9
+ * re-verified live on 7.3.70 (2026-10-12) — and against the 7.3.70 published
10
+ * docs otherwise (arm profiles, files — no instances on the console). The Integration API surface is much thinner than
11
11
  * the internal Protect app API — only observed/documented fields are typed;
12
12
  * `.passthrough()` still allows anything extra a firmware/hardware mix adds.
13
13
  */
@@ -30,7 +30,7 @@ const doorbellSettingsSchema = (0, common_js_1.passthroughObject)({
30
30
  defaultMessageText: (0, common_js_1.nullableString)("Default doorbell LCD message"),
31
31
  defaultMessageResetTimeoutMs: (0, common_js_1.unknownField)("Reset timeout in ms (number)"),
32
32
  customMessages: (0, common_js_1.unknownField)("Configured custom LCD messages (array of strings)"),
33
- customImages: (0, common_js_1.unknownField)("Configured custom LCD images (array)"),
33
+ customImages: (0, common_js_1.unknownField)("Configured custom LCD images (array of objects: preview, sprite)"),
34
34
  });
35
35
  exports.nvrSchema = (0, common_js_1.passthroughObject)({
36
36
  id: (0, common_js_1.idField)("NVR ID"),
@@ -38,8 +38,8 @@ exports.nvrSchema = (0, common_js_1.passthroughObject)({
38
38
  name: (0, common_js_1.nullableString)("NVR name"),
39
39
  ...(0, common_js_1.hardwareModelFields)(),
40
40
  mac: (0, common_js_1.nullableString)("MAC address, added in 7.3.x"),
41
- doorbellSettings: doorbellSettingsSchema.optional(),
42
- armMode: armModeSchema.optional(),
41
+ doorbellSettings: doorbellSettingsSchema.nullable().optional(),
42
+ armMode: armModeSchema.nullable().optional(),
43
43
  });
44
44
  // Single object — use the shape directly, NOT a list wrapper.
45
45
  exports.nvrOutputSchema = { ...exports.nvrSchema.shape };
@@ -62,7 +62,7 @@ exports.ulpUserSchema = (0, common_js_1.passthroughObject)({
62
62
  lastName: (0, common_js_1.nullableString)("Last name"),
63
63
  fullName: (0, common_js_1.nullableString)("Full name"),
64
64
  email: (0, common_js_1.nullableString)("Email address, added in 7.3.x. Empty string for service accounts " +
65
- "(verified live 7.3.47)"),
65
+ "(verified live 7.3.70)"),
66
66
  status: (0, common_js_1.nullableString)("Account status, e.g. ACTIVE"),
67
67
  });
68
68
  exports.ulpUserOutputSchema = { ...exports.ulpUserSchema.shape };
@@ -81,12 +81,12 @@ exports.liveviewSchema = (0, common_js_1.passthroughObject)({
81
81
  isGlobal: (0, common_js_1.unknownField)("Whether shared across all users (boolean)"),
82
82
  layout: (0, common_js_1.unknownField)("Grid layout / slot count (number)"),
83
83
  owner: (0, common_js_1.nullableString)("Owning user ID"),
84
- slots: zod_1.z.array(liveviewSlotSchema).optional().describe("Camera slots"),
84
+ slots: zod_1.z.array(liveviewSlotSchema).nullable().optional().describe("Camera slots"),
85
85
  });
86
86
  exports.liveviewOutputSchema = { ...exports.liveviewSchema.shape };
87
87
  exports.liveviewListOutputSchema = (0, common_js_1.listResultSchema)(exports.liveviewSchema);
88
88
  // --- Arm profiles ---
89
- // Fields from the 7.1.83 docs (no instances on the console to verify live).
89
+ // Fields from the 7.3.70 docs (no instances on the console to verify live).
90
90
  // Arm profiles carry no `modelKey`. Nested arrays stay unknownField.
91
91
  exports.armProfileSchema = (0, common_js_1.passthroughObject)({
92
92
  id: (0, common_js_1.idField)("Arm profile ID"),
@@ -102,15 +102,22 @@ exports.armProfileSchema = (0, common_js_1.passthroughObject)({
102
102
  exports.armProfileListOutputSchema = (0, common_js_1.listResultSchema)(exports.armProfileSchema);
103
103
  exports.armProfileOutputSchema = { ...exports.armProfileSchema.shape };
104
104
  // --- Files ---
105
- // Device asset files (only fileType=animations supported). Shape from 7.1.83
106
- // docs: { name, type, originalName, path } — no id/modelKey.
105
+ // Device asset files (only fileType=animations supported). The 7.3.70 docs show
106
+ // { name, type, originalName, path }; live 7.3.70 records (2026-10-12) also
107
+ // carry id (equal to name), size, createdAt, updatedAt and metadata. No modelKey.
107
108
  exports.fileSchema = (0, common_js_1.passthroughObject)({
109
+ id: (0, common_js_1.idField)("File ID (same value as name)"),
108
110
  name: (0, common_js_1.nullableString)("Stored file name (server-generated)"),
109
111
  type: (0, common_js_1.nullableString)('Asset file type, e.g. "animations"'),
110
- originalName: (0, common_js_1.nullableString)("Original uploaded file name"),
112
+ originalName: (0, common_js_1.nullableString)('Uploaded file name; Protect appends the stored extension, e.g. "welcome.gif.png"'),
111
113
  path: (0, common_js_1.nullableString)("Server-side storage path"),
114
+ size: (0, common_js_1.unknownField)("File size in bytes (number)"),
115
+ createdAt: (0, common_js_1.nullableString)("Creation time (ISO 8601)"),
116
+ updatedAt: (0, common_js_1.nullableString)("Last update time (ISO 8601)"),
117
+ metadata: (0, common_js_1.unknownField)("Asset metadata (object, empty in observed records)"),
112
118
  });
113
119
  exports.fileListOutputSchema = (0, common_js_1.listResultSchema)(exports.fileSchema);
120
+ exports.fileOutputSchema = { ...exports.fileSchema.shape };
114
121
  // --- Subscriptions ---
115
122
  exports.subscriptionOutputSchema = {
116
123
  messages: zod_1.z
@@ -2,7 +2,7 @@
2
2
  * Point-of-sale event ingestion, added in UniFi Protect 7.3.x
3
3
  * (POST /v1/pos/cameras/{id}/transactions).
4
4
  *
5
- * Shape taken from the 7.3.47 published docs with the response sample expanded
5
+ * Shape taken from the 7.3.70 published docs with the response sample expanded
6
6
  * — there is no POS-capable register on the available console, so this is
7
7
  * doc-verified rather than live-verified. `created` stays `unknownField()`
8
8
  * because we have not observed the boolean live; `eventId` publishes the string
@@ -6,7 +6,7 @@ const common_js_1 = require("./common.js");
6
6
  * Point-of-sale event ingestion, added in UniFi Protect 7.3.x
7
7
  * (POST /v1/pos/cameras/{id}/transactions).
8
8
  *
9
- * Shape taken from the 7.3.47 published docs with the response sample expanded
9
+ * Shape taken from the 7.3.70 published docs with the response sample expanded
10
10
  * — there is no POS-capable register on the available console, so this is
11
11
  * doc-verified rather than live-verified. `created` stays `unknownField()`
12
12
  * because we have not observed the boolean live; `eventId` publishes the string
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { ProtectClient } from "../client.js";
3
+ export declare function registerAlarmManagerTools(server: McpServer, client: ProtectClient, readOnly: boolean): void;
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerAlarmManagerTools = registerAlarmManagerTools;
4
+ const zod_1 = require("zod");
5
+ const responses_js_1 = require("../utils/responses.js");
6
+ const safety_js_1 = require("../utils/safety.js");
7
+ const url_js_1 = require("../utils/url.js");
8
+ function registerAlarmManagerTools(server, client, readOnly) {
9
+ if (readOnly)
10
+ return;
11
+ server.registerTool("protect_trigger_alarm_webhook", {
12
+ description: "Trigger an alarm manager webhook by ID. This fires an external alarm action.",
13
+ inputSchema: {
14
+ id: zod_1.z
15
+ .string()
16
+ .min(1)
17
+ .describe("User-defined webhook ID matching an alarm manager configuration"),
18
+ confirm: zod_1.z
19
+ .literal(true)
20
+ .describe("Must be true to confirm triggering the alarm webhook"),
21
+ dryRun: zod_1.z
22
+ .boolean()
23
+ .optional()
24
+ .describe("If true, return what would happen without making changes"),
25
+ },
26
+ annotations: safety_js_1.DESTRUCTIVE,
27
+ }, async ({ id, confirm, dryRun }) => {
28
+ const denied = (0, safety_js_1.requireConfirmation)(confirm, "trigger the alarm webhook");
29
+ if (denied)
30
+ return denied;
31
+ try {
32
+ if (dryRun) {
33
+ return (0, safety_js_1.formatDryRun)("POST", (0, url_js_1.safePath) `/alarm-manager/webhook/${id}`);
34
+ }
35
+ await client.post((0, url_js_1.safePath) `/alarm-manager/webhook/${id}`);
36
+ return (0, responses_js_1.formatSuccess)({ triggered: true, webhookId: id });
37
+ }
38
+ catch (err) {
39
+ return (0, responses_js_1.formatError)(err);
40
+ }
41
+ });
42
+ }