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.
Files changed (81) hide show
  1. package/README.md +11 -3
  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 +50 -0
  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 +25 -32
  22. package/dist/logger.js +5 -4
  23. package/dist/network.js +45 -14
  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 -38
  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 -75
  32. package/dist/tools/entity-lookup-tool.js +35 -0
  33. package/dist/tools/events-tool.js +188 -93
  34. package/dist/tools/faces-tool.js +59 -133
  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 -32
  38. package/dist/tools/lpr-tool.js +71 -0
  39. package/dist/tools/policy-alerts-tool.js +32 -43
  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 -55
  44. package/dist/tools/update-tool.js +262 -0
  45. package/dist/transports/stdio.js +10 -0
  46. package/dist/transports/streamable-http.js +201 -0
  47. package/dist/{tools/devices/camera-tool/types.js → types/camera-tool-types.js} +13 -0
  48. package/dist/types/clips-tool-types.js +41 -0
  49. package/dist/types/create-camera-policy-tool-types.js +44 -0
  50. package/dist/types/create-tool-types.js +8 -0
  51. package/dist/types/deviceType.js +1 -0
  52. package/dist/types/endpoint-to-keys-tool-types.js +7 -0
  53. package/dist/types/entity-lookup-tool-types.js +70 -0
  54. package/dist/types/events-tools-types.js +257 -0
  55. package/dist/types/faces-tools-types.js +143 -0
  56. package/dist/types/get-entity-tool-types.js +25 -0
  57. package/dist/types/get-org-information-tool-types.js +3 -0
  58. package/dist/types/location-tool-types.js +11 -0
  59. package/dist/types/lpr-tool-types.js +97 -0
  60. package/dist/types/policy-alerts-tool-types.js +74 -0
  61. package/dist/types/reboot-cameras-tool-types.js +8 -0
  62. package/dist/types/report-tool-types.js +268 -0
  63. package/dist/types/schema-components.js +7093 -0
  64. package/dist/types/schema.js +1 -0
  65. package/dist/types/semantic-search-tool-types.js +5 -0
  66. package/dist/types/time-conversion-tool-types.js +8 -0
  67. package/dist/types/time-tool-types.js +11 -0
  68. package/dist/types/update-tool-types.js +186 -0
  69. package/dist/types/zod-schemas.js +21315 -0
  70. package/dist/types.js +17 -7
  71. package/dist/util.js +96 -14
  72. package/dist/utils/confirmation.js +1 -7
  73. package/dist/utils/reduce-output.js +35 -0
  74. package/dist/utils/remove-nulls.js +28 -0
  75. package/dist/utils/temp.js +8 -0
  76. package/dist/utils/timestampInput.js +12 -0
  77. package/package.json +28 -3
  78. package/dist/tools/devices/camera-tool/camera-tool.js +0 -218
  79. package/dist/tools/devices/get-entity-tool.js +0 -117
  80. package/dist/tools/get-org-information.js +0 -18
  81. package/dist/tools/reboot-cameras.js +0 -63
