rhombus-node-mcp 0.1.56 → 0.1.57

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/dist/api/access-control-tool-api.js +231 -32
  2. package/dist/api/alarm-monitoring-tool-api.js +50 -1
  3. package/dist/api/automated-prompts-tool-api.js +9 -17
  4. package/dist/api/camera-policy-tool-api.js +122 -0
  5. package/dist/api/camera-tool-api.js +3 -5
  6. package/dist/api/camera-uptime-tool-api.js +62 -25
  7. package/dist/api/clips-tool-api.js +45 -9
  8. package/dist/api/create-tool-api.js +55 -7
  9. package/dist/api/door-tool-api.js +74 -13
  10. package/dist/api/entity-lookup-tool-api.js +8 -1
  11. package/dist/api/events-tool-api.js +79 -6
  12. package/dist/api/faces-tool-api.js +112 -7
  13. package/dist/api/get-accessible-apps.js +68 -6
  14. package/dist/api/get-entity-tool-api.js +12 -12
  15. package/dist/api/get-org-information-tool-api.js +2 -2
  16. package/dist/api/guest-management-tool-api.js +75 -1
  17. package/dist/api/location-tool-api.js +120 -28
  18. package/dist/api/lpr-tool-api.js +71 -16
  19. package/dist/api/policy-alerts-tool-api.js +7 -3
  20. package/dist/api/report-tool-api.js +4 -1
  21. package/dist/api/rules-tool-api.js +6 -16
  22. package/dist/api/schedule-tool-api.js +107 -0
  23. package/dist/api/search-tool-api.js +5 -13
  24. package/dist/api/update-tool-api.js +96 -1
  25. package/dist/api/user-tool-api.js +58 -13
  26. package/dist/createServer.js +18 -6
  27. package/dist/filtering-utils.js +421 -29
  28. package/dist/index.js +1 -1
  29. package/dist/network/network.js +122 -10
  30. package/dist/network/org-reference-cache.js +74 -0
  31. package/dist/telemetry/tracingProxy.js +4 -0
  32. package/dist/tools/entity-lookup-tool.js +6 -4
  33. package/dist/tools/get-entity-tool.js +40 -20
  34. package/dist/tools/get-org-information-tool.js +30 -2
  35. package/dist/tools/time-conversion-tool.js +59 -36
  36. package/dist/tools/time-tool.js +12 -3
  37. package/dist/tools/user-tool.js +100 -2
  38. package/dist/tools-console/access-anomaly-tool.js +2 -19
  39. package/dist/tools-console/access-control-tool.js +328 -2
  40. package/dist/tools-console/alarm-monitoring-tool.js +72 -2
  41. package/dist/tools-console/analytics-tool.js +2 -2
  42. package/dist/tools-console/badge-timeline-tool.js +2 -21
  43. package/dist/tools-console/camera-policy-tool.js +252 -0
  44. package/dist/tools-console/camera-tool.js +111 -89
  45. package/dist/tools-console/clips-tool.js +21 -31
  46. package/dist/tools-console/count-tool.js +8 -0
  47. package/dist/tools-console/create-camera-policy-tool.js +302 -122
  48. package/dist/tools-console/door-schedule-exception-tool.js +2 -33
  49. package/dist/tools-console/door-tool.js +130 -2
  50. package/dist/tools-console/elements-access-anomaly-tool.js +2 -19
  51. package/dist/tools-console/elements-badge-timeline-tool.js +2 -21
  52. package/dist/tools-console/elements-lost-badge-tool.js +2 -17
  53. package/dist/tools-console/elements-tool.js +2 -29
  54. package/dist/tools-console/events-tool.js +22 -101
  55. package/dist/tools-console/faces-tool.js +254 -28
  56. package/dist/tools-console/guest-management-tool.js +127 -2
  57. package/dist/tools-console/location-tool.js +156 -35
  58. package/dist/tools-console/lost-badge-tool.js +2 -17
  59. package/dist/tools-console/lpr-tool.js +89 -23
  60. package/dist/tools-console/netbox-access-anomaly-tool.js +2 -19
  61. package/dist/tools-console/netbox-badge-timeline-tool.js +2 -21
  62. package/dist/tools-console/netbox-lost-badge-tool.js +2 -17
  63. package/dist/tools-console/netbox-tool.js +2 -29
  64. package/dist/tools-console/onguard-tool.js +2 -30
  65. package/dist/tools-console/person-tracking-tool.js +2 -2
  66. package/dist/tools-console/policy-alerts-tool.js +5 -29
  67. package/dist/tools-console/reboot-cameras-tool.js +17 -2
  68. package/dist/tools-console/report-tool.js +10 -40
  69. package/dist/tools-console/schedule-tool.js +190 -0
  70. package/dist/tools-console/update-tool.js +298 -168
  71. package/dist/tools-console/video-walls-tool.js +93 -21
  72. package/dist/types/access-anomaly-tool-types.js +22 -2
  73. package/dist/types/access-control-tool-types.js +116 -1
  74. package/dist/types/alarm-monitoring-tool-types.js +42 -1
  75. package/dist/types/badge-timeline-tool-types.js +18 -3
  76. package/dist/types/camera-policy-tool-types.js +104 -0
  77. package/dist/types/camera-tool-types.js +80 -1
  78. package/dist/types/clips-tool-types.js +27 -3
  79. package/dist/types/create-camera-policy-tool-types.js +275 -26
  80. package/dist/types/door-schedule-exception-tool-types.js +20 -6
  81. package/dist/types/door-tool-types.js +36 -4
  82. package/dist/types/entity-lookup-tool-types.js +6 -1
  83. package/dist/types/events-tools-types.js +24 -16
  84. package/dist/types/faces-tools-types.js +106 -3
  85. package/dist/types/get-entity-tool-types.js +76 -12
  86. package/dist/types/guest-management-tool-types.js +103 -1
  87. package/dist/types/location-tool-types.js +100 -14
  88. package/dist/types/lost-badge-tool-types.js +17 -2
  89. package/dist/types/lpr-tool-types.js +47 -8
  90. package/dist/types/onguard-tool-types.js +21 -1
  91. package/dist/types/policy-alerts-tool-types.js +1 -1
  92. package/dist/types/report-tool-types.js +29 -1
  93. package/dist/types/schedule-tool-types.js +370 -0
  94. package/dist/types/schema.js +479 -9
  95. package/dist/types/time-conversion-tool-types.js +3 -4
  96. package/dist/types/update-tool-types.js +40 -18
  97. package/dist/types/user-tool-types.js +44 -1
  98. package/dist/types/video-walls-tool-types.js +30 -2
  99. package/dist/types/zod-schemas.js +5128 -996
  100. package/dist/util.js +62 -3
  101. package/dist/utils/entity-name-match.js +11 -0
  102. package/package.json +3 -1
