rhombus-node-mcp 0.1.11 → 0.1.13
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/README.md +40 -1
- package/dist/constants.js +2 -0
- package/dist/index.js +23 -534
- package/dist/logger.js +27 -0
- package/dist/network.js +81 -0
- package/dist/resources/getResources.js +19 -0
- package/dist/resources/llms.pdf.js +19 -0
- package/dist/resources/routes.json.js +31 -0
- package/dist/tools/clips-tool.js +62 -0
- package/dist/tools/create-tool.js +80 -0
- package/dist/tools/devices/camera-tool/camera-tool.js +218 -0
- package/dist/tools/devices/camera-tool/types.js +172 -0
- package/dist/tools/devices/get-entity-tool.js +117 -0
- package/dist/tools/events-tool.js +101 -0
- package/dist/tools/faces-tool.js +140 -0
- package/dist/tools/get-org-information.js +18 -0
- package/dist/tools/getTools.js +22 -0
- package/dist/tools/location-tool.js +34 -0
- package/dist/tools/policy-alerts-tool.js +57 -0
- package/dist/tools/reboot-cameras.js +63 -0
- package/dist/tools/time-tool.js +57 -0
- package/dist/types/deviceType.js +14 -0
- package/dist/types.js +16 -13
- package/dist/util.js +111 -0
- package/dist/utils/confirmation.js +45 -0
- package/package.json +3 -1
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { createToolArgs } from "../util.js";
|
|
3
|
+
import { postApi } from "../network.js";
|
|
4
|
+
import { addConfirmationParams, isConfirmed, requireConfirmation } from "../utils/confirmation.js";
|
|
5
|
+
async function rebootCameras(cameraUuids, requestModifiers) {
|
|
6
|
+
let successCount = 0;
|
|
7
|
+
let errorCount = 0;
|
|
8
|
+
for (const cameraUuid in cameraUuids) {
|
|
9
|
+
try {
|
|
10
|
+
const body = { cameraUuid };
|
|
11
|
+
const response = await postApi("/camera/reboot", body, requestModifiers);
|
|
12
|
+
if (response.error) {
|
|
13
|
+
errorCount++;
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
successCount++;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
const ret = `Error rebooting cameras: ${error}`;
|
|
21
|
+
return { error: true, status: ret };
|
|
22
|
+
}
|
|
23
|
+
let status;
|
|
24
|
+
if (successCount === cameraUuids.length)
|
|
25
|
+
status = "SUCCESS";
|
|
26
|
+
else if (successCount > 0 && successCount < cameraUuids.length)
|
|
27
|
+
status = "PARTIAL_SUCCESS";
|
|
28
|
+
else
|
|
29
|
+
status = "ERROR";
|
|
30
|
+
return { status, successCount, errorCount };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export function createTool(server) {
|
|
34
|
+
server.tool("reboot-cameras", "this tool is for rebooting one or more cameras causing them to reconnect to the server, this is a helpful option when a camera is experiencing connectivity issues or is in need of troubleshooting. THIS TOOL PERFORMS AN ACTION.", addConfirmationParams(createToolArgs({
|
|
35
|
+
cameraUuids: z
|
|
36
|
+
.array(z.string())
|
|
37
|
+
.describe("An array of camera UUID strings which are unique identifiers for cameras"),
|
|
38
|
+
})), async ({ cameraUuids, requestModifiers, confirmationId }) => {
|
|
39
|
+
const confirmation = requireConfirmation(confirmationId);
|
|
40
|
+
if (!isConfirmed(confirmation)) {
|
|
41
|
+
return confirmation;
|
|
42
|
+
}
|
|
43
|
+
const cameraRebootData = await rebootCameras(cameraUuids, requestModifiers);
|
|
44
|
+
if (!cameraRebootData) {
|
|
45
|
+
return {
|
|
46
|
+
content: [
|
|
47
|
+
{
|
|
48
|
+
type: "text",
|
|
49
|
+
text: "Failed to reboot cameras",
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
content: [
|
|
56
|
+
{
|
|
57
|
+
type: "text",
|
|
58
|
+
text: JSON.stringify(cameraRebootData),
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { parse } from "chrono-node";
|
|
3
|
+
import { DateTime } from "luxon";
|
|
4
|
+
function nullToUndefined(value) {
|
|
5
|
+
return value === null ? undefined : value;
|
|
6
|
+
}
|
|
7
|
+
export function createTool(server) {
|
|
8
|
+
server.tool("time-tool", "Tool for converting a natural language time description into a timestamp in milliseconds.", {
|
|
9
|
+
time_description: z
|
|
10
|
+
.string()
|
|
11
|
+
.describe("A natural language description of the time (e.g., '2pm today', 'tomorrow at noon')."),
|
|
12
|
+
timezone: z
|
|
13
|
+
.string()
|
|
14
|
+
.optional()
|
|
15
|
+
.describe("Optional IANA timezone string (e.g., 'America/Los_Angeles', 'UTC'). Will default to system timezone if not provided."),
|
|
16
|
+
}, async ({ time_description, timezone }) => {
|
|
17
|
+
// console.error(`🕛 handling tool call for time ${time_description} using timezone ${timezone}`);
|
|
18
|
+
const now = DateTime.now()
|
|
19
|
+
.setZone(timezone || undefined)
|
|
20
|
+
.toJSDate();
|
|
21
|
+
const parsed = parse(time_description, now, { forwardDate: true });
|
|
22
|
+
if (!parsed || parsed.length === 0) {
|
|
23
|
+
throw new Error(`Could not parse time description: ${time_description}`);
|
|
24
|
+
}
|
|
25
|
+
const dateComponents = parsed[0].start;
|
|
26
|
+
if (!dateComponents) {
|
|
27
|
+
throw new Error("Parsed time has no start component");
|
|
28
|
+
}
|
|
29
|
+
const dt = DateTime.fromObject({
|
|
30
|
+
year: nullToUndefined(dateComponents.get("year")),
|
|
31
|
+
month: nullToUndefined(dateComponents.get("month")),
|
|
32
|
+
day: nullToUndefined(dateComponents.get("day")),
|
|
33
|
+
hour: nullToUndefined(dateComponents.get("hour")),
|
|
34
|
+
minute: nullToUndefined(dateComponents.get("minute")),
|
|
35
|
+
second: nullToUndefined(dateComponents.get("second")),
|
|
36
|
+
millisecond: 0,
|
|
37
|
+
}, {
|
|
38
|
+
zone: timezone || "local",
|
|
39
|
+
});
|
|
40
|
+
if (!dt.isValid) {
|
|
41
|
+
throw new Error(`Could not construct valid DateTime: ${dt.invalidReason}`);
|
|
42
|
+
}
|
|
43
|
+
const timestamp = dt.toMillis();
|
|
44
|
+
return {
|
|
45
|
+
content: [
|
|
46
|
+
{
|
|
47
|
+
type: "text",
|
|
48
|
+
text: JSON.stringify({
|
|
49
|
+
timestamp,
|
|
50
|
+
iso: dt.toISO(),
|
|
51
|
+
timezone: dt.zoneName,
|
|
52
|
+
}),
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
};
|
|
56
|
+
});
|
|
57
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
var DeviceType;
|
|
2
|
+
(function (DeviceType) {
|
|
3
|
+
DeviceType["CAMERA"] = "camera";
|
|
4
|
+
DeviceType["DOORBELL_CAMERA"] = "doorbell-camera";
|
|
5
|
+
DeviceType["BADGE_READER"] = "badge-reader";
|
|
6
|
+
DeviceType["ACCESS_CONTROL_DOOR"] = "access-control-door";
|
|
7
|
+
DeviceType["AUDIO_GATEWAY"] = "audio-gateway";
|
|
8
|
+
DeviceType["DOOR_SENSOR"] = "door-sensor";
|
|
9
|
+
DeviceType["ENVIRONMENTAL_SENSOR"] = "environmental-sensor";
|
|
10
|
+
DeviceType["MOTION_SENSOR"] = "motion-sensor";
|
|
11
|
+
DeviceType["BUTTON"] = "button";
|
|
12
|
+
DeviceType["KEYPAD"] = "keypad";
|
|
13
|
+
})(DeviceType || (DeviceType = {}));
|
|
14
|
+
export default DeviceType;
|
package/dist/types.js
CHANGED
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
export const
|
|
3
|
-
|
|
2
|
+
export const UUID = z
|
|
3
|
+
.string()
|
|
4
|
+
.describe("This describes the UUID of some entity (device, location, etc.) and is unique and must come from data. This can not be fabricated. It is always 22 characters long");
|
|
5
|
+
export const VideoWallSettings = z.object({
|
|
6
|
+
rowCount: z.number(),
|
|
7
|
+
columnCount: z.number(),
|
|
8
|
+
intervalSeconds: z.number().optional(),
|
|
9
|
+
rotateStrategy: z.enum(["none", "motion", "interval"]),
|
|
10
|
+
});
|
|
11
|
+
export const CreateVideoWallOptions = z
|
|
12
|
+
.object({
|
|
13
|
+
displayName: z.string().optional().describe("What to call the video wall"),
|
|
4
14
|
orgUuid: z.string().describe("The uuid of the organization"),
|
|
5
15
|
deviceList: z
|
|
6
16
|
.array(z.string())
|
|
7
17
|
.min(1)
|
|
8
18
|
.describe("The list of camera uuids (unique identifiers) to exist in the video wall. You must provide this manually by prompting the user at least once."),
|
|
9
|
-
othersCanEdit: z
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
settings: z.object({
|
|
14
|
-
rowCount: z.number(),
|
|
15
|
-
columnCount: z.number(),
|
|
16
|
-
intervalSeconds: z.optional(z.number()),
|
|
17
|
-
rotateStrategy: z.enum(["none", "motion", "interval"]),
|
|
18
|
-
}),
|
|
19
|
-
}));
|
|
19
|
+
othersCanEdit: z.boolean().optional().describe("Whether or not other users can edit the wall, defaults to false"),
|
|
20
|
+
settings: VideoWallSettings,
|
|
21
|
+
})
|
|
22
|
+
.optional();
|
package/dist/util.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
export function generateRandomString(length) {
|
|
5
|
+
const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
6
|
+
let result = "";
|
|
7
|
+
const charactersLength = characters.length;
|
|
8
|
+
for (let i = 0; i < length; i++) {
|
|
9
|
+
result += characters.charAt(Math.floor(Math.random() * charactersLength));
|
|
10
|
+
}
|
|
11
|
+
return result;
|
|
12
|
+
}
|
|
13
|
+
const STATIC_ARGS = {
|
|
14
|
+
requestModifiers: z
|
|
15
|
+
.optional(z.object({
|
|
16
|
+
headers: z.nullable(z.record(z.string(), z.string())),
|
|
17
|
+
query: z.nullable(z.record(z.string(), z.string())),
|
|
18
|
+
}))
|
|
19
|
+
.describe("Optional headers accepted by tools. LLM should never ever use this. 😅"),
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Get all file paths in a directory in a directory
|
|
23
|
+
*
|
|
24
|
+
* i.e.
|
|
25
|
+
* foo:
|
|
26
|
+
* - folder1:
|
|
27
|
+
* - har
|
|
28
|
+
* - gar
|
|
29
|
+
* - bar
|
|
30
|
+
* - lar
|
|
31
|
+
*
|
|
32
|
+
* returns: ["folder1/har", "folder1/gar", "bar", "lar"]
|
|
33
|
+
*/
|
|
34
|
+
export function getFilePathsInDirectory(dirPath) {
|
|
35
|
+
const filePaths = [];
|
|
36
|
+
const fileNames = fs.readdirSync(dirPath);
|
|
37
|
+
for (const fileName of fileNames) {
|
|
38
|
+
const pathToAdd = path.join(dirPath, fileName);
|
|
39
|
+
const stats = fs.lstatSync(pathToAdd);
|
|
40
|
+
if (stats.isDirectory()) {
|
|
41
|
+
filePaths.push(...getFilePathsInDirectory(pathToAdd));
|
|
42
|
+
}
|
|
43
|
+
else if (stats.isFile()) {
|
|
44
|
+
filePaths.push(pathToAdd);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return filePaths;
|
|
48
|
+
}
|
|
49
|
+
export function createToolArgs(args) {
|
|
50
|
+
return {
|
|
51
|
+
...args,
|
|
52
|
+
...STATIC_ARGS,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Returns an object in the form expected by `server.tool`
|
|
57
|
+
*/
|
|
58
|
+
export function createToolTextContent(content) {
|
|
59
|
+
return {
|
|
60
|
+
content: [
|
|
61
|
+
{
|
|
62
|
+
type: "text",
|
|
63
|
+
text: content,
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Recursively removes fields with null values from a JavaScript object.
|
|
70
|
+
* If a nested object becomes empty after removing nulls, it will also be removed.
|
|
71
|
+
*
|
|
72
|
+
* @param {unknown} obj The object or array to clean.
|
|
73
|
+
* @returns {object | unknown[] | undefined} The cleaned object/array, or undefined if the input was null/undefined or an empty object/array resulted.
|
|
74
|
+
*/
|
|
75
|
+
export function removeNullFields(obj) {
|
|
76
|
+
if (obj === null || obj === undefined) {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
if (Array.isArray(obj)) {
|
|
80
|
+
const cleanedArray = [];
|
|
81
|
+
for (const item of obj) {
|
|
82
|
+
const cleanedItem = removeNullFields(item);
|
|
83
|
+
if (cleanedItem !== undefined) {
|
|
84
|
+
cleanedArray.push(cleanedItem);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return cleanedArray.length > 0 ? cleanedArray : undefined;
|
|
88
|
+
}
|
|
89
|
+
if (typeof obj !== "object") {
|
|
90
|
+
return obj; // Cast to any for primitive types
|
|
91
|
+
}
|
|
92
|
+
const cleanedObject = {};
|
|
93
|
+
for (const key in obj) {
|
|
94
|
+
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
95
|
+
const value = obj[key];
|
|
96
|
+
if (value === null) {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (typeof value === "object") {
|
|
100
|
+
const cleanedValue = removeNullFields(value);
|
|
101
|
+
if (cleanedValue !== undefined) {
|
|
102
|
+
cleanedObject[key] = cleanedValue;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
cleanedObject[key] = value;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return Object.keys(cleanedObject).length > 0 ? cleanedObject : undefined;
|
|
111
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Confirmation is an important flow for our MCP, as letting an AI perform tools that change and mutate can be potentially dangerous
|
|
3
|
+
*/
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { createToolTextContent, generateRandomString } from "../util.js";
|
|
6
|
+
import { logger } from "../logger.js";
|
|
7
|
+
// export function createToolArgs<TArgs extends object>(args: TArgs): TArgs & typeof STATIC_ARGS {
|
|
8
|
+
// return {
|
|
9
|
+
// ...args,
|
|
10
|
+
// ...STATIC_ARGS,
|
|
11
|
+
// };
|
|
12
|
+
// }
|
|
13
|
+
export const CONFIRMATION_ARGS = {
|
|
14
|
+
confirmationId: z.string().nullable().optional(),
|
|
15
|
+
};
|
|
16
|
+
export function addConfirmationParams(args) {
|
|
17
|
+
return {
|
|
18
|
+
...args,
|
|
19
|
+
...CONFIRMATION_ARGS,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
// IN MEMORY
|
|
23
|
+
const confirmationStore = new Map();
|
|
24
|
+
const CONFIRMATION_MAX_AGE = 15 * 60 * 1000; // 15 minutes
|
|
25
|
+
export function requireConfirmation(confirmationId) {
|
|
26
|
+
// remove any old confirmations
|
|
27
|
+
for (const entry of confirmationStore) {
|
|
28
|
+
if (Date.now() - entry[1] > CONFIRMATION_MAX_AGE)
|
|
29
|
+
confirmationStore.delete(entry[0]);
|
|
30
|
+
}
|
|
31
|
+
logger.log("Checking for confirmationId:", confirmationId);
|
|
32
|
+
if (!confirmationId || !confirmationStore.has(confirmationId ?? "")) {
|
|
33
|
+
const newId = generateRandomString(8);
|
|
34
|
+
confirmationStore.set(newId, Date.now());
|
|
35
|
+
return createToolTextContent(JSON.stringify({
|
|
36
|
+
needUserInput: true,
|
|
37
|
+
description: `Please request confirmation for the user for performing this action. Upon confirmation, call this tool again with the confirmation id: ${newId}`,
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
40
|
+
confirmationStore.delete(confirmationId);
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
export function isConfirmed(confirmation) {
|
|
44
|
+
return confirmation === true;
|
|
45
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rhombus-node-mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
4
4
|
"description": "MCP server for Rhombus API",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -34,6 +34,8 @@
|
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@modelcontextprotocol/sdk": "^1.9.0",
|
|
36
36
|
"chrono-node": "^2.8.0",
|
|
37
|
+
"dotenv": "^16.5.0",
|
|
38
|
+
"log4js": "^6.9.1",
|
|
37
39
|
"luxon": "^3.6.1",
|
|
38
40
|
"zod": "^3.24.2"
|
|
39
41
|
},
|