rhombus-node-mcp 0.1.55 → 0.1.57

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 (108) hide show
  1. package/dist/api/access-control-tool-api.js +231 -32
  2. package/dist/api/alarm-monitoring-tool-api.js +50 -1
  3. package/dist/api/automated-prompts-tool-api.js +9 -17
  4. package/dist/api/camera-policy-tool-api.js +122 -0
  5. package/dist/api/camera-tool-api.js +3 -5
  6. package/dist/api/camera-uptime-tool-api.js +62 -25
  7. package/dist/api/clips-tool-api.js +45 -9
  8. package/dist/api/create-tool-api.js +55 -7
  9. package/dist/api/door-tool-api.js +74 -13
  10. package/dist/api/entity-lookup-tool-api.js +8 -1
  11. package/dist/api/events-tool-api.js +79 -6
  12. package/dist/api/faces-tool-api.js +112 -7
  13. package/dist/api/get-accessible-apps.js +68 -6
  14. package/dist/api/get-entity-tool-api.js +12 -12
  15. package/dist/api/get-org-information-tool-api.js +2 -2
  16. package/dist/api/guest-management-tool-api.js +75 -1
  17. package/dist/api/location-tool-api.js +120 -28
  18. package/dist/api/lpr-tool-api.js +71 -16
  19. package/dist/api/policy-alerts-tool-api.js +7 -3
  20. package/dist/api/report-tool-api.js +4 -1
  21. package/dist/api/rules-tool-api.js +6 -16
  22. package/dist/api/schedule-tool-api.js +107 -0
  23. package/dist/api/search-tool-api.js +5 -13
  24. package/dist/api/update-tool-api.js +96 -1
  25. package/dist/api/user-tool-api.js +58 -13
  26. package/dist/createServer.js +18 -6
  27. package/dist/filtering-utils.js +421 -29
  28. package/dist/index.js +1 -1
  29. package/dist/network/network.js +122 -10
  30. package/dist/network/org-reference-cache.js +74 -0
  31. package/dist/telemetry/tracingProxy.js +4 -0
  32. package/dist/tools/entity-lookup-tool.js +8 -4
  33. package/dist/tools/get-entity-tool.js +46 -21
  34. package/dist/tools/get-org-information-tool.js +36 -3
  35. package/dist/tools/time-conversion-tool.js +65 -37
  36. package/dist/tools/time-tool.js +18 -4
  37. package/dist/tools/user-tool.js +101 -1
  38. package/dist/tools-console/access-anomaly-tool.js +4 -19
  39. package/dist/tools-console/access-control-tool.js +330 -2
  40. package/dist/tools-console/alarm-monitoring-tool.js +73 -1
  41. package/dist/tools-console/analytics-tool.js +4 -2
  42. package/dist/tools-console/automated-prompts-tool.js +2 -0
  43. package/dist/tools-console/badge-timeline-tool.js +4 -21
  44. package/dist/tools-console/camera-policy-tool.js +252 -0
  45. package/dist/tools-console/camera-tool.js +113 -89
  46. package/dist/tools-console/camera-uptime-tool.js +2 -0
  47. package/dist/tools-console/clips-tool.js +23 -31
  48. package/dist/tools-console/count-tool.js +19 -6
  49. package/dist/tools-console/create-camera-policy-tool.js +304 -122
  50. package/dist/tools-console/door-schedule-exception-tool.js +4 -33
  51. package/dist/tools-console/door-tool.js +131 -1
  52. package/dist/tools-console/elements-access-anomaly-tool.js +4 -19
  53. package/dist/tools-console/elements-badge-timeline-tool.js +4 -21
  54. package/dist/tools-console/elements-lost-badge-tool.js +4 -17
  55. package/dist/tools-console/elements-tool.js +4 -29
  56. package/dist/tools-console/events-tool.js +24 -101
  57. package/dist/tools-console/faces-tool.js +255 -27
  58. package/dist/tools-console/guest-management-tool.js +128 -1
  59. package/dist/tools-console/location-tool.js +161 -35
  60. package/dist/tools-console/lost-badge-tool.js +4 -17
  61. package/dist/tools-console/lpr-tool.js +90 -22
  62. package/dist/tools-console/netbox-access-anomaly-tool.js +4 -19
  63. package/dist/tools-console/netbox-badge-timeline-tool.js +4 -21
  64. package/dist/tools-console/netbox-lost-badge-tool.js +4 -17
  65. package/dist/tools-console/netbox-tool.js +4 -29
  66. package/dist/tools-console/onguard-tool.js +4 -30
  67. package/dist/tools-console/person-tracking-tool.js +4 -2
  68. package/dist/tools-console/policy-alerts-tool.js +6 -28
  69. package/dist/tools-console/reboot-cameras-tool.js +23 -3
  70. package/dist/tools-console/report-tool.js +12 -40
  71. package/dist/tools-console/rules-tool.js +2 -0
  72. package/dist/tools-console/schedule-tool.js +190 -0
  73. package/dist/tools-console/search-tool.js +2 -0
  74. package/dist/tools-console/update-tool.js +300 -168
  75. package/dist/tools-console/user-access-trail-tool.js +2 -0
  76. package/dist/tools-console/user-audit-tool.js +2 -0
  77. package/dist/tools-console/video-walls-tool.js +94 -20
  78. package/dist/types/access-anomaly-tool-types.js +22 -2
  79. package/dist/types/access-control-tool-types.js +116 -1
  80. package/dist/types/alarm-monitoring-tool-types.js +42 -1
  81. package/dist/types/badge-timeline-tool-types.js +18 -3
  82. package/dist/types/camera-policy-tool-types.js +104 -0
  83. package/dist/types/camera-tool-types.js +80 -1
  84. package/dist/types/clips-tool-types.js +27 -3
  85. package/dist/types/create-camera-policy-tool-types.js +275 -26
  86. package/dist/types/door-schedule-exception-tool-types.js +20 -6
  87. package/dist/types/door-tool-types.js +36 -4
  88. package/dist/types/entity-lookup-tool-types.js +6 -1
  89. package/dist/types/events-tools-types.js +24 -16
  90. package/dist/types/faces-tools-types.js +106 -3
  91. package/dist/types/get-entity-tool-types.js +76 -12
  92. package/dist/types/guest-management-tool-types.js +103 -1
  93. package/dist/types/location-tool-types.js +100 -14
  94. package/dist/types/lost-badge-tool-types.js +17 -2
  95. package/dist/types/lpr-tool-types.js +47 -8
  96. package/dist/types/onguard-tool-types.js +21 -1
  97. package/dist/types/policy-alerts-tool-types.js +1 -1
  98. package/dist/types/report-tool-types.js +29 -1
  99. package/dist/types/schedule-tool-types.js +370 -0
  100. package/dist/types/schema.js +479 -9
  101. package/dist/types/time-conversion-tool-types.js +3 -4
  102. package/dist/types/update-tool-types.js +40 -18
  103. package/dist/types/user-tool-types.js +44 -1
  104. package/dist/types/video-walls-tool-types.js +30 -2
  105. package/dist/types/zod-schemas.js +5128 -996
  106. package/dist/util.js +62 -3
  107. package/dist/utils/entity-name-match.js +11 -0
  108. package/package.json +3 -1
