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
|
@@ -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);
|
package/dist/types/deviceType.js
CHANGED
|
@@ -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;
|