rhombus-node-mcp 0.1.21 → 0.1.23

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 (82) hide show
  1. package/README.md +8 -0
  2. package/dist/api/access-control-tool-api.js +256 -0
  3. package/dist/api/alarm-monitoring-tool-api.js +65 -0
  4. package/dist/api/camera-tool-api.js +35 -15
  5. package/dist/api/camera-uptime-tool-api.js +112 -0
  6. package/dist/api/clips-tool-api.js +110 -21
  7. package/dist/api/create-tool-api.js +143 -16
  8. package/dist/api/door-tool-api.js +66 -0
  9. package/dist/api/events-tool-api.js +151 -28
  10. package/dist/api/faces-tool-api.js +118 -91
  11. package/dist/api/get-entity-tool-api.js +2 -0
  12. package/dist/api/guest-management-tool-api.js +75 -0
  13. package/dist/api/location-tool-api.js +48 -1
  14. package/dist/api/lpr-tool-api.js +26 -0
  15. package/dist/api/policy-alerts-tool-api.js +54 -0
  16. package/dist/api/report-tool-api.js +345 -23
  17. package/dist/api/rules-tool-api.js +78 -0
  18. package/dist/api/search-tool-api.js +93 -0
  19. package/dist/api/time-tool-api.js +3 -1
  20. package/dist/api/update-tool-api.js +204 -0
  21. package/dist/api/user-access-trail-tool-api.js +45 -0
  22. package/dist/api/user-audit-tool-api.js +47 -0
  23. package/dist/api/user-tool-api.js +77 -0
  24. package/dist/createServer.js +3 -1
  25. package/dist/filtering-utils.js +298 -0
  26. package/dist/index.js +0 -9
  27. package/dist/logger.js +6 -5
  28. package/dist/network.js +18 -4
  29. package/dist/tools/access-control-tool.js +97 -0
  30. package/dist/tools/alarm-monitoring-tool.js +50 -0
  31. package/dist/tools/analytics-tool.js +383 -0
  32. package/dist/tools/camera-tool.js +27 -9
  33. package/dist/tools/camera-uptime-tool.js +50 -0
  34. package/dist/tools/clips-tool.js +46 -16
  35. package/dist/tools/door-tool.js +66 -0
  36. package/dist/tools/events-tool.js +103 -81
  37. package/dist/tools/faces-tool.js +175 -32
  38. package/dist/tools/get-entity-tool.js +6 -1
  39. package/dist/tools/guest-management-tool.js +50 -0
  40. package/dist/tools/location-tool.js +23 -4
  41. package/dist/tools/lpr-tool.js +26 -20
  42. package/dist/tools/policy-alerts-tool.js +34 -4
  43. package/dist/tools/report-tool.js +183 -46
  44. package/dist/tools/rules-tool.js +76 -0
  45. package/dist/tools/search-tool.js +69 -0
  46. package/dist/tools/update-tool.js +63 -2
  47. package/dist/tools/user-access-trail-tool.js +64 -0
  48. package/dist/tools/user-audit-tool.js +53 -0
  49. package/dist/tools/user-tool.js +55 -0
  50. package/dist/tools/video-walls-tool.js +41 -0
  51. package/dist/transports/streamable-http.js +25 -15
  52. package/dist/types/access-control-tool-types.js +120 -0
  53. package/dist/types/alarm-monitoring-tool-types.js +60 -0
  54. package/dist/types/analytics-tool-types.js +190 -0
  55. package/dist/types/camera-tool-types.js +3 -2
  56. package/dist/types/camera-uptime-tool-types.js +52 -0
  57. package/dist/types/clips-tool-types.js +60 -11
  58. package/dist/types/door-tool-types.js +64 -0
  59. package/dist/types/events-tools-types.js +71 -3
  60. package/dist/types/faces-tools-types.js +36 -0
  61. package/dist/types/guest-management-tool-types.js +57 -0
  62. package/dist/types/location-tool-types.js +10 -1
  63. package/dist/types/lpr-tool-types.js +14 -0
  64. package/dist/types/policy-alerts-tool-types.js +38 -3
  65. package/dist/types/report-tool-types.js +284 -42
  66. package/dist/types/rules-tool-types.js +58 -0
  67. package/dist/types/schema.js +3123 -1
  68. package/dist/types/search-tool-types.js +82 -0
  69. package/dist/types/update-tool-types.js +4 -1
  70. package/dist/types/user-access-trail-tool-types.js +54 -0
  71. package/dist/types/user-audit-tool-types.js +48 -0
  72. package/dist/types/user-tool-types.js +61 -0
  73. package/dist/types/video-walls-tool-types.js +59 -0
  74. package/dist/types/zod-schemas.js +3237 -986
  75. package/dist/types.js +0 -21
  76. package/dist/util.js +34 -57
  77. package/package.json +7 -5
  78. package/dist/disabled-tools/semantic-search-tool.js +0 -90
  79. package/dist/tools/create-tool.js +0 -30
  80. package/dist/types/create-tool-types.js +0 -8
  81. package/dist/types/schema-components.js +0 -7211
  82. package/dist/types/semantic-search-tool-types.js +0 -5
