rhombus-node-mcp 0.1.13 → 0.1.16
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 +11 -3
- package/dist/api/camera-tool-api.js +108 -0
- package/dist/api/clips-tool-api.js +36 -0
- package/dist/api/create-camera-policy-tool-api.js +9 -0
- package/dist/api/create-tool-api.js +53 -0
- package/dist/api/entity-lookup-tool-api.js +37 -0
- package/dist/api/events-tool-api.js +320 -0
- package/dist/api/faces-tool-api.js +110 -0
- package/dist/api/get-entity-tool-api.js +176 -0
- package/dist/api/get-org-information-tool-api.js +9 -0
- package/dist/api/location-tool-api.js +9 -0
- package/dist/api/lpr-tool-api.js +68 -0
- package/dist/api/policy-alerts-tool-api.js +42 -0
- package/dist/api/reboot-cameras-tool-api.js +34 -0
- package/dist/api/report-tool-api.js +426 -0
- package/dist/api/time-tool-api.js +90 -0
- package/dist/api/update-tool-api.js +148 -0
- package/dist/createServer.js +50 -0
- package/dist/disabled-tools/endpoint-to-keys-tool.js +84 -0
- package/dist/disabled-tools/semantic-search-tool.js +90 -0
- package/dist/index.js +25 -32
- package/dist/logger.js +5 -4
- package/dist/network.js +45 -14
- package/dist/resources/routes.json.js +1 -1
- package/dist/services/embedding-service.js +153 -0
- package/dist/services/faiss-search-service.js +261 -0
- package/dist/tools/camera-tool.js +100 -0
- package/dist/tools/clips-tool.js +23 -38
- package/dist/tools/count-tool.js +25 -0
- package/dist/tools/create-camera-policy-tool.js +214 -0
- package/dist/tools/create-tool.js +25 -75
- package/dist/tools/entity-lookup-tool.js +35 -0
- package/dist/tools/events-tool.js +188 -93
- package/dist/tools/faces-tool.js +59 -133
- package/dist/tools/get-entity-tool.js +78 -0
- package/dist/tools/get-org-information-tool.js +18 -0
- package/dist/tools/location-tool.js +24 -32
- package/dist/tools/lpr-tool.js +71 -0
- package/dist/tools/policy-alerts-tool.js +32 -43
- package/dist/tools/reboot-cameras-tool.js +34 -0
- package/dist/tools/report-tool.js +190 -0
- package/dist/tools/time-conversion-tool.js +43 -0
- package/dist/tools/time-tool.js +17 -55
- package/dist/tools/update-tool.js +262 -0
- package/dist/transports/stdio.js +10 -0
- package/dist/transports/streamable-http.js +201 -0
- package/dist/{tools/devices/camera-tool/types.js → types/camera-tool-types.js} +13 -0
- package/dist/types/clips-tool-types.js +41 -0
- package/dist/types/create-camera-policy-tool-types.js +44 -0
- package/dist/types/create-tool-types.js +8 -0
- package/dist/types/deviceType.js +1 -0
- package/dist/types/endpoint-to-keys-tool-types.js +7 -0
- package/dist/types/entity-lookup-tool-types.js +70 -0
- package/dist/types/events-tools-types.js +257 -0
- package/dist/types/faces-tools-types.js +143 -0
- package/dist/types/get-entity-tool-types.js +25 -0
- package/dist/types/get-org-information-tool-types.js +3 -0
- package/dist/types/location-tool-types.js +11 -0
- package/dist/types/lpr-tool-types.js +97 -0
- package/dist/types/policy-alerts-tool-types.js +74 -0
- package/dist/types/reboot-cameras-tool-types.js +8 -0
- package/dist/types/report-tool-types.js +268 -0
- package/dist/types/schema-components.js +7093 -0
- package/dist/types/schema.js +1 -0
- package/dist/types/semantic-search-tool-types.js +5 -0
- package/dist/types/time-conversion-tool-types.js +8 -0
- package/dist/types/time-tool-types.js +11 -0
- package/dist/types/update-tool-types.js +186 -0
- package/dist/types/zod-schemas.js +21315 -0
- package/dist/types.js +17 -7
- package/dist/util.js +96 -14
- package/dist/utils/confirmation.js +1 -7
- package/dist/utils/reduce-output.js +35 -0
- package/dist/utils/remove-nulls.js +28 -0
- package/dist/utils/temp.js +8 -0
- package/dist/utils/timestampInput.js +12 -0
- package/package.json +28 -3
- package/dist/tools/devices/camera-tool/camera-tool.js +0 -218
- package/dist/tools/devices/get-entity-tool.js +0 -117
- package/dist/tools/get-org-information.js +0 -18
- package/dist/tools/reboot-cameras.js +0 -63
package/dist/types.js
CHANGED
|
@@ -1,22 +1,32 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
export
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
export function createUuidSchema() {
|
|
3
|
+
return z
|
|
4
|
+
.string()
|
|
5
|
+
.length(22)
|
|
6
|
+
// .regex(/[A-Za-z0-9\\-_]{22}/)
|
|
7
|
+
.describe(`This describes the UUID of some entity (device, location, etc.) and is unique and must come from data.
|
|
8
|
+
This can not be fabricated. It is always 22 characters long. Truncate any facet ids, such as .v0.
|
|
9
|
+
It contains only alphanumeric characters, digits, hyphens, and underscores.`);
|
|
10
|
+
}
|
|
11
|
+
export const UUID = createUuidSchema();
|
|
5
12
|
export const VideoWallSettings = z.object({
|
|
6
13
|
rowCount: z.number(),
|
|
7
14
|
columnCount: z.number(),
|
|
8
|
-
intervalSeconds: z.number().
|
|
15
|
+
intervalSeconds: z.number().nullable(),
|
|
9
16
|
rotateStrategy: z.enum(["none", "motion", "interval"]),
|
|
10
17
|
});
|
|
11
18
|
export const CreateVideoWallOptions = z
|
|
12
19
|
.object({
|
|
13
|
-
displayName: z.string().
|
|
20
|
+
displayName: z.string().nullable().describe("What to call the video wall"),
|
|
14
21
|
orgUuid: z.string().describe("The uuid of the organization"),
|
|
15
22
|
deviceList: z
|
|
16
23
|
.array(z.string())
|
|
17
24
|
.min(1)
|
|
18
25
|
.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."),
|
|
19
|
-
othersCanEdit: z
|
|
26
|
+
othersCanEdit: z
|
|
27
|
+
.boolean()
|
|
28
|
+
.nullable()
|
|
29
|
+
.describe("Whether or not other users can edit the wall, defaults to false"),
|
|
20
30
|
settings: VideoWallSettings,
|
|
21
31
|
})
|
|
22
|
-
.
|
|
32
|
+
.nullable();
|
package/dist/util.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import fs from "fs";
|
|
3
3
|
import path from "path";
|
|
4
|
+
import { DateTime } from "luxon";
|
|
4
5
|
export function generateRandomString(length) {
|
|
5
6
|
const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
6
7
|
let result = "";
|
|
@@ -10,14 +11,19 @@ export function generateRandomString(length) {
|
|
|
10
11
|
}
|
|
11
12
|
return result;
|
|
12
13
|
}
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
14
|
+
export const RequestModifiers = z
|
|
15
|
+
.object({
|
|
16
|
+
headers: z.optional(z.record(z.string(), z.string())),
|
|
17
|
+
query: z.optional(z.record(z.string(), z.string())),
|
|
18
|
+
})
|
|
19
|
+
.optional();
|
|
20
|
+
export function extractFromToolExtra(_extra) {
|
|
21
|
+
const extra = _extra;
|
|
22
|
+
return {
|
|
23
|
+
requestModifiers: extra._meta?.requestModifiers,
|
|
24
|
+
sessionId: extra.sessionId,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
21
27
|
/**
|
|
22
28
|
* Get all file paths in a directory in a directory
|
|
23
29
|
*
|
|
@@ -46,12 +52,6 @@ export function getFilePathsInDirectory(dirPath) {
|
|
|
46
52
|
}
|
|
47
53
|
return filePaths;
|
|
48
54
|
}
|
|
49
|
-
export function createToolArgs(args) {
|
|
50
|
-
return {
|
|
51
|
-
...args,
|
|
52
|
-
...STATIC_ARGS,
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
55
|
/**
|
|
56
56
|
* Returns an object in the form expected by `server.tool`
|
|
57
57
|
*/
|
|
@@ -65,6 +65,21 @@ export function createToolTextContent(content) {
|
|
|
65
65
|
],
|
|
66
66
|
};
|
|
67
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* Returns strucutred content expected by `server.tool`
|
|
70
|
+
* The generic type T allows for type safety. Set T to the output schema of the tool, and your content will be type checked
|
|
71
|
+
*/
|
|
72
|
+
export function createToolStructuredContent(content) {
|
|
73
|
+
return {
|
|
74
|
+
content: [
|
|
75
|
+
{
|
|
76
|
+
type: "text",
|
|
77
|
+
text: JSON.stringify(content),
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
structuredContent: content,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
68
83
|
/**
|
|
69
84
|
* Recursively removes fields with null values from a JavaScript object.
|
|
70
85
|
* If a nested object becomes empty after removing nulls, it will also be removed.
|
|
@@ -109,3 +124,70 @@ export function removeNullFields(obj) {
|
|
|
109
124
|
}
|
|
110
125
|
return Object.keys(cleanedObject).length > 0 ? cleanedObject : undefined;
|
|
111
126
|
}
|
|
127
|
+
export function filterIncludedFields(obj, fieldsToInclude) {
|
|
128
|
+
if (!fieldsToInclude || fieldsToInclude.length === 0) {
|
|
129
|
+
return obj;
|
|
130
|
+
}
|
|
131
|
+
if (Array.isArray(obj)) {
|
|
132
|
+
return obj
|
|
133
|
+
.map(item => filterIncludedFields(item, fieldsToInclude))
|
|
134
|
+
.filter(item => {
|
|
135
|
+
if (item === undefined || item === null) {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
if (Array.isArray(item)) {
|
|
139
|
+
return item.length > 0;
|
|
140
|
+
}
|
|
141
|
+
if (typeof item === "object") {
|
|
142
|
+
return Object.keys(item).length > 0;
|
|
143
|
+
}
|
|
144
|
+
// Keep primitives
|
|
145
|
+
return true;
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
if (typeof obj === "object" && obj !== null) {
|
|
149
|
+
const newObj = {};
|
|
150
|
+
for (const key in obj) {
|
|
151
|
+
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
152
|
+
if (fieldsToInclude.includes(key)) {
|
|
153
|
+
newObj[key] = obj[key];
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
const result = filterIncludedFields(obj[key], fieldsToInclude);
|
|
157
|
+
if (result !== undefined && result !== null) {
|
|
158
|
+
if (Array.isArray(result)) {
|
|
159
|
+
if (result.length > 0) {
|
|
160
|
+
newObj[key] = result;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
else if (typeof result === "object") {
|
|
164
|
+
if (Object.keys(result).length > 0) {
|
|
165
|
+
newObj[key] = result;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
newObj[key] = result;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return Object.keys(newObj).length > 0 ? newObj : undefined;
|
|
176
|
+
}
|
|
177
|
+
return undefined;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Formats a timestamp in milliseconds to a human-readable date string
|
|
181
|
+
* Format: "February 24, 2025 at 3:23 PM"
|
|
182
|
+
*
|
|
183
|
+
* @param timestampMs - Timestamp in milliseconds
|
|
184
|
+
* @param timeZone - Optional IANA timezone string (defaults to "America/Los_Angeles")
|
|
185
|
+
* @returns Formatted date string
|
|
186
|
+
*/
|
|
187
|
+
export function formatTimestamp(timestampMs, timeZone) {
|
|
188
|
+
return DateTime.fromMillis(timestampMs)
|
|
189
|
+
.setZone(timeZone || "America/Los_Angeles")
|
|
190
|
+
.toFormat("MMMM d, yyyy 'at' h:mm:ss a", {
|
|
191
|
+
locale: "en-US",
|
|
192
|
+
});
|
|
193
|
+
}
|
|
@@ -4,14 +4,8 @@
|
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { createToolTextContent, generateRandomString } from "../util.js";
|
|
6
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
7
|
export const CONFIRMATION_ARGS = {
|
|
14
|
-
confirmationId: z.string().nullable()
|
|
8
|
+
confirmationId: z.string().nullable(),
|
|
15
9
|
};
|
|
16
10
|
export function addConfirmationParams(args) {
|
|
17
11
|
return {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const REDUCE_OUTPUT_ARGS = {
|
|
3
|
+
includeFields: z
|
|
4
|
+
.array(z.string())
|
|
5
|
+
.describe("The fields to include in the response."),
|
|
6
|
+
};
|
|
7
|
+
export function addReduceOutputParams(args) {
|
|
8
|
+
return {
|
|
9
|
+
...args,
|
|
10
|
+
...REDUCE_OUTPUT_ARGS,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export function generateEndpointToKeysWorkflowText(endpointMap) {
|
|
14
|
+
const endpointList = Object.entries(endpointMap)
|
|
15
|
+
.map(([entityType, endpoint]) => `- ${entityType}: ${endpoint}`)
|
|
16
|
+
.join('\n');
|
|
17
|
+
// Automatically generate example from first entry in the map
|
|
18
|
+
const [firstEntityType, firstEndpoint] = Object.entries(endpointMap)[0];
|
|
19
|
+
const exampleEntityTypeName = firstEntityType.toLowerCase().replace(/_/g, ' ');
|
|
20
|
+
const exampleFields = ["field1", "field2", "field3"];
|
|
21
|
+
return `IMPORTANT: Before using this tool, you MUST first use the endpoint-to-keys-tool to get the available output fields for the relevant endpoints, then specify the fields you want in the includeFields parameter to reduce context size.
|
|
22
|
+
|
|
23
|
+
The following endpoints are called for each device type:
|
|
24
|
+
${endpointList}
|
|
25
|
+
|
|
26
|
+
REQUIRED WORKFLOW:
|
|
27
|
+
1. First call endpoint-to-keys-tool with the relevant endpoint(s) above, if the endpoint is not found, you can proceed to this tool with an empty includeFields parameter.
|
|
28
|
+
2. Then call this tool with the specific fields you need in includeFields parameter
|
|
29
|
+
|
|
30
|
+
Example for ${exampleEntityTypeName}s:
|
|
31
|
+
1. Call endpoint-to-keys-tool with "${firstEndpoint}"
|
|
32
|
+
2. Use the returned keys to populate includeFields (e.g., ${JSON.stringify(exampleFields)})
|
|
33
|
+
|
|
34
|
+
Always use includeFields to limit the response size and get only the data you need.`;
|
|
35
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remove all null values from an object.
|
|
3
|
+
* @param obj - The object to remove null values from.
|
|
4
|
+
* @returns A new object with all null values removed.
|
|
5
|
+
*/
|
|
6
|
+
export function removeNulls(obj) {
|
|
7
|
+
// Create a shallow copy to avoid modifying the original object
|
|
8
|
+
const copy = { ...obj };
|
|
9
|
+
for (const key in copy) {
|
|
10
|
+
if (Object.prototype.hasOwnProperty.call(copy, key)) {
|
|
11
|
+
const value = copy[key];
|
|
12
|
+
if (value === null) {
|
|
13
|
+
delete copy[key];
|
|
14
|
+
}
|
|
15
|
+
else if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
16
|
+
// Recursively clean nested objects
|
|
17
|
+
const cleaned = removeNulls(value);
|
|
18
|
+
if (Object.keys(cleaned).length === 0) {
|
|
19
|
+
delete copy[key];
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
copy[key] = cleaned;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return copy;
|
|
28
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export const ISOTimestampFormatDescription = 'Time format is in ISO 8601 format. Both UTC ("2025-08-04T20:54:27.123Z") and time zone offsets ("2025-08-04T13:54:27.123-07:00") are accepted to ensure an unambiguous point in time.';
|
|
3
|
+
export const createEpochSchema = () => {
|
|
4
|
+
return z.coerce.date({
|
|
5
|
+
errorMap: (issue, ctx) => {
|
|
6
|
+
if (issue.code === 'invalid_date') {
|
|
7
|
+
return { message: 'Invalid datetime string. Expected ISO 8601 format.' };
|
|
8
|
+
}
|
|
9
|
+
return { message: ctx.defaultError };
|
|
10
|
+
},
|
|
11
|
+
}).transform(date => date.getTime());
|
|
12
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rhombus-node-mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.16",
|
|
4
4
|
"description": "MCP server for Rhombus API",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -24,24 +24,49 @@
|
|
|
24
24
|
"mcp-server-rhombus": "dist/index.js"
|
|
25
25
|
},
|
|
26
26
|
"scripts": {
|
|
27
|
-
"build": "tsc && chmod +x dist/*.js",
|
|
27
|
+
"build": "rm -rf dist && tsc && chmod +x dist/*.js",
|
|
28
28
|
"prepare": "npm run build",
|
|
29
|
-
"watch": "tsc --watch"
|
|
29
|
+
"watch": "tsc --watch",
|
|
30
|
+
"generate-schemas": "npx tsx scripts/generate-zod-schemas.ts",
|
|
31
|
+
"scrape-sitemaps": "npx tsx scripts/scrape-sitemaps.ts",
|
|
32
|
+
"scrape-content": "npx tsx scripts/scrape-content.ts",
|
|
33
|
+
"scrape-progress": "npx tsx scripts/scrape-content.ts --progress",
|
|
34
|
+
"generate-embeddings": "npx tsx scripts/generate-embeddings.ts",
|
|
35
|
+
"embedding-progress": "npx tsx scripts/generate-embeddings.ts --progress",
|
|
36
|
+
"estimate-cost": "npx tsx scripts/generate-embeddings.ts --estimate-only",
|
|
37
|
+
"build-faiss-index": "npx tsx scripts/build-faiss-index.ts",
|
|
38
|
+
"extract-route-outputs": "npx tsx assets/extract-route-outputs.ts",
|
|
39
|
+
"create:tool": "tsx scripts/create-new-tool.ts"
|
|
30
40
|
},
|
|
31
41
|
"files": [
|
|
32
42
|
"dist"
|
|
33
43
|
],
|
|
34
44
|
"dependencies": {
|
|
35
45
|
"@modelcontextprotocol/sdk": "^1.9.0",
|
|
46
|
+
"axios": "^1.11.0",
|
|
47
|
+
"cheerio": "^1.1.2",
|
|
36
48
|
"chrono-node": "^2.8.0",
|
|
49
|
+
"cors": "^2.8.5",
|
|
37
50
|
"dotenv": "^16.5.0",
|
|
51
|
+
"express": "^5.1.0",
|
|
52
|
+
"express-jwt": "^8.5.1",
|
|
53
|
+
"faiss-node": "^0.5.1",
|
|
54
|
+
"jsonwebtoken": "^9.0.2",
|
|
55
|
+
"langchain": "^0.3.30",
|
|
38
56
|
"log4js": "^6.9.1",
|
|
39
57
|
"luxon": "^3.6.1",
|
|
58
|
+
"openai": "^5.12.1",
|
|
59
|
+
"tiktoken": "^1.0.21",
|
|
40
60
|
"zod": "^3.24.2"
|
|
41
61
|
},
|
|
42
62
|
"devDependencies": {
|
|
63
|
+
"@types/cors": "^2.8.19",
|
|
64
|
+
"@types/express": "^5.0.3",
|
|
65
|
+
"@types/jwt-express": "^1.1.6",
|
|
43
66
|
"@types/luxon": "^3.6.2",
|
|
44
67
|
"@types/node": "^22.14.0",
|
|
68
|
+
"openapi-typescript-codegen": "^0.29.0",
|
|
69
|
+
"openapi-zod-client": "^1.17.0",
|
|
45
70
|
"prettier": "3.5.3",
|
|
46
71
|
"typescript": "^5.8.3"
|
|
47
72
|
},
|
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import { getLogger } from "../../../logger.js";
|
|
3
|
-
import { appendQueryParams, AUTH_HEADERS, postApi, STATIC_HEADERS } from "../../../network.js";
|
|
4
|
-
import { createToolArgs, createToolTextContent, removeNullFields, } from "../../../util.js";
|
|
5
|
-
import { addConfirmationParams, isConfirmed, requireConfirmation, } from "../../../utils/confirmation.js";
|
|
6
|
-
import { ExternalUpdateableFacetedUserConfigSchema, } from "./types.js";
|
|
7
|
-
const logger = getLogger("camera-tool");
|
|
8
|
-
async function getImageForCameraAtTime(cameraUuid, timestampMs, requestModifiers) {
|
|
9
|
-
const body = {
|
|
10
|
-
cameraUuid: cameraUuid,
|
|
11
|
-
downscaleFactor: 10,
|
|
12
|
-
jpgQuality: 70,
|
|
13
|
-
permyriadCropHeight: 10000,
|
|
14
|
-
permyriadCropWidth: 5625,
|
|
15
|
-
permyriadCropX: 2188,
|
|
16
|
-
permyriadCropY: 0,
|
|
17
|
-
timestampMs: timestampMs,
|
|
18
|
-
};
|
|
19
|
-
logger.debug(`Getting frameUri from UUID: ${cameraUuid} at timestampMs: ${timestampMs}`);
|
|
20
|
-
const base64Image = await postApi("/video/getExactFrameUri", body, requestModifiers).then(async (res) => {
|
|
21
|
-
logger.debug(`Received frameUri ${res.frameUri}`);
|
|
22
|
-
// construct request headers
|
|
23
|
-
let requestHeaders = {
|
|
24
|
-
...(requestModifiers?.headers ?? AUTH_HEADERS),
|
|
25
|
-
...STATIC_HEADERS,
|
|
26
|
-
};
|
|
27
|
-
// add query params
|
|
28
|
-
if (requestModifiers?.query) {
|
|
29
|
-
res.frameUri = appendQueryParams(res.frameUri, requestModifiers.query);
|
|
30
|
-
}
|
|
31
|
-
logger.trace(`Fetching with headers\n${JSON.stringify(requestHeaders)}`);
|
|
32
|
-
return await fetch(res.frameUri, {
|
|
33
|
-
method: "GET",
|
|
34
|
-
headers: requestHeaders,
|
|
35
|
-
}).then(async (res) => {
|
|
36
|
-
if (!res.ok) {
|
|
37
|
-
logger.error(`Failed to fetch image: ${await res.text()}`);
|
|
38
|
-
logger.error(res);
|
|
39
|
-
return null;
|
|
40
|
-
}
|
|
41
|
-
const arrayBuffer = await res.arrayBuffer();
|
|
42
|
-
const buffer = Buffer.from(arrayBuffer);
|
|
43
|
-
const base64 = buffer.toString("base64");
|
|
44
|
-
return base64;
|
|
45
|
-
});
|
|
46
|
-
});
|
|
47
|
-
if (!base64Image) {
|
|
48
|
-
return {
|
|
49
|
-
success: false,
|
|
50
|
-
status: "failed to fetch image",
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
return {
|
|
54
|
-
success: true,
|
|
55
|
-
status: "successfully fetched image",
|
|
56
|
-
imageType: "base64",
|
|
57
|
-
imageData: base64Image,
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
export async function getCameraSettings(cameraUuid, requestModifiers) {
|
|
61
|
-
const res = await postApi("/camera/getFacetedConfig", {
|
|
62
|
-
deviceUuid: cameraUuid,
|
|
63
|
-
}, requestModifiers);
|
|
64
|
-
if (res.error) {
|
|
65
|
-
return {
|
|
66
|
-
success: false,
|
|
67
|
-
status: "failed to fetch camera settings",
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
return {
|
|
71
|
-
success: true,
|
|
72
|
-
config: res.config,
|
|
73
|
-
status: "fetched camera settings",
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
export async function updateCameraSettings(cameraUuid, update, requestModifiers) {
|
|
77
|
-
// remove any "null" values
|
|
78
|
-
const res = await postApi("/camera/updateFacetedConfig", {
|
|
79
|
-
configUpdate: {
|
|
80
|
-
deviceUuid: cameraUuid,
|
|
81
|
-
...removeNullFields(update),
|
|
82
|
-
},
|
|
83
|
-
}, requestModifiers);
|
|
84
|
-
if (res.error) {
|
|
85
|
-
return {
|
|
86
|
-
success: false,
|
|
87
|
-
status: "failed to update camera settings",
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
return {
|
|
91
|
-
success: true,
|
|
92
|
-
config: res.config,
|
|
93
|
-
status: "updated camera settings",
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
export function createTool(server) {
|
|
97
|
-
server.tool("camera-tool", `
|
|
98
|
-
This tool can perform some action pertaining to the video stream of a camera. There are three types of requests
|
|
99
|
-
that can be passed into "requestType":
|
|
100
|
-
- image
|
|
101
|
-
- get-settings
|
|
102
|
-
- update-settings
|
|
103
|
-
|
|
104
|
-
What follows is a description of the behavior of this tool given the requestType
|
|
105
|
-
|
|
106
|
-
If the requestType is "image":
|
|
107
|
-
|
|
108
|
-
This tool captures and returns a real-time snapshot from a designated security camera.
|
|
109
|
-
The image reflects the current scene in the camera\'s field of view and serves as a contextual
|
|
110
|
-
input source for downstream tasks such as object recognition, anomaly detection, incident investigation,
|
|
111
|
-
or situational assessment. When invoked, the tool provides the following: \n
|
|
112
|
-
• Visual Scene Capture: A high-resolution image of what the camera is actively observing, including people, vehicles, license plates, and any detectable objects. \n
|
|
113
|
-
• 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.
|
|
114
|
-
|
|
115
|
-
If the requestType is "get-settings":
|
|
116
|
-
|
|
117
|
-
THIS TOOL UPDATES AND SETS DATA.
|
|
118
|
-
|
|
119
|
-
This tool retrieves the current configuration for a specified camera or associated device (e.g., sensor, access controller). The returned JSON object can include detailed camera settings (e.g., resolution, bitrate) and various device-specific configurations.
|
|
120
|
-
Use Cases: Retrieve the current resolution of Camera A.
|
|
121
|
-
|
|
122
|
-
If the requestType is "update-settings":
|
|
123
|
-
|
|
124
|
-
THIS TOOL UPDATES AND SETS DATA.
|
|
125
|
-
|
|
126
|
-
You can call call this tool with requestType "get-settings" and/or with "image" first to get a better idea of what needs to be updated.
|
|
127
|
-
This tool updates the configuration for a camera or associated device using the "configUpdate" parameter, which must be a JSON object containing the specific fields and their new values. For example, you can modify streaming parameters.
|
|
128
|
-
Thus, "configUpdate' is a necessary parameter if updating settings.
|
|
129
|
-
Please make sure you only update the necessary fields, since any unnecessary changes may cause the camera to behave improperly.
|
|
130
|
-
It may be a good idea to call "image" on this tool again after updating settings to make sure the new settings were effective in fulfilling
|
|
131
|
-
the user's request.
|
|
132
|
-
Use Cases: Adjust streaming parameters for Camera E.
|
|
133
|
-
`, addConfirmationParams(createToolArgs({
|
|
134
|
-
requestType: z.enum(["image", "get-settings", "update-settings"]),
|
|
135
|
-
timestampMs: z.optional(z.number()).describe(`
|
|
136
|
-
the timestamp in milliseconds. You can default to the current time if the user didn't specify a time, or you can call time-tool to parse the user's time description
|
|
137
|
-
`),
|
|
138
|
-
cameraUuid: z.optional(z.string()).describe("the camera uuid requested"),
|
|
139
|
-
configUpdate: ExternalUpdateableFacetedUserConfigSchema.optional().describe('the config update that would be applied to the camera if the requestType is "update-settings"'),
|
|
140
|
-
})), async ({ cameraUuid, timestampMs, requestType, configUpdate, requestModifiers, confirmationId, }) => {
|
|
141
|
-
if (!cameraUuid) {
|
|
142
|
-
return {
|
|
143
|
-
content: [
|
|
144
|
-
{
|
|
145
|
-
type: "text",
|
|
146
|
-
text: JSON.stringify({
|
|
147
|
-
needUserInput: true,
|
|
148
|
-
commandForUser: "Which camera are you talking about?",
|
|
149
|
-
}),
|
|
150
|
-
},
|
|
151
|
-
],
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
let response;
|
|
155
|
-
switch (requestType) {
|
|
156
|
-
case "image":
|
|
157
|
-
if (!timestampMs) {
|
|
158
|
-
return {
|
|
159
|
-
content: [
|
|
160
|
-
{
|
|
161
|
-
type: "text",
|
|
162
|
-
text: JSON.stringify({
|
|
163
|
-
needUserInput: true,
|
|
164
|
-
commandForUser: "At what time?",
|
|
165
|
-
}),
|
|
166
|
-
},
|
|
167
|
-
],
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
response = await getImageForCameraAtTime(cameraUuid, timestampMs, requestModifiers);
|
|
171
|
-
if (!response.success || !response.imageData) {
|
|
172
|
-
return {
|
|
173
|
-
content: [{ type: "text", text: JSON.stringify(response) }],
|
|
174
|
-
};
|
|
175
|
-
}
|
|
176
|
-
return {
|
|
177
|
-
content: [
|
|
178
|
-
{
|
|
179
|
-
type: "image",
|
|
180
|
-
data: response.imageData,
|
|
181
|
-
mimeType: "image/jpeg",
|
|
182
|
-
},
|
|
183
|
-
],
|
|
184
|
-
};
|
|
185
|
-
case "get-settings":
|
|
186
|
-
response = await getCameraSettings(cameraUuid, requestModifiers);
|
|
187
|
-
return {
|
|
188
|
-
content: [{ type: "text", text: JSON.stringify(response) }],
|
|
189
|
-
};
|
|
190
|
-
case "update-settings":
|
|
191
|
-
const confirmation = requireConfirmation(confirmationId);
|
|
192
|
-
if (!isConfirmed(confirmation)) {
|
|
193
|
-
return confirmation;
|
|
194
|
-
}
|
|
195
|
-
if (!configUpdate) {
|
|
196
|
-
return createToolTextContent("Missing configUpdate");
|
|
197
|
-
}
|
|
198
|
-
response = await updateCameraSettings(cameraUuid, configUpdate, requestModifiers);
|
|
199
|
-
return {
|
|
200
|
-
content: [{ type: "text", text: JSON.stringify(response) }],
|
|
201
|
-
};
|
|
202
|
-
default:
|
|
203
|
-
response = {
|
|
204
|
-
error: true,
|
|
205
|
-
status: "missing unknown type from tool call",
|
|
206
|
-
};
|
|
207
|
-
break;
|
|
208
|
-
}
|
|
209
|
-
return {
|
|
210
|
-
content: [
|
|
211
|
-
{
|
|
212
|
-
type: "text",
|
|
213
|
-
text: JSON.stringify({ response }),
|
|
214
|
-
},
|
|
215
|
-
],
|
|
216
|
-
};
|
|
217
|
-
});
|
|
218
|
-
}
|