rhombus-node-mcp 0.1.3 → 0.1.5

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 (3) hide show
  1. package/README.md +10 -3
  2. package/dist/index.js +62 -35
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -23,9 +23,16 @@ An MCP server implementation that integrates the Rhombus API to provide Chatbot
23
23
  "mcpServers": {
24
24
  "rhombus": {
25
25
  "command": "docker",
26
- "args": ["run", "-i", "--rm", "-e", "RHOMBUS_API_KEY", "mcp-server-rhombus"],
26
+ "args": [
27
+ "run",
28
+ "-i",
29
+ "--rm",
30
+ "-e",
31
+ "RHOMBUS_API_KEY=YOUR_API_KEY_HERE",
32
+ "mcp-server-rhombus"
33
+ ],
27
34
  "env": {
28
- "RHOMBUS_API_KEY": "YOUR_API_KEY"
35
+ "RHOMBUS_API_KEY": "YOUR_API_KEY_HERE"
29
36
  }
30
37
  }
31
38
  }
@@ -39,7 +46,7 @@ An MCP server implementation that integrates the Rhombus API to provide Chatbot
39
46
  "mcpServers": {
40
47
  "rhombus": {
41
48
  "command": "npx",
42
- "args": ["-y", "server-rhombus"],
49
+ "args": ["--yes", "--package", "rhombus-node-mcp", "mcp-server-rhombus"],
43
50
  "env": {
44
51
  "RHOMBUS_API_KEY": "YOUR_API_KEY_HERE"
45
52
  }
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,26 @@ 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
+ const shouldUseCustomHeaders = (headers) => {
35
+ let valid = true;
36
+ if (!headers)
37
+ return false;
38
+ Object.keys(headers).forEach(key => {
39
+ if (headers[key].length === 0)
40
+ valid = false;
41
+ });
42
+ return valid;
43
+ };
44
+ async function postApi(url, body, customHeaders) {
45
+ let headers = {
46
+ ...(shouldUseCustomHeaders(customHeaders) ? customHeaders : AUTH_HEADERS),
47
+ ...STATIC_HEADERS,
48
+ };
28
49
  try {
29
50
  const response = await fetch(url, { method: "POST", headers, body });
30
51
  if (!response.ok) {
@@ -45,27 +66,28 @@ async function postApi(url, body) {
45
66
  };
46
67
  }
47
68
  }
48
- async function getOrg() {
69
+ async function getOrg(headers) {
49
70
  const url = BASE_URL + "/org/getOrgV2";
50
- return await postApi(url, "{}");
71
+ return await postApi(url, "{}", headers);
51
72
  }
52
- async function getLocations() {
73
+ async function getLocations(headers) {
53
74
  const url = BASE_URL + "/location/getLocationsV2";
54
- return await postApi(url, "{}");
75
+ console.error("headers:", JSON.stringify(headers));
76
+ return await postApi(url, "{}", headers);
55
77
  }
56
- async function getCameraList() {
78
+ async function getCameraList(headers) {
57
79
  const url = BASE_URL + "/camera/getMinimalCameraStateList";
58
- return await postApi(url, "{}").then(response => {
80
+ return await postApi(url, "{}", headers).then(response => {
59
81
  return {
60
82
  cameraStates: response.cameraStates.filter((camera) => !!camera.locationUuid),
61
83
  };
62
84
  });
63
85
  }
64
- async function getAccessControlledDoors() {
86
+ async function getAccessControlledDoors(headers) {
65
87
  const url = BASE_URL + "/component/findAccessControlledDoors";
66
- return await postApi(url, "{}");
88
+ return await postApi(url, "{}", headers);
67
89
  }
68
- async function getFaceEvents(_locationUuid) {
90
+ async function getFaceEvents(_locationUuid, headers) {
69
91
  const nowMs = Date.now();
70
92
  const rangeStartMs = nowMs - THREE_HOURS_MS;
71
93
  const rangeEndMs = nowMs - FIVE_SECONDS_MS;
@@ -86,7 +108,7 @@ async function getFaceEvents(_locationUuid) {
86
108
  },
87
109
  },
88
110
  });
89
- const response = await postApi(BASE_URL + "/faceRecognition/faceEvent/findFaceEventsByOrg", body).then(response => {
111
+ const response = await postApi(BASE_URL + "/faceRecognition/faceEvent/findFaceEventsByOrg", body, headers).then(response => {
90
112
  return {
91
113
  faceEvents: (response.faceEvents || []).map((event) => ({
92
114
  ...event,
@@ -96,13 +118,13 @@ async function getFaceEvents(_locationUuid) {
96
118
  });
97
119
  return response;
98
120
  }
99
- async function getAccessControlEvents(doorUuid) {
121
+ async function getAccessControlEvents(doorUuid, headers) {
100
122
  const url = BASE_URL + "/component/findComponentEventsByAccessControlledDoor";
101
123
  const body = JSON.stringify({
102
124
  limit: 50,
103
125
  accessControlledDoorUuid: doorUuid,
104
126
  });
105
- const response = await postApi(url, body).then(response => ({
127
+ const response = await postApi(url, body, headers).then(response => ({
106
128
  componentEvents: (response.componentEvents || []).map((event) => ({
107
129
  ...event,
108
130
  timestamp: new Date(event.timestampMs).toString(),
@@ -110,14 +132,14 @@ async function getAccessControlEvents(doorUuid) {
110
132
  }));
111
133
  return response;
112
134
  }
113
- async function rebootCameras(cameraUuids) {
135
+ async function rebootCameras(cameraUuids, headers) {
114
136
  const url = BASE_URL + "/camera/reboot";
115
137
  let successCount = 0;
116
138
  let errorCount = 0;
117
139
  for (const cameraUuid in cameraUuids) {
118
140
  try {
119
141
  const body = JSON.stringify({ cameraUuid: cameraUuid });
120
- const response = await postApi(url, body);
142
+ const response = await postApi(url, body, headers);
121
143
  if (response.error) {
122
144
  errorCount++;
123
145
  }
@@ -139,7 +161,7 @@ async function rebootCameras(cameraUuids) {
139
161
  return { status, successCount, errorCount };
140
162
  }
141
163
  }
142
- async function getImageForCameraAtTime(cameraUuid, timestampMs) {
164
+ async function getImageForCameraAtTime(cameraUuid, timestampMs, headers) {
143
165
  const url = BASE_URL + "/video/getExactFrameUri";
144
166
  const body = JSON.stringify({
145
167
  cameraUuid: cameraUuid,
@@ -151,7 +173,7 @@ async function getImageForCameraAtTime(cameraUuid, timestampMs) {
151
173
  permyriadCropY: 0,
152
174
  timestampMs: timestampMs,
153
175
  });
154
- const base64Image = await postApi(url, body).then(async (res) => {
176
+ const base64Image = await postApi(url, body, headers).then(async (res) => {
155
177
  return await fetch(res.frameUri, { method: "GET", headers: headers }).then(async (res) => {
156
178
  if (!res.ok)
157
179
  return null;
@@ -174,8 +196,8 @@ async function getImageForCameraAtTime(cameraUuid, timestampMs) {
174
196
  imageData: base64Image,
175
197
  };
176
198
  }
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();
199
+ 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 }) => {
200
+ const org = await getOrg(headers);
179
201
  return {
180
202
  content: [
181
203
  {
@@ -189,14 +211,15 @@ server.tool("get-entity-tool", "get a list of entities like cameras, access cont
189
211
  entityType: z
190
212
  .enum(["camera", "access-controlled-doors"])
191
213
  .describe("The entity type to retreive. Example: cameras."),
192
- }, async ({ entityType }) => {
214
+ ...STATIC_ARGS,
215
+ }, async ({ entityType, headers }) => {
193
216
  let ret;
194
217
  switch (entityType) {
195
218
  case "camera":
196
- ret = await getCameraList();
219
+ ret = await getCameraList(headers);
197
220
  break;
198
221
  case "access-controlled-doors":
199
- ret = await getAccessControlledDoors();
222
+ ret = await getAccessControlledDoors(headers);
200
223
  break;
201
224
  default:
202
225
  ret = {};
@@ -211,11 +234,12 @@ server.tool("get-entity-tool", "get a list of entities like cameras, access cont
211
234
  ],
212
235
  };
213
236
  });
214
- server.tool("camera-tool", "get specific requested information about a camera such as an image snapshot, or detailed analytics info", {
237
+ 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
238
  requestType: z.enum(["image"]),
216
239
  timestampMs: z.optional(z.number()).describe("the timestamp in milliseconds"),
217
240
  cameraUuid: z.optional(z.string()).describe("the camera uuid requested"),
218
- }, async ({ cameraUuid, timestampMs, requestType }) => {
241
+ ...STATIC_ARGS,
242
+ }, async ({ cameraUuid, timestampMs, requestType, headers }) => {
219
243
  if (!cameraUuid) {
220
244
  return {
221
245
  content: [
@@ -245,7 +269,7 @@ server.tool("camera-tool", "get specific requested information about a camera su
245
269
  let response;
246
270
  switch (requestType) {
247
271
  case "image":
248
- response = await getImageForCameraAtTime(cameraUuid, timestampMs);
272
+ response = await getImageForCameraAtTime(cameraUuid, timestampMs, headers);
249
273
  if (!response.success || !response.imageData) {
250
274
  return {
251
275
  content: [{ type: "text", text: JSON.stringify(response) }],
@@ -281,9 +305,10 @@ server.tool("events-tool", "event data for certain types of information like fac
281
305
  eventType: z.enum(["faces", "people", "access-control"]),
282
306
  locationUuid: z.optional(z.string()),
283
307
  accessControlledDoorUuid: z.optional(z.string()),
284
- }, async ({ eventType, locationUuid, accessControlledDoorUuid }) => {
308
+ ...STATIC_ARGS,
309
+ }, async ({ eventType, locationUuid, accessControlledDoorUuid, headers }) => {
285
310
  if (eventType === "faces" || eventType === "people") {
286
- const response = await getFaceEvents(locationUuid);
311
+ const response = await getFaceEvents(locationUuid, headers);
287
312
  return {
288
313
  content: [
289
314
  {
@@ -308,7 +333,7 @@ server.tool("events-tool", "event data for certain types of information like fac
308
333
  };
309
334
  }
310
335
  else {
311
- const events = await getAccessControlEvents(accessControlledDoorUuid);
336
+ const events = await getAccessControlEvents(accessControlledDoorUuid, headers);
312
337
  return {
313
338
  content: [
314
339
  {
@@ -331,11 +356,12 @@ server.tool("events-tool", "event data for certain types of information like fac
331
356
  server.tool("location-tool", "contains basic operations for locations and response in JSON format.", {
332
357
  action: z.enum(["get"]),
333
358
  locationUpdate: z.optional(z.object({ uuid: z.string(), name: z.optional(z.string()) })),
334
- }, async ({ action, locationUpdate }) => {
359
+ ...STATIC_ARGS,
360
+ }, async ({ action, locationUpdate, headers }) => {
335
361
  let ret;
336
362
  switch (action) {
337
363
  case "get":
338
- ret = await getLocations();
364
+ ret = await getLocations(headers);
339
365
  break;
340
366
  default:
341
367
  ret = { error: true, status: `unsupported location tool call: ${action}` };
@@ -349,8 +375,9 @@ server.tool("reboot-cameras", "this tool is for rebooting one or more cameras ca
349
375
  cameraUuids: z
350
376
  .array(z.string())
351
377
  .describe("An array of camera UUID strings which are unique identifiers for cameras"),
352
- }, async ({ cameraUuids }) => {
353
- const cameraRebootData = await rebootCameras(cameraUuids);
378
+ ...STATIC_ARGS,
379
+ }, async ({ cameraUuids, headers }) => {
380
+ const cameraRebootData = await rebootCameras(cameraUuids, headers);
354
381
  if (!cameraRebootData) {
355
382
  return {
356
383
  content: [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rhombus-node-mcp",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "MCP server for Rhombus API",
5
5
  "keywords": [
6
6
  "ai",