@@ -0,0 +1,383 @@
1
+ import { getCountReportsForDevicesAtLocation, getRunningAverage, getLineCrossingEnabledCameras, getBatchThresholdCrossingCountReport, getSummaryCountReport, triggerScenePrompt, } from "../api/report-tool-api.js";
2
+ import { AnalyticsRequestType, OUTPUT_SCHEMA, TOOL_ARGS, } from "../types/analytics-tool-types.js";
3
+ import { createToolStructuredContent, extractFromToolExtra } from "../util.js";
4
+ import { postApi } from "../network.js";
5
+ import { DateTime } from "luxon";
6
+ const TOOL_NAME = "analytics-tool";
7
+ const TOOL_DESCRIPTION = `
8
+ This tool generates composite operational analytics reports by combining multiple data sources.
9
+ Use it for high-level business questions about space utilization, traffic patterns, and real-time scene analysis.
10
+
11
+ It has the following modes of operation, determined by the "requestType" parameter:
12
+ - ${AnalyticsRequestType.SPACE_UTILIZATION}: Compare per-camera people counts against running averages at a location. Answers: "How busy is each area vs normal?"
13
+ - ${AnalyticsRequestType.TRAFFIC_FLOW}: Compare ingress/egress across ALL line-crossing cameras at a location. Answers: "Which entrance gets the most traffic?"
14
+ - ${AnalyticsRequestType.PEAK_VS_AVERAGE}: Show hourly actual counts vs historical averages. Answers: "When is it busiest? Is today above or below normal?"
15
+ - ${AnalyticsRequestType.SCENE_INTELLIGENCE}: Ask a camera an arbitrary question using AI vision (e.g. "How many treadmills are in use?"). Works in real-time or at a historical timestamp.
16
+ - ${AnalyticsRequestType.LOCATION_SUMMARY}: Generate a comprehensive multi-metric analytics summary for a location including people counts, traffic flow, and trend comparisons.
17
+ `;
18
+ async function handleSpaceUtilization(args, requestModifiers, sessionId) {
19
+ const req = args.spaceUtilizationRequest;
20
+ if (!req)
21
+ return { error: "spaceUtilizationRequest is required." };
22
+ const [deviceDataMap, averageData, camerasRes] = await Promise.all([
23
+ getCountReportsForDevicesAtLocation(req.locationUuid, req.startTimeMs, req.endTimeMs, req.interval, "PEOPLE", requestModifiers, sessionId),
24
+ getRunningAverage(req.locationUuid, req.startTimeMs, req.endTimeMs, req.interval, "LOCATION", undefined, requestModifiers, sessionId).catch(() => []),
25
+ getCameraNameMap(req.locationUuid, requestModifiers, sessionId),
26
+ ]);
27
+ const cameras = [];
28
+ let overallTotal = 0;
29
+ for (const [deviceUuid, dataPoints] of Object.entries(deviceDataMap)) {
30
+ let total = 0;
31
+ for (const dp of dataPoints) {
32
+ const counts = dp.eventCountMap ?? {};
33
+ for (const val of Object.values(counts)) {
34
+ if (typeof val === "number")
35
+ total += val;
36
+ }
37
+ }
38
+ overallTotal += total;
39
+ cameras.push({
40
+ cameraUuid: deviceUuid,
41
+ cameraName: camerasRes[deviceUuid] ?? deviceUuid,
42
+ totalCount: total,
43
+ });
44
+ }
45
+ if (averageData.length > 0) {
46
+ let totalAvg = 0;
47
+ let avgCount = 0;
48
+ for (const dp of averageData) {
49
+ if (dp.stats) {
50
+ for (const stat of Object.values(dp.stats)) {
51
+ if (stat?.avg != null) {
52
+ totalAvg += stat.avg;
53
+ avgCount++;
54
+ }
55
+ }
56
+ }
57
+ }
58
+ const overallAvg = avgCount > 0 ? totalAvg / avgCount : 0;
59
+ for (const cam of cameras) {
60
+ const camAvg = overallAvg > 0 ? (overallAvg * cam.totalCount) / (overallTotal || 1) : undefined;
61
+ cam.averageCount = camAvg != null ? Math.round(camAvg * 100) / 100 : undefined;
62
+ if (cam.averageCount && cam.averageCount > 0) {
63
+ cam.deltaPercent = Math.round(((cam.totalCount - cam.averageCount) / cam.averageCount) * 10000) / 100;
64
+ }
65
+ }
66
+ }
67
+ cameras.sort((a, b) => b.totalCount - a.totalCount);
68
+ return {
69
+ spaceUtilization: {
70
+ locationUuid: req.locationUuid,
71
+ cameras,
72
+ summary: {
73
+ totalPeopleCount: overallTotal,
74
+ busiestCamera: cameras[0]?.cameraName,
75
+ quietestCamera: cameras[cameras.length - 1]?.cameraName,
76
+ },
77
+ },
78
+ };
79
+ }
80
+ async function handleTrafficFlow(args, requestModifiers, sessionId) {
81
+ const req = args.trafficFlowRequest;
82
+ if (!req)
83
+ return { error: "trafficFlowRequest is required." };
84
+ const lcCameras = await getLineCrossingEnabledCameras(req.locationUuid, requestModifiers, sessionId);
85
+ const configMap = lcCameras?.camerasToConfigs ?? {};
86
+ const deviceUuids = Object.keys(configMap);
87
+ if (deviceUuids.length === 0) {
88
+ return { error: "No line-crossing enabled cameras found at this location." };
89
+ }
90
+ const [batchData, cameraNames] = await Promise.all([
91
+ getBatchThresholdCrossingCountReport(deviceUuids, req.startTimeMs, req.endTimeMs, req.bucketSize, req.crossingObject, true, undefined, requestModifiers, sessionId),
92
+ getCameraNameMap(req.locationUuid, requestModifiers, sessionId),
93
+ ]);
94
+ const cameras = [];
95
+ let grandIngress = 0;
96
+ let grandEgress = 0;
97
+ for (const deviceUuid of deviceUuids) {
98
+ const counts = batchData[deviceUuid] ?? [];
99
+ let totalIngress = 0;
100
+ let totalEgress = 0;
101
+ let peakTraffic = 0;
102
+ let peakTimestamp;
103
+ for (const c of counts) {
104
+ const ing = c.ingressCount ?? 0;
105
+ const eg = c.egressCount ?? 0;
106
+ totalIngress += ing;
107
+ totalEgress += eg;
108
+ const total = ing + eg;
109
+ if (total > peakTraffic) {
110
+ peakTraffic = total;
111
+ peakTimestamp = c.timestampMs;
112
+ }
113
+ }
114
+ grandIngress += totalIngress;
115
+ grandEgress += totalEgress;
116
+ cameras.push({
117
+ cameraUuid: deviceUuid,
118
+ cameraName: cameraNames[deviceUuid] ?? deviceUuid,
119
+ totalIngress,
120
+ totalEgress,
121
+ totalTraffic: totalIngress + totalEgress,
122
+ peakHour: peakTimestamp
123
+ ? DateTime.fromMillis(peakTimestamp).toFormat("h:mm a, MMM d")
124
+ : undefined,
125
+ peakHourTraffic: peakTraffic > 0 ? peakTraffic : undefined,
126
+ });
127
+ }
128
+ cameras.sort((a, b) => b.totalTraffic - a.totalTraffic);
129
+ return {
130
+ trafficFlow: {
131
+ locationUuid: req.locationUuid,
132
+ cameras,
133
+ summary: {
134
+ totalIngress: grandIngress,
135
+ totalEgress: grandEgress,
136
+ totalTraffic: grandIngress + grandEgress,
137
+ busiestEntrance: cameras[0]?.cameraName,
138
+ busiestEntranceTraffic: cameras[0]?.totalTraffic,
139
+ },
140
+ },
141
+ };
142
+ }
143
+ async function handlePeakVsAverage(args, requestModifiers, sessionId) {
144
+ const req = args.peakVsAverageRequest;
145
+ if (!req)
146
+ return { error: "peakVsAverageRequest is required." };
147
+ const [actualReport, averageData] = await Promise.all([
148
+ getSummaryCountReport("HOURLY", req.scope, ["PEOPLE"], req.uuid ?? undefined, req.endTimeMs, req.startTimeMs, requestModifiers, sessionId, req.timeZone ?? undefined),
149
+ getRunningAverage(req.uuid ?? undefined, req.startTimeMs, req.endTimeMs, "HOURLY", req.scope, req.timeZone ?? undefined, requestModifiers, sessionId).catch(() => []),
150
+ ]);
151
+ const avgByHour = {};
152
+ for (const dp of averageData) {
153
+ if (dp.date && dp.stats) {
154
+ for (const stat of Object.values(dp.stats)) {
155
+ if (stat?.avg != null) {
156
+ const hourKey = DateTime.fromISO(dp.date).toFormat("h a");
157
+ avgByHour[hourKey] = (avgByHour[hourKey] ?? 0) + stat.avg;
158
+ }
159
+ }
160
+ }
161
+ }
162
+ const hourlyComparison = [];
163
+ const dataPoints = actualReport?.timeSeriesDataPoints ?? [];
164
+ for (const dp of dataPoints) {
165
+ const dateStr = dp.dateUtcString;
166
+ if (!dateStr)
167
+ continue;
168
+ let count = 0;
169
+ if (dp.eventCountMap) {
170
+ for (const val of Object.values(dp.eventCountMap)) {
171
+ if (typeof val === "number")
172
+ count += val;
173
+ }
174
+ }
175
+ const hourLabel = DateTime.fromISO(dateStr).toFormat("h a");
176
+ const avg = avgByHour[hourLabel];
177
+ hourlyComparison.push({
178
+ hour: hourLabel,
179
+ actualCount: count,
180
+ averageCount: avg != null ? Math.round(avg * 100) / 100 : undefined,
181
+ deltaPercent: avg != null && avg > 0
182
+ ? Math.round(((count - avg) / avg) * 10000) / 100
183
+ : undefined,
184
+ });
185
+ }
186
+ let peakHour;
187
+ let peakCount = -1;
188
+ let quietHour;
189
+ let quietCount = Infinity;
190
+ for (const h of hourlyComparison) {
191
+ if (h.actualCount > peakCount) {
192
+ peakCount = h.actualCount;
193
+ peakHour = h.hour;
194
+ }
195
+ if (h.actualCount < quietCount) {
196
+ quietCount = h.actualCount;
197
+ quietHour = h.hour;
198
+ }
199
+ }
200
+ const totalActual = hourlyComparison.reduce((s, h) => s + h.actualCount, 0);
201
+ const totalAvg = hourlyComparison.reduce((s, h) => s + (h.averageCount ?? 0), 0);
202
+ const overallDelta = totalAvg > 0
203
+ ? Math.round(((totalActual - totalAvg) / totalAvg) * 10000) / 100
204
+ : undefined;
205
+ return {
206
+ peakVsAverage: {
207
+ hourlyComparison,
208
+ summary: {
209
+ peakHour,
210
+ peakHourCount: peakCount >= 0 ? peakCount : undefined,
211
+ quietestHour: quietHour,
212
+ quietestHourCount: quietCount < Infinity ? quietCount : undefined,
213
+ overallAverageDelta: overallDelta,
214
+ },
215
+ },
216
+ };
217
+ }
218
+ async function handleSceneIntelligence(args, requestModifiers, sessionId) {
219
+ const req = args.sceneIntelligenceRequest;
220
+ if (!req)
221
+ return { error: "sceneIntelligenceRequest is required." };
222
+ const result = await triggerScenePrompt(req.deviceFacetUuid, req.prompt, req.promptType, req.timestampMs ?? undefined, requestModifiers, sessionId);
223
+ return {
224
+ sceneIntelligence: {
225
+ value: result.value,
226
+ prompt: result.prompt,
227
+ timestampMs: result.timestampMs,
228
+ checkCondition: result.checkCondition,
229
+ },
230
+ };
231
+ }
232
+ async function handleLocationSummary(args, requestModifiers, sessionId) {
233
+ const req = args.locationSummaryRequest;
234
+ if (!req)
235
+ return { error: "locationSummaryRequest is required." };
236
+ const [deviceDataMap, averageData, lcCameras, cameraNames] = await Promise.all([
237
+ getCountReportsForDevicesAtLocation(req.locationUuid, req.startTimeMs, req.endTimeMs, "HOURLY", "PEOPLE", requestModifiers, sessionId).catch(() => ({})),
238
+ getRunningAverage(req.locationUuid, req.startTimeMs, req.endTimeMs, "HOURLY", "LOCATION", undefined, requestModifiers, sessionId).catch(() => []),
239
+ getLineCrossingEnabledCameras(req.locationUuid, requestModifiers, sessionId).catch(() => ({ camerasToConfigs: {} })),
240
+ getCameraNameMap(req.locationUuid, requestModifiers, sessionId),
241
+ ]);
242
+ let totalPeople = 0;
243
+ const hourlyTotals = {};
244
+ for (const [, dataPoints] of Object.entries(deviceDataMap)) {
245
+ for (const dp of dataPoints) {
246
+ const counts = dp.eventCountMap ?? {};
247
+ let dpTotal = 0;
248
+ for (const val of Object.values(counts)) {
249
+ if (typeof val === "number")
250
+ dpTotal += val;
251
+ }
252
+ totalPeople += dpTotal;
253
+ const hour = dp.dateUtcString ? DateTime.fromISO(dp.dateUtcString).toFormat("h a") : "unknown";
254
+ hourlyTotals[hour] = (hourlyTotals[hour] ?? 0) + dpTotal;
255
+ }
256
+ }
257
+ let peakHour;
258
+ let peakCount = -1;
259
+ for (const [hour, count] of Object.entries(hourlyTotals)) {
260
+ if (count > peakCount) {
261
+ peakCount = count;
262
+ peakHour = hour;
263
+ }
264
+ }
265
+ const periodHours = Math.max(1, (req.endTimeMs - req.startTimeMs) / (1000 * 60 * 60));
266
+ const avgPerHour = Math.round((totalPeople / periodHours) * 100) / 100;
267
+ let trafficFlow = undefined;
268
+ const lcDeviceUuids = Object.keys(lcCameras.camerasToConfigs ?? {});
269
+ if (lcDeviceUuids.length > 0) {
270
+ try {
271
+ const batchData = await getBatchThresholdCrossingCountReport(lcDeviceUuids, req.startTimeMs, req.endTimeMs, "HOUR", "HUMAN", true, undefined, requestModifiers, sessionId);
272
+ let totalIngress = 0;
273
+ let totalEgress = 0;
274
+ let busiestEntrance;
275
+ let busiestTraffic = 0;
276
+ for (const deviceUuid of lcDeviceUuids) {
277
+ const counts = batchData[deviceUuid] ?? [];
278
+ let devIngress = 0;
279
+ let devEgress = 0;
280
+ for (const c of counts) {
281
+ devIngress += c.ingressCount ?? 0;
282
+ devEgress += c.egressCount ?? 0;
283
+ }
284
+ totalIngress += devIngress;
285
+ totalEgress += devEgress;
286
+ const devTotal = devIngress + devEgress;
287
+ if (devTotal > busiestTraffic) {
288
+ busiestTraffic = devTotal;
289
+ busiestEntrance = cameraNames[deviceUuid] ?? deviceUuid;
290
+ }
291
+ }
292
+ trafficFlow = { totalIngress, totalEgress, busiestEntrance };
293
+ }
294
+ catch {
295
+ // traffic flow data unavailable
296
+ }
297
+ }
298
+ let totalAvg = 0;
299
+ let avgPoints = 0;
300
+ for (const dp of averageData) {
301
+ if (dp.stats) {
302
+ for (const stat of Object.values(dp.stats)) {
303
+ if (stat?.avg != null) {
304
+ totalAvg += stat.avg;
305
+ avgPoints++;
306
+ }
307
+ }
308
+ }
309
+ }
310
+ const overallAvg = avgPoints > 0 ? totalAvg : 0;
311
+ const overallDelta = overallAvg > 0
312
+ ? Math.round(((totalPeople - overallAvg) / overallAvg) * 10000) / 100
313
+ : undefined;
314
+ return {
315
+ locationSummary: {
316
+ locationUuid: req.locationUuid,
317
+ peopleCounts: {
318
+ totalPeople,
319
+ averagePerHour: avgPerHour,
320
+ peakHour,
321
+ peakCount: peakCount >= 0 ? peakCount : undefined,
322
+ },
323
+ trafficFlow,
324
+ comparison: overallDelta != null
325
+ ? {
326
+ overallDeltaPercent: overallDelta,
327
+ trend: overallDelta > 0 ? "busier" : overallDelta < 0 ? "quieter" : "normal",
328
+ }
329
+ : undefined,
330
+ },
331
+ };
332
+ }
333
+ async function getCameraNameMap(locationUuid, requestModifiers, sessionId) {
334
+ try {
335
+ const res = await postApi({
336
+ route: "/camera/getMinimalCameraStateList",
337
+ body: {},
338
+ modifiers: requestModifiers,
339
+ sessionId,
340
+ });
341
+ const map = {};
342
+ for (const cam of res.cameraStates ?? []) {
343
+ if (cam.locationUuid === locationUuid && cam.uuid && cam.name) {
344
+ map[cam.uuid] = cam.name;
345
+ }
346
+ }
347
+ return map;
348
+ }
349
+ catch {
350
+ return {};
351
+ }
352
+ }
353
+ const TOOL_HANDLER = async (args, _extra) => {
354
+ const { requestModifiers, sessionId } = extractFromToolExtra(_extra);
355
+ try {
356
+ switch (args.requestType) {
357
+ case AnalyticsRequestType.SPACE_UTILIZATION:
358
+ return createToolStructuredContent(await handleSpaceUtilization(args, requestModifiers, sessionId));
359
+ case AnalyticsRequestType.TRAFFIC_FLOW:
360
+ return createToolStructuredContent(await handleTrafficFlow(args, requestModifiers, sessionId));
361
+ case AnalyticsRequestType.PEAK_VS_AVERAGE:
362
+ return createToolStructuredContent(await handlePeakVsAverage(args, requestModifiers, sessionId));
363
+ case AnalyticsRequestType.SCENE_INTELLIGENCE:
364
+ return createToolStructuredContent(await handleSceneIntelligence(args, requestModifiers, sessionId));
365
+ case AnalyticsRequestType.LOCATION_SUMMARY:
366
+ return createToolStructuredContent(await handleLocationSummary(args, requestModifiers, sessionId));
367
+ }
368
+ }
369
+ catch (error) {
370
+ if (error instanceof Error) {
371
+ return createToolStructuredContent({ error: error.message });
372
+ }
373
+ return createToolStructuredContent({ error: "Unknown error" });
374
+ }
375
+ return createToolStructuredContent({ error: "Invalid request type" });
376
+ };
377
+ export function createTool(server) {
378
+ server.registerTool(TOOL_NAME, {
379
+ description: TOOL_DESCRIPTION,
380
+ inputSchema: TOOL_ARGS,
381
+ outputSchema: OUTPUT_SCHEMA.shape,
382
+ }, TOOL_HANDLER);
383
+ }
@@ -1,12 +1,15 @@
1
+ import { getCameraSettings, getImageForCameraAtTime, getCameraMediaUris, getCameraAIThresholds } from "../api/camera-tool-api.js";
1
2
  import { getLogger } from "../logger.js";
