rhombus-node-mcp 0.1.27 → 0.1.29

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.
@@ -75,6 +75,71 @@ async function getAccessControlEventsForDoor(doorUuid, startTime, endTime, timeZ
75
75
  }
76
76
  return allEvents;
77
77
  }
78
+ export async function getBrivoAccessControlEvents(startTime, endTime, timeZone, requestModifiers, sessionId) {
79
+ // Step 1: fetch Brivo integration config to get configured doors and their location UUIDs
80
+ const integrationResponse = await postApi({
81
+ route: "/integrations/accessControl/getBrivoIntegrationV2",
82
+ body: {},
83
+ modifiers: requestModifiers,
84
+ sessionId,
85
+ });
86
+ const brivoSettings = integrationResponse.orgIntegrationV2;
87
+ const integrationEnabled = brivoSettings?.enabled ?? false;
88
+ const doorInfoMap = brivoSettings?.doorInfoMap ?? {};
89
+ const brivoDoors = Object.entries(doorInfoMap)
90
+ .filter(([, info]) => info != null)
91
+ .map(([brivoDoornId, info]) => ({
92
+ brivoDoornId,
93
+ doorName: info.doorName ?? undefined,
94
+ locationUuid: info.locationUuid ?? undefined,
95
+ }));
96
+ if (brivoDoors.length === 0) {
97
+ return {
98
+ integrationEnabled,
99
+ brivoDoorsConfigured: 0,
100
+ brivoDoors: [],
101
+ events: [],
102
+ };
103
+ }
104
+ // Step 2: collect unique location UUIDs from configured Brivo doors
105
+ const locationUuids = [...new Set(brivoDoors.map(d => d.locationUuid).filter((id) => !!id))];
106
+ // Step 3: query CredentialReceivedEvents for each location
107
+ const MAX_LIMIT = 1000;
108
+ const allEvents = [];
109
+ await Promise.all(locationUuids.map(async (locationUuid) => {
110
+ const body = {
111
+ locationUuid,
112
+ typeFilter: ["CredentialReceivedEvent"],
113
+ ...(startTime ? { createdAfterMs: startTime } : {}),
114
+ ...(endTime ? { createdBeforeMs: endTime } : {}),
115
+ limit: MAX_LIMIT,
116
+ };
117
+ const response = await postApi({
118
+ route: "/component/findComponentEventsByLocation",
119
+ body,
120
+ modifiers: requestModifiers,
121
+ sessionId,
122
+ });
123
+ const mapped = (response.componentEvents || []).map(event => mapAccessControlEvent(event, timeZone));
124
+ allEvents.push(...mapped);
125
+ }));
126
+ allEvents.sort((a, b) => (b.timestampMs || 0) - (a.timestampMs || 0));
127
+ return {
128
+ integrationEnabled,
129
+ brivoDoorsConfigured: brivoDoors.length,
130
+ brivoDoors,
131
+ events: allEvents.map(e => ({
132
+ authenticationResult: e.authenticationResult ?? undefined,
133
+ authorizationResult: e.authorizationResult ?? undefined,
134
+ doorUuid: e.doorUuid ?? undefined,
135
+ locationUuid: e.locationUuid ?? undefined,
136
+ user: e.user ?? undefined,
137
+ credSource: e.credSource ?? undefined,
138
+ timestampMs: e.timestampMs ?? undefined,
139
+ datetime: e.datetime,
140
+ })),
141
+ };
142
+ }
78
143
  export async function getFaceEvents(_locationUuid, timeZone, requestModifiers, sessionId) {
79
144
  const nowMs = Date.now();
80
145
  const rangeStartMs = nowMs - THREE_HOURS_MS;
@@ -3,23 +3,25 @@ import { postApi } from "../network/network.js";
3
3
  import { formatTimestamp } from "../util.js";
4
4
  import { tempFunc } from "../utils/temp.js";
5
5
  export async function getCameraList(requestModifiers, sessionId) {
6
+ const body = await postApi({
7
+ route: "/camera/getMinimalCameraStateList",
8
+ body: {},
9
+ modifiers: requestModifiers,
10
+ sessionId,
11
+ });
6
12
  return {
7
- cameras: (await postApi({
8
- route: "/camera/getMinimalCameraStateList",
9
- body: {},
10
- modifiers: requestModifiers,
11
- sessionId,
12
- })).cameraStates.filter((camera) => !!camera.locationUuid),
13
+ cameras: (body.cameraStates ?? []).filter((camera) => !!camera.locationUuid),
13
14
  };
14
15
  }
15
16
  export async function getDoorbellCameras(requestModifiers, sessionId) {
17
+ const body = await postApi({
18
+ route: "/doorbellcamera/getMinimalStateList",
19
+ body: {},
20
+ modifiers: requestModifiers,
21
+ sessionId,
22
+ });
16
23
  return {
17
- doorbellCameras: (await postApi({
18
- route: "/doorbellcamera/getMinimalStateList",
19
- body: {},
20
- modifiers: requestModifiers,
21
- sessionId,
22
- })).minimalStates.filter((doorbellCamera) => !!doorbellCamera.locationUuid),
24
+ doorbellCameras: (body.minimalStates ?? []).filter((doorbellCamera) => !!doorbellCamera.locationUuid),
23
25
  };
24
26
  }
25
27
  export async function getBadgeReaders(requestModifiers, sessionId) {
@@ -30,7 +32,7 @@ export async function getBadgeReaders(requestModifiers, sessionId) {
30
32
  sessionId,
31
33
  }).then((response) => {
32
34
  return {
33
- badgeReaders: response.minimalStates.filter((badgeReader) => !!badgeReader.locationUuid),
35
+ badgeReaders: (response.minimalStates ?? []).filter((badgeReader) => !!badgeReader.locationUuid),
34
36
  };
35
37
  });
36
38
  }
