@owine/unifi-protect-mcp 2.0.2 → 2.1.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/tools/cameras.js +19 -18
- package/dist/tools/devices.js +4 -3
- package/dist/tools/files.js +5 -4
- package/dist/tools/liveviews.js +4 -3
- package/dist/utils/url.d.ts +12 -0
- package/dist/utils/url.js +19 -0
- package/package.json +1 -1
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) {
|
|
@@ -37,7 +38,7 @@ function registerCameraTools(server, client, readOnly) {
|
|
|
37
38
|
annotations: safety_js_1.READ_ONLY,
|
|
38
39
|
}, async ({ id }) => {
|
|
39
40
|
try {
|
|
40
|
-
const { data, mimeType } = await client.getBinary(`/cameras/${id}/snapshot`);
|
|
41
|
+
const { data, mimeType } = await client.getBinary((0, url_js_1.safePath) `/cameras/${id}/snapshot`);
|
|
41
42
|
return {
|
|
42
43
|
content: [
|
|
43
44
|
{
|
|
@@ -58,7 +59,7 @@ function registerCameraTools(server, client, readOnly) {
|
|
|
58
59
|
annotations: safety_js_1.READ_ONLY,
|
|
59
60
|
}, async ({ id }) => {
|
|
60
61
|
try {
|
|
61
|
-
const data = await client.get(`/cameras/${id}/rtsps-stream`);
|
|
62
|
+
const data = await client.get((0, url_js_1.safePath) `/cameras/${id}/rtsps-stream`);
|
|
62
63
|
return (0, responses_js_1.formatSuccess)(data);
|
|
63
64
|
}
|
|
64
65
|
catch (err) {
|
|
@@ -84,9 +85,9 @@ function registerCameraTools(server, client, readOnly) {
|
|
|
84
85
|
}, async ({ id, settings, dryRun }) => {
|
|
85
86
|
try {
|
|
86
87
|
if (dryRun) {
|
|
87
|
-
return (0, safety_js_1.formatDryRun)("PATCH", `/cameras/${id}`, settings);
|
|
88
|
+
return (0, safety_js_1.formatDryRun)("PATCH", (0, url_js_1.safePath) `/cameras/${id}`, settings);
|
|
88
89
|
}
|
|
89
|
-
const data = await client.patch(`/cameras/${id}`, settings);
|
|
90
|
+
const data = await client.patch((0, url_js_1.safePath) `/cameras/${id}`, settings);
|
|
90
91
|
return (0, responses_js_1.formatSuccess)(data);
|
|
91
92
|
}
|
|
92
93
|
catch (err) {
|
|
@@ -106,9 +107,9 @@ function registerCameraTools(server, client, readOnly) {
|
|
|
106
107
|
}, async ({ id, dryRun }) => {
|
|
107
108
|
try {
|
|
108
109
|
if (dryRun) {
|
|
109
|
-
return (0, safety_js_1.formatDryRun)("POST", `/cameras/${id}/rtsps-stream`);
|
|
110
|
+
return (0, safety_js_1.formatDryRun)("POST", (0, url_js_1.safePath) `/cameras/${id}/rtsps-stream`);
|
|
110
111
|
}
|
|
111
|
-
const data = await client.post(`/cameras/${id}/rtsps-stream`);
|
|
112
|
+
const data = await client.post((0, url_js_1.safePath) `/cameras/${id}/rtsps-stream`);
|
|
112
113
|
return (0, responses_js_1.formatSuccess)(data);
|
|
113
114
|
}
|
|
114
115
|
catch (err) {
|
|
@@ -128,9 +129,9 @@ function registerCameraTools(server, client, readOnly) {
|
|
|
128
129
|
}, async ({ id, dryRun }) => {
|
|
129
130
|
try {
|
|
130
131
|
if (dryRun) {
|
|
131
|
-
return (0, safety_js_1.formatDryRun)("DELETE", `/cameras/${id}/rtsps-stream`);
|
|
132
|
+
return (0, safety_js_1.formatDryRun)("DELETE", (0, url_js_1.safePath) `/cameras/${id}/rtsps-stream`);
|
|
132
133
|
}
|
|
133
|
-
const data = await client.delete(`/cameras/${id}/rtsps-stream`);
|
|
134
|
+
const data = await client.delete((0, url_js_1.safePath) `/cameras/${id}/rtsps-stream`);
|
|
134
135
|
return (0, responses_js_1.formatSuccess)(data);
|
|
135
136
|
}
|
|
136
137
|
catch (err) {
|
|
@@ -150,9 +151,9 @@ function registerCameraTools(server, client, readOnly) {
|
|
|
150
151
|
}, async ({ id, dryRun }) => {
|
|
151
152
|
try {
|
|
152
153
|
if (dryRun) {
|
|
153
|
-
return (0, safety_js_1.formatDryRun)("POST", `/cameras/${id}/talkback-session`);
|
|
154
|
+
return (0, safety_js_1.formatDryRun)("POST", (0, url_js_1.safePath) `/cameras/${id}/talkback-session`);
|
|
154
155
|
}
|
|
155
|
-
const data = await client.post(`/cameras/${id}/talkback-session`);
|
|
156
|
+
const data = await client.post((0, url_js_1.safePath) `/cameras/${id}/talkback-session`);
|
|
156
157
|
return (0, responses_js_1.formatSuccess)(data);
|
|
157
158
|
}
|
|
158
159
|
catch (err) {
|
|
@@ -173,7 +174,7 @@ function registerCameraTools(server, client, readOnly) {
|
|
|
173
174
|
if (denied)
|
|
174
175
|
return denied;
|
|
175
176
|
try {
|
|
176
|
-
const data = await client.post(`/cameras/${id}/disable-mic-permanently`);
|
|
177
|
+
const data = await client.post((0, url_js_1.safePath) `/cameras/${id}/disable-mic-permanently`);
|
|
177
178
|
return (0, responses_js_1.formatSuccess)(data);
|
|
178
179
|
}
|
|
179
180
|
catch (err) {
|
|
@@ -194,9 +195,9 @@ function registerCameraTools(server, client, readOnly) {
|
|
|
194
195
|
}, async ({ id, slot, dryRun }) => {
|
|
195
196
|
try {
|
|
196
197
|
if (dryRun) {
|
|
197
|
-
return (0, safety_js_1.formatDryRun)("POST", `/cameras/${id}/ptz/patrol/start/${slot}`);
|
|
198
|
+
return (0, safety_js_1.formatDryRun)("POST", (0, url_js_1.safePath) `/cameras/${id}/ptz/patrol/start/${slot}`);
|
|
198
199
|
}
|
|
199
|
-
const data = await client.post(`/cameras/${id}/ptz/patrol/start/${slot}`);
|
|
200
|
+
const data = await client.post((0, url_js_1.safePath) `/cameras/${id}/ptz/patrol/start/${slot}`);
|
|
200
201
|
return (0, responses_js_1.formatSuccess)(data);
|
|
201
202
|
}
|
|
202
203
|
catch (err) {
|
|
@@ -216,9 +217,9 @@ function registerCameraTools(server, client, readOnly) {
|
|
|
216
217
|
}, async ({ id, dryRun }) => {
|
|
217
218
|
try {
|
|
218
219
|
if (dryRun) {
|
|
219
|
-
return (0, safety_js_1.formatDryRun)("POST", `/cameras/${id}/ptz/patrol/stop`);
|
|
220
|
+
return (0, safety_js_1.formatDryRun)("POST", (0, url_js_1.safePath) `/cameras/${id}/ptz/patrol/stop`);
|
|
220
221
|
}
|
|
221
|
-
const data = await client.post(`/cameras/${id}/ptz/patrol/stop`);
|
|
222
|
+
const data = await client.post((0, url_js_1.safePath) `/cameras/${id}/ptz/patrol/stop`);
|
|
222
223
|
return (0, responses_js_1.formatSuccess)(data);
|
|
223
224
|
}
|
|
224
225
|
catch (err) {
|
|
@@ -239,9 +240,9 @@ function registerCameraTools(server, client, readOnly) {
|
|
|
239
240
|
}, async ({ id, slot, dryRun }) => {
|
|
240
241
|
try {
|
|
241
242
|
if (dryRun) {
|
|
242
|
-
return (0, safety_js_1.formatDryRun)("POST", `/cameras/${id}/ptz/goto/${slot}`);
|
|
243
|
+
return (0, safety_js_1.formatDryRun)("POST", (0, url_js_1.safePath) `/cameras/${id}/ptz/goto/${slot}`);
|
|
243
244
|
}
|
|
244
|
-
const data = await client.post(`/cameras/${id}/ptz/goto/${slot}`);
|
|
245
|
+
const data = await client.post((0, url_js_1.safePath) `/cameras/${id}/ptz/goto/${slot}`);
|
|
245
246
|
return (0, responses_js_1.formatSuccess)(data);
|
|
246
247
|
}
|
|
247
248
|
catch (err) {
|
package/dist/tools/devices.js
CHANGED
|
@@ -4,6 +4,7 @@ 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
10
|
sensor: "Known fields: name (string), mountType (string), motionSettings (object), humiditySettings (object), temperatureSettings (object), lightSettings (object), alarmSettings (object)",
|
|
@@ -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,6 +4,7 @@ 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
10
|
description: "List files of a given type (e.g. 'video', 'timelapse')",
|
|
@@ -15,7 +16,7 @@ function registerFileTools(server, client, readOnly) {
|
|
|
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) {
|
|
@@ -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) {
|
|
@@ -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/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) {
|
|
@@ -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,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
|
+
}
|