@owine/unifi-protect-mcp 2.1.0 → 2.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/client.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import WebSocket from "ws";
1
2
  import { Config } from "./config.js";
2
3
  export declare class ProtectClient {
3
4
  private baseUrl;
@@ -13,4 +14,5 @@ export declare class ProtectClient {
13
14
  mimeType: string;
14
15
  }>;
15
16
  postBinary(path: string, data: Buffer, contentType: string): Promise<unknown>;
17
+ connectWebSocket(path: string): WebSocket;
16
18
  }
package/dist/client.js CHANGED
@@ -1,6 +1,10 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.ProtectClient = void 0;
7
+ const ws_1 = __importDefault(require("ws"));
4
8
  class ProtectClient {
5
9
  baseUrl;
6
10
  headers;
@@ -80,5 +84,12 @@ class ProtectClient {
80
84
  }
81
85
  return response.text();
82
86
  }
87
+ connectWebSocket(path) {
88
+ const url = `wss://${this.baseUrl.replace(/^https?:\/\//, "")}${path}`;
89
+ return new ws_1.default(url, {
90
+ headers: { "X-API-KEY": this.headers["X-API-KEY"] },
91
+ rejectUnauthorized: process.env.NODE_TLS_REJECT_UNAUTHORIZED !== "0",
92
+ });
93
+ }
83
94
  }
84
95
  exports.ProtectClient = ProtectClient;
@@ -34,11 +34,20 @@ function registerCameraTools(server, client, readOnly) {
34
34
  });