@@ -1,48 +1,84 @@
1
- import { updateCameraConfig, getCameraDetails, cleanUpdatePayload, formatCameraSettings, validateCameraFeatureSupport, } from "../api/update-tool-api.js";
1
+ import { updateCameraConfig, getCameraDetails, cleanUpdatePayload, formatCameraSettings, validateCameraFeatureSupport, updateDoorbellCameraConfig, getDoorbellCameraDetails, getDoorbellCameraConfig, } from "../api/update-tool-api.js";
2
2
  import { OUTPUT_SCHEMA, TOOL_ARGS, UpdateCameraConfigPayload, CameraVideoSettings, CameraAudioSettings, CameraDeviceSettings, parseFacetedUuid, } from "../types/update-tool-types.js";
3
3
  import { logger } from "../logger.js";
4
4
  const TOOL_NAME = "update-tool";
5
5
  const TOOL_DESCRIPTION = `
6
- This tool allows updating configuration settings for various Rhombus entities. Currently supports:
7
- - **Cameras**: Update video settings (resolution, HDR, WDR, brightness, contrast, etc.), audio settings (recording, microphone, speaker), and device settings (name, timezone, LED control).
6
+ Updates configuration settings for Rhombus cameras and doorbell cameras: video settings (resolution, HDR/WDR, brightness, contrast, saturation), audio settings (recording, microphone, speaker) and device settings (name, timezone, LED control). Use this tool for ALL camera settings changes, including image-quality fixes (too dark, washed out, blurry).
8
7
 
9
- For LED control, use EXACTLY these field names in cameraDeviceSettings:
10
- - To turn LED off: {"led_stealth_mode": true} (recommended) or {"led_mode": "always_off"}
11
- - To turn LED on: {"led_stealth_mode": false} or {"led_mode": "always_on"} or {"led_mode": "auto"}
12
- - IMPORTANT: Use underscore in field names (led_mode, led_stealth_mode), not camelCase
8
+ MANDATORY confirmation flow: when you have proposed camera-settings fixes and the user replies with any affirmative ("yes", "confirm", "apply", "go ahead", ...), do not send text first — IMMEDIATELY call this tool with the settings you identified, and only report success after it returns. NEVER claim settings were updated without calling it; one confirmation covers all proposed changes.
13
9
 
14
- The tool supports faceted UUIDs (e.g., "cameraUuid.v0" or "cameraUuid.v1") to update specific camera facets. If no facet is specified, defaults to "v0".
15
-
16
- The tool guides users through a multi-step process:
17
- 1. Entity selection (if not provided)
18
- 2. Settings configuration with current values shown
19
- 3. Confirmation and application of changes
20
-
21
- Future support planned for:
22
- - Climate sensors
23
- - Door controllers
24
- - Environmental gateways
25
- - Audio gateways
26
- - Doorbell cameras
27
- - Badge readers
28
-
29
- The tool uses elicitation forms for rich user interaction and shows current settings before updates.
30
-
31
- ---
32
-
33
- **CAMERA SETTINGS UPDATE FLOW** — Use this tool for ALL camera settings updates (brightness, contrast, WDR, resolution, audio, LED, etc.).
34
- - For camera image-quality fixes: provide entityType="camera", entityUuid, and the specific settings to change in cameraVideoSettings.
35
- - Example for dark image: update-tool(entityType="camera", entityUuid="<uuid>", cameraVideoSettings='{"img_brightness": 0, "wdr_strength": 64}').
36
- - Example for washed out: update-tool(entityType="camera", entityUuid="<uuid>", cameraVideoSettings='{"img_brightness": -50, "img_contrast": 80}').
37
- - Saturation matters — saturation 0 yields a grayscale image. Most cameras look best with mid-range values; tune from there.
38
-
39
- **CONFIRMATION FLOW (MANDATORY)** — When the conversation history shows you analyzed a camera and proposed fixes, and the user replies with any affirmative ("yes", "confirm", "fix it", "apply", "do it", "go ahead", "proceed", "sure", "ok"):
40
- 1. DO NOT generate any text response first.
41
- 2. IMMEDIATELY call update-tool with the camera settings you previously identified.
42
- 3. Only after update-tool returns successfully, say "Done! Check your camera now…".
43
-
44
- NEVER respond saying settings were updated without first calling update-tool — without the call, no changes take effect. Avoid multiple rounds of confirmation; get one confirmation for all proposed changes.
10
+ Exact field names, LED rules, example payloads and faceted-UUID handling are documented on the parameters. The tool shows current settings before applying updates.
45
11
  `;
