rhombus-node-mcp 0.1.21 → 0.1.23
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 +8 -0
- package/dist/api/access-control-tool-api.js +256 -0
- package/dist/api/alarm-monitoring-tool-api.js +65 -0
- package/dist/api/camera-tool-api.js +35 -15
- package/dist/api/camera-uptime-tool-api.js +112 -0
- package/dist/api/clips-tool-api.js +110 -21
- package/dist/api/create-tool-api.js +143 -16
- package/dist/api/door-tool-api.js +66 -0
- package/dist/api/events-tool-api.js +151 -28
- package/dist/api/faces-tool-api.js +118 -91
- package/dist/api/get-entity-tool-api.js +2 -0
- package/dist/api/guest-management-tool-api.js +75 -0
- package/dist/api/location-tool-api.js +48 -1
- package/dist/api/lpr-tool-api.js +26 -0
- package/dist/api/policy-alerts-tool-api.js +54 -0
- package/dist/api/report-tool-api.js +345 -23
- package/dist/api/rules-tool-api.js +78 -0
- package/dist/api/search-tool-api.js +93 -0
- package/dist/api/time-tool-api.js +3 -1
- package/dist/api/update-tool-api.js +204 -0
- package/dist/api/user-access-trail-tool-api.js +45 -0
- package/dist/api/user-audit-tool-api.js +47 -0
- package/dist/api/user-tool-api.js +77 -0
- package/dist/createServer.js +3 -1
- package/dist/filtering-utils.js +298 -0
- package/dist/index.js +0 -9
- package/dist/logger.js +6 -5
- package/dist/network.js +18 -4
- package/dist/tools/access-control-tool.js +97 -0
- package/dist/tools/alarm-monitoring-tool.js +50 -0
- package/dist/tools/analytics-tool.js +383 -0
- package/dist/tools/camera-tool.js +27 -9
- package/dist/tools/camera-uptime-tool.js +50 -0
- package/dist/tools/clips-tool.js +46 -16
- package/dist/tools/door-tool.js +66 -0
- package/dist/tools/events-tool.js +103 -81
- package/dist/tools/faces-tool.js +175 -32
- package/dist/tools/get-entity-tool.js +6 -1
- package/dist/tools/guest-management-tool.js +50 -0
- package/dist/tools/location-tool.js +23 -4
- package/dist/tools/lpr-tool.js +26 -20
- package/dist/tools/policy-alerts-tool.js +34 -4
- package/dist/tools/report-tool.js +183 -46
- package/dist/tools/rules-tool.js +76 -0
- package/dist/tools/search-tool.js +69 -0
- package/dist/tools/update-tool.js +63 -2
- package/dist/tools/user-access-trail-tool.js +64 -0
- package/dist/tools/user-audit-tool.js +53 -0
- package/dist/tools/user-tool.js +55 -0
- package/dist/tools/video-walls-tool.js +41 -0
- package/dist/transports/streamable-http.js +25 -15
- package/dist/types/access-control-tool-types.js +120 -0
- package/dist/types/alarm-monitoring-tool-types.js +60 -0
- package/dist/types/analytics-tool-types.js +190 -0
- package/dist/types/camera-tool-types.js +3 -2
- package/dist/types/camera-uptime-tool-types.js +52 -0
- package/dist/types/clips-tool-types.js +60 -11
- package/dist/types/door-tool-types.js +64 -0
- package/dist/types/events-tools-types.js +71 -3
- package/dist/types/faces-tools-types.js +36 -0
- package/dist/types/guest-management-tool-types.js +57 -0
- package/dist/types/location-tool-types.js +10 -1
- package/dist/types/lpr-tool-types.js +14 -0
- package/dist/types/policy-alerts-tool-types.js +38 -3
- package/dist/types/report-tool-types.js +284 -42
- package/dist/types/rules-tool-types.js +58 -0
- package/dist/types/schema.js +3123 -1
- package/dist/types/search-tool-types.js +82 -0
- package/dist/types/update-tool-types.js +4 -1
- package/dist/types/user-access-trail-tool-types.js +54 -0
- package/dist/types/user-audit-tool-types.js +48 -0
- package/dist/types/user-tool-types.js +61 -0
- package/dist/types/video-walls-tool-types.js +59 -0
- package/dist/types/zod-schemas.js +3237 -986
- package/dist/types.js +0 -21
- package/dist/util.js +34 -57
- package/package.json +7 -5
- package/dist/disabled-tools/semantic-search-tool.js +0 -90
- package/dist/tools/create-tool.js +0 -30
- package/dist/types/create-tool-types.js +0 -8
- package/dist/types/schema-components.js +0 -7211
- package/dist/types/semantic-search-tool-types.js +0 -5
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
// ---------------------------------------------------------------------------
|
|
3
|
+
// Shared tool arg schemas
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
export const INCLUDE_FIELDS_ARG = z
|
|
6
|
+
.array(z.string())
|
|
7
|
+
.nullable()
|
|
8
|
+
.describe(`Dot-notation field paths to include in the response (e.g. "vehicleEvents.vehicleLicensePlate").
|
|
9
|
+
Pass null to return all fields. WARNING: some responses can exceed 400k characters — use includeFields
|
|
10
|
+
to request only the data you need. For high-volume tools this may be required to get a complete answer.`);
|
|
11
|
+
export const FILTER_BY_ARG = z
|
|
12
|
+
.array(z.object({
|
|
13
|
+
field: z
|
|
14
|
+
.string()
|
|
15
|
+
.describe("Dot-notation path within each array item to compare (e.g. 'vehicleLicensePlate', 'eventTimestamp')"),
|
|
16
|
+
op: z
|
|
17
|
+
.enum(["=", "!=", ">", ">=", "<", "<=", "contains"])
|
|
18
|
+
.describe("Comparison operator"),
|
|
19
|
+
value: z
|
|
20
|
+
.union([z.string(), z.number(), z.boolean()])
|
|
21
|
+
.describe("Value to compare against"),
|
|
22
|
+
}))
|
|
23
|
+
.nullable()
|
|
24
|
+
.describe(`Filter array items in the response by field values. All conditions are ANDed.
|
|
25
|
+
Example: [{field: "vehicleLicensePlate", op: "=", value: "ABC123"}, {field: "confidence", op: ">", value: 0.8}]
|
|
26
|
+
Use alongside includeFields to get only the specific records and fields you need.`);
|
|
27
|
+
/** These keys will always be included in processed output schemas */
|
|
28
|
+
const INCLUDE_WHITELIST = ["requestType"];
|
|
29
|
+
function buildTrie(paths) {
|
|
30
|
+
const trie = {};
|
|
31
|
+
for (const p of paths) {
|
|
32
|
+
const parts = p.split(".");
|
|
33
|
+
let node = trie;
|
|
34
|
+
for (const part of parts) {
|
|
35
|
+
if (!node[part])
|
|
36
|
+
node[part] = {};
|
|
37
|
+
node = node[part];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return trie;
|
|
41
|
+
}
|
|
42
|
+
function filterByTrie(obj, trie) {
|
|
43
|
+
// Empty trie at this level means "include everything"
|
|
44
|
+
if (Object.keys(trie).length === 0) {
|
|
45
|
+
return obj;
|
|
46
|
+
}
|
|
47
|
+
if (Array.isArray(obj)) {
|
|
48
|
+
const mapped = obj.map((item) => filterByTrie(item, trie)).filter((item) => item !== undefined);
|
|
49
|
+
return mapped.length > 0 ? mapped : undefined;
|
|
50
|
+
}
|
|
51
|
+
if (typeof obj === "object" && obj !== null) {
|
|
52
|
+
const result = {};
|
|
53
|
+
for (const key of Object.keys(trie)) {
|
|
54
|
+
if (key in obj) {
|
|
55
|
+
const child = filterByTrie(obj[key], trie[key]);
|
|
56
|
+
if (child !== undefined)
|
|
57
|
+
result[key] = child;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
61
|
+
}
|
|
62
|
+
// Primitive reached with a non-empty trie — return as-is (e.g. the trie
|
|
63
|
+
// targeted a leaf that is a scalar)
|
|
64
|
+
return obj;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Filters an object to only include the specified dot-notation field paths.
|
|
68
|
+
*
|
|
69
|
+
* Examples:
|
|
70
|
+
* filterIncludedFields(data, ["vehicleEvents.vehicleLicensePlate", "error"])
|
|
71
|
+
* → keeps vehicleLicensePlate within each vehicleEvents item, and the top-level error field
|
|
72
|
+
*/
|
|
73
|
+
export function filterIncludedFields(obj, fieldsToInclude) {
|
|
74
|
+
if (!fieldsToInclude || fieldsToInclude.length === 0) {
|
|
75
|
+
return obj;
|
|
76
|
+
}
|
|
77
|
+
const trie = buildTrie(fieldsToInclude);
|
|
78
|
+
return filterByTrie(obj, trie);
|
|
79
|
+
}
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// applyFilterBy — predicate filtering of array items
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
function getNestedValue(obj, fieldPath) {
|
|
84
|
+
if (!fieldPath)
|
|
85
|
+
return obj;
|
|
86
|
+
const parts = fieldPath.split(".");
|
|
87
|
+
let current = obj;
|
|
88
|
+
for (const part of parts) {
|
|
89
|
+
if (current === null || current === undefined)
|
|
90
|
+
return undefined;
|
|
91
|
+
current = current[part];
|
|
92
|
+
}
|
|
93
|
+
return current;
|
|
94
|
+
}
|
|
95
|
+
function matchesCondition(item, condition) {
|
|
96
|
+
const actual = getNestedValue(item, condition.field);
|
|
97
|
+
const { op, value } = condition;
|
|
98
|
+
if (actual === undefined || actual === null)
|
|
99
|
+
return false;
|
|
100
|
+
switch (op) {
|
|
101
|
+
case "=":
|
|
102
|
+
// biome-ignore lint/suspicious/noDoubleEquals: intentional loose equality for mixed string/number comparisons
|
|
103
|
+
return actual == value;
|
|
104
|
+
case "!=":
|
|
105
|
+
// biome-ignore lint/suspicious/noDoubleEquals: intentional loose equality for mixed string/number comparisons
|
|
106
|
+
return actual != value;
|
|
107
|
+
case ">":
|
|
108
|
+
return Number(actual) > Number(value);
|
|
109
|
+
case ">=":
|
|
110
|
+
return Number(actual) >= Number(value);
|
|
111
|
+
case "<":
|
|
112
|
+
return Number(actual) < Number(value);
|
|
113
|
+
case "<=":
|
|
114
|
+
return Number(actual) <= Number(value);
|
|
115
|
+
case "contains":
|
|
116
|
+
return String(actual).toLowerCase().includes(String(value).toLowerCase());
|
|
117
|
+
default:
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Filters array items in the output object based on field-level predicates.
|
|
123
|
+
* Conditions are grouped by their top-level key; arrays at that key are
|
|
124
|
+
* filtered so only items matching ALL conditions (AND) are kept.
|
|
125
|
+
*
|
|
126
|
+
* Example:
|
|
127
|
+
* applyFilterBy({ vehicleEvents: [...] }, [
|
|
128
|
+
* { field: "vehicleLicensePlate", op: "=", value: "ABC123" }
|
|
129
|
+
* ])
|
|
130
|
+
* → vehicleEvents array is filtered to items whose vehicleLicensePlate === "ABC123"
|
|
131
|
+
*
|
|
132
|
+
* Note: conditions whose `field` contains a dot are resolved by navigating the
|
|
133
|
+
* full path within each array item (e.g. "location.name" looks up item.location.name).
|
|
134
|
+
*/
|
|
135
|
+
export function applyFilterBy(obj, conditions) {
|
|
136
|
+
if (!conditions?.length || typeof obj !== "object" || obj === null) {
|
|
137
|
+
return obj;
|
|
138
|
+
}
|
|
139
|
+
if (Array.isArray(obj)) {
|
|
140
|
+
return obj.filter((item) => conditions.every((c) => matchesCondition(item, c)));
|
|
141
|
+
}
|
|
142
|
+
// For object payloads, group conditions by the top-level key so we know
|
|
143
|
+
// which array in the payload each condition targets.
|
|
144
|
+
const conditionsByTopKey = {};
|
|
145
|
+
for (const cond of conditions) {
|
|
146
|
+
const dotIdx = cond.field.indexOf(".");
|
|
147
|
+
// If the field has no dot, it's a direct property of the array item —
|
|
148
|
+
// use the condition as-is against all array values in the object.
|
|
149
|
+
const topKey = dotIdx === -1 ? "*" : cond.field.substring(0, dotIdx);
|
|
150
|
+
const subField = dotIdx === -1 ? cond.field : cond.field.substring(dotIdx + 1);
|
|
151
|
+
if (!conditionsByTopKey[topKey])
|
|
152
|
+
conditionsByTopKey[topKey] = [];
|
|
153
|
+
conditionsByTopKey[topKey].push({ ...cond, field: subField });
|
|
154
|
+
}
|
|
155
|
+
const result = { ...obj };
|
|
156
|
+
for (const [topKey, keyConds] of Object.entries(conditionsByTopKey)) {
|
|
157
|
+
if (topKey === "*") {
|
|
158
|
+
// Apply conditions to every top-level array in the object
|
|
159
|
+
for (const k of Object.keys(result)) {
|
|
160
|
+
if (Array.isArray(result[k])) {
|
|
161
|
+
result[k] = result[k].filter((item) => keyConds.every((c) => matchesCondition(item, c)));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
else if (topKey in result && Array.isArray(result[topKey])) {
|
|
166
|
+
result[topKey] = result[topKey].filter((item) => keyConds.every((c) => matchesCondition(item, c)));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return result;
|
|
170
|
+
}
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
// zodToDotNotationPaths — derive available field paths from an OUTPUT_SCHEMA
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
/**
|
|
175
|
+
* Recursively walks a Zod schema and returns all valid dot-notation field paths.
|
|
176
|
+
* Useful for auto-documenting available includeFields/filterBy paths in tool descriptions.
|
|
177
|
+
*
|
|
178
|
+
* Example: zodToDotNotationPaths(OUTPUT_SCHEMA)
|
|
179
|
+
* → ["vehicleEvents", "vehicleEvents.uuid", "vehicleEvents.vehicleLicensePlate", "error", ...]
|
|
180
|
+
*/
|
|
181
|
+
export function zodToDotNotationPaths(schema, prefix = "") {
|
|
182
|
+
// Unwrap optional / nullable / default
|
|
183
|
+
if (schema instanceof z.ZodOptional ||
|
|
184
|
+
schema instanceof z.ZodNullable) {
|
|
185
|
+
return zodToDotNotationPaths(schema.unwrap(), prefix);
|
|
186
|
+
}
|
|
187
|
+
if (schema instanceof z.ZodDefault) {
|
|
188
|
+
return zodToDotNotationPaths(schema._def.innerType, prefix);
|
|
189
|
+
}
|
|
190
|
+
// Recurse into ZodObject shape
|
|
191
|
+
if (schema instanceof z.ZodObject) {
|
|
192
|
+
const paths = [];
|
|
193
|
+
for (const [key, value] of Object.entries(schema.shape)) {
|
|
194
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
195
|
+
paths.push(fullKey);
|
|
196
|
+
paths.push(...zodToDotNotationPaths(value, fullKey));
|
|
197
|
+
}
|
|
198
|
+
return paths;
|
|
199
|
+
}
|
|
200
|
+
// Recurse into ZodArray element
|
|
201
|
+
if (schema instanceof z.ZodArray) {
|
|
202
|
+
return zodToDotNotationPaths(schema.element, prefix);
|
|
203
|
+
}
|
|
204
|
+
// ZodUnion — collect from all branches (deduplicated by caller if needed)
|
|
205
|
+
if (schema instanceof z.ZodUnion) {
|
|
206
|
+
const paths = [];
|
|
207
|
+
for (const option of schema._def.options) {
|
|
208
|
+
paths.push(...zodToDotNotationPaths(option, prefix));
|
|
209
|
+
}
|
|
210
|
+
return paths;
|
|
211
|
+
}
|
|
212
|
+
// Scalar — no further nesting
|
|
213
|
+
return [];
|
|
214
|
+
}
|
|
215
|
+
// ---------------------------------------------------------------------------
|
|
216
|
+
// createFilteringProxy — MCP server proxy that adds includeFields + filterBy
|
|
217
|
+
// ---------------------------------------------------------------------------
|
|
218
|
+
const FILTERING_DESCRIPTION_SUFFIX = `
|
|
219
|
+
|
|
220
|
+
**Output filtering (all tools):**
|
|
221
|
+
- \`includeFields\` (string[]): Dot-notation paths to keep in the response (e.g. \`"vehicleEvents.vehicleLicensePlate"\`). Omit to return all fields.
|
|
222
|
+
- \`filterBy\` (array): Predicates to filter array items. Each entry: \`{field, op, value}\` where op is one of \`= != > >= < <= contains\`. All conditions are ANDed. Example: \`[{field:"vehicleLicensePlate", op:"=", value:"ABC123"}]\`
|
|
223
|
+
WARNING: some tool responses exceed 400k characters — use these params to request only the data you need.`;
|
|
224
|
+
function applyFilteringToResult(result, includeFields, filterBy) {
|
|
225
|
+
if (!includeFields?.length && !filterBy?.length)
|
|
226
|
+
return result;
|
|
227
|
+
// Always include whitelisted fields
|
|
228
|
+
const effectiveIncludeFields = includeFields?.length
|
|
229
|
+
? [...INCLUDE_WHITELIST, ...includeFields]
|
|
230
|
+
: includeFields;
|
|
231
|
+
const filteredContent = result.content.map((item) => {
|
|
232
|
+
if (item.type !== "text")
|
|
233
|
+
return item;
|
|
234
|
+
try {
|
|
235
|
+
// biome-ignore lint/suspicious/noExplicitAny: runtime JSON manipulation
|
|
236
|
+
let parsed = JSON.parse(item.text);
|
|
237
|
+
if (filterBy?.length)
|
|
238
|
+
parsed = applyFilterBy(parsed, filterBy) ?? parsed;
|
|
239
|
+
if (effectiveIncludeFields?.length)
|
|
240
|
+
parsed = filterIncludedFields(parsed, effectiveIncludeFields) ?? parsed;
|
|
241
|
+
return { ...item, text: JSON.stringify(parsed) };
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
return item;
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
// biome-ignore lint/suspicious/noExplicitAny: runtime JSON manipulation
|
|
248
|
+
let filteredStructured = result.structuredContent;
|
|
249
|
+
if (filteredStructured) {
|
|
250
|
+
if (filterBy?.length)
|
|
251
|
+
filteredStructured = applyFilterBy(filteredStructured, filterBy) ?? filteredStructured;
|
|
252
|
+
if (effectiveIncludeFields?.length)
|
|
253
|
+
filteredStructured = filterIncludedFields(filteredStructured, effectiveIncludeFields) ?? filteredStructured;
|
|
254
|
+
}
|
|
255
|
+
return { ...result, content: filteredContent, structuredContent: filteredStructured };
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Returns a Proxy over an McpServer that intercepts every `registerTool` call to:
|
|
259
|
+
* 1. Inject `includeFields` and `filterBy` into the tool's inputSchema
|
|
260
|
+
* 2. Append a description suffix explaining the filtering params
|
|
261
|
+
* 3. Wrap the handler to apply filtering to the tool result
|
|
262
|
+
*
|
|
263
|
+
* Tools whose names appear in `blacklist` are registered without modification.
|
|
264
|
+
*
|
|
265
|
+
* This means no individual tool needs to know about filtering — it is handled
|
|
266
|
+
* transparently for all tools registered through this proxy.
|
|
267
|
+
*/
|
|
268
|
+
export function createFilteringProxy(server, blacklist = new Set()) {
|
|
269
|
+
return new Proxy(server, {
|
|
270
|
+
get(target, prop, receiver) {
|
|
271
|
+
if (prop !== "registerTool") {
|
|
272
|
+
return Reflect.get(target, prop, receiver);
|
|
273
|
+
}
|
|
274
|
+
// biome-ignore lint/suspicious/noExplicitAny: proxy intercept
|
|
275
|
+
return function (name, config, handler) {
|
|
276
|
+
if (blacklist.has(name)) {
|
|
277
|
+
return target.registerTool(name, config, handler);
|
|
278
|
+
}
|
|
279
|
+
const augmentedConfig = {
|
|
280
|
+
...config,
|
|
281
|
+
description: (config.description ?? "") + FILTERING_DESCRIPTION_SUFFIX,
|
|
282
|
+
inputSchema: {
|
|
283
|
+
...config.inputSchema,
|
|
284
|
+
includeFields: INCLUDE_FIELDS_ARG,
|
|
285
|
+
filterBy: FILTER_BY_ARG,
|
|
286
|
+
},
|
|
287
|
+
};
|
|
288
|
+
// biome-ignore lint/suspicious/noExplicitAny: proxy intercept
|
|
289
|
+
const wrappedHandler = async (args, extra) => {
|
|
290
|
+
const { includeFields, filterBy, ...restArgs } = args;
|
|
291
|
+
const result = await handler(restArgs, extra);
|
|
292
|
+
return applyFilteringToResult(result, includeFields, filterBy);
|
|
293
|
+
};
|
|
294
|
+
return target.registerTool(name, augmentedConfig, wrappedHandler);
|
|
295
|
+
};
|
|
296
|
+
},
|
|
297
|
+
});
|
|
298
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
2
|
import "dotenv/config";
|
|
4
3
|
import { serverInit } from "./createServer.js";
|
|
5
4
|
import { logger } from "./logger.js";
|
|
@@ -14,14 +13,6 @@ async function main() {
|
|
|
14
13
|
}
|
|
15
14
|
logger.info("🌐 Using server url", serverUrl);
|
|
16
15
|
await serverInit();
|
|
17
|
-
const server = new McpServer({
|
|
18
|
-
name: "rhombus",
|
|
19
|
-
version: "1.0.0",
|
|
20
|
-
capabilities: {
|
|
21
|
-
resources: {},
|
|
22
|
-
tools: {},
|
|
23
|
-
},
|
|
24
|
-
});
|
|
25
16
|
if (TRANSPORT_TYPE === "stdio") {
|
|
26
17
|
await stdioTransport();
|
|
27
18
|
}
|
package/dist/logger.js
CHANGED
|
@@ -18,10 +18,10 @@ const appenders = {
|
|
|
18
18
|
// stderr for terminal visibility (always available)
|
|
19
19
|
stderr: { type: "stderr" },
|
|
20
20
|
// only send warn+ to stderr (never stdout)
|
|
21
|
-
|
|
21
|
+
stderrInfo: {
|
|
22
22
|
type: "logLevelFilter",
|
|
23
23
|
appender: "stderr",
|
|
24
|
-
level: "
|
|
24
|
+
level: "info",
|
|
25
25
|
},
|
|
26
26
|
};
|
|
27
27
|
// Only add file appender if we successfully created the directory
|
|
@@ -36,14 +36,15 @@ if (fileLoggingEnabled) {
|
|
|
36
36
|
layout: { type: "basic" },
|
|
37
37
|
};
|
|
38
38
|
}
|
|
39
|
+
const stderrAppender = process.env.DEBUG === "true" ? "stderr" : "stderrInfo";
|
|
39
40
|
// Configure categories based on available appenders
|
|
40
41
|
const categories = {
|
|
41
42
|
default: {
|
|
42
|
-
appenders: fileLoggingEnabled ? ["mcpFile",
|
|
43
|
-
level: "
|
|
43
|
+
appenders: fileLoggingEnabled ? ["mcpFile", stderrAppender] : [stderrAppender],
|
|
44
|
+
level: "trace",
|
|
44
45
|
},
|
|
45
46
|
mcp: {
|
|
46
|
-
appenders: fileLoggingEnabled ? ["mcpFile",
|
|
47
|
+
appenders: fileLoggingEnabled ? ["mcpFile", stderrAppender] : [stderrAppender],
|
|
47
48
|
level: "trace",
|
|
48
49
|
},
|
|
49
50
|
};
|
package/dist/network.js
CHANGED
|
@@ -9,7 +9,7 @@ export const STATIC_HEADERS = {
|
|
|
9
9
|
accept: "application/json",
|
|
10
10
|
};
|
|
11
11
|
export const AUTH_HEADERS = {
|
|
12
|
-
"x-auth-apikey": RHOMBUS_API_KEY,
|
|
12
|
+
"x-auth-apikey": RHOMBUS_API_KEY ?? "",
|
|
13
13
|
"x-auth-scheme": "api-token",
|
|
14
14
|
};
|
|
15
15
|
export const appendQueryParams = (url, params) => {
|
|
@@ -26,8 +26,7 @@ export const appendQueryParams = (url, params) => {
|
|
|
26
26
|
const queryString = existingSearchParams.toString();
|
|
27
27
|
return queryString ? `${baseUrl}?${queryString}` : baseUrl;
|
|
28
28
|
};
|
|
29
|
-
export
|
|
30
|
-
let url = BASE_URL + route;
|
|
29
|
+
export function constructRequestHeaders(url, modifiers, sessionId) {
|
|
31
30
|
// construct auth headers
|
|
32
31
|
let authHeaders = {};
|
|
33
32
|
if (!sessionId) {
|
|
@@ -41,7 +40,13 @@ export async function postApi({ route, body, modifiers, sessionId, }) {
|
|
|
41
40
|
logger.error(`No auth found for sessionId: ${sessionId}`);
|
|
42
41
|
throw new Error(`No auth found for sessionId: ${sessionId}`);
|
|
43
42
|
}
|
|
44
|
-
if ("
|
|
43
|
+
if ("oauthToken" in auth) {
|
|
44
|
+
authHeaders = {
|
|
45
|
+
Authorization: `Bearer ${auth.oauthToken}`,
|
|
46
|
+
"x-auth-scheme": "api-oauth-token",
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
else if ("apiKey" in auth) {
|
|
45
50
|
authHeaders = {
|
|
46
51
|
"x-auth-apikey": auth.apiKey,
|
|
47
52
|
"x-auth-scheme": "api-token",
|
|
@@ -60,6 +65,9 @@ export async function postApi({ route, body, modifiers, sessionId, }) {
|
|
|
60
65
|
"x-auth-scheme": "web2",
|
|
61
66
|
cookie: auth.cookie,
|
|
62
67
|
};
|
|
68
|
+
if ("sessionAlias" in auth) {
|
|
69
|
+
url = appendQueryParams(url, { _rs: auth.sessionAlias });
|
|
70
|
+
}
|
|
63
71
|
}
|
|
64
72
|
}
|
|
65
73
|
// merge headers
|
|
@@ -71,6 +79,12 @@ export async function postApi({ route, body, modifiers, sessionId, }) {
|
|
|
71
79
|
if (modifiers?.query) {
|
|
72
80
|
url = appendQueryParams(url, modifiers.query);
|
|
73
81
|
}
|
|
82
|
+
return { url, requestHeaders };
|
|
83
|
+
}
|
|
84
|
+
export async function postApi({ route, body, modifiers, sessionId, }) {
|
|
85
|
+
let url = BASE_URL + route;
|
|
86
|
+
const { url: newUrl, requestHeaders } = constructRequestHeaders(BASE_URL + route, modifiers, sessionId);
|
|
87
|
+
url = newUrl;
|
|
74
88
|
// stringify body if it's not already a string
|
|
75
89
|
if (typeof body === "object") {
|
|
76
90
|
body = JSON.stringify(body);
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { unlockDoor, getAccessControlGroups, getCredentialsByUser, getLockdownPlans, activateLockdown, deactivateLockdown, getDoorScheduleExceptions, getAccessGrants, getRemoteUnlockUsers, } from "../api/access-control-tool-api.js";
|
|
2
|
+
import { AccessControlRequestType, OUTPUT_SCHEMA, TOOL_ARGS, } from "../types/access-control-tool-types.js";
|
|
3
|
+
import { createToolStructuredContent, createToolTextContent, extractFromToolExtra, } from "../util.js";
|
|
4
|
+
const TOOL_NAME = "access-control-tool";
|
|
5
|
+
const TOOL_DESCRIPTION = `
|
|
6
|
+
This tool manages Rhombus access control operations including door unlocking, access groups, credentials, lockdown plans, door schedules, and access grants.
|
|
7
|
+
|
|
8
|
+
It has the following modes of operation, determined by the "requestType" parameter:
|
|
9
|
+
- ${AccessControlRequestType.UNLOCK_DOOR}: Remotely unlock an access controlled door. Requires doorUuid.
|
|
10
|
+
- ${AccessControlRequestType.GET_GROUPS}: List all access control groups in the organization.
|
|
11
|
+
- ${AccessControlRequestType.GET_CREDENTIALS_BY_USER}: List all access control credentials for a specific user. Requires userUuid.
|
|
12
|
+
- ${AccessControlRequestType.GET_LOCKDOWN_PLANS}: List all lockdown plans in the organization.
|
|
13
|
+
- ${AccessControlRequestType.ACTIVATE_LOCKDOWN}: Activate a lockdown plan at a location. Requires locationUuid and lockdownPlanUuid.
|
|
14
|
+
- ${AccessControlRequestType.DEACTIVATE_LOCKDOWN}: Deactivate a lockdown plan at a location. Requires locationUuid and lockdownPlanUuid.
|
|
15
|
+
- ${AccessControlRequestType.GET_DOOR_SCHEDULES}: Get door schedule exceptions for a location. Requires locationUuid.
|
|
16
|
+
- ${AccessControlRequestType.GET_ACCESS_GRANTS}: List location access grants (physical badge/card access). Optionally accepts locationUuid to filter by location. Each grant includes userUuids (directly assigned users), groupUuids (assigned access control groups), and doorUuids (the doors this grant provides access to).
|
|
17
|
+
- ${AccessControlRequestType.GET_REMOTE_UNLOCK_USERS}: Get all users who have permission to remotely unlock doors at a location. Requires locationUuid. Returns a list of doors with remote unlock enabled and the users who can unlock each door, based on their permission group roles. This is the correct tool for questions about remote unlock permissions.
|
|
18
|
+
|
|
19
|
+
Use the get-entity-tool with entityType ACCESS_CONTROL_DOOR to get door UUIDs.
|
|
20
|
+
Use the user-tool to look up user UUIDs and resolve them to names/emails.
|
|
21
|
+
Use the location-tool to get location UUIDs.
|
|
22
|
+
`;
|
|
23
|
+
const TOOL_HANDLER = async (args, _extra) => {
|
|
24
|
+
const { requestModifiers, sessionId } = extractFromToolExtra(_extra);
|
|
25
|
+
try {
|
|
26
|
+
switch (args.requestType) {
|
|
27
|
+
case AccessControlRequestType.UNLOCK_DOOR: {
|
|
28
|
+
if (!args.doorUuid) {
|
|
29
|
+
return createToolTextContent(JSON.stringify({ error: "doorUuid is required for unlock-door." }));
|
|
30
|
+
}
|
|
31
|
+
const unlockResult = await unlockDoor(args.doorUuid, requestModifiers, sessionId);
|
|
32
|
+
return createToolStructuredContent({ unlockResult });
|
|
33
|
+
}
|
|
34
|
+
case AccessControlRequestType.GET_GROUPS: {
|
|
35
|
+
const accessControlGroups = await getAccessControlGroups(requestModifiers, sessionId);
|
|
36
|
+
return createToolStructuredContent({ accessControlGroups });
|
|
37
|
+
}
|
|
38
|
+
case AccessControlRequestType.GET_CREDENTIALS_BY_USER: {
|
|
39
|
+
if (!args.userUuid) {
|
|
40
|
+
return createToolTextContent(JSON.stringify({ error: "userUuid is required for get-credentials-by-user." }));
|
|
41
|
+
}
|
|
42
|
+
const credentials = await getCredentialsByUser(args.userUuid, requestModifiers, sessionId);
|
|
43
|
+
return createToolStructuredContent({ credentials });
|
|
44
|
+
}
|
|
45
|
+
case AccessControlRequestType.GET_LOCKDOWN_PLANS: {
|
|
46
|
+
const lockdownPlans = await getLockdownPlans(requestModifiers, sessionId);
|
|
47
|
+
return createToolStructuredContent({ lockdownPlans });
|
|
48
|
+
}
|
|
49
|
+
case AccessControlRequestType.ACTIVATE_LOCKDOWN: {
|
|
50
|
+
if (!args.locationUuid || !args.lockdownPlanUuid) {
|
|
51
|
+
return createToolTextContent(JSON.stringify({ error: "locationUuid and lockdownPlanUuid are required for activate-lockdown." }));
|
|
52
|
+
}
|
|
53
|
+
const lockdownResult = await activateLockdown(args.locationUuid, args.lockdownPlanUuid, requestModifiers, sessionId);
|
|
54
|
+
return createToolStructuredContent({ lockdownResult });
|
|
55
|
+
}
|
|
56
|
+
case AccessControlRequestType.DEACTIVATE_LOCKDOWN: {
|
|
57
|
+
if (!args.locationUuid || !args.lockdownPlanUuid) {
|
|
58
|
+
return createToolTextContent(JSON.stringify({ error: "locationUuid and lockdownPlanUuid are required for deactivate-lockdown." }));
|
|
59
|
+
}
|
|
60
|
+
const lockdownResult = await deactivateLockdown(args.locationUuid, args.lockdownPlanUuid, requestModifiers, sessionId);
|
|
61
|
+
return createToolStructuredContent({ lockdownResult });
|
|
62
|
+
}
|
|
63
|
+
case AccessControlRequestType.GET_DOOR_SCHEDULES: {
|
|
64
|
+
if (!args.locationUuid) {
|
|
65
|
+
return createToolTextContent(JSON.stringify({ error: "locationUuid is required for get-door-schedules." }));
|
|
66
|
+
}
|
|
67
|
+
const doorScheduleExceptions = await getDoorScheduleExceptions(args.locationUuid, requestModifiers, sessionId);
|
|
68
|
+
return createToolStructuredContent({ doorScheduleExceptions });
|
|
69
|
+
}
|
|
70
|
+
case AccessControlRequestType.GET_ACCESS_GRANTS: {
|
|
71
|
+
const accessGrants = await getAccessGrants(args.locationUuid, requestModifiers, sessionId);
|
|
72
|
+
return createToolStructuredContent({ accessGrants });
|
|
73
|
+
}
|
|
74
|
+
case AccessControlRequestType.GET_REMOTE_UNLOCK_USERS: {
|
|
75
|
+
if (!args.locationUuid) {
|
|
76
|
+
return createToolTextContent(JSON.stringify({ error: "locationUuid is required for get-remote-unlock-users." }));
|
|
77
|
+
}
|
|
78
|
+
const remoteUnlockUsers = await getRemoteUnlockUsers(args.locationUuid, requestModifiers, sessionId);
|
|
79
|
+
return createToolStructuredContent({ remoteUnlockUsers });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
if (error instanceof Error) {
|
|
85
|
+
return createToolStructuredContent({ error: error.message });
|
|
86
|
+
}
|
|
87
|
+
return createToolStructuredContent({ error: "Unknown error" });
|
|
88
|
+
}
|
|
89
|
+
return createToolStructuredContent({ error: "Invalid request type" });
|
|
90
|
+
};
|
|
91
|
+
export function createTool(server) {
|
|
92
|
+
server.registerTool(TOOL_NAME, {
|
|
93
|
+
description: TOOL_DESCRIPTION,
|
|
94
|
+
inputSchema: TOOL_ARGS,
|
|
95
|
+
outputSchema: OUTPUT_SCHEMA.shape,
|
|
96
|
+
}, TOOL_HANDLER);
|
|
97
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { getOrgAlarmStatus, getAlertMonitoringThreatCases, getLocationAlarmStatus, } from "../api/alarm-monitoring-tool-api.js";
|
|
2
|
+
import { AlarmMonitoringRequestType, OUTPUT_SCHEMA, TOOL_ARGS, } from "../types/alarm-monitoring-tool-types.js";
|
|
3
|
+
import { createToolStructuredContent, extractFromToolExtra } from "../util.js";
|
|
4
|
+
const TOOL_NAME = "alarm-monitoring-tool";
|
|
5
|
+
const TOOL_DESCRIPTION = `
|
|
6
|
+
This tool retrieves alarm monitoring status and threat case information across Rhombus locations.
|
|
7
|
+
|
|
8
|
+
It has the following modes of operation, determined by the "requestType" parameter:
|
|
9
|
+
- ${AlarmMonitoringRequestType.ORG_STATUS}: Get the armed/disarmed alarm monitoring status for ALL locations in the organization in a single call.
|
|
10
|
+
- ${AlarmMonitoringRequestType.GET_THREAT_CASES}: Retrieve alarm monitoring threat cases (security incidents). Supports filtering by time range and max results.
|
|
11
|
+
- ${AlarmMonitoringRequestType.LOCATION_STATUS}: Get detailed alarm monitoring status for a specific location. Requires locationUuid.
|
|
12
|
+
`;
|
|
13
|
+
const TOOL_HANDLER = async (args, _extra) => {
|
|
14
|
+
const { requestModifiers, sessionId } = extractFromToolExtra(_extra);
|
|
15
|
+
try {
|
|
16
|
+
switch (args.requestType) {
|
|
17
|
+
case AlarmMonitoringRequestType.ORG_STATUS: {
|
|
18
|
+
const locationStatuses = await getOrgAlarmStatus(requestModifiers, sessionId);
|
|
19
|
+
return createToolStructuredContent({ locationStatuses });
|
|
20
|
+
}
|
|
21
|
+
case AlarmMonitoringRequestType.GET_THREAT_CASES: {
|
|
22
|
+
const threatCases = await getAlertMonitoringThreatCases(args.startTimeMs ?? undefined, args.endTimeMs ?? undefined, args.maxResults ?? undefined, requestModifiers, sessionId);
|
|
23
|
+
return createToolStructuredContent({ threatCases });
|
|
24
|
+
}
|
|
25
|
+
case AlarmMonitoringRequestType.LOCATION_STATUS: {
|
|
26
|
+
if (!args.locationUuid) {
|
|
27
|
+
return createToolStructuredContent({
|
|
28
|
+
error: "locationUuid is required for location-status.",
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
const locationDetail = await getLocationAlarmStatus(args.locationUuid, requestModifiers, sessionId);
|
|
32
|
+
return createToolStructuredContent({ locationDetail });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
if (error instanceof Error) {
|
|
38
|
+
return createToolStructuredContent({ error: error.message });
|
|
39
|
+
}
|
|
40
|
+
return createToolStructuredContent({ error: "Unknown error" });
|
|
41
|
+
}
|
|
42
|
+
return createToolStructuredContent({ error: "Invalid request type" });
|
|
43
|
+
};
|
|
44
|
+
export function createTool(server) {
|
|
45
|
+
server.registerTool(TOOL_NAME, {
|
|
46
|
+
description: TOOL_DESCRIPTION,
|
|
47
|
+
inputSchema: TOOL_ARGS,
|
|
48
|
+
outputSchema: OUTPUT_SCHEMA.shape,
|
|
49
|
+
}, TOOL_HANDLER);
|
|
50
|
+
}
|