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,43 +1,15 @@
|
|
|
1
1
|
import { createToolStructuredContent, extractFromToolExtra } from "../util.js";
|
|
2
2
|
import { ApiPayloadSchema, OUTPUT_SCHEMA, TOOL_ARGS, } from "../types/clips-tool-types.js";
|
|
3
|
-
import { getSavedClips, getExpiringClips, getSharedLiveStreams, getTimelapseClips, getClipGroups, getSharedClipGroups, createClip, deleteClip, } from "../api/clips-tool-api.js";
|
|
3
|
+
import { getSavedClips, getExpiringClips, getSharedLiveStreams, getTimelapseClips, getClipGroups, getSharedClipGroups, createClip, deleteClip, updateClip, } from "../api/clips-tool-api.js";
|
|
4
4
|
const TOOL_NAME = "clips-tool";
|
|
5
5
|
const TOOL_DESCRIPTION = `
|
|
6
6
|
Retrieves saved video clips from the Rhombus system. Saved clips can be viewed for up to 2 years and are typically found in the "Clips" tab of the "Saved Video" section of the Rhombus Console.
|
|
7
7
|
Clips are either manually saved by the user, or automatically by some defined policy. Therefore, this tool
|
|
8
8
|
is not for looking up the events that have occured.
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
* Get saved clips or clips expiring soon (filter by devices, locations, search string, time range).
|
|
12
|
-
* Get all shared live video streams for the organization.
|
|
13
|
-
* Get all timelapse clips for the organization.
|
|
14
|
-
|
|
15
|
-
Filter options (for saved and expiringSoon only):
|
|
16
|
-
* Specific devices using their UUIDs (deviceUuidFilters).
|
|
17
|
-
* Specific locations using their UUIDs (locationUuidFilters).
|
|
18
|
-
* A simple string search on clip names (searchFilter).
|
|
19
|
-
* A time range: start (timestampISOAfter) and/or end (timestampISOBefore) timestamp in ISO 8601 format.
|
|
20
|
-
|
|
21
|
-
The tool returns a JSON object with the following structure and important fields:
|
|
22
|
-
* **errorMsg (string | null):** An error message if the request failed.
|
|
23
|
-
* **objecterror (boolean | null):** Indicates if an object-level error occurred.
|
|
24
|
-
* **pageToken (string | null):** A token to be supplied on the next search request to get the next page of results. If this token is null, there is no more data available.
|
|
25
|
-
* **savedClips (array of objects | null):** An array where each object represents a saved video clip. Each clip object contains the following important fields:
|
|
26
|
-
* **uuid (string):** The unique identifier for the video clip.
|
|
27
|
-
* **title (string):** The name given to the video clip.
|
|
28
|
-
* **description (string | null):** An optional description for the clip.
|
|
29
|
-
* **timestampMs (int64):** The start time of the video clip in milliseconds since epoch.
|
|
30
|
-
* **createdAtMs (int64):** The creation timestamp of the clip in milliseconds since epoch.
|
|
31
|
-
* **createdAtTimestamp (string):** The creation timestamp of the clip in ISO 8601 format.
|
|
32
|
-
* **deviceUuid (string):** The UUID of the primary device (e.g., camera) that recorded the clip.
|
|
33
|
-
* **deviceUuids (array of strings or null):** A list of UUIDs for all devices associated with the clip.
|
|
34
|
-
* **durationSec (int32):** The length of the video clip in seconds.
|
|
35
|
-
* **status (string):** The current processing status of the clip, with possible values such as INITIATING, UPLOADING, RENDERING, FAILED, COMPLETE, OFFLINE, or UNKNOWN.
|
|
36
|
-
* **userUuid (string | null):** The UUID of the user associated with the clip, if applicable.
|
|
37
|
-
* **sourceAlertUuid (string | null):** The UUID of the alert that triggered the creation of this clip, if any.
|
|
38
|
-
* **sharedLiveVideoStreams (array):** When requestType is sharedLiveStreams, list of shared live video stream objects.
|
|
39
|
-
* **timelapseClips (array):** When requestType is timelapseClips, list of timelapse clip objects.
|
|
10
|
+
Set "requestType" to: get saved clips or clips expiring soon (filter by devices, locations, name search, and time range); list the org's shared live video streams, timelapse clips, clip groups, or shared clip groups; **createClip** to save a new clip from a camera's footage (video evidence around an event); or **deleteClip** to permanently delete a saved clip — destructive and irreversible, confirm with the user first.
|
|
40
11
|
|
|
12
|
+
Each saved clip carries uuid, title, description, start and creation timestamps, the recording device(s), durationSec, processing status (INITIATING / UPLOADING / RENDERING / FAILED / COMPLETE / OFFLINE / UNKNOWN), and the alert that triggered it when there was one. Results paginate via pageToken (null when there is no more data).
|
|
41
13
|
`;
|
|
42
14
|
const TOOL_HANDLER = async (args, extra) => {
|
|
43
15
|
const { requestModifiers, sessionId } = extractFromToolExtra(extra);
|
|
@@ -61,6 +33,24 @@ const TOOL_HANDLER = async (args, extra) => {
|
|
|
61
33
|
}
|
|
62
34
|
return createToolStructuredContent(await createClip(args.spliceRequest.cameraUuid, args.spliceRequest.startTimeMs, args.spliceRequest.endTimeMs, requestModifiers, sessionId));
|
|
63
35
|
}
|
|
36
|
+
case "updateClip": {
|
|
37
|
+
if (!args.clipUuid) {
|
|
38
|
+
throw new Error("clipUuid is required for 'updateClip' requestType");
|
|
39
|
+
}
|
|
40
|
+
if (!args.clipTitle?.trim() && !args.clipDescription?.trim()) {
|
|
41
|
+
throw new Error("'updateClip' needs at least one of clipTitle or clipDescription — both were empty, so there is nothing to change");
|
|
42
|
+
}
|
|
43
|
+
const { existingTitle, ...result } = await updateClip(args.clipUuid, {
|
|
44
|
+
title: args.clipTitle?.trim() || undefined,
|
|
45
|
+
description: args.clipDescription?.trim() || undefined,
|
|
46
|
+
}, requestModifiers, sessionId);
|
|
47
|
+
return createToolStructuredContent({
|
|
48
|
+
...result,
|
|
49
|
+
note: result.updateResult
|
|
50
|
+
? `Renamed the clip${existingTitle ? ` "${existingTitle}"` : ""}${args.clipTitle?.trim() ? ` to "${args.clipTitle.trim()}"` : ""}. Fields you did not pass were left as they were.`
|
|
51
|
+
: undefined,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
64
54
|
case "deleteClip": {
|
|
65
55
|
if (!args.clipUuid) {
|
|
66
56
|
throw new Error("clipUuid is required for 'deleteClip' requestType");
|
|
@@ -72,8 +62,10 @@ const TOOL_HANDLER = async (args, extra) => {
|
|
|
72
62
|
};
|
|
73
63
|
export function createTool(server) {
|
|
74
64
|
server.registerTool(TOOL_NAME, {
|
|
65
|
+
title: "Clips",
|
|
75
66
|
description: TOOL_DESCRIPTION,
|
|
76
67
|
inputSchema: TOOL_ARGS,
|
|
77
68
|
outputSchema: OUTPUT_SCHEMA.shape,
|
|
69
|
+
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
78
70
|
}, TOOL_HANDLER);
|
|
79
71
|
}
|
|
@@ -1,23 +1,36 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { logger } from "../logger.js";
|
|
3
3
|
export function createTool(server) {
|
|
4
|
-
server.
|
|
5
|
-
|
|
4
|
+
server.registerTool("count-tool", {
|
|
5
|
+
title: "Count Items",
|
|
6
|
+
description: `
|
|
7
|
+
This tool counts the number of items by accepting an array of UUIDs. It can count anything that has UUIDs - users, devices,
|
|
6
8
|
records, or any other entities. Simply provide an array of UUID strings and it will return the precise count.
|
|
7
|
-
`,
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
`,
|
|
10
|
+
inputSchema: {
|
|
11
|
+
uuids: z
|
|
12
|
+
.array(z.string().describe("UUID string of an individual item"))
|
|
13
|
+
.describe("An array of UUID strings representing the items to count. Each string should be a valid UUID."),
|
|
14
|
+
},
|
|
15
|
+
// Every result from a tool with an outputSchema must carry
|
|
16
|
+
// structuredContent or isError: true — the SDK otherwise replaces the
|
|
17
|
+
// real message with an opaque -32602 (see CLAUDE.md).
|
|
18
|
+
outputSchema: {
|
|
19
|
+
count: z.number().optional().describe("How many UUIDs were provided"),
|
|
20
|
+
},
|
|
21
|
+
annotations: { readOnlyHint: true },
|
|
11
22
|
}, async ({ uuids }) => {
|
|
12
23
|
try {
|
|
13
24
|
logger.info("Counting UUIDs", uuids);
|
|
14
25
|
return {
|
|
15
26
|
content: [{ type: "text", text: `Count: ${uuids.length}` }],
|
|
27
|
+
structuredContent: { count: uuids.length },
|
|
16
28
|
};
|
|
17
29
|
}
|
|
18
30
|
catch (e) {
|
|
19
31
|
const errorMessage = e instanceof Error ? e.message : `Unknown error: ${e}`;
|
|
20
32
|
return {
|
|
33
|
+
isError: true,
|
|
21
34
|
content: [{ type: "text", text: `Error counting UUIDs: ${errorMessage}` }],
|
|
22
35
|
};
|
|
23
36
|
}
|
|
@@ -1,141 +1,333 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { createCameraPolicy } from "../api/create-camera-policy-tool-api.js";
|
|
3
|
-
import { ApiPayloadSchema, OUTPUT_SCHEMA } from "../types/create-camera-policy-tool-types.js";
|
|
3
|
+
import { ApiPayloadSchema, OUTPUT_SCHEMA, SCHEDULE_CONFIGS_EXAMPLE, SUGGESTED_CAMERA_ACTIVITIES, isEmptyScheduleConfigs, parseCameraUuids, parseScheduleConfigs, } from "../types/create-camera-policy-tool-types.js";
|
|
4
4
|
import { postApi } from "../network/network.js";
|
|
5
5
|
const TOOL_NAME = "create-camera-policy-tool";
|
|
6
6
|
const TOOL_DESCRIPTION = `
|
|
7
|
-
A tool for creating a camera policy
|
|
7
|
+
A tool for creating a camera policy.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
Then, the user is presented with a form to configure the schedules for the policy.
|
|
11
|
-
Finally, the user is presented with a form to assign the policy to cameras.
|
|
9
|
+
Preferred (single call): pass name, description, orgUuid, scheduleConfigs, and cameraUuids together — the tool creates the policy, configures its schedule triggers, and assigns the cameras all in one call. Omit policyUuid; it is created for you.
|
|
12
10
|
|
|
13
|
-
|
|
11
|
+
Legacy step-by-step: calling with only a subset of args advances one phase at a time (name/description/orgUuid to create → policyUuid+scheduleConfigs for schedules → policyUuid+cameraUuids for camera assignment).
|
|
12
|
+
|
|
13
|
+
Errors are labelled: a "RETRYABLE" error means nothing was written and you should call again with corrected arguments; a "PARTIAL STATE" error means part of the policy now exists and you must stop and report it.
|
|
14
14
|
`;
|
|
15
|
+
// All args are optional: each step of the flow uses a different subset, and
|
|
16
|
+
// requiring the unused ones forces callers to pad with "" — the guided
|
|
17
|
+
// workflow's "pass ONLY this step's args" instruction then fails validation.
|
|
15
18
|
const TOOL_ARGS = {
|
|
16
19
|
// Step 1: Policy creation
|
|
17
|
-
name: z.string().describe("Policy name (for creating policy)"),
|
|
18
|
-
description: z.string().describe("Policy description (for creating policy)"),
|
|
19
|
-
orgUuid: z.string().describe("Organization UUID (for creating policy)"),
|
|
20
|
+
name: z.string().optional().describe("Policy name (for creating policy)"),
|
|
21
|
+
description: z.string().optional().describe("Policy description (for creating policy)"),
|
|
22
|
+
orgUuid: z.string().optional().describe("Organization UUID (for creating policy)"),
|
|
20
23
|
// Step 2: Schedule configuration
|
|
21
|
-
policyUuid: z
|
|
22
|
-
|
|
24
|
+
policyUuid: z
|
|
25
|
+
.string()
|
|
26
|
+
.optional()
|
|
27
|
+
.describe("Policy UUID of an EXISTING policy (for configuring schedules or assigning cameras on an already-created policy). Omit it when creating a policy — including on a single full call that also passes scheduleConfigs and cameraUuids."),
|
|
28
|
+
scheduleConfigs: z
|
|
29
|
+
.string()
|
|
30
|
+
.optional()
|
|
31
|
+
.describe(`JSON array string of schedule configurations: [{"scheduleUuid": <uuid>, "activities": [<activity>, ...]}]. ` +
|
|
32
|
+
`"activities" must be a real JSON array of activity strings, not a string containing an array, and each ` +
|
|
33
|
+
`value must be an API activity constant (e.g. ${SUGGESTED_CAMERA_ACTIVITIES.slice(0, 6).join(", ")}) — ` +
|
|
34
|
+
`never a display label like "Human Movement". Example: ${SCHEDULE_CONFIGS_EXAMPLE}`),
|
|
23
35
|
// Step 3: Camera assignment
|
|
24
|
-
cameraUuids: z
|
|
25
|
-
|
|
36
|
+
cameraUuids: z
|
|
37
|
+
.string()
|
|
38
|
+
.optional()
|
|
39
|
+
.describe('Comma-separated camera UUIDs to assign policy to, e.g. "uuidA,uuidB". Camera uuids only — resolve names to uuids first.'),
|
|
40
|
+
policyName: z.string().optional().describe("Policy name (for reference)"),
|
|
26
41
|
};
|
|
27
42
|
const TOOL_ARGS_SCHEMA = z.object(TOOL_ARGS);
|
|
43
|
+
// Every result from this tool MUST carry structuredContent: the tool registers
|
|
44
|
+
// an outputSchema, and the MCP SDK rejects any result without structuredContent
|
|
45
|
+
// as "MCP error -32602" — which REPLACES the real failure text and historically
|
|
46
|
+
// made the model (and user) see a validation crash instead of the API error.
|
|
47
|
+
function errorResult(text) {
|
|
48
|
+
return {
|
|
49
|
+
isError: true,
|
|
50
|
+
content: [{ type: "text", text }],
|
|
51
|
+
structuredContent: { needUserInput: false, message: text },
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
// Errors are labelled by whether anything was written, because the caller cannot
|
|
55
|
+
// otherwise tell. The guided workflow says "on error, STOP and make no further
|
|
56
|
+
// calls" — correct after a partial mutation (a retry would create a SECOND
|
|
57
|
+
// policy) and wrong before one, where a corrected retry is free. Prod 2026-08-04:
|
|
58
|
+
// a malformed `scheduleConfigs` was rejected before any write, the model stopped
|
|
59
|
+
// and apologized, and the identical request succeeded when the user asked again.
|
|
60
|
+
const RETRYABLE_PREFIX = "RETRYABLE — nothing was created or changed by this call.";
|
|
61
|
+
const PARTIAL_PREFIX = "PARTIAL STATE — do NOT retry this call and do NOT create another policy.";
|
|
62
|
+
const RETRY_INSTRUCTION = "Fix the arguments named above, then call this tool ONCE more with corrected arguments. Do not tell the user the policy could not be created unless a corrected retry also fails.";
|
|
63
|
+
function retryableError(detail) {
|
|
64
|
+
return errorResult(`${RETRYABLE_PREFIX} ${detail} ${RETRY_INSTRUCTION}`);
|
|
65
|
+
}
|
|
66
|
+
/** Retryable, but a policy from an earlier call already exists — don't re-create it. */
|
|
67
|
+
function retryableStepError(detail, existingPolicyUuid) {
|
|
68
|
+
return errorResult(`${RETRYABLE_PREFIX} Policy ${existingPolicyUuid} already exists from an earlier call — do NOT create another one. ${detail} ${RETRY_INSTRUCTION}`);
|
|
69
|
+
}
|
|
70
|
+
function partialStateError(detail) {
|
|
71
|
+
return errorResult(`${PARTIAL_PREFIX} ${detail} Report exactly this state to the user.`);
|
|
72
|
+
}
|
|
73
|
+
// postApi returns {error: true, status: "..."} on transport/HTTP failures and
|
|
74
|
+
// domain errors arrive as {error: true, errorMsg: "..."} — normalize both.
|
|
75
|
+
function apiErrorText(result) {
|
|
76
|
+
return (result.errorMsg?.trim() ||
|
|
77
|
+
result.status?.trim() ||
|
|
78
|
+
"the Rhombus API reported a failure without a message");
|
|
79
|
+
}
|
|
80
|
+
/** api2 can succeed while reporting a caveat; dropping it hides real problems. */
|
|
81
|
+
function apiWarningSuffix(result) {
|
|
82
|
+
const warning = result.warningMsg?.trim();
|
|
83
|
+
return warning ? ` Note from the API: ${warning}` : "";
|
|
84
|
+
}
|
|
28
85
|
const TOOL_HANDLER = async (args, extra) => {
|
|
29
86
|
const { name, description, orgUuid, policyUuid, scheduleConfigs, cameraUuids, policyName } = args;
|
|
87
|
+
// An empty list is how the legacy step-1 call spells "no schedules yet", so it
|
|
88
|
+
// must not select the schedule phase. Detected by parsing, not by comparing to
|
|
89
|
+
// the literal "[]" — "[ ]" and "\"[]\"" used to slip past that compare and
|
|
90
|
+
// fall through into policy creation, silently creating a DUPLICATE policy.
|
|
91
|
+
const hasScheduleConfigs = Boolean(scheduleConfigs?.trim() && !isEmptyScheduleConfigs(scheduleConfigs));
|
|
92
|
+
const hasFullInputSet = Boolean(name?.trim() && hasScheduleConfigs && cameraUuids?.trim());
|
|
93
|
+
// A full input set PLUS a policyUuid is ambiguous: create everything, or treat
|
|
94
|
+
// the uuid as an existing policy? It used to silently fall through to
|
|
95
|
+
// camera-assignment-only and then report "setup completely finished" for a
|
|
96
|
+
// policy whose schedules were never configured.
|
|
97
|
+
if (hasFullInputSet && policyUuid?.trim()) {
|
|
98
|
+
return retryableError(`Ambiguous arguments: name, scheduleConfigs, and cameraUuids were all provided (a full creation call) together with policyUuid "${policyUuid.trim()}". ` +
|
|
99
|
+
`To create a new policy, drop policyUuid. To modify the existing policy ${policyUuid.trim()}, drop name and send policyUuid with ONE of scheduleConfigs or cameraUuids.`);
|
|
100
|
+
}
|
|
101
|
+
// Full run: all inputs present and no pre-existing policyUuid — create the
|
|
102
|
+
// policy, configure schedules, and assign cameras in ONE call. The guided
|
|
103
|
+
// workflow collects everything before confirming, and executing across three
|
|
104
|
+
// model round trips proved fragile: the model wobbles at each call boundary
|
|
105
|
+
// (re-renders forms, splits args wrong), so the boundaries are removed.
|
|
106
|
+
if (hasFullInputSet) {
|
|
107
|
+
// Validate BOTH structured inputs BEFORE any mutation, so anything wrong
|
|
108
|
+
// with them is a clean retryable failure rather than a half-built policy.
|
|
109
|
+
const parsedConfigs = parseScheduleConfigs(scheduleConfigs);
|
|
110
|
+
if (!parsedConfigs.ok) {
|
|
111
|
+
return retryableError(parsedConfigs.message);
|
|
112
|
+
}
|
|
113
|
+
const parsedCameras = parseCameraUuids(cameraUuids);
|
|
114
|
+
if (!parsedCameras.ok) {
|
|
115
|
+
return retryableError(parsedCameras.message);
|
|
116
|
+
}
|
|
117
|
+
const scheduledTriggers = parsedConfigs.value.map(config => ({
|
|
118
|
+
scheduleUuid: config.scheduleUuid,
|
|
119
|
+
triggerSet: config.activities.map(activity => ({ activity })),
|
|
120
|
+
}));
|
|
121
|
+
const cameraList = parsedCameras.value;
|
|
122
|
+
// Phase 1: create.
|
|
123
|
+
let createdUuid;
|
|
124
|
+
let createWarning = "";
|
|
125
|
+
try {
|
|
126
|
+
const payload = ApiPayloadSchema.parse({
|
|
127
|
+
policy: {
|
|
128
|
+
name,
|
|
129
|
+
description: description?.trim() ? description : undefined,
|
|
130
|
+
orgUuid: orgUuid?.trim() ? orgUuid : undefined,
|
|
131
|
+
scheduledTriggers: [],
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
const result = await createCameraPolicy(payload, extra._meta?.requestModifiers, extra.sessionId);
|
|
135
|
+
if (result.error) {
|
|
136
|
+
return retryableError(`Creating the policy failed: ${apiErrorText(result)}.`);
|
|
137
|
+
}
|
|
138
|
+
if (!result.policyUuid) {
|
|
139
|
+
return retryableError("Creating the policy returned no policyUuid, so it did not succeed.");
|
|
140
|
+
}
|
|
141
|
+
createdUuid = result.policyUuid;
|
|
142
|
+
createWarning = apiWarningSuffix(result);
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
return retryableError(`Creating the policy failed: ${error instanceof Error ? error.message : "Unknown error"}.`);
|
|
146
|
+
}
|
|
147
|
+
// Phase 2: schedules (updateCameraPolicy REPLACES the policy object, so
|
|
148
|
+
// resend the identity fields — see the legacy schedule branch below).
|
|
149
|
+
try {
|
|
150
|
+
const result = await postApi({
|
|
151
|
+
route: "/policy/updateCameraPolicy",
|
|
152
|
+
body: {
|
|
153
|
+
policy: {
|
|
154
|
+
uuid: createdUuid,
|
|
155
|
+
name,
|
|
156
|
+
description: description?.trim() ? description : undefined,
|
|
157
|
+
scheduledTriggers,
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
modifiers: extra._meta?.requestModifiers,
|
|
161
|
+
sessionId: extra.sessionId,
|
|
162
|
+
});
|
|
163
|
+
if (result.error) {
|
|
164
|
+
return partialStateError(`Policy "${name}" was created (uuid ${createdUuid}) but configuring its schedules failed: ${apiErrorText(result)}. No cameras were assigned.`);
|
|
165
|
+
}
|
|
166
|
+
createWarning += apiWarningSuffix(result);
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
return partialStateError(`Policy "${name}" was created (uuid ${createdUuid}) but configuring its schedules failed: ${error instanceof Error ? error.message : "Unknown error"}. No cameras were assigned.`);
|
|
170
|
+
}
|
|
171
|
+
// Phase 3: cameras.
|
|
172
|
+
try {
|
|
173
|
+
const result = await postApi({
|
|
174
|
+
route: "/camera/updateDetailsBulkV2",
|
|
175
|
+
body: {
|
|
176
|
+
cameraBulkDetails: cameraList.map(cameraUuid => ({
|
|
177
|
+
uuid: cameraUuid,
|
|
178
|
+
policyUuid: createdUuid,
|
|
179
|
+
policyUuidUpdated: true,
|
|
180
|
+
})),
|
|
181
|
+
},
|
|
182
|
+
modifiers: extra._meta?.requestModifiers,
|
|
183
|
+
sessionId: extra.sessionId,
|
|
184
|
+
});
|
|
185
|
+
if (result?.error) {
|
|
186
|
+
return partialStateError(`Policy "${name}" was created with its schedules (uuid ${createdUuid}) but camera assignment failed: ${apiErrorText(result)}.`);
|
|
187
|
+
}
|
|
188
|
+
createWarning += apiWarningSuffix(result);
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
return partialStateError(`Policy "${name}" was created with its schedules (uuid ${createdUuid}) but camera assignment failed: ${error instanceof Error ? error.message : "Unknown error"}.`);
|
|
192
|
+
}
|
|
193
|
+
const fullRunResponse = {
|
|
194
|
+
needUserInput: false,
|
|
195
|
+
message: `Policy "${name}" created with ${scheduledTriggers.length} schedule trigger(s) and assigned to ${cameraList.length} camera(s).${createWarning}`,
|
|
196
|
+
policyUuid: createdUuid,
|
|
197
|
+
policyName: name,
|
|
198
|
+
};
|
|
199
|
+
return {
|
|
200
|
+
content: [
|
|
201
|
+
{
|
|
202
|
+
type: "text",
|
|
203
|
+
text: `🎉 Camera policy setup completely finished!\n\n✅ Policy "${name}" created with ${scheduledTriggers.length} schedule trigger(s)\n✅ Assigned to ${cameraList.length} camera(s)\n✅ Policy is now fully active${createWarning}`,
|
|
204
|
+
},
|
|
205
|
+
],
|
|
206
|
+
structuredContent: fullRunResponse,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
30
209
|
// Step 3: Camera assignment (if cameraUuids provided)
|
|
31
210
|
if (cameraUuids?.trim()) {
|
|
211
|
+
// Guard BEFORE the API call: an empty policyUuid here would write
|
|
212
|
+
// policyUuid: "" with policyUuidUpdated: true to every listed camera —
|
|
213
|
+
// i.e. UNASSIGN their existing policies, not assign the new one.
|
|
214
|
+
if (!policyUuid?.trim()) {
|
|
215
|
+
return retryableError(`Cannot assign cameras: policyUuid is empty, so the policy has not been created yet. ` +
|
|
216
|
+
`Either pass the full set (name, scheduleConfigs, cameraUuids) in one call to create everything at once, or create the policy first (name/description/orgUuid call) and pass the returned policyUuid.`);
|
|
217
|
+
}
|
|
218
|
+
const parsedCameras = parseCameraUuids(cameraUuids);
|
|
219
|
+
if (!parsedCameras.ok) {
|
|
220
|
+
// Previously an unusable cameraUuids string (e.g. ",") left this branch
|
|
221
|
+
// with no return at all and fell through into policy creation, creating a
|
|
222
|
+
// duplicate policy and reporting success.
|
|
223
|
+
return retryableStepError(parsedCameras.message, policyUuid.trim());
|
|
224
|
+
}
|
|
225
|
+
const cameraList = parsedCameras.value;
|
|
32
226
|
try {
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
.filter((uuid) => uuid);
|
|
37
|
-
if (cameraList.length > 0) {
|
|
38
|
-
const cameraPayload = {
|
|
227
|
+
const result = await postApi({
|
|
228
|
+
route: "/camera/updateDetailsBulkV2",
|
|
229
|
+
body: {
|
|
39
230
|
cameraBulkDetails: cameraList.map(cameraUuid => ({
|
|
40
231
|
uuid: cameraUuid,
|
|
41
232
|
policyUuid: policyUuid,
|
|
42
233
|
policyUuidUpdated: true,
|
|
43
234
|
})),
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
needUserInput: false,
|
|
53
|
-
message: `Excellent! Policy created and assigned to ${cameraList.length} camera(s)!`,
|
|
54
|
-
policyUuid,
|
|
55
|
-
policyName,
|
|
56
|
-
};
|
|
57
|
-
return {
|
|
58
|
-
content: [
|
|
59
|
-
{
|
|
60
|
-
type: "text",
|
|
61
|
-
text: `🎉 Camera policy setup completely finished!\n\n✅ Policy "${policyName}" assigned to ${cameraList.length} camera(s)\n✅ Policy is now fully active\n\nYour cameras will now generate alerts according to the policy configuration.`,
|
|
62
|
-
},
|
|
63
|
-
],
|
|
64
|
-
structuredContent: jsonResultResponse,
|
|
65
|
-
};
|
|
235
|
+
},
|
|
236
|
+
modifiers: extra._meta?.requestModifiers,
|
|
237
|
+
sessionId: extra.sessionId,
|
|
238
|
+
});
|
|
239
|
+
if (result?.error) {
|
|
240
|
+
// Assigning cameras is idempotent, so a corrected retry is safe — but
|
|
241
|
+
// the policy itself already exists and must not be created again.
|
|
242
|
+
return retryableStepError(`Assigning the policy to the cameras failed: ${apiErrorText(result)}.`, policyUuid);
|
|
66
243
|
}
|
|
67
|
-
|
|
68
|
-
|
|
244
|
+
const warning = apiWarningSuffix(result);
|
|
245
|
+
const jsonResultResponse = {
|
|
246
|
+
needUserInput: false,
|
|
247
|
+
message: `Policy "${policyName?.trim() || policyUuid}" assigned to ${cameraList.length} camera(s).${warning}`,
|
|
248
|
+
policyUuid,
|
|
249
|
+
policyName,
|
|
250
|
+
};
|
|
69
251
|
return {
|
|
70
252
|
content: [
|
|
71
253
|
{
|
|
72
254
|
type: "text",
|
|
73
|
-
|
|
255
|
+
// Claims only what THIS call did: schedules may or may not have been
|
|
256
|
+
// configured by an earlier call, and this branch cannot know.
|
|
257
|
+
text: `✅ Policy "${policyName?.trim() || policyUuid}" assigned to ${cameraList.length} camera(s).${warning}`,
|
|
74
258
|
},
|
|
75
259
|
],
|
|
260
|
+
structuredContent: jsonResultResponse,
|
|
76
261
|
};
|
|
77
262
|
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
return retryableStepError(`Assigning the policy to the cameras failed: ${error instanceof Error ? error.message : "Unknown error"}.`, policyUuid);
|
|
265
|
+
}
|
|
78
266
|
}
|
|
79
267
|
// Step 2: Schedule configuration (if scheduleConfigs provided and non-empty)
|
|
80
|
-
if (
|
|
268
|
+
if (hasScheduleConfigs) {
|
|
269
|
+
if (!policyUuid?.trim()) {
|
|
270
|
+
return retryableError(`Cannot configure schedules: policyUuid is empty. Create the policy first (name/description/orgUuid call) and pass the returned policyUuid, or pass the full set (name, scheduleConfigs, cameraUuids) in one call to do everything at once.`);
|
|
271
|
+
}
|
|
272
|
+
const parsedConfigs = parseScheduleConfigs(scheduleConfigs);
|
|
273
|
+
if (!parsedConfigs.ok) {
|
|
274
|
+
return retryableStepError(parsedConfigs.message, policyUuid.trim());
|
|
275
|
+
}
|
|
276
|
+
const scheduledTriggers = parsedConfigs.value.map(config => ({
|
|
277
|
+
scheduleUuid: config.scheduleUuid,
|
|
278
|
+
triggerSet: config.activities.map(activity => ({ activity })),
|
|
279
|
+
}));
|
|
280
|
+
// /policy/updateCameraPolicy has REPLACE semantics: any field omitted
|
|
281
|
+
// from the policy object is NULLED on the stored policy. Sending only
|
|
282
|
+
// {uuid, scheduledTriggers} erases the name/description that step 1
|
|
283
|
+
// just set (observed in ITG: wizard-created policies with name: null,
|
|
284
|
+
// invisible in the Console). Always resend the identity fields.
|
|
285
|
+
const effectiveName = policyName?.trim() || name?.trim();
|
|
286
|
+
if (!effectiveName) {
|
|
287
|
+
return retryableStepError(`Cannot configure schedules: policyName is required — updateCameraPolicy replaces the whole policy object, so omitting the name would erase it. Pass policyName (and description, if any) along with policyUuid and scheduleConfigs.`, policyUuid.trim());
|
|
288
|
+
}
|
|
81
289
|
try {
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
return {
|
|
100
|
-
content: [
|
|
101
|
-
{
|
|
102
|
-
type: "text",
|
|
103
|
-
text: `Failed to configure policy schedules: ${result.errorMsg || "Unknown error"}`,
|
|
104
|
-
},
|
|
105
|
-
],
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
// Success - now show camera assignment form
|
|
109
|
-
const jsonResultResponse = {
|
|
110
|
-
needUserInput: true,
|
|
111
|
-
message: `Excellent! Policy schedules configured.\n\nFinal step: Please select which cameras should use this policy.`,
|
|
112
|
-
requestType: "camera-assignment",
|
|
113
|
-
submitAction: "create-camera-policy-tool",
|
|
114
|
-
policyUuid,
|
|
115
|
-
policyName,
|
|
116
|
-
};
|
|
117
|
-
return {
|
|
118
|
-
content: [
|
|
119
|
-
{
|
|
120
|
-
type: "text",
|
|
121
|
-
text: JSON.stringify(jsonResultResponse),
|
|
122
|
-
},
|
|
123
|
-
],
|
|
124
|
-
structuredContent: jsonResultResponse,
|
|
125
|
-
};
|
|
290
|
+
const result = await postApi({
|
|
291
|
+
route: "/policy/updateCameraPolicy",
|
|
292
|
+
body: {
|
|
293
|
+
policy: {
|
|
294
|
+
uuid: policyUuid,
|
|
295
|
+
name: effectiveName,
|
|
296
|
+
description: description?.trim() ? description : undefined,
|
|
297
|
+
scheduledTriggers,
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
modifiers: extra._meta?.requestModifiers,
|
|
301
|
+
sessionId: extra.sessionId,
|
|
302
|
+
});
|
|
303
|
+
if (result.error) {
|
|
304
|
+
// Replacing the schedules is idempotent — safe to retry, as long as the
|
|
305
|
+
// policy is not created a second time.
|
|
306
|
+
return retryableStepError(`Configuring the policy schedules failed: ${apiErrorText(result)}.`, policyUuid);
|
|
126
307
|
}
|
|
127
|
-
//
|
|
128
|
-
|
|
129
|
-
|
|
308
|
+
// Neutral, fact-only message: imperative text like "Please select
|
|
309
|
+
// which cameras..." is an instruction the model follows over the
|
|
310
|
+
// workflow playbook (observed: it re-rendered the confirm form or
|
|
311
|
+
// stopped mid-execution to ask for cameras it already had).
|
|
312
|
+
const jsonResultResponse = {
|
|
313
|
+
needUserInput: false,
|
|
314
|
+
message: `Schedules configured for policy "${effectiveName}" (${policyUuid}).${apiWarningSuffix(result)}`,
|
|
315
|
+
policyUuid,
|
|
316
|
+
policyName,
|
|
317
|
+
};
|
|
130
318
|
return {
|
|
131
319
|
content: [
|
|
132
320
|
{
|
|
133
321
|
type: "text",
|
|
134
|
-
text:
|
|
322
|
+
text: JSON.stringify(jsonResultResponse),
|
|
135
323
|
},
|
|
136
324
|
],
|
|
325
|
+
structuredContent: jsonResultResponse,
|
|
137
326
|
};
|
|
138
327
|
}
|
|
328
|
+
catch (error) {
|
|
329
|
+
return retryableStepError(`Configuring the policy schedules failed: ${error instanceof Error ? error.message : "Unknown error"}.`, policyUuid);
|
|
330
|
+
}
|
|
139
331
|
}
|
|
140
332
|
// Step 1: Policy creation (if name provided)
|
|
141
333
|
if (name?.trim()) {
|
|
@@ -150,22 +342,16 @@ const TOOL_HANDLER = async (args, extra) => {
|
|
|
150
342
|
});
|
|
151
343
|
const result = await createCameraPolicy(payload, extra._meta?.requestModifiers, extra.sessionId);
|
|
152
344
|
if (result.error) {
|
|
153
|
-
return {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
text: `Failed to create camera policy: ${result.errorMsg || "Unknown error"}`,
|
|
158
|
-
},
|
|
159
|
-
],
|
|
160
|
-
};
|
|
345
|
+
return retryableError(`Creating the policy failed: ${apiErrorText(result)}.`);
|
|
346
|
+
}
|
|
347
|
+
if (!result.policyUuid) {
|
|
348
|
+
return retryableError("Creating the policy returned no policyUuid, so it did not succeed.");
|
|
161
349
|
}
|
|
162
|
-
console.error(`[createCameraPolicyTool] --
|
|
163
|
-
//
|
|
350
|
+
console.error(`[createCameraPolicyTool] -- Policy created. Got result ${JSON.stringify(result)}`);
|
|
351
|
+
// Neutral, fact-only message (see the schedule-step comment above).
|
|
164
352
|
const jsonResultResponse = {
|
|
165
|
-
needUserInput:
|
|
166
|
-
message: `
|
|
167
|
-
requestType: "schedule-trigger-configuration",
|
|
168
|
-
submitAction: "create-camera-policy-tool",
|
|
353
|
+
needUserInput: false,
|
|
354
|
+
message: `Policy "${name}" created with UUID: ${result.policyUuid}${apiWarningSuffix(result)}`,
|
|
169
355
|
policyUuid: result.policyUuid,
|
|
170
356
|
policyName: name,
|
|
171
357
|
};
|
|
@@ -180,35 +366,31 @@ const TOOL_HANDLER = async (args, extra) => {
|
|
|
180
366
|
};
|
|
181
367
|
}
|
|
182
368
|
catch (error) {
|
|
183
|
-
return {
|
|
184
|
-
content: [
|
|
185
|
-
{
|
|
186
|
-
type: "text",
|
|
187
|
-
text: `Error creating camera policy: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
188
|
-
},
|
|
189
|
-
],
|
|
190
|
-
};
|
|
369
|
+
return retryableError(`Creating the policy failed: ${error instanceof Error ? error.message : "Unknown error"}.`);
|
|
191
370
|
}
|
|
192
371
|
}
|
|
193
|
-
// Step 0:
|
|
372
|
+
// Step 0: no args provided — tell the caller what's needed (plain facts, no
|
|
373
|
+
// legacy elicitation requestType/submitAction hints).
|
|
374
|
+
const initialFormResponse = {
|
|
375
|
+
needUserInput: true,
|
|
376
|
+
message: "No policy was created — this call had no arguments. To create a camera policy, collect from the user: a policy name (required), an optional description, the schedule triggers, and the cameras to assign, then call this tool again with name, description, orgUuid, scheduleConfigs, and cameraUuids together.",
|
|
377
|
+
};
|
|
194
378
|
return {
|
|
195
379
|
content: [
|
|
196
380
|
{
|
|
197
381
|
type: "text",
|
|
198
|
-
text: JSON.stringify(
|
|
199
|
-
needUserInput: true,
|
|
200
|
-
message: "Please provide the following information to create your camera policy:\n\n1. **Policy Name** (required): A descriptive name for the policy\n2. **Policy Description** (optional): What this policy does\n3. **Organization UUID** (optional): Leave blank to use your current organization\n\nOnce you provide this information, I'll create the policy for you.",
|
|
201
|
-
requestType: "policy-creation-form",
|
|
202
|
-
submitAction: "create-camera-policy-tool",
|
|
203
|
-
}),
|
|
382
|
+
text: JSON.stringify(initialFormResponse),
|
|
204
383
|
},
|
|
205
384
|
],
|
|
385
|
+
structuredContent: initialFormResponse,
|
|
206
386
|
};
|
|
207
387
|
};
|
|
208
388
|
export function createTool(server) {
|
|
209
389
|
server.registerTool(TOOL_NAME, {
|
|
390
|
+
title: "Create Camera Policy",
|
|
210
391
|
description: TOOL_DESCRIPTION,
|
|
211
392
|
inputSchema: TOOL_ARGS,
|
|
212
393
|
outputSchema: OUTPUT_SCHEMA.shape,
|
|
394
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
213
395
|
}, TOOL_HANDLER);
|
|
214
396
|
}
|