12
+ // EVERY result from this tool must carry structuredContent: the tool registers
13
+ // an outputSchema, and the MCP SDK rejects any non-isError result without
14
+ // structuredContent as "MCP error -32602: ... no structured content was
15
+ // provided" — which REPLACES the real failure text. That is how an out-of-range
16
+ // img_sharpness once reached the user as "a system error" with nothing for the
17
+ // model to correct, so it retried the identical bad value and gave up.
18
+ function errorResult(text) {
19
+ return {
20
+ isError: true,
21
+ content: [{ type: "text", text }],
22
+ structuredContent: { needUserInput: false, success: false, message: text },
23
+ };
24
+ }
25
+ // Slice a doorbell camera's flat config down to the fields this tool can set
26
+ // (the keys of one of the three camera settings schemas). The full config has
27
+ // ~150 internal fields — showing all of them would bury the settable ones.
28
+ function pickDoorbellSettings(config, settingsSchema) {
29
+ const out = {};
30
+ for (const key of Object.keys(settingsSchema.shape)) {
31
+ const value = config[key];
32
+ if (value !== undefined && value !== null)
33
+ out[key] = value;
34
+ }
35
+ return out;
36
+ }
37
+ function valueAtPath(root, path) {
38
+ return path.reduce((node, key) => node && typeof node === "object" ? node[key] : undefined, root);
39
+ }
40
+ // The settings blocks are free-form JSON strings, so an out-of-range value is
41
+ // the normal failure here, not an exotic one. The message MUST name the field,
42
+ // the bound, and what was received — otherwise the model has nothing to act on.
43
+ function describeSettingsIssues(label, error, input) {
44
+ const details = error.issues.map(issue => {
45
+ const field = issue.path.length ? issue.path.join(".") : label;
46
+ const received = valueAtPath(input, issue.path);
47
+ const suffix = received === undefined ? "" : ` (received ${JSON.stringify(received)})`;
48
+ if (issue.code === "too_big") {
49
+ return `${field} must be at most ${issue.maximum}${suffix}`;
50
+ }
51
+ if (issue.code === "too_small") {
52
+ return `${field} must be at least ${issue.minimum}${suffix}`;
53
+ }
54
+ return `${field}: ${issue.message}${suffix}`;
55
+ });
56
+ return `Invalid ${label}: ${details.join("; ")}. Check the allowed ranges documented on this tool's parameters, then call again with in-range values. No settings were changed.`;
57
+ }
58
+ function parseSettingsBlock(label, raw, schema, normalize) {
59
+ let parsed;
60
+ try {
61
+ parsed = JSON.parse(raw);
62
+ }
63
+ catch (error) {
64
+ return {
65
+ ok: false,
66
+ message: `Invalid ${label}: not valid JSON (${error instanceof Error ? error.message : "parse error"}). Pass a JSON object string, e.g. '{"img_brightness": 0}'. No settings were changed.`,
67
+ };
68
+ }
69
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
70
+ return {
71
+ ok: false,
72
+ message: `Invalid ${label}: expected a JSON object, received ${Array.isArray(parsed) ? "an array" : String(parsed === null ? "null" : typeof parsed)}. No settings were changed.`,
73
+ };
74
+ }
75
+ normalize?.(parsed);
76
+ const result = schema.safeParse(parsed);
77
+ if (!result.success) {
78
+ return { ok: false, message: describeSettingsIssues(label, result.error, parsed) };
79
+ }
80
+ return { ok: true, value: result.data };
81
+ }
46
82
  const TOOL_HANDLER = async (args, extra) => {
47
83
  const { entityType, entityUuid, cameraVideoSettings, cameraAudioSettings, cameraDeviceSettings, step, } = args;
48
84
  // Handle camera updates
@@ -62,41 +98,45 @@ const TOOL_HANDLER = async (args, extra) => {
62
98
  };
63
99
  // Parse and add video settings
64
100
  if (cameraVideoSettings && cameraVideoSettings.trim()) {
65
- const videoSettings = JSON.parse(cameraVideoSettings);
66
- // Validate with zod schema
67
- const validatedVideo = CameraVideoSettings.parse(videoSettings);
101
+ const video = parseSettingsBlock("camera video settings", cameraVideoSettings, CameraVideoSettings);
102
+ if (!video.ok) {
103
+ return errorResult(video.message);
104
+ }
68
105
  updatePayload.configUpdate.videoFacetSettings = {
69
- [facet]: cleanUpdatePayload(validatedVideo),
106
+ [facet]: cleanUpdatePayload(video.value),
70
107
  };
71
108
  }
72
109
  // Parse and add audio settings
73
110
  if (cameraAudioSettings && cameraAudioSettings.trim()) {
74
- const audioSettings = JSON.parse(cameraAudioSettings);
75
- // Validate with zod schema
76
- const validatedAudio = CameraAudioSettings.parse(audioSettings);
111
+ const audio = parseSettingsBlock("camera audio settings", cameraAudioSettings, CameraAudioSettings);
112
+ if (!audio.ok) {
113
+ return errorResult(audio.message);
114
+ }
77
115
  updatePayload.configUpdate.audioFacetSettings = {
78
- [facet]: cleanUpdatePayload(validatedAudio),
116
+ [facet]: cleanUpdatePayload(audio.value),
79
117
  };
80
118
  }
81
119
  // Parse and add device settings
82
120
  if (cameraDeviceSettings && cameraDeviceSettings.trim()) {
83
- const deviceSettings = JSON.parse(cameraDeviceSettings);
84
- logger.debug("[update-tool] Raw device settings:", deviceSettings);
85
- // Transform ledMode to led_mode and OFF to always_off before validation
86
- if ("ledMode" in deviceSettings) {
87
- deviceSettings.led_mode =
88
- deviceSettings.ledMode === "OFF" ? "always_off" : deviceSettings.ledMode;
89
- delete deviceSettings.ledMode;
121
+ const device = parseSettingsBlock("camera device settings", cameraDeviceSettings, CameraDeviceSettings, deviceSettings => {
122
+ logger.debug("[update-tool] Raw device settings:", deviceSettings);
123
+ // Transform ledMode to led_mode and OFF to always_off before validation
124
+ if ("ledMode" in deviceSettings) {
125
+ deviceSettings.led_mode =
126
+ deviceSettings.ledMode === "OFF" ? "always_off" : deviceSettings.ledMode;
127
+ delete deviceSettings.ledMode;
128
+ }
129
+ // Convert string "true"/"false" to boolean for led_stealth_mode
130
+ if ("led_stealth_mode" in deviceSettings &&
131
+ typeof deviceSettings.led_stealth_mode === "string") {
132
+ deviceSettings.led_stealth_mode = deviceSettings.led_stealth_mode === "true";
133
+ }
134
+ });
135
+ if (!device.ok) {
136
+ return errorResult(device.message);
90
137
  }
91
- // Convert string "true"/"false" to boolean for led_stealth_mode
92
- if ("led_stealth_mode" in deviceSettings &&
93
- typeof deviceSettings.led_stealth_mode === "string") {
94
- deviceSettings.led_stealth_mode = deviceSettings.led_stealth_mode === "true";
95
- }
96
- // Validate with zod schema
97
- const validatedDevice = CameraDeviceSettings.parse(deviceSettings);
98
- logger.debug("[update-tool] Validated device settings:", validatedDevice);
99
- const cleaned = cleanUpdatePayload(validatedDevice);
138
+ logger.debug("[update-tool] Validated device settings:", device.value);
139
+ const cleaned = cleanUpdatePayload(device.value);
100
140
  logger.debug("[update-tool] Cleaned device settings:", cleaned);
101
141
  updatePayload.configUpdate.deviceSettings = cleaned;
102
142
  }
@@ -104,15 +144,8 @@ const TOOL_HANDLER = async (args, extra) => {
104
144
  const validatedPayload = UpdateCameraConfigPayload.parse(updatePayload);
105
145
  const featureValidation = await validateCameraFeatureSupport(validatedPayload, extra._meta?.requestModifiers, extra.sessionId);
106
146
  if (!featureValidation.canProceed) {
107
- return {
108
- content: [
109
- {
110
- type: "text",
111
- text: featureValidation.error ||
112
- "This camera does not support one or more requested features.",
113
- },
114
- ],
115
- };
147
+ return errorResult(featureValidation.error ||
148
+ "This camera does not support one or more requested features.");
116
149
  }
117
150
  // Apply the updates
118
151
  const result = await updateCameraConfig(validatedPayload, extra._meta?.requestModifiers, extra.sessionId);
@@ -121,14 +154,7 @@ const TOOL_HANDLER = async (args, extra) => {
121
154
  const normalizedError = hasCapabilitySensitiveChange
122
155
  ? "This camera may not support one or more requested settings."
123
156
  : result.error;
124
- return {
125
- content: [
126
- {
127
- type: "text",
128
- text: `Failed to update camera settings: ${normalizedError}`,
129
- },
130
- ],
131
- };
157
+ return errorResult(`Failed to update camera settings: ${normalizedError}`);
132
158
  }
133
159
  // Format the updated settings for display
134
160
  const formattedSettings = formatCameraSettings(validatedPayload.configUpdate);
@@ -151,14 +177,7 @@ const TOOL_HANDLER = async (args, extra) => {
151
177
  };
152
178
  }
153
179
  catch (error) {
154
- return {
155
- content: [
156
- {
157
- type: "text",
158
- text: `Error updating camera settings: ${error instanceof Error ? error.message : "Unknown error"}`,
159
- },
160
- ],
161
- };
180
+ return errorResult(`Error updating camera settings: ${error instanceof Error ? error.message : "Unknown error"}`);
162
181
  }
163
182
  }
164
183
  // Step 2: Show settings configuration form (if entityUuid provided but no settings)
@@ -169,14 +188,7 @@ const TOOL_HANDLER = async (args, extra) => {
169
188
  // Get current camera details
170
189
  const cameraDetails = await getCameraDetails(baseUuid, extra._meta?.requestModifiers, extra.sessionId);
171
190
  if (!cameraDetails.success || !cameraDetails.data) {
172
- return {
173
- content: [
174
- {
175
- type: "text",
176
- text: `Failed to get camera details: ${cameraDetails.error || "Camera not found"}`,
177
- },
178
- ],
179
- };
191
+ return errorResult(`Failed to get camera details: ${cameraDetails.error || "Camera not found"}`);
180
192
  }
181
193
  const camera = cameraDetails.data;
182
194
  const currentVideoSettings = camera.videoSettings || {};
@@ -211,14 +223,7 @@ const TOOL_HANDLER = async (args, extra) => {
211
223
  };
212
224
  }
213
225
  catch (error) {
214
- return {
215
- content: [
216
- {
217
- type: "text",
218
- text: `Error retrieving camera details: ${error instanceof Error ? error.message : "Unknown error"}`,
219
- },
220
- ],
221
- };
226
+ return errorResult(`Error retrieving camera details: ${error instanceof Error ? error.message : "Unknown error"}`);
222
227
  }
223
228
  }
