rhombus-node-mcp 0.1.15 → 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.
Files changed (80) hide show
  1. package/README.md +9 -1
  2. package/dist/api/camera-tool-api.js +108 -0
  3. package/dist/api/clips-tool-api.js +36 -0
  4. package/dist/api/create-camera-policy-tool-api.js +9 -0
  5. package/dist/api/create-tool-api.js +53 -0
  6. package/dist/api/entity-lookup-tool-api.js +37 -0
  7. package/dist/api/events-tool-api.js +320 -0
  8. package/dist/api/faces-tool-api.js +110 -0
  9. package/dist/api/get-entity-tool-api.js +176 -0
  10. package/dist/api/get-org-information-tool-api.js +9 -0
  11. package/dist/api/location-tool-api.js +9 -0
  12. package/dist/api/lpr-tool-api.js +68 -0
  13. package/dist/api/policy-alerts-tool-api.js +42 -0
  14. package/dist/api/reboot-cameras-tool-api.js +34 -0
  15. package/dist/api/report-tool-api.js +426 -0
  16. package/dist/api/time-tool-api.js +90 -0
  17. package/dist/api/update-tool-api.js +148 -0
  18. package/dist/createServer.js +7 -1
  19. package/dist/disabled-tools/endpoint-to-keys-tool.js +84 -0
  20. package/dist/disabled-tools/semantic-search-tool.js +90 -0
  21. package/dist/index.js +3 -5
  22. package/dist/logger.js +4 -3
  23. package/dist/network.js +42 -15
  24. package/dist/resources/routes.json.js +1 -1
  25. package/dist/services/embedding-service.js +153 -0
  26. package/dist/services/faiss-search-service.js +261 -0
  27. package/dist/tools/camera-tool.js +100 -0
  28. package/dist/tools/clips-tool.js +23 -37
  29. package/dist/tools/count-tool.js +25 -0
  30. package/dist/tools/create-camera-policy-tool.js +214 -0
  31. package/dist/tools/create-tool.js +25 -74
  32. package/dist/tools/entity-lookup-tool.js +35 -0
  33. package/dist/tools/events-tool.js +188 -92
  34. package/dist/tools/faces-tool.js +59 -132
  35. package/dist/tools/get-entity-tool.js +78 -0
  36. package/dist/tools/get-org-information-tool.js +18 -0
  37. package/dist/tools/location-tool.js +24 -31
  38. package/dist/tools/lpr-tool.js +71 -0
  39. package/dist/tools/policy-alerts-tool.js +32 -42
  40. package/dist/tools/reboot-cameras-tool.js +34 -0
  41. package/dist/tools/report-tool.js +190 -0
  42. package/dist/tools/time-conversion-tool.js +43 -0
  43. package/dist/tools/time-tool.js +17 -57
  44. package/dist/tools/update-tool.js +262 -0
  45. package/dist/transports/streamable-http.js +160 -57
  46. package/dist/{tools/devices/camera-tool/types.js → types/camera-tool-types.js} +13 -0
  47. package/dist/types/clips-tool-types.js +41 -0
  48. package/dist/types/create-camera-policy-tool-types.js +44 -0
  49. package/dist/types/create-tool-types.js +8 -0
  50. package/dist/types/deviceType.js +1 -0
  51. package/dist/types/endpoint-to-keys-tool-types.js +7 -0
  52. package/dist/types/entity-lookup-tool-types.js +70 -0
  53. package/dist/types/events-tools-types.js +257 -0
  54. package/dist/types/faces-tools-types.js +143 -0
  55. package/dist/types/get-entity-tool-types.js +25 -0
  56. package/dist/types/get-org-information-tool-types.js +3 -0
  57. package/dist/types/location-tool-types.js +11 -0
  58. package/dist/types/lpr-tool-types.js +97 -0
  59. package/dist/types/policy-alerts-tool-types.js +74 -0
  60. package/dist/types/reboot-cameras-tool-types.js +8 -0
  61. package/dist/types/report-tool-types.js +268 -0
  62. package/dist/types/schema-components.js +7093 -0
  63. package/dist/types/schema.js +1 -0
  64. package/dist/types/semantic-search-tool-types.js +5 -0
  65. package/dist/types/time-conversion-tool-types.js +8 -0
  66. package/dist/types/time-tool-types.js +11 -0
  67. package/dist/types/update-tool-types.js +186 -0
  68. package/dist/types/zod-schemas.js +21315 -0
  69. package/dist/types.js +17 -7
  70. package/dist/util.js +94 -2
  71. package/dist/utils/confirmation.js +1 -1
  72. package/dist/utils/reduce-output.js +35 -0
  73. package/dist/utils/remove-nulls.js +28 -0
  74. package/dist/utils/temp.js +8 -0
  75. package/dist/utils/timestampInput.js +12 -0
  76. package/package.json +23 -3
  77. package/dist/tools/devices/camera-tool/camera-tool.js +0 -218
  78. package/dist/tools/devices/get-entity-tool.js +0 -118
  79. package/dist/tools/get-org-information.js +0 -17
  80. package/dist/tools/reboot-cameras.js +0 -62
