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