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.
Files changed (2) hide show
  1. package/dist/index.js +129 -4
  2. 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
- return await response.json();
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", "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", {
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.optional(z.number()).describe("the timestamp in milliseconds"),
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.9",
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"