@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.
@@ -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) {
@@ -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) {
@@ -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) {
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owine/unifi-protect-mcp",
3
- "version": "2.0.2",
3
+ "version": "2.1.0",
4
4
  "description": "MCP server for UniFi Protect Integration API",
5
5
  "main": "dist/index.js",
6
6
  "bin": {