@@ -0,0 +1,261 @@
1
+ import * as fs from 'fs/promises';
2
+ import * as path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import faiss from 'faiss-node';
5
+ import { EmbeddingService } from './embedding-service.js';
6
+ // Constants following coding style guidelines
7
+ const FAISS_INDEX_TYPE = 'IndexFlatIP'; // Inner Product for cosine similarity
8
+ const SEARCH_RESULTS_LIMIT = 50;
9
+ const SIMILARITY_THRESHOLD = 0.3;
10
+ const EMBEDDING_DIMENSIONS = 1536; // text-embedding-3-small dimensions
11
+ // Get project root directory - resolves relative to the current module file
12
+ const __filename = fileURLToPath(import.meta.url);
13
+ const __dirname = path.dirname(__filename);
14
+ const PROJECT_ROOT = path.resolve(__dirname, '../..');
15
+ const EMBEDDINGS_DATA_PATH = path.join(PROJECT_ROOT, 'data/embeddings/embeddings.json');
16
+ const FAISS_INDEX_PATH = path.join(PROJECT_ROOT, 'data/embeddings/faiss.index');
17
+ export class FaissSearchService {
18
+ index = null;
19
+ embeddings = [];
20
+ embeddingService;
21
+ constructor() {
22
+ this.embeddingService = new EmbeddingService();
23
+ }
24
+ /**
25
+ * Load embeddings from file and build FAISS index
26
+ * @param embeddingsPath - Path to embeddings.json file (optional, defaults to project data directory)
27
+ */
28
+ async loadEmbeddings(embeddingsPath) {
29
+ const resolvedEmbeddingsPath = embeddingsPath || EMBEDDINGS_DATA_PATH;
30
+ console.log(`📚 Loading embeddings from ${resolvedEmbeddingsPath}...`);
31
+ try {
32
+ const fileContent = await fs.readFile(resolvedEmbeddingsPath, 'utf-8');
33
+ this.embeddings = JSON.parse(fileContent);
34
+ if (!Array.isArray(this.embeddings)) {
35
+ throw new Error('Embeddings file does not contain a valid array');
36
+ }
37
+ console.log(`✅ Loaded ${this.embeddings.length} embeddings`);
38
+ // Validate embedding dimensions
39
+ if (this.embeddings.length > 0) {
40
+ const firstEmbedding = this.embeddings[0];
41
+ if (!firstEmbedding.embedding || firstEmbedding.embedding.length !== EMBEDDING_DIMENSIONS) {
42
+ throw new Error(`Invalid embedding dimensions. Expected ${EMBEDDING_DIMENSIONS}, got ${firstEmbedding.embedding?.length || 0}`);
43
+ }
44
+ }
45
+ }
46
+ catch (error) {
47
+ throw new Error(`Failed to load embeddings: ${error instanceof Error ? error.message : 'Unknown error'}`);
48
+ }
49
+ }
50
+ /**
51
+ * Build FAISS index from loaded embeddings
52
+ */
53
+ async buildIndex() {
54
+ if (this.embeddings.length === 0) {
55
+ throw new Error('No embeddings loaded. Call loadEmbeddings() first.');
56
+ }
57
+ console.log(`🏗️ Building FAISS index with ${this.embeddings.length} vectors...`);
58
+ const startTime = Date.now();
59
+ try {
60
+ // Create FAISS index for inner product (cosine similarity with normalized vectors)
61
+ this.index = new faiss.IndexFlatIP(EMBEDDING_DIMENSIONS);
62
+ // Prepare embeddings matrix
63
+ const embeddingMatrix = new Float32Array(this.embeddings.length * EMBEDDING_DIMENSIONS);
64
+ for (let i = 0; i < this.embeddings.length; i++) {
65
+ const embedding = this.embeddings[i].embedding;
66
+ // Normalize the embedding for cosine similarity
67
+ const normalizedEmbedding = this.normalizeVector(embedding);
68
+ // Copy normalized embedding to matrix
69
+ for (let j = 0; j < EMBEDDING_DIMENSIONS; j++) {
70
+ embeddingMatrix[i * EMBEDDING_DIMENSIONS + j] = normalizedEmbedding[j];
71
+ }
72
+ }
73
+ // Add vectors to index
74
+ this.index.add(Array.from(embeddingMatrix));
75
+ const buildTime = Date.now() - startTime;
76
+ console.log(`✅ FAISS index built successfully in ${buildTime}ms`);
77
+ console.log(` 📊 Index type: ${FAISS_INDEX_TYPE}`);
78
+ console.log(` 📏 Dimensions: ${EMBEDDING_DIMENSIONS}`);
79
+ console.log(` 🔢 Total vectors: ${this.index.ntotal()}`);
80
+ }
81
+ catch (error) {
82
+ throw new Error(`Failed to build FAISS index: ${error instanceof Error ? error.message : 'Unknown error'}`);
83
+ }
84
+ }
85
+ /**
86
+ * Save FAISS index to file
87
+ * @param indexPath - Path to save the index file
88
+ */
89
+ async saveIndex(indexPath) {
90
+ if (!this.index) {
91
+ throw new Error('No index built. Call buildIndex() first.');
92
+ }
93
+ console.log(`💾 Saving FAISS index to ${indexPath}...`);
94
+ try {
95
+ // Ensure directory exists
96
+ const indexDir = path.dirname(indexPath);
97
+ await fs.mkdir(indexDir, { recursive: true });
98
+ // Save the index
99
+ this.index.write(indexPath);
100
+ console.log(`✅ Index saved successfully`);
101
+ }
102
+ catch (error) {
103
+ throw new Error(`Failed to save FAISS index: ${error instanceof Error ? error.message : 'Unknown error'}`);
104
+ }
105
+ }
106
+ /**
107
+ * Load FAISS index from file
108
+ * @param indexPath - Path to the index file (optional, defaults to project data directory)
109
+ */
110
+ async loadIndex(indexPath) {
111
+ const resolvedIndexPath = indexPath || FAISS_INDEX_PATH;
112
+ console.log(`📖 Loading FAISS index from ${resolvedIndexPath}...`);
113
+ try {
114
+ this.index = faiss.IndexFlatIP.read(resolvedIndexPath);
115
+ console.log(`✅ Index loaded successfully`);
116
+ console.log(` 📏 Dimensions: ${EMBEDDING_DIMENSIONS}`);
117
+ console.log(` 🔢 Total vectors: ${this.index.ntotal()}`);
118
+ // Note: IndexFlatIP doesn't expose dimension property directly
119
+ // We validate using our known dimensions constant
120
+ }
121
+ catch (error) {
122
+ throw new Error(`Failed to load FAISS index: ${error instanceof Error ? error.message : 'Unknown error'}`);
123
+ }
124
+ }
125
+ /**
126
+ * Search for similar vectors using natural language query
127
+ * @param params - Search parameters
128
+ * @returns Search results with metadata
129
+ */
130
+ async search(params) {
131
+ if (!this.index) {
132
+ throw new Error('No index loaded. Call loadIndex() or buildIndex() first.');
133
+ }
134
+ if (this.embeddings.length === 0) {
135
+ throw new Error('No embeddings metadata loaded. Call loadEmbeddings() first.');
136
+ }
137
+ const startTime = Date.now();
138
+ try {
139
+ // Generate embedding for the query
140
+ console.log(`🔍 Searching for: "${params.query}"`);
141
+ const queryEmbeddingResult = await this.embeddingService.generateSingleEmbedding(params.query, `query-${Date.now()}`);
142
+ // Normalize query embedding for cosine similarity
143
+ const normalizedQueryEmbedding = this.normalizeVector(queryEmbeddingResult.embedding);
144
+ // Determine search parameters
145
+ const searchLimit = Math.min(params.limit || SEARCH_RESULTS_LIMIT, this.embeddings.length);
146
+ const similarityThreshold = params.minSimilarity || SIMILARITY_THRESHOLD;
147
+ // Perform FAISS search
148
+ const searchResults = this.index.search(normalizedQueryEmbedding, searchLimit);
149
+ // Process results
150
+ const results = [];
151
+ let filteredCount = 0;
152
+ for (let i = 0; i < searchResults.labels.length; i++) {
153
+ const vectorIndex = searchResults.labels[i];
154
+ const similarity = searchResults.distances[i]; // Inner product similarity
155
+ // Skip invalid indices
156
+ if (vectorIndex < 0 || vectorIndex >= this.embeddings.length) {
157
+ continue;
158
+ }
159
+ // Apply similarity threshold
160
+ if (similarity < similarityThreshold) {
161
+ filteredCount++;
162
+ continue;
163
+ }
164
+ const embedding = this.embeddings[vectorIndex];
165
+ // Apply URL pattern filter if specified
166
+ if (params.urlPattern && !embedding.sourceUrl.includes(params.urlPattern)) {
167
+ filteredCount++;
168
+ continue;
169
+ }
170
+ results.push({
171
+ chunkText: embedding.chunkText,
172
+ sourceUrl: embedding.sourceUrl,
173
+ sourceTitle: embedding.sourceTitle,
174
+ similarity: similarity,
175
+ chunkId: embedding.chunkId,
176
+ chunkIndex: embedding.chunkIndex,
177
+ tokenCount: embedding.tokenCount,
178
+ metadata: embedding.metadata,
179
+ });
180
+ }
181
+ const searchTime = Date.now() - startTime;
182
+ const stats = {
183
+ totalVectors: this.index.ntotal(),
184
+ searchTime,
185
+ resultsReturned: results.length,
186
+ resultsFiltered: filteredCount,
187
+ };
188
+ console.log(`✅ Search completed in ${searchTime}ms`);
189
+ console.log(` 🔍 Query tokens: ${queryEmbeddingResult.tokenCount}`);
190
+ console.log(` 📊 Results found: ${results.length}`);
191
+ console.log(` 🚫 Results filtered: ${filteredCount}`);
192
+ return { results, stats };
193
+ }
194
+ catch (error) {
195
+ throw new Error(`Search failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
196
+ }
197
+ }
198
+ /**
199
+ * Get index statistics
200
+ */
201
+ getIndexStats() {
202
+ if (!this.index) {
203
+ return null;
204
+ }
205
+ return {
206
+ totalVectors: this.index.ntotal(),
207
+ dimensions: EMBEDDING_DIMENSIONS,
208
+ indexType: FAISS_INDEX_TYPE,
209
+ };
210
+ }
211
+ /**
212
+ * Normalize vector for cosine similarity computation
213
+ * @param vector - Input vector
214
+ * @returns Normalized vector
215
+ */
216
+ normalizeVector(vector) {
217
+ const magnitude = Math.sqrt(vector.reduce((sum, val) => sum + val * val, 0));
218
+ if (magnitude === 0) {
219
+ return vector.slice(); // Return copy of zero vector
220
+ }
221
+ return vector.map(val => val / magnitude);
222
+ }
223
+ /**
224
+ * Validate embeddings data integrity
225
+ */
226
+ validateEmbeddings() {
227
+ const errors = [];
228
+ if (this.embeddings.length === 0) {
229
+ errors.push('No embeddings loaded');
230
+ return { isValid: false, errors };
231
+ }
232
+ // Check each embedding
233
+ for (let i = 0; i < Math.min(this.embeddings.length, 10); i++) { // Sample first 10
234
+ const embedding = this.embeddings[i];
235
+ if (!embedding.chunkId) {
236
+ errors.push(`Embedding ${i}: Missing chunkId`);
237
+ }
238
+ if (!embedding.embedding || embedding.embedding.length !== EMBEDDING_DIMENSIONS) {
239
+ errors.push(`Embedding ${i}: Invalid embedding dimensions`);
240
+ }
241
+ if (!embedding.chunkText || embedding.chunkText.trim().length === 0) {
242
+ errors.push(`Embedding ${i}: Missing or empty chunkText`);
243
+ }
244
+ if (!embedding.sourceUrl) {
245
+ errors.push(`Embedding ${i}: Missing sourceUrl`);
246
+ }
247
+ }
248
+ return {
249
+ isValid: errors.length === 0,
250
+ errors,
251
+ };
252
+ }
253
+ /**
254
+ * Clean up resources
255
+ */
256
+ dispose() {
257
+ // FAISS resources are automatically cleaned up
258
+ this.index = null;
259
+ this.embeddings = [];
260
+ }
261
+ }
@@ -0,0 +1,100 @@
1
+ import { getLogger } from "../logger.js";
2
+ import { getCameraSettings, getImageForCameraAtTime } from "../api/camera-tool-api.js";
3
+ import { BASE_TOOL_ARGS } from "../types/camera-tool-types.js";
4
+ const TOOL_NAME = "camera-tool";
5
+ const TOOL_DESCRIPTION = `
6
+ This tool can perform some action pertaining to the video stream of a camera. There are two types of requests
7
+ that can be passed into "requestType":
8
+ - image
9
+ - get-settings
10
+
11
+ What follows is a description of the behavior of this tool given the requestType "image"
12
+
13
+ This tool should be used any time someone wants to specify a subset of cameras to use for a task, based on some features that the camera sees. For example, interior cameras, cameras facing the street, cameras with a view of X, Y, Z, etc.
14
+
15
+ For instance if someone says "I want X using cameras with Y" then this tool should get a snapshot of the image to answer the question of if the camera satisfies the Y predicate.
16
+
17
+ This tool captures and returns a real-time snapshot from a designated security camera.
18
+ The image reflects the current scene in the camera's field of view and serves as a contextual
19
+ input source for downstream tasks such as object recognition, anomaly detection, incident investigation,
20
+ or situational assessment. When invoked, the tool provides the following:
21
+ • Visual Scene Capture: A high-resolution image of what the camera is actively observing, including people, vehicles, license plates, and any detectable objects.
22
+
23
+ What follows is a description of the behavior of this tool given the requestType "get-settings"
24
+
25
+ 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.
26
+
27
+ NOTE: To update camera settings, use the update-tool instead.
28
+ `;
29
+ const logger = getLogger("camera-tool");
30
+ const TOOL_ARGS = BASE_TOOL_ARGS;
31
+ const TOOL_HANDLER = async (args, extra) => {
32
+ const { cameraUuid, timestampISO, requestType } = args;
33
+ if (!cameraUuid) {
34
+ return {
35
+ content: [
36
+ {
37
+ type: "text",
38
+ text: JSON.stringify({
39
+ needUserInput: true,
40
+ commandForUser: "Which camera are you talking about?",
41
+ }),
42
+ },
43
+ ],
44
+ };
45
+ }
46
+ let response;
47
+ const timestampMs = timestampISO
48
+ ? new Date(timestampISO).getTime()
49
+ : new Date().getTime() - 1000 * 60 * 5;
50
+ switch (requestType) {
51
+ case "image":
52
+ response = await getImageForCameraAtTime(cameraUuid, timestampMs, extra._meta?.requestModifiers, extra.sessionId);
53
+ if (!response.success || !response.imageData) {
54
+ return {
55
+ content: [{ type: "text", text: JSON.stringify(response) }],
56
+ };
57
+ }
58
+ logger.debug(`Received image response:\n ${JSON.stringify(response)}`);
59
+ return {
60
+ content: [
61
+ {
62
+ type: "image",
63
+ data: response.imageData,
64
+ mimeType: "image/jpeg",
65
+ },
66
+ {
67
+ type: "text",
68
+ text: JSON.stringify({
69
+ success: true,
70
+ status: "image-attached",
71
+ cameraUuid,
72
+ timestampMs,
73
+ }),
74
+ },
75
+ ],
76
+ };
77
+ case "get-settings":
78
+ response = await getCameraSettings(cameraUuid, extra._meta?.requestModifiers, extra.sessionId);
79
+ return {
80
+ content: [{ type: "text", text: JSON.stringify(response) }],
81
+ };
82
+ default:
83
+ response = {
84
+ error: true,
85
+ status: "missing unknown type from tool call",
86
+ };
87
+ break;
88
+ }
89
+ return {
90
+ content: [
91
+ {
92
+ type: "text",
93
+ text: JSON.stringify({ response }),
94
+ },
95
+ ],
96
+ };
97
+ };
98
+ export function createTool(server) {
99
+ server.tool(TOOL_NAME, TOOL_DESCRIPTION, TOOL_ARGS, TOOL_HANDLER);
100
+ }
@@ -1,37 +1,15 @@
1
- import { z } from "zod";
2
- import { postApi } from "../network.js";
3
- const ClipsArgs = z.object({
4
- deviceUuidFilters: z
5
- .array(z.string())
6
- .optional()
7
- .describe("A list of UUIDs representing specific devices to filter clips by. Only clips emitted by these devices will be returned."),
8
- locationUuidFilters: z
9
- .array(z.string())
10
- .optional()
11
- .describe("A list of UUIDs representing specific locations to filter clips by. Only clips associated with these locations will be returned."),
12
- searchFilter: z
13
- .string()
14
- .optional()
15
- .describe("A simple string to search for within the names of the clips."),
16
- timestampMsAfter: z
17
- .number()
18
- .describe("The start of the time range (in milliseconds since epoch) for which to retrieve clips. Only clips that occurred AFTER this timestamp will be returned."),
19
- timestampMsBefore: z
20
- .number()
21
- .describe("The end of the time range (in milliseconds since epoch) for which to retrieve clips. Only clips that occurred BEFORE this timestamp will be returned."),
22
- });
23
- async function getSavedClips(args, requestModifiers) {
24
- return await postApi("/event/getClipsWithProgress", args, requestModifiers);
25
- }
26
- export function createTool(server) {
27
- server.tool("clips-tool", `
1
+ import { ApiPayloadSchema, TOOL_ARGS } from "../types/clips-tool-types.js";
2
+ import { getSavedClips, getExpiringClips } from "../api/clips-tool-api.js";
3
+ const TOOL_NAME = "clips-tool";
4
+ const TOOL_DESCRIPTION = `
28
5
  Retrieves saved video clips from the Rhombus system. Saved clips can be viewed for up to 2 years and are typically found in the "Clips" tab of the "Saved Video" section of the Rhombus Console.
29
6
 
30
7
  This tool allows you to filter clips by:
8
+ * Whether or not they are expiring soon.
31
9
  * Specific devices using their UUIDs.
32
10
  * Specific locations using their UUIDs.
33
11
  * A simple string search on clip names.
34
- * A time range, specifying a start (timestampMsAfter) and/or end (timestampMsBefore) timestamp in milliseconds since epoch.
12
+ * A time range, specifying a start (timestampISOAfter) and/or end (timestampISOBefore) timestamp in ISO 8601 format.
35
13
 
36
14
  The tool returns a JSON object with the following structure and important fields:
37
15
  * **errorMsg (string | null):** An error message if the request failed.
@@ -43,19 +21,27 @@ The tool returns a JSON object with the following structure and important fields
43
21
  * **description (string | null):** An optional description for the clip.
44
22
  * **timestampMs (int64):** The start time of the video clip in milliseconds since epoch.
45
23
  * **createdAtMs (int64):** The creation timestamp of the clip in milliseconds since epoch.
24
+ * **createdAtTimestamp (string):** The creation timestamp of the clip in ISO 8601 format.
46
25
  * **deviceUuid (string):** The UUID of the primary device (e.g., camera) that recorded the clip.
47
26
  * **deviceUuids (array of strings or null):** A list of UUIDs for all devices associated with the clip.
48
27
  * **durationSec (int32):** The length of the video clip in seconds.
49
28
  * **status (string):** The current processing status of the clip, with possible values such as INITIATING, UPLOADING, RENDERING, FAILED, COMPLETE, OFFLINE, or UNKNOWN.
50
29
  * **userUuid (string | null):** The UUID of the user associated with the clip, if applicable.
51
30
  * **sourceAlertUuid (string | null):** The UUID of the alert that triggered the creation of this clip, if any.
52
- `, {
53
- ...ClipsArgs.shape,
54
- }, async ({ ...args }, extra) => {
55
- let ret;
56
- ret = await getSavedClips(args, extra._meta?.requestModifiers);
57
- return {
58
- content: [{ type: "text", text: JSON.stringify(ret) }],
59
- };
60
- });
31
+ `;
32
+ const TOOL_HANDLER = async (args, extra) => {
33
+ const payload = ApiPayloadSchema.parse(args);
34
+ let ret;
35
+ switch (args.queryType) {
36
+ case "saved":
37
+ ret = await getSavedClips(payload, extra._meta?.requestModifiers, extra.sessionId);
38
+ case "expiringSoon":
39
+ ret = await getExpiringClips(payload, extra._meta?.requestModifiers, extra.sessionId);
40
+ }
41
+ return {
42
+ content: [{ type: "text", text: JSON.stringify(ret) }],
43
+ };
44
+ };
45
+ export function createTool(server) {
46
+ server.tool(TOOL_NAME, TOOL_DESCRIPTION, TOOL_ARGS, TOOL_HANDLER);
61
47
  }
@@ -0,0 +1,25 @@
1
+ import { z } from "zod";
2
+ import { logger } from "../logger.js";
3
+ export function createTool(server) {
4
+ server.tool("count-tool", `
5
+ This tool counts the number of items by accepting an array of UUIDs. It can count anything that has UUIDs - users, devices,
6
+ records, or any other entities. Simply provide an array of UUID strings and it will return the precise count.
7
+ `, {
8
+ uuids: z
9
+ .array(z.string().describe("UUID string of an individual item"))
10
+ .describe("An array of UUID strings representing the items to count. Each string should be a valid UUID."),
11
+ }, async ({ uuids }) => {
12
+ try {
13
+ logger.info("Counting UUIDs", uuids);
14
+ return {
15
+ content: [{ type: "text", text: `Count: ${uuids.length}` }],
16
+ };
17
+ }
18
+ catch (e) {
19
+ const errorMessage = e instanceof Error ? e.message : `Unknown error: ${e}`;
20
+ return {
21
+ content: [{ type: "text", text: `Error counting UUIDs: ${errorMessage}` }],
22
+ };
23
+ }
24
+ });
25
+ }
@@ -0,0 +1,214 @@
1
+ import { z } from "zod";
2
+ import { createCameraPolicy } from "../api/create-camera-policy-tool-api.js";
3
+ import { ApiPayloadSchema, OUTPUT_SCHEMA } from "../types/create-camera-policy-tool-types.js";
4
+ import { postApi } from "../network.js";
5
+ const TOOL_NAME = "create-camera-policy-tool";
6
+ const TOOL_DESCRIPTION = `
7
+ A tool for creating a camera policy that walks users through a multi-step process.
8
+
9
+ The step begins with the user providing a policy name, description, and organization UUID.
10
+ Then, the user is presented with a form to configure the schedules for the policy.
11
+ Finally, the user is presented with a form to assign the policy to cameras.
12
+
13
+ Uses elicitation forms for rich user interaction.
14
+ `;
15
+ const TOOL_ARGS = {
16
+ // Step 1: Policy creation
17
+ name: z.string().describe("Policy name (for creating policy)"),
18
+ description: z.string().describe("Policy description (for creating policy)"),
19
+ orgUuid: z.string().describe("Organization UUID (for creating policy)"),
20
+ // Step 2: Schedule configuration
21
+ policyUuid: z.string().describe("Policy UUID (for configuring schedules)"),
22
+ scheduleConfigs: z.string().describe("JSON string of schedule configurations"),
23
+ // Step 3: Camera assignment
24
+ cameraUuids: z.string().describe("Comma-separated camera UUIDs to assign policy to"),
25
+ policyName: z.string().describe("Policy name (for reference)"),
26
+ };
27
+ const TOOL_ARGS_SCHEMA = z.object(TOOL_ARGS);
28
+ const TOOL_HANDLER = async (args, extra) => {
29
+ const { name, description, orgUuid, policyUuid, scheduleConfigs, cameraUuids, policyName } = args;
30
+ // Step 3: Camera assignment (if cameraUuids provided)
31
+ if (cameraUuids?.trim()) {
32
+ try {
33
+ const cameraList = cameraUuids
34
+ .split(",")
35
+ .map((uuid) => uuid.trim())
36
+ .filter((uuid) => uuid);
37
+ if (cameraList.length > 0) {
38
+ const cameraPayload = {
39
+ cameraBulkDetails: cameraList.map(cameraUuid => ({
40
+ uuid: cameraUuid,
41
+ policyUuid: policyUuid,
42
+ policyUuidUpdated: true,
43
+ })),
44
+ };
45
+ await postApi({
46
+ route: "/camera/updateDetailsBulkV2",
47
+ body: cameraPayload,
48
+ modifiers: extra._meta?.requestModifiers,
49
+ sessionId: extra.sessionId,
50
+ });
51
+ const jsonResultResponse = {
52
+ needUserInput: false,
53
+ message: `Excellent! Policy created and assigned to ${cameraList.length} camera(s)!`,
54
+ policyUuid,
55
+ policyName,
56
+ };
57
+ return {
58
+ content: [
59
+ {
60
+ type: "text",
61
+ text: `🎉 Camera policy setup completely finished!\n\n✅ Policy "${policyName}" assigned to ${cameraList.length} camera(s)\n✅ Policy is now fully active\n\nYour cameras will now generate alerts according to the policy configuration.`,
62
+ },
63
+ ],
64
+ structuredContent: jsonResultResponse,
65
+ };
66
+ }
67
+ }
68
+ catch (error) {
69
+ return {
70
+ content: [
71
+ {
72
+ type: "text",
73
+ text: `Failed to assign policy to cameras: ${error instanceof Error ? error.message : "Unknown error"}`,
74
+ },
75
+ ],
76
+ };
77
+ }
78
+ }
79
+ // Step 2: Schedule configuration (if scheduleConfigs provided and non-empty)
80
+ if (scheduleConfigs?.trim() && scheduleConfigs.trim() !== "[]") {
81
+ try {
82
+ const configs = JSON.parse(scheduleConfigs);
83
+ // Only proceed if we have actual schedule configurations
84
+ if (configs.length > 0) {
85
+ const scheduledTriggers = configs.map(config => ({
86
+ scheduleUuid: config.scheduleUuid,
87
+ triggerSet: config.activities.map(activity => ({ activity })),
88
+ }));
89
+ const payload = {
90
+ policy: { uuid: policyUuid, scheduledTriggers },
91
+ };
92
+ const result = await postApi({
93
+ route: "/policy/updateCameraPolicy",
94
+ body: payload,
95
+ modifiers: extra._meta?.requestModifiers,
96
+ sessionId: extra.sessionId,
97
+ });
98
+ if (result.error) {
99
+ return {
100
+ content: [
101
+ {
102
+ type: "text",
103
+ text: `Failed to configure policy schedules: ${result.errorMsg || "Unknown error"}`,
104
+ },
105
+ ],
106
+ };
107
+ }
108
+ // Success - now show camera assignment form
109
+ const jsonResultResponse = {
110
+ needUserInput: true,
111
+ message: `Excellent! Policy schedules configured.\n\nFinal step: Please select which cameras should use this policy.`,
112
+ requestType: "camera-assignment",
113
+ submitAction: "create-camera-policy-tool",
114
+ policyUuid,
115
+ policyName,
116
+ };
117
+ return {
118
+ content: [
119
+ {
120
+ type: "text",
121
+ text: JSON.stringify(jsonResultResponse),
122
+ },
123
+ ],
124
+ structuredContent: jsonResultResponse,
125
+ };
126
+ }
127
+ // If configs.length === 0, fall through to policy creation
128
+ }
129
+ catch (error) {
130
+ return {
131
+ content: [
132
+ {
133
+ type: "text",
134
+ text: `Error configuring schedules: ${error instanceof Error ? error.message : "Unknown error"}`,
135
+ },
136
+ ],
137
+ };
138
+ }
139
+ }
140
+ // Step 1: Policy creation (if name provided)
141
+ if (name?.trim()) {
142
+ try {
143
+ const payload = ApiPayloadSchema.parse({
144
+ policy: {
145
+ name,
146
+ description: description?.trim() ? description : undefined,
147
+ orgUuid: orgUuid?.trim() ? orgUuid : undefined,
148
+ scheduledTriggers: [],
149
+ },
150
+ });
151
+ const result = await createCameraPolicy(payload, extra._meta?.requestModifiers, extra.sessionId);
152
+ if (result.error) {
153
+ return {
154
+ content: [
155
+ {
156
+ type: "text",
157
+ text: `Failed to create camera policy: ${result.errorMsg || "Unknown error"}`,
158
+ },
159
+ ],
160
+ };
161
+ }
162
+ console.error(`[createCameraPolicyTool] -- Proceeding with schedule-trigger-configuration form. Got result ${JSON.stringify(result)}`);
163
+ // Success - now show schedule configuration form
164
+ const jsonResultResponse = {
165
+ needUserInput: true,
166
+ message: `Great! Your policy "${name}" was created with UUID: ${result.policyUuid}\n\nNext, let's configure when this policy should be active and what activities should trigger alerts.`,
167
+ requestType: "schedule-trigger-configuration",
168
+ submitAction: "create-camera-policy-tool",
169
+ policyUuid: result.policyUuid,
170
+ policyName: name,
171
+ };
172
+ return {
173
+ content: [
174
+ {
175
+ type: "text",
176
+ text: JSON.stringify(jsonResultResponse),
177
+ },
178
+ ],
179
+ structuredContent: jsonResultResponse,
180
+ };
181
+ }
182
+ catch (error) {
183
+ return {
184
+ content: [
185
+ {
186
+ type: "text",
187
+ text: `Error creating camera policy: ${error instanceof Error ? error.message : "Unknown error"}`,
188
+ },
189
+ ],
190
+ };
191
+ }
192
+ }
193
+ // Step 0: Show initial form (no args provided)
194
+ return {
195
+ content: [
196
+ {
197
+ type: "text",
198
+ text: JSON.stringify({
199
+ needUserInput: true,
200
+ message: "Please provide the following information to create your camera policy:\n\n1. **Policy Name** (required): A descriptive name for the policy\n2. **Policy Description** (optional): What this policy does\n3. **Organization UUID** (optional): Leave blank to use your current organization\n\nOnce you provide this information, I'll create the policy for you.",
201
+ requestType: "policy-creation-form",
202
+ submitAction: "create-camera-policy-tool",
203
+ }),
204
+ },
205
+ ],
206
+ };
207
+ };
208
+ export function createTool(server) {
209
+ server.registerTool(TOOL_NAME, {
210
+ description: TOOL_DESCRIPTION,
211
+ inputSchema: TOOL_ARGS,
212
+ outputSchema: OUTPUT_SCHEMA.shape,
213
+ }, TOOL_HANDLER);
214
+ }