224
229
  // Step 1: Entity selection (if no entityUuid)
@@ -241,80 +246,205 @@ const TOOL_HANDLER = async (args, extra) => {
241
246
  };
242
247
  }
243
248
  }
244
- // Handle other entity types (future implementation)
245
- if (entityType === "climate-sensor") {
246
- return {
247
- content: [
248
- {
249
- type: "text",
250
- text: "Climate sensor updates are not yet implemented. Coming soon!",
251
- },
252
- ],
253
- };
254
- }
255
- if (entityType === "door-controller") {
256
- return {
257
- content: [
258
- {
259
- type: "text",
260
- text: "Door controller updates are not yet implemented. Coming soon!",
261
- },
262
- ],
263
- };
264
- }
265
- if (entityType === "environmental-gateway") {
266
- return {
267
- content: [
268
- {
269
- type: "text",
270
- text: "Environmental gateway updates are not yet implemented. Coming soon!",
271
- },
272
- ],
273
- };
274
- }
275
- if (entityType === "audio-gateway") {
276
- return {
277
- content: [
278
- {
279
- type: "text",
280
- text: "Audio gateway updates are not yet fully implemented. Coming soon!",
281
- },
282
- ],
283
- };
284
- }
249
+ // Doorbell cameras take the same setting names as cameras, but through
250
+ // /doorbellcamera/updateConfig with ONE FLAT configUpdate rather than the
251
+ // faceted video/audio/device split. Validation reuses the camera schemas, so
252
+ // the documented ranges apply identically.
285
253
  if (entityType === "doorbell-camera") {
286
- return {
287
- content: [
288
- {
289
- type: "text",
290
- text: "Doorbell camera updates are not yet fully implemented. Coming soon!",
291
- },
292
- ],
293
- };
294
- }
295
- if (entityType === "badge-reader") {
296
- return {
297
- content: [
298
- {
299
- type: "text",
300
- text: "Badge reader updates are not yet fully implemented. Coming soon!",
301
- },
302
- ],
303
- };
254
+ if (!entityUuid) {
255
+ return errorResult("To update a doorbell camera, pass entityUuid plus at least one of cameraVideoSettings, cameraAudioSettings or cameraDeviceSettings. Use get-entity-tool with entityType doorbell-camera to find the uuid.");
256
+ }
257
+ const hasDoorbellSettings = Boolean((cameraVideoSettings && cameraVideoSettings.trim()) ||
258
+ (cameraAudioSettings && cameraAudioSettings.trim()) ||
259
+ (cameraDeviceSettings && cameraDeviceSettings.trim()));
260
+ // Step 2 equivalent: uuid but no settings — show the current config so the
261
+ // model proposes changes against real values rather than guesses. The read
262
+ // comes from /doorbellcamera/getConfig, which returns the same flat object
263
+ // the update writes.
264
+ if (!hasDoorbellSettings) {
265
+ try {
266
+ const { baseUuid } = parseFacetedUuid(entityUuid);
267
+ const details = await getDoorbellCameraDetails(baseUuid, extra._meta?.requestModifiers, extra.sessionId);
268
+ if (!details.success) {
269
+ return errorResult(details.error ?? "Could not verify the doorbell camera.");
270
+ }
271
+ const current = await getDoorbellCameraConfig(baseUuid, extra._meta?.requestModifiers, extra.sessionId);
272
+ if (!current.success || !current.config) {
273
+ return errorResult(`Failed to read the doorbell camera's current settings: ${current.error ?? "unknown error"}`);
274
+ }
275
+ const jsonResultResponse = {
276
+ needUserInput: true,
277
+ message: `Configure settings for doorbell camera "${details.name ?? baseUuid}" (${baseUuid})\n\nCurrent settings are shown below. Modify the values you want to change:`,
278
+ requestType: "camera-settings-configuration",
279
+ submitAction: "update-tool",
280
+ entityType: "doorbell-camera",
281
+ entityUuid: baseUuid,
282
+ currentSettings: {
283
+ video: pickDoorbellSettings(current.config, CameraVideoSettings),
284
+ audio: pickDoorbellSettings(current.config, CameraAudioSettings),
285
+ device: pickDoorbellSettings(current.config, CameraDeviceSettings),
286
+ },
287
+ };
288
+ return {
289
+ content: [{ type: "text", text: JSON.stringify(jsonResultResponse) }],
290
+ structuredContent: jsonResultResponse,
291
+ };
292
+ }
293
+ catch (error) {
294
+ return errorResult(`Error retrieving doorbell camera settings: ${error instanceof Error ? error.message : "Unknown error"}`);
295
+ }
296
+ }
297
+ try {
298
+ // Doorbell configs are not faceted, so any .v0 suffix is dropped.
299
+ const { baseUuid } = parseFacetedUuid(entityUuid);
300
+ const flatSettings = {};
301
+ if (cameraVideoSettings && cameraVideoSettings.trim()) {
302
+ const video = parseSettingsBlock("doorbell camera video settings", cameraVideoSettings, CameraVideoSettings);
303
+ if (!video.ok) {
304
+ return errorResult(video.message);
305
+ }
306
+ Object.assign(flatSettings, cleanUpdatePayload(video.value));
307
+ }
308
+ if (cameraAudioSettings && cameraAudioSettings.trim()) {
309
+ const audio = parseSettingsBlock("doorbell camera audio settings", cameraAudioSettings, CameraAudioSettings);
310
+ if (!audio.ok) {
311
+ return errorResult(audio.message);
312
+ }
313
+ Object.assign(flatSettings, cleanUpdatePayload(audio.value));
314
+ }
315
+ if (cameraDeviceSettings && cameraDeviceSettings.trim()) {
316
+ const device = parseSettingsBlock("doorbell camera device settings", cameraDeviceSettings, CameraDeviceSettings, deviceSettings => {
317
+ if ("ledMode" in deviceSettings) {
318
+ deviceSettings.led_mode =
319
+ deviceSettings.ledMode === "OFF" ? "always_off" : deviceSettings.ledMode;
320
+ delete deviceSettings.ledMode;
321
+ }
322
+ if ("led_stealth_mode" in deviceSettings &&
323
+ typeof deviceSettings.led_stealth_mode === "string") {
324
+ deviceSettings.led_stealth_mode = deviceSettings.led_stealth_mode === "true";
325
+ }
326
+ });
327
+ if (!device.ok) {
328
+ return errorResult(device.message);
329
+ }
330
+ Object.assign(flatSettings, cleanUpdatePayload(device.value));
331
+ }
332
+ if (Object.keys(flatSettings).length === 0) {
333
+ return errorResult("No doorbell camera settings were recognised in the provided values, so nothing was changed.");
334
+ }
335
+ // Confirm the device exists first: /doorbellcamera/updateConfig accepts an
336
+ // unknown uuid without complaining, which would report success for a
337
+ // change that reached no device.
338
+ const details = await getDoorbellCameraDetails(baseUuid, extra._meta?.requestModifiers, extra.sessionId);
339
+ if (!details.success) {
340
+ return errorResult(details.error ?? "Could not verify the doorbell camera.");
341
+ }
342
+ // The update response only echoes the caller's input, so a before/after
343
+ // read of /doorbellcamera/getConfig is the ONLY way to tell a real write
344
+ // from a silent no-op. Both reads are best-effort: a failed read skips
345
+ // verification rather than blocking the write the user asked for.
346
+ const before = await getDoorbellCameraConfig(baseUuid, extra._meta?.requestModifiers, extra.sessionId);
347
+ const result = await updateDoorbellCameraConfig(baseUuid, flatSettings, extra._meta?.requestModifiers, extra.sessionId);
348
+ if (!result.success) {
349
+ return errorResult(`Failed to update doorbell camera settings: ${result.error}`);
350
+ }
351
+ const changedKeys = Object.keys(flatSettings);
352
+ const deviceName = details.name ?? baseUuid;
353
+ const previousSettings = {};
354
+ if (before.success && before.config) {
355
+ for (const key of changedKeys) {
356
+ previousSettings[key] = before.config[key] ?? null;
357
+ }
358
+ }
359
+ const after = await getDoorbellCameraConfig(baseUuid, extra._meta?.requestModifiers, extra.sessionId);
360
+ if (!after.success || !after.config) {
361
+ const doorbellResponse = {
362
+ needUserInput: false,
363
+ message: `Updated ${changedKeys.length} setting(s) on the doorbell camera "${deviceName}": ${changedKeys.join(", ")}. NOTE: the config read-back failed (${after.error ?? "unknown error"}), so the new values could not be verified.`,
364
+ entityType: "doorbell-camera",
365
+ entityUuid: baseUuid,
366
+ success: true,
367
+ updatedSettings: flatSettings,
368
+ ...(before.success && before.config ? { previousSettings } : {}),
369
+ };
370
+ return {
371
+ content: [{ type: "text", text: JSON.stringify(doorbellResponse) }],
372
+ structuredContent: doorbellResponse,
373
+ };
374
+ }
375
+ const applied = [];
376
+ const notApplied = [];
377
+ const notVerifiable = [];
378
+ for (const key of changedKeys) {
379
+ const actual = after.config[key];
380
+ if (actual === undefined) {
381
+ // getConfig doesn't report this key (e.g. camera_name / led_mode live
382
+ // outside the doorbell config object), so it can't be checked here.
383
+ notVerifiable.push(key);
384
+ }
385
+ else if (JSON.stringify(actual) === JSON.stringify(flatSettings[key])) {
386
+ applied.push(key);
387
+ }
388
+ else {
389
+ notApplied.push({ setting: key, requested: flatSettings[key], actual });
390
+ }
391
+ }
392
+ // api2 said success but the read-back shows every value unchanged: the
393
+ // write was a silent no-op. Retrying the same arguments would no-op again
394
+ // — surface it instead of reporting a success that didn't happen.
395
+ if (notApplied.length > 0 && applied.length === 0 && notVerifiable.length === 0) {
396
+ return errorResult(`The API accepted the update for doorbell camera "${deviceName}" but a config read-back shows the value(s) unchanged: ${notApplied
397
+ .map(f => `${f.setting} is still ${JSON.stringify(f.actual)} (requested ${JSON.stringify(f.requested)})`)
398
+ .join("; ")}. Do not retry with the same arguments — tell the user the setting did not take effect.`);
399
+ }
400
+ const verificationNotes = [];
401
+ if (notApplied.length > 0) {
402
+ verificationNotes.push(`WARNING — ${notApplied.length} setting(s) did NOT take effect: ${notApplied
403
+ .map(f => `${f.setting} is still ${JSON.stringify(f.actual)} (requested ${JSON.stringify(f.requested)})`)
404
+ .join("; ")}`);
405
+ }
406
+ if (notVerifiable.length > 0) {
407
+ verificationNotes.push(`${notVerifiable.length} setting(s) are not reported by the doorbell config and could not be verified: ${notVerifiable.join(", ")}`);
408
+ }
409
+ const doorbellResponse = {
410
+ needUserInput: false,
411
+ message: `Updated ${changedKeys.length} setting(s) on the doorbell camera "${deviceName}"` +
412
+ (applied.length > 0
413
+ ? ` — verified by reading the config back: ${applied.join(", ")}.`
414
+ : `: ${changedKeys.join(", ")}.`) +
415
+ (verificationNotes.length > 0 ? ` ${verificationNotes.join(". ")}.` : ""),
416
+ entityType: "doorbell-camera",
417
+ entityUuid: baseUuid,
418
+ success: true,
419
+ updatedSettings: flatSettings,
420
+ ...(before.success && before.config ? { previousSettings } : {}),
421
+ ...(notApplied.length > 0 ? { settingsNotApplied: notApplied } : {}),
422
+ ...(notVerifiable.length > 0 ? { settingsNotVerified: notVerifiable } : {}),
423
+ };
424
+ return {
425
+ content: [{ type: "text", text: JSON.stringify(doorbellResponse) }],
426
+ structuredContent: doorbellResponse,
427
+ };
428
+ }
429
+ catch (error) {
430
+ return errorResult(`Failed to update doorbell camera settings: ${error instanceof Error ? error.message : "Unknown error"}`);
431
+ }
304
432
  }