2
- import { getCameraSettings, getImageForCameraAtTime } from "../api/camera-tool-api.js";
3
3
  import { BASE_TOOL_ARGS } from "../types/camera-tool-types.js";
4
+ import { extractFromToolExtra } from "../util.js";
4
5
  const TOOL_NAME = "camera-tool";
5
6
  const TOOL_DESCRIPTION = `
6
- This tool can perform some action pertaining to the video stream of a camera. There are two types of requests
7
+ This tool can perform some action pertaining to the video stream of a camera. There are four types of requests
7
8
  that can be passed into "requestType":
8
9
  - image
9
10
  - get-settings
11
+ - get-media-uris
12
+ - get-ai-thresholds
10
13
 
11
14
  What follows is a description of the behavior of this tool given the requestType "image"
12
15
 
@@ -18,7 +21,8 @@ This tool captures and returns a real-time snapshot from a designated security c
18
21
  The image reflects the current scene in the camera's field of view and serves as a contextual
19
22
  input source for downstream tasks such as object recognition, anomaly detection, incident investigation,
20
23
  or situational assessment. When invoked, the tool provides the following:
21
- Visual Scene Capture: A high-resolution image of what the camera is actively observing, including people, vehicles, license plates, and any detectable objects.
24
+ - Visual Scene Capture: A high-resolution image of what the camera is actively observing, including people, vehicles, license plates, and any detectable objects.
25
+ - The frameUri that was used to fetch the image. It may be useful to show the user this image as well through the frameUri.
22
26
 
23
27
  What follows is a description of the behavior of this tool given the requestType "get-settings"
24
28
 
@@ -43,13 +47,13 @@ const TOOL_HANDLER = async (args, extra) => {
43
47
  ],
44
48
  };
45
49
  }
50
+ // biome-ignore lint/suspicious/noExplicitAny: this will be returned, and can be any type since it will be JSON.stringify'd
46
51
  let response;
47
- const timestampMs = timestampISO
48
- ? new Date(timestampISO).getTime()
49
- : new Date().getTime() - 1000 * 60 * 5;
52
+ const timestampMs = timestampISO ? new Date(timestampISO).getTime() : Date.now() - 1000 * 60 * 5;
53
+ const { requestModifiers, sessionId } = extractFromToolExtra(extra);
50
54
  switch (requestType) {
51
55
  case "image":
52
- response = await getImageForCameraAtTime(cameraUuid, timestampMs, extra._meta?.requestModifiers, extra.sessionId);
56
+ response = await getImageForCameraAtTime(cameraUuid, timestampMs, requestModifiers, sessionId);
53
57
  if (!response.success || !response.imageData) {
54
58
  return {
55
59
  content: [{ type: "text", text: JSON.stringify(response) }],
@@ -70,12 +74,23 @@ const TOOL_HANDLER = async (args, extra) => {
70
74
  status: "image-attached",
71
75
  cameraUuid,
72
76
  timestampMs,
77
+ frameUri: response.frameUri,
73
78
  }),
74
79
  },
75
80
  ],
76
81
  };
77
82
  case "get-settings":
78
- response = await getCameraSettings(cameraUuid, extra._meta?.requestModifiers, extra.sessionId);
83
+ response = await getCameraSettings(cameraUuid, requestModifiers, sessionId);
84
+ return {
85
+ content: [{ type: "text", text: JSON.stringify(response) }],
86
+ };
87
+ case "get-media-uris":
88
+ response = await getCameraMediaUris(cameraUuid, requestModifiers, sessionId);
89
+ return {
90
+ content: [{ type: "text", text: JSON.stringify(response) }],
91
+ };
92
+ case "get-ai-thresholds":
93
+ response = await getCameraAIThresholds(cameraUuid, requestModifiers, sessionId);
79
94
  return {
80
95
  content: [{ type: "text", text: JSON.stringify(response) }],
81
96
  };
@@ -96,5 +111,8 @@ const TOOL_HANDLER = async (args, extra) => {
96
111
  };
97
112
  };
98
113
  export function createTool(server) {
99
- server.tool(TOOL_NAME, TOOL_DESCRIPTION, TOOL_ARGS, TOOL_HANDLER);
114
+ server.registerTool(TOOL_NAME, {
115
+ description: TOOL_DESCRIPTION,
116
+ inputSchema: TOOL_ARGS,
117
+ }, TOOL_HANDLER);
100
118
  }
@@ -0,0 +1,50 @@
1
+ import { getCameraUptime, getFleetUptime } from "../api/camera-uptime-tool-api.js";
2
+ import { CameraUptimeRequestType, OUTPUT_SCHEMA, TOOL_ARGS, } from "../types/camera-uptime-tool-types.js";
3
+ import { createToolStructuredContent, extractFromToolExtra } from "../util.js";
4
+ const TOOL_NAME = "camera-uptime-tool";
5
+ const TOOL_DESCRIPTION = `
6
+ This tool analyzes camera uptime and reliability over a time period. It computes uptime percentages, outage counts, and longest outage durations.
7
+
8
+ It has the following modes of operation, determined by the "requestType" parameter:
9
+ - ${CameraUptimeRequestType.GET_CAMERA_UPTIME}: Get uptime statistics for a single camera. Requires cameraUuid, startTimeSec, and endTimeSec.
10
+ - ${CameraUptimeRequestType.GET_FLEET_UPTIME}: Get uptime statistics for ALL cameras in the organization, sorted by worst uptime first. Includes a fleet-wide summary with averages. Requires startTimeSec and endTimeSec.
11
+
12
+ startTimeSec and endTimeSec are UNIX timestamps in seconds.
13
+ `;
14
+ const TOOL_HANDLER = async (args, _extra) => {
15
+ const { requestModifiers, sessionId } = extractFromToolExtra(_extra);
16
+ try {
17
+ switch (args.requestType) {
18
+ case CameraUptimeRequestType.GET_CAMERA_UPTIME: {
19
+ if (!args.cameraUuid) {
20
+ return createToolStructuredContent({
21
+ error: "cameraUuid is required for get-camera-uptime.",
22
+ });
23
+ }
24
+ const cameraUptime = await getCameraUptime(args.cameraUuid, args.startTimeSec, args.endTimeSec, requestModifiers, sessionId);
25
+ return createToolStructuredContent({ cameraUptime });
26
+ }
27
+ case CameraUptimeRequestType.GET_FLEET_UPTIME: {
28
+ const result = await getFleetUptime(args.startTimeSec, args.endTimeSec, requestModifiers, sessionId);
29
+ return createToolStructuredContent({
30
+ fleetUptime: result.cameras,
31
+ fleetSummary: result.summary,
32
+ });
33
+ }
34
+ }
35
+ }
36
+ catch (error) {
37
+ if (error instanceof Error) {
38
+ return createToolStructuredContent({ error: error.message });
39
+ }
40
+ return createToolStructuredContent({ error: "Unknown error" });
41
+ }
42
+ return createToolStructuredContent({ error: "Invalid request type" });
43
+ };
44
+ export function createTool(server) {
45
+ server.registerTool(TOOL_NAME, {
46
+ description: TOOL_DESCRIPTION,
47
+ inputSchema: TOOL_ARGS,
48
+ outputSchema: OUTPUT_SCHEMA.shape,
49
+ }, TOOL_HANDLER);
50
+ }
@@ -1,17 +1,22 @@
1
- import { ApiPayloadSchema, TOOL_ARGS } from "../types/clips-tool-types.js";
2
- import { getSavedClips, getExpiringClips } from "../api/clips-tool-api.js";
1
+ import { createToolStructuredContent, extractFromToolExtra } from "../util.js";
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
4
  const TOOL_NAME = "clips-tool";
4
5
  const TOOL_DESCRIPTION = `
