rhombus-node-mcp 0.1.56 → 0.1.58

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 (102) 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 +6 -4
  33. package/dist/tools/get-entity-tool.js +40 -20
  34. package/dist/tools/get-org-information-tool.js +30 -2
  35. package/dist/tools/time-conversion-tool.js +59 -36
  36. package/dist/tools/time-tool.js +12 -3
  37. package/dist/tools/user-tool.js +100 -2
  38. package/dist/tools-console/access-anomaly-tool.js +2 -19
  39. package/dist/tools-console/access-control-tool.js +328 -2
  40. package/dist/tools-console/alarm-monitoring-tool.js +72 -2
  41. package/dist/tools-console/analytics-tool.js +2 -2
  42. package/dist/tools-console/badge-timeline-tool.js +2 -21
  43. package/dist/tools-console/camera-policy-tool.js +252 -0
  44. package/dist/tools-console/camera-tool.js +111 -89
  45. package/dist/tools-console/clips-tool.js +21 -31
  46. package/dist/tools-console/count-tool.js +8 -0
  47. package/dist/tools-console/create-camera-policy-tool.js +302 -122
  48. package/dist/tools-console/door-schedule-exception-tool.js +2 -33
  49. package/dist/tools-console/door-tool.js +130 -2
  50. package/dist/tools-console/elements-access-anomaly-tool.js +2 -19
  51. package/dist/tools-console/elements-badge-timeline-tool.js +2 -21
  52. package/dist/tools-console/elements-lost-badge-tool.js +2 -17
  53. package/dist/tools-console/elements-tool.js +2 -29
  54. package/dist/tools-console/events-tool.js +22 -101
  55. package/dist/tools-console/faces-tool.js +254 -28
  56. package/dist/tools-console/guest-management-tool.js +127 -2
  57. package/dist/tools-console/location-tool.js +156 -35
  58. package/dist/tools-console/lost-badge-tool.js +2 -17
  59. package/dist/tools-console/lpr-tool.js +89 -23
  60. package/dist/tools-console/netbox-access-anomaly-tool.js +2 -19
  61. package/dist/tools-console/netbox-badge-timeline-tool.js +2 -21
  62. package/dist/tools-console/netbox-lost-badge-tool.js +2 -17
  63. package/dist/tools-console/netbox-tool.js +2 -29
  64. package/dist/tools-console/onguard-tool.js +2 -30
  65. package/dist/tools-console/person-tracking-tool.js +2 -2
  66. package/dist/tools-console/policy-alerts-tool.js +5 -29
  67. package/dist/tools-console/reboot-cameras-tool.js +17 -2
  68. package/dist/tools-console/report-tool.js +11 -41
  69. package/dist/tools-console/schedule-tool.js +190 -0
  70. package/dist/tools-console/update-tool.js +298 -168
  71. package/dist/tools-console/video-walls-tool.js +93 -21
  72. package/dist/types/access-anomaly-tool-types.js +22 -2
  73. package/dist/types/access-control-tool-types.js +116 -1
  74. package/dist/types/alarm-monitoring-tool-types.js +42 -1
  75. package/dist/types/badge-timeline-tool-types.js +18 -3
  76. package/dist/types/camera-policy-tool-types.js +104 -0
  77. package/dist/types/camera-tool-types.js +80 -1
  78. package/dist/types/clips-tool-types.js +27 -3
  79. package/dist/types/create-camera-policy-tool-types.js +275 -26
  80. package/dist/types/door-schedule-exception-tool-types.js +20 -6
  81. package/dist/types/door-tool-types.js +36 -4
  82. package/dist/types/entity-lookup-tool-types.js +6 -1
  83. package/dist/types/events-tools-types.js +24 -16
  84. package/dist/types/faces-tools-types.js +106 -3
  85. package/dist/types/get-entity-tool-types.js +76 -12
  86. package/dist/types/guest-management-tool-types.js +103 -1
  87. package/dist/types/location-tool-types.js +100 -14
  88. package/dist/types/lost-badge-tool-types.js +17 -2
  89. package/dist/types/lpr-tool-types.js +47 -8
  90. package/dist/types/onguard-tool-types.js +21 -1
  91. package/dist/types/policy-alerts-tool-types.js +1 -1
  92. package/dist/types/report-tool-types.js +29 -1
  93. package/dist/types/schedule-tool-types.js +370 -0
  94. package/dist/types/schema.js +479 -9
  95. package/dist/types/time-conversion-tool-types.js +3 -4
  96. package/dist/types/update-tool-types.js +40 -18
  97. package/dist/types/user-tool-types.js +44 -1
  98. package/dist/types/video-walls-tool-types.js +30 -2
  99. package/dist/types/zod-schemas.js +5128 -996
  100. package/dist/util.js +62 -3
  101. package/dist/utils/entity-name-match.js +11 -0
  102. package/package.json +3 -1
@@ -1,4 +1,4 @@
1
- import { listUsers, findUserByEmail, getPermissionsForCurrentUser, getPermissionGroups, } from "../api/user-tool-api.js";
1
+ import { listUsers, findUserByEmail, getPermissionsForCurrentUser, getPermissionGroups, createUser, updateUser, deleteUser, } from "../api/user-tool-api.js";
2
2
  import { UserToolRequestType, OUTPUT_SCHEMA, TOOL_ARGS, } from "../types/user-tool-types.js";
3
3
  import { createToolStructuredContent, createToolTextContent, extractFromToolExtra, } from "../util.js";
4
4
  const TOOL_NAME = "user-tool";
@@ -10,6 +10,11 @@ It has the following modes of operation, determined by the "requestType" paramet
10
10
  - ${UserToolRequestType.FIND_BY_EMAIL}: Find a specific user by their email address. Requires the email parameter.
11
11
  - ${UserToolRequestType.GET_PERMISSIONS}: Get the permissions for the current API user/token.
12
12
  - ${UserToolRequestType.GET_PERMISSION_GROUPS}: List all permission groups defined in the organization. Each row can be very large — see below.
13
+ - ${UserToolRequestType.CREATE_USER}: Add a user to the organization. Requires userName, email and permissionGroupUuid. Sends them an invitation email unless suppressWelcomeEmail is true.
14
+ - ${UserToolRequestType.UPDATE_USER}: Change a user's name and/or role. Requires userUuid plus userName and/or permissionGroupUuid.
15
+ - ${UserToolRequestType.DELETE_USER}: Remove a user. Requires userUuid and confirmDelete.
16
+
17
+ Creating a user emails a real person and a role change alters what they can see across the organization, so confirm the name, email address and role NAME with the user before writing. Never guess a userUuid or permissionGroupUuid.
13
18
 
