rhombus-node-mcp 0.1.4 → 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.
- package/dist/index.js +62 -35
- 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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
353
|
-
|
|
378
|
+
...STATIC_ARGS,
|
|
379
|
+
}, async ({ cameraUuids, headers }) => {
|
|
380
|
+
const cameraRebootData = await rebootCameras(cameraUuids, headers);
|
|
354
381
|
if (!cameraRebootData) {
|
|
355
382
|
return {
|
|
356
383
|
content: [
|