35
35
  server.registerTool("protect_get_snapshot", {
36
36
  description: "Get a JPEG snapshot from a camera (returns image)",
37
- inputSchema: { id: zod_1.z.string().describe("Camera ID") },
37
+ inputSchema: {
38
+ id: zod_1.z.string().describe("Camera ID"),
39
+ highQuality: zod_1.z
40
+ .boolean()
41
+ .optional()
42
+ .describe("If true, request a high-quality snapshot"),
43
+ },
38
44
  annotations: safety_js_1.READ_ONLY,
39
- }, async ({ id }) => {
45
+ }, async ({ id, highQuality }) => {
40
46
  try {
41
- const { data, mimeType } = await client.getBinary((0, url_js_1.safePath) `/cameras/${id}/snapshot`);
47
+ const url = highQuality
48
+ ? (0, url_js_1.safePath) `/cameras/${id}/snapshot?highQuality=true`
49
+ : (0, url_js_1.safePath) `/cameras/${id}/snapshot`;
50
+ const { data, mimeType } = await client.getBinary(url);
42
51
  return {
43
52
  content: [
44
53
  {
@@ -75,7 +84,7 @@ function registerCameraTools(server, client, readOnly) {
75
84
  id: zod_1.z.string().describe("Camera ID"),
76
85
  settings: zod_1.z
77
86
  .record(zod_1.z.string(), zod_1.z.unknown())
78
- .describe("Partial camera settings to update. Known fields: name (string), osdSettings (object), ledSettings (object), lcdMessage (object), videoMode (string), hdrType (string), smartDetectSettings (object)"),
87
+ .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)"),
79
88
  dryRun: zod_1.z
80
89
  .boolean()
81
90
  .optional()
@@ -98,18 +107,23 @@ function registerCameraTools(server, client, readOnly) {
98
107
  description: "Create an RTSPS stream session for a camera",
99
108
  inputSchema: {
100
109
  id: zod_1.z.string().describe("Camera ID"),
110
+ qualities: zod_1.z
111
+ .array(zod_1.z.enum(["high", "medium", "low", "package"]))
112
+ .describe("Quality levels to include in the RTSPS stream"),
101
113
  dryRun: zod_1.z
102
114
  .boolean()
103
115
  .optional()
104
116
  .describe("If true, return what would happen without making changes"),
105
117
  },
106
118
  annotations: safety_js_1.WRITE,
107
- }, async ({ id, dryRun }) => {
119
+ }, async ({ id, qualities, dryRun }) => {
108
120
  try {
109
121
  if (dryRun) {
110
- return (0, safety_js_1.formatDryRun)("POST", (0, url_js_1.safePath) `/cameras/${id}/rtsps-stream`);
122
+ return (0, safety_js_1.formatDryRun)("POST", (0, url_js_1.safePath) `/cameras/${id}/rtsps-stream`, { qualities });
111
123
  }
112
- const data = await client.post((0, url_js_1.safePath) `/cameras/${id}/rtsps-stream`);
124
+ const data = await client.post((0, url_js_1.safePath) `/cameras/${id}/rtsps-stream`, {
125
+ qualities,
126
+ });
113
127
  return (0, responses_js_1.formatSuccess)(data);
114
128
  }
115
129
  catch (err) {
@@ -120,18 +134,21 @@ function registerCameraTools(server, client, readOnly) {
120
134
  description: "Stop and delete an active RTSPS stream session for a camera",
121
135
  inputSchema: {
122
136
  id: zod_1.z.string().describe("Camera ID"),
137
+ qualities: zod_1.z
138
+ .array(zod_1.z.enum(["high", "medium", "low", "package"]))
139
+ .describe("Quality levels to remove from the RTSPS stream"),
123
140
  dryRun: zod_1.z
124
141
  .boolean()
125
142
  .optional()
126
143
  .describe("If true, return what would happen without making changes"),
127
144
  },
128
145
  annotations: safety_js_1.DESTRUCTIVE,
129
- }, async ({ id, dryRun }) => {
146
+ }, async ({ id, qualities, dryRun }) => {
130
147
  try {
131
148
  if (dryRun) {
132
- return (0, safety_js_1.formatDryRun)("DELETE", (0, url_js_1.safePath) `/cameras/${id}/rtsps-stream`);
149
+ return (0, safety_js_1.formatDryRun)("DELETE", (0, url_js_1.safePath) `/cameras/${id}/rtsps-stream?qualities=${qualities.join(",")}`);
133
150
  }
134
- const data = await client.delete((0, url_js_1.safePath) `/cameras/${id}/rtsps-stream`);
151
+ const data = await client.delete((0, url_js_1.safePath) `/cameras/${id}/rtsps-stream?qualities=${qualities.join(",")}`);
135
152
  return (0, responses_js_1.formatSuccess)(data);
136
153
  }
137
154
  catch (err) {
@@ -185,7 +202,7 @@ function registerCameraTools(server, client, readOnly) {
185
202
  description: "Start PTZ patrol on a camera at a given slot",
186
203
  inputSchema: {
187
204
  id: zod_1.z.string().describe("Camera ID"),
188
- slot: zod_1.z.number().int().describe("Patrol slot number"),
205
+ slot: zod_1.z.number().int().min(0).max(4).describe("Patrol slot number (0-4)"),
189
206
  dryRun: zod_1.z
190
207
  .boolean()
191
208
  .optional()
@@ -230,7 +247,7 @@ function registerCameraTools(server, client, readOnly) {
230
247
  description: "Move camera PTZ to a preset position",
231
248
  inputSchema: {
232
249
  id: zod_1.z.string().describe("Camera ID"),
233
- slot: zod_1.z.number().int().describe("PTZ preset slot number"),
250
+ slot: zod_1.z.number().int().min(-1).max(4).describe("PTZ preset slot number (0-4, or -1 for home position)"),
234
251
  dryRun: zod_1.z
235
252
  .boolean()
236
253
  .optional()
@@ -7,8 +7,8 @@ const safety_js_1 = require("../utils/safety.js");
7
7
  const url_js_1 = require("../utils/url.js");
8
8
  const settingsHints = {
9
9
  light: "Known fields: name (string), isLightForceEnabled (boolean), lightModeSettings (object with mode, enableAt), lightDeviceSettings (object with isIndicatorEnabled, pirDuration, pirSensitivity, ledLevel)",
10
- sensor: "Known fields: name (string), mountType (string), motionSettings (object), humiditySettings (object), temperatureSettings (object), lightSettings (object), alarmSettings (object)",
11
- chime: "Known fields: name (string), volume (number), ringSettings (array of ring tone configurations)",
10
+ sensor: "Known fields: name (string), motionSettings (object), humiditySettings (object), temperatureSettings (object), lightSettings (object), alarmSettings (object)",
11
+ chime: "Known fields: name (string), cameraIds (array of camera IDs linked to the chime), ringSettings (array of ring tone configurations)",
12
12
  viewer: "Known fields: name (string), liveview (string, liveview ID to display)",
13
13
  };
14
14
  function registerDeviceCrud(server, client, deviceType, readOnly) {
@@ -7,11 +7,11 @@ const safety_js_1 = require("../utils/safety.js");
7
7
  const url_js_1 = require("../utils/url.js");
8
8
  function registerFileTools(server, client, readOnly) {
9
9
  server.registerTool("protect_list_files", {
10
- description: "List files of a given type (e.g. 'video', 'timelapse')",
10
+ description: "List files of a given type (only 'animations' is supported)",
11
11
  inputSchema: {
12
12
  fileType: zod_1.z
13
13
  .string()
14
- .describe("File type to list (e.g. 'video', 'timelapse')"),
14
+ .describe("File type to list (only 'animations' is currently supported)"),
15
15
  },
16
16
  annotations: safety_js_1.READ_ONLY,
17
17
  }, async ({ fileType }) => {
@@ -28,7 +28,7 @@ function registerFileTools(server, client, readOnly) {
28
28
  server.registerTool("protect_trigger_alarm_webhook", {
29
29
  description: "Trigger an alarm manager webhook by ID. This fires an external alarm action.",
30
30
  inputSchema: {
31
- id: zod_1.z.string().describe("Webhook ID"),
31
+ id: zod_1.z.string().describe("User-defined webhook ID matching an alarm manager configuration"),
32
32
  confirm: zod_1.z
33
33
  .literal(true)
34
34
  .describe("Must be true to confirm triggering the alarm webhook"),
@@ -53,8 +53,8 @@ function registerFileTools(server, client, readOnly) {
53
53
  base64Data: zod_1.z.string().describe("Base64-encoded file content"),
54
54
  contentType: zod_1.z
55
55
  .string()
56
- .default("application/octet-stream")
57
- .describe("MIME type of the file"),
56
+ .default("image/gif")
57
+ .describe("MIME type of the file (accepted: image/gif, image/png, image/jpeg)"),
58
58
  dryRun: zod_1.z
59
59
  .boolean()
60
60
  .optional()
@@ -6,8 +6,10 @@ const cameras_js_1 = require("./cameras.js");
6
6
  const devices_js_1 = require("./devices.js");
7
7
  const liveviews_js_1 = require("./liveviews.js");
8
8
  const files_js_1 = require("./files.js");
9
+ const subscriptions_js_1 = require("./subscriptions.js");
9
10
  function registerAllTools(server, client, readOnly) {
10
11
  (0, system_js_1.registerSystemTools)(server, client);
12
+ (0, subscriptions_js_1.registerSubscriptionTools)(server, client);
11
13
  (0, cameras_js_1.registerCameraTools)(server, client, readOnly);
12
14
  (0, devices_js_1.registerDeviceTools)(server, client, readOnly);
13
15
  (0, liveviews_js_1.registerLiveviewTools)(server, client, readOnly);
@@ -38,7 +38,7 @@ function registerLiveviewTools(server, client, readOnly) {
38
38
  inputSchema: {
39
39
  settings: zod_1.z
40
40
  .record(zod_1.z.string(), zod_1.z.unknown())
41
- .describe("Liveview configuration. Required fields: name (string), layout (number of slots), slots (array of {cameras: string[], cycleMode: string, cycleInterval: number}), isDefault (boolean), isGlobal (boolean)"),
41
+ .describe("Liveview configuration. Required fields: id (string), modelKey (string, must be 'liveview'), owner (string, user ID), name (string), layout (number of slots), slots (array of {cameras: string[], cycleMode: string, cycleInterval: number}), isDefault (boolean), isGlobal (boolean)"),
42
42
  dryRun: zod_1.z
43
43
  .boolean()
44
44
  .optional()
@@ -58,12 +58,12 @@ function registerLiveviewTools(server, client, readOnly) {
58
58
  }
59
59
  });
60
60
  server.registerTool("protect_update_liveview", {
61
- description: "Update an existing live view (partial update via PATCH)",
61
+ description: "Update an existing live view (full replacement via PUT-style PATCH — include all fields)",
62
62
  inputSchema: {
63
63
  id: zod_1.z.string().describe("Liveview ID"),
64
64
  settings: zod_1.z
65
65
  .record(zod_1.z.string(), zod_1.z.unknown())
66
- .describe("Partial liveview settings to update. Known fields: name (string), layout (number), slots (array of {cameras: string[], cycleMode: string, cycleInterval: number}), isDefault (boolean), isGlobal (boolean)"),
66
+ .describe("Full liveview object for update (all fields required). Known fields: id (string), modelKey (string, must be 'liveview'), owner (string, user ID), name (string), layout (number), slots (array of {cameras: string[], cycleMode: string, cycleInterval: number}), isDefault (boolean), isGlobal (boolean)"),
67
67
  dryRun: zod_1.z
68
68
  .boolean()
69
69
  .optional()
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { ProtectClient } from "../client.js";
3
+ export declare function registerSubscriptionTools(server: McpServer, client: ProtectClient): void;
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerSubscriptionTools = registerSubscriptionTools;
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 durationSchema = zod_1.z
8
+ .number()
9
+ .min(1)
10
+ .max(30)
11
+ .default(5)
12
+ .describe("Seconds to listen (1-30, default 5)");
13
+ function collectMessages(client, path, duration) {
14
+ return new Promise((resolve) => {
15
+ const messages = [];
16
+ let error;
17
+ const ws = client.connectWebSocket(path);
18
+ const timeout = setTimeout(() => {
19
+ ws.close();
20
+ }, duration * 1000);
21
+ ws.on("message", (data) => {
22
+ const text = String(data);
23
+ try {
24
+ messages.push(JSON.parse(text));
25
+ }
26
+ catch {
27
+ messages.push(text);
28
+ }
29
+ });
30
+ ws.on("error", (err) => {
31
+ error = err.message;
32
+ clearTimeout(timeout);
33
+ ws.close();
34
+ });
35
+ ws.on("close", () => {
36
+ clearTimeout(timeout);
37
+ const result = { messages, duration };
38
+ if (error)
39
+ result.error = error;
40
+ resolve(result);
41
+ });
42
+ });
43
+ }
44
+ function registerSubscriptionTools(server, client) {
45
+ server.registerTool("protect_subscribe_devices", {
46
+ description: "Connect to the device update WebSocket and collect messages for a specified duration. Returns real-time add/update/remove events for Protect-managed hardware devices.",
47
+ inputSchema: { duration: durationSchema },
48
+ annotations: safety_js_1.READ_ONLY,
49
+ }, async ({ duration }) => {
50
+ try {
51
+ const data = await collectMessages(client, "/subscribe/devices", duration);
52
+ return (0, responses_js_1.formatSuccess)(data);
53
+ }
54
+ catch (err) {
55
+ return (0, responses_js_1.formatError)(err);
56
+ }
57
+ });
58
+ server.registerTool("protect_subscribe_events", {
59
+ description: "Connect to the Protect event WebSocket and collect messages for a specified duration. Returns events including motion, ring (doorbell), and smartDetect (person/vehicle/animal/package).",
60
+ inputSchema: { duration: durationSchema },
61
+ annotations: safety_js_1.READ_ONLY,
62
+ }, async ({ duration }) => {
63
+ try {
64
+ const data = await collectMessages(client, "/subscribe/events", duration);
65
+ return (0, responses_js_1.formatSuccess)(data);
66
+ }
67
+ catch (err) {
68
+ return (0, responses_js_1.formatError)(err);
69
+ }
70
+ });
71
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owine/unifi-protect-mcp",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "MCP server for UniFi Protect Integration API",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -37,11 +37,13 @@
37
37
  "license": "MIT",
38
38
  "dependencies": {
39
39
  "@modelcontextprotocol/sdk": "^1.12.1",
40
+ "ws": "^8.19.0",
40
41
  "zod": "^4.0.0"
41
42
  },
42
43
  "devDependencies": {
43
44
  "@eslint/js": "10.0.1",
44
45
  "@types/node": "24.12.0",
46
+ "@types/ws": "^8.18.0",
45
47
  "@vitest/coverage-v8": "4.0.18",
46
48
  "eslint": "10.0.3",
47
49
  "typescript": "5.9.3",