rhombus-node-mcp 0.1.4 → 0.1.6

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 (2) hide show
  1. package/dist/index.js +51 -35
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -7,14 +7,16 @@ const FIVE_SECONDS_MS = 5 * 1000;
7
7
  const RHOMBUS_API_KEY = process.env.RHOMBUS_API_KEY;
8
8
  if (!RHOMBUS_API_KEY) {
9
9
  console.error("Missing RHOMBUS_API_KEY");
10
- process.exit(1);
11
10
  }
12
11
  const BASE_URL = "https://api2.rhombussystems.com/api";
13
- const headers = {
12
+ const STATIC_HEADERS = {
14
13
  "Content-Type": "application/json",
14
+ accept: "application/json",
15
+ };
16
+ const AUTH_HEADERS = {
15
17
  "x-auth-apikey": RHOMBUS_API_KEY,
16
18
  "x-auth-scheme": "api-token",
17
- accept: "application/json",
19
+ "x-rhombus-agent": "chatbot",
18
20
  };
19
21
  const server = new McpServer({
20
22
  name: "rhombus",
@@ -24,7 +26,16 @@ const server = new McpServer({
24
26
  tools: {},
25
27
  },
26
28
  });
27
- async function postApi(url, body) {
29
+ const STATIC_ARGS = {
30
+ headers: z
31
+ .optional(z.any())
32
+ .describe("Optional headers accepted by tools. LLM should never ever use this. 😅"),
33
+ };
34
+ async function postApi(url, body, customHeaders) {
35
+ let headers = {
36
+ ...(customHeaders || AUTH_HEADERS),
37
+ ...STATIC_HEADERS,
38
+ };
28
39
  try {
29
40
  const response = await fetch(url, { method: "POST", headers, body });
30
41
  if (!response.ok) {
@@ -45,27 +56,27 @@ async function postApi(url, body) {
45
56
  };
46
57
  }
47
58
  }
48
- async function getOrg() {
59
+ async function getOrg(headers) {
49
60
  const url = BASE_URL + "/org/getOrgV2";
50
- return await postApi(url, "{}");
61
+ return await postApi(url, "{}", headers);
51
62
  }
52
- async function getLocations() {
63
+ async function getLocations(headers) {
53
64
  const url = BASE_URL + "/location/getLocationsV2";
54
- return await postApi(url, "{}");
65
+ return await postApi(url, "{}", headers);
55
66
  }
56
- async function getCameraList() {
67
+ async function getCameraList(headers) {
57
68
  const url = BASE_URL + "/camera/getMinimalCameraStateList";
58
- return await postApi(url, "{}").then(response => {
69
+ return await postApi(url, "{}", headers).then(response => {
59
70
  return {
60
71
  cameraStates: response.cameraStates.filter((camera) => !!camera.locationUuid),
61
72
  };
62
73
  });
63
74
  }
64
- async function getAccessControlledDoors() {
75
+ async function getAccessControlledDoors(headers) {
65
76
  const url = BASE_URL + "/component/findAccessControlledDoors";
66
- return await postApi(url, "{}");
77
+ return await postApi(url, "{}", headers);
67
78
  }
68
- async function getFaceEvents(_locationUuid) {
79
+ async function getFaceEvents(_locationUuid, headers) {
69
80
  const nowMs = Date.now();
70
81
  const rangeStartMs = nowMs - THREE_HOURS_MS;
71
82
  const rangeEndMs = nowMs - FIVE_SECONDS_MS;
@@ -86,7 +97,7 @@ async function getFaceEvents(_locationUuid) {
86
97
  },
87
98
  },
88
99
  });
89
- const response = await postApi(BASE_URL + "/faceRecognition/faceEvent/findFaceEventsByOrg", body).then(response => {
100
+ const response = await postApi(BASE_URL + "/faceRecognition/faceEvent/findFaceEventsByOrg", body, headers).then(response => {
90
101
  return {
91
102
  faceEvents: (response.faceEvents || []).map((event) => ({
92
103
  ...event,
@@ -96,13 +107,13 @@ async function getFaceEvents(_locationUuid) {
96
107
  });
97
108
  return response;
98
109
  }
99
- async function getAccessControlEvents(doorUuid) {
110
+ async function getAccessControlEvents(doorUuid, headers) {
100
111
  const url = BASE_URL + "/component/findComponentEventsByAccessControlledDoor";
101
112
  const body = JSON.stringify({
102
113
  limit: 50,
103
114
  accessControlledDoorUuid: doorUuid,
104
115
  });
105
- const response = await postApi(url, body).then(response => ({
116
+ const response = await postApi(url, body, headers).then(response => ({
106
117
  componentEvents: (response.componentEvents || []).map((event) => ({
107
118
  ...event,
108
119
  timestamp: new Date(event.timestampMs).toString(),
@@ -110,14 +121,14 @@ async function getAccessControlEvents(doorUuid) {
110
121
  }));
111
122
  return response;
112
123
  }
113
- async function rebootCameras(cameraUuids) {
124
+ async function rebootCameras(cameraUuids, headers) {
114
125
  const url = BASE_URL + "/camera/reboot";
115
126
  let successCount = 0;
116
127
  let errorCount = 0;
117
128
  for (const cameraUuid in cameraUuids) {
118
129
  try {
119
130
  const body = JSON.stringify({ cameraUuid: cameraUuid });
120
- const response = await postApi(url, body);
131
+ const response = await postApi(url, body, headers);
121
132
  if (response.error) {
122
133
  errorCount++;
123
134
  }
@@ -139,7 +150,7 @@ async function rebootCameras(cameraUuids) {
139
150
  return { status, successCount, errorCount };
140
151
  }
141
152
  }
142
- async function getImageForCameraAtTime(cameraUuid, timestampMs) {
153
+ async function getImageForCameraAtTime(cameraUuid, timestampMs, headers) {
143
154
  const url = BASE_URL + "/video/getExactFrameUri";
144
155
  const body = JSON.stringify({
145
156
  cameraUuid: cameraUuid,
@@ -151,7 +162,7 @@ async function getImageForCameraAtTime(cameraUuid, timestampMs) {
151
162
  permyriadCropY: 0,
152
163
  timestampMs: timestampMs,
153
164
  });
154
- const base64Image = await postApi(url, body).then(async (res) => {
165
+ const base64Image = await postApi(url, body, headers).then(async (res) => {
155
166
  return await fetch(res.frameUri, { method: "GET", headers: headers }).then(async (res) => {
156
167
  if (!res.ok)
157
168
  return null;
@@ -174,8 +185,8 @@ async function getImageForCameraAtTime(cameraUuid, timestampMs) {
174
185
  imageData: base64Image,
175
186
  };
176
187
  }
177
- server.tool("get-org-information", "Get general information about the organization including org name, camera configuration defaults, contact information, and org settings.", {}, async ({}) => {
178
- const org = await getOrg();
188
+ server.tool("get-org-information", "Get general information about the organization including org name, camera configuration defaults, contact information, and org settings.", { ...STATIC_ARGS }, async ({ headers }) => {
189
+ const org = await getOrg(headers);
179
190
  return {
180
191
  content: [
181
192
  {
@@ -189,14 +200,15 @@ server.tool("get-entity-tool", "get a list of entities like cameras, access cont
189
200
  entityType: z
190
201
  .enum(["camera", "access-controlled-doors"])
191
202
  .describe("The entity type to retreive. Example: cameras."),
192
- }, async ({ entityType }) => {
203
+ ...STATIC_ARGS,
204
+ }, async ({ entityType, headers }) => {
193
205
  let ret;
194
206
  switch (entityType) {
195
207
  case "camera":
196
- ret = await getCameraList();
208
+ ret = await getCameraList(headers);
197
209
  break;
198
210
  case "access-controlled-doors":
199
- ret = await getAccessControlledDoors();
211
+ ret = await getAccessControlledDoors(headers);
200
212
  break;
201
213
  default:
202
214
  ret = {};
@@ -211,11 +223,12 @@ server.tool("get-entity-tool", "get a list of entities like cameras, access cont
211
223
  ],
212
224
  };
213
225
  });
214
- server.tool("camera-tool", "get specific requested information about a camera such as an image snapshot, or detailed analytics info", {
226
+ server.tool("camera-tool", "get specific requested information about a camera such as an image snapshot, or detailed analytics info. this can be used to answer questions about tracking people across cameras", {
215
227
  requestType: z.enum(["image"]),
216
228
  timestampMs: z.optional(z.number()).describe("the timestamp in milliseconds"),
217
229
  cameraUuid: z.optional(z.string()).describe("the camera uuid requested"),
218
- }, async ({ cameraUuid, timestampMs, requestType }) => {
230
+ ...STATIC_ARGS,
231
+ }, async ({ cameraUuid, timestampMs, requestType, headers }) => {
219
232
  if (!cameraUuid) {
220
233
  return {
221
234
  content: [
@@ -245,7 +258,7 @@ server.tool("camera-tool", "get specific requested information about a camera su
245
258
  let response;
246
259
  switch (requestType) {
247
260
  case "image":
248
- response = await getImageForCameraAtTime(cameraUuid, timestampMs);
261
+ response = await getImageForCameraAtTime(cameraUuid, timestampMs, headers);
249
262
  if (!response.success || !response.imageData) {
250
263
  return {
251
264
  content: [{ type: "text", text: JSON.stringify(response) }],
@@ -281,9 +294,10 @@ server.tool("events-tool", "event data for certain types of information like fac
281
294
  eventType: z.enum(["faces", "people", "access-control"]),
282
295
  locationUuid: z.optional(z.string()),
283
296
  accessControlledDoorUuid: z.optional(z.string()),
284
- }, async ({ eventType, locationUuid, accessControlledDoorUuid }) => {
297
+ ...STATIC_ARGS,
298
+ }, async ({ eventType, locationUuid, accessControlledDoorUuid, headers }) => {
285
299
  if (eventType === "faces" || eventType === "people") {
286
- const response = await getFaceEvents(locationUuid);
300
+ const response = await getFaceEvents(locationUuid, headers);
287
301
  return {
288
302
  content: [
289
303
  {
@@ -308,7 +322,7 @@ server.tool("events-tool", "event data for certain types of information like fac
308
322
  };
309
323
  }
310
324
  else {
311
- const events = await getAccessControlEvents(accessControlledDoorUuid);
325
+ const events = await getAccessControlEvents(accessControlledDoorUuid, headers);
312
326
  return {
313
327
  content: [
314
328
  {
@@ -331,11 +345,12 @@ server.tool("events-tool", "event data for certain types of information like fac
331
345
  server.tool("location-tool", "contains basic operations for locations and response in JSON format.", {
332
346
  action: z.enum(["get"]),
333
347
  locationUpdate: z.optional(z.object({ uuid: z.string(), name: z.optional(z.string()) })),
334
- }, async ({ action, locationUpdate }) => {
348
+ ...STATIC_ARGS,
349
+ }, async ({ action, locationUpdate, headers }) => {
335
350
  let ret;
336
351
  switch (action) {
337
352
  case "get":
338
- ret = await getLocations();
353
+ ret = await getLocations(headers);
339
354
  break;
340
355
  default:
341
356
  ret = { error: true, status: `unsupported location tool call: ${action}` };
@@ -349,8 +364,9 @@ server.tool("reboot-cameras", "this tool is for rebooting one or more cameras ca
349
364
  cameraUuids: z
350
365
  .array(z.string())
351
366
  .describe("An array of camera UUID strings which are unique identifiers for cameras"),
352
- }, async ({ cameraUuids }) => {
353
- const cameraRebootData = await rebootCameras(cameraUuids);
367
+ ...STATIC_ARGS,
368
+ }, async ({ cameraUuids, headers }) => {
369
+ const cameraRebootData = await rebootCameras(cameraUuids, headers);
354
370
  if (!cameraRebootData) {
355
371
  return {
356
372
  content: [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rhombus-node-mcp",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "MCP server for Rhombus API",
5
5
  "keywords": [
6
6
  "ai",