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.
- package/dist/api/access-control-tool-api.js +231 -32
- package/dist/api/alarm-monitoring-tool-api.js +50 -1
- package/dist/api/automated-prompts-tool-api.js +9 -17
- package/dist/api/camera-policy-tool-api.js +122 -0
- package/dist/api/camera-tool-api.js +3 -5
- package/dist/api/camera-uptime-tool-api.js +62 -25
- package/dist/api/clips-tool-api.js +45 -9
- package/dist/api/create-tool-api.js +55 -7
- package/dist/api/door-tool-api.js +74 -13
- package/dist/api/entity-lookup-tool-api.js +8 -1
- package/dist/api/events-tool-api.js +79 -6
- package/dist/api/faces-tool-api.js +112 -7
- package/dist/api/get-accessible-apps.js +68 -6
- package/dist/api/get-entity-tool-api.js +12 -12
- package/dist/api/get-org-information-tool-api.js +2 -2
- package/dist/api/guest-management-tool-api.js +75 -1
- package/dist/api/location-tool-api.js +120 -28
- package/dist/api/lpr-tool-api.js +71 -16
- package/dist/api/policy-alerts-tool-api.js +7 -3
- package/dist/api/report-tool-api.js +4 -1
- package/dist/api/rules-tool-api.js +6 -16
- package/dist/api/schedule-tool-api.js +107 -0
- package/dist/api/search-tool-api.js +5 -13
- package/dist/api/update-tool-api.js +96 -1
- package/dist/api/user-tool-api.js +58 -13
- package/dist/createServer.js +18 -6
- package/dist/filtering-utils.js +421 -29
- package/dist/index.js +1 -1
- package/dist/network/network.js +122 -10
- package/dist/network/org-reference-cache.js +74 -0
- package/dist/telemetry/tracingProxy.js +4 -0
- package/dist/tools/entity-lookup-tool.js +8 -4
- package/dist/tools/get-entity-tool.js +46 -21
- package/dist/tools/get-org-information-tool.js +36 -3
- package/dist/tools/time-conversion-tool.js +65 -37
- package/dist/tools/time-tool.js +18 -4
- package/dist/tools/user-tool.js +101 -1
- package/dist/tools-console/access-anomaly-tool.js +4 -19
- package/dist/tools-console/access-control-tool.js +330 -2
- package/dist/tools-console/alarm-monitoring-tool.js +73 -1
- package/dist/tools-console/analytics-tool.js +4 -2
- package/dist/tools-console/automated-prompts-tool.js +2 -0
- package/dist/tools-console/badge-timeline-tool.js +4 -21
- package/dist/tools-console/camera-policy-tool.js +252 -0
- package/dist/tools-console/camera-tool.js +113 -89
- package/dist/tools-console/camera-uptime-tool.js +2 -0
- package/dist/tools-console/clips-tool.js +23 -31
- package/dist/tools-console/count-tool.js +19 -6
- package/dist/tools-console/create-camera-policy-tool.js +304 -122
- package/dist/tools-console/door-schedule-exception-tool.js +4 -33
- package/dist/tools-console/door-tool.js +131 -1
- package/dist/tools-console/elements-access-anomaly-tool.js +4 -19
- package/dist/tools-console/elements-badge-timeline-tool.js +4 -21
- package/dist/tools-console/elements-lost-badge-tool.js +4 -17
- package/dist/tools-console/elements-tool.js +4 -29
- package/dist/tools-console/events-tool.js +24 -101
- package/dist/tools-console/faces-tool.js +255 -27
- package/dist/tools-console/guest-management-tool.js +128 -1
- package/dist/tools-console/location-tool.js +161 -35
- package/dist/tools-console/lost-badge-tool.js +4 -17
- package/dist/tools-console/lpr-tool.js +90 -22
- package/dist/tools-console/netbox-access-anomaly-tool.js +4 -19
- package/dist/tools-console/netbox-badge-timeline-tool.js +4 -21
- package/dist/tools-console/netbox-lost-badge-tool.js +4 -17
- package/dist/tools-console/netbox-tool.js +4 -29
- package/dist/tools-console/onguard-tool.js +4 -30
- package/dist/tools-console/person-tracking-tool.js +4 -2
- package/dist/tools-console/policy-alerts-tool.js +6 -28
- package/dist/tools-console/reboot-cameras-tool.js +23 -3
- package/dist/tools-console/report-tool.js +12 -40
- package/dist/tools-console/rules-tool.js +2 -0
- package/dist/tools-console/schedule-tool.js +190 -0
- package/dist/tools-console/search-tool.js +2 -0
- package/dist/tools-console/update-tool.js +300 -168
- package/dist/tools-console/user-access-trail-tool.js +2 -0
- package/dist/tools-console/user-audit-tool.js +2 -0
- package/dist/tools-console/video-walls-tool.js +94 -20
- package/dist/types/access-anomaly-tool-types.js +22 -2
- package/dist/types/access-control-tool-types.js +116 -1
- package/dist/types/alarm-monitoring-tool-types.js +42 -1
- package/dist/types/badge-timeline-tool-types.js +18 -3
- package/dist/types/camera-policy-tool-types.js +104 -0
- package/dist/types/camera-tool-types.js +80 -1
- package/dist/types/clips-tool-types.js +27 -3
- package/dist/types/create-camera-policy-tool-types.js +275 -26
- package/dist/types/door-schedule-exception-tool-types.js +20 -6
- package/dist/types/door-tool-types.js +36 -4
- package/dist/types/entity-lookup-tool-types.js +6 -1
- package/dist/types/events-tools-types.js +24 -16
- package/dist/types/faces-tools-types.js +106 -3
- package/dist/types/get-entity-tool-types.js +76 -12
- package/dist/types/guest-management-tool-types.js +103 -1
- package/dist/types/location-tool-types.js +100 -14
- package/dist/types/lost-badge-tool-types.js +17 -2
- package/dist/types/lpr-tool-types.js +47 -8
- package/dist/types/onguard-tool-types.js +21 -1
- package/dist/types/policy-alerts-tool-types.js +1 -1
- package/dist/types/report-tool-types.js +29 -1
- package/dist/types/schedule-tool-types.js +370 -0
- package/dist/types/schema.js +479 -9
- package/dist/types/time-conversion-tool-types.js +3 -4
- package/dist/types/update-tool-types.js +40 -18
- package/dist/types/user-tool-types.js +44 -1
- package/dist/types/video-walls-tool-types.js +30 -2
- package/dist/types/zod-schemas.js +5128 -996
- package/dist/util.js +62 -3
- package/dist/utils/entity-name-match.js +11 -0
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
66
|
-
|
|
67
|
-
|
|
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(
|
|
106
|
+
[facet]: cleanUpdatePayload(video.value),
|
|
70
107
|
};
|
|
71
108
|
}
|
|
72
109
|
// Parse and add audio settings
|
|
73
110
|
if (cameraAudioSettings && cameraAudioSettings.trim()) {
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
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(
|
|
116
|
+
[facet]: cleanUpdatePayload(audio.value),
|
|
79
117
|
};
|
|
80
118
|
}
|
|
81
119
|
// Parse and add device settings
|
|
82
120
|
if (cameraDeviceSettings && cameraDeviceSettings.trim()) {
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
92
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
{
|
|
299
|
-
|
|
300
|
-
|
|
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
|
}
|