rhombus-node-mcp 0.1.8 → 0.1.10
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 +197 -45
- package/package.json +4 -1
package/dist/index.js
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
+
import { CreateVideoWallOptions } from "./types.js";
|
|
6
|
+
import { parse } from "chrono-node";
|
|
7
|
+
import { DateTime } from "luxon";
|
|
5
8
|
const THREE_HOURS_MS = 3 * 60 * 60 * 1000;
|
|
6
9
|
const FIVE_SECONDS_MS = 5 * 1000;
|
|
7
10
|
const RHOMBUS_API_KEY = process.env.RHOMBUS_API_KEY;
|
|
@@ -9,15 +12,17 @@ if (!RHOMBUS_API_KEY) {
|
|
|
9
12
|
console.error("Missing RHOMBUS_API_KEY");
|
|
10
13
|
}
|
|
11
14
|
const enableLogs = process.env.ENABLE_LOGS;
|
|
12
|
-
const
|
|
15
|
+
const serverUrl = process.env.RHOMBUS_API_SERVER || "api2.rhombussystems.com";
|
|
16
|
+
console.error("🌐 Using server url", serverUrl);
|
|
17
|
+
const BASE_URL = `https://${serverUrl}/api`;
|
|
13
18
|
const STATIC_HEADERS = {
|
|
14
19
|
"Content-Type": "application/json",
|
|
20
|
+
"x-rhombus-agent": "chatbot",
|
|
15
21
|
accept: "application/json",
|
|
16
22
|
};
|
|
17
23
|
const AUTH_HEADERS = {
|
|
18
24
|
"x-auth-apikey": RHOMBUS_API_KEY,
|
|
19
25
|
"x-auth-scheme": "api-token",
|
|
20
|
-
"x-rhombus-agent": "chatbot",
|
|
21
26
|
};
|
|
22
27
|
const server = new McpServer({
|
|
23
28
|
name: "rhombus",
|
|
@@ -28,8 +33,11 @@ const server = new McpServer({
|
|
|
28
33
|
},
|
|
29
34
|
});
|
|
30
35
|
const STATIC_ARGS = {
|
|
31
|
-
|
|
32
|
-
.optional(z.
|
|
36
|
+
requestModifiers: z
|
|
37
|
+
.optional(z.object({
|
|
38
|
+
headers: z.optional(z.any()),
|
|
39
|
+
query: z.optional(z.any()),
|
|
40
|
+
}))
|
|
33
41
|
.describe("Optional headers accepted by tools. LLM should never ever use this. 😅"),
|
|
34
42
|
};
|
|
35
43
|
const log = (msg) => {
|
|
@@ -37,16 +45,34 @@ const log = (msg) => {
|
|
|
37
45
|
return;
|
|
38
46
|
console.error(msg);
|
|
39
47
|
};
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
48
|
+
const appendQueryParams = (url, params) => {
|
|
49
|
+
if (!params || typeof params !== "object")
|
|
50
|
+
return url;
|
|
51
|
+
const urlObj = new URL(url);
|
|
52
|
+
const existingSearchParams = new URLSearchParams(urlObj.search);
|
|
53
|
+
for (const [key, value] of Object.entries(params)) {
|
|
54
|
+
if (value !== undefined && value !== null) {
|
|
55
|
+
existingSearchParams.append(key, String(value));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const baseUrl = url.split("?")[0];
|
|
59
|
+
const queryString = existingSearchParams.toString();
|
|
60
|
+
return queryString ? `${baseUrl}?${queryString}` : baseUrl;
|
|
61
|
+
};
|
|
62
|
+
async function postApi(url, body, modifiers) {
|
|
63
|
+
let requestHeaders = {
|
|
64
|
+
...(modifiers?.headers || AUTH_HEADERS),
|
|
43
65
|
...STATIC_HEADERS,
|
|
44
66
|
};
|
|
67
|
+
if (modifiers?.query) {
|
|
68
|
+
url = appendQueryParams(url, modifiers.query);
|
|
69
|
+
}
|
|
45
70
|
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
|
|
71
|
+
log(`[POSTAPI] REQUEST - ${url} - ${body} - ${JSON.stringify(requestHeaders)}`);
|
|
72
|
+
const response = await fetch(url, { method: "POST", headers: requestHeaders, body });
|
|
73
|
+
log(`[POSTAPI] RESPONSE - ${JSON.stringify(response)}`);
|
|
49
74
|
if (!response.ok) {
|
|
75
|
+
log(`❌ RESPONSE - ${response.ok} - ${response.status}`);
|
|
50
76
|
if (response.status === 401 || response.status === 403) {
|
|
51
77
|
return {
|
|
52
78
|
error: true,
|
|
@@ -55,36 +81,39 @@ async function postApi(url, body, customHeaders) {
|
|
|
55
81
|
}
|
|
56
82
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
57
83
|
}
|
|
58
|
-
|
|
84
|
+
const ret = await response.json();
|
|
85
|
+
log(`❌ RESPONSE - ${response.ok} - ${JSON.stringify(ret)}`);
|
|
86
|
+
return ret;
|
|
59
87
|
}
|
|
60
88
|
catch (error) {
|
|
89
|
+
log(`[POSTAPI] ERROR - ${JSON.stringify(error || {})}`);
|
|
61
90
|
return {
|
|
62
91
|
error: true,
|
|
63
92
|
status: `Request Error: ${error}`,
|
|
64
93
|
};
|
|
65
94
|
}
|
|
66
95
|
}
|
|
67
|
-
async function getOrg(
|
|
96
|
+
async function getOrg(requestModifiers) {
|
|
68
97
|
const url = BASE_URL + "/org/getOrgV2";
|
|
69
|
-
return await postApi(url, "{}",
|
|
98
|
+
return await postApi(url, "{}", requestModifiers);
|
|
70
99
|
}
|
|
71
|
-
async function getLocations(
|
|
100
|
+
async function getLocations(requestModifiers) {
|
|
72
101
|
const url = BASE_URL + "/location/getLocationsV2";
|
|
73
|
-
return await postApi(url, "{}",
|
|
102
|
+
return await postApi(url, "{}", requestModifiers);
|
|
74
103
|
}
|
|
75
|
-
async function getCameraList(
|
|
104
|
+
async function getCameraList(requestModifiers) {
|
|
76
105
|
const url = BASE_URL + "/camera/getMinimalCameraStateList";
|
|
77
|
-
return await postApi(url, "{}",
|
|
106
|
+
return await postApi(url, "{}", requestModifiers).then(response => {
|
|
78
107
|
return {
|
|
79
108
|
cameraStates: response.cameraStates.filter((camera) => !!camera.locationUuid),
|
|
80
109
|
};
|
|
81
110
|
});
|
|
82
111
|
}
|
|
83
|
-
async function getAccessControlledDoors(
|
|
112
|
+
async function getAccessControlledDoors(requestModifiers) {
|
|
84
113
|
const url = BASE_URL + "/component/findAccessControlledDoors";
|
|
85
|
-
return await postApi(url, "{}",
|
|
114
|
+
return await postApi(url, "{}", requestModifiers);
|
|
86
115
|
}
|
|
87
|
-
async function getFaceEvents(_locationUuid,
|
|
116
|
+
async function getFaceEvents(_locationUuid, requestModifiers) {
|
|
88
117
|
const nowMs = Date.now();
|
|
89
118
|
const rangeStartMs = nowMs - THREE_HOURS_MS;
|
|
90
119
|
const rangeEndMs = nowMs - FIVE_SECONDS_MS;
|
|
@@ -105,7 +134,7 @@ async function getFaceEvents(_locationUuid, headers) {
|
|
|
105
134
|
},
|
|
106
135
|
},
|
|
107
136
|
});
|
|
108
|
-
const response = await postApi(BASE_URL + "/faceRecognition/faceEvent/findFaceEventsByOrg", body,
|
|
137
|
+
const response = await postApi(BASE_URL + "/faceRecognition/faceEvent/findFaceEventsByOrg", body, requestModifiers).then(response => {
|
|
109
138
|
return {
|
|
110
139
|
faceEvents: (response.faceEvents || []).map((event) => ({
|
|
111
140
|
...event,
|
|
@@ -115,13 +144,13 @@ async function getFaceEvents(_locationUuid, headers) {
|
|
|
115
144
|
});
|
|
116
145
|
return response;
|
|
117
146
|
}
|
|
118
|
-
async function getAccessControlEvents(doorUuid,
|
|
147
|
+
async function getAccessControlEvents(doorUuid, requestModifiers) {
|
|
119
148
|
const url = BASE_URL + "/component/findComponentEventsByAccessControlledDoor";
|
|
120
149
|
const body = JSON.stringify({
|
|
121
150
|
limit: 50,
|
|
122
151
|
accessControlledDoorUuid: doorUuid,
|
|
123
152
|
});
|
|
124
|
-
const response = await postApi(url, body,
|
|
153
|
+
const response = await postApi(url, body, requestModifiers).then(response => ({
|
|
125
154
|
componentEvents: (response.componentEvents || []).map((event) => ({
|
|
126
155
|
...event,
|
|
127
156
|
timestamp: new Date(event.timestampMs).toString(),
|
|
@@ -129,14 +158,14 @@ async function getAccessControlEvents(doorUuid, headers) {
|
|
|
129
158
|
}));
|
|
130
159
|
return response;
|
|
131
160
|
}
|
|
132
|
-
async function rebootCameras(cameraUuids,
|
|
161
|
+
async function rebootCameras(cameraUuids, requestModifiers) {
|
|
133
162
|
const url = BASE_URL + "/camera/reboot";
|
|
134
163
|
let successCount = 0;
|
|
135
164
|
let errorCount = 0;
|
|
136
165
|
for (const cameraUuid in cameraUuids) {
|
|
137
166
|
try {
|
|
138
167
|
const body = JSON.stringify({ cameraUuid: cameraUuid });
|
|
139
|
-
const response = await postApi(url, body,
|
|
168
|
+
const response = await postApi(url, body, requestModifiers);
|
|
140
169
|
if (response.error) {
|
|
141
170
|
errorCount++;
|
|
142
171
|
}
|
|
@@ -158,7 +187,26 @@ async function rebootCameras(cameraUuids, headers) {
|
|
|
158
187
|
return { status, successCount, errorCount };
|
|
159
188
|
}
|
|
160
189
|
}
|
|
161
|
-
async function
|
|
190
|
+
async function createVideoWall(options, headers) {
|
|
191
|
+
const url = BASE_URL + "/camera/createVideoWall";
|
|
192
|
+
const body = JSON.stringify({
|
|
193
|
+
videoWall: {
|
|
194
|
+
displayName: options?.displayName,
|
|
195
|
+
deviceList: options?.deviceList,
|
|
196
|
+
othersCanEdit: true,
|
|
197
|
+
orgUuid: options?.orgUuid,
|
|
198
|
+
shared: true,
|
|
199
|
+
settings: {
|
|
200
|
+
gridSize: { width: options?.settings.columnCount, height: options?.settings.columnCount },
|
|
201
|
+
gridLayout: "1 2\n3 4",
|
|
202
|
+
intervalSeconds: options?.settings.intervalSeconds || 5,
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
const response = await postApi(url, body, headers);
|
|
207
|
+
return response;
|
|
208
|
+
}
|
|
209
|
+
async function getImageForCameraAtTime(cameraUuid, timestampMs, requestModifiers) {
|
|
162
210
|
const url = BASE_URL + "/video/getExactFrameUri";
|
|
163
211
|
const body = JSON.stringify({
|
|
164
212
|
cameraUuid: cameraUuid,
|
|
@@ -170,8 +218,12 @@ async function getImageForCameraAtTime(cameraUuid, timestampMs, headers) {
|
|
|
170
218
|
permyriadCropY: 0,
|
|
171
219
|
timestampMs: timestampMs,
|
|
172
220
|
});
|
|
173
|
-
const base64Image = await postApi(url, body,
|
|
174
|
-
|
|
221
|
+
const base64Image = await postApi(url, body, requestModifiers).then(async (res) => {
|
|
222
|
+
let requestHeaders = {
|
|
223
|
+
...(requestModifiers?.headers || AUTH_HEADERS),
|
|
224
|
+
...STATIC_HEADERS,
|
|
225
|
+
};
|
|
226
|
+
return await fetch(res.frameUri, { method: "GET", headers: requestHeaders }).then(async (res) => {
|
|
175
227
|
if (!res.ok)
|
|
176
228
|
return null;
|
|
177
229
|
const arrayBuffer = await res.arrayBuffer();
|
|
@@ -193,8 +245,8 @@ async function getImageForCameraAtTime(cameraUuid, timestampMs, headers) {
|
|
|
193
245
|
imageData: base64Image,
|
|
194
246
|
};
|
|
195
247
|
}
|
|
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(
|
|
248
|
+
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 }) => {
|
|
249
|
+
const org = await getOrg(requestModifiers);
|
|
198
250
|
return {
|
|
199
251
|
content: [
|
|
200
252
|
{
|
|
@@ -204,19 +256,117 @@ server.tool("get-org-information", "Get general information about the organizati
|
|
|
204
256
|
],
|
|
205
257
|
};
|
|
206
258
|
});
|
|
259
|
+
async function handleCreateVideoWallRequest(videoWallCreateOptions, headers) {
|
|
260
|
+
let text = "Unable to create video wall!";
|
|
261
|
+
// console.error("🔨 Creating video wall");
|
|
262
|
+
if (!videoWallCreateOptions?.displayName) {
|
|
263
|
+
text = JSON.stringify({
|
|
264
|
+
needUserInput: true,
|
|
265
|
+
commandForUser: "What should the name of the video wall be?",
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
else if ((videoWallCreateOptions?.deviceList || []).length === 0) {
|
|
269
|
+
text = JSON.stringify({
|
|
270
|
+
needUserInput: true,
|
|
271
|
+
commandForUser: "Which cameras would you like on this video wall?",
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
// console.error("Creating video wall with options: ", JSON.stringify(videoWallCreateOptions));
|
|
276
|
+
text = JSON.stringify(await createVideoWall(videoWallCreateOptions, headers));
|
|
277
|
+
}
|
|
278
|
+
return Promise.resolve({
|
|
279
|
+
content: [
|
|
280
|
+
{
|
|
281
|
+
type: "text",
|
|
282
|
+
text,
|
|
283
|
+
},
|
|
284
|
+
],
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
server.tool("create-tool", "Tool for creating many entity types such as video walls.", {
|
|
288
|
+
entityType: z.enum(["video-wall"]).describe("The entity type to create. Example: video wall."),
|
|
289
|
+
videoWallCreateOptions: CreateVideoWallOptions,
|
|
290
|
+
...STATIC_ARGS,
|
|
291
|
+
}, async ({ entityType, videoWallCreateOptions, requestModifiers }) => {
|
|
292
|
+
switch (entityType) {
|
|
293
|
+
case "video-wall":
|
|
294
|
+
return await handleCreateVideoWallRequest(videoWallCreateOptions, requestModifiers);
|
|
295
|
+
default:
|
|
296
|
+
}
|
|
297
|
+
return {
|
|
298
|
+
content: [
|
|
299
|
+
{
|
|
300
|
+
type: "text",
|
|
301
|
+
text: "",
|
|
302
|
+
},
|
|
303
|
+
],
|
|
304
|
+
};
|
|
305
|
+
});
|
|
306
|
+
function nullToUndefined(value) {
|
|
307
|
+
return value === null ? undefined : value;
|
|
308
|
+
}
|
|
309
|
+
server.tool("time-tool", "Tool for converting a natural language time description into a timestamp in milliseconds.", {
|
|
310
|
+
time_description: z
|
|
311
|
+
.string()
|
|
312
|
+
.describe("A natural language description of the time (e.g., '2pm today', 'tomorrow at noon')."),
|
|
313
|
+
timezone: z
|
|
314
|
+
.optional(z.string())
|
|
315
|
+
.describe("Optional IANA timezone string (e.g., 'America/Los_Angeles', 'UTC'). Defaults to system timezone."),
|
|
316
|
+
}, async ({ time_description, timezone }) => {
|
|
317
|
+
// console.error(`🕛 handling tool call for time ${time_description} using timezone ${timezone}`);
|
|
318
|
+
const now = DateTime.now()
|
|
319
|
+
.setZone(timezone || undefined)
|
|
320
|
+
.toJSDate();
|
|
321
|
+
const parsed = parse(time_description, now, { forwardDate: true });
|
|
322
|
+
if (!parsed || parsed.length === 0) {
|
|
323
|
+
throw new Error(`Could not parse time description: ${time_description}`);
|
|
324
|
+
}
|
|
325
|
+
const dateComponents = parsed[0].start;
|
|
326
|
+
if (!dateComponents) {
|
|
327
|
+
throw new Error("Parsed time has no start component");
|
|
328
|
+
}
|
|
329
|
+
const dt = DateTime.fromObject({
|
|
330
|
+
year: nullToUndefined(dateComponents.get("year")),
|
|
331
|
+
month: nullToUndefined(dateComponents.get("month")),
|
|
332
|
+
day: nullToUndefined(dateComponents.get("day")),
|
|
333
|
+
hour: nullToUndefined(dateComponents.get("hour")),
|
|
334
|
+
minute: nullToUndefined(dateComponents.get("minute")),
|
|
335
|
+
second: nullToUndefined(dateComponents.get("second")),
|
|
336
|
+
millisecond: 0,
|
|
337
|
+
}, {
|
|
338
|
+
zone: timezone || "local",
|
|
339
|
+
});
|
|
340
|
+
if (!dt.isValid) {
|
|
341
|
+
throw new Error(`Could not construct valid DateTime: ${dt.invalidReason}`);
|
|
342
|
+
}
|
|
343
|
+
const timestamp = dt.toMillis();
|
|
344
|
+
return {
|
|
345
|
+
content: [
|
|
346
|
+
{
|
|
347
|
+
type: "text",
|
|
348
|
+
text: JSON.stringify({
|
|
349
|
+
timestamp,
|
|
350
|
+
iso: dt.toISO(),
|
|
351
|
+
timezone: dt.zoneName,
|
|
352
|
+
}),
|
|
353
|
+
},
|
|
354
|
+
],
|
|
355
|
+
};
|
|
356
|
+
});
|
|
207
357
|
server.tool("get-entity-tool", "get a list of entities like cameras, access controlled doors, sensors, etc", {
|
|
208
358
|
entityType: z
|
|
209
359
|
.enum(["camera", "access-controlled-doors"])
|
|
210
360
|
.describe("The entity type to retreive. Example: cameras."),
|
|
211
361
|
...STATIC_ARGS,
|
|
212
|
-
}, async ({ entityType,
|
|
362
|
+
}, async ({ entityType, requestModifiers }) => {
|
|
213
363
|
let ret;
|
|
214
364
|
switch (entityType) {
|
|
215
365
|
case "camera":
|
|
216
|
-
ret = await getCameraList(
|
|
366
|
+
ret = await getCameraList(requestModifiers);
|
|
217
367
|
break;
|
|
218
368
|
case "access-controlled-doors":
|
|
219
|
-
ret = await getAccessControlledDoors(
|
|
369
|
+
ret = await getAccessControlledDoors(requestModifiers);
|
|
220
370
|
break;
|
|
221
371
|
default:
|
|
222
372
|
ret = {};
|
|
@@ -231,12 +381,14 @@ server.tool("get-entity-tool", "get a list of entities like cameras, access cont
|
|
|
231
381
|
],
|
|
232
382
|
};
|
|
233
383
|
});
|
|
234
|
-
server.tool("camera-tool",
|
|
384
|
+
server.tool("camera-tool", 'This tool captures and returns a real-time snapshot from a designated security camera. The image reflects the current scene in the camera\'s field of view and serves as a contextual input source for downstream tasks such as object recognition, anomaly detection, incident investigation, or situational assessment. When invoked, the tool provides the following: \n • Visual Scene Capture: A high-resolution image of what the camera is actively observing, including people, vehicles, license plates, and any detectable objects. \n• Scene Context: Metadata such as camera ID, location name, timestamp, and motion detection status if available. \n• Enriched Data Potential: The image can be paired with AI models or downstream analytics to extract insights such as: \n• Number and type of objects in frame (e.g., humans, cars, packages) \n• Unusual behaviors (e.g., loitering, unauthorized access) \n• Environmental conditions (e.g., lighting, obstruction, cleanliness) \nUse Cases: \n• Verify what triggered a motion alert or analytic rule. \n• Provide visual context for access events or alarms. \n• Support live incident triage or retrospective investigations. \n• Feed contextual imagery to agents making security or operational decisions. \nInvocation Notes: \nTo use this tool correctly, the agent should provide the specific camera identifier or location name. If possible, include the intent (e.g., "verify unauthorized access", "identify vehicle", "check for obstructions") to enhance downstream processing or summarization.', {
|
|
235
385
|
requestType: z.enum(["image"]),
|
|
236
|
-
timestampMs: z
|
|
386
|
+
timestampMs: z
|
|
387
|
+
.optional(z.number())
|
|
388
|
+
.describe("the timestamp in milliseconds which should always be obtained using time-tool"),
|
|
237
389
|
cameraUuid: z.optional(z.string()).describe("the camera uuid requested"),
|
|
238
390
|
...STATIC_ARGS,
|
|
239
|
-
}, async ({ cameraUuid, timestampMs, requestType,
|
|
391
|
+
}, async ({ cameraUuid, timestampMs, requestType, requestModifiers }) => {
|
|
240
392
|
if (!cameraUuid) {
|
|
241
393
|
return {
|
|
242
394
|
content: [
|
|
@@ -266,7 +418,7 @@ server.tool("camera-tool", "get specific requested information about a camera su
|
|
|
266
418
|
let response;
|
|
267
419
|
switch (requestType) {
|
|
268
420
|
case "image":
|
|
269
|
-
response = await getImageForCameraAtTime(cameraUuid, timestampMs,
|
|
421
|
+
response = await getImageForCameraAtTime(cameraUuid, timestampMs, requestModifiers);
|
|
270
422
|
if (!response.success || !response.imageData) {
|
|
271
423
|
return {
|
|
272
424
|
content: [{ type: "text", text: JSON.stringify(response) }],
|
|
@@ -303,9 +455,9 @@ server.tool("events-tool", "event data for certain types of information like fac
|
|
|
303
455
|
locationUuid: z.optional(z.string()),
|
|
304
456
|
accessControlledDoorUuid: z.optional(z.string()),
|
|
305
457
|
...STATIC_ARGS,
|
|
306
|
-
}, async ({ eventType, locationUuid, accessControlledDoorUuid,
|
|
458
|
+
}, async ({ eventType, locationUuid, accessControlledDoorUuid, requestModifiers }) => {
|
|
307
459
|
if (eventType === "faces" || eventType === "people") {
|
|
308
|
-
const response = await getFaceEvents(locationUuid,
|
|
460
|
+
const response = await getFaceEvents(locationUuid, requestModifiers);
|
|
309
461
|
return {
|
|
310
462
|
content: [
|
|
311
463
|
{
|
|
@@ -330,7 +482,7 @@ server.tool("events-tool", "event data for certain types of information like fac
|
|
|
330
482
|
};
|
|
331
483
|
}
|
|
332
484
|
else {
|
|
333
|
-
const events = await getAccessControlEvents(accessControlledDoorUuid,
|
|
485
|
+
const events = await getAccessControlEvents(accessControlledDoorUuid, requestModifiers);
|
|
334
486
|
return {
|
|
335
487
|
content: [
|
|
336
488
|
{
|
|
@@ -351,14 +503,14 @@ server.tool("events-tool", "event data for certain types of information like fac
|
|
|
351
503
|
};
|
|
352
504
|
});
|
|
353
505
|
server.tool("location-tool", "contains basic operations for locations and response in JSON format.", {
|
|
354
|
-
action: z.enum(["get"]),
|
|
506
|
+
action: z.enum(["get", "update"]),
|
|
355
507
|
locationUpdate: z.optional(z.object({ uuid: z.string(), name: z.optional(z.string()) })),
|
|
356
508
|
...STATIC_ARGS,
|
|
357
|
-
}, async ({ action, locationUpdate,
|
|
509
|
+
}, async ({ action, locationUpdate, requestModifiers }) => {
|
|
358
510
|
let ret;
|
|
359
511
|
switch (action) {
|
|
360
512
|
case "get":
|
|
361
|
-
ret = await getLocations(
|
|
513
|
+
ret = await getLocations(requestModifiers);
|
|
362
514
|
break;
|
|
363
515
|
default:
|
|
364
516
|
ret = { error: true, status: `unsupported location tool call: ${action}` };
|
|
@@ -373,8 +525,8 @@ server.tool("reboot-cameras", "this tool is for rebooting one or more cameras ca
|
|
|
373
525
|
.array(z.string())
|
|
374
526
|
.describe("An array of camera UUID strings which are unique identifiers for cameras"),
|
|
375
527
|
...STATIC_ARGS,
|
|
376
|
-
}, async ({ cameraUuids,
|
|
377
|
-
const cameraRebootData = await rebootCameras(cameraUuids,
|
|
528
|
+
}, async ({ cameraUuids, requestModifiers }) => {
|
|
529
|
+
const cameraRebootData = await rebootCameras(cameraUuids, requestModifiers);
|
|
378
530
|
if (!cameraRebootData) {
|
|
379
531
|
return {
|
|
380
532
|
content: [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rhombus-node-mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"description": "MCP server for Rhombus API",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -33,9 +33,12 @@
|
|
|
33
33
|
],
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@modelcontextprotocol/sdk": "^1.9.0",
|
|
36
|
+
"chrono-node": "^2.8.0",
|
|
37
|
+
"luxon": "^3.6.1",
|
|
36
38
|
"zod": "^3.24.2"
|
|
37
39
|
},
|
|
38
40
|
"devDependencies": {
|
|
41
|
+
"@types/luxon": "^3.6.2",
|
|
39
42
|
"@types/node": "^22.14.0",
|
|
40
43
|
"prettier": "3.5.3",
|
|
41
44
|
"typescript": "^5.8.3"
|