@owine/unifi-protect-mcp 2.14.2 → 3.0.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.
Files changed (60) hide show
  1. package/README.md +248 -92
  2. package/dist/client.d.ts +43 -3
  3. package/dist/client.js +255 -85
  4. package/dist/config.d.ts +7 -1
  5. package/dist/config.js +114 -6
  6. package/dist/index.js +14 -6
  7. package/dist/instructions.d.ts +6 -0
  8. package/dist/instructions.js +24 -0
  9. package/dist/schemas/cameras.d.ts +32 -30
  10. package/dist/schemas/cameras.js +7 -6
  11. package/dist/schemas/common.d.ts +31 -6
  12. package/dist/schemas/common.js +45 -6
  13. package/dist/schemas/devices.d.ts +2 -10
  14. package/dist/schemas/devices.js +35 -26
  15. package/dist/schemas/misc.d.ts +50 -18
  16. package/dist/schemas/misc.js +23 -15
  17. package/dist/schemas/pos.d.ts +1 -1
  18. package/dist/schemas/pos.js +1 -1
  19. package/dist/tls.d.ts +41 -0
  20. package/dist/tls.js +75 -0
  21. package/dist/tools/alarm-manager.d.ts +3 -0
  22. package/dist/tools/alarm-manager.js +32 -0
  23. package/dist/tools/arm-profiles.d.ts +8 -2
  24. package/dist/tools/arm-profiles.js +98 -95
  25. package/dist/tools/cameras.d.ts +2 -2
  26. package/dist/tools/cameras.js +156 -181
  27. package/dist/tools/device-actions.d.ts +2 -2
  28. package/dist/tools/device-actions.js +84 -97
  29. package/dist/tools/devices.d.ts +6 -3
  30. package/dist/tools/devices.js +50 -51
  31. package/dist/tools/files.d.ts +2 -2
  32. package/dist/tools/files.js +82 -57
  33. package/dist/tools/index.d.ts +2 -2
  34. package/dist/tools/index.js +15 -10
  35. package/dist/tools/liveviews.d.ts +2 -2
  36. package/dist/tools/liveviews.js +52 -52
  37. package/dist/tools/pos.d.ts +6 -4
  38. package/dist/tools/pos.js +15 -20
  39. package/dist/tools/registrar.d.ts +34 -0
  40. package/dist/tools/registrar.js +103 -0
  41. package/dist/tools/subscriptions.d.ts +13 -2
  42. package/dist/tools/subscriptions.js +148 -27
  43. package/dist/tools/system.d.ts +2 -2
  44. package/dist/tools/system.js +5 -15
  45. package/dist/tools/users.d.ts +2 -2
  46. package/dist/tools/users.js +18 -35
  47. package/dist/utils/limits.d.ts +33 -0
  48. package/dist/utils/limits.js +74 -0
  49. package/dist/utils/paging.d.ts +15 -0
  50. package/dist/utils/paging.js +28 -0
  51. package/dist/utils/params.d.ts +13 -0
  52. package/dist/utils/params.js +40 -0
  53. package/dist/utils/responses.js +2 -3
  54. package/dist/utils/safety.d.ts +39 -3
  55. package/dist/utils/safety.js +45 -3
  56. package/dist/utils/url.d.ts +4 -1
  57. package/dist/utils/url.js +4 -1
  58. package/dist/version.d.ts +6 -0
  59. package/dist/version.js +11 -0
  60. package/package.json +14 -12
@@ -5,44 +5,41 @@ 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
7
  const url_js_1 = require("../utils/url.js");
8
+ const paging_js_1 = require("../utils/paging.js");
9
+ const params_js_1 = require("../utils/params.js");
8
10
  const cameras_js_1 = require("../schemas/cameras.js");
