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
@@ -1,4 +1,4 @@
1
- import { getAccessControlEvents, getEventsForEnvironmentalGateway, getClimateEventsForSensor, getComponentEventsByLocation, getHumanMotionEvents, } from "../api/events-tool-api.js";
1
+ import { getAccessControlEvents, getEventsForEnvironmentalGateway, getClimateEventsForSensor, getComponentEventsByLocation, getHumanMotionEvents, getButtonPressEvents, getOccupancyEvents, getProximityEvents, getDoorbellEvents, } from "../api/events-tool-api.js";
2
2
  import { EventsToolRequestType, OUTPUT_SCHEMA, TOOL_ARGS, } from "../types/events-tools-types.js";
3
3
  import { createToolStructuredContent } from "../util.js";
4
4
  import { getLogger } from "../logger.js";
@@ -7,56 +7,59 @@ const logger = getLogger("events-tool");
7
7
  const TOOL_NAME = "events-tool";
8
8
  // "faces" | "people" | "human" | "access-control"
9
9
  const TOOL_DESCRIPTION = `
10
- This tool interacts with the Rhombus events system to retrieve information about various types of events within the system. It has 5 modes of operation, determined by the "eventType" parameter: access-control, environmental-gateway, climate-sensor, component-events, and camera
10
+ **Scope:** This tool returns **raw, event-level data** (individual events with timestamps). Use **report-tool** when you need aggregated counts, time-series summaries, or analytics over intervals.
11
11
 
12
- This tool should should be used any time someone is asking for specifics or reports for access control related events like unlocks, badge ins, credentials, arrivals etc., environmental gateway events, climate sensor events, camera motion events, or any other component events.
12
+ This tool has 5 modes, set by "eventType": access-control, environmental-gateway, climate-sensor, component-events, camera. Use it when the user asks for specific events (unlocks, badge ins, credentials, arrivals, environmental readings, climate data, camera motion, or other component events). It can return large result sets; keep time ranges narrow. For ranges spanning more than ~24 hours, prefer report-tool for aggregates. For maximum flexibility across event types at a location, use eventType "component-events".
13
13
 
14
- For maximum flexibility, use eventType "component-events" which allows querying any combination of event types (doorbell pushes, badge scans, door state changes, button presses, etc.) for a location.
14
+ ---
15
15
 
16
- This tool retrieves a list of events captured by the access control door system pertaining to arrivals, badge ins, credentials received, etc.
16
+ When eventType is "access-control":
17
17
 
18
- This tool can return a lot of data. Please make sure the time range provided is not too large.
19
- This tool takes 3 arguments:
20
- * **accessControlledDoorUuid (string):** The unique identifier for the access controlled door.
21
- * **startTime (string):** The timestamp (in ISO 8601 format) representing the start or earliest time of access control events.
22
- * **endTime (string):** The timestamp (in ISO 8601 format) representing the end or latest time of access control events.
18
+ Retrieves access control events (arrivals, badge ins, credentials, unlocks) for the given door(s). Can return a lot of data—use a narrow time range.
23
19
 
24
- The tool returns a JSON object with access control events data.
20
+ Arguments:
21
+ * **accessControlledDoorUuids (array of strings):** UUIDs of the access-controlled doors.
22
+ * **startTime (string):** Start of the time range (ISO 8601).
23
+ * **endTime (string):** End of the time range (ISO 8601).
25
24
 
26
- When eventType is "environmental-gateway":
25
+ The \`credSource\` field indicates how the event was triggered:
26
+ * **REMOTE:** Rhombus Key app remote unlock.
27
+ * **REMOTE (Admin):** Unlock via Rhombus console or browser/mobile app.
28
+ * **BLE_WAVE:** User waved hand over the reader.
29
+ * **NFC:** User tapped badge or phone on the reader.
27
30
 
28
- This tool retrieves environmental gateway events for a specific environmental gateway device within a time range. The data returned will have a timestamp that is in
29
- the timezone of the **device**, not necessarily UTC time.
31
+ ---
30
32
 
31
- This tool takes 3 arguments:
32
- * **deviceUuid (string):** The unique identifier for the environmental gateway device.
33
- * **startTime (string):** The timestamp (in ISO 8601 format) representing the start time of events.
34
- * **endTime (string):** The timestamp (in ISO 8601 format) representing the end time of events.
33
+ Retrieves environmental gateway events (sensor readings, derived values) for a device in a time range. Timestamps are in the **device** timezone, not necessarily UTC.
35
34
 
36
- The tool returns a JSON object with environmental gateway events data.
35
+ Arguments:
36
+ * **deviceUuid (string):** UUID of the environmental gateway device.
37
+ * **startTime (string):** Start of range (ISO 8601).
38
+ * **endTime (string):** End of range (ISO 8601).
39
+
40
+ ---
37
41
 
38
42
  When eventType is "climate-sensor":
39
43
 
40
- This tool retrieves climate sensor events for a specific climate sensor within a time range. The data returned will have a timestamp that is in
41
- the timezone of the **sensor**, not necessarily UTC time.
44
+ Retrieves climate sensor events (temperature, humidity, air quality, etc.) for a sensor in a time range. Timestamps are in the **sensor** timezone, not necessarily UTC.
42
45
 
43
- This tool takes 4 arguments:
44
- * **sensorUuid (string):** The unique identifier for the climate sensor.
45
- * **startTime (string):** The timestamp (in ISO 8601 format) representing the start time of events.
46
- * **endTime (string):** The timestamp (in ISO 8601 format) representing the end time of events.
47
- * **limit (number, optional):** Maximum number of climate events to return. Default is 1000.
46
+ Arguments:
47
+ * **sensorUuid (string):** UUID of the climate sensor.
48
+ * **startTime (string):** Start of range (ISO 8601).
49
+ * **endTime (string):** End of range (ISO 8601).
50
+ * **limit (number, optional):** Max events to return. Default 1000.
48
51
 
49
- The tool returns a JSON object with climate sensor events data.
52
+ ---
50
53
 
51
54
  When eventType is "component-events":
52
55
 
53
- This tool retrieves ALL types of component events for a specific location within a time range. This is the most flexible option and allows filtering by specific event types. The data returned will have a timestamp that is in the timezone of the **location**, not necessarily UTC time.
56
+ Retrieves all component event types for a location in a time range. Most flexible option; filter by event type via componentEventTypes. Timestamps are in the **location** timezone, not necessarily UTC.
54
57
 
55
- This tool takes 4 arguments:
56
- * **locationUuid (string):** The unique identifier for the location.
57
- * **componentEventTypes (array of strings, optional):** Array of event types to filter by. If empty or not provided, returns all event types.
58
- * **startTime (string):** The timestamp (in ISO 8601 format) representing the start time of events.
59
- * **endTime (string):** The timestamp (in ISO 8601 format) representing the end time of events.
58
+ Arguments:
59
+ * **locationUuid (string):** UUID of the location.
60
+ * **componentEventTypes (array, optional):** Event types to include. If empty or omitted, returns all types.
61
+ * **startTime (string):** Start of range (ISO 8601).
62
+ * **endTime (string):** End of range (ISO 8601).
60
63
 
61
64
  Valid event types include:
62
65
  * **DoorbellEvent:** Doorbell button press events
@@ -71,108 +74,127 @@ Valid event types include:
71
74
  * **WaveToUnlockIntentExpiredEvent:** Wave-to-unlock timeout events
72
75
  * **DoorAuthFirstInStateEvent:** First-in authentication state events
73
76
  * **DoorScheduleFirstInStateEvent:** First-in schedule state events
74
- * And more...
77
+ * And more (see input schema for full list).
78
+
79
+ ---
75
80
 
76
81
  When eventType is "camera":
77
82
 
78
- This tool retrieves human motion events detected by a specific camera within a time range. The data returned will have timestamps in milliseconds.
83
+ Retrieves human motion events for a camera in a time range. Timestamps in milliseconds.
79
84
 
80
- This tool takes 3 arguments:
81
- * **cameraUuid (string):** The unique identifier for the camera.
82
- * **startTime (string):** The timestamp (in ISO 8601 format) representing the start time of events.
83
- * **duration (number):** Duration in seconds to search for human motion events. Default is 3600 (1 hour).
85
+ Arguments:
86
+ * **cameraUuid (string):** UUID of the camera.
87
+ * **startTime (string):** Start of range (ISO 8601).
88
+ * **duration (number):** Search window in seconds. Default 3600 (1 hour).
84
89
 
85
- The tool returns a JSON object with camera events data.
86
90
  `;