@@ -1,4 +1,4 @@
1
- import { postApi } from "../network/network.js";
1
+ import { postApi, throwIfApiError } from "../network/network.js";
2
2
  import { formatTimestamp } from "../util.js";
3
3
  export async function getVehicleEvents(args, timeZone, requestModifiers, sessionId) {
4
4
  const requestArgs = {
@@ -15,9 +15,7 @@ export async function getVehicleEvents(args, timeZone, requestModifiers, session
15
15
  modifiers: requestModifiers,
16
16
  sessionId,
17
17
  });
18
- if (res.error) {
19
- throw new Error(JSON.stringify(res));
20
- }
18
+ throwIfApiError(res);
21
19
  return (res.events?.map(event => ({
22
20
  uuid: event.uuid ?? "",
23
21
  deviceUuid: event.deviceUuid ?? undefined,
@@ -35,9 +33,7 @@ export async function getSavedVehicles(timeZone, requestModifiers, sessionId) {
35
33
  modifiers: requestModifiers,
36
34
  sessionId,
37
35
  });
38
- if (res.error) {
39
- throw new Error(JSON.stringify(res));
40
- }
36
+ throwIfApiError(res);
41
37
  return (res.vehicles?.map(vehicle => ({
42
38
  createdTimestamp: formatTimestamp(vehicle.createdAtMillis ?? Date.now(), timeZone),
43
39
  name: vehicle.name ?? "No Name",
@@ -53,9 +49,7 @@ export async function getVehicleLabels(requestModifiers, sessionId) {
53
49
  modifiers: requestModifiers,
54
50
  sessionId,
55
51
  });
56
- if (res.error) {
57
- throw new Error(JSON.stringify(res));
58
- }
52
+ throwIfApiError(res);
59
53
  const processedVehicleLabels = {};
60
54
  for (const [licensePlate, labels] of Object.entries(res.vehicleLabels ?? {})) {
61
55
  const filteredLabels = labels?.filter(label => label !== null) ?? [];
@@ -73,22 +67,83 @@ export async function searchLicensePlates(query, timeZone, requestModifiers, ses
73
67
  modifiers: requestModifiers,
74
68
  sessionId,
75
69
  });
76
- if (res.error)
77
- throw new Error(JSON.stringify(res));
70
+ throwIfApiError(res);
78
71
  return (res.licensePlateEvents || []).map((event) => ({
79
72
  licensePlate: event.vehicleLicensePlate ?? undefined,
80
73
  deviceUuid: event.deviceUuid ?? undefined,
81
74
  timestampMs: event.eventTimestamp ?? undefined,
82
75
  }));
83
76
  }
77
+ /**
78
+ * Save (or overwrite) a vehicle. The plate is the key, so calling this for a
79
+ * plate that already exists replaces its name and description.
80
+ *
81
+ * NOTE the field-name asymmetry, which this used to get wrong: the REQUEST
82
+ * field is `vehicleLicensePlate` (`Vehicle_SaveVehicleWSRequest`) while the
83
+ * READ field is `licensePlate` (`VehicleV2Type`). Sending `licensePlate` here
84
+ * silently saved a vehicle with no plate attached.
85
+ */
84
86
  export async function saveVehicle(name, licensePlate, description, requestModifiers, sessionId) {
85
87
  const res = await postApi({
86
88
  route: "/vehicle/saveVehicle",
87
- body: { name, licensePlate, description: description || "" },
89
+ body: {
90
+ name,
91
+ vehicleLicensePlate: licensePlate,
92
+ description: description || "",
93
+ },
94
+ modifiers: requestModifiers,
95
+ sessionId,
96
+ });
97
+ throwIfApiError(res);
98
+ return { success: true, licensePlate, warningMsg: res.warningMsg ?? undefined };
99
+ }
100
+ export async function deleteVehicle(licensePlate, requestModifiers, sessionId) {
101
+ const res = await postApi({
102
+ route: "/vehicle/deleteVehicle",
103
+ body: {
104
+ vehicleLicensePlate: licensePlate,
105
+ },
106
+ modifiers: requestModifiers,
107
+ sessionId,
108
+ });
109
+ throwIfApiError(res);
110
+ return { success: true, licensePlate, warningMsg: res.warningMsg ?? undefined };
111
+ }
112
+ export async function addVehicleLabel(licensePlate, label, requestModifiers, sessionId) {
113
+ const res = await postApi({
114
+ route: "/vehicle/addVehicleLabel",
115
+ body: {
116
+ vehicleLicensePlate: licensePlate,
117
+ label,
118
+ },
119
+ modifiers: requestModifiers,
120
+ sessionId,
121
+ });
122
+ throwIfApiError(res);
123
+ return { success: true, licensePlate, label, warningMsg: res.warningMsg ?? undefined };
124
+ }
125
+ export async function removeVehicleLabel(licensePlate, label, requestModifiers, sessionId) {
126
+ const res = await postApi({
127
+ route: "/vehicle/removeVehicleLabel",
128
+ body: {
129
+ vehicleLicensePlate: licensePlate,
130
+ label,
131
+ },
132
+ modifiers: requestModifiers,
133
+ sessionId,
134
+ });
135
+ throwIfApiError(res);
136
+ return { success: true, licensePlate, label, warningMsg: res.warningMsg ?? undefined };
137
+ }
138
+ /** The raw saved vehicle, for the exists-checks the write paths need. */
139
+ export async function findSavedVehicle(licensePlate, requestModifiers, sessionId) {
140
+ const res = await postApi({
141
+ route: "/vehicle/getVehicles",
142
+ body: {},
88
143
  modifiers: requestModifiers,
89
144
  sessionId,
90
145
  });
91
- if (res.error)
92
- throw new Error(JSON.stringify(res));
93
- return { success: true };
146
+ throwIfApiError(res);
147
+ const normalized = licensePlate.trim().toUpperCase();
148
+ return res.vehicles?.find(vehicle => vehicle?.licensePlate?.trim().toUpperCase() === normalized);
94
149
  }
@@ -9,12 +9,14 @@ export async function getPolicyAlerts(args, requestModifiers, sessionId) {
9
9
  }).then(response => {
10
10
  return {
11
11
  ...response,
12
- policyAlerts: (response.policyAlerts || []).map((alert) => ({
12
+ policyAlerts: (response.policyAlerts || [])
13
+ .filter((alert) => alert != null)
14
+ .map((alert) => ({
13
15
  ...alert,
14
16
  labels: [
15
17
  ...(alert.alertingEventFaces || []),
16
18
  ...(alert.alertingEventVehicles || []),
17
- ].flatMap(alert => alert.labels),
19
+ ].flatMap(event => event?.labels ?? []),
18
20
  createdOnString: alert.timestampMs
19
21
  ? formatTimestamp(alert.timestampMs, args.timeZone)
20
22
  : undefined,
@@ -31,7 +33,9 @@ export async function getExpiringPolicyAlerts(args, requestModifiers, sessionId)
31
33
  }).then(response => {
32
34
  return {
33
35
  ...response,
34
- policyAlerts: (response.policyAlerts || []).map((alert) => ({
36
+ policyAlerts: (response.policyAlerts || [])
37
+ .filter((alert) => alert != null)
38
+ .map((alert) => ({
35
39
  ...alert,
36
40
  createdOnString: alert.timestampMs
37
41
  ? formatTimestamp(alert.timestampMs)
@@ -423,7 +423,10 @@ export async function getCustomLLMReport(promptUuid, promptType, startTimeMs, en
423
423
  return result;
424
424
  }
425
425
  export async function getAuditFeed(startTimeMs, endTimeMs, requestModifiers, sessionId) {
426
- const body = { startTimeMs, endTimeMs };
426
+ const body = {
427
+ timestampMsAfter: startTimeMs,
428
+ timestampMsBefore: endTimeMs,
429
+ };
427
430
  const response = await postApi({
428
431
  route: "/report/getAuditFeed",
429
432
  body,
@@ -1,4 +1,4 @@
1
- import { postApi } from "../network/network.js";
1
+ import { postApi, throwIfApiError } from "../network/network.js";
2
2
  export async function listRules(requestModifiers, sessionId) {
3
3
  const res = await postApi({
4
4
  route: "/rules/getRulesForOrg",
@@ -6,9 +6,7 @@ export async function listRules(requestModifiers, sessionId) {
6
6
  modifiers: requestModifiers,
7
7
  sessionId,
8
8
  });
9
- if (res.error) {
10
- throw new Error(JSON.stringify(res));
11
- }
9
+ throwIfApiError(res);
12
10
  return (res.rules?.map((rule) => ({
13
11
  uuid: rule.uuid ?? undefined,
14
12
  name: rule.name ?? undefined,
@@ -26,9 +24,7 @@ export async function createRule(ruleConfig, requestModifiers, sessionId) {
26
24
  modifiers: requestModifiers,
27
25
  sessionId,
28
26
  });
29
- if (res.error) {
30
- throw new Error(JSON.stringify(res));
31
- }
27
+ throwIfApiError(res);
32
28
  return {
33
29
  uuid: res.ruleUuid ?? undefined,
34
30
  success: true,
@@ -41,9 +37,7 @@ export async function updateRule(ruleConfig, requestModifiers, sessionId) {
41
37
  modifiers: requestModifiers,
42
38
  sessionId,
43
39
  });
44
- if (res.error) {
45
- throw new Error(JSON.stringify(res));
46
- }
40
+ throwIfApiError(res);
47
41
  return { success: true };
48
42
  }
49
43
  export async function deleteRule(ruleUuid, requestModifiers, sessionId) {
@@ -53,9 +47,7 @@ export async function deleteRule(ruleUuid, requestModifiers, sessionId) {
53
47
  modifiers: requestModifiers,
54
48
  sessionId,
55
49
  });
56
- if (res.error) {
57
- throw new Error(JSON.stringify(res));
58
- }
50
+ throwIfApiError(res);
59
51
  return { uuid: ruleUuid, success: true };
60
52
  }
61
53
  export async function getRuleRecords(ruleUuid, requestModifiers, sessionId) {
@@ -65,9 +57,7 @@ export async function getRuleRecords(ruleUuid, requestModifiers, sessionId) {
65
57
  modifiers: requestModifiers,
66
58
  sessionId,
67
59
  });
68
- if (res.error) {
69
- throw new Error(JSON.stringify(res));
70
- }
60
+ throwIfApiError(res);
71
61
  return (res.rulesEventRecords?.map((record) => ({
72
62
  uuid: record.uuid ?? undefined,
73
63
  ruleUuid: record.ruleUuid ?? undefined,
@@ -0,0 +1,107 @@
1
+ import { postApi, throwIfApiError } from "../network/network.js";
2
+ import { describeWeeklyInterval } from "../util.js";
3
+ /**
4
+ * api2 models a weekly schedule as `{name, strategy, intervalList}` where every
5
+ * interval is a pair of "minute of week" integers. We return the raw integers
6
+ * *and* a rendered `window` string — see `describeWeeklyInterval`.
7
+ */
8
+ function mapSchedule(raw) {
9
+ return {
10
+ uuid: raw.uuid ?? undefined,
11
+ name: raw.name ?? undefined,
12
+ strategy: raw.strategy ?? undefined,
13
+ mutable: raw.mutable ?? undefined,
14
+ oneTimeUse: raw.oneTimeUse ?? undefined,
15
+ intervals: raw.intervalList?.map(interval => ({
16
+ minuteOfWeekStart: interval.minuteOfWeekStart ?? undefined,
17
+ minuteOfWeekStop: interval.minuteOfWeekStop ?? undefined,
18
+ window: describeWeeklyInterval(interval.minuteOfWeekStart, interval.minuteOfWeekStop),
19
+ })) ?? [],
20
+ };
21
+ }
22
+ export async function findSchedules(requestModifiers, sessionId) {
23
+ const res = await postApi({
24
+ route: "/policy/findSchedules",
25
+ body: {},
26
+ modifiers: requestModifiers,
27
+ sessionId,
28
+ });
29
+ throwIfApiError(res);
30
+ return {
31
+ schedules: res.schedules?.flatMap(raw => (raw ? [mapSchedule(raw)] : [])) ?? [],
32
+ warningMsg: res.warningMsg ?? undefined,
33
+ };
34
+ }
35
+ export async function createSchedule(scheduleInput, requestModifiers, sessionId) {
36
+ const res = await postApi({
37
+ route: "/policy/createSchedule",
38
+ body: {
39
+ schedule: scheduleInput,
40
+ },
41
+ modifiers: requestModifiers,
42
+ sessionId,
43
+ });
44
+ throwIfApiError(res);
45
+ return {
46
+ scheduleUuid: res.scheduleUuid ?? undefined,
47
+ warningMsg: res.warningMsg ?? undefined,
48
+ };
49
+ }
50
+ export async function updateSchedule(scheduleInput, requestModifiers, sessionId) {
51
+ const res = await postApi({
52
+ route: "/policy/updateSchedule",
53
+ body: {
54
+ schedule: scheduleInput,
55
+ },
56
+ modifiers: requestModifiers,
57
+ sessionId,
58
+ });
59
+ throwIfApiError(res);
60
+ return {
61
+ updated: { success: true, scheduleUuid: scheduleInput.uuid },
62
+ warningMsg: res.warningMsg ?? undefined,
63
+ };
64
+ }
65
+ export async function deleteSchedule(scheduleUuid, requestModifiers, sessionId) {
66
+ const res = await postApi({
67
+ route: "/policy/deleteSchedule",
68
+ body: { scheduleUuid },
69
+ modifiers: requestModifiers,
70
+ sessionId,
71
+ });
72
+ throwIfApiError(res);
73
+ return {
74
+ deleted: { success: true, scheduleUuid },
75
+ warningMsg: res.warningMsg ?? undefined,
76
+ };
77
+ }
78
+ /**
79
+ * Which policies reference a schedule. Deleting a schedule that is still in use
80
+ * silently changes the behaviour of every policy attached to it, so the delete
81
+ * path calls this first and refuses when the list is non-empty.
82
+ */
83
+ export async function getPoliciesUsingSchedule(scheduleUuid, requestModifiers, sessionId) {
84
+ const res = await postApi({
85
+ route: "/policy/getPoliciesUsingSchedule",
86
+ body: {
87
+ scheduleUuid,
88
+ },
89
+ modifiers: requestModifiers,
90
+ sessionId,
91
+ });
92
+ throwIfApiError(res);
93
+ const collect = (list, policyType) => list?.flatMap(entry => entry ? [{ uuid: entry.uuid ?? undefined, name: entry.name ?? undefined, policyType }] : []) ?? [];
94
+ return {
95
+ usedBy: [
96
+ ...collect(res.cameraPolicyList, "camera"),
97
+ ...collect(res.doorPolicyList, "door"),
98
+ ...collect(res.accessControlledDoorPolicyList, "access-controlled-door"),
99
+ ...collect(res.audioPolicyList, "audio"),
100
+ ...collect(res.climatePolicyList, "climate"),
101
+ ...collect(res.occupancyPolicyList, "occupancy"),
102
+ ...collect(res.proximityPolicyList, "proximity"),
103
+ ...collect(res.videoIntercomPolicyList, "video-intercom"),
104
+ ],
105
+ warningMsg: res.warningMsg ?? undefined,
106
+ };
107
+ }
@@ -1,4 +1,4 @@
1
- import { postApi } from "../network/network.js";
1
+ import { postApi, throwIfApiError } from "../network/network.js";
2
2
  export async function searchLicensePlates(query, startTimeMs, endTimeMs, deviceUuids, locationUuids, requestModifiers, sessionId) {
3
3
  const body = {
4
4
  licensePlate: query,
@@ -13,9 +13,7 @@ export async function searchLicensePlates(query, startTimeMs, endTimeMs, deviceU
13
13
  modifiers: requestModifiers,
14
14
  sessionId,
15
15
  });
16
- if (res.error) {
17
- throw new Error(JSON.stringify(res));
18
- }
16
+ throwIfApiError(res);
19
17
  return (res.vehicleEvents?.map((event) => ({
20
18
  licensePlate: event.vehicleLicensePlate ?? undefined,
21
19
  deviceUuid: event.deviceUuid ?? undefined,
@@ -36,9 +34,7 @@ export async function searchObjectsByColor(color, cameraUuid, startTimeMs, endTi
36
34
  modifiers: requestModifiers,
37
35
  sessionId,
38
36
  });
39
- if (res.error) {
40
- throw new Error(JSON.stringify(res));
41
- }
37
+ throwIfApiError(res);
42
38
  return (res.objects?.map((result) => ({
43
39
  deviceUuid: result.deviceUuid ?? undefined,
44
40
  timestampMs: result.timestampMs ?? undefined,
@@ -62,9 +58,7 @@ export async function searchObjectsByText(textQuery, startTimeMs, endTimeMs, dev
62
58
  modifiers: requestModifiers,
63
59
  sessionId,
64
60
  });
65
- if (res.error) {
66
- throw new Error(JSON.stringify(res));
67
- }
61
+ throwIfApiError(res);
68
62
  return (res.similarEmbeddings?.map((match) => ({
69
63
  deviceUuid: match.embedding?.deviceUuid ?? undefined,
70
64
  timestampMs: match.embedding?.timestamp ?? undefined,
@@ -84,9 +78,7 @@ export async function searchMotionGrid(cameraUuid, startTimeMs, endTimeMs, reque
84
78
  modifiers: requestModifiers,
85
79
  sessionId,
86
80
  });
87
- if (res.error) {
88
- throw new Error(JSON.stringify(res));
89
- }
81
+ throwIfApiError(res);
90
82
  return (res.timeUtcSecsList?.map((ts) => ({
91
83
  timestampSecs: ts,
92
84
  })) ?? []);
@@ -1,4 +1,4 @@
1
- import { postApi } from "../network/network.js";
1
+ import { apiFailureMessage, postApi } from "../network/network.js";
2
2
  /**
3
3
  * Updates camera configuration using the faceted config API
4
4
  */
@@ -350,3 +350,98 @@ export function formatCameraSettings(settings) {
350
350
  }
351
351
  return sections.join("\n\n");
352
352
  }
353
+ /**
354
+ * Apply settings to a DOORBELL camera.
355
+ *
356
+ * Doorbell cameras take the same setting NAMES as cameras but a different
357
+ * envelope: `/doorbellcamera/updateConfig` wants one FLAT `configUpdate` object,
358
+ * where `/camera/updateFacetedConfig` wants `videoFacetSettings` /
359
+ * `audioFacetSettings` / `deviceSettings` keyed by facet. Sending the faceted
360
+ * shape here is accepted and silently changes nothing, so the flattening has to
361
+ * happen on this side.
362
+ */
363
+ export async function updateDoorbellCameraConfig(deviceUuid, settings, requestModifiers, sessionId) {
364
+ try {
365
+ const configUpdate = { deviceUuid, ...settings };
366
+ const result = await postApi({
367
+ route: "/doorbellcamera/updateConfig",
368
+ body: { configUpdate },
369
+ modifiers: requestModifiers,
370
+ sessionId,
371
+ });
372
+ if (result.error) {
373
+ return {
374
+ success: false,
375
+ error: result.errorMsg || "Failed to update doorbell camera configuration",
376
+ };
377
+ }
378
+ return { success: true, updatedSettings: configUpdate };
379
+ }
380
+ catch (error) {
381
+ return {
382
+ success: false,
383
+ error: error instanceof Error ? error.message : "Unknown error occurred",
384
+ };
385
+ }
386
+ }
387
+ /**
388
+ * Read a doorbell camera's current config. The response `config` is the same
389
+ * FLAT object `/doorbellcamera/updateConfig` writes, so a write can be verified
390
+ * by reading the changed keys back — which matters here because the update
391
+ * response only echoes the caller's own input (`updatedSettings`) and cannot
392
+ * distinguish a real write from a silent no-op.
393
+ */
394
+ export async function getDoorbellCameraConfig(deviceUuid, requestModifiers, sessionId) {
395
+ try {
396
+ const result = await postApi({
397
+ route: "/doorbellcamera/getConfig",
398
+ body: { deviceUuid },
399
+ modifiers: requestModifiers,
400
+ sessionId,
401
+ });
402
+ if (result.error) {
403
+ return {
404
+ success: false,
405
+ error: apiFailureMessage(result) ?? "Failed to read the doorbell camera's configuration",
406
+ };
407
+ }
408
+ return { success: true, config: (result.config ?? {}) };
409
+ }
410
+ catch (error) {
411
+ return {
412
+ success: false,
413
+ error: error instanceof Error ? error.message : "Unknown error occurred",
414
+ };
415
+ }
416
+ }
417
+ /** Confirm a doorbell camera exists before writing to it. */
418
+ export async function getDoorbellCameraDetails(deviceUuid, requestModifiers, sessionId) {
419
+ try {
420
+ const result = await postApi({
421
+ route: "/doorbellcamera/getMinimalStateList",
422
+ body: {},
423
+ modifiers: requestModifiers,
424
+ sessionId,
425
+ });
426
+ if (result.error) {
427
+ return {
428
+ success: false,
429
+ error: result.errorMsg || "Failed to list doorbell cameras",
430
+ };
431
+ }
432
+ const match = result.minimalStates?.find(state => state?.uuid === deviceUuid);
433
+ if (!match) {
434
+ return {
435
+ success: false,
436
+ error: `No doorbell camera in this organization has the uuid "${deviceUuid}". Use get-entity-tool with entityType doorbell-camera to list them — do not guess a uuid.`,
437
+ };
438
+ }
439
+ return { success: true, name: match.name ?? undefined };
440
+ }
441
+ catch (error) {
442
+ return {
443
+ success: false,
444
+ error: error instanceof Error ? error.message : "Unknown error occurred",
445
+ };
446
+ }
447
+ }
@@ -1,4 +1,4 @@
1
- import { postApi } from "../network/network.js";
1
+ import { apiWarning, postApi, throwIfApiError } from "../network/network.js";
2
2
  export async function listUsers(requestModifiers, sessionId) {
3
3
  const res = await postApi({
4
4
  route: "/user/getUsersInOrg",
@@ -6,9 +6,7 @@ export async function listUsers(requestModifiers, sessionId) {
6
6
  modifiers: requestModifiers,
7
7
  sessionId,
8
8
  });
9
- if (res.error) {
10
- throw new Error(JSON.stringify(res));
11
- }
9
+ throwIfApiError(res);
12
10
  return (res.users?.map(user => ({
13
11
  uuid: user.uuid ?? undefined,
14
12
  email: user.email ?? undefined,
@@ -23,9 +21,7 @@ export async function findUserByEmail(email, requestModifiers, sessionId) {
23
21
  modifiers: requestModifiers,
24
22
  sessionId,
25
23
  });
26
- if (res.error) {
27
- throw new Error(JSON.stringify(res));
28
- }
24
+ throwIfApiError(res);
29
25
  const user = res.user;
30
26
  if (!user)
31
27
  return undefined;
@@ -48,9 +44,7 @@ export async function getPermissionsForCurrentUser(requestModifiers, sessionId)
48
44
  modifiers: requestModifiers,
49
45
  sessionId,
50
46
  });
51
- if (res.error) {
52
- throw new Error(JSON.stringify(res));
53
- }
47
+ throwIfApiError(res);
54
48
  return {
55
49
  role: res.role ?? undefined,
56
50
  functionalityAccessMap: res.functionalityAccessMap
@@ -98,9 +92,7 @@ export async function getPermissionGroups(requestModifiers, sessionId) {
98
92
  modifiers: requestModifiers,
99
93
  sessionId,
100
94
  });
101
- if (res.error) {
102
- throw new Error(JSON.stringify(res));
103
- }
95
+ throwIfApiError(res);
104
96
  return (res.permissionGroups?.map(group => ({
105
97
  uuid: group.uuid ?? undefined,
106
98
  name: group.name ?? undefined,
@@ -124,3 +116,56 @@ export async function getPermissionGroups(requestModifiers, sessionId) {
124
116
  locationGranularAccessMap: toNestedStringMap(group.locationGranularAccessMap),
125
117
  })) ?? []);
126
118
  }
119
+ // ---------------------------------------------------------------------------
120
+ // User write paths
121
+ // ---------------------------------------------------------------------------
122
+ export async function createUser(input, requestModifiers, sessionId) {
123
+ const res = await postApi({
124
+ route: "/user/createUser",
125
+ body: {
126
+ name: input.name,
127
+ email: input.email,
128
+ permissionGroupUuid: input.permissionGroupUuid,
129
+ // Creating a user normally emails them an invitation. That is an
130
+ // outward-facing side effect, so the caller has to opt into suppressing it
131
+ // rather than us silently picking either behaviour.
132
+ suppressWelcomeEmail: input.suppressWelcomeEmail ?? false,
133
+ },
134
+ modifiers: requestModifiers,
135
+ sessionId,
136
+ });
137
+ throwIfApiError(res);
138
+ return {
139
+ success: true,
140
+ uuid: res.userUuid ?? undefined,
141
+ warningMsg: res.warningMsg ?? undefined,
142
+ };
143
+ }
144
+ /**
145
+ * Selective user update — omitted fields are left alone, so a rename cannot
146
+ * blank someone's notification settings or MFA flag.
147
+ */
148
+ export async function updateUser(userUuid, changes, requestModifiers, sessionId) {
149
+ const res = await postApi({
150
+ route: "/user/updateUserSelective",
151
+ body: {
152
+ userUuid,
153
+ name: changes.name,
154
+ permissionGroupUuid: changes.permissionGroupUuid,
155
+ },
156
+ modifiers: requestModifiers,
157
+ sessionId,
158
+ });
159
+ throwIfApiError(res);
160
+ return { success: true, uuid: userUuid, warningMsg: apiWarning(res) };
161
+ }
162
+ export async function deleteUser(userUuid, requestModifiers, sessionId) {
163
+ const res = await postApi({
164
+ route: "/user/deleteUser",
165
+ body: { userUuid },
166
+ modifiers: requestModifiers,
167
+ sessionId,
168
+ });
169
+ throwIfApiError(res);
170
+ return { success: true, uuid: userUuid, warningMsg: apiWarning(res) };
171
+ }
@@ -1,7 +1,7 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import path from "path";
3
3
  import { createTracingProxy } from "./telemetry/tracingProxy.js";
4
- import { resolveAccessibleApps } from "./api/get-accessible-apps.js";
4
+ import { resolveAccessibleApps, resolveSessionIdentity, } from "./api/get-accessible-apps.js";
5
5
  import { logger } from "./logger.js";
6
6
  import { createFilteringProxy } from "./util.js";
7
7
  import getResources from "./resources/getResources.js";
@@ -42,6 +42,11 @@ export async function serverInit() {
42
42
  *
43
43
  * Otherwise (unresolved session, empty apps, or only other enums like
44
44
  * RHOMBUS_KEY), fall back to the permissive union of console + partner sets.
45
+ *
46
+ * Support-authority sessions (getCurrentUser with no `user`, or
47
+ * `sessionType === "SUPPORT"`) are resolved to exactly `[CONSOLE]` upstream in
48
+ * `resolveAccessibleApps`, so they take the console branch here and never the
49
+ * permissive union.
45
50
  */
46
51
  function pickToolsForSession(apps) {
47
52
  if (apps !== null && apps.length > 0 && apps.includes(RhombusAppEnum.PARTNER)) {
@@ -59,7 +64,11 @@ function isNodeDevEnvironment() {
59
64
  * Human-readable caller + tool-set summary for dev logs (must stay aligned with
60
65
  * {@link pickToolsForSession}).
61
66
  */
62
- function describeCallerForDevLogs(apps) {
67
+ function describeCallerForDevLogs(apps, identity) {
68
+ if (identity?.sessionType === "SUPPORT") {
69
+ const who = [identity.name, identity.email].filter(Boolean).join(" ");
70
+ return `caller=support session (${who || "no name"}, id=${identity.userId ?? "?"}); tool sets=shared + console`;
71
+ }
63
72
  if (apps === null) {
64
73
  return "caller=unknown (no session or unresolved apps); tool sets=shared + console + partner (permissive)";
65
74
  }
@@ -77,11 +86,11 @@ function describeCallerForDevLogs(apps) {
77
86
  }
78
87
  return `caller=other (accessibleRhombusApps=[${apps.join(", ")}]); tool sets=shared + console + partner (permissive)`;
79
88
  }
80
- function logDevToolRegistration(sessionId, apps, toolsToRegister) {
89
+ function logDevToolRegistration(sessionId, apps, identity, toolsToRegister) {
81
90
  if (!isNodeDevEnvironment())
82
91
  return;
83
92
  const names = toolsToRegister.map((t) => path.basename(t.name, ".js")).sort();
84
- logger.info(`[dev MCP tools] session=${sessionId ?? "(none)"} — ${describeCallerForDevLogs(apps)} — registering ${names.length} tools`);
93
+ logger.info(`[dev MCP tools] session=${sessionId ?? "(none)"} — ${describeCallerForDevLogs(apps, identity)} — registering ${names.length} tools`);
85
94
  logger.info(`[dev MCP tools] tool names: ${names.join(", ")}`);
86
95
  }
87
96
  export default async function createServer({ sessionId } = {}) {
@@ -103,9 +112,12 @@ export default async function createServer({ sessionId } = {}) {
103
112
  }
104
113
  logger.info(`📚 Registered ${resources.length} resources`);
105
114
  const apps = await resolveAccessibleApps(sessionId);
115
+ // Same cached getCurrentUser payload — no second API call.
116
+ const identity = await resolveSessionIdentity(sessionId);
106
117
  const toolsToRegister = pickToolsForSession(apps);
107
- logDevToolRegistration(sessionId, apps, toolsToRegister);
108
- logger.info(`🔒 Session ${sessionId ?? "(none)"}: apps=[${apps?.join(", ") ?? "unknown"}] — registering ${toolsToRegister.length} tools`);
118
+ logDevToolRegistration(sessionId, apps, identity, toolsToRegister);
119
+ const sessionKind = identity?.sessionType === "SUPPORT" ? " sessionType=SUPPORT" : "";
120
+ logger.info(`🔒 Session ${sessionId ?? "(none)"}:${sessionKind} apps=[${apps?.join(", ") ?? "unknown"}] — registering ${toolsToRegister.length} tools`);
109
121
  // Tracing wraps every tool handler; filtering wraps on top so handlers are
110
122
  // timed after includeFields/filterBy are stripped (keeping them out of
111
123
  // mcp.tool.arg_keys). Spans are no-ops unless a host registers an OTel SDK.