14
19
  User UUIDs returned here can be used with the access-control-tool to look up credentials.
15
20
 
@@ -20,6 +25,11 @@ Each permission group row includes five access maps whose size scales with the o
20
25
  - Heavy fields (O(locations) / O(devices) / O(groups) per row): 'permissionGroups.locationAccessMap', 'permissionGroups.accessControlLocationAccessMap', 'permissionGroups.deviceAccessMap', 'permissionGroups.userPermissionGroupAccessMap', 'permissionGroups.locationGranularAccessMap'. Only request these when you specifically need them for a user.
21
26
  Typical usage when just picking a role uuid: 'includeFields: ["permissionGroups.uuid", "permissionGroups.name", "permissionGroups.description"]'.
22
27
  `;
28
+ /** Users carry firstName/lastName, not a single name field. */
29
+ function displayName(user) {
30
+ const name = [user.firstName, user.lastName].filter(Boolean).join(" ").trim();
31
+ return name || undefined;
32
+ }
23
33
  const TOOL_HANDLER = async (args, _extra) => {
24
34
  const { requestModifiers, sessionId } = extractFromToolExtra(_extra);
25
35
  try {
@@ -43,6 +53,94 @@ const TOOL_HANDLER = async (args, _extra) => {
43
53
  const permissionGroups = await getPermissionGroups(requestModifiers, sessionId);
44
54
  return createToolStructuredContent({ permissionGroups });
45
55
  }
56
+ case UserToolRequestType.CREATE_USER: {
57
+ if (!args.userName?.trim() || !args.email?.trim() || !args.permissionGroupUuid) {
58
+ return createToolTextContent("userName, email and permissionGroupUuid are all required for create-user. Get role UUIDs from get-permission-groups.");
59
+ }
60
+ // A wrong role here silently over- or under-privileges a real person, so
61
+ // verify the role exists rather than trusting the uuid.
62
+ const permissionGroups = await getPermissionGroups(requestModifiers, sessionId);
63
+ const role = permissionGroups.find(group => group.uuid === args.permissionGroupUuid);
64
+ if (!role) {
65
+ return createToolTextContent(`No permission group in this organization has the uuid "${args.permissionGroupUuid}". Use get-permission-groups to see the roles that exist — do not guess a role uuid, because it decides what the new user can see and do.`);
66
+ }
67
+ const existing = await findUserByEmail(args.email.trim(), requestModifiers, sessionId);
68
+ if (existing?.uuid) {
69
+ return createToolStructuredContent({
70
+ user: existing,
71
+ note: `Nothing was created — ${args.email.trim()} is already a user in this organization (${displayName(existing) ?? "no name"}). Use update-user to change their name or role.`,
72
+ });
73
+ }
74
+ const created = await createUser({
75
+ name: args.userName.trim(),
76
+ email: args.email.trim(),
77
+ permissionGroupUuid: args.permissionGroupUuid,
78
+ suppressWelcomeEmail: args.suppressWelcomeEmail ?? false,
79
+ }, requestModifiers, sessionId);
80
+ return createToolStructuredContent({
81
+ created,
82
+ warningMsg: created.warningMsg,
83
+ note: `Created the user "${args.userName.trim()}" (${args.email.trim()}) with the role "${role.name ?? args.permissionGroupUuid}". ${args.suppressWelcomeEmail ? "No invitation email was sent, so they cannot sign in until invited." : "An invitation email has been sent to that address."}`,
84
+ });
85
+ }
86
+ case UserToolRequestType.UPDATE_USER: {
87
+ if (!args.userUuid) {
88
+ return createToolTextContent("userUuid is required for update-user.");
89
+ }
90
+ if (!args.userName?.trim() && !args.permissionGroupUuid) {
91
+ return createToolTextContent("update-user needs at least one of userName or permissionGroupUuid — both were empty, so there is nothing to change.");
92
+ }
93
+ const users = await listUsers(requestModifiers, sessionId);
94
+ const existing = users.find(user => user.uuid === args.userUuid);
95
+ if (!existing) {
96
+ return createToolTextContent(`No user in this organization has the uuid "${args.userUuid}". Use list-users or find-by-email to find them — do not guess a uuid.`);
97
+ }
98
+ let roleName;
99
+ if (args.permissionGroupUuid) {
100
+ const permissionGroups = await getPermissionGroups(requestModifiers, sessionId);
101
+ const role = permissionGroups.find(group => group.uuid === args.permissionGroupUuid);
102
+ if (!role) {
103
+ return createToolTextContent(`No permission group in this organization has the uuid "${args.permissionGroupUuid}". Use get-permission-groups to see the roles that exist.`);
104
+ }
105
+ roleName = role.name ?? args.permissionGroupUuid;
106
+ }
107
+ const updated = await updateUser(args.userUuid, {
108
+ name: args.userName?.trim() || undefined,
109
+ permissionGroupUuid: args.permissionGroupUuid ?? undefined,
110
+ }, requestModifiers, sessionId);
111
+ return createToolStructuredContent({
112
+ updated,
113
+ warningMsg: updated.warningMsg,
114
+ note: roleName
115
+ ? `Updated ${displayName(existing) ?? args.userUuid}. Their role is now "${roleName}", which changes what they can see and do across the organization.`
116
+ : `Renamed ${displayName(existing) ?? args.userUuid} to "${args.userName?.trim()}". Their role and access are unchanged.`,
117
+ });
118
+ }
119
+ case UserToolRequestType.DELETE_USER: {
120
+ if (!args.userUuid) {
121
+ return createToolTextContent("userUuid is required for delete-user.");
122
+ }
123
+ const users = await listUsers(requestModifiers, sessionId);
124
+ const existing = users.find(user => user.uuid === args.userUuid);
125
+ if (!existing) {
126
+ return createToolTextContent(`No user in this organization has the uuid "${args.userUuid}". Use list-users or find-by-email to find them.`);
127
+ }
128
+ if (!args.confirmDelete) {
129
+ return createToolStructuredContent({
130
+ user: existing,
131
+ note: `NOT DELETED — nothing was changed. Deleting ${displayName(existing) ?? args.userUuid} (${existing.email ?? "no email"}) removes their ` +
132
+ `console access and their access control credentials, and cannot be undone. If the intent is only to stop their door access, ` +
133
+ `access-control-tool's suspend-credential is reversible. To delete the user, confirm with the user and call again with ` +
134
+ `confirmDelete: true.`,
135
+ });
136
+ }
137
+ const deleted = await deleteUser(args.userUuid, requestModifiers, sessionId);
138
+ return createToolStructuredContent({
139
+ deleted,
140
+ warningMsg: deleted.warningMsg,
141
+ note: `Deleted the user ${displayName(existing) ?? args.userUuid} (${existing.email ?? "no email"}). They can no longer sign in, and their access control credentials no longer work.`,
142
+ });
143
+ }
46
144
  }
