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.
- package/dist/index.js +68 -41
- 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
|
|
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
|
-
|
|
32
|
-
.optional(z.
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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(
|
|
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(
|
|
90
|
+
async function getOrg(requestModifiers) {
|
|
68
91
|
const url = BASE_URL + "/org/getOrgV2";
|
|
69
|
-
return await postApi(url, "{}",
|
|
92
|
+
return await postApi(url, "{}", requestModifiers);
|
|
70
93
|
}
|
|
71
|
-
async function getLocations(
|
|
94
|
+
async function getLocations(requestModifiers) {
|
|
72
95
|
const url = BASE_URL + "/location/getLocationsV2";
|
|
73
|
-
return await postApi(url, "{}",
|
|
96
|
+
return await postApi(url, "{}", requestModifiers);
|
|
74
97
|
}
|
|
75
|
-
async function getCameraList(
|
|
98
|
+
async function getCameraList(requestModifiers) {
|
|
76
99
|
const url = BASE_URL + "/camera/getMinimalCameraStateList";
|
|
77
|
-
return await postApi(url, "{}",
|
|
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(
|
|
106
|
+
async function getAccessControlledDoors(requestModifiers) {
|
|
84
107
|
const url = BASE_URL + "/component/findAccessControlledDoors";
|
|
85
|
-
return await postApi(url, "{}",
|
|
108
|
+
return await postApi(url, "{}", requestModifiers);
|
|
86
109
|
}
|
|
87
|
-
async function getFaceEvents(_locationUuid,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
174
|
-
|
|
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 ({
|
|
197
|
-
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);
|
|
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,
|
|
239
|
+
}, async ({ entityType, requestModifiers }) => {
|
|
213
240
|
let ret;
|
|
214
241
|
switch (entityType) {
|
|
215
242
|
case "camera":
|
|
216
|
-
ret = await getCameraList(
|
|
243
|
+
ret = await getCameraList(requestModifiers);
|
|
217
244
|
break;
|
|
218
245
|
case "access-controlled-doors":
|
|
219
|
-
ret = await getAccessControlledDoors(
|
|
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,
|
|
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,
|
|
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,
|
|
333
|
+
}, async ({ eventType, locationUuid, accessControlledDoorUuid, requestModifiers }) => {
|
|
307
334
|
if (eventType === "faces" || eventType === "people") {
|
|
308
|
-
const response = await getFaceEvents(locationUuid,
|
|
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,
|
|
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,
|
|
384
|
+
}, async ({ action, locationUpdate, requestModifiers }) => {
|
|
358
385
|
let ret;
|
|
359
386
|
switch (action) {
|
|
360
387
|
case "get":
|
|
361
|
-
ret = await getLocations(
|
|
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,
|
|
377
|
-
const cameraRebootData = await rebootCameras(cameraUuids,
|
|
403
|
+
}, async ({ cameraUuids, requestModifiers }) => {
|
|
404
|
+
const cameraRebootData = await rebootCameras(cameraUuids, requestModifiers);
|
|
378
405
|
if (!cameraRebootData) {
|
|
379
406
|
return {
|
|
380
407
|
content: [
|