305
433
  // Step 0: Show initial form (no entityType provided)
434
+ const entityTypeFormResponse = {
435
+ needUserInput: true,
436
+ message: "Welcome to the Rhombus entity update tool!\n\nWhat type of entity would you like to update?\n\n• **camera** - Update camera video, audio, or device settings\n• **doorbell-camera** - Update doorbell camera video, audio, or device settings\n\nPlease specify the entity type to continue.",
437
+ requestType: "entity-type-selection",
438
+ submitAction: "update-tool",
439
+ };
306
440
  return {
307
441
  content: [
308
442
  {
309
443
  type: "text",
310
- text: JSON.stringify({
311
- needUserInput: true,
312
- message: "Welcome to the Rhombus entity update tool!\n\nWhat type of entity would you like to update?\n\n• **camera** - Update camera video, audio, or device settings\n• **climate-sensor** - Update climate sensor settings (coming soon)\n• **door-controller** - Update door controller settings (coming soon)\n• **environmental-gateway** - Update environmental gateway settings (coming soon)\n\nPlease specify the entity type to continue.",
313
- requestType: "entity-type-selection",
314
- submitAction: "update-tool",
315
- }),
444
+ text: JSON.stringify(entityTypeFormResponse),
316
445
  },
317
446
  ],
447
+ structuredContent: entityTypeFormResponse,
318
448
  };