@@ -0,0 +1,262 @@
1
+ import { updateCameraConfig, getCameraDetails, cleanUpdatePayload, formatCameraSettings, } from "../api/update-tool-api.js";
2
+ import { OUTPUT_SCHEMA, TOOL_ARGS, UpdateCameraConfigPayload, CameraVideoSettings, CameraAudioSettings, CameraDeviceSettings, parseFacetedUuid, } from "../types/update-tool-types.js";
3
+ import { logger } from "../logger.js";
4
+ const TOOL_NAME = "update-tool";
5
+ const TOOL_DESCRIPTION = `
6
+ This tool allows updating configuration settings for various Rhombus entities. Currently supports:
7
+ - **Cameras**: Update video settings (resolution, HDR, WDR, brightness, contrast, etc.), audio settings (recording, microphone, speaker), and device settings (name, timezone, LED control).
8
+
9
+ For LED control, use EXACTLY these field names in cameraDeviceSettings:
10
+ - To turn LED off: {"led_stealth_mode": true} (recommended) or {"led_mode": "always_off"}
11
+ - To turn LED on: {"led_stealth_mode": false} or {"led_mode": "always_on"} or {"led_mode": "auto"}
12
+ - IMPORTANT: Use underscore in field names (led_mode, led_stealth_mode), not camelCase
13
+
14
+ The tool supports faceted UUIDs (e.g., "cameraUuid.v0" or "cameraUuid.v1") to update specific camera facets. If no facet is specified, defaults to "v0".
15
+
16
+ The tool guides users through a multi-step process:
17
+ 1. Entity selection (if not provided)
18
+ 2. Settings configuration with current values shown
19
+ 3. Confirmation and application of changes
20
+
21
+ Future support planned for:
22
+ - Climate sensors
23
+ - Door controllers
24
+ - Environmental gateways
25
+
26
+ The tool uses elicitation forms for rich user interaction and shows current settings before updates.
27
+ `;
28
+ const TOOL_HANDLER = async (args, extra) => {
29
+ const { entityType, entityUuid, cameraVideoSettings, cameraAudioSettings, cameraDeviceSettings, step, } = args;
30
+ // Handle camera updates
31
+ if (entityType === "camera") {
32
+ // Step 3: Apply settings (if settings provided)
33
+ if (entityUuid &&
34
+ ((cameraVideoSettings && cameraVideoSettings.trim()) ||
35
+ (cameraAudioSettings && cameraAudioSettings.trim()) ||
36
+ (cameraDeviceSettings && cameraDeviceSettings.trim()))) {
37
+ try {
38
+ // Parse the faceted UUID to extract base UUID and facet
39
+ const { baseUuid, facet } = parseFacetedUuid(entityUuid);
40
+ const updatePayload = {
41
+ configUpdate: {
42
+ deviceUuid: baseUuid,
43
+ },
44
+ };
45
+ // Parse and add video settings
46
+ if (cameraVideoSettings && cameraVideoSettings.trim()) {
47
+ const videoSettings = JSON.parse(cameraVideoSettings);
48
+ // Validate with zod schema
49
+ const validatedVideo = CameraVideoSettings.parse(videoSettings);
50
+ updatePayload.configUpdate.videoFacetSettings = {
51
+ [facet]: cleanUpdatePayload(validatedVideo),
52
+ };
53
+ }
54
+ // Parse and add audio settings
55
+ if (cameraAudioSettings && cameraAudioSettings.trim()) {
56
+ const audioSettings = JSON.parse(cameraAudioSettings);
57
+ // Validate with zod schema
58
+ const validatedAudio = CameraAudioSettings.parse(audioSettings);
59
+ updatePayload.configUpdate.audioFacetSettings = {
60
+ [facet]: cleanUpdatePayload(validatedAudio),
61
+ };
62
+ }
63
+ // Parse and add device settings
64
+ if (cameraDeviceSettings && cameraDeviceSettings.trim()) {
65
+ const deviceSettings = JSON.parse(cameraDeviceSettings);
66
+ logger.debug("[update-tool] Raw device settings:", deviceSettings);
67
+ // Transform ledMode to led_mode and OFF to always_off before validation
68
+ if ("ledMode" in deviceSettings) {
69
+ deviceSettings.led_mode =
70
+ deviceSettings.ledMode === "OFF" ? "always_off" : deviceSettings.ledMode;
71
+ delete deviceSettings.ledMode;
72
+ }
73
+ // Convert string "true"/"false" to boolean for led_stealth_mode
74
+ if ("led_stealth_mode" in deviceSettings &&
75
+ typeof deviceSettings.led_stealth_mode === "string") {
76
+ deviceSettings.led_stealth_mode = deviceSettings.led_stealth_mode === "true";
77
+ }
78
+ // Validate with zod schema
79
+ const validatedDevice = CameraDeviceSettings.parse(deviceSettings);
80
+ logger.debug("[update-tool] Validated device settings:", validatedDevice);
81
+ const cleaned = cleanUpdatePayload(validatedDevice);
82
+ logger.debug("[update-tool] Cleaned device settings:", cleaned);
83
+ updatePayload.configUpdate.deviceSettings = cleaned;
84
+ }
85
+ // Validate the full payload
86
+ const validatedPayload = UpdateCameraConfigPayload.parse(updatePayload);
87
+ // Apply the updates
88
+ const result = await updateCameraConfig(validatedPayload, extra._meta?.requestModifiers, extra.sessionId);
89
+ if (!result.success) {
90
+ return {
91
+ content: [
92
+ {
93
+ type: "text",
94
+ text: `Failed to update camera settings: ${result.error}`,
95
+ },
96
+ ],
97
+ };
98
+ }
99
+ // Format the updated settings for display
100
+ const formattedSettings = formatCameraSettings(validatedPayload.configUpdate);
101
+ const jsonResultResponse = {
102
+ needUserInput: false,
103
+ success: true,
104
+ message: `✅ Camera settings updated successfully!\n\n${formattedSettings}`,
105
+ entityType: "camera",
106
+ entityUuid,
107
+ updatedSettings: validatedPayload.configUpdate,
108
+ };
109
+ return {
110
+ content: [
111
+ {
112
+ type: "text",
113
+ text: jsonResultResponse.message,
114
+ },
115
+ ],
116
+ structuredContent: jsonResultResponse,
117
+ };
118
+ }
119
+ catch (error) {
120
+ return {
121
+ content: [
122
+ {
123
+ type: "text",
124
+ text: `Error updating camera settings: ${error instanceof Error ? error.message : "Unknown error"}`,
125
+ },
126
+ ],
127
+ };
128
+ }
129
+ }
130
+ // Step 2: Show settings configuration form (if entityUuid provided but no settings)
131
+ if (entityUuid && step !== "entity-selection") {
132
+ try {
133
+ // Parse the faceted UUID to extract base UUID
134
+ const { baseUuid } = parseFacetedUuid(entityUuid);
135
+ // Get current camera details
136
+ const cameraDetails = await getCameraDetails(baseUuid, extra._meta?.requestModifiers, extra.sessionId);
137
+ if (!cameraDetails.success || !cameraDetails.data) {
138
+ return {
139
+ content: [
140
+ {
141
+ type: "text",
142
+ text: `Failed to get camera details: ${cameraDetails.error || "Camera not found"}`,
143
+ },
144
+ ],
145
+ };
146
+ }
147
+ const camera = cameraDetails.data;
148
+ const currentVideoSettings = camera.videoSettings || {};
149
+ const currentAudioSettings = camera.audioSettings || {};
150
+ const currentDeviceSettings = {
151
+ camera_name: camera.name,
152
+ camera_timezone: camera.timezone,
153
+ led_intensity: camera.ledIntensity,
154
+ led_mode: camera.ledMode,
155
+ };
156
+ const jsonResultResponse = {
157
+ needUserInput: true,
158
+ message: `Configure settings for camera "${camera.name}" (${entityUuid})\n\nCurrent settings are shown below. Modify the values you want to change:`,
159
+ requestType: "camera-settings-configuration",
160
+ submitAction: "update-tool",
161
+ entityType: "camera",
162
+ entityUuid,
163
+ currentSettings: {
164
+ video: currentVideoSettings,
165
+ audio: currentAudioSettings,
166
+ device: currentDeviceSettings,
167
+ },
168
+ };
169
+ return {
170
+ content: [
171
+ {
172
+ type: "text",
173
+ text: JSON.stringify(jsonResultResponse),
174
+ },
175
+ ],
176
+ structuredContent: jsonResultResponse,
177
+ };
178
+ }
179
+ catch (error) {
180
+ return {
181
+ content: [
182
+ {
183
+ type: "text",
184
+ text: `Error retrieving camera details: ${error instanceof Error ? error.message : "Unknown error"}`,
185
+ },
186
+ ],
187
+ };
188
+ }
189
+ }
190
+ // Step 1: Entity selection (if no entityUuid)
191
+ if (!entityUuid) {
192
+ const jsonResultResponse = {
193
+ needUserInput: true,
194
+ message: "Please provide the camera UUID you want to update.\n\nYou can provide a faceted UUID (e.g., 'cameraUuid.v0' or 'cameraUuid.v1') to update a specific facet, or just the base UUID to update facet v0 by default.\n\nYou can find the camera UUID in the Rhombus console or by using the search tools.",
195
+ requestType: "entity-selection",
196
+ submitAction: "update-tool",
197
+ entityType: "camera",
198
+ };
199
+ return {
200
+ content: [
201
+ {
202
+ type: "text",
203
+ text: JSON.stringify(jsonResultResponse),
204
+ },
205
+ ],
206
+ structuredContent: jsonResultResponse,
207
+ };
208
+ }
209
+ }
210
+ // Handle other entity types (future implementation)
211
+ if (entityType === "climate-sensor") {
212
+ return {
213
+ content: [
214
+ {
215
+ type: "text",
216
+ text: "Climate sensor updates are not yet implemented. Coming soon!",
217
+ },
218
+ ],
219
+ };
220
+ }
221
+ if (entityType === "door-controller") {
222
+ return {
223
+ content: [
224
+ {
225
+ type: "text",
226
+ text: "Door controller updates are not yet implemented. Coming soon!",
227
+ },
228
+ ],
229
+ };
230
+ }
231
+ if (entityType === "environmental-gateway") {
232
+ return {
233
+ content: [
234
+ {
235
+ type: "text",
236
+ text: "Environmental gateway updates are not yet implemented. Coming soon!",
237
+ },
238
+ ],
239
+ };
240
+ }
241
+ // Step 0: Show initial form (no entityType provided)
242
+ return {
243
+ content: [
244
+ {
245
+ type: "text",
246
+ text: JSON.stringify({
247
+ needUserInput: true,
248
+ message: "Welcome to the Rhombus entity update tool!\n\nWhat type of entity would you like to update?\n\n• **camera** - Update camera video, audio, or device settings\n• **climate-sensor** - Update climate sensor settings (coming soon)\n• **door-controller** - Update door controller settings (coming soon)\n• **environmental-gateway** - Update environmental gateway settings (coming soon)\n\nPlease specify the entity type to continue.",
249
+ requestType: "entity-type-selection",
250
+ submitAction: "update-tool",
251
+ }),
252
+ },
253
+ ],
254
+ };
255
+ };
256
+ export function createTool(server) {
257
+ server.registerTool(TOOL_NAME, {
258
+ description: TOOL_DESCRIPTION,
259
+ inputSchema: TOOL_ARGS,
260
+ outputSchema: OUTPUT_SCHEMA.shape,
261
+ }, TOOL_HANDLER);
262
+ }
@@ -0,0 +1,10 @@
1
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2
+ import createServer from "../createServer.js";
3
+ import { logger } from "../logger.js";
4
+ export default async function stdioTransport() {
5
+ const server = await createServer();
6
+ const transport = new StdioServerTransport();
7
+ logger.info(`🚙 Starting stdio transport`);
8
+ await server.connect(transport);
9
+ logger.info(`🚙🌬️ Connected.`);
10
+ }
@@ -0,0 +1,201 @@
1
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
2
+ import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
3
+ import cors from "cors";
4
+ import express from "express";
5
+ import crypto from "node:crypto";
6
+ import createServer from "../createServer.js";
7
+ import { logger } from "../logger.js";
8
+ var AuthScheme;
9
+ (function (AuthScheme) {
10
+ AuthScheme["API_TOKEN"] = "api-token";
11
+ AuthScheme["CHATBOT"] = "chatbot";
12
+ AuthScheme["WEB2"] = "web2";
13
+ })(AuthScheme || (AuthScheme = {}));
14
+ export const authStore = new Map();
15
+ const transports = new Map();
16
+ export default function streamableHttpTransport() {
17
+ const app = express();
18
+ app.use(express.json());
19
+ app.use(cors({
20
+ // TODO: domain
21
+ origin: ["*"],
22
+ exposedHeaders: ["mcp-session-id"],
23
+ allowedHeaders: ["Content-Type", "mcp-session-id"],
24
+ }));
25
+ /**
26
+ * STATEFUL ENDPOINT
27
+ */
28
+ const authRequired = ["tools/call"];
29
+ app.post("/mcp", async (req, res) => {
30
+ logger.info(`Received MCP request`, JSON.stringify(req.body));
31
+ // Check for existing session ID
32
+ const sessionId = req.headers["mcp-session-id"];
33
+ let transport;
34
+ if (sessionId && transports.has(sessionId)) {
35
+ // Reuse existing transport
36
+ transport = transports.get(sessionId);
37
+ }
38
+ else if (!sessionId && isInitializeRequest(req.body)) {
39
+ // New initialization request
40
+ transport = new StreamableHTTPServerTransport({
41
+ sessionIdGenerator: crypto.randomUUID,
42
+ onsessioninitialized: sessionId => {
43
+ // Store the transport by session ID
44
+ transports.set(sessionId, transport);
45
+ logger.info(`🔒 MCP request initialized with sessionId: ${sessionId}`);
46
+ try {
47
+ const authScheme = req.headers["x-auth-scheme"] ?? "api-token";
48
+ // if api key is provided
49
+ if (authScheme === AuthScheme.API_TOKEN) {
50
+ const apiKey = "x-auth-apikey" in req.headers
51
+ ? req.headers["x-auth-apikey"]
52
+ : process.env.RHOMBUS_API_KEY;
53
+ if (!apiKey) {
54
+ throw new Error("Invalid API Key provided! Please check the headers or environment variables");
55
+ }
56
+ logger.info(`🔒 MCP request authenticated with api key: ${apiKey}`);
57
+ authStore.set(sessionId, {
58
+ apiKey: apiKey,
59
+ createdMs: Date.now(),
60
+ });
61
+ }
62
+ else if (authScheme === AuthScheme.CHATBOT &&
63
+ "x-auth-session" in req.headers &&
64
+ "x-auth-chat" in req.headers) {
65
+ logger.info(`🔒 MCP request authenticated with x-auth-session: ${req.headers["x-auth-session"]} and x-auth-chat: ${req.headers["x-auth-chat"]}`);
66
+ // otherwise, store the sessionId and latestRecordUuid in authStore
67
+ authStore.set(sessionId, {
68
+ sessionId: req.headers["x-auth-session"],
69
+ latestRecordUuid: req.headers["x-auth-chat"],
70
+ createdMs: Date.now(),
71
+ });
72
+ }
73
+ else if (authScheme === AuthScheme.WEB2 && "x-auth-cookie" in req.headers) {
74
+ authStore.set(sessionId, {
75
+ cookie: req.headers["x-auth-cookie"],
76
+ createdMs: Date.now(),
77
+ });
78
+ }
79
+ else {
80
+ throw new Error(`Invalid auth scheme provided! x-auth-scheme: ${req.headers["x-auth-scheme"]}, x-auth-session: ${req.headers["x-auth-session"]}, x-auth-chat: ${req.headers["x-auth-chat"]}`);
81
+ }
82
+ }
83
+ catch (e) {
84
+ // only throw if this is an auth required call
85
+ if (authRequired.includes(req.body.method)) {
86
+ if (e instanceof Error) {
87
+ logger.error(e.message);
88
+ }
89
+ else {
90
+ logger.error(e);
91
+ }
92
+ throw e;
93
+ }
94
+ }
95
+ },
96
+ // DNS rebinding protection is disabled by default for backwards compatibility. If you are running this server
97
+ // locally, make sure to set:
98
+ // enableDnsRebindingProtection: true,
99
+ // allowedHosts: ['127.0.0.1'],
100
+ });
101
+ // Clean up transport when closed
102
+ transport.onclose = () => {
103
+ if (transport.sessionId) {
104
+ transports.delete(transport.sessionId);
105
+ authStore.delete(transport.sessionId);
106
+ }
107
+ };
108
+ const server = await createServer();
109
+ // Connect to the MCP server
110
+ await server.connect(transport);
111
+ logger.info(`🔗 Transport connected with sessionId: ${sessionId}`);
112
+ }
113
+ else {
114
+ // Invalid request
115
+ res.status(400).json({
116
+ jsonrpc: "2.0",
117
+ error: {
118
+ code: -32000,
119
+ message: "Bad Request: No valid session ID provided",
120
+ },
121
+ id: null,
122
+ });
123
+ return;
124
+ }
125
+ // Handle the request
126
+ // modify request body to include meta
127
+ // if (req?.body?.params) {
128
+ // req.body.params._meta = {
129
+ // ...(req.body.params._meta ?? {}),
130
+ // foo: "bar",
131
+ // };
132
+ // }
133
+ // logger.trace("blah blah blah", JSON.stringify(req.body, null, 2));
134
+ await transport.handleRequest(req, res, req.body);
135
+ });
136
+ app.get("/mcp", async (_, res) => {
137
+ logger.warn("Received Not Allowed GET MCP request");
138
+ res.writeHead(405).end(JSON.stringify({
139
+ jsonrpc: "2.0",
140
+ error: {
141
+ code: -32000,
142
+ message: "Method not allowed.",
143
+ },
144
+ id: null,
145
+ }));
146
+ });
147
+ app.delete("/mcp", async (req, res) => {
148
+ logger.warn("Received Not Allowed DELETE MCP request");
149
+ res.writeHead(405).end(JSON.stringify({
150
+ jsonrpc: "2.0",
151
+ error: {
152
+ code: -32000,
153
+ message: "Method not allowed.",
154
+ },
155
+ id: null,
156
+ }));
157
+ });
158
+ /**
159
+ * STATELESS ENDPOINT
160
+ */
161
+ app.post("/mcp-stateless", async (req, res) => {
162
+ logger.info(`Received stateless MCP request`, req.body);
163
+ let transport;
164
+ transport = new StreamableHTTPServerTransport({
165
+ sessionIdGenerator: undefined,
166
+ });
167
+ const server = await createServer();
168
+ // Connect to the MCP server
169
+ await server.connect(transport);
170
+ logger.info(`🔗 Stateless Transport connected`);
171
+ // Handle the request
172
+ await transport.handleRequest(req, res, req.body);
173
+ });
174
+ app.get("/mcp-stateless", async (req, res) => {
175
+ logger.warn("Received Not Allowed GET MCP request");
176
+ res.writeHead(405).end(JSON.stringify({
177
+ jsonrpc: "2.0",
178
+ error: {
179
+ code: -32000,
180
+ message: "Method not allowed.",
181
+ },
182
+ id: null,
183
+ }));
184
+ });
185
+ app.delete("/mcp-stateless", async (req, res) => {
186
+ logger.warn("Received Not Allowed DELETE MCP request");
187
+ res.writeHead(405).end(JSON.stringify({
188
+ jsonrpc: "2.0",
189
+ error: {
190
+ code: -32000,
191
+ message: "Method not allowed.",
192
+ },
193
+ id: null,
194
+ }));
195
+ });
196
+ // Start the server
197
+ const PORT = process.env.PORT || 3000;
198
+ app.listen(PORT, () => {
199
+ logger.info(`rhombus-node-mcp listening on port ${PORT}`);
200
+ });
201
+ }
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { ISOTimestampFormatDescription } from "../utils/timestampInput.js";
2
3
  export const VideoFacetSettings = z
