rhombus-node-mcp 0.1.9 → 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 +129 -4
- 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;
|
|
@@ -69,6 +72,7 @@ async function postApi(url, body, modifiers) {
|
|
|
69
72
|
const response = await fetch(url, { method: "POST", headers: requestHeaders, body });
|
|
70
73
|
log(`[POSTAPI] RESPONSE - ${JSON.stringify(response)}`);
|
|
71
74
|
if (!response.ok) {
|
|
75
|
+
log(`❌ RESPONSE - ${response.ok} - ${response.status}`);
|
|
72
76
|
if (response.status === 401 || response.status === 403) {
|
|
73
77
|
return {
|
|
74
78
|
error: true,
|
|
@@ -77,7 +81,9 @@ async function postApi(url, body, modifiers) {
|
|
|
77
81
|
}
|
|
78
82
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
79
83
|
}
|
|
80
|
-
|
|
84
|
+
const ret = await response.json();
|
|
85
|
+
log(`❌ RESPONSE - ${response.ok} - ${JSON.stringify(ret)}`);
|
|
86
|
+
return ret;
|
|
81
87
|
}
|
|
82
88
|
catch (error) {
|
|
83
89
|
log(`[POSTAPI] ERROR - ${JSON.stringify(error || {})}`);
|
|
@@ -181,6 +187,25 @@ async function rebootCameras(cameraUuids, requestModifiers) {
|
|
|
181
187
|
return { status, successCount, errorCount };
|
|
182
188
|
}
|
|
183
189
|
}
|
|
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
|
+
}
|
|
184
209
|
async function getImageForCameraAtTime(cameraUuid, timestampMs, requestModifiers) {
|
|
185
210
|
const url = BASE_URL + "/video/getExactFrameUri";
|
|
186
211
|
const body = JSON.stringify({
|
|
@@ -231,6 +256,104 @@ server.tool("get-org-information", "Get general information about the organizati
|
|
|
231
256
|
],
|
|
232
257
|
};
|
|
233
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
|
+
});
|
|
234
357
|
server.tool("get-entity-tool", "get a list of entities like cameras, access controlled doors, sensors, etc", {
|
|
235
358
|
entityType: z
|
|
236
359
|
.enum(["camera", "access-controlled-doors"])
|
|
@@ -258,9 +381,11 @@ server.tool("get-entity-tool", "get a list of entities like cameras, access cont
|
|
|
258
381
|
],
|
|
259
382
|
};
|
|
260
383
|
});
|
|
261
|
-
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.', {
|
|
262
385
|
requestType: z.enum(["image"]),
|
|
263
|
-
timestampMs: z
|
|
386
|
+
timestampMs: z
|
|
387
|
+
.optional(z.number())
|
|
388
|
+
.describe("the timestamp in milliseconds which should always be obtained using time-tool"),
|
|
264
389
|
cameraUuid: z.optional(z.string()).describe("the camera uuid requested"),
|
|
265
390
|
...STATIC_ARGS,
|
|
266
391
|
}, async ({ cameraUuid, timestampMs, requestType, requestModifiers }) => {
|
|
@@ -378,7 +503,7 @@ server.tool("events-tool", "event data for certain types of information like fac
|
|
|
378
503
|
};
|
|
379
504
|
});
|
|
380
505
|
server.tool("location-tool", "contains basic operations for locations and response in JSON format.", {
|
|
381
|
-
action: z.enum(["get"]),
|
|
506
|
+
action: z.enum(["get", "update"]),
|
|
382
507
|
locationUpdate: z.optional(z.object({ uuid: z.string(), name: z.optional(z.string()) })),
|
|
383
508
|
...STATIC_ARGS,
|
|
384
509
|
}, async ({ action, locationUpdate, requestModifiers }) => {
|
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"
|