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.
- package/dist/index.js +74 -40
- 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
|
|
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
|
-
|
|
31
|
-
.optional(z.
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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(
|
|
90
|
+
async function getOrg(requestModifiers) {
|
|
61
91
|
const url = BASE_URL + "/org/getOrgV2";
|
|
62
|
-
return await postApi(url, "{}",
|
|
92
|
+
return await postApi(url, "{}", requestModifiers);
|
|
63
93
|
}
|
|
64
|
-
async function getLocations(
|
|
94
|
+
async function getLocations(requestModifiers) {
|
|
65
95
|
const url = BASE_URL + "/location/getLocationsV2";
|
|
66
|
-
return await postApi(url, "{}",
|
|
96
|
+
return await postApi(url, "{}", requestModifiers);
|
|
67
97
|
}
|
|
68
|
-
async function getCameraList(
|
|
98
|
+
async function getCameraList(requestModifiers) {
|
|
69
99
|
const url = BASE_URL + "/camera/getMinimalCameraStateList";
|
|
70
|
-
return await postApi(url, "{}",
|
|
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(
|
|
106
|
+
async function getAccessControlledDoors(requestModifiers) {
|
|
77
107
|
const url = BASE_URL + "/component/findAccessControlledDoors";
|
|
78
|
-
return await postApi(url, "{}",
|
|
108
|
+
return await postApi(url, "{}", requestModifiers);
|
|
79
109
|
}
|
|
80
|
-
async function getFaceEvents(_locationUuid,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
167
|
-
|
|
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 ({
|
|
190
|
-
const org = await getOrg(
|
|
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,
|
|
239
|
+
}, async ({ entityType, requestModifiers }) => {
|
|
206
240
|
let ret;
|
|
207
241
|
switch (entityType) {
|
|
208
242
|
case "camera":
|
|
209
|
-
ret = await getCameraList(
|
|
243
|
+
ret = await getCameraList(requestModifiers);
|
|
210
244
|
break;
|
|
211
245
|
case "access-controlled-doors":
|
|
212
|
-
ret = await getAccessControlledDoors(
|
|
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,
|
|
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,
|
|
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,
|
|
333
|
+
}, async ({ eventType, locationUuid, accessControlledDoorUuid, requestModifiers }) => {
|
|
300
334
|
if (eventType === "faces" || eventType === "people") {
|
|
301
|
-
const response = await getFaceEvents(locationUuid,
|
|
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,
|
|
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,
|
|
384
|
+
}, async ({ action, locationUpdate, requestModifiers }) => {
|
|
351
385
|
let ret;
|
|
352
386
|
switch (action) {
|
|
353
387
|
case "get":
|
|
354
|
-
ret = await getLocations(
|
|
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,
|
|
370
|
-
const cameraRebootData = await rebootCameras(cameraUuids,
|
|
403
|
+
}, async ({ cameraUuids, requestModifiers }) => {
|
|
404
|
+
const cameraRebootData = await rebootCameras(cameraUuids, requestModifiers);
|
|
371
405
|
if (!cameraRebootData) {
|
|
372
406
|
return {
|
|
373
407
|
content: [
|