47
145
  }
48
146
  catch (error) {
@@ -59,6 +157,6 @@ export function createTool(server) {
59
157
  description: TOOL_DESCRIPTION,
60
158
  inputSchema: TOOL_ARGS,
61
159
  outputSchema: OUTPUT_SCHEMA.shape,
62
- annotations: { readOnlyHint: true },
160
+ annotations: { readOnlyHint: false, destructiveHint: true },
63
161
  }, TOOL_HANDLER);
64
162
  }
@@ -1,26 +1,9 @@
1
1
  import { getAccessAnomalies } from "../api/access-anomaly-tool-api.js";
2
2
  import { OUTPUT_SCHEMA, TOOL_ARGS } from "../types/access-anomaly-tool-types.js";
3
3
  import { createToolStructuredContent, extractFromToolExtra } from "../util.js";
4
+ import { buildAccessAnomalyToolDescription } from "../types/access-anomaly-tool-types.js";
4
5
  const TOOL_NAME = "access-anomaly-tool";
5
- const TOOL_DESCRIPTION = `
6
- Scans Honeywell OnGuard (Lenel) access events over a time window and flags anomalous badge activity. Use for
7
- "find unusual badge activity", "anything suspicious in access this week", or proactive access review.
8
-
9
- Runs deterministic rules and returns ranked findings (high severity first):
10
- - lost_or_inactive_badge: a non-active badge (lost/suspended) was used
11
- - entry_not_made: access granted but no entry made (possible tailgating)
12
- - off_hours: entry outside business hours (configurable)
13
- - impossible_travel: one cardholder at two different areas seconds apart
14
- - area_novelty: a cardholder's first-ever entry to an area vs their prior history (needs a baseline window)
15
-
16
- Each finding includes cardholderName, the rule, severity, datetime, area, the camera deviceUuid, a plain-language
17
- rationale, and clip/still hints. Resolve relative times (e.g. "this week") to ISO 8601 first via the timestamp tool.
18
-
19
- This is a triage aid: present findings grouped by severity, and for the notable ones call the camera-tool
20
- (requestType "image", cameraUuid = finding.deviceUuid, timestamp = finding.timestampMs) and/or clips-tool
21
- ("createClip" using finding.clipHint) — in PARALLEL — so a human can confirm. Don't assert wrongdoing; surface the
22
- evidence.
23
- `;
6
+ const TOOL_DESCRIPTION = buildAccessAnomalyToolDescription("Honeywell OnGuard (Lenel)");
24
7
  const TOOL_HANDLER = async (args, _extra) => {
25
8
  const { requestModifiers, sessionId } = extractFromToolExtra(_extra);
26
9
  try {
@@ -1,4 +1,4 @@
1
- import { unlockDoor, getAccessControlGroups, getCredentialsByUser, getLockdownPlans, activateLockdown, deactivateLockdown, getDoorScheduleExceptions, getAccessGrants, getRemoteUnlockUsers, } from "../api/access-control-tool-api.js";
1
+ import { unlockDoor, getAccessControlGroups, createAccessControlGroup, updateAccessControlGroup, deleteAccessControlGroup, changeAccessControlGroupMembers, getCredentialsByUser, assignCredential, changeCredentialState, updateCredentialNote, getLockdownPlans, renameLockdownPlan, deleteLockdownPlan, activateLockdown, deactivateLockdown, getDoorScheduleExceptions, getAccessGrants, writeAccessGrant, deleteAccessGrant, getRemoteUnlockUsers, } from "../api/access-control-tool-api.js";
2
2
  import { AccessControlRequestType, OUTPUT_SCHEMA, TOOL_ARGS, } from "../types/access-control-tool-types.js";
3
3
  import { createToolStructuredContent, createToolTextContent, extractFromToolExtra, } from "../util.js";
4
4
  const TOOL_NAME = "access-control-tool";
@@ -12,13 +12,32 @@ It has the following modes of operation, determined by the "requestType" paramet
12
12
  - ${AccessControlRequestType.GET_LOCKDOWN_PLANS}: List all lockdown plans in the organization.
13
13
  - ${AccessControlRequestType.ACTIVATE_LOCKDOWN}: Activate a lockdown plan at a location. Requires locationUuid and lockdownPlanUuid.
14
14
  - ${AccessControlRequestType.DEACTIVATE_LOCKDOWN}: Deactivate a lockdown plan at a location. Requires locationUuid and lockdownPlanUuid.
15
- - ${AccessControlRequestType.GET_DOOR_SCHEDULES}: Get door schedule exceptions for a location. Requires locationUuid.
15
+ - ${AccessControlRequestType.GET_DOOR_SCHEDULES}: Get door schedule EXCEPTIONS for a location (despite the name, this does not list schedules). Requires locationUuid. For anything more than a location-scoped list use door-schedule-exception-tool; for the schedules themselves use schedule-tool.
16
16
  - ${AccessControlRequestType.GET_ACCESS_GRANTS}: List location access grants (physical badge/card access). Optionally accepts locationUuid to filter by location. Each grant includes userUuids (directly assigned users), groupUuids (assigned access control groups), and doorUuids (the doors this grant provides access to).
17
17
  - ${AccessControlRequestType.GET_REMOTE_UNLOCK_USERS}: Get all users who have permission to remotely unlock doors at a location. Requires locationUuid. Returns a list of doors with remote unlock enabled and the users who can unlock each door, based on their permission group roles. This is the correct tool for questions about remote unlock permissions.
18
18
 
19
+ Write operations — every one of these changes who can physically open a door:
20
+ - ${AccessControlRequestType.CREATE_GROUP}: Create an access control group. Requires groupName; optionally groupDescription and userUuids.
21
+ - ${AccessControlRequestType.UPDATE_GROUP}: Rename or re-describe a group. Requires groupUuid plus groupName and/or groupDescription.
22
+ - ${AccessControlRequestType.DELETE_GROUP}: Delete a group. Requires groupUuid and confirmDelete.
23
+ - ${AccessControlRequestType.ADD_USERS_TO_GROUP} / ${AccessControlRequestType.REMOVE_USERS_FROM_GROUP}: Change group membership. Requires groupUuid and userUuids.
24
+ - ${AccessControlRequestType.ASSIGN_CREDENTIAL}: Attach an existing unassigned badge/fob to a user. Requires credentialHexValue and userUuid.
25
+ - ${AccessControlRequestType.SUSPEND_CREDENTIAL} / ${AccessControlRequestType.UNSUSPEND_CREDENTIAL}: Temporarily disable / re-enable a credential. Requires credentialUuid. Reversible — prefer this for a lost badge that may turn up.
26
+ - ${AccessControlRequestType.REVOKE_CREDENTIAL}: Detach a credential from its user, keeping the credential record. Requires credentialUuid.
27
+ - ${AccessControlRequestType.DELETE_CREDENTIAL}: Destroy the credential record entirely. Requires credentialUuid and confirmDelete. Not reversible — suspend or revoke instead unless the user asked for permanent deletion.
28
+ - ${AccessControlRequestType.UPDATE_CREDENTIAL_NOTE}: Set a credential's note. Requires credentialUuid and credentialNote.
29
+ - ${AccessControlRequestType.CREATE_ACCESS_GRANT}: Grant access to doors. Requires accessGrantName, locationUuid and doorUuids; optionally userUuids, groupUuids, scheduleUuid.
30
+ - ${AccessControlRequestType.UPDATE_ACCESS_GRANT}: Change a grant. Requires accessGrantUuid. doorUuids / userUuids / groupUuids REPLACE the existing lists when passed.
31
+ - ${AccessControlRequestType.DELETE_ACCESS_GRANT}: Delete a grant, removing that access. Requires accessGrantUuid and confirmDelete.
32
+ - ${AccessControlRequestType.RENAME_LOCKDOWN_PLAN}: Rename a lockdown plan. Requires lockdownPlanUuid and groupName. The plan's door states and activation steps cannot be authored through this tool — that is deliberate, since a wrong door state during a lockdown is a life-safety issue. Direct the user to the Rhombus Console for those.
33
+ - ${AccessControlRequestType.DELETE_LOCKDOWN_PLAN}: Delete a lockdown plan. Requires lockdownPlanUuid and confirmDelete.
34
+
35
+ Before any write, name the specific people and doors affected and get the user's confirmation. Never guess a user, door, group or credential UUID: a plausible-but-wrong UUID here silently grants or removes physical access, and nothing in the response will reveal it.
36
+
19
37
  Use the get-entity-tool with entityType ACCESS_CONTROL_DOOR to get door UUIDs.
20
38
  Use the user-tool to look up user UUIDs and resolve them to names/emails.
21
39
  Use the location-tool to get location UUIDs.
40
+ Use the schedule-tool to get schedule UUIDs.
22
41
  `;
23
42
  const TOOL_HANDLER = async (args, _extra) => {
24
43
  const { requestModifiers, sessionId } = extractFromToolExtra(_extra);
@@ -78,6 +97,313 @@ const TOOL_HANDLER = async (args, _extra) => {
78
97
  const remoteUnlockUsers = await getRemoteUnlockUsers(args.locationUuid, requestModifiers, sessionId);
79
98
  return createToolStructuredContent({ remoteUnlockUsers });
80
99
  }
100
+ // ---------------------------------------------------------------------
101
+ // Access control groups
102
+ // ---------------------------------------------------------------------
103
+ case AccessControlRequestType.CREATE_GROUP: {
104
+ if (!args.groupName?.trim()) {
105
+ return createToolTextContent("groupName is required for create-group.");
106
+ }
107
+ const created = await createAccessControlGroup(args.groupName.trim(), args.groupDescription ?? undefined, args.userUuids ?? [], requestModifiers, sessionId);
108
+ return createToolStructuredContent({
109
+ created,
110
+ note: `Created the access control group "${args.groupName.trim()}" with ${created.memberCount} member(s). A group grants no access on its own — attach it to an access grant with create-access-grant or update-access-grant.`,
111
+ });
112
+ }
113
+ case AccessControlRequestType.UPDATE_GROUP: {
114
+ if (!args.groupUuid) {
115
+ return createToolTextContent("groupUuid is required for update-group.");
116
+ }
117
+ if (!args.groupName?.trim() && !args.groupDescription?.trim()) {
118
+ return createToolTextContent("update-group needs at least one of groupName or groupDescription — both were empty, so there is nothing to change.");
119
+ }
120
+ const groups = await getAccessControlGroups(requestModifiers, sessionId);
121
+ const existing = groups.find(group => group.uuid === args.groupUuid);
122
+ if (!existing) {
123
+ return createToolTextContent(`No access control group in this organization has the uuid "${args.groupUuid}". Use get-groups to see the groups that exist — do not guess a uuid.`);
124
+ }
125
+ const updated = await updateAccessControlGroup(args.groupUuid, {
126
+ name: args.groupName?.trim() || existing.name,
127
+ description: args.groupDescription?.trim() || existing.description,
128
+ }, requestModifiers, sessionId);
129
+ return createToolStructuredContent({
130
+ updated,
131
+ note: `Updated the access control group "${args.groupName?.trim() || existing.name || args.groupUuid}". Its membership and the access it confers are unchanged.`,
132
+ });
133
+ }
134
+ case AccessControlRequestType.DELETE_GROUP: {
135
+ if (!args.groupUuid) {
136
+ return createToolTextContent("groupUuid is required for delete-group.");
137
+ }
138
+ const groups = await getAccessControlGroups(requestModifiers, sessionId);
139
+ const existing = groups.find(group => group.uuid === args.groupUuid);
140
+ if (!existing) {
141
+ return createToolTextContent(`No access control group in this organization has the uuid "${args.groupUuid}". Use get-groups to see the groups that exist.`);
142
+ }
143
+ if (!args.confirmDelete) {
144
+ const memberCount = existing.userUuids?.length ?? 0;
145
+ return createToolStructuredContent({
146
+ accessControlGroups: [existing],
147
+ note: `NOT DELETED — nothing was changed. Deleting the group "${existing.name ?? args.groupUuid}" removes it from every access ` +
148
+ `grant that uses it, so its ${memberCount} member(s) lose whatever door access the group provided. Tell the user which ` +
149
+ `people are affected, then call this tool again with confirmDelete: true.`,
150
+ });
151
+ }
152
+ const deleted = await deleteAccessControlGroup(args.groupUuid, requestModifiers, sessionId);
153
+ return createToolStructuredContent({
154
+ deleted,
155
+ note: `Deleted the access control group "${existing.name ?? args.groupUuid}". Its members lose any door access that came from this group.`,
156
+ });
157
+ }
158
+ case AccessControlRequestType.ADD_USERS_TO_GROUP:
159
+ case AccessControlRequestType.REMOVE_USERS_FROM_GROUP: {
160
+ const removing = args.requestType === AccessControlRequestType.REMOVE_USERS_FROM_GROUP;
161
+ if (!args.groupUuid) {
162
+ return createToolTextContent(`groupUuid is required for ${args.requestType}.`);
163
+ }
164
+ if (!args.userUuids?.length) {
165
+ return createToolTextContent(`userUuids is required for ${args.requestType} and must contain at least one user UUID. Resolve names to UUIDs with user-tool first.`);
166
+ }
167
+ const groups = await getAccessControlGroups(requestModifiers, sessionId);
168
+ const existing = groups.find(group => group.uuid === args.groupUuid);
169
+ if (!existing) {
170
+ return createToolTextContent(`No access control group in this organization has the uuid "${args.groupUuid}". Use get-groups to see the groups that exist.`);
171
+ }
172
+ const updated = await changeAccessControlGroupMembers(args.groupUuid, args.userUuids, removing ? "remove" : "add", requestModifiers, sessionId);
173
+ return createToolStructuredContent({
174
+ updated,
175
+ note: `${removing ? "Removed" : "Added"} ${updated.userCount} user(s) ${removing ? "from" : "to"} "${existing.name ?? args.groupUuid}". They ${removing ? "lose" : "gain"} every door this group's access grants cover. Confirm the specific people with the user by name.`,
176
+ });
177
+ }
178
+ // ---------------------------------------------------------------------
179
+ // Credentials
180
+ // ---------------------------------------------------------------------
181
+ case AccessControlRequestType.ASSIGN_CREDENTIAL: {
182
+ if (!args.credentialHexValue?.trim() || !args.userUuid) {
183
+ return createToolTextContent("credentialHexValue and userUuid are both required for assign-credential.");
184
+ }
185
+ const result = await assignCredential(args.credentialHexValue.trim(), args.userUuid, requestModifiers, sessionId);
186
+ return createToolStructuredContent({
187
+ credentialChange: {
188
+ success: result.success,
189
+ userUuid: result.userUuid,
190
+ action: "assign",
191
+ },
192
+ warningMsg: result.warningMsg,
193
+ note: `Assigned credential ${args.credentialHexValue.trim()} to the user. The credential opens whatever doors that user's access grants cover — confirm the user's name and their access with get-access-grants if it matters.`,
194
+ });
195
+ }
196
+ case AccessControlRequestType.REVOKE_CREDENTIAL:
197
+ case AccessControlRequestType.SUSPEND_CREDENTIAL:
198
+ case AccessControlRequestType.UNSUSPEND_CREDENTIAL:
199
+ case AccessControlRequestType.DELETE_CREDENTIAL: {
200
+ const action = args.requestType === AccessControlRequestType.REVOKE_CREDENTIAL
201
+ ? "revoke"
202
+ : args.requestType === AccessControlRequestType.SUSPEND_CREDENTIAL
203
+ ? "suspend"
204
+ : args.requestType === AccessControlRequestType.UNSUSPEND_CREDENTIAL
205
+ ? "unsuspend"
206
+ : "delete";
207
+ if (!args.credentialUuid) {
208
+ return createToolTextContent(`credentialUuid is required for ${args.requestType}.`);
209
+ }
210
+ if (action === "delete" && !args.confirmDelete) {
211
+ return createToolStructuredContent({
212
+ note: `NOT DELETED — nothing was changed. Deleting credential ${args.credentialUuid} destroys the record and cannot be undone. ` +
213
+ `If the badge is only lost or temporarily out of use, 'suspend-credential' is reversible and usually what is wanted. ` +
214
+ `To delete anyway, confirm with the user and call again with confirmDelete: true.`,
215
+ });
216
+ }
217
+ const result = await changeCredentialState(args.credentialUuid, action, requestModifiers, sessionId);
218
+ const notes = {
219
+ revoke: "Revoked the credential: it is detached from its user and will no longer open any door. The credential record still exists and can be reassigned with assign-credential.",
220
+ suspend: "Suspended the credential: it will not open any door until 'unsuspend-credential' is used. It is still assigned to its user.",
221
+ unsuspend: "Un-suspended the credential: it opens doors again, per its user's access grants.",
222
+ delete: "Deleted the credential record permanently. It cannot be reassigned; a replacement badge has to be added and assigned.",
223
+ };
224
+ return createToolStructuredContent({
225
+ credentialChange: { success: result.success, uuid: result.uuid, action },
226
+ warningMsg: result.warningMsg,
227
+ note: notes[action],
228
+ });
229
+ }
230
+ case AccessControlRequestType.UPDATE_CREDENTIAL_NOTE: {
231
+ if (!args.credentialUuid || args.credentialNote === null) {
232
+ return createToolTextContent("credentialUuid and credentialNote are both required for update-credential-note.");
233
+ }
234
+ const result = await updateCredentialNote(args.credentialUuid, args.credentialNote, requestModifiers, sessionId);
235
+ return createToolStructuredContent({
236
+ credentialChange: { success: result.success, uuid: result.uuid, action: "update-note" },
237
+ warningMsg: result.warningMsg,
238
+ note: "Updated the credential's note. This is a label only — it does not change what the credential can open.",
239
+ });
240
+ }
241
+ // ---------------------------------------------------------------------
242
+ // Location access grants
243
+ // ---------------------------------------------------------------------
244
+ case AccessControlRequestType.CREATE_ACCESS_GRANT: {
245
+ if (!args.accessGrantName?.trim()) {
246
+ return createToolTextContent("accessGrantName is required for create-access-grant.");
247
+ }
248
+ if (!args.locationUuid) {
249
+ return createToolTextContent("locationUuid is required for create-access-grant.");
250
+ }
251
+ if (!args.doorUuids?.length) {
252
+ return createToolTextContent("doorUuids is required for create-access-grant and must contain at least one door UUID — a grant with no doors gives nobody access. Get UUIDs from get-entity-tool with entityType ACCESS_CONTROL_DOOR.");
253
+ }
254
+ if (!args.userUuids?.length && !args.groupUuids?.length) {
255
+ return createToolTextContent("create-access-grant needs at least one of userUuids or groupUuids — a grant with no users and no groups gives nobody access.");
256
+ }
257
+ const result = await writeAccessGrant({
258
+ name: args.accessGrantName.trim(),
259
+ locationUuid: args.locationUuid,
260
+ accessControlledDoorUuids: args.doorUuids,
261
+ userUuids: args.userUuids ?? [],
262
+ groupUuids: args.groupUuids ?? [],
263
+ scheduleUuid: args.scheduleUuid ?? undefined,
264
+ }, "create", requestModifiers, sessionId);
265
+ const unlicensed = [
266
+ ...result.expiredACDLicensesDoorUuids,
267
+ ...result.unassignedACDLicensesDoorUuids,
268
+ ];
269
+ return createToolStructuredContent({
270
+ created: { success: result.success, uuid: result.uuid },
271
+ doorsWithoutLicense: unlicensed.length > 0 ? unlicensed : undefined,
272
+ warningMsg: result.warningMsg,
273
+ note: unlicensed.length > 0
274
+ ? `Created the access grant "${args.accessGrantName.trim()}" over ${args.doorUuids.length} door(s), BUT ${unlicensed.length} of them has an expired or unassigned access-control license and will grant nobody access until that is fixed: ${unlicensed.join(", ")}. Tell the user this — the grant looks correct otherwise.`
275
+ : `Created the access grant "${args.accessGrantName.trim()}" over ${args.doorUuids.length} door(s) for ${args.userUuids?.length ?? 0} user(s) and ${args.groupUuids?.length ?? 0} group(s)${args.scheduleUuid ? ", limited to the given schedule" : ", with no schedule limit — it applies at all times"}.`,
276
+ });
277
+ }
278
+ case AccessControlRequestType.UPDATE_ACCESS_GRANT: {
279
+ if (!args.accessGrantUuid) {
280
+ return createToolTextContent("accessGrantUuid is required for update-access-grant.");
281
+ }
282
+ const hasChange = !!args.accessGrantName?.trim() ||
283
+ !!args.doorUuids ||
284
+ !!args.userUuids ||
285
+ !!args.groupUuids ||
286
+ !!args.scheduleUuid;
287
+ if (!hasChange) {
288
+ return createToolTextContent("update-access-grant needs at least one of accessGrantName, doorUuids, userUuids, groupUuids or scheduleUuid — all were empty, so there is nothing to change.");
289
+ }
290
+ // createAccessGrant/updateAccessGrant both take the WHOLE grant, so a
291
+ // partial body would blank the lists the caller did not mention.
292
+ const grants = await getAccessGrants(null, requestModifiers, sessionId);
293
+ const existing = grants.find(grant => grant.uuid === args.accessGrantUuid);
294
+ if (!existing) {
295
+ return createToolTextContent(`No access grant in this organization has the uuid "${args.accessGrantUuid}". Use get-access-grants to see the grants that exist — do not guess a uuid.`);
296
+ }
297
+ const nextDoors = args.doorUuids ?? existing.doorUuids ?? [];
298
+ const nextUsers = args.userUuids ?? existing.userUuids ?? [];
299
+ const nextGroups = args.groupUuids ?? existing.groupUuids ?? [];
300
+ if (nextDoors.length === 0) {
301
+ return createToolTextContent("RETRYABLE — nothing was changed. The update would leave the grant with no doors, which gives nobody access. Pass the full set of doors the grant should cover, or use delete-access-grant.");
302
+ }
303
+ if (nextUsers.length === 0 && nextGroups.length === 0) {
304
+ return createToolTextContent("RETRYABLE — nothing was changed. The update would leave the grant with no users and no groups, which gives nobody access. Pass the full set to keep, or use delete-access-grant.");
305
+ }
306
+ const result = await writeAccessGrant({
307
+ uuid: args.accessGrantUuid,
308
+ name: args.accessGrantName?.trim() || existing.name,
309
+ locationUuid: args.locationUuid ?? existing.locationUuid,
310
+ accessControlledDoorUuids: nextDoors,
311
+ userUuids: nextUsers,
312
+ groupUuids: nextGroups,
313
+ scheduleUuid: args.scheduleUuid ?? existing.scheduleUuid ?? undefined,
314
+ }, "update", requestModifiers, sessionId);
315
+ const removedDoors = (existing.doorUuids ?? []).filter(uuid => !nextDoors.includes(uuid));
316
+ const removedUsers = (existing.userUuids ?? []).filter(uuid => !nextUsers.includes(uuid));
317
+ const unlicensed = [
318
+ ...result.expiredACDLicensesDoorUuids,
319
+ ...result.unassignedACDLicensesDoorUuids,
320
+ ];
321
+ const notes = [
322
+ `Updated the access grant "${args.accessGrantName?.trim() || existing.name || args.accessGrantUuid}".`,
323
+ ];
324
+ if (removedDoors.length > 0) {
325
+ notes.push(`ACCESS REMOVED: ${removedDoors.length} door(s) were dropped from the grant (${removedDoors.join(", ")}) — nobody on this grant can open them any more.`);
326
+ }
327
+ if (removedUsers.length > 0) {
328
+ notes.push(`ACCESS REMOVED: ${removedUsers.length} user(s) were dropped from the grant (${removedUsers.join(", ")}) — they lose the access it provided.`);
329
+ }
330
+ if (unlicensed.length > 0) {
331
+ notes.push(`${unlicensed.length} door(s) on this grant have an expired or unassigned access-control license and grant nobody access until that is fixed: ${unlicensed.join(", ")}.`);
332
+ }
333
+ return createToolStructuredContent({
334
+ updated: { success: result.success, uuid: result.uuid },
335
+ doorsWithoutLicense: unlicensed.length > 0 ? unlicensed : undefined,
336
+ warningMsg: result.warningMsg,
337
+ note: notes.join(" "),
338
+ });
339
+ }
340
+ case AccessControlRequestType.DELETE_ACCESS_GRANT: {
341
+ if (!args.accessGrantUuid) {
342
+ return createToolTextContent("accessGrantUuid is required for delete-access-grant.");
343
+ }
344
+ const grants = await getAccessGrants(null, requestModifiers, sessionId);
345
+ const existing = grants.find(grant => grant.uuid === args.accessGrantUuid);
346
+ if (!existing) {
347
+ return createToolTextContent(`No access grant in this organization has the uuid "${args.accessGrantUuid}". Use get-access-grants to see the grants that exist.`);
348
+ }
349
+ if (!args.confirmDelete) {
350
+ return createToolStructuredContent({
351
+ accessGrants: [existing],
352
+ note: `NOT DELETED — nothing was changed. Deleting the access grant "${existing.name ?? args.accessGrantUuid}" removes door access ` +
353
+ `for ${existing.userUuids?.length ?? 0} directly-assigned user(s) and ${existing.groupUuids?.length ?? 0} group(s) across ` +
354
+ `${existing.doorUuids?.length ?? 0} door(s). Resolve those users and doors to names with user-tool and get-entity-tool, tell the ` +
355
+ `user exactly who loses access to what, then call this tool again with confirmDelete: true.`,
356
+ });
357
+ }
358
+ const result = await deleteAccessGrant(args.accessGrantUuid, requestModifiers, sessionId);
359
+ return createToolStructuredContent({
360
+ deleted: { success: result.success, uuid: result.uuid },
361
+ warningMsg: result.warningMsg,
362
+ note: `Deleted the access grant "${existing.name ?? args.accessGrantUuid}". ${existing.userUuids?.length ?? 0} user(s) and ${existing.groupUuids?.length ?? 0} group(s) no longer have access to its ${existing.doorUuids?.length ?? 0} door(s).`,
363
+ });
364
+ }
365
+ // ---------------------------------------------------------------------
366
+ // Lockdown plans
367
+ // ---------------------------------------------------------------------
368
+ case AccessControlRequestType.RENAME_LOCKDOWN_PLAN: {
369
+ if (!args.lockdownPlanUuid) {
370
+ return createToolTextContent("lockdownPlanUuid is required for rename-lockdown-plan.");
371
+ }
372
+ if (!args.groupName?.trim()) {
373
+ return createToolTextContent("groupName is required for rename-lockdown-plan — it carries the plan's new name.");
374
+ }
375
+ const result = await renameLockdownPlan(args.lockdownPlanUuid, args.groupName.trim(), requestModifiers, sessionId);
376
+ if (result.missing) {
377
+ return createToolTextContent(`No lockdown plan has the uuid "${args.lockdownPlanUuid}". Use get-lockdown-plans to see the plans that exist.`);
378
+ }
379
+ return createToolStructuredContent({
380
+ updated: { success: result.success, uuid: result.uuid },
381
+ note: `Renamed the lockdown plan${result.previousName ? ` "${result.previousName}"` : ""} to "${args.groupName.trim()}". Only the name changed — its door states and activation steps are untouched.`,
382
+ });
383
+ }
384
+ case AccessControlRequestType.DELETE_LOCKDOWN_PLAN: {
385
+ if (!args.lockdownPlanUuid) {
386
+ return createToolTextContent("lockdownPlanUuid is required for delete-lockdown-plan.");
387
+ }
388
+ const plans = await getLockdownPlans(requestModifiers, sessionId);
389
+ const existing = plans.find(plan => plan.uuid === args.lockdownPlanUuid);
390
+ if (!existing) {
391
+ return createToolTextContent(`No lockdown plan has the uuid "${args.lockdownPlanUuid}". Use get-lockdown-plans to see the plans that exist.`);
392
+ }
393
+ if (!args.confirmDelete) {
394
+ return createToolStructuredContent({
395
+ lockdownPlans: [existing],
396
+ note: `NOT DELETED — nothing was changed. "${existing.name ?? args.lockdownPlanUuid}" is an emergency lockdown plan: deleting it ` +
397
+ `removes the ability to lock down this location, and it cannot be recreated through this tool. This is a life-safety ` +
398
+ `configuration — get explicit confirmation from the user, then call again with confirmDelete: true.`,
399
+ });
400
+ }
401
+ const deleted = await deleteLockdownPlan(args.lockdownPlanUuid, requestModifiers, sessionId);
402
+ return createToolStructuredContent({
403
+ deleted,
404
+ note: `Deleted the lockdown plan "${existing.name ?? args.lockdownPlanUuid}". This location can no longer be put into that lockdown. Recreating it requires the Rhombus Console.`,
405
+ });
406
+ }
81
407
  }
