@owine/unifi-protect-mcp 2.7.5 → 2.8.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.
@@ -5,40 +5,123 @@ exports.itemOutputShape = itemOutputShape;
5
5
  exports.listOutputShape = listOutputShape;
6
6
  const common_js_1 = require("./common.js");
7
7
  /**
8
- * UNVERIFIED DOMAINS.
8
+ * Device schemas for the config-driven Protect device types.
9
9
  *
10
- * The verification console (Protect 7.1.60) has zero instances of any of these
11
- * device types (lights, sensors, chimes, viewers, sirens, fobs, relays,
12
- * speakers, bridges, link-stations, alarm-hubs), so their real Integration API
13
- * response shapes could not be observed.
10
+ * Field sets are derived from the UniFi Protect Integration API 7.1.83 docs
11
+ * (per-type response samples). Chimes were additionally verified against live
12
+ * console responses (2026-06-20); 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
15
+ * the field for the LLM without risking SDK output-validation failures on a
16
+ * shape we have not observed live. `.passthrough()` lets any extra field flow.
14
17
  *
15
- * Per the unifi-api-update skill's "soften unverifiable claims" rule, we do NOT
16
- * assert device-specific field lists here. The schema only types the universal
17
- * identity fields seen consistently across every *verified* Protect resource
18
- * (camera, nvr, user): id, modelKey, name, mac, state. `.passthrough()` lets
19
- * every real (unverified) field through without rejection.
20
- *
21
- * When a console with these devices is available, capture live samples and
22
- * tighten these per the skill (Step 4/5).
18
+ * Convention (see schemas/common.ts): only `id` is strictly typed; strings use
19
+ * `nullableString()` (API returns explicit null for absent strings); booleans,
20
+ * numbers, and unverified nested structures use `unknownField()`.
23
21
  */
