rhombus-node-mcp 0.1.11 → 0.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -1
- package/dist/constants.js +2 -0
- package/dist/index.js +23 -534
- package/dist/logger.js +27 -0
- package/dist/network.js +81 -0
- package/dist/resources/getResources.js +19 -0
- package/dist/resources/llms.pdf.js +19 -0
- package/dist/resources/routes.json.js +31 -0
- package/dist/tools/clips-tool.js +62 -0
- package/dist/tools/create-tool.js +80 -0
- package/dist/tools/devices/camera-tool/camera-tool.js +218 -0
- package/dist/tools/devices/camera-tool/types.js +172 -0
- package/dist/tools/devices/get-entity-tool.js +117 -0
- package/dist/tools/events-tool.js +101 -0
- package/dist/tools/faces-tool.js +140 -0
- package/dist/tools/get-org-information.js +18 -0
- package/dist/tools/getTools.js +22 -0
- package/dist/tools/location-tool.js +34 -0
- package/dist/tools/policy-alerts-tool.js +57 -0
- package/dist/tools/reboot-cameras.js +63 -0
- package/dist/tools/time-tool.js +57 -0
- package/dist/types/deviceType.js +14 -0
- package/dist/types.js +16 -13
- package/dist/util.js +111 -0
- package/dist/utils/confirmation.js +45 -0
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# 🚀 Rhombus MCP Server - Supercharge Your AI Tools!
|
|
2
|
+
[](https://smithery.ai/server/@RhombusSystems/rhombus-node-mcp1)
|
|
2
3
|
|
|
3
4
|
Unleash the power of the Rhombus API with this cutting-edge MCP server implementation! Transform your chatbot experience with advanced security and surveillance capabilities at your fingertips.
|
|
4
5
|
|
|
@@ -48,7 +49,7 @@ Your insights will directly influence our development roadmap and help us create
|
|
|
48
49
|
"--rm",
|
|
49
50
|
"-e",
|
|
50
51
|
"RHOMBUS_API_KEY=YOUR_API_KEY_HERE",
|
|
51
|
-
"mcp-server-rhombus"
|
|
52
|
+
"johnrhombusdocker/mcp-server-rhombus"
|
|
52
53
|
],
|
|
53
54
|
"env": {
|
|
54
55
|
"RHOMBUS_API_KEY": "YOUR_API_KEY_HERE"
|
|
@@ -90,6 +91,44 @@ Time to see the magic happen! Let's verify that Claude for Desktop is detecting
|
|
|
90
91
|
|
|
91
92
|
If they're visible, YOU'VE DONE IT! 🎉 Your integration is live and ready to rock! Claude can now communicate directly with Rhombus systems, giving you the same incredible capabilities as the Rhombus web app, but with the added power of Claude's intelligence!
|
|
92
93
|
|
|
94
|
+
## 🚀 Running Locally: Unleash Your Inner Developer!
|
|
95
|
+
|
|
96
|
+
Want to get hands-on and test out the Rhombus MCP server right on your machine? Follow these steps to build and run a local Docker image, then connect it to your MCP client of choice!
|
|
97
|
+
|
|
98
|
+
### 1. Build Your Local Docker Image 🛠️
|
|
99
|
+
First, let's get that Docker image built. This will allow you to run the MCP server in a local environment.
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
npm install
|
|
103
|
+
docker build -t mcp-server-rhombus .
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### 2. Update Your Claude Config for Local Use ⚡
|
|
107
|
+
Now, you'll need to adjust your `claude_desktop_config.json` to point to your newly built local Docker image.
|
|
108
|
+
|
|
109
|
+
> ***Note:*** When running locally, the Docker image name changes to `mcp-server-rhombus` from `johnrhombusdocker/mcp-server-rhombus`. Make sure to update this in your configuration!
|
|
110
|
+
|
|
111
|
+
```json
|
|
112
|
+
{
|
|
113
|
+
"mcpServers": {
|
|
114
|
+
"rhombus": {
|
|
115
|
+
"command": "docker",
|
|
116
|
+
"args": [
|
|
117
|
+
"run",
|
|
118
|
+
"-i",
|
|
119
|
+
"--rm",
|
|
120
|
+
"-e",
|
|
121
|
+
"RHOMBUS_API_KEY=YOUR_API_KEY_HERE",
|
|
122
|
+
"mcp-server-rhombus"
|
|
123
|
+
],
|
|
124
|
+
"env": {
|
|
125
|
+
"RHOMBUS_API_KEY": "YOUR_API_KEY_HERE"
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
93
132
|
### Hitting a Snag? We've Got You! 🛟
|
|
94
133
|
|
|
95
134
|
Check out Claude's fantastic [troubleshooting guide](https://modelcontextprotocol.io/docs/tools/debugging) for quick fixes! Still stuck? Our team of experts is ready to help!
|
package/dist/index.js
CHANGED
|
@@ -1,30 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
import
|
|
8
|
-
const THREE_HOURS_MS = 3 * 60 * 60 * 1000;
|
|
9
|
-
const FIVE_SECONDS_MS = 5 * 1000;
|
|
4
|
+
import "dotenv/config";
|
|
5
|
+
import getTools from "./tools/getTools.js";
|
|
6
|
+
import { logger } from "./logger.js";
|
|
7
|
+
import getResources from "./resources/getResources.js";
|
|
10
8
|
const RHOMBUS_API_KEY = process.env.RHOMBUS_API_KEY;
|
|
11
9
|
if (!RHOMBUS_API_KEY) {
|
|
12
|
-
|
|
10
|
+
logger.info("Missing RHOMBUS_API_KEY");
|
|
13
11
|
}
|
|
14
|
-
const enableLogs = process.env.ENABLE_LOGS;
|
|
15
12
|
const serverUrl = process.env.RHOMBUS_API_SERVER || "api2.rhombussystems.com";
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
"x-rhombus-agent": "chatbot",
|
|
21
|
-
accept: "application/json",
|
|
22
|
-
};
|
|
23
|
-
const AUTH_HEADERS = {
|
|
24
|
-
"x-auth-apikey": RHOMBUS_API_KEY,
|
|
25
|
-
"x-auth-scheme": "api-token",
|
|
26
|
-
};
|
|
27
|
-
const server = new McpServer({
|
|
13
|
+
logger.info(`Using API_KEY: ${RHOMBUS_API_KEY}`);
|
|
14
|
+
logger.info(`To hit API server: ${serverUrl}`);
|
|
15
|
+
logger.info("🌐 Using server url", serverUrl);
|
|
16
|
+
export const server = new McpServer({
|
|
28
17
|
name: "rhombus",
|
|
29
18
|
version: "1.0.0",
|
|
30
19
|
capabilities: {
|
|
@@ -32,523 +21,23 @@ const server = new McpServer({
|
|
|
32
21
|
tools: {},
|
|
33
22
|
},
|
|
34
23
|
});
|
|
35
|
-
const STATIC_ARGS = {
|
|
36
|
-
requestModifiers: z
|
|
37
|
-
.optional(z.object({
|
|
38
|
-
headers: z.optional(z.any()),
|
|
39
|
-
query: z.optional(z.any()),
|
|
40
|
-
}))
|
|
41
|
-
.describe("Optional headers accepted by tools. LLM should never ever use this. 😅"),
|
|
42
|
-
};
|
|
43
|
-
const log = (msg) => {
|
|
44
|
-
if (!enableLogs)
|
|
45
|
-
return;
|
|
46
|
-
console.error(msg);
|
|
47
|
-
};
|
|
48
|
-
const appendQueryParams = (url, params) => {
|
|
49
|
-
if (!params || typeof params !== "object")
|
|
50
|
-
return url;
|
|
51
|
-
const urlObj = new URL(url);
|
|
52
|
-
const existingSearchParams = new URLSearchParams(urlObj.search);
|
|
53
|
-
for (const [key, value] of Object.entries(params)) {
|
|
54
|
-
if (value !== undefined && value !== null) {
|
|
55
|
-
existingSearchParams.append(key, String(value));
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
const baseUrl = url.split("?")[0];
|
|
59
|
-
const queryString = existingSearchParams.toString();
|
|
60
|
-
return queryString ? `${baseUrl}?${queryString}` : baseUrl;
|
|
61
|
-
};
|
|
62
|
-
async function postApi(url, body, modifiers) {
|
|
63
|
-
let requestHeaders = {
|
|
64
|
-
...(modifiers?.headers || AUTH_HEADERS),
|
|
65
|
-
...STATIC_HEADERS,
|
|
66
|
-
};
|
|
67
|
-
if (modifiers?.query) {
|
|
68
|
-
url = appendQueryParams(url, modifiers.query);
|
|
69
|
-
}
|
|
70
|
-
try {
|
|
71
|
-
log(`[POSTAPI] REQUEST - ${url} - ${body} - ${JSON.stringify(requestHeaders)}`);
|
|
72
|
-
const response = await fetch(url, { method: "POST", headers: requestHeaders, body });
|
|
73
|
-
log(`[POSTAPI] RESPONSE - ${JSON.stringify(response)}`);
|
|
74
|
-
if (!response.ok) {
|
|
75
|
-
log(`❌ RESPONSE - ${response.ok} - ${response.status}`);
|
|
76
|
-
if (response.status === 401 || response.status === 403) {
|
|
77
|
-
return {
|
|
78
|
-
error: true,
|
|
79
|
-
status: "Sorry, I don't have permission to help with this request. Consider upgrading my permissions by changing the role of the API Key I am using.",
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
throw new Error(`HTTP error! status: ${response.status}`);
|
|
83
|
-
}
|
|
84
|
-
const ret = await response.json();
|
|
85
|
-
log(`❌ RESPONSE - ${response.ok} - ${JSON.stringify(ret)}`);
|
|
86
|
-
return ret;
|
|
87
|
-
}
|
|
88
|
-
catch (error) {
|
|
89
|
-
log(`[POSTAPI] ERROR - ${JSON.stringify(error || {})}`);
|
|
90
|
-
return {
|
|
91
|
-
error: true,
|
|
92
|
-
status: `Request Error: ${error}`,
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
async function getOrg(requestModifiers) {
|
|
97
|
-
const url = BASE_URL + "/org/getOrgV2";
|
|
98
|
-
return await postApi(url, "{}", requestModifiers);
|
|
99
|
-
}
|
|
100
|
-
async function getLocations(requestModifiers) {
|
|
101
|
-
const url = BASE_URL + "/location/getLocationsV2";
|
|
102
|
-
return await postApi(url, "{}", requestModifiers);
|
|
103
|
-
}
|
|
104
|
-
async function getCameraList(requestModifiers) {
|
|
105
|
-
const url = BASE_URL + "/camera/getMinimalCameraStateList";
|
|
106
|
-
return await postApi(url, "{}", requestModifiers).then(response => {
|
|
107
|
-
return {
|
|
108
|
-
cameraStates: response.cameraStates.filter((camera) => !!camera.locationUuid),
|
|
109
|
-
};
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
async function getAccessControlledDoors(requestModifiers) {
|
|
113
|
-
const url = BASE_URL + "/component/findAccessControlledDoors";
|
|
114
|
-
return await postApi(url, "{}", requestModifiers);
|
|
115
|
-
}
|
|
116
|
-
async function getFaceEvents(_locationUuid, requestModifiers) {
|
|
117
|
-
const nowMs = Date.now();
|
|
118
|
-
const rangeStartMs = nowMs - THREE_HOURS_MS;
|
|
119
|
-
const rangeEndMs = nowMs - FIVE_SECONDS_MS;
|
|
120
|
-
const body = JSON.stringify({
|
|
121
|
-
pageRequest: {
|
|
122
|
-
lastEvaluatedKey: undefined,
|
|
123
|
-
maxPageSize: 75,
|
|
124
|
-
},
|
|
125
|
-
searchFilter: {
|
|
126
|
-
deviceUuids: [],
|
|
127
|
-
faceNames: [],
|
|
128
|
-
labels: [],
|
|
129
|
-
locationUuids: [],
|
|
130
|
-
personUuids: [],
|
|
131
|
-
timestampFilter: {
|
|
132
|
-
rangeStart: rangeStartMs,
|
|
133
|
-
rangeEnd: rangeEndMs,
|
|
134
|
-
},
|
|
135
|
-
},
|
|
136
|
-
});
|
|
137
|
-
const response = await postApi(BASE_URL + "/faceRecognition/faceEvent/findFaceEventsByOrg", body, requestModifiers).then(response => {
|
|
138
|
-
return {
|
|
139
|
-
faceEvents: (response.faceEvents || []).map((event) => ({
|
|
140
|
-
...event,
|
|
141
|
-
eventTimestamp: new Date(event.eventTimestamp).toString(),
|
|
142
|
-
})),
|
|
143
|
-
};
|
|
144
|
-
});
|
|
145
|
-
return response;
|
|
146
|
-
}
|
|
147
|
-
async function getAccessControlEvents(doorUuid, requestModifiers) {
|
|
148
|
-
const url = BASE_URL + "/component/findComponentEventsByAccessControlledDoor";
|
|
149
|
-
const body = JSON.stringify({
|
|
150
|
-
limit: 50,
|
|
151
|
-
accessControlledDoorUuid: doorUuid,
|
|
152
|
-
});
|
|
153
|
-
const response = await postApi(url, body, requestModifiers).then(response => ({
|
|
154
|
-
componentEvents: (response.componentEvents || []).map((event) => ({
|
|
155
|
-
...event,
|
|
156
|
-
timestamp: new Date(event.timestampMs).toString(),
|
|
157
|
-
})),
|
|
158
|
-
}));
|
|
159
|
-
return response;
|
|
160
|
-
}
|
|
161
|
-
async function rebootCameras(cameraUuids, requestModifiers) {
|
|
162
|
-
const url = BASE_URL + "/camera/reboot";
|
|
163
|
-
let successCount = 0;
|
|
164
|
-
let errorCount = 0;
|
|
165
|
-
for (const cameraUuid in cameraUuids) {
|
|
166
|
-
try {
|
|
167
|
-
const body = JSON.stringify({ cameraUuid: cameraUuid });
|
|
168
|
-
const response = await postApi(url, body, requestModifiers);
|
|
169
|
-
if (response.error) {
|
|
170
|
-
errorCount++;
|
|
171
|
-
}
|
|
172
|
-
else {
|
|
173
|
-
successCount++;
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
catch (error) {
|
|
177
|
-
const ret = `Error rebooting cameras: ${error}`;
|
|
178
|
-
return { error: true, status: ret };
|
|
179
|
-
}
|
|
180
|
-
let status;
|
|
181
|
-
if (successCount === cameraUuids.length)
|
|
182
|
-
status = "SUCCESS";
|
|
183
|
-
else if (successCount > 0 && successCount < cameraUuids.length)
|
|
184
|
-
status = "PARTIAL_SUCCESS";
|
|
185
|
-
else
|
|
186
|
-
status = "ERROR";
|
|
187
|
-
return { status, successCount, errorCount };
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
async function createVideoWall(options, headers) {
|
|
191
|
-
const url = BASE_URL + "/camera/createVideoWall";
|
|
192
|
-
const body = JSON.stringify({
|
|
193
|
-
videoWall: {
|
|
194
|
-
displayName: options?.displayName,
|
|
195
|
-
deviceList: options?.deviceList,
|
|
196
|
-
othersCanEdit: true,
|
|
197
|
-
orgUuid: options?.orgUuid,
|
|
198
|
-
shared: true,
|
|
199
|
-
settings: {
|
|
200
|
-
gridSize: { width: options?.settings.columnCount, height: options?.settings.columnCount },
|
|
201
|
-
gridLayout: "1 2\n3 4",
|
|
202
|
-
intervalSeconds: options?.settings.intervalSeconds || 5,
|
|
203
|
-
},
|
|
204
|
-
},
|
|
205
|
-
});
|
|
206
|
-
const response = await postApi(url, body, headers);
|
|
207
|
-
return response;
|
|
208
|
-
}
|
|
209
|
-
async function getImageForCameraAtTime(cameraUuid, timestampMs, requestModifiers) {
|
|
210
|
-
const url = BASE_URL + "/video/getExactFrameUri";
|
|
211
|
-
const body = JSON.stringify({
|
|
212
|
-
cameraUuid: cameraUuid,
|
|
213
|
-
downscaleFactor: 10,
|
|
214
|
-
jpgQuality: 70,
|
|
215
|
-
permyriadCropHeight: 10000,
|
|
216
|
-
permyriadCropWidth: 5625,
|
|
217
|
-
permyriadCropX: 2188,
|
|
218
|
-
permyriadCropY: 0,
|
|
219
|
-
timestampMs: timestampMs,
|
|
220
|
-
});
|
|
221
|
-
const base64Image = await postApi(url, body, requestModifiers).then(async (res) => {
|
|
222
|
-
let requestHeaders = {
|
|
223
|
-
...(requestModifiers?.headers || AUTH_HEADERS),
|
|
224
|
-
...STATIC_HEADERS,
|
|
225
|
-
};
|
|
226
|
-
return await fetch(res.frameUri, { method: "GET", headers: requestHeaders }).then(async (res) => {
|
|
227
|
-
if (!res.ok)
|
|
228
|
-
return null;
|
|
229
|
-
const arrayBuffer = await res.arrayBuffer();
|
|
230
|
-
const buffer = Buffer.from(arrayBuffer);
|
|
231
|
-
const base64 = buffer.toString("base64");
|
|
232
|
-
return base64;
|
|
233
|
-
});
|
|
234
|
-
});
|
|
235
|
-
if (!base64Image) {
|
|
236
|
-
return {
|
|
237
|
-
success: false,
|
|
238
|
-
status: "failed to fetch image",
|
|
239
|
-
};
|
|
240
|
-
}
|
|
241
|
-
return {
|
|
242
|
-
success: true,
|
|
243
|
-
status: "successfully fetched image",
|
|
244
|
-
imageType: "base64",
|
|
245
|
-
imageData: base64Image,
|
|
246
|
-
};
|
|
247
|
-
}
|
|
248
|
-
server.tool("get-org-information", "Get general information about the organization including org name, camera configuration defaults, contact information, and org settings.", { ...STATIC_ARGS }, async ({ requestModifiers }) => {
|
|
249
|
-
const org = await getOrg(requestModifiers);
|
|
250
|
-
return {
|
|
251
|
-
content: [
|
|
252
|
-
{
|
|
253
|
-
type: "text",
|
|
254
|
-
text: JSON.stringify(org),
|
|
255
|
-
},
|
|
256
|
-
],
|
|
257
|
-
};
|
|
258
|
-
});
|
|
259
|
-
async function handleCreateVideoWallRequest(videoWallCreateOptions, headers) {
|
|
260
|
-
let text = "Unable to create video wall!";
|
|
261
|
-
// console.error("🔨 Creating video wall");
|
|
262
|
-
if (!videoWallCreateOptions?.displayName) {
|
|
263
|
-
text = JSON.stringify({
|
|
264
|
-
needUserInput: true,
|
|
265
|
-
commandForUser: "What should the name of the video wall be?",
|
|
266
|
-
});
|
|
267
|
-
}
|
|
268
|
-
else if ((videoWallCreateOptions?.deviceList || []).length === 0) {
|
|
269
|
-
text = JSON.stringify({
|
|
270
|
-
needUserInput: true,
|
|
271
|
-
commandForUser: "Which cameras would you like on this video wall?",
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
|
-
else {
|
|
275
|
-
// console.error("Creating video wall with options: ", JSON.stringify(videoWallCreateOptions));
|
|
276
|
-
text = JSON.stringify(await createVideoWall(videoWallCreateOptions, headers));
|
|
277
|
-
}
|
|
278
|
-
return Promise.resolve({
|
|
279
|
-
content: [
|
|
280
|
-
{
|
|
281
|
-
type: "text",
|
|
282
|
-
text,
|
|
283
|
-
},
|
|
284
|
-
],
|
|
285
|
-
});
|
|
286
|
-
}
|
|
287
|
-
server.tool("create-tool", "Tool for creating many entity types such as video walls.", {
|
|
288
|
-
entityType: z.enum(["video-wall"]).describe("The entity type to create. Example: video wall."),
|
|
289
|
-
videoWallCreateOptions: CreateVideoWallOptions,
|
|
290
|
-
...STATIC_ARGS,
|
|
291
|
-
}, async ({ entityType, videoWallCreateOptions, requestModifiers }) => {
|
|
292
|
-
switch (entityType) {
|
|
293
|
-
case "video-wall":
|
|
294
|
-
return await handleCreateVideoWallRequest(videoWallCreateOptions, requestModifiers);
|
|
295
|
-
default:
|
|
296
|
-
}
|
|
297
|
-
return {
|
|
298
|
-
content: [
|
|
299
|
-
{
|
|
300
|
-
type: "text",
|
|
301
|
-
text: "",
|
|
302
|
-
},
|
|
303
|
-
],
|
|
304
|
-
};
|
|
305
|
-
});
|
|
306
|
-
function nullToUndefined(value) {
|
|
307
|
-
return value === null ? undefined : value;
|
|
308
|
-
}
|
|
309
|
-
server.tool("time-tool", "Tool for converting a natural language time description into a timestamp in milliseconds.", {
|
|
310
|
-
time_description: z
|
|
311
|
-
.string()
|
|
312
|
-
.describe("A natural language description of the time (e.g., '2pm today', 'tomorrow at noon')."),
|
|
313
|
-
timezone: z
|
|
314
|
-
.optional(z.string())
|
|
315
|
-
.describe("Optional IANA timezone string (e.g., 'America/Los_Angeles', 'UTC'). Defaults to system timezone."),
|
|
316
|
-
}, async ({ time_description, timezone }) => {
|
|
317
|
-
// console.error(`🕛 handling tool call for time ${time_description} using timezone ${timezone}`);
|
|
318
|
-
const now = DateTime.now()
|
|
319
|
-
.setZone(timezone || undefined)
|
|
320
|
-
.toJSDate();
|
|
321
|
-
const parsed = parse(time_description, now, { forwardDate: true });
|
|
322
|
-
if (!parsed || parsed.length === 0) {
|
|
323
|
-
throw new Error(`Could not parse time description: ${time_description}`);
|
|
324
|
-
}
|
|
325
|
-
const dateComponents = parsed[0].start;
|
|
326
|
-
if (!dateComponents) {
|
|
327
|
-
throw new Error("Parsed time has no start component");
|
|
328
|
-
}
|
|
329
|
-
const dt = DateTime.fromObject({
|
|
330
|
-
year: nullToUndefined(dateComponents.get("year")),
|
|
331
|
-
month: nullToUndefined(dateComponents.get("month")),
|
|
332
|
-
day: nullToUndefined(dateComponents.get("day")),
|
|
333
|
-
hour: nullToUndefined(dateComponents.get("hour")),
|
|
334
|
-
minute: nullToUndefined(dateComponents.get("minute")),
|
|
335
|
-
second: nullToUndefined(dateComponents.get("second")),
|
|
336
|
-
millisecond: 0,
|
|
337
|
-
}, {
|
|
338
|
-
zone: timezone || "local",
|
|
339
|
-
});
|
|
340
|
-
if (!dt.isValid) {
|
|
341
|
-
throw new Error(`Could not construct valid DateTime: ${dt.invalidReason}`);
|
|
342
|
-
}
|
|
343
|
-
const timestamp = dt.toMillis();
|
|
344
|
-
return {
|
|
345
|
-
content: [
|
|
346
|
-
{
|
|
347
|
-
type: "text",
|
|
348
|
-
text: JSON.stringify({
|
|
349
|
-
timestamp,
|
|
350
|
-
iso: dt.toISO(),
|
|
351
|
-
timezone: dt.zoneName,
|
|
352
|
-
}),
|
|
353
|
-
},
|
|
354
|
-
],
|
|
355
|
-
};
|
|
356
|
-
});
|
|
357
|
-
server.tool("get-entity-tool", "get a list of entities like cameras, access controlled doors, sensors, etc", {
|
|
358
|
-
entityType: z
|
|
359
|
-
.enum(["camera", "access-controlled-doors"])
|
|
360
|
-
.describe("The entity type to retreive. Example: cameras."),
|
|
361
|
-
...STATIC_ARGS,
|
|
362
|
-
}, async ({ entityType, requestModifiers }) => {
|
|
363
|
-
let ret;
|
|
364
|
-
switch (entityType) {
|
|
365
|
-
case "camera":
|
|
366
|
-
ret = await getCameraList(requestModifiers);
|
|
367
|
-
break;
|
|
368
|
-
case "access-controlled-doors":
|
|
369
|
-
ret = await getAccessControlledDoors(requestModifiers);
|
|
370
|
-
break;
|
|
371
|
-
default:
|
|
372
|
-
ret = {};
|
|
373
|
-
break;
|
|
374
|
-
}
|
|
375
|
-
return {
|
|
376
|
-
content: [
|
|
377
|
-
{
|
|
378
|
-
type: "text",
|
|
379
|
-
text: JSON.stringify(ret),
|
|
380
|
-
},
|
|
381
|
-
],
|
|
382
|
-
};
|
|
383
|
-
});
|
|
384
|
-
server.tool("camera-tool", 'This tool captures and returns a real-time snapshot from a designated security camera. The image reflects the current scene in the camera\'s field of view and serves as a contextual input source for downstream tasks such as object recognition, anomaly detection, incident investigation, or situational assessment. When invoked, the tool provides the following: \n • Visual Scene Capture: A high-resolution image of what the camera is actively observing, including people, vehicles, license plates, and any detectable objects. \n• Scene Context: Metadata such as camera ID, location name, timestamp, and motion detection status if available. \n• 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.', {
|
|
385
|
-
requestType: z.enum(["image"]),
|
|
386
|
-
timestampMs: z
|
|
387
|
-
.optional(z.number())
|
|
388
|
-
.describe("the timestamp in milliseconds which should always be obtained using time-tool"),
|
|
389
|
-
cameraUuid: z.optional(z.string()).describe("the camera uuid requested"),
|
|
390
|
-
...STATIC_ARGS,
|
|
391
|
-
}, async ({ cameraUuid, timestampMs, requestType, requestModifiers }) => {
|
|
392
|
-
if (!cameraUuid) {
|
|
393
|
-
return {
|
|
394
|
-
content: [
|
|
395
|
-
{
|
|
396
|
-
type: "text",
|
|
397
|
-
text: JSON.stringify({
|
|
398
|
-
needUserInput: true,
|
|
399
|
-
commandForUser: "Which camera are you talking about?",
|
|
400
|
-
}),
|
|
401
|
-
},
|
|
402
|
-
],
|
|
403
|
-
};
|
|
404
|
-
}
|
|
405
|
-
if (!timestampMs) {
|
|
406
|
-
return {
|
|
407
|
-
content: [
|
|
408
|
-
{
|
|
409
|
-
type: "text",
|
|
410
|
-
text: JSON.stringify({
|
|
411
|
-
needUserInput: true,
|
|
412
|
-
commandForUser: "At what time?",
|
|
413
|
-
}),
|
|
414
|
-
},
|
|
415
|
-
],
|
|
416
|
-
};
|
|
417
|
-
}
|
|
418
|
-
let response;
|
|
419
|
-
switch (requestType) {
|
|
420
|
-
case "image":
|
|
421
|
-
response = await getImageForCameraAtTime(cameraUuid, timestampMs, requestModifiers);
|
|
422
|
-
if (!response.success || !response.imageData) {
|
|
423
|
-
return {
|
|
424
|
-
content: [{ type: "text", text: JSON.stringify(response) }],
|
|
425
|
-
};
|
|
426
|
-
}
|
|
427
|
-
return {
|
|
428
|
-
content: [
|
|
429
|
-
{
|
|
430
|
-
type: "image",
|
|
431
|
-
data: response.imageData,
|
|
432
|
-
mimeType: "image/jpeg",
|
|
433
|
-
},
|
|
434
|
-
],
|
|
435
|
-
};
|
|
436
|
-
break;
|
|
437
|
-
default:
|
|
438
|
-
response = {
|
|
439
|
-
error: true,
|
|
440
|
-
status: "mising unknown type from tool call",
|
|
441
|
-
};
|
|
442
|
-
break;
|
|
443
|
-
}
|
|
444
|
-
return {
|
|
445
|
-
content: [
|
|
446
|
-
{
|
|
447
|
-
type: "text",
|
|
448
|
-
text: JSON.stringify({ response }),
|
|
449
|
-
},
|
|
450
|
-
],
|
|
451
|
-
};
|
|
452
|
-
});
|
|
453
|
-
server.tool("events-tool", "event data for certain types of information like faces and license plates", {
|
|
454
|
-
eventType: z.enum(["faces", "people", "access-control"]),
|
|
455
|
-
locationUuid: z.optional(z.string()),
|
|
456
|
-
accessControlledDoorUuid: z.optional(z.string()),
|
|
457
|
-
...STATIC_ARGS,
|
|
458
|
-
}, async ({ eventType, locationUuid, accessControlledDoorUuid, requestModifiers }) => {
|
|
459
|
-
if (eventType === "faces" || eventType === "people") {
|
|
460
|
-
const response = await getFaceEvents(locationUuid, requestModifiers);
|
|
461
|
-
return {
|
|
462
|
-
content: [
|
|
463
|
-
{
|
|
464
|
-
type: "text",
|
|
465
|
-
text: JSON.stringify(response),
|
|
466
|
-
},
|
|
467
|
-
],
|
|
468
|
-
};
|
|
469
|
-
}
|
|
470
|
-
if (eventType === "access-control") {
|
|
471
|
-
if (!accessControlledDoorUuid) {
|
|
472
|
-
return {
|
|
473
|
-
content: [
|
|
474
|
-
{
|
|
475
|
-
type: "text",
|
|
476
|
-
text: JSON.stringify({
|
|
477
|
-
needUserInput: true,
|
|
478
|
-
commandForUser: "Which door are you asking about?",
|
|
479
|
-
}),
|
|
480
|
-
},
|
|
481
|
-
],
|
|
482
|
-
};
|
|
483
|
-
}
|
|
484
|
-
else {
|
|
485
|
-
const events = await getAccessControlEvents(accessControlledDoorUuid, requestModifiers);
|
|
486
|
-
return {
|
|
487
|
-
content: [
|
|
488
|
-
{
|
|
489
|
-
type: "text",
|
|
490
|
-
text: JSON.stringify(events),
|
|
491
|
-
},
|
|
492
|
-
],
|
|
493
|
-
};
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
return {
|
|
497
|
-
content: [
|
|
498
|
-
{
|
|
499
|
-
type: "text",
|
|
500
|
-
text: JSON.stringify({}),
|
|
501
|
-
},
|
|
502
|
-
],
|
|
503
|
-
};
|
|
504
|
-
});
|
|
505
|
-
server.tool("location-tool", "contains basic operations for locations and response in JSON format.", {
|
|
506
|
-
action: z.enum(["get", "update"]),
|
|
507
|
-
locationUpdate: z.optional(z.object({ uuid: z.string(), name: z.optional(z.string()) })),
|
|
508
|
-
...STATIC_ARGS,
|
|
509
|
-
}, async ({ action, locationUpdate, requestModifiers }) => {
|
|
510
|
-
let ret;
|
|
511
|
-
switch (action) {
|
|
512
|
-
case "get":
|
|
513
|
-
ret = await getLocations(requestModifiers);
|
|
514
|
-
break;
|
|
515
|
-
default:
|
|
516
|
-
ret = { error: true, status: `unsupported location tool call: ${action}` };
|
|
517
|
-
break;
|
|
518
|
-
}
|
|
519
|
-
return {
|
|
520
|
-
content: [{ type: "text", text: JSON.stringify(ret) }],
|
|
521
|
-
};
|
|
522
|
-
});
|
|
523
|
-
server.tool("reboot-cameras", "this tool is for rebooting one or more cameras causing them to reconnect to the server, this is a helpful option when a camera is experiencing connectivity issues or is in need of troubleshooting", {
|
|
524
|
-
cameraUuids: z
|
|
525
|
-
.array(z.string())
|
|
526
|
-
.describe("An array of camera UUID strings which are unique identifiers for cameras"),
|
|
527
|
-
...STATIC_ARGS,
|
|
528
|
-
}, async ({ cameraUuids, requestModifiers }) => {
|
|
529
|
-
const cameraRebootData = await rebootCameras(cameraUuids, requestModifiers);
|
|
530
|
-
if (!cameraRebootData) {
|
|
531
|
-
return {
|
|
532
|
-
content: [
|
|
533
|
-
{
|
|
534
|
-
type: "text",
|
|
535
|
-
text: "Failed to reboot cameras",
|
|
536
|
-
},
|
|
537
|
-
],
|
|
538
|
-
};
|
|
539
|
-
}
|
|
540
|
-
return {
|
|
541
|
-
content: [
|
|
542
|
-
{
|
|
543
|
-
type: "text",
|
|
544
|
-
text: JSON.stringify(cameraRebootData),
|
|
545
|
-
},
|
|
546
|
-
],
|
|
547
|
-
};
|
|
548
|
-
});
|
|
549
24
|
async function main() {
|
|
25
|
+
const resources = await getResources();
|
|
26
|
+
logger.info(`🛠️ Registering ${resources.length} resources`);
|
|
27
|
+
for (const resource of resources) {
|
|
28
|
+
resource.create(server);
|
|
29
|
+
logger.debug(`🔧 Registered resource ${resource.name}`);
|
|
30
|
+
}
|
|
31
|
+
const tools = await getTools();
|
|
32
|
+
logger.info(`🛠️ Registering ${tools.length} tools`);
|
|
33
|
+
for (const tool of tools) {
|
|
34
|
+
tool.create(server);
|
|
35
|
+
logger.debug(`🔧 Registered tool ${tool.name}`);
|
|
36
|
+
}
|
|
550
37
|
const transport = new StdioServerTransport();
|
|
38
|
+
logger.info(`🚙 Starting stdio transport`);
|
|
551
39
|
await server.connect(transport);
|
|
40
|
+
logger.info(`🚙🌬️ Connected.`);
|
|
552
41
|
}
|
|
553
42
|
main().catch(error => {
|
|
554
43
|
console.error("Fatal error in main():", error);
|
package/dist/logger.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import log4js from "log4js";
|
|
2
|
+
import path from "path";
|
|
3
|
+
log4js.configure({
|
|
4
|
+
appenders: {
|
|
5
|
+
mcp: {
|
|
6
|
+
type: "file",
|
|
7
|
+
filename: path.resolve(process.env.LOG_FOLDER ?? process.cwd(), "rhombus-node-mcp.log"),
|
|
8
|
+
maxLogSize: "1K",
|
|
9
|
+
layout: {
|
|
10
|
+
type: "basic",
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
stderr: { type: "stderr" },
|
|
14
|
+
},
|
|
15
|
+
categories: {
|
|
16
|
+
default: {
|
|
17
|
+
appenders: ["mcp", "stderr"],
|
|
18
|
+
level: "trace",
|
|
19
|
+
},
|
|
20
|
+
mcp: {
|
|
21
|
+
appenders: ["mcp", "stderr"],
|
|
22
|
+
level: "trace",
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
export const getLogger = log4js.getLogger;
|
|
27
|
+
export const logger = log4js.getLogger("mcp");
|