11
+ // Each gate's confirm input and handler check share one action text.
12
+ const OPEN_STREAM = (0, safety_js_1.confirmGate)("open an RTSPS stream (its URL is a credential for live video)");
13
+ const OPEN_TALKBACK = (0, safety_js_1.confirmGate)("open a two-way audio session");
14
+ const DISABLE_MIC = (0, safety_js_1.confirmGate)("permanently disable the microphone");
9
15
  function registerCameraTools(server, client, readOnly) {
10
16
  // --- Read-only tools (always registered) ---
11
17
  server.registerTool("protect_list_cameras", {
12
- description: "List all cameras managed by UniFi Protect. Returns array; each camera includes (Integration API 7.3.47-verified fields): id, mac, name, modelKey, state (CONNECTED/DISCONNECTED), type (hardware model name, e.g. UVC G6 Turret), guid (hardware MODEL GUID — every camera of the same model returns the same value, so it is NOT a per-camera identifier; use id), activePatrolSlot, hasPackageCamera, hdrType, isMicEnabled, micVolume, videoMode, featureFlags (hasHdr, hasMic, hasSpeaker, hasLedStatus, smartDetectTypes[], smartDetectAudioTypes[], videoModes[], supportFullHdSnapshot), lcdMessage (type, resetAt, text), ledSettings (isEnabled, floodLed, welcomeLed), osdSettings (isNameEnabled, isDateEnabled, isLogoEnabled, isDebugEnabled, overlayLocation), smartDetectSettings (objectTypes[], audioTypes[]). The Integration API does NOT expose recording state, motion timestamps, connection/last-seen, firmware, host, or per-channel stream config.",
18
+ description: "List all cameras managed by UniFi Protect. Returns array; each camera includes (Integration API 7.3.70-verified fields): id, mac, name, modelKey, state (CONNECTED/DISCONNECTED), type (hardware model name, e.g. UVC G6 Turret), guid (hardware MODEL GUID — every camera of the same model returns the same value, so it is NOT a per-camera identifier; use id), activePatrolSlot, hasPackageCamera, hdrType, isMicEnabled, micVolume, videoMode, featureFlags (hasHdr, hasMic, hasSpeaker, hasLedStatus, smartDetectTypes[], smartDetectAudioTypes[], videoModes[], supportFullHdSnapshot), lcdMessage (type, resetAt, text), ledSettings (isEnabled, floodLed, welcomeLed), osdSettings (isNameEnabled, isDateEnabled, isLogoEnabled, isDebugEnabled, overlayLocation), smartDetectSettings (objectTypes[], audioTypes[]). The Integration API does NOT expose recording state, motion timestamps, connection/last-seen, firmware, host, or per-channel stream config." + paging_js_1.PAGING_NOTE,
19
+ inputSchema: paging_js_1.pagingInput,
13
20
  outputSchema: cameras_js_1.cameraListOutputSchema,
14
21
  annotations: safety_js_1.READ_ONLY,
15
- }, async () => {
16
- try {
17
- const data = await client.get("/cameras");
18
- return (0, responses_js_1.formatSuccess)(data);
19
- }
20
- catch (err) {
21
- return (0, responses_js_1.formatError)(err);
22
- }
22
+ }, async ({ limit, offset }) => {
23
+ const data = await client.get("/cameras");
24
+ return (0, responses_js_1.formatSuccess)((0, paging_js_1.pageOf)(data, limit, offset));
23
25
  });
24
26
  server.registerTool("protect_get_camera", {
25
- description: "Get details for a specific camera by ID. The Protect Integration API returns the SAME field set as protect_list_cameras entries (id, mac, name, modelKey, state, type, guid, activePatrolSlot, hasPackageCamera, hdrType, isMicEnabled, micVolume, videoMode, featureFlags, lcdMessage, ledSettings, osdSettings, smartDetectSettings) — there is no extended/by-id-only payload (confirmed live on 7.3.47). Recording state, motion events, zones, and channel/RTSP config are NOT exposed by this API surface.",
26
- inputSchema: { id: zod_1.z.string().describe("Camera ID") },
27
+ description: "Get details for a specific camera by ID. The Protect Integration API returns the SAME field set as protect_list_cameras entries (id, mac, name, modelKey, state, type, guid, activePatrolSlot, hasPackageCamera, hdrType, isMicEnabled, micVolume, videoMode, featureFlags, lcdMessage, ledSettings, osdSettings, smartDetectSettings) — there is no extended/by-id-only payload (confirmed live on 7.3.70). Recording state, motion events, zones, and channel/RTSP config are NOT exposed by this API surface.",
28
+ inputSchema: { id: (0, params_js_1.resourceId)().describe("Camera ID") },
27
29
  outputSchema: cameras_js_1.cameraOutputSchema,
28
30
  annotations: safety_js_1.READ_ONLY,
29
31
  }, async ({ id }) => {
30
- try {
31
- const data = await client.get((0, url_js_1.safePath) `/cameras/${id}`);
32
- return (0, responses_js_1.formatSuccess)(data);
33
- }
34
- catch (err) {
35
- return (0, responses_js_1.formatError)(err);
36
- }
32
+ const data = await client.get((0, url_js_1.safePath) `/cameras/${id}`);
33
+ return (0, responses_js_1.formatSuccess)(data);
37
34
  });
38
35
  server.registerTool("protect_get_snapshot", {
39
- description: "Get a JPEG snapshot from a camera. Returns a base64-encoded image/jpeg (rendered directly by MCP clients). Use highQuality=true for full-resolution capture; set channel=package to capture from the secondary package camera on doorbells with hasPackageCamera=true.",
36
+ description: "Get a JPEG snapshot from a camera. Returns a base64-encoded image/jpeg (rendered directly by MCP clients). Use highQuality=true to force a 1080p-or-higher capture; set channel=package to capture from the secondary package camera on doorbells with hasPackageCamera=true. Fails with HTTP 503 when the camera is offline or unreachable. A snapshot too large for one MCP message is refused with an error; try without highQuality.",
40
37
  inputSchema: {
41
- id: zod_1.z.string().describe("Camera ID"),
38
+ id: (0, params_js_1.resourceId)().describe("Camera ID"),
42
39
  highQuality: zod_1.z
43
40
  .boolean()
44
41
  .optional()
45
- .describe("If true, request a high-quality snapshot"),
42
+ .describe("If true, force a 1080p-or-higher resolution snapshot"),
46
43
  channel: zod_1.z
47
44
  .enum(["main", "package"])
48
45
  .optional()
@@ -50,237 +47,215 @@ function registerCameraTools(server, client, readOnly) {
50
47
  },
51
48
  annotations: safety_js_1.READ_ONLY,
52
49
  }, async ({ id, highQuality, channel }) => {
53
- try {
54
- const params = new URLSearchParams();
55
- if (highQuality)
56
- params.set("highQuality", "true");
57
- if (channel)
58
- params.set("channel", channel);
59
- const query = params.toString();
60
- const url = query
61
- ? (0, url_js_1.safePath) `/cameras/${id}/snapshot` + `?${query}`
62
- : (0, url_js_1.safePath) `/cameras/${id}/snapshot`;
63
- const { data, mimeType } = await client.getBinary(url);
64
- return {
65
- content: [
66
- {
67
- type: "image",
68
- data: data.toString("base64"),
69
- mimeType,
70
- },
71
- ],
72
- };
73
- }
74
- catch (err) {
75
- return (0, responses_js_1.formatError)(err);
76
- }
50
+ const params = new URLSearchParams();
51
+ if (highQuality)
52
+ params.set("highQuality", "true");
53
+ if (channel)
54
+ params.set("channel", channel);
55
+ const query = params.toString();
56
+ const url = query
57
+ ? (0, url_js_1.safePath) `/cameras/${id}/snapshot` + `?${query}`
58
+ : (0, url_js_1.safePath) `/cameras/${id}/snapshot`;
59
+ const { data, mimeType } = await client.getBinary(url);
60
+ return {
61
+ content: [
62
+ {
63
+ type: "image",
64
+ data: data.toString("base64"),
65
+ mimeType,
66
+ },
67
+ ],
68
+ };
77
69
  });
78
70
  server.registerTool("protect_get_rtsp_streams", {
79
- description: "Get active RTSPS stream sessions for a camera. Returns the per-quality stream URLs currently published (keys typically: high, medium, low, package). Empty/missing keys mean no session is currently active at that quality — use protect_create_rtsp_stream to start one.",
80
- inputSchema: { id: zod_1.z.string().describe("Camera ID") },
71
+ description: "Get active RTSPS stream sessions for a camera. Returns: high, medium, low, package — each an rtsps:// URL, or null (or absent) when no session is active at that quality. Use protect_create_rtsp_stream to start one. Each URL is a bearer credential for live video: do not repeat it unless the user asked for it.",
72
+ inputSchema: { id: (0, params_js_1.resourceId)().describe("Camera ID") },
81
73
  outputSchema: cameras_js_1.rtspStreamOutputSchema,
82
74
  annotations: safety_js_1.READ_ONLY,
83
75
  }, async ({ id }) => {
84
- try {
85
- const data = await client.get((0, url_js_1.safePath) `/cameras/${id}/rtsps-stream`);
86
- return (0, responses_js_1.formatSuccess)(data);
87
- }
88
- catch (err) {
89
- return (0, responses_js_1.formatError)(err);
90
- }
76
+ const data = await client.get((0, url_js_1.safePath) `/cameras/${id}/rtsps-stream`);
77
+ return (0, responses_js_1.formatSuccess)(data);
91
78
  });
92
79
  // --- Write tools (skipped in read-only mode) ---
93
80
  if (readOnly)
94
81
  return;
95
82
  server.registerTool("protect_update_camera", {
96
- description: "Update camera settings (partial update via PATCH)",
83
+ description: "Update camera settings (partial update via PATCH). Returns: the full updated camera (same fields as protect_get_camera).",
97
84
  inputSchema: {
98
- id: zod_1.z.string().describe("Camera ID"),
85
+ id: (0, params_js_1.resourceId)().describe("Camera ID"),
99
86
  settings: zod_1.z
100
- .record(zod_1.z.string(), zod_1.z.unknown())
101
- .describe("Partial camera settings to update. Known fields: name (string), osdSettings (object), ledSettings (object), lcdMessage (object: type, resetAt, text), videoMode (string: \"default\" | \"highFps\" | \"sport\" | \"slowShutter\" | \"lprReflex\" | \"lprNoneReflex\"), hdrType (string: \"auto\" | \"on\" | \"off\"), micVolume (number 0-100), smartDetectSettings (object)"),
102
- dryRun: zod_1.z
103
- .boolean()
104
- .optional()
105
- .describe("If true, return what would happen without making changes"),
87
+ .object({
88
+ name: zod_1.z.string().optional().describe("Camera name"),
89
+ osdSettings: zod_1.z
90
+ .record(zod_1.z.string(), zod_1.z.unknown())
91
+ .optional()
92
+ .describe("On-screen display (isNameEnabled, isDateEnabled, isLogoEnabled, isDebugEnabled, overlayLocation)"),
93
+ ledSettings: zod_1.z
94
+ .record(zod_1.z.string(), zod_1.z.unknown())
95
+ .optional()
96
+ .describe("LED settings (isEnabled, welcomeLed, floodLed)"),
97
+ lcdMessage: zod_1.z
98
+ .record(zod_1.z.string(), zod_1.z.unknown())
99
+ .optional()
100
+ .describe('Doorbell LCD message (type, resetAt, text), e.g. { type: "DO_NOT_DISTURB", resetAt: 0 }'),
101
+ micVolume: zod_1.z.number().min(0).max(100).optional().describe("Mic volume (0-100)"),
102
+ videoMode: zod_1.z
103
+ .enum(["default", "highFps", "sport", "slowShutter", "lprReflex", "lprNoneReflex"])
104
+ .optional()
105
+ .describe("Video mode"),
106
+ hdrType: zod_1.z.enum(["auto", "on", "off"]).optional().describe("HDR mode"),
107
+ smartDetectSettings: zod_1.z
108
+ .record(zod_1.z.string(), zod_1.z.unknown())
109
+ .optional()
110
+ .describe("Smart detection (objectTypes[], audioTypes[])"),
111
+ })
112
+ .loose()
113
+ .refine((o) => Object.keys(o).length > 0, "settings must include at least one field")
114
+ .describe("Partial camera settings to update"),
115
+ dryRun: (0, safety_js_1.dryRunInput)(),
106
116
  },
107
117
  outputSchema: cameras_js_1.cameraOutputSchema,
108
- annotations: safety_js_1.WRITE,
118
+ annotations: safety_js_1.DESTRUCTIVE,
109
119
  }, async ({ id, settings, dryRun }) => {
110
- try {
111
- if (dryRun) {
112
- return (0, safety_js_1.formatDryRun)("PATCH", (0, url_js_1.safePath) `/cameras/${id}`, settings);
113
- }
114
- const data = await client.patch((0, url_js_1.safePath) `/cameras/${id}`, settings);
115
- return (0, responses_js_1.formatSuccess)(data);
116
- }
117
- catch (err) {
118
- return (0, responses_js_1.formatError)(err);
120
+ if (dryRun) {
121
+ return (0, safety_js_1.formatDryRun)("PATCH", (0, url_js_1.safePath) `/cameras/${id}`, settings);
119
122
  }
123
+ const data = await client.patch((0, url_js_1.safePath) `/cameras/${id}`, settings);
124
+ return (0, responses_js_1.formatSuccess)(data);
120
125
  });
121
126
  server.registerTool("protect_create_rtsp_stream", {
122
- description: "Create an RTSPS stream session for a camera",
127
+ description: "Create an RTSPS stream session for a camera. Returns: one key per requested quality (high, medium, low, package), each an rtsps:// URL. The package quality is only available on cameras with hasPackageCamera=true. Requires confirm: true: the returned URLs are bearer credentials for live video.",
123
128
  inputSchema: {
124
- id: zod_1.z.string().describe("Camera ID"),
129
+ id: (0, params_js_1.resourceId)().describe("Camera ID"),
125
130
  qualities: zod_1.z
126
131
  .array(zod_1.z.enum(["high", "medium", "low", "package"]))
127
- .describe("Quality levels to include in the RTSPS stream"),
128
- dryRun: zod_1.z
129
- .boolean()
130
- .optional()
131
- .describe("If true, return what would happen without making changes"),
132
+ .min(1)
133
+ .describe("Quality levels to include in the RTSPS stream (at least one)"),
134
+ confirm: OPEN_STREAM.input,
135
+ dryRun: (0, safety_js_1.dryRunInput)(),
132
136
  },
133
137
  outputSchema: cameras_js_1.rtspStreamOutputSchema,
134
- annotations: safety_js_1.WRITE,
135
- }, async ({ id, qualities, dryRun }) => {
136
- try {
137
- if (dryRun) {
138
- return (0, safety_js_1.formatDryRun)("POST", (0, url_js_1.safePath) `/cameras/${id}/rtsps-stream`, { qualities });
139
- }
140
- const data = await client.post((0, url_js_1.safePath) `/cameras/${id}/rtsps-stream`, {
141
- qualities,
142
- });
143
- return (0, responses_js_1.formatSuccess)(data);
144
- }
145
- catch (err) {
146
- return (0, responses_js_1.formatError)(err);
138
+ annotations: safety_js_1.DESTRUCTIVE,
139
+ }, async ({ id, qualities, confirm, dryRun }) => {
140
+ if (dryRun) {
141
+ return (0, safety_js_1.formatDryRun)("POST", (0, url_js_1.safePath) `/cameras/${id}/rtsps-stream`, { qualities });
147
142
  }
143
+ const denied = OPEN_STREAM.check(confirm);
144
+ if (denied)
145
+ return denied;
146
+ const data = await client.post((0, url_js_1.safePath) `/cameras/${id}/rtsps-stream`, {
147
+ qualities,
148
+ });
149
+ return (0, responses_js_1.formatSuccess)(data);
148
150
  });
149
151
  server.registerTool("protect_delete_rtsp_stream", {
150
152
  description: "Stop and delete an active RTSPS stream session for a camera",
151
153
  inputSchema: {
152
- id: zod_1.z.string().describe("Camera ID"),
154
+ id: (0, params_js_1.resourceId)().describe("Camera ID"),
153
155
  qualities: zod_1.z
154
156
  .array(zod_1.z.enum(["high", "medium", "low", "package"]))
155
- .describe("Quality levels to remove from the RTSPS stream"),
156
- dryRun: zod_1.z
157
- .boolean()
158
- .optional()
159
- .describe("If true, return what would happen without making changes"),
157
+ .min(1)
158
+ .describe("Quality levels to remove from the RTSPS stream (at least one)"),
159
+ dryRun: (0, safety_js_1.dryRunInput)(),
160
160
  },
161
161
  annotations: safety_js_1.DESTRUCTIVE,
162
162
  }, async ({ id, qualities, dryRun }) => {
163
- try {
164
- if (dryRun) {
165
- return (0, safety_js_1.formatDryRun)("DELETE", (0, url_js_1.safePath) `/cameras/${id}/rtsps-stream?qualities=${qualities.join(",")}`);
166
- }
167
- const data = await client.delete((0, url_js_1.safePath) `/cameras/${id}/rtsps-stream?qualities=${qualities.join(",")}`);
168
- return (0, responses_js_1.formatSuccess)(data);
169
- }
170
- catch (err) {
171
- return (0, responses_js_1.formatError)(err);
163
+ // Protect expects repeated keys (qualities=high&qualities=medium), not
164
+ // a comma-joined value — safePath would encode the comma as %2C.
165
+ const params = new URLSearchParams();
166
+ for (const quality of qualities)
167
+ params.append("qualities", quality);
168
+ const url = (0, url_js_1.safePath) `/cameras/${id}/rtsps-stream` + `?${params.toString()}`;
169
+ if (dryRun) {
170
+ return (0, safety_js_1.formatDryRun)("DELETE", url);
172
171
  }
172
+ const data = await client.delete(url);
173
+ return (0, responses_js_1.formatSuccess)(data);
173
174
  });
174
175
  server.registerTool("protect_create_talkback", {
175
- description: "Create a talkback (two-way audio) session for a camera. Returns: url, codec, samplingRate, bitsPerSample (the audio config for encoding outbound audio).",
176
+ description: "Create a talkback (two-way audio) session for a camera. Returns: url, codec, samplingRate, bitsPerSample (the audio config for encoding outbound audio). Requires confirm: true: the session opens two-way audio to the camera, and the returned url lets anyone holding it talk through the camera.",
176
177
  inputSchema: {
177
- id: zod_1.z.string().describe("Camera ID"),
178
- dryRun: zod_1.z
179
- .boolean()
180
- .optional()
181
- .describe("If true, return what would happen without making changes"),
178
+ id: (0, params_js_1.resourceId)().describe("Camera ID"),
179
+ confirm: OPEN_TALKBACK.input,
180
+ dryRun: (0, safety_js_1.dryRunInput)(),
182
181
  },
183
182
  outputSchema: cameras_js_1.talkbackSessionOutputSchema,
184
- annotations: safety_js_1.WRITE,
185
- }, async ({ id, dryRun }) => {
186
- try {
187
- if (dryRun) {
188
- return (0, safety_js_1.formatDryRun)("POST", (0, url_js_1.safePath) `/cameras/${id}/talkback-session`);
189
- }
190
- const data = await client.post((0, url_js_1.safePath) `/cameras/${id}/talkback-session`);
191
- return (0, responses_js_1.formatSuccess)(data);
192
- }
193
- catch (err) {
194
- return (0, responses_js_1.formatError)(err);
183
+ annotations: safety_js_1.DESTRUCTIVE,
184
+ }, async ({ id, confirm, dryRun }) => {
185
+ if (dryRun) {
186
+ return (0, safety_js_1.formatDryRun)("POST", (0, url_js_1.safePath) `/cameras/${id}/talkback-session`);
195
187
  }
188
+ const denied = OPEN_TALKBACK.check(confirm);
189
+ if (denied)
190
+ return denied;
191
+ const data = await client.post((0, url_js_1.safePath) `/cameras/${id}/talkback-session`);
192
+ return (0, responses_js_1.formatSuccess)(data);
196
193
  });
197
194
  server.registerTool("protect_disable_mic", {
198
- description: "IRREVERSIBLE: Permanently disable the microphone on a camera. Can only be re-enabled by factory resetting the camera.",
195
+ description: "IRREVERSIBLE: Permanently disable the microphone on a camera. Can only be re-enabled by factory resetting the camera. Returns: the full updated camera (same fields as protect_get_camera). Requires confirm: true.",
199
196
  inputSchema: {
200
- id: zod_1.z.string().describe("Camera ID"),
201
- confirm: zod_1.z
202
- .literal(true)
203
- .describe("Must be true to confirm this irreversible action"),
197
+ id: (0, params_js_1.resourceId)().describe("Camera ID"),
198
+ confirm: DISABLE_MIC.input,
199
+ dryRun: (0, safety_js_1.dryRunInput)(),
204
200
  },
201
+ outputSchema: cameras_js_1.cameraOutputSchema,
205
202
  annotations: safety_js_1.DESTRUCTIVE,
206
- }, async ({ id, confirm }) => {
207
- const denied = (0, safety_js_1.requireConfirmation)(confirm, "permanently disable the microphone");
203
+ }, async ({ id, confirm, dryRun }) => {
204
+ if (dryRun) {
205
+ return (0, safety_js_1.formatDryRun)("POST", (0, url_js_1.safePath) `/cameras/${id}/disable-mic-permanently`);
206
+ }
207
+ const denied = DISABLE_MIC.check(confirm);
208
208
  if (denied)
209
209
  return denied;
210
- try {
211
- const data = await client.post((0, url_js_1.safePath) `/cameras/${id}/disable-mic-permanently`);
212
- return (0, responses_js_1.formatSuccess)(data);
213
- }
214
- catch (err) {
215
- return (0, responses_js_1.formatError)(err);
216
- }
210
+ const data = await client.post((0, url_js_1.safePath) `/cameras/${id}/disable-mic-permanently`);
211
+ return (0, responses_js_1.formatSuccess)(data);
217
212
  });
218
213
  server.registerTool("protect_start_ptz_patrol", {
219
214
  description: "Start PTZ patrol on a camera at a given slot",
220
215
  inputSchema: {
221
- id: zod_1.z.string().describe("Camera ID"),
216
+ id: (0, params_js_1.resourceId)().describe("Camera ID"),
222
217
  slot: zod_1.z.number().int().min(0).max(4).describe("Patrol slot number (0-4)"),
223
- dryRun: zod_1.z
224
- .boolean()
225
- .optional()
226
- .describe("If true, return what would happen without making changes"),
218
+ dryRun: (0, safety_js_1.dryRunInput)(),
227
219
  },
228
220
  annotations: safety_js_1.WRITE,
229
221
  }, async ({ id, slot, dryRun }) => {
230
- try {
231
- if (dryRun) {
232
- return (0, safety_js_1.formatDryRun)("POST", (0, url_js_1.safePath) `/cameras/${id}/ptz/patrol/start/${slot}`);
233
- }
234
- const data = await client.post((0, url_js_1.safePath) `/cameras/${id}/ptz/patrol/start/${slot}`);
235
- return (0, responses_js_1.formatSuccess)(data);
236
- }
237
- catch (err) {
238
- return (0, responses_js_1.formatError)(err);
222
+ if (dryRun) {
223
+ return (0, safety_js_1.formatDryRun)("POST", (0, url_js_1.safePath) `/cameras/${id}/ptz/patrol/start/${slot}`);
239
224
  }
225
+ const data = await client.post((0, url_js_1.safePath) `/cameras/${id}/ptz/patrol/start/${slot}`);
226
+ return (0, responses_js_1.formatSuccess)(data);
240
227
  });
241
228
  server.registerTool("protect_stop_ptz_patrol", {
242
229
  description: "Stop PTZ patrol on a camera",
243
230
  inputSchema: {
244
- id: zod_1.z.string().describe("Camera ID"),
245
- dryRun: zod_1.z
246
- .boolean()
247
- .optional()
248
- .describe("If true, return what would happen without making changes"),
231
+ id: (0, params_js_1.resourceId)().describe("Camera ID"),
232
+ dryRun: (0, safety_js_1.dryRunInput)(),
249
233
  },
250
234
  annotations: safety_js_1.WRITE,
251
235
  }, async ({ id, dryRun }) => {
252
- try {
253
- if (dryRun) {
254
- return (0, safety_js_1.formatDryRun)("POST", (0, url_js_1.safePath) `/cameras/${id}/ptz/patrol/stop`);
255
- }
256
- const data = await client.post((0, url_js_1.safePath) `/cameras/${id}/ptz/patrol/stop`);
257
- return (0, responses_js_1.formatSuccess)(data);
258
- }
259
- catch (err) {
260
- return (0, responses_js_1.formatError)(err);
236
+ if (dryRun) {
237
+ return (0, safety_js_1.formatDryRun)("POST", (0, url_js_1.safePath) `/cameras/${id}/ptz/patrol/stop`);
261
238
  }
239
+ const data = await client.post((0, url_js_1.safePath) `/cameras/${id}/ptz/patrol/stop`);
240
+ return (0, responses_js_1.formatSuccess)(data);
262
241
  });
263
242
  server.registerTool("protect_goto_ptz_preset", {
264
243
  description: "Move camera PTZ to a preset position",
265
244
  inputSchema: {
266
- id: zod_1.z.string().describe("Camera ID"),
267
- slot: zod_1.z.number().int().min(-1).max(4).describe("PTZ preset slot number (0-4, or -1 for home position)"),
268
- dryRun: zod_1.z
269
- .boolean()
270
- .optional()
271
- .describe("If true, return what would happen without making changes"),
245
+ id: (0, params_js_1.resourceId)().describe("Camera ID"),
246
+ slot: zod_1.z
247
+ .number()
248
+ .int()
249
+ .min(-1)
250
+ .describe("PTZ preset slot: -1 for the home position, 0 or higher for saved presets"),
251
+ dryRun: (0, safety_js_1.dryRunInput)(),
272
252
  },
273
253
  annotations: safety_js_1.WRITE,
274
254
  }, async ({ id, slot, dryRun }) => {
275
- try {
276
- if (dryRun) {
277
- return (0, safety_js_1.formatDryRun)("POST", (0, url_js_1.safePath) `/cameras/${id}/ptz/goto/${slot}`);
278
- }
279
- const data = await client.post((0, url_js_1.safePath) `/cameras/${id}/ptz/goto/${slot}`);
280
- return (0, responses_js_1.formatSuccess)(data);
281
- }
282
- catch (err) {
283
- return (0, responses_js_1.formatError)(err);
255
+ if (dryRun) {
256
+ return (0, safety_js_1.formatDryRun)("POST", (0, url_js_1.safePath) `/cameras/${id}/ptz/goto/${slot}`);
284
257
  }
258
+ const data = await client.post((0, url_js_1.safePath) `/cameras/${id}/ptz/goto/${slot}`);
259
+ return (0, responses_js_1.formatSuccess)(data);
285
260
  });
286
261
  }
@@ -1,3 +1,3 @@
1
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
1
+ import type { ToolRegistrar } from "./registrar.js";
2
2
  import { ProtectClient } from "../client.js";
3
- export declare function registerDeviceActionTools(server: McpServer, client: ProtectClient, readOnly: boolean): void;
3
+ export declare function registerDeviceActionTools(server: ToolRegistrar, client: ProtectClient, readOnly: boolean): void;