rhombus-node-mcp 0.1.7 → 0.1.9

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 +74 -40
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -8,15 +8,18 @@ const RHOMBUS_API_KEY = process.env.RHOMBUS_API_KEY;
8
8
  if (!RHOMBUS_API_KEY) {
9
9
  console.error("Missing RHOMBUS_API_KEY");
10
10
  }
11
- const BASE_URL = "https://api2.rhombussystems.com/api";
11
+ const enableLogs = process.env.ENABLE_LOGS;
12
+ const serverUrl = process.env.RHOMBUS_API_SERVER || "api2.rhombussystems.com";
13
+ console.error("🌐 Using server url", serverUrl);
14
+ const BASE_URL = `https://${serverUrl}/api`;
12
15
  const STATIC_HEADERS = {
13
16
  "Content-Type": "application/json",
17
+ "x-rhombus-agent": "chatbot",
14
18
  accept: "application/json",
15
19
  };
16
20
  const AUTH_HEADERS = {
17
21
  "x-auth-apikey": RHOMBUS_API_KEY,
18
22
  "x-auth-scheme": "api-token",
19
- "x-rhombus-agent": "chatbot",
20
23
  };
21
24
  const server = new McpServer({
22
25
  name: "rhombus",
@@ -27,18 +30,44 @@ const server = new McpServer({
27
30
  },
28
31
  });
29
32
  const STATIC_ARGS = {
30
- headers: z
31
- .optional(z.any())
33
+ requestModifiers: z
34
+ .optional(z.object({
35
+ headers: z.optional(z.any()),
36
+ query: z.optional(z.any()),
37
+ }))
32
38
  .describe("Optional headers accepted by tools. LLM should never ever use this. 😅"),
33
39
  };
34
- async function postApi(url, body, customHeaders) {
35
- let headers = {
36
- ...(customHeaders || AUTH_HEADERS),
40
+ const log = (msg) => {
41
+ if (!enableLogs)
42
+ return;
43
+ console.error(msg);
44
+ };
45
+ const appendQueryParams = (url, params) => {
46
+ if (!params || typeof params !== "object")
47
+ return url;
48
+ const urlObj = new URL(url);
49
+ const existingSearchParams = new URLSearchParams(urlObj.search);
50
+ for (const [key, value] of Object.entries(params)) {
51
+ if (value !== undefined && value !== null) {
52
+ existingSearchParams.append(key, String(value));
53
+ }
54
+ }
55
+ const baseUrl = url.split("?")[0];
56
+ const queryString = existingSearchParams.toString();
57
+ return queryString ? `${baseUrl}?${queryString}` : baseUrl;
58
+ };
59
+ async function postApi(url, body, modifiers) {
60
+ let requestHeaders = {
61
+ ...(modifiers?.headers || AUTH_HEADERS),
37
62
  ...STATIC_HEADERS,
38
63
  };
64
+ if (modifiers?.query) {
65
+ url = appendQueryParams(url, modifiers.query);
66
+ }
39
67
  try {
40
- console.error(`[POSTAPI] - ${url} - ${body} - ${JSON.stringify(customHeaders)}`);
41
- const response = await fetch(url, { method: "POST", headers, body });
68
+ log(`[POSTAPI] REQUEST - ${url} - ${body} - ${JSON.stringify(requestHeaders)}`);
69
+ const response = await fetch(url, { method: "POST", headers: requestHeaders, body });
70
+ log(`[POSTAPI] RESPONSE - ${JSON.stringify(response)}`);
42
71
  if (!response.ok) {
43
72
  if (response.status === 401 || response.status === 403) {
44
73
  return {
@@ -51,33 +80,34 @@ async function postApi(url, body, customHeaders) {
51
80
  return await response.json();
52
81
  }
53
82
  catch (error) {
83
+ log(`[POSTAPI] ERROR - ${JSON.stringify(error || {})}`);
54
84
  return {
55
85
  error: true,
56
86
  status: `Request Error: ${error}`,
57
87
  };
58
88
  }
59
89
  }
60
- async function getOrg(headers) {
90
+ async function getOrg(requestModifiers) {
61
91
  const url = BASE_URL + "/org/getOrgV2";
62
- return await postApi(url, "{}", headers);
92
+ return await postApi(url, "{}", requestModifiers);
63
93
  }
64
- async function getLocations(headers) {
94
+ async function getLocations(requestModifiers) {
65
95
  const url = BASE_URL + "/location/getLocationsV2";
66
- return await postApi(url, "{}", headers);
96
+ return await postApi(url, "{}", requestModifiers);
67
97
  }
68
- async function getCameraList(headers) {
98
+ async function getCameraList(requestModifiers) {
69
99
  const url = BASE_URL + "/camera/getMinimalCameraStateList";
70
- return await postApi(url, "{}", headers).then(response => {
100
+ return await postApi(url, "{}", requestModifiers).then(response => {
71
101
  return {
72
102
  cameraStates: response.cameraStates.filter((camera) => !!camera.locationUuid),
73
103
  };
74
104
  });
75
105
  }
76
- async function getAccessControlledDoors(headers) {
106
+ async function getAccessControlledDoors(requestModifiers) {
77
107
  const url = BASE_URL + "/component/findAccessControlledDoors";
78
- return await postApi(url, "{}", headers);
108
+ return await postApi(url, "{}", requestModifiers);
79
109
  }
80
- async function getFaceEvents(_locationUuid, headers) {
110
+ async function getFaceEvents(_locationUuid, requestModifiers) {
81
111
  const nowMs = Date.now();
82
112
  const rangeStartMs = nowMs - THREE_HOURS_MS;
83
113
  const rangeEndMs = nowMs - FIVE_SECONDS_MS;
@@ -98,7 +128,7 @@ async function getFaceEvents(_locationUuid, headers) {
98
128
  },
99
129
  },
100
130
  });
101
- const response = await postApi(BASE_URL + "/faceRecognition/faceEvent/findFaceEventsByOrg", body, headers).then(response => {
131
+ const response = await postApi(BASE_URL + "/faceRecognition/faceEvent/findFaceEventsByOrg", body, requestModifiers).then(response => {
102
132
  return {
103
133
  faceEvents: (response.faceEvents || []).map((event) => ({
104
134
  ...event,
@@ -108,13 +138,13 @@ async function getFaceEvents(_locationUuid, headers) {
108
138
  });
109
139
  return response;
110
140
  }
111
- async function getAccessControlEvents(doorUuid, headers) {
141
+ async function getAccessControlEvents(doorUuid, requestModifiers) {
112
142
  const url = BASE_URL + "/component/findComponentEventsByAccessControlledDoor";
113
143
  const body = JSON.stringify({
114
144
  limit: 50,
115
145
  accessControlledDoorUuid: doorUuid,
116
146
  });
117
- const response = await postApi(url, body, headers).then(response => ({
147
+ const response = await postApi(url, body, requestModifiers).then(response => ({
118
148
  componentEvents: (response.componentEvents || []).map((event) => ({
119
149
  ...event,
120
150
  timestamp: new Date(event.timestampMs).toString(),
@@ -122,14 +152,14 @@ async function getAccessControlEvents(doorUuid, headers) {
122
152
  }));
123
153
  return response;
124
154
  }
125
- async function rebootCameras(cameraUuids, headers) {
155
+ async function rebootCameras(cameraUuids, requestModifiers) {
126
156
  const url = BASE_URL + "/camera/reboot";
127
157
  let successCount = 0;
128
158
  let errorCount = 0;
129
159
  for (const cameraUuid in cameraUuids) {
130
160
  try {
131
161
  const body = JSON.stringify({ cameraUuid: cameraUuid });
132
- const response = await postApi(url, body, headers);
162
+ const response = await postApi(url, body, requestModifiers);
133
163
  if (response.error) {
134
164
  errorCount++;
135
165
  }
@@ -151,7 +181,7 @@ async function rebootCameras(cameraUuids, headers) {
151
181
  return { status, successCount, errorCount };
152
182
  }
153
183
  }
154
- async function getImageForCameraAtTime(cameraUuid, timestampMs, headers) {
184
+ async function getImageForCameraAtTime(cameraUuid, timestampMs, requestModifiers) {
155
185
  const url = BASE_URL + "/video/getExactFrameUri";
156
186
  const body = JSON.stringify({
157
187
  cameraUuid: cameraUuid,
@@ -163,8 +193,12 @@ async function getImageForCameraAtTime(cameraUuid, timestampMs, headers) {
163
193
  permyriadCropY: 0,
164
194
  timestampMs: timestampMs,
165
195
  });
166
- const base64Image = await postApi(url, body, headers).then(async (res) => {
167
- return await fetch(res.frameUri, { method: "GET", headers: headers }).then(async (res) => {
196
+ const base64Image = await postApi(url, body, requestModifiers).then(async (res) => {
197
+ let requestHeaders = {
198
+ ...(requestModifiers?.headers || AUTH_HEADERS),
199
+ ...STATIC_HEADERS,
200
+ };
201
+ return await fetch(res.frameUri, { method: "GET", headers: requestHeaders }).then(async (res) => {
168
202
  if (!res.ok)
169
203
  return null;
170
204
  const arrayBuffer = await res.arrayBuffer();
@@ -186,8 +220,8 @@ async function getImageForCameraAtTime(cameraUuid, timestampMs, headers) {
186
220
  imageData: base64Image,
187
221
  };
188
222
  }
189
- 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 }) => {
190
- const org = await getOrg(headers);
223
+ 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 ({ requestModifiers }) => {
224
+ const org = await getOrg(requestModifiers);
191
225
  return {
192
226
  content: [
193
227
  {
@@ -202,14 +236,14 @@ server.tool("get-entity-tool", "get a list of entities like cameras, access cont
202
236
  .enum(["camera", "access-controlled-doors"])
203
237
  .describe("The entity type to retreive. Example: cameras."),
204
238
  ...STATIC_ARGS,
205
- }, async ({ entityType, headers }) => {
239
+ }, async ({ entityType, requestModifiers }) => {
206
240
  let ret;
207
241
  switch (entityType) {
208
242
  case "camera":
209
- ret = await getCameraList(headers);
243
+ ret = await getCameraList(requestModifiers);
210
244
  break;
211
245
  case "access-controlled-doors":
212
- ret = await getAccessControlledDoors(headers);
246
+ ret = await getAccessControlledDoors(requestModifiers);
213
247
  break;
214
248
  default:
215
249
  ret = {};
@@ -229,7 +263,7 @@ server.tool("camera-tool", "get specific requested information about a camera su
229
263
  timestampMs: z.optional(z.number()).describe("the timestamp in milliseconds"),
230
264
  cameraUuid: z.optional(z.string()).describe("the camera uuid requested"),
231
265
  ...STATIC_ARGS,
232
- }, async ({ cameraUuid, timestampMs, requestType, headers }) => {
266
+ }, async ({ cameraUuid, timestampMs, requestType, requestModifiers }) => {
233
267
  if (!cameraUuid) {
234
268
  return {
235
269
  content: [
@@ -259,7 +293,7 @@ server.tool("camera-tool", "get specific requested information about a camera su
259
293
  let response;
260
294
  switch (requestType) {
261
295
  case "image":
262
- response = await getImageForCameraAtTime(cameraUuid, timestampMs, headers);
296
+ response = await getImageForCameraAtTime(cameraUuid, timestampMs, requestModifiers);
263
297
  if (!response.success || !response.imageData) {
264
298
  return {
265
299
  content: [{ type: "text", text: JSON.stringify(response) }],
@@ -296,9 +330,9 @@ server.tool("events-tool", "event data for certain types of information like fac
296
330
  locationUuid: z.optional(z.string()),
297
331
  accessControlledDoorUuid: z.optional(z.string()),
298
332
  ...STATIC_ARGS,
299
- }, async ({ eventType, locationUuid, accessControlledDoorUuid, headers }) => {
333
+ }, async ({ eventType, locationUuid, accessControlledDoorUuid, requestModifiers }) => {
300
334
  if (eventType === "faces" || eventType === "people") {
301
- const response = await getFaceEvents(locationUuid, headers);
335
+ const response = await getFaceEvents(locationUuid, requestModifiers);
302
336
  return {
303
337
  content: [
304
338
  {
@@ -323,7 +357,7 @@ server.tool("events-tool", "event data for certain types of information like fac
323
357
  };
324
358
  }
325
359
  else {
326
- const events = await getAccessControlEvents(accessControlledDoorUuid, headers);
360
+ const events = await getAccessControlEvents(accessControlledDoorUuid, requestModifiers);
327
361
  return {
328
362
  content: [
329
363
  {
@@ -347,11 +381,11 @@ server.tool("location-tool", "contains basic operations for locations and respon
347
381
  action: z.enum(["get"]),
348
382
  locationUpdate: z.optional(z.object({ uuid: z.string(), name: z.optional(z.string()) })),
349
383
  ...STATIC_ARGS,
350
- }, async ({ action, locationUpdate, headers }) => {
384
+ }, async ({ action, locationUpdate, requestModifiers }) => {
351
385
  let ret;
352
386
  switch (action) {
353
387
  case "get":
354
- ret = await getLocations(headers);
388
+ ret = await getLocations(requestModifiers);
355
389
  break;
356
390
  default:
357
391
  ret = { error: true, status: `unsupported location tool call: ${action}` };
@@ -366,8 +400,8 @@ server.tool("reboot-cameras", "this tool is for rebooting one or more cameras ca
366
400
  .array(z.string())
367
401
  .describe("An array of camera UUID strings which are unique identifiers for cameras"),
368
402
  ...STATIC_ARGS,
369
- }, async ({ cameraUuids, headers }) => {
370
- const cameraRebootData = await rebootCameras(cameraUuids, headers);
403
+ }, async ({ cameraUuids, requestModifiers }) => {
404
+ const cameraRebootData = await rebootCameras(cameraUuids, requestModifiers);
371
405
  if (!cameraRebootData) {
372
406
  return {
373
407
  content: [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rhombus-node-mcp",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "MCP server for Rhombus API",
5
5
  "keywords": [
6
6
  "ai",