5
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.
6
7
  Clips are either manually saved by the user, or automatically by some defined policy. Therefore, this tool
7
8
  is not for looking up the events that have occured.
8
9
 
9
- This tool allows you to filter clips by:
10
- * Whether or not they are expiring soon.
11
- * Specific devices using their UUIDs.
12
- * Specific locations using their UUIDs.
13
- * A simple string search on clip names.
14
- * A time range, specifying a start (timestampISOAfter) and/or end (timestampISOBefore) timestamp in ISO 8601 format.
10
+ This tool allows you to:
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.
15
20
 
16
21
  The tool returns a JSON object with the following structure and important fields:
17
22
  * **errorMsg (string | null):** An error message if the request failed.
@@ -30,20 +35,45 @@ The tool returns a JSON object with the following structure and important fields
30
35
  * **status (string):** The current processing status of the clip, with possible values such as INITIATING, UPLOADING, RENDERING, FAILED, COMPLETE, OFFLINE, or UNKNOWN.
31
36
  * **userUuid (string | null):** The UUID of the user associated with the clip, if applicable.
32
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.
40
+
33
41
  `;
34
42
  const TOOL_HANDLER = async (args, extra) => {
43
+ const { requestModifiers, sessionId } = extractFromToolExtra(extra);
35
44
  const payload = ApiPayloadSchema.parse(args);
36
- let ret;
37
- switch (args.queryType) {
45
+ switch (args.requestType) {
38
46
  case "saved":
39
- ret = await getSavedClips(payload, extra._meta?.requestModifiers, extra.sessionId);
47
+ return createToolStructuredContent(await getSavedClips(payload, requestModifiers, sessionId));
40
48
  case "expiringSoon":
41
- ret = await getExpiringClips(payload, extra._meta?.requestModifiers, extra.sessionId);
49
+ return createToolStructuredContent(await getExpiringClips(payload, requestModifiers, sessionId));
50
+ case "sharedLiveStreams":
51
+ return createToolStructuredContent(await getSharedLiveStreams(payload, requestModifiers, sessionId));
52
+ case "timelapseClips":
53
+ return createToolStructuredContent(await getTimelapseClips(payload, requestModifiers, sessionId));
54
+ case "clipGroups":
55
+ return createToolStructuredContent(await getClipGroups(requestModifiers, sessionId));
56
+ case "sharedClips":
57
+ return createToolStructuredContent(await getSharedClipGroups(requestModifiers, sessionId));
58
+ case "createClip": {
59
+ if (!args.spliceRequest) {
60
+ throw new Error("spliceRequest is required for 'createClip' requestType");
61
+ }
62
+ return createToolStructuredContent(await createClip(args.spliceRequest.cameraUuid, args.spliceRequest.startTimeMs, args.spliceRequest.endTimeMs, requestModifiers, sessionId));
63
+ }
64
+ case "deleteClip": {
65
+ if (!args.clipUuid) {
66
+ throw new Error("clipUuid is required for 'deleteClip' requestType");
67
+ }
68
+ return createToolStructuredContent(await deleteClip(args.clipUuid, requestModifiers, sessionId));
69
+ }
42
70
  }
43
- return {
44
- content: [{ type: "text", text: JSON.stringify(ret) }],
45
- };
71
+ return createToolStructuredContent({ error: "Invalid requestType" });
46
72
  };
47
73
  export function createTool(server) {
48
- server.tool(TOOL_NAME, TOOL_DESCRIPTION, TOOL_ARGS, TOOL_HANDLER);
74
+ server.registerTool(TOOL_NAME, {
75
+ description: TOOL_DESCRIPTION,
76
+ inputSchema: TOOL_ARGS,
77
+ outputSchema: OUTPUT_SCHEMA.shape,
78
+ }, TOOL_HANDLER);
49
79
  }
@@ -0,0 +1,66 @@
1
+ import { getDoorControllerRules, createDoorControllerRule, getDoorPolicies, createDoorPolicy, } from "../api/door-tool-api.js";
2
+ import { DoorToolRequestType, OUTPUT_SCHEMA, TOOL_ARGS, } from "../types/door-tool-types.js";
3
+ import { createToolStructuredContent, createToolTextContent, extractFromToolExtra, } from "../util.js";
4
+ const TOOL_NAME = "door-tool";
5
+ const TOOL_DESCRIPTION = `
6
+ This tool manages Rhombus door controller rules and door policies.
7
+
8
+ It has the following modes of operation, determined by the "requestType" parameter:
9
+ - ${DoorToolRequestType.GET_DOOR_CONTROLLER_RULES}: Get all rules for a specific door controller. Requires doorControllerUuid.
10
+ - ${DoorToolRequestType.CREATE_DOOR_CONTROLLER_RULE}: Create a new rule for a door controller. Requires doorControllerUuid and ruleConfig (JSON string).
11
+ - ${DoorToolRequestType.GET_DOOR_POLICIES}: Get all door alert policies in the organization.
12
+ - ${DoorToolRequestType.CREATE_DOOR_POLICY}: Create a new door policy. Requires policyName and policyConfig (JSON string).
13
+
14
+ Use the get-entity-tool to find door controller and access controlled door UUIDs.
15
+ `;
16
+ const TOOL_HANDLER = async (args, _extra) => {
17
+ const { requestModifiers, sessionId } = extractFromToolExtra(_extra);
18
+ try {
19
+ switch (args.requestType) {
20
+ case DoorToolRequestType.GET_DOOR_CONTROLLER_RULES: {
21
+ if (!args.doorControllerUuid) {
22
+ return createToolTextContent(JSON.stringify({ error: "doorControllerUuid is required for get-door-controller-rules." }));
23
+ }
24
+ const doorControllerRules = await getDoorControllerRules(args.doorControllerUuid, requestModifiers, sessionId);
25
+ return createToolStructuredContent({ doorControllerRules });
26
+ }
27
+ case DoorToolRequestType.CREATE_DOOR_CONTROLLER_RULE: {
28
+ if (!args.doorControllerUuid || !args.ruleConfig) {
29
+ return createToolTextContent(JSON.stringify({ error: "doorControllerUuid and ruleConfig are required for create-door-controller-rule." }));
30
+ }
31
+ const config = JSON.parse(args.ruleConfig);
32
+ config.deviceUuid = args.doorControllerUuid;
33
+ const createdRule = await createDoorControllerRule(config, requestModifiers, sessionId);
34
+ return createToolStructuredContent({ createdRule });
35
+ }
36
+ case DoorToolRequestType.GET_DOOR_POLICIES: {
37
+ const doorPolicies = await getDoorPolicies(requestModifiers, sessionId);
38
+ return createToolStructuredContent({ doorPolicies });
39
+ }
40
+ case DoorToolRequestType.CREATE_DOOR_POLICY: {
41
+ if (!args.policyConfig) {
42
+ return createToolTextContent(JSON.stringify({ error: "policyConfig is required for create-door-policy." }));
43
+ }
44
+ const config = JSON.parse(args.policyConfig);
45
+ if (args.policyName)
46
+ config.name = args.policyName;
47
+ const createdPolicy = await createDoorPolicy(config, requestModifiers, sessionId);
48
+ return createToolStructuredContent({ createdPolicy });
49
+ }
50
+ }
51
+ }
52
+ catch (error) {
53
+ if (error instanceof Error) {
54
+ return createToolStructuredContent({ error: error.message });
55
+ }
56
+ return createToolStructuredContent({ error: "Unknown error" });
57
+ }
58
+ return createToolStructuredContent({ error: "Invalid request type" });
59
+ };
60
+ export function createTool(server) {
61
+ server.registerTool(TOOL_NAME, {
62
+ description: TOOL_DESCRIPTION,
63
+ inputSchema: TOOL_ARGS,
64
+ outputSchema: OUTPUT_SCHEMA.shape,
65
+ }, TOOL_HANDLER);
66
+ }