rhombus-node-mcp 0.1.26 → 0.1.28

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.
@@ -45,14 +45,15 @@ function buildDateRangeFilter(filter) {
45
45
  localEndDateRangeEnd,
46
46
  };
47
47
  }
48
- async function verifyExceptionConfig(exception, requestModifiers, sessionId) {
48
+ async function verifyExceptionConfig(exception, options, requestModifiers, sessionId) {
49
49
  const verified = {
50
50
  ...exception,
51
51
  };
52
- const cleanedDoorUuids = (Array.isArray(exception.doorUuids)
53
- ? exception.doorUuids.filter((doorUuid) => !!doorUuid)
54
- : []) ?? [];
55
- if (cleanedDoorUuids.length > 0) {
52
+ const doorUuidsProvided = Array.isArray(exception.doorUuids);
53
+ const cleanedDoorUuids = doorUuidsProvided
54
+ ? (exception.doorUuids?.filter((doorUuid) => !!doorUuid) ?? [])
55
+ : undefined;
56
+ if (cleanedDoorUuids !== undefined) {
56
57
  verified.doorUuids = cleanedDoorUuids;
57
58
  }
58
59
  if ((!Array.isArray(exception.intervals) || exception.intervals.length === 0) &&
@@ -67,27 +68,33 @@ async function verifyExceptionConfig(exception, requestModifiers, sessionId) {
67
68
  },
68
69
  ];
69
70
  }
70
- // construct locationToDoorsMap
71
- const locationToDoorsMap = {};
72
- if (cleanedDoorUuids.length > 0) {
73
- const doors = await getAccessControlledDoors(requestModifiers, sessionId);
74
- const doorsMap = await createUuidMap(doors.accessControlledDoors ?? [], "uuid");
75
- for (const doorUuid of cleanedDoorUuids) {
76
- const door = doorsMap.get(doorUuid);
77
- const locationUuid = door?.locationUuid;
78
- if (locationUuid) {
79
- if (!locationToDoorsMap[locationUuid]) {
80
- locationToDoorsMap[locationUuid] = [];
71
+ // construct locationToDoorsMap only when doors are explicitly provided.
72
+ // For update, omitting doorUuids should preserve the existing map.
73
+ if (cleanedDoorUuids !== undefined) {
74
+ const locationToDoorsMap = {};
75
+ if (cleanedDoorUuids.length > 0) {
76
+ const doors = await getAccessControlledDoors(requestModifiers, sessionId);
77
+ const doorsMap = await createUuidMap(doors.accessControlledDoors ?? [], "uuid");
78
+ for (const doorUuid of cleanedDoorUuids) {
79
+ const door = doorsMap.get(doorUuid);
80
+ const locationUuid = door?.locationUuid;
81
+ if (locationUuid) {
82
+ if (!locationToDoorsMap[locationUuid]) {
83
+ locationToDoorsMap[locationUuid] = [];
84
+ }
85
+ locationToDoorsMap[locationUuid].push(doorUuid);
81
86
  }
82
- locationToDoorsMap[locationUuid].push(doorUuid);
83
87
  }
84
88
  }
89
+ verified.locationToDoorsMap = locationToDoorsMap;
90
+ }
91
+ else if (!options.isUpdate) {
92
+ verified.locationToDoorsMap = {};
85
93
  }
86
- verified.locationToDoorsMap = locationToDoorsMap;
87
94
  return verified;
88
95
  }
89
96
  export async function createDoorScheduleException(exception, requestModifiers, sessionId) {
90
- const normalizedException = await verifyExceptionConfig(exception, requestModifiers, sessionId);
97
+ const normalizedException = await verifyExceptionConfig(exception, { isUpdate: false }, requestModifiers, sessionId);
91
98
  const res = await postApi({
92
99
  route: "/accesscontrol/doorScheduleException/createExceptionV2",
93
100
  body: {
@@ -103,7 +110,7 @@ export async function createDoorScheduleException(exception, requestModifiers, s
103
110
  };
104
111
  }
105
112
  export async function updateDoorScheduleException(exception, requestModifiers, sessionId) {
106
- const normalizedException = await verifyExceptionConfig(exception, requestModifiers, sessionId);
113
+ const normalizedException = await verifyExceptionConfig(exception, { isUpdate: true }, requestModifiers, sessionId);
107
114
  const res = await postApi({
108
115
  route: "/accesscontrol/doorScheduleException/updateExceptionV2",
109
116
  body: {
@@ -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,
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,5 +1,5 @@
1
1
  import { createDoorScheduleException, deleteDoorScheduleException, findDoorScheduleExceptions, findDoorScheduleExceptionsForDoor, findDoorScheduleExceptionsForLocation, getDoorScheduleException, updateDoorScheduleException, } from "../api/door-schedule-exception-tool-api.js";
2
- import { DoorScheduleExceptionRequestType, OUTPUT_SCHEMA, TOOL_ARGS, } from "../types/door-schedule-exception-tool-types.js";
2
+ import { CREATE_DOOR_SCHEDULE_EXCEPTION_INPUT_SCHEMA, DoorScheduleExceptionRequestType, OUTPUT_SCHEMA, TOOL_ARGS, UPDATE_DOOR_SCHEDULE_EXCEPTION_INPUT_SCHEMA, } from "../types/door-schedule-exception-tool-types.js";
3
3
  import { createToolStructuredContent, createToolTextContent, extractFromToolExtra, } from "../util.js";
4
4
  const TOOL_NAME = "door-schedule-exception-tool";
5
5
  const TOOL_DESCRIPTION = `
@@ -9,6 +9,10 @@ If a lock/unlock exception is enabled, it will overwrite the existing lock/unloc
9
9
  A schedule exception allows you to create a custom schedule that is only active for the specified dates/times.
10
10
  Once the date/time a schedule exception is set for passes, the original schedule will resume.
11
11
 
12
+ Door schedule exceptions can be either expired or not expired. If its scheduled date is in the past, then it is expired.
13
+ Users through the web console can toggle whether to see expired door schedule exceptions or not. Please mirror this behavior
14
+ when responding to the user.
15
+
12
16
  It has the following modes of operation, determined by the "requestType" parameter:
13
17
  - ${DoorScheduleExceptionRequestType.CREATE_EXCEPTION}: Create a door schedule exception. Requires exception (DoorScheduleExceptionType object). If locationUuid is missing but doorUuids are provided, the tool will resolve the location automatically.
14
18
  - ${DoorScheduleExceptionRequestType.DELETE_EXCEPTION}: Delete a door schedule exception. Requires exceptionUuid.
@@ -38,7 +42,13 @@ const TOOL_HANDLER = async (args, _extra) => {
38
42
  error: "exception is required for create-exception.",
39
43
  }));
40
44
  }
41
- const created = await createDoorScheduleException(args.exception, requestModifiers, sessionId);
45
+ const parsedException = CREATE_DOOR_SCHEDULE_EXCEPTION_INPUT_SCHEMA.safeParse(args.exception);
46
+ if (!parsedException.success) {
47
+ return createToolTextContent(JSON.stringify({
48
+ error: parsedException.error.issues[0]?.message ?? "Invalid exception payload for create-exception.",
49
+ }));
50
+ }
51
+ const created = await createDoorScheduleException(parsedException.data, requestModifiers, sessionId);
42
52
  return createToolStructuredContent(created);
43
53
  }
44
54
  case DoorScheduleExceptionRequestType.DELETE_EXCEPTION: {
@@ -87,7 +97,14 @@ const TOOL_HANDLER = async (args, _extra) => {
87
97
  error: "exception is required for update-exception.",
88
98
  }));
89
99
  }
90
- const updated = await updateDoorScheduleException(args.exception, requestModifiers, sessionId);
100
+ const parsedException = UPDATE_DOOR_SCHEDULE_EXCEPTION_INPUT_SCHEMA.safeParse(args.exception);
101
+ if (!parsedException.success) {
102
+ return createToolTextContent(JSON.stringify({
103
+ error: parsedException.error.issues[0]?.message ??
104
+ "Invalid exception payload for update-exception.",
105
+ }));
106
+ }
107
+ const updated = await updateDoorScheduleException(parsedException.data, requestModifiers, sessionId);
91
108
  return createToolStructuredContent(updated);
92
109
  }
93
110
  }
@@ -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
  });
@@ -11,29 +11,29 @@ export var DoorScheduleExceptionRequestType;
11
11
  DoorScheduleExceptionRequestType["GET_EXCEPTION"] = "get-exception";
12
12
  DoorScheduleExceptionRequestType["UPDATE_EXCEPTION"] = "update-exception";
13
13
  })(DoorScheduleExceptionRequestType || (DoorScheduleExceptionRequestType = {}));
14
- export const DOOR_SCHEDULE_EXCEPTION_INPUT_SCHEMA = z
14
+ const DOOR_INTERVAL_SCHEMA = z.object({
15
+ localEndDateTime: z.string().nullable(),
16
+ localStartDateTime: z.string().nullable(),
17
+ state: z.enum(AccessControlledDoorStateEnumType),
18
+ });
19
+ const DOOR_SCHEDULE_EXCEPTION_BASE_SCHEMA = z
15
20
  .object({
16
- createdAtMillis: z.number().nullable(),
17
- defaultState: z.enum(AccessControlledDoorStateEnumType).nullable(),
18
- description: z.string().nullable(),
21
+ uuid: z.string().nullable().optional(),
22
+ createdAtMillis: z.number().nullable().optional(),
23
+ defaultState: z.enum(AccessControlledDoorStateEnumType).nullable().optional(),
24
+ description: z.string().nullable().optional(),
19
25
  doorUuids: z.array(z.string().nullable()).nullable().optional(),
20
- intervals: z
21
- .array(z.object({
22
- localEndDateTime: z.string().nullable(),
23
- localStartDateTime: z.string().nullable(),
24
- state: z.enum(AccessControlledDoorStateEnumType),
25
- }))
26
- .nullable(),
27
- localEndDate: z.string().nullable(),
28
- localStartDate: z.string().nullable(),
26
+ intervals: z.array(DOOR_INTERVAL_SCHEMA).nullable().optional(),
27
+ localEndDate: z.string().nullable().optional(),
28
+ localStartDate: z.string().nullable().optional(),
29
29
  locationToDoorsMap: z
30
30
  .record(z.string(), z.array(z.string().nullable()).nullable())
31
31
  .nullable()
32
32
  .optional(),
33
- name: z.string().nullable(),
34
- updatedAtMillis: z.number().nullable(),
35
- })
36
- .superRefine((value, ctx) => {
33
+ name: z.string().nullable().optional(),
34
+ updatedAtMillis: z.number().nullable().optional(),
35
+ });
36
+ export const CREATE_DOOR_SCHEDULE_EXCEPTION_INPUT_SCHEMA = DOOR_SCHEDULE_EXCEPTION_BASE_SCHEMA.superRefine((value, ctx) => {
37
37
  if (!value.name) {
38
38
  ctx.addIssue({
39
39
  code: z.ZodIssueCode.custom,
@@ -74,6 +74,36 @@ export const DOOR_SCHEDULE_EXCEPTION_INPUT_SCHEMA = z
74
74
  });
75
75
  }
76
76
  });
77
+ export const UPDATE_DOOR_SCHEDULE_EXCEPTION_INPUT_SCHEMA = DOOR_SCHEDULE_EXCEPTION_BASE_SCHEMA.superRefine((value, ctx) => {
78
+ if (!value.uuid) {
79
+ ctx.addIssue({
80
+ code: z.ZodIssueCode.custom,
81
+ message: "exception.uuid is required for update-exception.",
82
+ path: ["uuid"],
83
+ });
84
+ }
85
+ const hasMutableField = [
86
+ value.name,
87
+ value.description,
88
+ value.defaultState,
89
+ value.localStartDate,
90
+ value.localEndDate,
91
+ value.intervals,
92
+ value.doorUuids,
93
+ value.locationToDoorsMap,
94
+ ].some((field) => field !== undefined);
95
+ if (!hasMutableField) {
96
+ ctx.addIssue({
97
+ code: z.ZodIssueCode.custom,
98
+ message: "exception update must provide at least one mutable field (for example name, description, dates, intervals, doorUuids, or locationToDoorsMap).",
99
+ path: [],
100
+ });
101
+ }
102
+ });
103
+ export const DOOR_SCHEDULE_EXCEPTION_INPUT_SCHEMA = z.union([
104
+ CREATE_DOOR_SCHEDULE_EXCEPTION_INPUT_SCHEMA,
105
+ UPDATE_DOOR_SCHEDULE_EXCEPTION_INPUT_SCHEMA,
106
+ ]);
77
107
  export const TOOL_ARGS = {
78
108
  requestType: z
79
109
  .nativeEnum(DoorScheduleExceptionRequestType)
@@ -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.26",
3
+ "version": "0.1.28",
4
4
  "description": "MCP server for Rhombus API",
5
5
  "keywords": [
6
6
  "ai",