24
- const unverifiedDeviceSchema = () => (0, common_js_1.passthroughObject)({
22
+ /** Identity fields common to every Protect device resource. */
23
+ const deviceIdentity = () => ({
25
24
  id: (0, common_js_1.idField)("Device ID"),
26
25
  modelKey: (0, common_js_1.nullableString)("Resource kind"),
27
26
  name: (0, common_js_1.nullableString)("Device name"),
28
27
  mac: (0, common_js_1.nullableString)("MAC address"),
29
28
  state: (0, common_js_1.nullableString)("CONNECTED | DISCONNECTED | ..."),
30
29
  });
31
- exports.lightSchema = unverifiedDeviceSchema();
32
- exports.sensorSchema = unverifiedDeviceSchema();
33
- exports.chimeSchema = unverifiedDeviceSchema();
34
- exports.viewerSchema = unverifiedDeviceSchema();
35
- exports.sirenSchema = unverifiedDeviceSchema();
36
- exports.fobSchema = unverifiedDeviceSchema();
37
- exports.relaySchema = unverifiedDeviceSchema();
38
- exports.speakerSchema = unverifiedDeviceSchema();
39
- exports.bridgeSchema = unverifiedDeviceSchema();
40
- exports.linkStationSchema = unverifiedDeviceSchema();
41
- exports.alarmHubSchema = unverifiedDeviceSchema();
30
+ exports.lightSchema = (0, common_js_1.passthroughObject)({
31
+ ...deviceIdentity(),
32
+ lightModeSettings: (0, common_js_1.unknownField)("Activation settings (object: mode, enableAt)"),
33
+ lightDeviceSettings: (0, common_js_1.unknownField)("Hardware settings (object: isIndicatorEnabled, pirDuration, pirSensitivity, ledLevel)"),
34
+ isDark: (0, common_js_1.unknownField)("Whether it is currently dark out (boolean)"),
35
+ isLightOn: (0, common_js_1.unknownField)("Whether the light is currently on (boolean)"),
36
+ isLightForceEnabled: (0, common_js_1.unknownField)("Main LED force-enabled (boolean)"),
37
+ lastMotion: (0, common_js_1.unknownField)("Last motion timestamp in epoch ms (number)"),
38
+ isPirMotionDetected: (0, common_js_1.unknownField)("PIR motion currently detected (boolean)"),
39
+ camera: (0, common_js_1.nullableString)("Paired camera ID"),
40
+ });
41
+ exports.sensorSchema = (0, common_js_1.passthroughObject)({
42
+ ...deviceIdentity(),
43
+ mountType: (0, common_js_1.nullableString)('Mount type, e.g. "door", "leak", "garage"'),
44
+ batteryStatus: (0, common_js_1.unknownField)("Battery status (object: percentage, isLow)"),
45
+ stats: (0, common_js_1.unknownField)("Environmental stats (object: light, humidity, temperature)"),
46
+ lightSettings: (0, common_js_1.unknownField)("Light threshold settings (object)"),
47
+ humiditySettings: (0, common_js_1.unknownField)("Humidity threshold settings (object)"),
48
+ temperatureSettings: (0, common_js_1.unknownField)("Temperature threshold settings (object)"),
49
+ isOpened: (0, common_js_1.unknownField)("Open/close contact state (boolean)"),
50
+ openStatusChangedAt: (0, common_js_1.unknownField)("Open-status change timestamp in epoch ms (number)"),
51
+ isMotionDetected: (0, common_js_1.unknownField)("Motion currently detected (boolean)"),
52
+ motionDetectedAt: (0, common_js_1.unknownField)("Last motion timestamp in epoch ms (number)"),
53
+ motionSettings: (0, common_js_1.unknownField)("Motion detection settings (object)"),
54
+ glassBreakSettings: (0, common_js_1.unknownField)("Glass-break detection settings (object)"),
55
+ scheduleMode: (0, common_js_1.nullableString)('Schedule mode: "always" | "when_armed"'),
56
+ armProfileIds: (0, common_js_1.unknownField)("Arm profile IDs this sensor belongs to (array of strings)"),
57
+ hasCustomSensitivityWhenArmed: (0, common_js_1.unknownField)("Custom armed sensitivity enabled (boolean)"),
58
+ alarmTriggeredAt: (0, common_js_1.unknownField)("Last alarm timestamp in epoch ms (number)"),
59
+ alarmSettings: (0, common_js_1.unknownField)("Alarm settings (object: isEnabled)"),
60
+ leakDetectedAt: (0, common_js_1.unknownField)("Last leak timestamp in epoch ms (number)"),
61
+ externalLeakDetectedAt: (0, common_js_1.unknownField)("Last external-leak timestamp in epoch ms (number)"),
62
+ leakSettings: (0, common_js_1.unknownField)("Leak detection settings (object)"),
63
+ tamperingDetectedAt: (0, common_js_1.unknownField)("Last tampering timestamp in epoch ms (number)"),
64
+ wirelessConnectionState: (0, common_js_1.unknownField)("Wireless link state (object: signalState, batteryStatus, bridge)"),
65
+ });
66
+ // Verified live (7.1.83): chimes return cameraIds[] and ringSettings[] whose
67
+ // items are { cameraId, volume, ringtoneId, repeatTimes }.
68
+ exports.chimeSchema = (0, common_js_1.passthroughObject)({
69
+ ...deviceIdentity(),
70
+ cameraIds: (0, common_js_1.unknownField)("Paired camera IDs (array of strings)"),
71
+ ringSettings: (0, common_js_1.unknownField)("Per-camera ring config (array of objects: cameraId, volume, ringtoneId, repeatTimes)"),
72
+ });
73
+ exports.viewerSchema = (0, common_js_1.passthroughObject)({
74
+ ...deviceIdentity(),
75
+ liveview: (0, common_js_1.nullableString)("Assigned live view ID, or null"),
76
+ streamLimit: (0, common_js_1.unknownField)("Max concurrent streams (number)"),
77
+ });
78
+ exports.sirenSchema = (0, common_js_1.passthroughObject)({
79
+ ...deviceIdentity(),
80
+ volume: (0, common_js_1.unknownField)("Siren volume (number)"),
81
+ ledSettings: (0, common_js_1.unknownField)("LED settings (object: isEnabled)"),
82
+ sirenStatus: (0, common_js_1.unknownField)("Current siren status (object: isActive, activatedAt, duration)"),
83
+ connectionType: (0, common_js_1.nullableString)('Connection type, e.g. "lora"'),
84
+ wirelessConnectionState: (0, common_js_1.unknownField)("Wireless link state (object)"),
85
+ });
86
+ exports.fobSchema = (0, common_js_1.passthroughObject)({
87
+ ...deviceIdentity(),
88
+ awayState: (0, common_js_1.nullableString)('Away state, e.g. "ONLINE"'),
89
+ buttonLabels: (0, common_js_1.nullableString)('Button label preset, e.g. "securityActions"'),
90
+ featureFlags: (0, common_js_1.unknownField)("Feature flags (object: buttons[])"),
91
+ wirelessConnectionState: (0, common_js_1.unknownField)("Wireless link state (object)"),
92
+ });
93
+ exports.relaySchema = (0, common_js_1.passthroughObject)({
94
+ ...deviceIdentity(),
95
+ ledSettings: (0, common_js_1.unknownField)("LED settings (object: isEnabled)"),
96
+ outputs: (0, common_js_1.unknownField)("Output channels (array of objects)"),
97
+ inputs: (0, common_js_1.unknownField)("Input channels (array of objects)"),
98
+ wirelessConnectionState: (0, common_js_1.unknownField)("Wireless link state (object)"),
99
+ });
100
+ exports.speakerSchema = (0, common_js_1.passthroughObject)({
101
+ ...deviceIdentity(),
102
+ volume: (0, common_js_1.unknownField)("Speaker volume (number)"),
103
+ micVolume: (0, common_js_1.unknownField)("Microphone volume (number)"),
104
+ isMicEnabled: (0, common_js_1.unknownField)("Microphone enabled (boolean)"),
105
+ speakerState: (0, common_js_1.unknownField)("Speaker state (object: status, mode)"),
106
+ featureFlags: (0, common_js_1.unknownField)("Feature flags (object: hasMic)"),
107
+ });
108
+ exports.bridgeSchema = (0, common_js_1.passthroughObject)({
109
+ ...deviceIdentity(),
110
+ platform: (0, common_js_1.nullableString)('Hardware platform, e.g. "mt7621"'),
111
+ clients: (0, common_js_1.unknownField)("Connected client MACs (array of strings)"),
112
+ maxClients: (0, common_js_1.unknownField)("Max client capacity (number)"),
113
+ });
114
+ // Link stations and alarm hubs share one shape (both report modelKey
115
+ // "linkstation"); /alarm-hubs is the same resource filtered to alarm hubs.
116
+ const linkStationLikeSchema = () => (0, common_js_1.passthroughObject)({
117
+ ...deviceIdentity(),
118
+ isAlarmHub: (0, common_js_1.unknownField)("Whether this device is an alarm hub (boolean)"),
119
+ ledSettings: (0, common_js_1.unknownField)("LED settings (object: isEnabled)"),
120
+ lastEvent: (0, common_js_1.unknownField)("Last event timestamp in epoch ms (number)"),
121
+ alarmHub: (0, common_js_1.unknownField)("Alarm hub status (object: armed, battery, connector, cover, output, input, …)"),
122
+ });
123
+ exports.linkStationSchema = linkStationLikeSchema();
124
+ exports.alarmHubSchema = linkStationLikeSchema();
42
125
  exports.DEVICE_SCHEMAS = {
43
126
  light: exports.lightSchema,
44
127
  sensor: exports.sensorSchema,
@@ -1,10 +1,11 @@
1
1
  import { z } from "zod";
2
2
  /**
3
- * Schemas below are verified against live UniFi Protect Integration API 7.1.60
4
- * responses (Melrose NVR Pro console, 2026-05-15). The Integration API surface
5
- * is much thinner than the internal Protect app API — only the fields observed
6
- * in real responses are typed; `.passthrough()` still allows anything extra a
7
- * different firmware/hardware mix might add.
3
+ * Schemas below are verified against live UniFi Protect Integration API 7.1.83
4
+ * responses (Melrose NVR Pro console, 2026-06-20) where instances exist, and
5
+ * against the 7.1.83 published docs otherwise (arm profiles, files — no
6
+ * instances on the console). The Integration API surface is much thinner than
7
+ * the internal Protect app API — only observed/documented fields are typed;
8
+ * `.passthrough()` still allows anything extra a firmware/hardware mix adds.
8
9
  */
9
10
  export declare const infoOutputSchema: {
10
11
  applicationVersion: z.ZodOptional<z.ZodNullable<z.ZodString>>;
@@ -21,6 +22,7 @@ export declare const nvrSchema: z.ZodObject<{
21
22
  }, z.core.$loose>>;
22
23
  armMode: z.ZodOptional<z.ZodObject<{
23
24
  status: z.ZodOptional<z.ZodNullable<z.ZodString>>;
25
+ armProfileId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
24
26
  armedAt: z.ZodOptional<z.ZodUnknown>;
25
27
  willBeArmedAt: z.ZodOptional<z.ZodUnknown>;
26
28
  breachDetectedAt: z.ZodOptional<z.ZodUnknown>;
@@ -41,6 +43,7 @@ export declare const nvrOutputSchema: {
41
43
  }, z.core.$loose>>;
42
44
  armMode: z.ZodOptional<z.ZodObject<{
43
45
  status: z.ZodOptional<z.ZodNullable<z.ZodString>>;
46
+ armProfileId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
44
47
  armedAt: z.ZodOptional<z.ZodUnknown>;
45
48
  willBeArmedAt: z.ZodOptional<z.ZodUnknown>;
46
49
  breachDetectedAt: z.ZodOptional<z.ZodUnknown>;
@@ -150,31 +153,51 @@ export declare const liveviewListOutputSchema: {
150
153
  };
151
154
  export declare const armProfileSchema: z.ZodObject<{
152
155
  id: z.ZodOptional<z.ZodString>;
153
- modelKey: z.ZodOptional<z.ZodNullable<z.ZodString>>;
154
156
  name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
157
+ automations: z.ZodOptional<z.ZodUnknown>;
158
+ creator: z.ZodOptional<z.ZodNullable<z.ZodString>>;
159
+ schedules: z.ZodOptional<z.ZodUnknown>;
160
+ recordEverything: z.ZodOptional<z.ZodUnknown>;
161
+ activationDelay: z.ZodOptional<z.ZodUnknown>;
162
+ createdAt: z.ZodOptional<z.ZodUnknown>;
163
+ updatedAt: z.ZodOptional<z.ZodUnknown>;
155
164
  }, z.core.$loose>;
156
165
  export declare const armProfileListOutputSchema: {
157
166
  result: z.ZodArray<z.ZodObject<{
158
167
  id: z.ZodOptional<z.ZodString>;
159
- modelKey: z.ZodOptional<z.ZodNullable<z.ZodString>>;
160
168
  name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
169
+ automations: z.ZodOptional<z.ZodUnknown>;
170
+ creator: z.ZodOptional<z.ZodNullable<z.ZodString>>;
171
+ schedules: z.ZodOptional<z.ZodUnknown>;
172
+ recordEverything: z.ZodOptional<z.ZodUnknown>;
173
+ activationDelay: z.ZodOptional<z.ZodUnknown>;
174
+ createdAt: z.ZodOptional<z.ZodUnknown>;
175
+ updatedAt: z.ZodOptional<z.ZodUnknown>;
161
176
  }, z.core.$loose>>;
162
177
  };
163
178
  export declare const armProfileOutputSchema: {
164
179
  id: z.ZodOptional<z.ZodString>;
165
- modelKey: z.ZodOptional<z.ZodNullable<z.ZodString>>;
166
180
  name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
181
+ automations: z.ZodOptional<z.ZodUnknown>;
182
+ creator: z.ZodOptional<z.ZodNullable<z.ZodString>>;
183
+ schedules: z.ZodOptional<z.ZodUnknown>;
184
+ recordEverything: z.ZodOptional<z.ZodUnknown>;
185
+ activationDelay: z.ZodOptional<z.ZodUnknown>;
186
+ createdAt: z.ZodOptional<z.ZodUnknown>;
187
+ updatedAt: z.ZodOptional<z.ZodUnknown>;
167
188
  };
168
189
  export declare const fileSchema: z.ZodObject<{
169
- id: z.ZodOptional<z.ZodString>;
170
- modelKey: z.ZodOptional<z.ZodNullable<z.ZodString>>;
171
190
  name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
191
+ type: z.ZodOptional<z.ZodNullable<z.ZodString>>;
192
+ originalName: z.ZodOptional<z.ZodNullable<z.ZodString>>;
193
+ path: z.ZodOptional<z.ZodNullable<z.ZodString>>;
172
194
  }, z.core.$loose>;
173
195
  export declare const fileListOutputSchema: {
174
196
  result: z.ZodArray<z.ZodObject<{
175
- id: z.ZodOptional<z.ZodString>;
176
- modelKey: z.ZodOptional<z.ZodNullable<z.ZodString>>;
177
197
  name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
198
+ type: z.ZodOptional<z.ZodNullable<z.ZodString>>;
199
+ originalName: z.ZodOptional<z.ZodNullable<z.ZodString>>;
200
+ path: z.ZodOptional<z.ZodNullable<z.ZodString>>;
178
201
  }, z.core.$loose>>;
179
202
  };
180
203
  export declare const subscriptionOutputSchema: {
@@ -4,11 +4,12 @@ exports.subscriptionOutputSchema = exports.fileListOutputSchema = exports.fileSc
4
4
  const zod_1 = require("zod");
5
5
  const common_js_1 = require("./common.js");
6
6
  /**
7
- * Schemas below are verified against live UniFi Protect Integration API 7.1.60
8
- * responses (Melrose NVR Pro console, 2026-05-15). The Integration API surface
9
- * is much thinner than the internal Protect app API — only the fields observed
10
- * in real responses are typed; `.passthrough()` still allows anything extra a
11
- * different firmware/hardware mix might add.
7
+ * Schemas below are verified against live UniFi Protect Integration API 7.1.83
8
+ * responses (Melrose NVR Pro console, 2026-06-20) where instances exist, and
9
+ * against the 7.1.83 published docs otherwise (arm profiles, files — no
10
+ * instances on the console). The Integration API surface is much thinner than
11
+ * the internal Protect app API — only observed/documented fields are typed;
12
+ * `.passthrough()` still allows anything extra a firmware/hardware mix adds.
12
13
  */
13
14
  // --- System ---
14
15
  exports.infoOutputSchema = {
@@ -16,7 +17,8 @@ exports.infoOutputSchema = {
16
17
  };
17
18
  // GET /nvrs returns a SINGLE nvr object (not an array, despite the tool name).
18
19
  const armModeSchema = (0, common_js_1.passthroughObject)({
19
- status: (0, common_js_1.nullableString)("disabled | armed | ..."),
20
+ status: (0, common_js_1.nullableString)("disabled | arming | armed | ..."),
21
+ armProfileId: (0, common_js_1.nullableString)("Active arm profile ID (present when arming/armed)"),
20
22
  armedAt: (0, common_js_1.unknownField)("Unix epoch ms when armed, or null (number|null)"),
21
23
  willBeArmedAt: (0, common_js_1.unknownField)("Scheduled arm time, or null (number|null)"),
22
24
  breachDetectedAt: (0, common_js_1.unknownField)("Unix epoch ms of last breach, or null"),
@@ -80,21 +82,29 @@ exports.liveviewSchema = (0, common_js_1.passthroughObject)({
80
82
  exports.liveviewOutputSchema = { ...exports.liveviewSchema.shape };
81
83
  exports.liveviewListOutputSchema = (0, common_js_1.listResultSchema)(exports.liveviewSchema);
82
84
  // --- Arm profiles ---
83
- // No instances on the verification console — fields unverified. Kept minimal:
84
- // only the identity field is typed; everything else flows via passthrough.
85
+ // Fields from the 7.1.83 docs (no instances on the console to verify live).
86
+ // Arm profiles carry no `modelKey`. Nested arrays stay unknownField.
85
87
  exports.armProfileSchema = (0, common_js_1.passthroughObject)({
86
88
  id: (0, common_js_1.idField)("Arm profile ID"),
87
- modelKey: (0, common_js_1.nullableString)("Resource kind"),
88
89
  name: (0, common_js_1.nullableString)("Arm profile name"),
90
+ automations: (0, common_js_1.unknownField)("Associated automation IDs (array of strings)"),
91
+ creator: (0, common_js_1.nullableString)("ID of the user who created the profile"),
92
+ schedules: (0, common_js_1.unknownField)("Arm schedules (array of objects)"),
93
+ recordEverything: (0, common_js_1.unknownField)("Record everything while active (boolean)"),
94
+ activationDelay: (0, common_js_1.unknownField)("Activation delay in ms: 0 | 60000 | 300000 | 600000 (number)"),
95
+ createdAt: (0, common_js_1.unknownField)("Creation timestamp in epoch ms (number)"),
96
+ updatedAt: (0, common_js_1.unknownField)("Last update timestamp in epoch ms (number)"),
89
97
  });
90
98
  exports.armProfileListOutputSchema = (0, common_js_1.listResultSchema)(exports.armProfileSchema);
91
99
  exports.armProfileOutputSchema = { ...exports.armProfileSchema.shape };
92
100
  // --- Files ---
93
- // `protect_list_files` (only fileType=animations supported) — unverified shape.
101
+ // Device asset files (only fileType=animations supported). Shape from 7.1.83
102
+ // docs: { name, type, originalName, path } — no id/modelKey.
94
103
  exports.fileSchema = (0, common_js_1.passthroughObject)({
95
- id: (0, common_js_1.idField)("File ID"),
96
- modelKey: (0, common_js_1.nullableString)("Resource kind"),
97
- name: (0, common_js_1.nullableString)("File name"),
104
+ name: (0, common_js_1.nullableString)("Stored file name (server-generated)"),
105
+ type: (0, common_js_1.nullableString)('Asset file type, e.g. "animations"'),
106
+ originalName: (0, common_js_1.nullableString)("Original uploaded file name"),
107
+ path: (0, common_js_1.nullableString)("Server-side storage path"),
98
108
  });
99
109
  exports.fileListOutputSchema = (0, common_js_1.listResultSchema)(exports.fileSchema);
100
110
  // --- Subscriptions ---
@@ -9,7 +9,7 @@ const misc_js_1 = require("../schemas/misc.js");
9
9
  const ACTIVATION_DELAY_VALUES = [0, 60000, 300000, 600000];
10
10
  function registerArmProfileTools(server, client, readOnly) {
11
11
  server.registerTool("protect_list_arm_profiles", {
12
- description: "List all arm profiles (only available when using the local alarm manager — the standalone NVR alarm system, not Protect cloud alerts). Returns array. Response shape is NOT verified against Protect 7.1.60 (no arm profiles on the reference console); expect at least id, modelKey, name plus profile configuration fields — inspect a live response to confirm.",
12
+ description: "List all arm profiles (only available when using the local alarm manager — the standalone NVR alarm system, not Protect cloud alerts). Returns array; each profile (7.1.83 docs): id, name, automations[], creator, schedules[], recordEverything, activationDelay (0 | 60000 | 300000 | 600000), createdAt, updatedAt.",
13
13
  outputSchema: misc_js_1.armProfileListOutputSchema,
14
14
  annotations: safety_js_1.READ_ONLY,
15
15
  }, async () => {
@@ -9,7 +9,7 @@ const cameras_js_1 = require("../schemas/cameras.js");
9
9
  function registerCameraTools(server, client, readOnly) {
10
10
  // --- Read-only tools (always registered) ---
11
11
  server.registerTool("protect_list_cameras", {
12
- description: "List all cameras managed by UniFi Protect. Returns array; each camera includes (Integration API 7.1.60-verified fields): id, mac, name, modelKey, state (CONNECTED/DISCONNECTED), activePatrolSlot, hasPackageCamera, hdrType, isMicEnabled, micVolume, videoMode, featureFlags (hasHdr, hasMic, hasSpeaker, hasLedStatus, smartDetectTypes[], smartDetectAudioTypes[], videoModes[], supportFullHdSnapshot), lcdMessage, ledSettings (isEnabled, floodLed, welcomeLed), osdSettings (isNameEnabled, isDateEnabled, isLogoEnabled, isDebugEnabled, overlayLocation), smartDetectSettings (objectTypes[], audioTypes[]). The Integration API does NOT expose recording state, motion timestamps, connection/last-seen, firmware, host, or per-channel stream config.",
12
+ description: "List all cameras managed by UniFi Protect. Returns array; each camera includes (Integration API 7.1.83-verified fields): id, mac, name, modelKey, state (CONNECTED/DISCONNECTED), activePatrolSlot, hasPackageCamera, hdrType, isMicEnabled, micVolume, videoMode, featureFlags (hasHdr, hasMic, hasSpeaker, hasLedStatus, smartDetectTypes[], smartDetectAudioTypes[], videoModes[], supportFullHdSnapshot), lcdMessage (type, resetAt, text), ledSettings (isEnabled, floodLed, welcomeLed), osdSettings (isNameEnabled, isDateEnabled, isLogoEnabled, isDebugEnabled, overlayLocation), smartDetectSettings (objectTypes[], audioTypes[]). The Integration API does NOT expose recording state, motion timestamps, connection/last-seen, firmware, host, or per-channel stream config.",
13
13
  outputSchema: cameras_js_1.cameraListOutputSchema,
14
14
  annotations: safety_js_1.READ_ONLY,
15
15
  }, async () => {
@@ -22,7 +22,7 @@ function registerCameraTools(server, client, readOnly) {
22
22
  }
23
23
  });
24
24
  server.registerTool("protect_get_camera", {
25
- description: "Get details for a specific camera by ID. The Protect Integration API returns the SAME field set as protect_list_cameras entries (id, mac, name, modelKey, state, activePatrolSlot, hasPackageCamera, hdrType, isMicEnabled, micVolume, videoMode, featureFlags, lcdMessage, ledSettings, osdSettings, smartDetectSettings) — there is no extended/by-id-only payload. Recording state, motion events, zones, and channel/RTSP config are NOT exposed by this API surface.",
25
+ description: "Get details for a specific camera by ID. The Protect Integration API returns the SAME field set as protect_list_cameras entries (id, mac, name, modelKey, state, activePatrolSlot, hasPackageCamera, hdrType, isMicEnabled, micVolume, videoMode, featureFlags, lcdMessage, ledSettings, osdSettings, smartDetectSettings) — there is no extended/by-id-only payload (confirmed live on 7.1.83). Recording state, motion events, zones, and channel/RTSP config are NOT exposed by this API surface.",
26
26
  inputSchema: { id: zod_1.z.string().describe("Camera ID") },
27
27
  outputSchema: cameras_js_1.cameraOutputSchema,
28
28
  annotations: safety_js_1.READ_ONLY,
@@ -36,19 +36,29 @@ function registerCameraTools(server, client, readOnly) {
36
36
  }
37
37
  });
38
38
  server.registerTool("protect_get_snapshot", {
39
- description: "Get a JPEG snapshot from a camera. Returns a base64-encoded image/jpeg (rendered directly by MCP clients). Use highQuality=true for full-resolution capture from the camera's main channel; otherwise a low-res thumbnail is returned.",
39
+ description: "Get a JPEG snapshot from a camera. Returns a base64-encoded image/jpeg (rendered directly by MCP clients). Use highQuality=true for full-resolution capture; set channel=package to capture from the secondary package camera on doorbells with hasPackageCamera=true.",
40
40
  inputSchema: {
41
41
  id: zod_1.z.string().describe("Camera ID"),
42
42
  highQuality: zod_1.z
43
43
  .boolean()
44
44
  .optional()
45
45
  .describe("If true, request a high-quality snapshot"),
46
+ channel: zod_1.z
47
+ .enum(["main", "package"])
48
+ .optional()
49
+ .describe('Camera channel to capture. Use "package" for cameras with hasPackageCamera=true (defaults to main)'),
46
50
  },
47
51
  annotations: safety_js_1.READ_ONLY,
48
- }, async ({ id, highQuality }) => {
52
+ }, async ({ id, highQuality, channel }) => {
49
53
  try {
50
- const url = highQuality
51
- ? (0, url_js_1.safePath) `/cameras/${id}/snapshot?highQuality=true`
54
+ const params = new URLSearchParams();
55
+ if (highQuality)
56
+ params.set("highQuality", "true");
57
+ if (channel)
58
+ params.set("channel", channel);
59
+ const query = params.toString();
60
+ const url = query
61
+ ? (0, url_js_1.safePath) `/cameras/${id}/snapshot` + `?${query}`
52
62
  : (0, url_js_1.safePath) `/cameras/${id}/snapshot`;
53
63
  const { data, mimeType } = await client.getBinary(url);
54
64
  return {
@@ -88,7 +98,7 @@ function registerCameraTools(server, client, readOnly) {
88
98
  id: zod_1.z.string().describe("Camera ID"),
89
99
  settings: zod_1.z
90
100
  .record(zod_1.z.string(), zod_1.z.unknown())
91
- .describe("Partial camera settings to update. Known fields: name (string), osdSettings (object), ledSettings (object), lcdMessage (object), videoMode (string: \"default\" | \"highFps\" | \"slo-mo\"), hdrType (string: \"off\" | \"normal\" | \"always\"), micVolume (number 0-100), smartDetectSettings (object)"),
101
+ .describe("Partial camera settings to update. Known fields: name (string), osdSettings (object), ledSettings (object), lcdMessage (object: type, resetAt, text), videoMode (string: \"default\" | \"highFps\" | \"sport\" | \"slowShutter\" | \"lprReflex\" | \"lprNoneReflex\"), hdrType (string: \"auto\" | \"on\" | \"off\"), micVolume (number 0-100), smartDetectSettings (object)"),
92
102
  dryRun: zod_1.z
93
103
  .boolean()
94
104
  .optional()
@@ -162,7 +172,7 @@ function registerCameraTools(server, client, readOnly) {
162
172
  }
163
173
  });
164
174
  server.registerTool("protect_create_talkback", {
165
- description: "Create a talkback (two-way audio) session for a camera",
175
+ description: "Create a talkback (two-way audio) session for a camera. Returns: url, codec, samplingRate, bitsPerSample (the audio config for encoding outbound audio).",
166
176
  inputSchema: {
167
177
  id: zod_1.z.string().describe("Camera ID"),
168
178
  dryRun: zod_1.z
@@ -7,6 +7,10 @@ const responses_js_1 = require("../utils/responses.js");
7
7
  const safety_js_1 = require("../utils/safety.js");
8
8
  const url_js_1 = require("../utils/url.js");
9
9
  const devices_js_1 = require("../schemas/devices.js");
10
+ // Field lists below are from the UniFi Protect Integration API 7.1.83 docs.
11
+ // Chimes are additionally verified against live console responses; the rest
12
+ // have no instances on the available console (top-level fields documented,
13
+ // nested shapes intentionally left opaque in the output schema).
10
14
  const DEVICES = [
11
15
  {
12
16
  urlPath: "lights",
@@ -14,31 +18,31 @@ const DEVICES = [
14
18
  plural: "lights",
15
19
  label: "Light",
16
20
  hint: "Known fields: name (string), isLightForceEnabled (boolean), lightModeSettings (object with mode, enableAt), lightDeviceSettings (object with isIndicatorEnabled, pirDuration, pirSensitivity, ledLevel)",
17
- returns: "id, modelKey, name, mac, state (universal identity fields). Protect Integration API 7.1.60 returns a thin object; additional device-specific fields are NOT verified (no instances on reference console) — inspect a live response before relying on them",
21
+ returns: "id, modelKey, name, mac, state, lightModeSettings (mode, enableAt), lightDeviceSettings (isIndicatorEnabled, pirDuration, pirSensitivity, ledLevel), isDark, isLightOn, isLightForceEnabled, lastMotion, isPirMotionDetected, camera (7.1.83 docs)",
18
22
  },
19
23
  {
20
24
  urlPath: "sensors",
21
25
  singular: "sensor",
22
26
  plural: "sensors",
23
27
  label: "Sensor",
24
- hint: "Known fields: name (string), motionSettings (object), humiditySettings (object), temperatureSettings (object), lightSettings (object), alarmSettings (object)",
25
- returns: "id, modelKey, name, mac, state (universal identity fields). Protect Integration API 7.1.60 returns a thin object; additional device-specific fields are NOT verified (no instances on reference console) — inspect a live response before relying on them",
28
+ hint: "Known fields: name (string), motionSettings (object), humiditySettings (object), temperatureSettings (object), lightSettings (object), alarmSettings (object), glassBreakSettings (object), leakSettings (object), scheduleMode (\"always\" | \"when_armed\"), armProfileIds (array of up to 32 strings), hasCustomSensitivityWhenArmed (boolean)",
29
+ returns: "id, modelKey, name, mac, state, mountType, batteryStatus (percentage, isLow), stats (light, humidity, temperature), lightSettings, humiditySettings, temperatureSettings, isOpened, openStatusChangedAt, isMotionDetected, motionDetectedAt, motionSettings, glassBreakSettings, scheduleMode, armProfileIds, hasCustomSensitivityWhenArmed, alarmTriggeredAt, alarmSettings, leakDetectedAt, externalLeakDetectedAt, leakSettings, tamperingDetectedAt, wirelessConnectionState (7.1.83 docs)",
26
30
  },
27
31
  {
28
32
  urlPath: "chimes",
29
33
  singular: "chime",
30
34
  plural: "chimes",
31
35
  label: "Chime",
32
- hint: "Known fields: name (string), cameraIds (array of camera IDs linked to the chime), ringSettings (array of ring tone configurations)",
33
- returns: "id, modelKey, name, mac, state (universal identity fields). Protect Integration API 7.1.60 returns a thin object; additional device-specific fields are NOT verified (no instances on reference console) — inspect a live response before relying on them",
36
+ hint: "Known fields: name (string), cameraIds (array of camera IDs linked to the chime), ringSettings (array of objects: cameraId, volume, ringtoneId, repeatTimes)",
37
+ returns: "id, modelKey, name, mac, state, cameraIds (array of camera IDs), ringSettings (array of objects: cameraId, volume, ringtoneId, repeatTimes) — verified live 7.1.83",
34
38
  },
35
39
  {
36
40
  urlPath: "viewers",
37
41
  singular: "viewer",
38
42
  plural: "viewers",
39
43
  label: "Viewer",
40
- hint: "Known fields: name (string), liveview (string, liveview ID to display)",
41
- returns: "id, modelKey, name, mac, state (universal identity fields). Protect Integration API 7.1.60 returns a thin object; additional device-specific fields are NOT verified (no instances on reference console) — inspect a live response before relying on them",
44
+ hint: "Known fields: name (string), liveview (string, liveview ID to display, or null)",
45
+ returns: "id, modelKey, name, mac, state, liveview, streamLimit (7.1.83 docs)",
42
46
  },
43
47
  {
44
48
  urlPath: "sirens",
@@ -46,7 +50,7 @@ const DEVICES = [
46
50
  plural: "sirens",
47
51
  label: "Siren",
48
52
  hint: "Known fields: name (string), volume (integer 1-100), ledSettings (object with isEnabled boolean)",
49
- returns: "id, modelKey, name, mac, state (universal identity fields). Protect Integration API 7.1.60 returns a thin object; additional device-specific fields are NOT verified (no instances on reference console) — inspect a live response before relying on them",
53
+ returns: "id, modelKey, name, mac, state, volume, ledSettings (isEnabled), sirenStatus (isActive, activatedAt, duration), connectionType, wirelessConnectionState (7.1.83 docs)",
50
54
  },
51
55
  {
52
56
  urlPath: "fobs",
@@ -54,7 +58,7 @@ const DEVICES = [
54
58
  plural: "fobs",
55
59
  label: "Fob",
56
60
  hint: "Known fields: name (string)",
57
- returns: "id, modelKey, name, mac, state (universal identity fields). Protect Integration API 7.1.60 returns a thin object; additional device-specific fields are NOT verified (no instances on reference console) — inspect a live response before relying on them",
61
+ returns: "id, modelKey, name, mac, state, awayState, buttonLabels, featureFlags (buttons[]), wirelessConnectionState (7.1.83 docs)",
58
62
  },
59
63
  {
60
64
  urlPath: "relays",
@@ -62,7 +66,7 @@ const DEVICES = [
62
66
  plural: "relays",
63
67
  label: "Relay",
64
68
  hint: "Known fields: name (string), ledSettings (object with isEnabled boolean)",
65
- returns: "id, modelKey, name, mac, state (universal identity fields). Protect Integration API 7.1.60 returns a thin object; additional device-specific fields are NOT verified (no instances on reference console) — inspect a live response before relying on them",
69
+ returns: "id, modelKey, name, mac, state, ledSettings (isEnabled), outputs (array), inputs (array), wirelessConnectionState (7.1.83 docs)",
66
70
  },
67
71
  {
68
72
  urlPath: "speakers",
@@ -70,7 +74,7 @@ const DEVICES = [
70
74
  plural: "speakers",
71
75
  label: "Speaker",
72
76
  hint: "Known fields: name (string), volume (integer 0-100), micVolume (integer 0-100), isMicEnabled (boolean)",
73
- returns: "id, modelKey, name, mac, state (universal identity fields). Protect Integration API 7.1.60 returns a thin object; additional device-specific fields are NOT verified (no instances on reference console) — inspect a live response before relying on them",
77
+ returns: "id, modelKey, name, mac, state, volume, micVolume, isMicEnabled, speakerState (status, mode), featureFlags (hasMic) (7.1.83 docs)",
74
78
  },
75
79
  {
76
80
  urlPath: "bridges",
@@ -78,7 +82,7 @@ const DEVICES = [
78
82
  plural: "bridges",
79
83
  label: "Bridge",
80
84
  hint: "Known fields: name (string)",
81
- returns: "id, modelKey, name, mac, state (universal identity fields). Protect Integration API 7.1.60 returns a thin object; additional device-specific fields are NOT verified (no instances on reference console) — inspect a live response before relying on them",
85
+ returns: "id, modelKey, name, mac, state, platform, clients (array of MACs), maxClients (7.1.83 docs)",
82
86
  },
83
87
  {
84
88
  urlPath: "link-stations",
@@ -86,7 +90,7 @@ const DEVICES = [
86
90
  plural: "link_stations",
87
91
  label: "Link station",
88
92
  hint: "Known fields: name (string)",
89
- returns: "id, modelKey, name, mac, state (universal identity fields). Protect Integration API 7.1.60 returns a thin object; additional device-specific fields are NOT verified (no instances on reference console) — inspect a live response before relying on them",
93
+ returns: "id, modelKey (\"linkstation\"), name, mac, state, isAlarmHub, ledSettings (isEnabled), lastEvent, alarmHub (object) (7.1.83 docs)",
90
94
  },
91
95
  {
92
96
  urlPath: "alarm-hubs",
@@ -94,7 +98,7 @@ const DEVICES = [
94
98
  plural: "alarm_hubs",
95
99
  label: "Alarm hub",
96
100
  hint: "Known fields: name (string)",
97
- returns: "id, modelKey, name, mac, state (universal identity fields). Protect Integration API 7.1.60 returns a thin object; additional device-specific fields are NOT verified (no instances on reference console) — inspect a live response before relying on them",
101
+ returns: "id, modelKey (\"linkstation\"), name, mac, state, isAlarmHub, ledSettings (isEnabled), lastEvent, alarmHub (object) (7.1.83 docs)",
98
102
  },
99
103
  ];
100
104
  function registerDeviceCrud(server, client, cfg, readOnly) {
@@ -8,7 +8,7 @@ const url_js_1 = require("../utils/url.js");
8
8
  const misc_js_1 = require("../schemas/misc.js");
9
9
  function registerFileTools(server, client, readOnly) {
10
10
  server.registerTool("protect_list_files", {
11
- description: "List files of a given type (only 'animations' is currently supported — GIF clips uploadable to G4 Doorbell screens). Returns array. Per-record shape is NOT verified against Protect 7.1.60 (no files on the reference console) — inspect a live response to confirm fields.",
11
+ description: "List files of a given type (only 'animations' is currently supported — clips uploadable to G4 Doorbell screens). Returns array; each record (7.1.83 docs): name (server-generated), type, originalName, path.",
12
12
  inputSchema: {
13
13
  fileType: zod_1.z
14
14
  .string()
@@ -56,7 +56,7 @@ function registerFileTools(server, client, readOnly) {
56
56
  contentType: zod_1.z
57
57
  .string()
58
58
  .default("image/gif")
59
- .describe("MIME type of the file (accepted: image/gif, image/png, image/jpeg)"),
59
+ .describe("MIME type of the file (accepted: image/gif, image/jpeg, image/png, audio/mpeg, audio/mp4, audio/wave, audio/x-caf)"),
60
60
  dryRun: zod_1.z
61
61
  .boolean()
62
62
  .optional()
@@ -8,7 +8,7 @@ const url_js_1 = require("../utils/url.js");
8
8
  const misc_js_1 = require("../schemas/misc.js");
9
9
  function registerLiveviewTools(server, client, readOnly) {
10
10
  server.registerTool("protect_list_liveviews", {
11
- description: "List all live views (camera grid layouts shown on viewers / in the Protect UI). Returns array; each liveview includes (Integration API 7.1.60-verified): id, modelKey, name, isDefault, isGlobal, layout (number, slot count), owner (user ID), slots (array of {cameras: string[], cycleMode, cycleInterval}). NOTE: slots use a `cameras` string-array, not a single `cameraId`.",
11
+ description: "List all live views (camera grid layouts shown on viewers / in the Protect UI). Returns array; each liveview includes (Integration API 7.1.83-verified): id, modelKey, name, isDefault, isGlobal, layout (number, slot count), owner (user ID), slots (array of {cameras: string[], cycleMode, cycleInterval}). NOTE: slots use a `cameras` string-array, not a single `cameraId`.",
12
12
  outputSchema: misc_js_1.liveviewListOutputSchema,
13
13
  annotations: safety_js_1.READ_ONLY,
14
14
  }, async () => {
@@ -8,7 +8,7 @@ const url_js_1 = require("../utils/url.js");
8
8
  const misc_js_1 = require("../schemas/misc.js");
9
9
  function registerUserTools(server, client) {
10
10
  server.registerTool("protect_list_users", {
11
- description: "List all Protect users (filtered by the API key's access permissions). Returns array; each user includes (Integration API 7.1.60-verified): id, modelKey, name, firstName, lastName, email, ucoreUserId. The Integration API does NOT expose roles, permissions, login history, groups, or notification settings.",
11
+ description: "List all Protect users (filtered by the API key's access permissions). Returns array; each user includes (Integration API 7.1.83-verified): id, modelKey, name, firstName, lastName, email, ucoreUserId. The Integration API does NOT expose roles, permissions, login history, groups, or notification settings.",
12
12
  outputSchema: misc_js_1.userListOutputSchema,
13
13
  annotations: safety_js_1.READ_ONLY,
14
14
  }, async () => {
@@ -35,7 +35,7 @@ function registerUserTools(server, client) {
35
35
  }
36
36
  });
37
37
  server.registerTool("protect_list_ulp_users", {
38
- description: "List all UniFi Identity (ULP) users. Returns array; each ULP user includes (Integration API 7.1.60-verified): id, modelKey, firstName, lastName, fullName, status (e.g. ACTIVE). Enrolled-credential detail (NFC cards, fingerprints) is NOT exposed by this API surface.",
38
+ description: "List all UniFi Identity (ULP) users. Returns array; each ULP user includes (Integration API 7.1.83-verified): id, modelKey, firstName, lastName, fullName, status (e.g. ACTIVE). Enrolled-credential detail (NFC cards, fingerprints) is NOT exposed by this API surface.",
39
39
  outputSchema: misc_js_1.ulpUserListOutputSchema,
40
40
  annotations: safety_js_1.READ_ONLY,
41
41
  }, async () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owine/unifi-protect-mcp",
3
- "version": "2.7.5",
3
+ "version": "2.8.0",
4
4
  "description": "MCP server for UniFi Protect Integration API",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -22,7 +22,7 @@
22
22
  "engines": {
23
23
  "node": "^22.13.0 || ^24.0.0"
24
24
  },
25
- "packageManager": "pnpm@11.6.0",
25
+ "packageManager": "pnpm@11.8.0",
26
26
  "repository": {
27
27
  "type": "git",
28
28
  "url": "https://github.com/owine/unifi-protect-mcp.git"
@@ -45,11 +45,11 @@
45
45
  "@eslint/js": "10.0.1",
46
46
  "@types/node": "24.13.2",
47
47
  "@types/ws": "8.18.1",
48
- "@vitest/coverage-v8": "4.1.8",
48
+ "@vitest/coverage-v8": "4.1.9",
49
49
  "eslint": "10.5.0",
50
50
  "lefthook": "2.1.9",
51
51
  "typescript": "6.0.3",
52
- "typescript-eslint": "8.61.0",
53
- "vitest": "4.1.8"
52
+ "typescript-eslint": "8.61.1",
53
+ "vitest": "4.1.9"
54
54
  }
55
55
  }