@owine/unifi-protect-mcp 2.0.0 → 2.0.1

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.
@@ -3,14 +3,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.registerCameraTools = registerCameraTools;
4
4
  const zod_1 = require("zod");
5
5
  const responses_js_1 = require("../utils/responses.js");
6
- const READ_ONLY_ANNOTATIONS = { readOnlyHint: true, destructiveHint: false };
7
- const WRITE_ANNOTATIONS = { readOnlyHint: false, destructiveHint: false };
8
- const DESTRUCTIVE_ANNOTATIONS = { readOnlyHint: false, destructiveHint: true };
6
+ const safety_js_1 = require("../utils/safety.js");
9
7
  function registerCameraTools(server, client, readOnly) {
10
8
  // --- Read-only tools (always registered) ---
11
9
  server.registerTool("protect_list_cameras", {
12
10
  description: "List all cameras managed by UniFi Protect",
13
- annotations: READ_ONLY_ANNOTATIONS,
11
+ annotations: safety_js_1.READ_ONLY,
14
12
  }, async () => {
15
13
  try {
16
14
  const data = await client.get("/cameras");
@@ -23,7 +21,7 @@ function registerCameraTools(server, client, readOnly) {
23
21
  server.registerTool("protect_get_camera", {
24
22
  description: "Get details for a specific camera by ID",
25
23
  inputSchema: { id: zod_1.z.string().describe("Camera ID") },
26
- annotations: READ_ONLY_ANNOTATIONS,
24
+ annotations: safety_js_1.READ_ONLY,
27
25
  }, async ({ id }) => {
28
26
  try {
29
27
  const data = await client.get(`/cameras/${id}`);
@@ -36,7 +34,7 @@ function registerCameraTools(server, client, readOnly) {
36
34
  server.registerTool("protect_get_snapshot", {
37
35
  description: "Get a JPEG snapshot from a camera (returns image)",
38
36
  inputSchema: { id: zod_1.z.string().describe("Camera ID") },
39
- annotations: READ_ONLY_ANNOTATIONS,
37
+ annotations: safety_js_1.READ_ONLY,
40
38
  }, async ({ id }) => {
41
39
  try {
42
40
  const { data, mimeType } = await client.getBinary(`/cameras/${id}/snapshot`);
@@ -57,7 +55,7 @@ function registerCameraTools(server, client, readOnly) {
57
55
  server.registerTool("protect_get_rtsp_streams", {
58
56
  description: "Get active RTSPS stream sessions for a camera",
59
57
  inputSchema: { id: zod_1.z.string().describe("Camera ID") },
60
- annotations: READ_ONLY_ANNOTATIONS,
58
+ annotations: safety_js_1.READ_ONLY,
61
59
  }, async ({ id }) => {
62
60
  try {
63
61
  const data = await client.get(`/cameras/${id}/rtsps-stream`);
@@ -82,11 +80,11 @@ function registerCameraTools(server, client, readOnly) {
82
80
  .optional()
83
81
  .describe("If true, return what would happen without making changes"),
84
82
  },
85
- annotations: WRITE_ANNOTATIONS,
83
+ annotations: safety_js_1.WRITE,
86
84
  }, async ({ id, settings, dryRun }) => {
87
85
  try {
88
86
  if (dryRun) {
89
- return (0, responses_js_1.formatSuccess)({ dryRun: true, action: "PATCH", path: `/cameras/${id}`, body: settings });
87
+ return (0, safety_js_1.formatDryRun)("PATCH", `/cameras/${id}`, settings);
90
88
  }
91
89
  const data = await client.patch(`/cameras/${id}`, settings);
92
90
  return (0, responses_js_1.formatSuccess)(data);
@@ -104,11 +102,11 @@ function registerCameraTools(server, client, readOnly) {
104
102
  .optional()
105
103
  .describe("If true, return what would happen without making changes"),
106
104
  },
107
- annotations: WRITE_ANNOTATIONS,
105
+ annotations: safety_js_1.WRITE,
108
106
  }, async ({ id, dryRun }) => {
109
107
  try {
110
108
  if (dryRun) {
111
- return (0, responses_js_1.formatSuccess)({ dryRun: true, action: "POST", path: `/cameras/${id}/rtsps-stream` });
109
+ return (0, safety_js_1.formatDryRun)("POST", `/cameras/${id}/rtsps-stream`);
112
110
  }
113
111
  const data = await client.post(`/cameras/${id}/rtsps-stream`);
114
112
  return (0, responses_js_1.formatSuccess)(data);
@@ -126,11 +124,11 @@ function registerCameraTools(server, client, readOnly) {
126
124
  .optional()
127
125
  .describe("If true, return what would happen without making changes"),
128
126
  },
129
- annotations: DESTRUCTIVE_ANNOTATIONS,
127
+ annotations: safety_js_1.DESTRUCTIVE,
130
128
  }, async ({ id, dryRun }) => {
131
129
  try {
132
130
  if (dryRun) {
133
- return (0, responses_js_1.formatSuccess)({ dryRun: true, action: "DELETE", path: `/cameras/${id}/rtsps-stream` });
131
+ return (0, safety_js_1.formatDryRun)("DELETE", `/cameras/${id}/rtsps-stream`);
134
132
  }
135
133
  const data = await client.delete(`/cameras/${id}/rtsps-stream`);
136
134
  return (0, responses_js_1.formatSuccess)(data);
@@ -148,11 +146,11 @@ function registerCameraTools(server, client, readOnly) {
148
146
  .optional()
149
147
  .describe("If true, return what would happen without making changes"),
150
148
  },
151
- annotations: WRITE_ANNOTATIONS,
149
+ annotations: safety_js_1.WRITE,
152
150
  }, async ({ id, dryRun }) => {
153
151
  try {
154
152
  if (dryRun) {
155
- return (0, responses_js_1.formatSuccess)({ dryRun: true, action: "POST", path: `/cameras/${id}/talkback-session` });
153
+ return (0, safety_js_1.formatDryRun)("POST", `/cameras/${id}/talkback-session`);
156
154
  }
157
155
  const data = await client.post(`/cameras/${id}/talkback-session`);
158
156
  return (0, responses_js_1.formatSuccess)(data);
@@ -169,8 +167,11 @@ function registerCameraTools(server, client, readOnly) {
169
167
  .literal(true)
170
168
  .describe("Must be true to confirm this irreversible action"),
171
169
  },
172
- annotations: DESTRUCTIVE_ANNOTATIONS,
173
- }, async ({ id }) => {
170
+ annotations: safety_js_1.DESTRUCTIVE,
171
+ }, async ({ id, confirm }) => {
172
+ const denied = (0, safety_js_1.requireConfirmation)(confirm, "permanently disable the microphone");
173
+ if (denied)
174
+ return denied;
174
175
  try {
175
176
  const data = await client.post(`/cameras/${id}/disable-mic-permanently`);
176
177
  return (0, responses_js_1.formatSuccess)(data);
@@ -189,11 +190,11 @@ function registerCameraTools(server, client, readOnly) {
189
190
  .optional()
190
191
  .describe("If true, return what would happen without making changes"),
191
192
  },
192
- annotations: WRITE_ANNOTATIONS,
193
+ annotations: safety_js_1.WRITE,
193
194
  }, async ({ id, slot, dryRun }) => {
194
195
  try {
195
196
  if (dryRun) {
196
- return (0, responses_js_1.formatSuccess)({ dryRun: true, action: "POST", path: `/cameras/${id}/ptz/patrol/start/${slot}` });
197
+ return (0, safety_js_1.formatDryRun)("POST", `/cameras/${id}/ptz/patrol/start/${slot}`);
197
198
  }
198
199
  const data = await client.post(`/cameras/${id}/ptz/patrol/start/${slot}`);
199
200
  return (0, responses_js_1.formatSuccess)(data);
@@ -211,11 +212,11 @@ function registerCameraTools(server, client, readOnly) {
211
212
  .optional()
212
213
  .describe("If true, return what would happen without making changes"),
213
214
  },
214
- annotations: WRITE_ANNOTATIONS,
215
+ annotations: safety_js_1.WRITE,
215
216
  }, async ({ id, dryRun }) => {
216
217
  try {
217
218
  if (dryRun) {
218
- return (0, responses_js_1.formatSuccess)({ dryRun: true, action: "POST", path: `/cameras/${id}/ptz/patrol/stop` });
219
+ return (0, safety_js_1.formatDryRun)("POST", `/cameras/${id}/ptz/patrol/stop`);
219
220
  }
220
221
  const data = await client.post(`/cameras/${id}/ptz/patrol/stop`);
221
222
  return (0, responses_js_1.formatSuccess)(data);
@@ -234,11 +235,11 @@ function registerCameraTools(server, client, readOnly) {
234
235
  .optional()
235
236
  .describe("If true, return what would happen without making changes"),
236
237
  },
237
- annotations: WRITE_ANNOTATIONS,
238
+ annotations: safety_js_1.WRITE,
238
239
  }, async ({ id, slot, dryRun }) => {
239
240
  try {
240
241
  if (dryRun) {
241
- return (0, responses_js_1.formatSuccess)({ dryRun: true, action: "POST", path: `/cameras/${id}/ptz/goto/${slot}` });
242
+ return (0, safety_js_1.formatDryRun)("POST", `/cameras/${id}/ptz/goto/${slot}`);
242
243
  }
243
244
  const data = await client.post(`/cameras/${id}/ptz/goto/${slot}`);
244
245
  return (0, responses_js_1.formatSuccess)(data);
@@ -3,8 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.registerDeviceTools = registerDeviceTools;
4
4
  const zod_1 = require("zod");
5
5
  const responses_js_1 = require("../utils/responses.js");
6
- const READ_ONLY_ANNOTATIONS = { readOnlyHint: true, destructiveHint: false };
7
- const WRITE_ANNOTATIONS = { readOnlyHint: false, destructiveHint: false };
6
+ const safety_js_1 = require("../utils/safety.js");
8
7
  const settingsHints = {
9
8
  light: "Known fields: name (string), isLightForceEnabled (boolean), lightModeSettings (object with mode, enableAt), lightDeviceSettings (object with isIndicatorEnabled, pirDuration, pirSensitivity, ledLevel)",
10
9
  sensor: "Known fields: name (string), mountType (string), motionSettings (object), humiditySettings (object), temperatureSettings (object), lightSettings (object), alarmSettings (object)",
@@ -16,7 +15,7 @@ function registerDeviceCrud(server, client, deviceType, readOnly) {
16
15
  const label = deviceType.charAt(0).toUpperCase() + deviceType.slice(1);
17
16
  server.registerTool(`protect_list_${plural}`, {
18
17
  description: `List all ${plural} managed by UniFi Protect`,
19
- annotations: READ_ONLY_ANNOTATIONS,
18
+ annotations: safety_js_1.READ_ONLY,
20
19
  }, async () => {
21
20
  try {
22
21
  const data = await client.get(`/${plural}`);
@@ -29,7 +28,7 @@ function registerDeviceCrud(server, client, deviceType, readOnly) {
29
28
  server.registerTool(`protect_get_${deviceType}`, {
30
29
  description: `Get details for a specific ${deviceType} by ID`,
31
30
  inputSchema: { id: zod_1.z.string().describe(`${label} ID`) },
32
- annotations: READ_ONLY_ANNOTATIONS,
31
+ annotations: safety_js_1.READ_ONLY,
33
32
  }, async ({ id }) => {
34
33
  try {
35
34
  const data = await client.get(`/${plural}/${id}`);
@@ -52,11 +51,11 @@ function registerDeviceCrud(server, client, deviceType, readOnly) {
52
51
  .optional()
53
52
  .describe("If true, return what would happen without making changes"),
54
53
  },
55
- annotations: WRITE_ANNOTATIONS,
54
+ annotations: safety_js_1.WRITE,
56
55
  }, async ({ id, settings, dryRun }) => {
57
56
  try {
58
57
  if (dryRun) {
59
- return (0, responses_js_1.formatSuccess)({ dryRun: true, action: "PATCH", path: `/${plural}/${id}`, body: settings });
58
+ return (0, safety_js_1.formatDryRun)("PATCH", `/${plural}/${id}`, settings);
60
59
  }
61
60
  const data = await client.patch(`/${plural}/${id}`, settings);
62
61
  return (0, responses_js_1.formatSuccess)(data);
@@ -3,9 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.registerFileTools = registerFileTools;
4
4
  const zod_1 = require("zod");
5
5
  const responses_js_1 = require("../utils/responses.js");
6
- const READ_ONLY_ANNOTATIONS = { readOnlyHint: true, destructiveHint: false };
7
- const WRITE_ANNOTATIONS = { readOnlyHint: false, destructiveHint: false };
8
- const DESTRUCTIVE_ANNOTATIONS = { readOnlyHint: false, destructiveHint: true };
6
+ const safety_js_1 = require("../utils/safety.js");
9
7
  function registerFileTools(server, client, readOnly) {
10
8
  server.registerTool("protect_list_files", {
11
9
  description: "List files of a given type (e.g. 'video', 'timelapse')",
@@ -14,7 +12,7 @@ function registerFileTools(server, client, readOnly) {
14
12
  .string()
15
13
  .describe("File type to list (e.g. 'video', 'timelapse')"),
16
14
  },
17
- annotations: READ_ONLY_ANNOTATIONS,
15
+ annotations: safety_js_1.READ_ONLY,
18
16
  }, async ({ fileType }) => {
19
17
  try {
20
18
  const data = await client.get(`/files/${fileType}`);
@@ -34,8 +32,11 @@ function registerFileTools(server, client, readOnly) {
34
32
  .literal(true)
35
33
  .describe("Must be true to confirm triggering the alarm webhook"),
36
34
  },
37
- annotations: DESTRUCTIVE_ANNOTATIONS,
38
- }, async ({ id }) => {
35
+ annotations: safety_js_1.DESTRUCTIVE,
36
+ }, async ({ id, confirm }) => {
37
+ const denied = (0, safety_js_1.requireConfirmation)(confirm, "trigger the alarm webhook");
38
+ if (denied)
39
+ return denied;
39
40
  try {
40
41
  await client.post(`/alarm-manager/webhook/${id}`);
41
42
  return (0, responses_js_1.formatSuccess)({ triggered: true, webhookId: id });
@@ -58,14 +59,11 @@ function registerFileTools(server, client, readOnly) {
58
59
  .optional()
59
60
  .describe("If true, return what would happen without making changes"),
60
61
  },
61
- annotations: WRITE_ANNOTATIONS,
62
+ annotations: safety_js_1.WRITE,
62
63
  }, async ({ fileType, base64Data, contentType, dryRun }) => {
63
64
  try {
64
65
  if (dryRun) {
65
- return (0, responses_js_1.formatSuccess)({
66
- dryRun: true,
67
- action: "POST",
68
- path: `/files/${fileType}`,
66
+ return (0, safety_js_1.formatDryRun)("POST", `/files/${fileType}`, {
69
67
  contentType,
70
68
  dataLength: base64Data.length,
71
69
  });
@@ -3,12 +3,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.registerLiveviewTools = registerLiveviewTools;
4
4
  const zod_1 = require("zod");
5
5
  const responses_js_1 = require("../utils/responses.js");
6
- const READ_ONLY_ANNOTATIONS = { readOnlyHint: true, destructiveHint: false };
7
- const WRITE_ANNOTATIONS = { readOnlyHint: false, destructiveHint: false };
6
+ const safety_js_1 = require("../utils/safety.js");
8
7
  function registerLiveviewTools(server, client, readOnly) {
9
8
  server.registerTool("protect_list_liveviews", {
10
9
  description: "List all live views configured in UniFi Protect",
11
- annotations: READ_ONLY_ANNOTATIONS,
10
+ annotations: safety_js_1.READ_ONLY,
12
11
  }, async () => {
13
12
  try {
14
13
  const data = await client.get("/liveviews");
@@ -21,7 +20,7 @@ function registerLiveviewTools(server, client, readOnly) {
21
20
  server.registerTool("protect_get_liveview", {
22
21
  description: "Get details for a specific live view by ID",
23
22
  inputSchema: { id: zod_1.z.string().describe("Liveview ID") },
24
- annotations: READ_ONLY_ANNOTATIONS,
23
+ annotations: safety_js_1.READ_ONLY,
25
24
  }, async ({ id }) => {
26
25
  try {
27
26
  const data = await client.get(`/liveviews/${id}`);
@@ -44,11 +43,11 @@ function registerLiveviewTools(server, client, readOnly) {
44
43
  .optional()
45
44
  .describe("If true, return what would happen without making changes"),
46
45
  },
47
- annotations: WRITE_ANNOTATIONS,
46
+ annotations: safety_js_1.WRITE,
48
47
  }, async ({ settings, dryRun }) => {
49
48
  try {
50
49
  if (dryRun) {
51
- return (0, responses_js_1.formatSuccess)({ dryRun: true, action: "POST", path: "/liveviews", body: settings });
50
+ return (0, safety_js_1.formatDryRun)("POST", "/liveviews", settings);
52
51
  }
53
52
  const data = await client.post("/liveviews", settings);
54
53
  return (0, responses_js_1.formatSuccess)(data);
@@ -69,11 +68,11 @@ function registerLiveviewTools(server, client, readOnly) {
69
68
  .optional()
70
69
  .describe("If true, return what would happen without making changes"),
71
70
  },
72
- annotations: WRITE_ANNOTATIONS,
71
+ annotations: safety_js_1.WRITE,
73
72
  }, async ({ id, settings, dryRun }) => {
74
73
  try {
75
74
  if (dryRun) {
76
- return (0, responses_js_1.formatSuccess)({ dryRun: true, action: "PATCH", path: `/liveviews/${id}`, body: settings });
75
+ return (0, safety_js_1.formatDryRun)("PATCH", `/liveviews/${id}`, settings);
77
76
  }
78
77
  const data = await client.patch(`/liveviews/${id}`, settings);
79
78
  return (0, responses_js_1.formatSuccess)(data);
@@ -2,11 +2,11 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.registerSystemTools = registerSystemTools;
4
4
  const responses_js_1 = require("../utils/responses.js");
5
- const READ_ONLY_ANNOTATIONS = { readOnlyHint: true, destructiveHint: false };
5
+ const safety_js_1 = require("../utils/safety.js");
6
6
  function registerSystemTools(server, client) {
7
7
  server.registerTool("protect_get_info", {
8
8
  description: "Get UniFi Protect system information and version details",
9
- annotations: READ_ONLY_ANNOTATIONS,
9
+ annotations: safety_js_1.READ_ONLY,
10
10
  }, async () => {
11
11
  try {
12
12
  const data = await client.get("/meta/info");
@@ -18,7 +18,7 @@ function registerSystemTools(server, client) {
18
18
  });
19
19
  server.registerTool("protect_list_nvrs", {
20
20
  description: "List all NVR (Network Video Recorder) devices",
21
- annotations: READ_ONLY_ANNOTATIONS,
21
+ annotations: safety_js_1.READ_ONLY,
22
22
  }, async () => {
23
23
  try {
24
24
  const data = await client.get("/nvrs");
@@ -0,0 +1,34 @@
1
+ /** Annotation constants for tool safety classification */
2
+ export declare const READ_ONLY: {
3
+ readonly readOnlyHint: true;
4
+ readonly destructiveHint: false;
5
+ };
6
+ export declare const WRITE: {
7
+ readonly readOnlyHint: false;
8
+ readonly destructiveHint: false;
9
+ };
10
+ export declare const DESTRUCTIVE: {
11
+ readonly readOnlyHint: false;
12
+ readonly destructiveHint: true;
13
+ };
14
+ /**
15
+ * Build a standard dry-run preview response.
16
+ * Returns the MCP tool result that describes what *would* happen.
17
+ */
18
+ export declare function formatDryRun(method: string, path: string, body?: unknown): {
19
+ content: {
20
+ type: "text";
21
+ text: string;
22
+ }[];
23
+ };
24
+ /**
25
+ * Guard for destructive actions that require explicit confirmation.
26
+ * Returns an error result if `confirm` is not `true`, otherwise `null`.
27
+ */
28
+ export declare function requireConfirmation(confirm: boolean, action: string): {
29
+ content: {
30
+ type: "text";
31
+ text: string;
32
+ }[];
33
+ isError: boolean;
34
+ } | null;
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DESTRUCTIVE = exports.WRITE = exports.READ_ONLY = void 0;
4
+ exports.formatDryRun = formatDryRun;
5
+ exports.requireConfirmation = requireConfirmation;
6
+ const responses_js_1 = require("./responses.js");
7
+ /** Annotation constants for tool safety classification */
8
+ exports.READ_ONLY = { readOnlyHint: true, destructiveHint: false };
9
+ exports.WRITE = { readOnlyHint: false, destructiveHint: false };
10
+ exports.DESTRUCTIVE = { readOnlyHint: false, destructiveHint: true };
11
+ /**
12
+ * Build a standard dry-run preview response.
13
+ * Returns the MCP tool result that describes what *would* happen.
14
+ */
15
+ function formatDryRun(method, path, body) {
16
+ const preview = { dryRun: true, action: method, path };
17
+ if (body !== undefined) {
18
+ preview.body = body;
19
+ }
20
+ return (0, responses_js_1.formatSuccess)(preview);
21
+ }
22
+ /**
23
+ * Guard for destructive actions that require explicit confirmation.
24
+ * Returns an error result if `confirm` is not `true`, otherwise `null`.
25
+ */
26
+ function requireConfirmation(confirm, action) {
27
+ if (confirm !== true) {
28
+ return {
29
+ content: [
30
+ {
31
+ type: "text",
32
+ text: `Error: You must set confirm to true to ${action}`,
33
+ },
34
+ ],
35
+ isError: true,
36
+ };
37
+ }
38
+ return null;
39
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owine/unifi-protect-mcp",
3
- "version": "2.0.0",
3
+ "version": "2.0.1",
4
4
  "description": "MCP server for UniFi Protect Integration API",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -20,7 +20,7 @@
20
20
  "lint:fix": "eslint . --fix"
21
21
  },
22
22
  "engines": {
23
- "node": ">=20.0.0"
23
+ "node": "^20.19.0 || ^22.13.0 || >=24"
24
24
  },
25
25
  "repository": {
26
26
  "type": "git",
@@ -40,12 +40,12 @@
40
40
  "zod": "^4.0.0"
41
41
  },
42
42
  "devDependencies": {
43
- "@eslint/js": "9.39.2",
43
+ "@eslint/js": "10.0.1",
44
44
  "@types/node": "24.10.13",
45
45
  "@vitest/coverage-v8": "4.0.18",
46
- "eslint": "9.39.2",
46
+ "eslint": "10.0.0",
47
47
  "typescript": "5.9.3",
48
- "typescript-eslint": "8.55.0",
48
+ "typescript-eslint": "8.56.0",
49
49
  "vitest": "4.0.18"
50
50
  }
51
51
  }