87
91
  const TOOL_HANDLER = async (args, extra) => {
88
- const { eventType, accessControlledDoorUuids, deviceUuid, sensorUuid, locationUuid, componentEventTypes, startTime, endTime, limit, timeZone, tempUnit, cameraUuid, duration, } = args;
92
+ const { eventType, accessControlledDoorUuids, deviceUuid, sensorUuid, locationUuid, componentEventTypes, startTime, endTime, limit, timeZone, tempUnit, cameraUuid, duration, buttonSensorUuid, occupancySensorUuid, proximityTagUuids, doorbellCameraUuid, } = args;
89
93
  logger.debug(`eventType: ${eventType}`);
90
94
  switch (eventType) {
91
95
  case "access-control": {
92
96
  if (!accessControlledDoorUuids || accessControlledDoorUuids.length === 0) {
93
- const result = {
97
+ return createToolStructuredContent({
94
98
  needUserInput: true,
95
99
  commandForUser: "Which door are you asking about?",
96
- };
97
- return createToolStructuredContent(result);
100
+ });
98
101
  }
99
102
  else {
100
103
  const events = await getAccessControlEvents(accessControlledDoorUuids, startTime ? new Date(startTime).getTime() : undefined, endTime ? new Date(endTime).getTime() : undefined, timeZone, extra._meta?.requestModifiers, extra.sessionId);
101
- const result = {
102
- eventType: "access-control",
103
- accessControlEvents: events,
104
- };
105
- return createToolStructuredContent(result);
104
+ return createToolStructuredContent({ eventType: "access-control", accessControlEvents: events });
106
105
  }
107
106
  }
108
107
  case "environmental-gateway": {
109
108
  if (!deviceUuid) {
110
- const result = {
109
+ return createToolStructuredContent({
111
110
  needUserInput: true,
112
111
  commandForUser: "Which environmental gateway device are you asking about?",
113
- };
114
- return createToolStructuredContent(result);
112
+ });
115
113
  }
116
114
  else {
117
115
  const events = await getEventsForEnvironmentalGateway(deviceUuid, startTime ? new Date(startTime).getTime() : undefined, endTime ? new Date(endTime).getTime() : undefined, timeZone, tempUnit ?? TempUnit.CELSIUS, extra._meta?.requestModifiers, extra.sessionId);
118
- const result = {
119
- eventType: "environmental-gateway",
120
- environmentalGatewayEvents: events,
121
- };
122
- return createToolStructuredContent(result);
116
+ return createToolStructuredContent({ eventType: "environmental-gateway", environmentalGatewayEvents: events });
123
117
  }
124
118
  }
125
119
  case "climate-sensor": {
126
120
  if (!sensorUuid) {
127
- const result = {
121
+ return createToolStructuredContent({
128
122
  needUserInput: true,
129
123
  commandForUser: "Which climate sensor are you asking about?",
130
- };
131
- return createToolStructuredContent(result);
124
+ });
132
125
  }
133
126
  else {
134
127
  const events = await getClimateEventsForSensor(sensorUuid, startTime ? new Date(startTime).getTime() : undefined, endTime ? new Date(endTime).getTime() : undefined, limit ?? null, timeZone, tempUnit ?? TempUnit.CELSIUS, extra._meta?.requestModifiers, extra.sessionId);
135
- const result = {
136
- eventType: "climate-sensor",
137
- climateSensorEvents: events,
138
- };
139
- return createToolStructuredContent(result);
128
+ return createToolStructuredContent({ eventType: "climate-sensor", climateSensorEvents: events });
140
129
  }
141
130
  }
142
131
  case "component-events": {
143
132
  if (!locationUuid) {
144
- const result = {
133
+ return createToolStructuredContent({
145
134
  needUserInput: true,
146
135
  commandForUser: "Which location are you asking about?",
147
- };
148
- return createToolStructuredContent(result);
136
+ });
149
137
  }
150
138
  else {
151
139
  const events = await getComponentEventsByLocation(locationUuid, componentEventTypes || [], startTime ? new Date(startTime).getTime() : undefined, endTime ? new Date(endTime).getTime() : undefined, timeZone, extra._meta?.requestModifiers, extra.sessionId);
152
- const result = {
153
- eventType: "component-events",
154
- componentEvents: events,
155
- };
156
- return createToolStructuredContent(result);
140
+ return createToolStructuredContent({ eventType: "component-events", componentEvents: events });
157
141
  }
158
142
  }
159
143
  case EventsToolRequestType.CAMERA: {
160
144
  if (!cameraUuid) {
161
- const result = {
145
+ return createToolStructuredContent({
162
146
  needUserInput: true,
163
147
  commandForUser: "Which camera are you asking about?",
164
- };
165
- return createToolStructuredContent(result);
148
+ });
166
149
  }
167
150
  else {
168
- const events = await getHumanMotionEvents(cameraUuid, duration ?? 3600, // Default to 1 hour if not provided
169
- startTime ? new Date(startTime).getTime() : Date.now() - 3600000, // Default to 1 hour ago if not provided
170
- extra._meta?.requestModifiers, extra.sessionId);
151
+ const events = await getHumanMotionEvents(cameraUuid, duration ?? 3600, startTime ? new Date(startTime).getTime() : Date.now() - 3600000, extra._meta?.requestModifiers, extra.sessionId);
152
+ return createToolStructuredContent({ eventType: "camera", cameraEvents: events.uniqueHumanEvents });
153
+ }
154
+ }
155
+ case EventsToolRequestType.BUTTON_PRESS: {
156
+ const bSensorUuid = args.buttonSensorUuid;
157
+ if (!bSensorUuid) {
158
+ return createToolStructuredContent({
159
+ needUserInput: true,
160
+ commandForUser: "Which button sensor are you asking about?",
161
+ });
162
+ }
163
+ const buttonEvents = await getButtonPressEvents(bSensorUuid, startTime ? new Date(startTime).getTime() : undefined, endTime ? new Date(endTime).getTime() : undefined, timeZone, extra._meta?.requestModifiers, extra.sessionId);
164
+ return createToolStructuredContent({ eventType: "button-press", buttonPressEvents: buttonEvents });
165
+ }
166
+ case EventsToolRequestType.OCCUPANCY: {
167
+ const occSensorUuid = args.occupancySensorUuid;
168
+ if (!occSensorUuid) {
171
169
  return createToolStructuredContent({
172
- eventType: "camera",
173
- cameraEvents: events.uniqueHumanEvents,
170
+ needUserInput: true,
171
+ commandForUser: "Which occupancy sensor are you asking about?",
172
+ });
173
+ }
174
+ const occupancyEvts = await getOccupancyEvents(occSensorUuid, startTime ? new Date(startTime).getTime() : undefined, endTime ? new Date(endTime).getTime() : undefined, timeZone, extra._meta?.requestModifiers, extra.sessionId);
175
+ return createToolStructuredContent({ eventType: "occupancy", occupancyEvents: occupancyEvts });
176
+ }
177
+ case EventsToolRequestType.PROXIMITY: {
178
+ const tagUuids = args.proximityTagUuids;
179
+ if (!tagUuids || tagUuids.length === 0) {
180
+ return createToolStructuredContent({
181
+ needUserInput: true,
182
+ commandForUser: "Which proximity tags are you asking about?",
183
+ });
184
+ }
185
+ const proxEvents = await getProximityEvents(tagUuids, startTime ? new Date(startTime).getTime() : undefined, endTime ? new Date(endTime).getTime() : undefined, timeZone, extra._meta?.requestModifiers, extra.sessionId);
186
+ return createToolStructuredContent({ eventType: "proximity", proximityEvents: proxEvents });
187
+ }
188
+ case EventsToolRequestType.DOORBELL: {
189
+ const dbCamUuid = args.doorbellCameraUuid;
190
+ if (!dbCamUuid) {
191
+ return createToolStructuredContent({
192
+ needUserInput: true,
193
+ commandForUser: "Which doorbell camera are you asking about?",
174
194
  });
175
195
  }
196
+ const doorbellEvts = await getDoorbellEvents(dbCamUuid, startTime ? new Date(startTime).getTime() : undefined, endTime ? new Date(endTime).getTime() : undefined, timeZone, extra._meta?.requestModifiers, extra.sessionId);
197
+ return createToolStructuredContent({ eventType: "doorbell", doorbellEvents: doorbellEvts });
176
198
  }
177
199
  }
178
200
  // This should not happen, but return empty result if eventType is unknown
@@ -1,61 +1,204 @@
1
- import { getFaceEvents, getRegisteredFaces } from "../api/faces-tool-api.js";
1
+ import { getFaceEvents, getFaceEventsByPerson, getFaceMatchmakers, getPersonLabels, getRegisteredFaces, searchSimilarFaces } from "../api/faces-tool-api.js";
2
+ import { logger } from "../logger.js";
2
3
  import { OUTPUT_SCHEMA, RequestType, TOOL_ARGS, } from "../types/faces-tools-types.js";
3
- import { extractFromToolExtra } from "../util.js";
4
+ import { createToolStructuredContent, extractFromToolExtra } from "../util.js";
4
5
  const TOOL_NAME = "faces-tool";
5
- // TODO: deviceUuids filter was removed, see TOOL_ARGS for reason
6
+ /**
7
+ * Fuzzy-match user-provided names against the registered faces directory.
8
+ * Scoring: 4 = exact full name, 3 = first name, 2 = last name, 1 = substring (min 3 chars).
9
+ * Returns the best match per queried name.
10
+ */
11
+ function resolveNamesToRegisteredFaces(userNames, registeredPeople) {
12
+ return userNames.map(userName => {
13
+ const input = userName.toLowerCase().trim();
14
+ if (!input)
15
+ return { queriedName: userName, resolvedName: null, resolvedPersonUuid: null };
16
+ let best = null;
17
+ for (const person of registeredPeople) {
18
+ if (!person.name || !person.uuid)
19
+ continue;
20
+ const fullName = person.name.toLowerCase().trim();
21
+ const parts = fullName.split(/\s+/);
22
+ let score = 0;
23
+ if (fullName === input)
24
+ score = 4;
25
+ else if (parts[0] === input)
26
+ score = 3;
27
+ else if (parts.length > 1 && parts[parts.length - 1] === input)
28
+ score = 2;
29
+ else if (input.length >= 3 && fullName.includes(input))
30
+ score = 1;
31
+ if (score > 0 && (!best || score > best.score)) {
32
+ best = { name: person.name, uuid: person.uuid, score };
33
+ }
34
+ }
35
+ return {
36
+ queriedName: userName,
37
+ resolvedName: best?.name ?? null,
38
+ resolvedPersonUuid: best?.uuid ?? null,
39
+ };
40
+ });
41
+ }
6
42
  const TOOL_DESCRIPTION = `
7
- This tool interacts with the Rhombus face recognition system to retrieve information about face sightings and registered faces. It has two primary modes of operation, determined by the "requestType" parameter:
43
+ This tool interacts with the Rhombus face recognition system to retrieve information about face sightings and registered faces.
8
44
 
9
- It should only be used if someone is specifically asking about faces. If they are asking about people or humans, you should use the events-tool with eventType "people" instead as this tool is for face sightings only.
45
+ If the user is asking about how many people were seen (head count / occupancy), use the report-tool with GET_OCCUPANCY_ENABLED_CAMERAS and GET_OCCUPANCY_COUNT_REPORT instead. This tool (faces-tool) is best for identifying *who* was seen (unique individuals by name), and its face count data is also automatically included in report-tool people-counting responses via the faceCountEnrichment field.
46
+
47
+ **Important for person-presence questions:** When asked whether specific people were seen or are present, you should ALSO call events-tool with eventType "access-control" to check badge-in records. Face recognition and access control are complementary — someone may badge in without face recognition triggering, or be seen by a camera without badging in.
10
48
 
11
49
  If the requestType is "get-face-events":
12
- - Use this tool to answer questions about face sightings, including questions like "who was in the office" or "who was seen today". Can be used for reporting, to generate a report on who was seen by the camera system.
13
- - This tool retrieves detailed face events such as detections and recognitions. It provides comprehensive information about each event, including details about the detected face, the person matched (if any), and the event context.
50
+ - Use this tool to answer questions about face sightings, including questions like "who was in the office" or "who was seen today". Can be used for reporting, to generate a report on who was seen by the camera system.
51
+ - **Automatic name resolution:** You can pass partial or first-name-only names in faceNames (e.g., "Brandon", "Omar"). The tool automatically looks up the registered faces directory and resolves them to exact names and person UUIDs before searching. Check the "resolvedNames" field in the response to see what each queried name was matched to (null means no match found).
14
52
  - You can filter face events using parameters like 'faceNames', 'hasEmbedding', 'hasName', 'labels', 'locationUuids', 'personUuids', and a time range using 'rangeStart' and 'rangeEnd' (timestamps in milliseconds).
15
53
  - If you'd like to know about all face events at a location, pass in a location UUID and no device UUIDs. This will correctly return all face events at that location.
54
+ - When the user asks about a specific person at a location (e.g. "Jane Doe at Main Office"), call get-registered-faces first to get the list of registered names, find the best match, then call get-face-events with that precise name. The tool expects precise names as stored in the system.
55
+ - When querying faces at a location, pass only the location UUID in searchFilter; do not pass device UUIDs in searchFilter.deviceUuids, so the API returns all faces detected at that location.
16
56
 
17
57
  If the requestType is "get-registered-faces":
18
- - This tool retrieves a list of all people (registered faces) currently known to the Rhombus system for your organization. This list includes information about each registered person.
19
- - this returns ALL people registered in the system, regardless of the provided timestampFilter.
20
- - This is useful to call before calling "get-face-events" to get a list of all people that have been seen by the camera system, and then you can use personUuids or the name *in the system* to filter face events.
58
+ - This tool retrieves a list of all people (registered faces) currently known to the Rhombus system for your organization. This list includes information about each registered person, including their assigned labels.
59
+ - This returns ALL people registered in the system, regardless of the provided timestampFilter.
60
+ - Each person in the response includes a "labels" array showing which label groups they belong to (e.g., "Engineering", "Visitors"). Use these labels to answer questions about groups of people.
61
+
62
+ If the requestType is "get-person-labels":
63
+ - This retrieves a mapping of all person UUIDs to their assigned labels across the organization.
64
+ - Use this to discover what label groups exist and which registered faces belong to each group.
65
+ - Useful when the user asks about a group (e.g., "was anyone from Engineering seen today?") — get the labels first, find the person UUIDs for that label, then query face events filtered by those personUuids or labels.
21
66
  `;
22
67
  const TOOL_HANDLER = async (args, extra) => {
23
68
  const { requestModifiers, sessionId } = extractFromToolExtra(extra);
24
- let ret = {
25
- requestType: args.requestType,
26
- };
27
69
  if (args.requestType === RequestType.GET_FACE_EVENTS) {
28
- const response = await getFaceEvents(args.faceEventFilter, args.timeZone, requestModifiers, sessionId);
29
- ret = {
70
+ const faceEventArgs = args.faceEventFilter;
71
+ let resolvedNamesOutput;
72
+ const providedNames = faceEventArgs.searchFilter?.faceNames ?? [];
73
+ if (providedNames.length > 0 && faceEventArgs.searchFilter) {
74
+ try {
75
+ const peopleResponse = await getRegisteredFaces({}, requestModifiers, sessionId);
76
+ if (peopleResponse.people) {
77
+ const resolutions = resolveNamesToRegisteredFaces(providedNames, peopleResponse.people);
78
+ resolvedNamesOutput = {};
79
+ const resolvedUuids = [];
80
+ const resolvedExactNames = [];
81
+ const unresolvedNames = [];
82
+ for (const r of resolutions) {
83
+ resolvedNamesOutput[r.queriedName] = r.resolvedName;
84
+ if (r.resolvedPersonUuid && r.resolvedName) {
85
+ resolvedUuids.push(r.resolvedPersonUuid);
86
+ resolvedExactNames.push(r.resolvedName);
87
+ }
88
+ else {
89
+ unresolvedNames.push(r.queriedName);
90
+ }
91
+ }
92
+ if (resolvedUuids.length > 0) {
93
+ const existingUuids = faceEventArgs.searchFilter.personUuids ?? [];
94
+ const mergedUuids = [...new Set([...existingUuids, ...resolvedUuids])];
95
+ faceEventArgs.searchFilter.personUuids = mergedUuids;
96
+ faceEventArgs.searchFilter.faceNames = [...resolvedExactNames, ...unresolvedNames];
97
+ logger.info(`[faces-tool] Auto-resolved names: ${JSON.stringify(resolvedNamesOutput)}`);
98
+ }
99
+ }
100
+ }
101
+ catch (e) {
102
+ logger.warn("[faces-tool] Name resolution failed, proceeding with original args", e);
103
+ }
104
+ }
105
+ let { faceEvents, lastEvaluatedKey } = await getFaceEvents(faceEventArgs, args.timeZone, requestModifiers, sessionId);
106
+ const hadLocationFilter = faceEventArgs.searchFilter?.locationUuids &&
107
+ faceEventArgs.searchFilter.locationUuids.length > 0;
108
+ if (faceEvents.length === 0 && hadLocationFilter) {
109
+ logger.info(`[faces-tool] Empty results with locationUuids filter ${JSON.stringify(faceEventArgs.searchFilter.locationUuids)}, retrying without location filter`);
110
+ const retryArgs = {
111
+ ...faceEventArgs,
112
+ searchFilter: { ...faceEventArgs.searchFilter, locationUuids: [] },
113
+ };
114
+ const retry = await getFaceEvents(retryArgs, args.timeZone, requestModifiers, sessionId);
115
+ faceEvents = retry.faceEvents;
116
+ lastEvaluatedKey = retry.lastEvaluatedKey;
117
+ }
118
+ return createToolStructuredContent({
30
119
  requestType: RequestType.GET_FACE_EVENTS,
31
- getFaceEventsResponse: response,
32
- };
120
+ getFaceEventsResponse: faceEvents,
121
+ lastEvaluatedKey: lastEvaluatedKey ?? undefined,
122
+ resolvedNames: resolvedNamesOutput ?? undefined,
123
+ });
33
124
  }
34
- else if (args.requestType === RequestType.GET_REGISTERED_FACES) {
35
- const response = await getRegisteredFaces(args, requestModifiers, sessionId);
36
- if (response.people) {
37
- ret = {
125
+ if (args.requestType === RequestType.GET_REGISTERED_FACES) {
126
+ const [peopleResponse, labelsResponse] = await Promise.all([
127
+ getRegisteredFaces(args, requestModifiers, sessionId),
128
+ getPersonLabels(requestModifiers, sessionId),
129
+ ]);
130
+ const labelsByPerson = labelsResponse.labelsByPerson ?? {};
131
+ if (peopleResponse.people) {
132
+ return createToolStructuredContent({
38
133
  requestType: RequestType.GET_REGISTERED_FACES,
39
- getSavedFacesResponse: response.people.map(p => ({
134
+ getSavedFacesResponse: peopleResponse.people.map(p => ({
40
135
  createdOn: p.createdOn ? parseInt(p.createdOn, 10) : undefined,
41
136
  name: p.name ?? undefined,
42
137
  orgUuid: p.orgUuid ?? undefined,
43
138
  updatedOn: p.updatedOn ? parseInt(p.updatedOn, 10) : undefined,
44
139
  uuid: p.uuid ?? undefined,
140
+ labels: p.uuid
141
+ ? (labelsByPerson[p.uuid] ?? []).filter((l) => l != null)
142
+ : undefined,
45
143
  })),
46
- };
144
+ });
47
145
  }
48
- else {
49
- ret = {
50
- requestType: RequestType.GET_REGISTERED_FACES,
51
- error: String(response.error),
52
- };
146
+ return createToolStructuredContent({
147
+ requestType: RequestType.GET_REGISTERED_FACES,
148
+ error: String(peopleResponse.error),
149
+ });
150
+ }
151
+ if (args.requestType === RequestType.GET_PERSON_LABELS) {
152
+ const response = await getPersonLabels(requestModifiers, sessionId);
153
+ const cleaned = {};
154
+ if (response.labelsByPerson) {
155
+ for (const [personUuid, labels] of Object.entries(response.labelsByPerson)) {
156
+ if (labels) {
157
+ cleaned[personUuid] = labels.filter((l) => l != null);
158
+ }
159
+ }
160
+ }
161
+ return createToolStructuredContent({
162
+ requestType: RequestType.GET_PERSON_LABELS,
163
+ getPersonLabelsResponse: cleaned,
164
+ });
165
+ }
166
+ if (args.requestType === RequestType.SEARCH_SIMILAR_FACES) {
167
+ if (!args.faceEventUuid) {
168
+ return createToolStructuredContent({
169
+ requestType: RequestType.SEARCH_SIMILAR_FACES,
170
+ error: "faceEventUuid is required for search-similar-faces",
171
+ });
53
172
  }
173
+ const similarEvents = await searchSimilarFaces(args.faceEventUuid, args.timeZone, requestModifiers, sessionId);
174
+ return createToolStructuredContent({
175
+ requestType: RequestType.SEARCH_SIMILAR_FACES,
176
+ similarFaceEvents: similarEvents,
177
+ });
178
+ }
179
+ if (args.requestType === RequestType.GET_FACE_MATCHMAKERS) {
180
+ const matchmakers = await getFaceMatchmakers(requestModifiers, sessionId);
181
+ return createToolStructuredContent({
182
+ requestType: RequestType.GET_FACE_MATCHMAKERS,
183
+ faceMatchmakers: matchmakers,
184
+ });
54
185
  }
55
- return {
56
- content: [{ type: "text", text: JSON.stringify(ret) }],
57
- structuredContent: ret,
58
- };
186
+ if (args.requestType === RequestType.GET_FACE_EVENTS_BY_PERSON) {
187
+ if (!args.personUuid) {
188
+ return createToolStructuredContent({
189
+ requestType: RequestType.GET_FACE_EVENTS_BY_PERSON,
190
+ error: "personUuid is required for get-face-events-by-person",
191
+ });
192
+ }
193
+ const { faceEvents: personEvents } = await getFaceEventsByPerson(args.personUuid, args.timeZone, requestModifiers, sessionId);
194
+ return createToolStructuredContent({
195
+ requestType: RequestType.GET_FACE_EVENTS_BY_PERSON,
196
+ personFaceEvents: personEvents,
197
+ });
198
+ }
199
+ return createToolStructuredContent({
200
+ requestType: args.requestType,
201
+ });
59
202
  };
60
203
  export function createTool(server) {
61
204
  server.registerTool(TOOL_NAME, {
@@ -7,7 +7,12 @@ const TOOL_DESCRIPTION = `
7
7
  Retrieves entities (or devices) of certain types.
8
8
  Can request multiple entity types at once.
9
9
  The return structure is a JSON string that contains the states of the requested entities.
10
- This data is exact. Whatever entities exist will be returned here.`;
10
+ This data is exact. Whatever entities exist will be returned here.
11
+
12
+ This is the primary tool for checking device health and connectivity status. Each device in the response
13
+ includes a "connected" boolean field indicating whether it is currently online (true) or offline (false).
14
+ When asked about device health, offline devices, or connectivity issues, use this tool to fetch all device
15
+ types and check the "connected" field to identify which devices are offline or unreachable.`;
11
16
  const TOOL_HANDLER = async (args, extra) => {
12
17
  const { entityTypes, timeZone, filterBy, tempUnit } = args;
13
18
  const { requestModifiers, sessionId } = extractFromToolExtra(extra);
@@ -0,0 +1,50 @@
1
+ import { getAllGuestsByOrg, getGuestActivityLogs, getActivitiesForLocation, } from "../api/guest-management-tool-api.js";
2
+ import { GuestManagementRequestType, OUTPUT_SCHEMA, TOOL_ARGS, } from "../types/guest-management-tool-types.js";
3
+ import { createToolStructuredContent, extractFromToolExtra } from "../util.js";
4
+ const TOOL_NAME = "guest-management-tool";
5
+ const TOOL_DESCRIPTION = `
6
+ This tool manages Rhombus guest/visitor operations. It can list all guests, retrieve activity logs, and filter activities by location.
7
+
8
+ It has the following modes of operation, determined by the "requestType" parameter:
9
+ - ${GuestManagementRequestType.GET_ALL_GUESTS}: List all guests across the organization with their check-in status, host, company, and contact info.
10
+ - ${GuestManagementRequestType.GET_ACTIVITY_LOGS}: Get org-wide guest activity logs (sign-ins, sign-outs). Supports optional time range filtering via startTimeMs/endTimeMs.
11
+ - ${GuestManagementRequestType.GET_ACTIVITIES_FOR_LOCATION}: Get guest activity logs for a specific location. Requires locationUuid. Supports optional time range filtering.
12
+ `;
13
+ const TOOL_HANDLER = async (args, _extra) => {
14
+ const { requestModifiers, sessionId } = extractFromToolExtra(_extra);
15
+ try {
16
+ switch (args.requestType) {
17
+ case GuestManagementRequestType.GET_ALL_GUESTS: {
18
+ const guests = await getAllGuestsByOrg(requestModifiers, sessionId);
19
+ return createToolStructuredContent({ guests });
20
+ }
21
+ case GuestManagementRequestType.GET_ACTIVITY_LOGS: {
22
+ const activities = await getGuestActivityLogs(args.startTimeMs ?? undefined, args.endTimeMs ?? undefined, requestModifiers, sessionId);
23
+ return createToolStructuredContent({ activities });
24
+ }
25
+ case GuestManagementRequestType.GET_ACTIVITIES_FOR_LOCATION: {
26
+ if (!args.locationUuid) {
27
+ return createToolStructuredContent({
28
+ error: "locationUuid is required for get-activities-for-location.",
29
+ });
30
+ }
31
+ const activities = await getActivitiesForLocation(args.locationUuid, args.startTimeMs ?? undefined, args.endTimeMs ?? undefined, requestModifiers, sessionId);
32
+ return createToolStructuredContent({ activities });
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,8 +1,11 @@
1
- import { getLocations } from "../api/location-tool-api.js";
1
+ import { getLocations, createLocation, updateLocation, getLocationLabels } from "../api/location-tool-api.js";
2
2
  import { TOOL_ARGS } from "../types/location-tool-types.js";
3
3
  const TOOL_NAME = "location-tool";
4
4
  const TOOL_DESCRIPTION = `This tool performs operations on locations.
5
- - 'get': Retrieves all locations. When generating reports with location details, use location names not uuids.`;
5
+ - 'get': Retrieves all locations. When generating reports with location details, use location names not uuids.
6
+ - 'create': Creates a new location with a name and optional address.
7
+ - 'update': Updates an existing location (requires locationUuid).
8
+ - 'get-labels': Retrieves all location labels for the organization.`;
6
9
  const TOOL_HANDLER = async (args, extra) => {
7
10
  const { action } = args;
8
11
  let ret;
@@ -10,8 +13,24 @@ const TOOL_HANDLER = async (args, extra) => {
10
13
  case "get":
11
14
  ret = await getLocations(extra._meta?.requestModifiers, extra.sessionId);
12
15
  break;
13
- case "update":
14
- ret = { error: true, status: "not implemented" };
16
+ case "create": {
17
+ if (!args.locationName) {
18
+ ret = { error: true, status: "locationName is required for 'create'" };
19
+ break;
20
+ }
21
+ ret = await createLocation(args.locationName, args.locationAddress ?? undefined, extra._meta?.requestModifiers, extra.sessionId);
22
+ break;
23
+ }
24
+ case "update": {
25
+ if (!args.locationUuid) {
26
+ ret = { error: true, status: "locationUuid is required for 'update'" };
27
+ break;
28
+ }
29
+ ret = await updateLocation(args.locationUuid, args.locationUpdate?.name, args.locationAddress ?? undefined, extra._meta?.requestModifiers, extra.sessionId);
30
+ break;
31
+ }
32
+ case "get-labels":
33
+ ret = await getLocationLabels(extra._meta?.requestModifiers, extra.sessionId);
15
34
  break;
16
35
  default:
17
36
  ret = { error: true, status: `unsupported location tool call: ${action}` };