3
4
  .object({
4
5
  // blocked_debounce_time_ms: z.number().int().nullable(),
@@ -170,3 +171,15 @@ export const ExternalUpdateableFacetedUserConfigSchema = z.object({
170
171
  })
171
172
  .describe("For each of the object keys: v0, v1, v2, v3, they correspond to a specific facet of the camera, since a camera may have multiple facets, up to 4. Hence, why they are zero-indexed."),
172
173
  });
174
+ export const BASE_TOOL_ARGS = {
175
+ cameraUuid: z.string().nullable().describe("the camera uuid requested"),
176
+ timestampISO: z
177
+ .string()
178
+ .datetime({ message: "Invalid ISO 8601 date format.", offset: true })
179
+ .nullable()
180
+ .describe(`
181
+ the timestamp for the image. This will default to 5 minutes before the current time. You can also call time-tool to parse the user's time description.
182
+ ` + ISOTimestampFormatDescription),
183
+ requestType: z.enum(["image", "get-settings"]),
184
+ };
185
+ const BASE_TOOL_ARGS_SCHEMA = z.object(BASE_TOOL_ARGS);
@@ -0,0 +1,41 @@
1
+ import { z } from "zod";
2
+ import { createEpochSchema, ISOTimestampFormatDescription } from "../utils/timestampInput.js";
3
+ import { createUuidSchema } from "../types.js";
4
+ export const TOOL_ARGS = {
5
+ queryType: z
6
+ .enum(["saved", "expiringSoon"])
7
+ .describe('The type of clips to retrieve. Use "saved" to get regular saved clips, and "expiringSoon" to get clips that are nearing their expiration date.'),
8
+ deviceUuidFilters: z
9
+ .array(createUuidSchema())
10
+ .nullable()
11
+ .describe("A list of UUIDs representing specific devices to filter clips by. Only clips emitted by these devices will be returned. Please truncate any facets, such as .v0. It is always 22 characters long."),
12
+ locationUuidFilters: z
13
+ .array(createUuidSchema())
14
+ .nullable()
15
+ .describe("A list of UUIDs representing specific locations to filter clips by. Only clips associated with these locations will be returned. Please truncate any facets, such as .v0. It is always 22 characters long."),
16
+ searchFilter: z
17
+ .string()
18
+ .nullable()
19
+ .describe("A simple string to search for within the names of the clips."),
20
+ timestampISOAfter: z
21
+ .string()
22
+ .datetime({ message: "Invalid datetime string. Expected ISO 8601 format.", offset: true })
23
+ .describe("The start of the time range for which to retrieve clips. Only clips that occurred AFTER this timestamp will be returned."
24
+ + ISOTimestampFormatDescription),
25
+ timestampISOBefore: z
26
+ .string()
27
+ .datetime({ message: "Invalid datetime string. Expected ISO 8601 format.", offset: true })
28
+ .describe("The end of the time range for which to retrieve clips. Only clips that occurred BEFORE this timestamp will be returned."
29
+ + ISOTimestampFormatDescription),
30
+ };
31
+ const TOOL_ARGS_SCHEMA = z.object(TOOL_ARGS);
32
+ export const ApiPayloadSchema = TOOL_ARGS_SCHEMA.transform((args) => {
33
+ const { queryType, timestampISOAfter, timestampISOBefore, ...rest } = args;
34
+ const timestampMsAfter = createEpochSchema().parse(timestampISOAfter);
35
+ const timestampMsBefore = createEpochSchema().parse(timestampISOBefore);
36
+ return {
37
+ ...rest,
38
+ timestampMsAfter,
39
+ timestampMsBefore,
40
+ };
41
+ });
@@ -0,0 +1,44 @@
1
+ import { z } from "zod";
2
+ import { schemas } from "./zod-schemas.js";
3
+ // Base schema for the elicitation request
4
+ export const PolicyInfoSchema = z
5
+ .object({
6
+ name: z.string().min(1, "Policy name is required"),
7
+ description: z.string().optional(),
8
+ orgUuid: z.string().optional(),
9
+ })
10
+ .describe("Camera policy information");
11
+ export const TOOL_ARGS = {};
12
+ const TOOL_ARGS_SCHEMA = z.object(TOOL_ARGS);
13
+ export const ApiPayloadSchema = schemas.Policy_CreateCameraPolicyWSRequest;
14
+ export const OUTPUT_SCHEMA = z.object({
15
+ needUserInput: z.boolean().optional(),
16
+ message: z
17
+ .string()
18
+ .optional()
19
+ .describe("The message for this stage in the policy creation process. This message will be displayed to the user."),
20
+ requestType: z
21
+ .enum(["policy-creation-form", "schedule-trigger-configuration", "camera-assignment"])
22
+ .optional()
23
+ .describe("The type of form to display to the user as the next step in the policy creation process"),
24
+ submitAction: z
25
+ .string()
26
+ .optional()
27
+ .describe("The action to take when the user completes the form, corresponds to the tool name to interact with next"),
28
+ policyUuid: z
29
+ .string()
30
+ .optional()
31
+ .describe("The UUID of the policy that was created during this workflow"),
32
+ policyName: z
33
+ .string()
34
+ .optional()
35
+ .describe("The name of the policy that was created during this workflow"),
36
+ scheduleData: z
37
+ .array(z.object({
38
+ uuid: z.string(),
39
+ name: z.string(),
40
+ }))
41
+ .nullable()
42
+ .optional()
43
+ .describe("The schedules that were created during this workflow, can be null or undefined if user has not created them yet with schedule-trigger-configuration"),
44
+ });
@@ -0,0 +1,8 @@
1
+ import { z } from "zod";
2
+ import { CreateVideoWallOptions } from "../types.js";
3
+ import { addConfirmationParams } from "../utils/confirmation.js";
4
+ export const TOOL_ARGS = addConfirmationParams({
5
+ entityType: z.enum(["video-wall"]).describe("The entity type to create. Example: video wall."),
6
+ videoWallCreateOptions: CreateVideoWallOptions,
7
+ });
8
+ const TOOL_ARGS_SCHEMA = z.object(TOOL_ARGS);
@@ -10,5 +10,6 @@ var DeviceType;
10
10
  DeviceType["MOTION_SENSOR"] = "motion-sensor";
11
11
  DeviceType["BUTTON"] = "button";
12
12
  DeviceType["KEYPAD"] = "keypad";
13
+ DeviceType["ENVIRONMENTAL_GATEWAY"] = "environmental-gateway";
13
14
  })(DeviceType || (DeviceType = {}));
14
15
  export default DeviceType;
@@ -0,0 +1,7 @@
1
+ import { z } from "zod";
2
+ export const TOOL_ARGS = {
3
+ endpoint: z
4
+ .string()
5
+ .describe("The API endpoint to get output keys for (e.g., 'POST /api/camera/getMinimalCameraStateList')"),
6
+ };
7
+ const TOOL_ARGS_SCHEMA = z.object(TOOL_ARGS);