82
408
  }
83
409
  catch (error) {
@@ -1,4 +1,4 @@
1
- import { getOrgAlarmStatus, getAlertMonitoringThreatCases, getLocationAlarmStatus, } from "../api/alarm-monitoring-tool-api.js";
1
+ import { getOrgAlarmStatus, getAlertMonitoringThreatCases, getLocationAlarmStatus, setLocationMonitoring, changeThreatCaseState, } from "../api/alarm-monitoring-tool-api.js";
2
2
  import { AlarmMonitoringRequestType, OUTPUT_SCHEMA, TOOL_ARGS, } from "../types/alarm-monitoring-tool-types.js";
3
3
  import { createToolStructuredContent, extractFromToolExtra } from "../util.js";
4
4
  const TOOL_NAME = "alarm-monitoring-tool";
@@ -9,6 +9,12 @@ It has the following modes of operation, determined by the "requestType" paramet
9
9
  - ${AlarmMonitoringRequestType.ORG_STATUS}: Get the armed/disarmed alarm monitoring status for ALL locations in the organization in a single call.
10
10
  - ${AlarmMonitoringRequestType.GET_THREAT_CASES}: Retrieve alarm monitoring threat cases (security incidents). Supports filtering by time range and max results.
11
11
  - ${AlarmMonitoringRequestType.LOCATION_STATUS}: Get detailed alarm monitoring status for a specific location. Requires locationUuid.
12
+ - ${AlarmMonitoringRequestType.ENABLE_MONITORING} / ${AlarmMonitoringRequestType.DISABLE_MONITORING}: Turn alarm monitoring on or off for a location. Requires locationUuid and the location's alarm pin; disabling also requires confirmAction.
13
+ - ${AlarmMonitoringRequestType.DISMISS_THREAT_CASE}: Close a threat case as not requiring action. Requires threatCaseUuid.
14
+ - ${AlarmMonitoringRequestType.CANCEL_THREAT_CASE}: Cancel a threat case so no response is taken. Requires threatCaseUuid.
15
+ - ${AlarmMonitoringRequestType.ESCALATE_THREAT_CASE}: Escalate a threat case to an alarm, triggering a real monitoring response. Requires threatCaseUuid and confirmAction.
16
+
17
+ These write operations affect whether a real site is being watched and whether responders are dispatched. State plainly what will happen and get the user's confirmation first. Arming and disarming on a schedule is not available here — direct the user to the Rhombus Console for that.
12
18
  `;
13
19
  const TOOL_HANDLER = async (args, _extra) => {
14
20
  const { requestModifiers, sessionId } = extractFromToolExtra(_extra);
@@ -31,6 +37,70 @@ const TOOL_HANDLER = async (args, _extra) => {
31
37
  const locationDetail = await getLocationAlarmStatus(args.locationUuid, requestModifiers, sessionId);
32
38
  return createToolStructuredContent({ locationDetail });
33
39
  }
40
+ case AlarmMonitoringRequestType.ENABLE_MONITORING:
41
+ case AlarmMonitoringRequestType.DISABLE_MONITORING: {
42
+ const enabling = args.requestType === AlarmMonitoringRequestType.ENABLE_MONITORING;
43
+ if (!args.locationUuid) {
44
+ return createToolStructuredContent({
45
+ error: `locationUuid is required for ${args.requestType}.`,
46
+ });
47
+ }
48
+ if (!args.pin?.trim()) {
49
+ return createToolStructuredContent({
50
+ error: `pin is required for ${args.requestType} — this is the location's alarm PIN. Ask the user for it; it cannot be looked up.`,
51
+ });
52
+ }
53
+ if (!enabling && !args.confirmAction) {
54
+ return createToolStructuredContent({
55
+ note: "NOTHING WAS CHANGED. Disabling alarm monitoring means this location stops being watched and no threat cases will be raised " +
56
+ "for it. Confirm with the user that this is what they want, then call again with confirmAction: true.",
57
+ });
58
+ }
59
+ const monitoringChange = await setLocationMonitoring(args.locationUuid, args.pin.trim(), enabling, requestModifiers, sessionId);
60
+ return createToolStructuredContent({
61
+ // Deliberately does not echo the PIN.
62
+ monitoringChange: {
63
+ success: monitoringChange.success,
64
+ locationUuid: monitoringChange.locationUuid,
65
+ enabled: monitoringChange.enabled,
66
+ },
67
+ warningMsg: monitoringChange.warningMsg,
68
+ note: enabling
69
+ ? "Alarm monitoring is now ENABLED for this location — threat cases will be raised again."
70
+ : "Alarm monitoring is now DISABLED for this location. It is no longer being watched and no threat cases will be raised until it is re-enabled.",
71
+ });
72
+ }
73
+ case AlarmMonitoringRequestType.DISMISS_THREAT_CASE:
74
+ case AlarmMonitoringRequestType.CANCEL_THREAT_CASE:
75
+ case AlarmMonitoringRequestType.ESCALATE_THREAT_CASE: {
76
+ const action = args.requestType === AlarmMonitoringRequestType.DISMISS_THREAT_CASE
77
+ ? "dismiss"
78
+ : args.requestType === AlarmMonitoringRequestType.CANCEL_THREAT_CASE
79
+ ? "cancel"
80
+ : "escalate";
81
+ if (!args.threatCaseUuid) {
82
+ return createToolStructuredContent({
83
+ error: `threatCaseUuid is required for ${args.requestType}.`,
84
+ });
85
+ }
86
+ if (action === "escalate" && !args.confirmAction) {
87
+ return createToolStructuredContent({
88
+ note: "NOTHING WAS CHANGED. Escalating a threat case to an alarm dispatches a real monitoring response and can result in " +
89
+ "responders being sent. Confirm with the user that they want to escalate, then call again with confirmAction: true.",
90
+ });
91
+ }
92
+ const threatCaseChange = await changeThreatCaseState(args.threatCaseUuid, action, requestModifiers, sessionId);
93
+ const notes = {
94
+ dismiss: "Dismissed the threat case. It is closed as not requiring action and will no longer appear as open.",
95
+ cancel: "Cancelled the threat case. No monitoring response will be taken on it.",
96
+ escalate: "Escalated the threat case to an alarm. A monitoring response has been triggered.",
97
+ };
98
+ return createToolStructuredContent({
99
+ threatCaseChange,
100
+ warningMsg: threatCaseChange.warningMsg,
101
+ note: notes[action],
102
+ });
103
+ }
34
104
  }