@@ -65,7 +67,7 @@ export async function getAudioGateways(timeZone, requestModifiers, sessionId) {
65
67
  sessionId,
66
68
  }).then((response) => {
67
69
  return {
68
- audioGateways: response.audioGatewayStates
70
+ audioGateways: (response.audioGatewayStates ?? [])
69
71
  .filter((camera) => !!camera.locationUuid)
70
72
  .map((gateway) => ({
71
73
  ...gateway,
@@ -84,7 +86,7 @@ export async function getDoorSensors(requestModifiers, sessionId) {
84
86
  sessionId,
85
87
  }).then((response) => {
86
88
  return {
87
- doorStates: response.doorStates.filter((door) => !!door.locationUuid),
89
+ doorStates: (response.doorStates ?? []).filter((door) => !!door.locationUuid),
88
90
  };
89
91
  });
90
92
  }
@@ -97,7 +99,7 @@ export async function getEnvironmentalSensors(timeZone, tempUnit, requestModifie
97
99
  }).then((response) => {
98
100
  logger.info("Using tempUnit: ", tempUnit);
99
101
  return {
100
- climateStates: response.climateStates
102
+ climateStates: (response.climateStates ?? [])
101
103
  .filter((sensor) => !!sensor.locationUuid)
102
104
  .map((_sensor) => {
103
105
  const { temperatureCelcius, ...sensor } = _sensor;
@@ -120,7 +122,7 @@ export async function getMotionSensors(requestModifiers, sessionId) {
120
122
  sessionId,
121
123
  }).then((response) => {
122
124
  return {
123
- occupancySensorStates: response.occupancySensorStates.filter((occupancySensor) => !!occupancySensor.locationUuid),
125
+ occupancySensorStates: (response.occupancySensorStates ?? []).filter((occupancySensor) => !!occupancySensor.locationUuid),
124
126
  };
125
127
  });
126
128
  }
@@ -132,7 +134,7 @@ export async function getButtons(timeZone, requestModifiers, sessionId) {
132
134
  sessionId,
133
135
  }).then((response) => {
134
136
  return {
135
- buttonStates: response.states
137
+ buttonStates: (response.states ?? [])
136
138
  .filter((button) => !!button.locationUuid)
137
139
  .map((button) => ({
138
140
  ...button,
@@ -151,7 +153,7 @@ export async function getKeypads(requestModifiers, sessionId) {
151
153
  sessionId,
152
154
  }).then((response) => {
153
155
  return {
154
- keypadStates: response.keypads.filter((keypad) => !!keypad.locationUuid),
156
+ keypadStates: (response.keypads ?? []).filter((keypad) => !!keypad.locationUuid),
155
157
  };
156
158
  });
157
159
  }
@@ -163,7 +165,7 @@ export async function getEnvironmentalGateways(timeZone, requestModifiers, sessi
163
165
  sessionId,
164
166
  }).then((response) => {
165
167
  return {
166
- minimalEnvironmentalGatewayStates: response.minimalEnvironmentalGatewayStates
168
+ minimalEnvironmentalGatewayStates: (response.minimalEnvironmentalGatewayStates ?? [])
167
169
  .filter((gateway) => !!gateway.locationUuid)
168
170
  .map((gateway) => ({
169
171
  ...gateway,
@@ -29,7 +29,7 @@ export default async function createServer() {
29
29
  version: "1.0.0",
30
30
  }, {
31
31
  capabilities: {
32
- resources: {},
32
+ ...(resources.length > 0 ? { resources: {} } : {}),
33
33
  tools: {},
34
34
  },
35
35
  });
package/dist/logger.js CHANGED
@@ -17,7 +17,7 @@ catch (error) {
17
17
  const appenders = {
18
18
  // stderr for terminal visibility (always available)
19
19
  stderr: { type: "stderr" },
20
- // only send warn+ to stderr (never stdout)
20
+ // production stderr: hide trace/debug (e.g. full response snippets); still shows info+ like [POSTAPI] REQUEST
21
21
  stderrInfo: {
22
22
  type: "logLevelFilter",
23
23
  appender: "stderr",
@@ -36,7 +36,8 @@ if (fileLoggingEnabled) {
36
36
  layout: { type: "basic" },
37
37
  };
38
38
  }
39
- const stderrAppender = process.env.DEBUG === "true" ? "stderr" : "stderrInfo";
39
+ const isProduction = process.env.NODE_ENV === "production";
40
+ const stderrAppender = isProduction ? "stderrInfo" : "stderr";
40
41
  // Configure categories based on available appenders
41
42
  const categories = {
42
43
  default: {
@@ -1,4 +1,4 @@
1
- import { getAccessControlEvents, getEventsForEnvironmentalGateway, getClimateEventsForSensor, getComponentEventsByLocation, getHumanMotionEvents, getButtonPressEvents, getOccupancyEvents, getProximityEvents, getDoorbellEvents, } from "../api/events-tool-api.js";
1
+ import { getAccessControlEvents, getBrivoAccessControlEvents, 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";
@@ -9,7 +9,27 @@ const TOOL_NAME = "events-tool";
9
9
  const TOOL_DESCRIPTION = `
10
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 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".
12
+ This tool has multiple modes, set by "eventType": access-control, brivo-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
+
14
+ ---
15
+
16
+ When eventType is "brivo-access-control":
17
+
18
+ Retrieves badge/credential events from Brivo-integrated doors. Automatically fetches the Brivo integration configuration to determine which locations have Brivo doors mapped. No door UUIDs are required.
19
+
20
+ Use this when the user asks specifically about Brivo events, Brivo badge ins, Brivo access control, or events from Brivo doors.
21
+
22
+ Arguments:
23
+ * **startTime (string):** Start of the time range (ISO 8601).
24
+ * **endTime (string):** End of the time range (ISO 8601).
25
+
26
+ Returns:
27
+ * **integrationEnabled:** Whether the Brivo integration is currently enabled.
28
+ * **brivoDoorsConfigured:** Number of Brivo doors configured in the integration.
29
+ * **brivoDoors:** List of Brivo doors with their IDs, names, and associated Rhombus location UUIDs.
30
+ * **events:** Credential received events from all locations that have Brivo doors configured, sorted newest first.
31
+
32
+ Note: Events are fetched at the location level, so results may include events from all access-controlled doors at locations where Brivo is configured.
13
33
 
14
34
  ---
15
35
 
@@ -92,6 +112,13 @@ const TOOL_HANDLER = async (args, extra) => {
92
112
  const { eventType, accessControlledDoorUuids, deviceUuid, sensorUuid, locationUuid, componentEventTypes, startTime, endTime, limit, timeZone, tempUnit, cameraUuid, duration, buttonSensorUuid, occupancySensorUuid, proximityTagUuids, doorbellCameraUuid, } = args;
93
113
  logger.debug(`eventType: ${eventType}`);
94
114
  switch (eventType) {
115
+ case EventsToolRequestType.BRIVO_ACCESS_CONTROL: {
116
+ const result = await getBrivoAccessControlEvents(startTime ? new Date(startTime).getTime() : undefined, endTime ? new Date(endTime).getTime() : undefined, timeZone, extra._meta?.requestModifiers, extra.sessionId);
117
+ return createToolStructuredContent({
118
+ eventType: "brivo-access-control",
119
+ brivoAccessControlEvents: result,
120
+ });
121
+ }
95
122
  case "access-control": {
96
123
  if (!accessControlledDoorUuids || accessControlledDoorUuids.length === 0) {
97
124
  return createToolStructuredContent({
@@ -14,7 +14,9 @@ includes a "connected" boolean field indicating whether it is currently online (
14
14
  When asked about device health, offline devices, or connectivity issues, use this tool to fetch all device
15
15
  types and check the "connected" field to identify which devices are offline or unreachable.`;
16
16
  const TOOL_HANDLER = async (args, extra) => {
17
- const { entityTypes, timeZone, filterBy, tempUnit } = args;
17
+ const { entityTypes, timeZone, tempUnit } = args;
18
+ const filterBy = args.filterBy ?? { locationUuids: null };
19
+ const locationFilter = filterBy.locationUuids;
18
20
  const { requestModifiers, sessionId } = extractFromToolExtra(extra);
19
21
  const promises = [];
20
22
  if (entityTypes.includes(DeviceType.CAMERA)) {
@@ -61,8 +63,10 @@ const TOOL_HANDLER = async (args, extra) => {
61
63
  // then filter
62
64
  response[key] = value.filter((item) => {
63
65
  let pass = true;
64
- if (item.locationUuid && filterBy.locationUuids && filterBy.locationUuids.length > 0) {
65
- pass = pass && filterBy.locationUuids.includes(item.locationUuid);
66
+ if (item.locationUuid &&
67
+ locationFilter &&
68
+ locationFilter.length > 0) {
69
+ pass = pass && locationFilter.includes(item.locationUuid);
66
70
  }
67
71
  return pass;
68
72
  });
@@ -13,7 +13,7 @@ export var AccessControlRequestType;
13
13
  AccessControlRequestType["GET_REMOTE_UNLOCK_USERS"] = "get-remote-unlock-users";
14
14
  })(AccessControlRequestType || (AccessControlRequestType = {}));
15
15
  export const TOOL_ARGS = {
16
- requestType: z.enum(AccessControlRequestType).describe("The type of access control request to make."),
16
+ requestType: z.nativeEnum(AccessControlRequestType).describe("The type of access control request to make."),
17
17
  doorUuid: z
18
18
  .string()
19
19
  .nullable()
@@ -6,6 +6,7 @@ import { TempUnit } from "../utils/temp.js";
6
6
  export var EventsToolRequestType;
7
7
  (function (EventsToolRequestType) {
8
8
  EventsToolRequestType["ACCESS_CONTROL"] = "access-control";
9
+ EventsToolRequestType["BRIVO_ACCESS_CONTROL"] = "brivo-access-control";
9
10
  EventsToolRequestType["ENVIRONMENTAL_GATEWAY"] = "environmental-gateway";
10
11
  EventsToolRequestType["CLIMATE_SENSOR"] = "climate-sensor";
11
12
  EventsToolRequestType["COMPONENT_EVENTS"] = "component-events";
@@ -20,6 +21,7 @@ export const TOOL_ARGS = {
20
21
  .nativeEnum(EventsToolRequestType)
21
22
  .describe("The type of events to retrieve. " +
22
23
  "access-control: Access control events like unlocks, badge ins, credentials, arrivals. " +
24
+ "brivo-access-control: Badge/credential events from Brivo-integrated doors. Does not require door UUIDs — automatically looks up which doors are configured via the Brivo integration. " +
23
25
  "environmental-gateway: Environmental gateway events with sensor readings and derived values. " +
24
26
  "climate-sensor: Climate sensor events with temperature, humidity, air quality readings. " +
25
27
  "component-events: All types of component events for a location (most flexible option). " +
@@ -143,6 +145,7 @@ export const OUTPUT_SCHEMA = z.object({
143
145
  eventType: z
144
146
  .enum([
145
147
  "access-control",
148
+ "brivo-access-control",
146
149
  "environmental-gateway",
147
150
  "climate-sensor",
148
151
  "component-events",
@@ -153,6 +156,52 @@ export const OUTPUT_SCHEMA = z.object({
153
156
  "doorbell",
154
157
  ])
155
158
  .optional(),
159
+ brivoAccessControlEvents: z.optional(z
160
+ .object({
161
+ integrationEnabled: z.boolean().describe("Whether the Brivo integration is enabled"),
162
+ brivoDoorsConfigured: z
163
+ .number()
164
+ .describe("Number of Brivo doors configured in the integration"),
165
+ brivoDoors: z
166
+ .array(z.object({
167
+ brivoDoornId: z.string().describe("Brivo's door ID"),
168
+ doorName: z.string().optional().describe("Brivo door name"),
169
+ locationUuid: z.string().optional().describe("Rhombus location UUID for this door"),
170
+ }))
171
+ .describe("List of Brivo doors configured in the integration"),
172
+ events: z
173
+ .array(z.object({
174
+ authenticationResult: z
175
+ .string()
176
+ .optional()
177
+ .describe("The result of the authentication process"),
178
+ authorizationResult: z
179
+ .string()
180
+ .optional()
181
+ .describe("The result of the authorization process"),
182
+ doorUuid: z
183
+ .string()
184
+ .optional()
185
+ .describe("The Rhombus access controlled door UUID"),
186
+ locationUuid: z
187
+ .string()
188
+ .optional()
189
+ .describe("The Rhombus location UUID where the event occurred"),
190
+ user: z.string().optional().describe("Username of the person who triggered the event"),
191
+ credSource: z
192
+ .string()
193
+ .optional()
194
+ .describe("The source of the credential (e.g. WIEGAND, NFC, BLE_WAVE, REMOTE)"),
195
+ timestampMs: z
196
+ .number()
197
+ .optional()
198
+ .describe("Timestamp in milliseconds when the event occurred"),
199
+ datetime: z.string().optional().describe("Formatted datetime string of the event"),
200
+ }))
201
+ .describe("Credential received events from locations that have Brivo doors configured, sorted by timestamp (newest first)"),
202
+ })
203
+ .nullable()
204
+ .describe("Brivo access control events. Fetches credential events from all locations that have Brivo doors configured in the integration.")),
156
205
  accessControlEvents: z.optional(z
157
206
  .array(z.object({
158
207
  authenticationResult: z
@@ -2,18 +2,21 @@ import { z } from "zod";
2
2
  import { createUuidSchema } from "../types.js";
3
3
  import DeviceType from "./deviceType.js";
4
4
  import { TempUnit } from "../utils/temp.js";
5
+ const filterByObjectSchema = z.object({
6
+ locationUuids: z
7
+ .array(createUuidSchema())
8
+ .nullish()
9
+ .describe("The UUIDs of the locations to filter by. Set to null or an empty array to not filter by location."),
10
+ });
5
11
  export const TOOL_ARGS = {
6
12
  entityTypes: z
7
13
  .array(z.nativeEnum(DeviceType).describe("The entity type to retreive"))
8
14
  .describe("What type of entities to retrieve."),
9
15
  filterBy: z
10
- .object({
11
- locationUuids: z
12
- .array(createUuidSchema())
13
- .nullable()
14
- .describe("The UUIDs of the locations to filter by. Set to null or an enmpty array to not filter by location."),
15
- })
16
- .describe("Additional filters that can be applied to the result."),
16
+ .union([filterByObjectSchema, z.null()])
17
+ .optional()
18
+ .transform((v) => v ?? { locationUuids: null })
19
+ .describe("Additional filters that can be applied to the result. Omit or pass null for no filtering."),
17
20
  timeZone: z
18
21
  .string()
19
22
  .describe("The timezone for formatting timestamps. This is necessary for the tool to produce accurate formatted timestamps."),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rhombus-node-mcp",
3
- "version": "0.1.27",
3
+ "version": "0.1.29",
4
4
  "description": "MCP server for Rhombus API",
5
5
  "keywords": [
6
6
  "ai",