319
449
  };
320
450
  function hasCapabilitySensitiveSettings(payload) {
@@ -331,8 +461,10 @@ function hasCapabilitySensitiveSettings(payload) {
331
461
  }
332
462
  export function createTool(server) {
333
463
  server.registerTool(TOOL_NAME, {
464
+ title: "Update Settings",
334
465
  description: TOOL_DESCRIPTION,
335
466
  inputSchema: TOOL_ARGS,
336
467
  outputSchema: OUTPUT_SCHEMA.shape,
468
+ annotations: { readOnlyHint: false, destructiveHint: false },
337
469
  }, TOOL_HANDLER);
338
470
  }
@@ -57,8 +57,10 @@ const TOOL_HANDLER = async (args, _extra) => {
57
57
  };
58
58
  export function createTool(server) {
59
59
  server.registerTool(TOOL_NAME, {
60
+ title: "User Access Trail",
60
61
  description: TOOL_DESCRIPTION,
61
62
  inputSchema: TOOL_ARGS,
62
63
  outputSchema: OUTPUT_SCHEMA.shape,
64
+ annotations: { readOnlyHint: true },
63
65
  }, TOOL_HANDLER);
64
66
  }
@@ -46,8 +46,10 @@ const TOOL_HANDLER = async (args, _extra) => {
46
46
  };
47
47
  export function createTool(server) {
48
48
  server.registerTool(TOOL_NAME, {
49
+ title: "User Audit Log",
49
50
  description: TOOL_DESCRIPTION,
50
51
  inputSchema: TOOL_ARGS,
51
52
  outputSchema: OUTPUT_SCHEMA.shape,
53
+ annotations: { readOnlyHint: true },
52
54
  }, TOOL_HANDLER);
53
55
  }