35
105
  }
36
106
  catch (error) {
@@ -47,6 +117,6 @@ export function createTool(server) {
47
117
  description: TOOL_DESCRIPTION,
48
118
  inputSchema: TOOL_ARGS,
49
119
  outputSchema: OUTPUT_SCHEMA.shape,
50
- annotations: { readOnlyHint: true },
120
+ annotations: { readOnlyHint: false, destructiveHint: true },
51
121
  }, TOOL_HANDLER);
52
122
  }
@@ -1,7 +1,7 @@
1
1
  import { getCountReportsForDevicesAtLocation, getRunningAverage, getLineCrossingEnabledCameras, getBatchThresholdCrossingCountReport, getSummaryCountReport, triggerScenePrompt, } from "../api/report-tool-api.js";
2
2
  import { AnalyticsRequestType, OUTPUT_SCHEMA, TOOL_ARGS, } from "../types/analytics-tool-types.js";
3
3
  import { createToolStructuredContent, extractFromToolExtra } from "../util.js";
4
- import { postApi } from "../network/network.js";
4
+ import { cachedPostApi } from "../network/org-reference-cache.js";
5
5
  import { DateTime } from "luxon";
6
6
  const TOOL_NAME = "analytics-tool";
7
7
  const TOOL_DESCRIPTION = `
@@ -332,7 +332,7 @@ async function handleLocationSummary(args, requestModifiers, sessionId) {
332
332
  }
333
333
  async function getCameraNameMap(locationUuid, requestModifiers, sessionId) {
334
334
  try {
335
- const res = await postApi({
335
+ const res = await cachedPostApi({
336
336
  route: "/camera/getMinimalCameraStateList",
337
337
  body: {},
338
338
  modifiers: requestModifiers,