@owine/unifi-protect-mcp 2.0.2 → 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 +2 -0
- package/dist/client.js +11 -0
- package/dist/tools/cameras.js +43 -25
- package/dist/tools/devices.js +6 -5
- package/dist/tools/files.js +10 -9
- package/dist/tools/index.js +2 -0
- package/dist/tools/liveviews.js +7 -6
- package/dist/tools/subscriptions.d.ts +3 -0
- package/dist/tools/subscriptions.js +71 -0
- package/dist/utils/url.d.ts +12 -0
- package/dist/utils/url.js +19 -0
- package/package.json +3 -1
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;
|
package/dist/tools/cameras.js
CHANGED
|
@@ -4,6 +4,7 @@ exports.registerCameraTools = registerCameraTools;
|
|
|
4
4
|
const zod_1 = require("zod");
|
|
5
5
|
const responses_js_1 = require("../utils/responses.js");
|
|
6
6
|
const safety_js_1 = require("../utils/safety.js");
|
|
7
|
+
const url_js_1 = require("../utils/url.js");
|
|
7
8
|
function registerCameraTools(server, client, readOnly) {
|
|
8
9
|
// --- Read-only tools (always registered) ---
|
|
9
10
|
server.registerTool("protect_list_cameras", {
|
|
@@ -24,7 +25,7 @@ function registerCameraTools(server, client, readOnly) {
|
|
|
24
25
|
annotations: safety_js_1.READ_ONLY,
|
|
25
26
|
}, async ({ id }) => {
|
|
26
27
|
try {
|
|
27
|
-
const data = await client.get(`/cameras/${id}`);
|
|
28
|
+
const data = await client.get((0, url_js_1.safePath) `/cameras/${id}`);
|
|
28
29
|
return (0, responses_js_1.formatSuccess)(data);
|
|
29
30
|
}
|
|
30
31
|
catch (err) {
|
|
@@ -33,11 +34,20 @@ function registerCameraTools(server, client, readOnly) {
|
|
|
33
34
|
});
|
|
34
35
|
server.registerTool("protect_get_snapshot", {
|
|
35
36
|
description: "Get a JPEG snapshot from a camera (returns image)",
|
|
36
|
-
inputSchema: {
|
|
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
|
+
},
|
|
37
44
|
annotations: safety_js_1.READ_ONLY,
|
|
38
|
-
}, async ({ id }) => {
|
|
45
|
+
}, async ({ id, highQuality }) => {
|
|
39
46
|
try {
|
|
40
|
-
const
|
|
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);
|
|
41
51
|
return {
|
|
42
52
|
content: [
|
|
43
53
|
{
|
|
@@ -58,7 +68,7 @@ function registerCameraTools(server, client, readOnly) {
|
|
|
58
68
|
annotations: safety_js_1.READ_ONLY,
|
|
59
69
|
}, async ({ id }) => {
|
|
60
70
|
try {
|
|
61
|
-
const data = await client.get(`/cameras/${id}/rtsps-stream`);
|
|
71
|
+
const data = await client.get((0, url_js_1.safePath) `/cameras/${id}/rtsps-stream`);
|
|
62
72
|
return (0, responses_js_1.formatSuccess)(data);
|
|
63
73
|
}
|
|
64
74
|
catch (err) {
|
|
@@ -74,7 +84,7 @@ function registerCameraTools(server, client, readOnly) {
|
|
|
74
84
|
id: zod_1.z.string().describe("Camera ID"),
|
|
75
85
|
settings: zod_1.z
|
|
76
86
|
.record(zod_1.z.string(), zod_1.z.unknown())
|
|
77
|
-
.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)"),
|
|
78
88
|
dryRun: zod_1.z
|
|
79
89
|
.boolean()
|
|
80
90
|
.optional()
|
|
@@ -84,9 +94,9 @@ function registerCameraTools(server, client, readOnly) {
|
|
|
84
94
|
}, async ({ id, settings, dryRun }) => {
|
|
85
95
|
try {
|
|
86
96
|
if (dryRun) {
|
|
87
|
-
return (0, safety_js_1.formatDryRun)("PATCH", `/cameras/${id}`, settings);
|
|
97
|
+
return (0, safety_js_1.formatDryRun)("PATCH", (0, url_js_1.safePath) `/cameras/${id}`, settings);
|
|
88
98
|
}
|
|
89
|
-
const data = await client.patch(`/cameras/${id}`, settings);
|
|
99
|
+
const data = await client.patch((0, url_js_1.safePath) `/cameras/${id}`, settings);
|
|
90
100
|
return (0, responses_js_1.formatSuccess)(data);
|
|
91
101
|
}
|
|
92
102
|
catch (err) {
|
|
@@ -97,18 +107,23 @@ function registerCameraTools(server, client, readOnly) {
|
|
|
97
107
|
description: "Create an RTSPS stream session for a camera",
|
|
98
108
|
inputSchema: {
|
|
99
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"),
|
|
100
113
|
dryRun: zod_1.z
|
|
101
114
|
.boolean()
|
|
102
115
|
.optional()
|
|
103
116
|
.describe("If true, return what would happen without making changes"),
|
|
104
117
|
},
|
|
105
118
|
annotations: safety_js_1.WRITE,
|
|
106
|
-
}, async ({ id, dryRun }) => {
|
|
119
|
+
}, async ({ id, qualities, dryRun }) => {
|
|
107
120
|
try {
|
|
108
121
|
if (dryRun) {
|
|
109
|
-
return (0, safety_js_1.formatDryRun)("POST", `/cameras/${id}/rtsps-stream
|
|
122
|
+
return (0, safety_js_1.formatDryRun)("POST", (0, url_js_1.safePath) `/cameras/${id}/rtsps-stream`, { qualities });
|
|
110
123
|
}
|
|
111
|
-
const data = await client.post(`/cameras/${id}/rtsps-stream
|
|
124
|
+
const data = await client.post((0, url_js_1.safePath) `/cameras/${id}/rtsps-stream`, {
|
|
125
|
+
qualities,
|
|
126
|
+
});
|
|
112
127
|
return (0, responses_js_1.formatSuccess)(data);
|
|
113
128
|
}
|
|
114
129
|
catch (err) {
|
|
@@ -119,18 +134,21 @@ function registerCameraTools(server, client, readOnly) {
|
|
|
119
134
|
description: "Stop and delete an active RTSPS stream session for a camera",
|
|
120
135
|
inputSchema: {
|
|
121
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"),
|
|
122
140
|
dryRun: zod_1.z
|
|
123
141
|
.boolean()
|
|
124
142
|
.optional()
|
|
125
143
|
.describe("If true, return what would happen without making changes"),
|
|
126
144
|
},
|
|
127
145
|
annotations: safety_js_1.DESTRUCTIVE,
|
|
128
|
-
}, async ({ id, dryRun }) => {
|
|
146
|
+
}, async ({ id, qualities, dryRun }) => {
|
|
129
147
|
try {
|
|
130
148
|
if (dryRun) {
|
|
131
|
-
return (0, safety_js_1.formatDryRun)("DELETE", `/cameras/${id}/rtsps-stream`);
|
|
149
|
+
return (0, safety_js_1.formatDryRun)("DELETE", (0, url_js_1.safePath) `/cameras/${id}/rtsps-stream?qualities=${qualities.join(",")}`);
|
|
132
150
|
}
|
|
133
|
-
const data = await client.delete(`/cameras/${id}/rtsps-stream`);
|
|
151
|
+
const data = await client.delete((0, url_js_1.safePath) `/cameras/${id}/rtsps-stream?qualities=${qualities.join(",")}`);
|
|
134
152
|
return (0, responses_js_1.formatSuccess)(data);
|
|
135
153
|
}
|
|
136
154
|
catch (err) {
|
|
@@ -150,9 +168,9 @@ function registerCameraTools(server, client, readOnly) {
|
|
|
150
168
|
}, async ({ id, dryRun }) => {
|
|
151
169
|
try {
|
|
152
170
|
if (dryRun) {
|
|
153
|
-
return (0, safety_js_1.formatDryRun)("POST", `/cameras/${id}/talkback-session`);
|
|
171
|
+
return (0, safety_js_1.formatDryRun)("POST", (0, url_js_1.safePath) `/cameras/${id}/talkback-session`);
|
|
154
172
|
}
|
|
155
|
-
const data = await client.post(`/cameras/${id}/talkback-session`);
|
|
173
|
+
const data = await client.post((0, url_js_1.safePath) `/cameras/${id}/talkback-session`);
|
|
156
174
|
return (0, responses_js_1.formatSuccess)(data);
|
|
157
175
|
}
|
|
158
176
|
catch (err) {
|
|
@@ -173,7 +191,7 @@ function registerCameraTools(server, client, readOnly) {
|
|
|
173
191
|
if (denied)
|
|
174
192
|
return denied;
|
|
175
193
|
try {
|
|
176
|
-
const data = await client.post(`/cameras/${id}/disable-mic-permanently`);
|
|
194
|
+
const data = await client.post((0, url_js_1.safePath) `/cameras/${id}/disable-mic-permanently`);
|
|
177
195
|
return (0, responses_js_1.formatSuccess)(data);
|
|
178
196
|
}
|
|
179
197
|
catch (err) {
|
|
@@ -184,7 +202,7 @@ function registerCameraTools(server, client, readOnly) {
|
|
|
184
202
|
description: "Start PTZ patrol on a camera at a given slot",
|
|
185
203
|
inputSchema: {
|
|
186
204
|
id: zod_1.z.string().describe("Camera ID"),
|
|
187
|
-
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)"),
|
|
188
206
|
dryRun: zod_1.z
|
|
189
207
|
.boolean()
|
|
190
208
|
.optional()
|
|
@@ -194,9 +212,9 @@ function registerCameraTools(server, client, readOnly) {
|
|
|
194
212
|
}, async ({ id, slot, dryRun }) => {
|
|
195
213
|
try {
|
|
196
214
|
if (dryRun) {
|
|
197
|
-
return (0, safety_js_1.formatDryRun)("POST", `/cameras/${id}/ptz/patrol/start/${slot}`);
|
|
215
|
+
return (0, safety_js_1.formatDryRun)("POST", (0, url_js_1.safePath) `/cameras/${id}/ptz/patrol/start/${slot}`);
|
|
198
216
|
}
|
|
199
|
-
const data = await client.post(`/cameras/${id}/ptz/patrol/start/${slot}`);
|
|
217
|
+
const data = await client.post((0, url_js_1.safePath) `/cameras/${id}/ptz/patrol/start/${slot}`);
|
|
200
218
|
return (0, responses_js_1.formatSuccess)(data);
|
|
201
219
|
}
|
|
202
220
|
catch (err) {
|
|
@@ -216,9 +234,9 @@ function registerCameraTools(server, client, readOnly) {
|
|
|
216
234
|
}, async ({ id, dryRun }) => {
|
|
217
235
|
try {
|
|
218
236
|
if (dryRun) {
|
|
219
|
-
return (0, safety_js_1.formatDryRun)("POST", `/cameras/${id}/ptz/patrol/stop`);
|
|
237
|
+
return (0, safety_js_1.formatDryRun)("POST", (0, url_js_1.safePath) `/cameras/${id}/ptz/patrol/stop`);
|
|
220
238
|
}
|
|
221
|
-
const data = await client.post(`/cameras/${id}/ptz/patrol/stop`);
|
|
239
|
+
const data = await client.post((0, url_js_1.safePath) `/cameras/${id}/ptz/patrol/stop`);
|
|
222
240
|
return (0, responses_js_1.formatSuccess)(data);
|
|
223
241
|
}
|
|
224
242
|
catch (err) {
|
|
@@ -229,7 +247,7 @@ function registerCameraTools(server, client, readOnly) {
|
|
|
229
247
|
description: "Move camera PTZ to a preset position",
|
|
230
248
|
inputSchema: {
|
|
231
249
|
id: zod_1.z.string().describe("Camera ID"),
|
|
232
|
-
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)"),
|
|
233
251
|
dryRun: zod_1.z
|
|
234
252
|
.boolean()
|
|
235
253
|
.optional()
|
|
@@ -239,9 +257,9 @@ function registerCameraTools(server, client, readOnly) {
|
|
|
239
257
|
}, async ({ id, slot, dryRun }) => {
|
|
240
258
|
try {
|
|
241
259
|
if (dryRun) {
|
|
242
|
-
return (0, safety_js_1.formatDryRun)("POST", `/cameras/${id}/ptz/goto/${slot}`);
|
|
260
|
+
return (0, safety_js_1.formatDryRun)("POST", (0, url_js_1.safePath) `/cameras/${id}/ptz/goto/${slot}`);
|
|
243
261
|
}
|
|
244
|
-
const data = await client.post(`/cameras/${id}/ptz/goto/${slot}`);
|
|
262
|
+
const data = await client.post((0, url_js_1.safePath) `/cameras/${id}/ptz/goto/${slot}`);
|
|
245
263
|
return (0, responses_js_1.formatSuccess)(data);
|
|
246
264
|
}
|
|
247
265
|
catch (err) {
|
package/dist/tools/devices.js
CHANGED
|
@@ -4,10 +4,11 @@ exports.registerDeviceTools = registerDeviceTools;
|
|
|
4
4
|
const zod_1 = require("zod");
|
|
5
5
|
const responses_js_1 = require("../utils/responses.js");
|
|
6
6
|
const safety_js_1 = require("../utils/safety.js");
|
|
7
|
+
const url_js_1 = require("../utils/url.js");
|
|
7
8
|
const settingsHints = {
|
|
8
9
|
light: "Known fields: name (string), isLightForceEnabled (boolean), lightModeSettings (object with mode, enableAt), lightDeviceSettings (object with isIndicatorEnabled, pirDuration, pirSensitivity, ledLevel)",
|
|
9
|
-
sensor: "Known fields: name (string),
|
|
10
|
-
chime: "Known fields: name (string),
|
|
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)",
|
|
11
12
|
viewer: "Known fields: name (string), liveview (string, liveview ID to display)",
|
|
12
13
|
};
|
|
13
14
|
function registerDeviceCrud(server, client, deviceType, readOnly) {
|
|
@@ -31,7 +32,7 @@ function registerDeviceCrud(server, client, deviceType, readOnly) {
|
|
|
31
32
|
annotations: safety_js_1.READ_ONLY,
|
|
32
33
|
}, async ({ id }) => {
|
|
33
34
|
try {
|
|
34
|
-
const data = await client.get(`/${plural}/${id}`);
|
|
35
|
+
const data = await client.get((0, url_js_1.safePath) `/${plural}/${id}`);
|
|
35
36
|
return (0, responses_js_1.formatSuccess)(data);
|
|
36
37
|
}
|
|
37
38
|
catch (err) {
|
|
@@ -55,9 +56,9 @@ function registerDeviceCrud(server, client, deviceType, readOnly) {
|
|
|
55
56
|
}, async ({ id, settings, dryRun }) => {
|
|
56
57
|
try {
|
|
57
58
|
if (dryRun) {
|
|
58
|
-
return (0, safety_js_1.formatDryRun)("PATCH", `/${plural}/${id}`, settings);
|
|
59
|
+
return (0, safety_js_1.formatDryRun)("PATCH", (0, url_js_1.safePath) `/${plural}/${id}`, settings);
|
|
59
60
|
}
|
|
60
|
-
const data = await client.patch(`/${plural}/${id}`, settings);
|
|
61
|
+
const data = await client.patch((0, url_js_1.safePath) `/${plural}/${id}`, settings);
|
|
61
62
|
return (0, responses_js_1.formatSuccess)(data);
|
|
62
63
|
}
|
|
63
64
|
catch (err) {
|
package/dist/tools/files.js
CHANGED
|
@@ -4,18 +4,19 @@ exports.registerFileTools = registerFileTools;
|
|
|
4
4
|
const zod_1 = require("zod");
|
|
5
5
|
const responses_js_1 = require("../utils/responses.js");
|
|
6
6
|
const safety_js_1 = require("../utils/safety.js");
|
|
7
|
+
const url_js_1 = require("../utils/url.js");
|
|
7
8
|
function registerFileTools(server, client, readOnly) {
|
|
8
9
|
server.registerTool("protect_list_files", {
|
|
9
|
-
description: "List files of a given type (
|
|
10
|
+
description: "List files of a given type (only 'animations' is supported)",
|
|
10
11
|
inputSchema: {
|
|
11
12
|
fileType: zod_1.z
|
|
12
13
|
.string()
|
|
13
|
-
.describe("File type to list (
|
|
14
|
+
.describe("File type to list (only 'animations' is currently supported)"),
|
|
14
15
|
},
|
|
15
16
|
annotations: safety_js_1.READ_ONLY,
|
|
16
17
|
}, async ({ fileType }) => {
|
|
17
18
|
try {
|
|
18
|
-
const data = await client.get(`/files/${fileType}`);
|
|
19
|
+
const data = await client.get((0, url_js_1.safePath) `/files/${fileType}`);
|
|
19
20
|
return (0, responses_js_1.formatSuccess)(data);
|
|
20
21
|
}
|
|
21
22
|
catch (err) {
|
|
@@ -27,7 +28,7 @@ function registerFileTools(server, client, readOnly) {
|
|
|
27
28
|
server.registerTool("protect_trigger_alarm_webhook", {
|
|
28
29
|
description: "Trigger an alarm manager webhook by ID. This fires an external alarm action.",
|
|
29
30
|
inputSchema: {
|
|
30
|
-
id: zod_1.z.string().describe("
|
|
31
|
+
id: zod_1.z.string().describe("User-defined webhook ID matching an alarm manager configuration"),
|
|
31
32
|
confirm: zod_1.z
|
|
32
33
|
.literal(true)
|
|
33
34
|
.describe("Must be true to confirm triggering the alarm webhook"),
|
|
@@ -38,7 +39,7 @@ function registerFileTools(server, client, readOnly) {
|
|
|
38
39
|
if (denied)
|
|
39
40
|
return denied;
|
|
40
41
|
try {
|
|
41
|
-
await client.post(`/alarm-manager/webhook/${id}`);
|
|
42
|
+
await client.post((0, url_js_1.safePath) `/alarm-manager/webhook/${id}`);
|
|
42
43
|
return (0, responses_js_1.formatSuccess)({ triggered: true, webhookId: id });
|
|
43
44
|
}
|
|
44
45
|
catch (err) {
|
|
@@ -52,8 +53,8 @@ function registerFileTools(server, client, readOnly) {
|
|
|
52
53
|
base64Data: zod_1.z.string().describe("Base64-encoded file content"),
|
|
53
54
|
contentType: zod_1.z
|
|
54
55
|
.string()
|
|
55
|
-
.default("
|
|
56
|
-
.describe("MIME type of the file"),
|
|
56
|
+
.default("image/gif")
|
|
57
|
+
.describe("MIME type of the file (accepted: image/gif, image/png, image/jpeg)"),
|
|
57
58
|
dryRun: zod_1.z
|
|
58
59
|
.boolean()
|
|
59
60
|
.optional()
|
|
@@ -63,13 +64,13 @@ function registerFileTools(server, client, readOnly) {
|
|
|
63
64
|
}, async ({ fileType, base64Data, contentType, dryRun }) => {
|
|
64
65
|
try {
|
|
65
66
|
if (dryRun) {
|
|
66
|
-
return (0, safety_js_1.formatDryRun)("POST", `/files/${fileType}`, {
|
|
67
|
+
return (0, safety_js_1.formatDryRun)("POST", (0, url_js_1.safePath) `/files/${fileType}`, {
|
|
67
68
|
contentType,
|
|
68
69
|
dataLength: base64Data.length,
|
|
69
70
|
});
|
|
70
71
|
}
|
|
71
72
|
const buffer = Buffer.from(base64Data, "base64");
|
|
72
|
-
const data = await client.postBinary(`/files/${fileType}`, buffer, contentType);
|
|
73
|
+
const data = await client.postBinary((0, url_js_1.safePath) `/files/${fileType}`, buffer, contentType);
|
|
73
74
|
return (0, responses_js_1.formatSuccess)(data);
|
|
74
75
|
}
|
|
75
76
|
catch (err) {
|
package/dist/tools/index.js
CHANGED
|
@@ -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);
|
package/dist/tools/liveviews.js
CHANGED
|
@@ -4,6 +4,7 @@ exports.registerLiveviewTools = registerLiveviewTools;
|
|
|
4
4
|
const zod_1 = require("zod");
|
|
5
5
|
const responses_js_1 = require("../utils/responses.js");
|
|
6
6
|
const safety_js_1 = require("../utils/safety.js");
|
|
7
|
+
const url_js_1 = require("../utils/url.js");
|
|
7
8
|
function registerLiveviewTools(server, client, readOnly) {
|
|
8
9
|
server.registerTool("protect_list_liveviews", {
|
|
9
10
|
description: "List all live views configured in UniFi Protect",
|
|
@@ -23,7 +24,7 @@ function registerLiveviewTools(server, client, readOnly) {
|
|
|
23
24
|
annotations: safety_js_1.READ_ONLY,
|
|
24
25
|
}, async ({ id }) => {
|
|
25
26
|
try {
|
|
26
|
-
const data = await client.get(`/liveviews/${id}`);
|
|
27
|
+
const data = await client.get((0, url_js_1.safePath) `/liveviews/${id}`);
|
|
27
28
|
return (0, responses_js_1.formatSuccess)(data);
|
|
28
29
|
}
|
|
29
30
|
catch (err) {
|
|
@@ -37,7 +38,7 @@ function registerLiveviewTools(server, client, readOnly) {
|
|
|
37
38
|
inputSchema: {
|
|
38
39
|
settings: zod_1.z
|
|
39
40
|
.record(zod_1.z.string(), zod_1.z.unknown())
|
|
40
|
-
.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)"),
|
|
41
42
|
dryRun: zod_1.z
|
|
42
43
|
.boolean()
|
|
43
44
|
.optional()
|
|
@@ -57,12 +58,12 @@ function registerLiveviewTools(server, client, readOnly) {
|
|
|
57
58
|
}
|
|
58
59
|
});
|
|
59
60
|
server.registerTool("protect_update_liveview", {
|
|
60
|
-
description: "Update an existing live view (
|
|
61
|
+
description: "Update an existing live view (full replacement via PUT-style PATCH — include all fields)",
|
|
61
62
|
inputSchema: {
|
|
62
63
|
id: zod_1.z.string().describe("Liveview ID"),
|
|
63
64
|
settings: zod_1.z
|
|
64
65
|
.record(zod_1.z.string(), zod_1.z.unknown())
|
|
65
|
-
.describe("
|
|
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)"),
|
|
66
67
|
dryRun: zod_1.z
|
|
67
68
|
.boolean()
|
|
68
69
|
.optional()
|
|
@@ -72,9 +73,9 @@ function registerLiveviewTools(server, client, readOnly) {
|
|
|
72
73
|
}, async ({ id, settings, dryRun }) => {
|
|
73
74
|
try {
|
|
74
75
|
if (dryRun) {
|
|
75
|
-
return (0, safety_js_1.formatDryRun)("PATCH", `/liveviews/${id}`, settings);
|
|
76
|
+
return (0, safety_js_1.formatDryRun)("PATCH", (0, url_js_1.safePath) `/liveviews/${id}`, settings);
|
|
76
77
|
}
|
|
77
|
-
const data = await client.patch(`/liveviews/${id}`, settings);
|
|
78
|
+
const data = await client.patch((0, url_js_1.safePath) `/liveviews/${id}`, settings);
|
|
78
79
|
return (0, responses_js_1.formatSuccess)(data);
|
|
79
80
|
}
|
|
80
81
|
catch (err) {
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tagged template that encodes interpolated values for safe URL path construction.
|
|
3
|
+
*
|
|
4
|
+
* This function URL-encodes only the interpolated values, which prevents them from
|
|
5
|
+
* introducing path traversal (`/`), query (`?`), or fragment (`#`) delimiters.
|
|
6
|
+
* The literal template segments are not modified, so they must be trusted and should
|
|
7
|
+
* not contain user-controlled data.
|
|
8
|
+
*
|
|
9
|
+
* Values must be raw (unencoded). Pre-encoded values will be double-encoded
|
|
10
|
+
* (e.g. `%2F` becomes `%252F`), which is the correct defensive behavior.
|
|
11
|
+
*/
|
|
12
|
+
export declare function safePath(strings: TemplateStringsArray, ...values: (string | number)[]): string;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.safePath = safePath;
|
|
4
|
+
/**
|
|
5
|
+
* Tagged template that encodes interpolated values for safe URL path construction.
|
|
6
|
+
*
|
|
7
|
+
* This function URL-encodes only the interpolated values, which prevents them from
|
|
8
|
+
* introducing path traversal (`/`), query (`?`), or fragment (`#`) delimiters.
|
|
9
|
+
* The literal template segments are not modified, so they must be trusted and should
|
|
10
|
+
* not contain user-controlled data.
|
|
11
|
+
*
|
|
12
|
+
* Values must be raw (unencoded). Pre-encoded values will be double-encoded
|
|
13
|
+
* (e.g. `%2F` becomes `%252F`), which is the correct defensive behavior.
|
|
14
|
+
*/
|
|
15
|
+
function safePath(strings, ...values) {
|
|
16
|
+
return strings.reduce((result, str, i) => i < values.length
|
|
17
|
+
? result + str + encodeURIComponent(String(values[i]))
|
|
18
|
+
: result + str, "");
|
|
19
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@owine/unifi-protect-mcp",
|
|
3
|
-
"version": "2.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",
|