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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { updateCameraConfig, getCameraDetails, cleanUpdatePayload, formatCameraSettings, } from "../api/update-tool-api.js";
|
|
1
|
+
import { updateCameraConfig, getCameraDetails, cleanUpdatePayload, formatCameraSettings, validateCameraFeatureSupport, } from "../api/update-tool-api.js";
|
|
2
2
|
import { OUTPUT_SCHEMA, TOOL_ARGS, UpdateCameraConfigPayload, CameraVideoSettings, CameraAudioSettings, CameraDeviceSettings, parseFacetedUuid, } from "../types/update-tool-types.js";
|
|
3
3
|
import { logger } from "../logger.js";
|
|
4
4
|
const TOOL_NAME = "update-tool";
|
|
@@ -22,6 +22,9 @@ Future support planned for:
|
|
|
22
22
|
- Climate sensors
|
|
23
23
|
- Door controllers
|
|
24
24
|
- Environmental gateways
|
|
25
|
+
- Audio gateways
|
|
26
|
+
- Doorbell cameras
|
|
27
|
+
- Badge readers
|
|
25
28
|
|
|
26
29
|
The tool uses elicitation forms for rich user interaction and shows current settings before updates.
|
|
27
30
|
`;
|
|
@@ -84,14 +87,30 @@ const TOOL_HANDLER = async (args, extra) => {
|
|
|
84
87
|
}
|
|
85
88
|
// Validate the full payload
|
|
86
89
|
const validatedPayload = UpdateCameraConfigPayload.parse(updatePayload);
|
|
90
|
+
const featureValidation = await validateCameraFeatureSupport(validatedPayload, extra._meta?.requestModifiers, extra.sessionId);
|
|
91
|
+
if (!featureValidation.canProceed) {
|
|
92
|
+
return {
|
|
93
|
+
content: [
|
|
94
|
+
{
|
|
95
|
+
type: "text",
|
|
96
|
+
text: featureValidation.error ||
|
|
97
|
+
"This camera does not support one or more requested features.",
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
};
|
|
101
|
+
}
|
|
87
102
|
// Apply the updates
|
|
88
103
|
const result = await updateCameraConfig(validatedPayload, extra._meta?.requestModifiers, extra.sessionId);
|
|
89
104
|
if (!result.success) {
|
|
105
|
+
const hasCapabilitySensitiveChange = hasCapabilitySensitiveSettings(validatedPayload);
|
|
106
|
+
const normalizedError = hasCapabilitySensitiveChange
|
|
107
|
+
? "This camera may not support one or more requested settings."
|
|
108
|
+
: result.error;
|
|
90
109
|
return {
|
|
91
110
|
content: [
|
|
92
111
|
{
|
|
93
112
|
type: "text",
|
|
94
|
-
text: `Failed to update camera settings: ${
|
|
113
|
+
text: `Failed to update camera settings: ${normalizedError}`,
|
|
95
114
|
},
|
|
96
115
|
],
|
|
97
116
|
};
|
|
@@ -238,6 +257,36 @@ const TOOL_HANDLER = async (args, extra) => {
|
|
|
238
257
|
],
|
|
239
258
|
};
|
|
240
259
|
}
|
|
260
|
+
if (entityType === "audio-gateway") {
|
|
261
|
+
return {
|
|
262
|
+
content: [
|
|
263
|
+
{
|
|
264
|
+
type: "text",
|
|
265
|
+
text: "Audio gateway updates are not yet fully implemented. Coming soon!",
|
|
266
|
+
},
|
|
267
|
+
],
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
if (entityType === "doorbell-camera") {
|
|
271
|
+
return {
|
|
272
|
+
content: [
|
|
273
|
+
{
|
|
274
|
+
type: "text",
|
|
275
|
+
text: "Doorbell camera updates are not yet fully implemented. Coming soon!",
|
|
276
|
+
},
|
|
277
|
+
],
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
if (entityType === "badge-reader") {
|
|
281
|
+
return {
|
|
282
|
+
content: [
|
|
283
|
+
{
|
|
284
|
+
type: "text",
|
|
285
|
+
text: "Badge reader updates are not yet fully implemented. Coming soon!",
|
|
286
|
+
},
|
|
287
|
+
],
|
|
288
|
+
};
|
|
289
|
+
}
|
|
241
290
|
// Step 0: Show initial form (no entityType provided)
|
|
242
291
|
return {
|
|
243
292
|
content: [
|
|
@@ -253,6 +302,18 @@ const TOOL_HANDLER = async (args, extra) => {
|
|
|
253
302
|
],
|
|
254
303
|
};
|
|
255
304
|
};
|
|
305
|
+
function hasCapabilitySensitiveSettings(payload) {
|
|
306
|
+
const requestedVideoSettings = Object.values(payload.configUpdate.videoFacetSettings ?? {});
|
|
307
|
+
const hasCapabilityDependentVideoChange = requestedVideoSettings.some(settings => settings.resolution !== undefined ||
|
|
308
|
+
settings.hdr_enabled !== undefined ||
|
|
309
|
+
settings.wdr_enabled !== undefined ||
|
|
310
|
+
settings.wdr_strength !== undefined);
|
|
311
|
+
const requestedAudioSettings = Object.values(payload.configUpdate.audioFacetSettings ?? {});
|
|
312
|
+
const hasCapabilityDependentAudioChange = requestedAudioSettings.some(settings => settings.audio_record !== undefined ||
|
|
313
|
+
settings.device_mic_enabled !== undefined ||
|
|
314
|
+
settings.device_speaker_enabled !== undefined);
|
|
315
|
+
return hasCapabilityDependentVideoChange || hasCapabilityDependentAudioChange;
|
|
316
|
+
}
|
|
256
317
|
export function createTool(server) {
|
|
257
318
|
server.registerTool(TOOL_NAME, {
|
|
258
319
|
description: TOOL_DESCRIPTION,
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { resolveUserUuid, getAccessEventsByUser, } from "../api/user-access-trail-tool-api.js";
|
|
2
|
+
import { UserAccessTrailRequestType, OUTPUT_SCHEMA, TOOL_ARGS, } from "../types/user-access-trail-tool-types.js";
|
|
3
|
+
import { createToolStructuredContent, extractFromToolExtra } from "../util.js";
|
|
4
|
+
const TOOL_NAME = "user-access-trail-tool";
|
|
5
|
+
const TOOL_DESCRIPTION = `
|
|
6
|
+
This tool traces a specific user's physical access events across all doors and access points. It answers questions like "Where did this person badge in?" or "Show me all access events for this user."
|
|
7
|
+
|
|
8
|
+
It has the following modes of operation, determined by the "requestType" parameter:
|
|
9
|
+
- ${UserAccessTrailRequestType.GET_ACCESS_EVENTS_BY_USER}: Get access events using a known userUuid. Requires userUuid.
|
|
10
|
+
- ${UserAccessTrailRequestType.GET_ACCESS_EVENTS_BY_EMAIL}: Get access events by looking up the user's email first. Requires email. Automatically resolves the user UUID.
|
|
11
|
+
|
|
12
|
+
Both modes support optional startTimeMs, endTimeMs, and limit parameters.
|
|
13
|
+
Use the user-tool to find user UUIDs if needed, or use the email-based lookup directly.
|
|
14
|
+
`;
|
|
15
|
+
const TOOL_HANDLER = async (args, _extra) => {
|
|
16
|
+
const { requestModifiers, sessionId } = extractFromToolExtra(_extra);
|
|
17
|
+
try {
|
|
18
|
+
let userUuid;
|
|
19
|
+
let userName;
|
|
20
|
+
switch (args.requestType) {
|
|
21
|
+
case UserAccessTrailRequestType.GET_ACCESS_EVENTS_BY_USER: {
|
|
22
|
+
if (!args.userUuid) {
|
|
23
|
+
return createToolStructuredContent({
|
|
24
|
+
error: "userUuid is required for get-access-events-by-user.",
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
userUuid = args.userUuid;
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
case UserAccessTrailRequestType.GET_ACCESS_EVENTS_BY_EMAIL: {
|
|
31
|
+
if (!args.email) {
|
|
32
|
+
return createToolStructuredContent({
|
|
33
|
+
error: "email is required for get-access-events-by-email.",
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
const resolved = await resolveUserUuid(args.email, requestModifiers, sessionId);
|
|
37
|
+
userUuid = resolved.uuid;
|
|
38
|
+
userName = resolved.name;
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
default:
|
|
42
|
+
return createToolStructuredContent({ error: "Invalid request type" });
|
|
43
|
+
}
|
|
44
|
+
const accessEvents = await getAccessEventsByUser(userUuid, args.startTimeMs ?? undefined, args.endTimeMs ?? undefined, args.limit ?? undefined, requestModifiers, sessionId);
|
|
45
|
+
return createToolStructuredContent({
|
|
46
|
+
userUuid,
|
|
47
|
+
userName,
|
|
48
|
+
accessEvents,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
if (error instanceof Error) {
|
|
53
|
+
return createToolStructuredContent({ error: error.message });
|
|
54
|
+
}
|
|
55
|
+
return createToolStructuredContent({ error: "Unknown error" });
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
export function createTool(server) {
|
|
59
|
+
server.registerTool(TOOL_NAME, {
|
|
60
|
+
description: TOOL_DESCRIPTION,
|
|
61
|
+
inputSchema: TOOL_ARGS,
|
|
62
|
+
outputSchema: OUTPUT_SCHEMA.shape,
|
|
63
|
+
}, TOOL_HANDLER);
|
|
64
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { getAuditFeedForPrincipal, getAuditFeedForTarget, } from "../api/user-audit-tool-api.js";
|
|
2
|
+
import { UserAuditRequestType, OUTPUT_SCHEMA, TOOL_ARGS, } from "../types/user-audit-tool-types.js";
|
|
3
|
+
import { createToolStructuredContent, extractFromToolExtra } from "../util.js";
|
|
4
|
+
const TOOL_NAME = "user-audit-tool";
|
|
5
|
+
const TOOL_DESCRIPTION = `
|
|
6
|
+
This tool retrieves configuration audit trails for specific users or specific targets (devices, policies, etc.).
|
|
7
|
+
Unlike the general audit feed, this focuses on a SINGLE user or a SINGLE target entity.
|
|
8
|
+
|
|
9
|
+
It has the following modes of operation, determined by the "requestType" parameter:
|
|
10
|
+
- ${UserAuditRequestType.AUDIT_BY_USER}: Get all configuration changes made BY a specific user. Requires userUuid (use user-tool to find it). Answers: "What did this admin change recently?"
|
|
11
|
+
- ${UserAuditRequestType.AUDIT_BY_TARGET}: Get all configuration changes made TO a specific entity (camera, door, policy, etc.). Requires targetUuid. Answers: "Who changed the settings on this camera?"
|
|
12
|
+
|
|
13
|
+
Both modes return audit events with action, display text, who did it, what was changed, and when.
|
|
14
|
+
`;
|
|
15
|
+
const TOOL_HANDLER = async (args, _extra) => {
|
|
16
|
+
const { requestModifiers, sessionId } = extractFromToolExtra(_extra);
|
|
17
|
+
try {
|
|
18
|
+
switch (args.requestType) {
|
|
19
|
+
case UserAuditRequestType.AUDIT_BY_USER: {
|
|
20
|
+
if (!args.userUuid) {
|
|
21
|
+
return createToolStructuredContent({
|
|
22
|
+
error: "userUuid is required for audit-by-user. Use user-tool to find it.",
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
const auditEvents = await getAuditFeedForPrincipal(args.userUuid, args.maxResults ?? undefined, requestModifiers, sessionId);
|
|
26
|
+
return createToolStructuredContent({ auditEvents });
|
|
27
|
+
}
|
|
28
|
+
case UserAuditRequestType.AUDIT_BY_TARGET: {
|
|
29
|
+
if (!args.targetUuid) {
|
|
30
|
+
return createToolStructuredContent({
|
|
31
|
+
error: "targetUuid is required for audit-by-target.",
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
const auditEvents = await getAuditFeedForTarget(args.targetUuid, args.maxResults ?? undefined, requestModifiers, sessionId);
|
|
35
|
+
return createToolStructuredContent({ auditEvents });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
if (error instanceof Error) {
|
|
41
|
+
return createToolStructuredContent({ error: error.message });
|
|
42
|
+
}
|
|
43
|
+
return createToolStructuredContent({ error: "Unknown error" });
|
|
44
|
+
}
|
|
45
|
+
return createToolStructuredContent({ error: "Invalid request type" });
|
|
46
|
+
};
|
|
47
|
+
export function createTool(server) {
|
|
48
|
+
server.registerTool(TOOL_NAME, {
|
|
49
|
+
description: TOOL_DESCRIPTION,
|
|
50
|
+
inputSchema: TOOL_ARGS,
|
|
51
|
+
outputSchema: OUTPUT_SCHEMA.shape,
|
|
52
|
+
}, TOOL_HANDLER);
|
|
53
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { listUsers, findUserByEmail, getPermissionsForCurrentUser, getPermissionGroups, } from "../api/user-tool-api.js";
|
|
2
|
+
import { UserToolRequestType, OUTPUT_SCHEMA, TOOL_ARGS, } from "../types/user-tool-types.js";
|
|
3
|
+
import { createToolStructuredContent, createToolTextContent, extractFromToolExtra, } from "../util.js";
|
|
4
|
+
const TOOL_NAME = "user-tool";
|
|
5
|
+
const TOOL_DESCRIPTION = `
|
|
6
|
+
This tool manages Rhombus user operations including listing users, finding users by email, and viewing permissions.
|
|
7
|
+
|
|
8
|
+
It has the following modes of operation, determined by the "requestType" parameter:
|
|
9
|
+
- ${UserToolRequestType.LIST_USERS}: List all users in the organization with their details and roles.
|
|
10
|
+
- ${UserToolRequestType.FIND_BY_EMAIL}: Find a specific user by their email address. Requires the email parameter.
|
|
11
|
+
- ${UserToolRequestType.GET_PERMISSIONS}: Get the permissions for the current API user/token.
|
|
12
|
+
- ${UserToolRequestType.GET_PERMISSION_GROUPS}: List all permission groups defined in the organization.
|
|
13
|
+
|
|
14
|
+
User UUIDs returned here can be used with the access-control-tool to look up credentials.
|
|
15
|
+
`;
|
|
16
|
+
const TOOL_HANDLER = async (args, _extra) => {
|
|
17
|
+
const { requestModifiers, sessionId } = extractFromToolExtra(_extra);
|
|
18
|
+
try {
|
|
19
|
+
switch (args.requestType) {
|
|
20
|
+
case UserToolRequestType.LIST_USERS: {
|
|
21
|
+
const users = await listUsers(requestModifiers, sessionId);
|
|
22
|
+
return createToolStructuredContent({ users });
|
|
23
|
+
}
|
|
24
|
+
case UserToolRequestType.FIND_BY_EMAIL: {
|
|
25
|
+
if (!args.email) {
|
|
26
|
+
return createToolTextContent(JSON.stringify({ error: "email is required for find-by-email." }));
|
|
27
|
+
}
|
|
28
|
+
const user = await findUserByEmail(args.email, requestModifiers, sessionId);
|
|
29
|
+
return createToolStructuredContent({ user });
|
|
30
|
+
}
|
|
31
|
+
case UserToolRequestType.GET_PERMISSIONS: {
|
|
32
|
+
const permissions = await getPermissionsForCurrentUser(requestModifiers, sessionId);
|
|
33
|
+
return createToolStructuredContent({ permissions });
|
|
34
|
+
}
|
|
35
|
+
case UserToolRequestType.GET_PERMISSION_GROUPS: {
|
|
36
|
+
const permissionGroups = await getPermissionGroups(requestModifiers, sessionId);
|
|
37
|
+
return createToolStructuredContent({ permissionGroups });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
if (error instanceof Error) {
|
|
43
|
+
return createToolStructuredContent({ error: error.message });
|
|
44
|
+
}
|
|
45
|
+
return createToolStructuredContent({ error: "Unknown error" });
|
|
46
|
+
}
|
|
47
|
+
return createToolStructuredContent({ error: "Invalid request type" });
|
|
48
|
+
};
|
|
49
|
+
export function createTool(server) {
|
|
50
|
+
server.registerTool(TOOL_NAME, {
|
|
51
|
+
description: TOOL_DESCRIPTION,
|
|
52
|
+
inputSchema: TOOL_ARGS,
|
|
53
|
+
outputSchema: OUTPUT_SCHEMA.shape,
|
|
54
|
+
}, TOOL_HANDLER);
|
|
55
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { getVideoWalls, handleCreateVideoWallRequest, } from "../api/create-tool-api.js";
|
|
2
|
+
import { OUTPUT_SCHEMA, TOOL_ARGS, } from "../types/video-walls-tool-types.js";
|
|
3
|
+
import { createToolStructuredContent, createToolTextContent, extractFromToolExtra, } from "../util.js";
|
|
4
|
+
const TOOL_NAME = "video-walls-tool";
|
|
5
|
+
const TOOL_DESCRIPTION = `
|
|
6
|
+
This tool interacts with Rhombus video walls. Rhombus video walls are a collection of camera feeds combined into a single view,
|
|
7
|
+
allowing users to monitor multiple cameras.
|
|
8
|
+
|
|
9
|
+
The layout of created video walls is automatically determined by the number of cameras in video wall settings "numVisibleDevicesAtOnce".
|
|
10
|
+
`;
|
|
11
|
+
const TOOL_HANDLER = async (args, extra) => {
|
|
12
|
+
const { requestType, videoWallCreateOptions } = args;
|
|
13
|
+
const { requestModifiers, sessionId } = extractFromToolExtra(extra);
|
|
14
|
+
switch (requestType) {
|
|
15
|
+
case "create":
|
|
16
|
+
if (!videoWallCreateOptions) {
|
|
17
|
+
return createToolTextContent(JSON.stringify({
|
|
18
|
+
error: "videoWallCreateOptions is required. Please try again.",
|
|
19
|
+
}));
|
|
20
|
+
}
|
|
21
|
+
return createToolStructuredContent(await handleCreateVideoWallRequest(videoWallCreateOptions, requestModifiers, sessionId));
|
|
22
|
+
case "list":
|
|
23
|
+
return createToolStructuredContent(await getVideoWalls(requestModifiers, sessionId));
|
|
24
|
+
default:
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
content: [
|
|
28
|
+
{
|
|
29
|
+
type: "text",
|
|
30
|
+
text: "Invalid entity type. Please try again.",
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
export function createTool(server) {
|
|
36
|
+
server.registerTool(TOOL_NAME, {
|
|
37
|
+
description: TOOL_DESCRIPTION,
|
|
38
|
+
inputSchema: TOOL_ARGS,
|
|
39
|
+
outputSchema: OUTPUT_SCHEMA.shape,
|
|
40
|
+
}, TOOL_HANDLER);
|
|
41
|
+
}
|
|
@@ -7,6 +7,7 @@ import createServer from "../createServer.js";
|
|
|
7
7
|
import { logger } from "../logger.js";
|
|
8
8
|
var AuthScheme;
|
|
9
9
|
(function (AuthScheme) {
|
|
10
|
+
AuthScheme["OAUTH"] = "oauth";
|
|
10
11
|
AuthScheme["API_TOKEN"] = "api-token";
|
|
11
12
|
AuthScheme["CHATBOT"] = "chatbot";
|
|
12
13
|
AuthScheme["WEB2"] = "web2";
|
|
@@ -27,13 +28,17 @@ export default function streamableHttpTransport() {
|
|
|
27
28
|
*/
|
|
28
29
|
const authRequired = ["tools/call"];
|
|
29
30
|
app.post("/mcp", async (req, res) => {
|
|
30
|
-
logger.info(`Received MCP request`, JSON.stringify(req.body));
|
|
31
|
+
logger.info(`Received MCP request`, JSON.stringify(req.body, null, 2));
|
|
31
32
|
// Check for existing session ID
|
|
32
33
|
const sessionId = req.headers["mcp-session-id"];
|
|
33
34
|
let transport;
|
|
34
35
|
if (sessionId && transports.has(sessionId)) {
|
|
35
36
|
// Reuse existing transport
|
|
36
|
-
|
|
37
|
+
const _transport = transports.get(sessionId);
|
|
38
|
+
if (!_transport) {
|
|
39
|
+
throw new Error(`Transport not found for sessionId: ${sessionId}`);
|
|
40
|
+
}
|
|
41
|
+
transport = _transport;
|
|
37
42
|
}
|
|
38
43
|
else if (!sessionId && isInitializeRequest(req.body)) {
|
|
39
44
|
// New initialization request
|
|
@@ -44,9 +49,17 @@ export default function streamableHttpTransport() {
|
|
|
44
49
|
transports.set(sessionId, transport);
|
|
45
50
|
logger.info(`🔒 MCP request initialized with sessionId: ${sessionId}`);
|
|
46
51
|
try {
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
if
|
|
52
|
+
const oauthToken = req.headers["x-auth-access-token"];
|
|
53
|
+
const authScheme = req.headers["x-auth-scheme"] ?? AuthScheme.API_TOKEN; // otherwise, read 'x-auth-scheme'
|
|
54
|
+
// if oauth token is provided
|
|
55
|
+
if (oauthToken && typeof oauthToken === "string") {
|
|
56
|
+
authStore.set(sessionId, {
|
|
57
|
+
oauthToken: oauthToken,
|
|
58
|
+
createdMs: Date.now(),
|
|
59
|
+
});
|
|
60
|
+
logger.info(`🔒 MCP request authenticated with oauth token: ${oauthToken}`);
|
|
61
|
+
}
|
|
62
|
+
else if (authScheme === AuthScheme.API_TOKEN) {
|
|
50
63
|
const apiKey = "x-auth-apikey" in req.headers
|
|
51
64
|
? req.headers["x-auth-apikey"]
|
|
52
65
|
: process.env.RHOMBUS_API_KEY;
|
|
@@ -73,6 +86,7 @@ export default function streamableHttpTransport() {
|
|
|
73
86
|
else if (authScheme === AuthScheme.WEB2 && "x-auth-cookie" in req.headers) {
|
|
74
87
|
authStore.set(sessionId, {
|
|
75
88
|
cookie: req.headers["x-auth-cookie"],
|
|
89
|
+
sessionAlias: req.headers["x-auth-session-alias"],
|
|
76
90
|
createdMs: Date.now(),
|
|
77
91
|
});
|
|
78
92
|
}
|
|
@@ -85,11 +99,16 @@ export default function streamableHttpTransport() {
|
|
|
85
99
|
if (authRequired.includes(req.body.method)) {
|
|
86
100
|
if (e instanceof Error) {
|
|
87
101
|
logger.error(e.message);
|
|
102
|
+
res
|
|
103
|
+
.status(401)
|
|
104
|
+
.setHeader("WWW-Authenticate", `Bearer realm="${process.env.REALM}", error="invalid_token", error_description="The access token is missing or invalid"`)
|
|
105
|
+
.send(e.message);
|
|
106
|
+
return;
|
|
88
107
|
}
|
|
89
108
|
else {
|
|
90
109
|
logger.error(e);
|
|
110
|
+
throw e;
|
|
91
111
|
}
|
|
92
|
-
throw e;
|
|
93
112
|
}
|
|
94
113
|
}
|
|
95
114
|
},
|
|
@@ -122,15 +141,6 @@ export default function streamableHttpTransport() {
|
|
|
122
141
|
});
|
|
123
142
|
return;
|
|
124
143
|
}
|
|
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
144
|
await transport.handleRequest(req, res, req.body);
|
|
135
145
|
});
|
|
136
146
|
app.get("/mcp", async (_, res) => {
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { INCLUDE_FIELDS_ARG, FILTER_BY_ARG } from "../util.js";
|
|
3
|
+
export var AccessControlRequestType;
|
|
4
|
+
(function (AccessControlRequestType) {
|
|
5
|
+
AccessControlRequestType["UNLOCK_DOOR"] = "unlock-door";
|
|
6
|
+
AccessControlRequestType["GET_GROUPS"] = "get-groups";
|
|
7
|
+
AccessControlRequestType["GET_CREDENTIALS_BY_USER"] = "get-credentials-by-user";
|
|
8
|
+
AccessControlRequestType["GET_LOCKDOWN_PLANS"] = "get-lockdown-plans";
|
|
9
|
+
AccessControlRequestType["ACTIVATE_LOCKDOWN"] = "activate-lockdown";
|
|
10
|
+
AccessControlRequestType["DEACTIVATE_LOCKDOWN"] = "deactivate-lockdown";
|
|
11
|
+
AccessControlRequestType["GET_DOOR_SCHEDULES"] = "get-door-schedules";
|
|
12
|
+
AccessControlRequestType["GET_ACCESS_GRANTS"] = "get-access-grants";
|
|
13
|
+
AccessControlRequestType["GET_REMOTE_UNLOCK_USERS"] = "get-remote-unlock-users";
|
|
14
|
+
})(AccessControlRequestType || (AccessControlRequestType = {}));
|
|
15
|
+
export const TOOL_ARGS = {
|
|
16
|
+
requestType: z.nativeEnum(AccessControlRequestType).describe("The type of access control request to make."),
|
|
17
|
+
doorUuid: z
|
|
18
|
+
.string()
|
|
19
|
+
.nullable()
|
|
20
|
+
.describe("The UUID of the access controlled door. Required for 'unlock-door'."),
|
|
21
|
+
userUuid: z
|
|
22
|
+
.string()
|
|
23
|
+
.nullable()
|
|
24
|
+
.describe("The UUID of the user. Required for 'get-credentials-by-user'."),
|
|
25
|
+
locationUuid: z
|
|
26
|
+
.string()
|
|
27
|
+
.nullable()
|
|
28
|
+
.describe("The UUID of the location. Required for 'activate-lockdown', 'deactivate-lockdown', 'get-door-schedules', and 'get-remote-unlock-users'. Optional for 'get-access-grants' to filter by location."),
|
|
29
|
+
lockdownPlanUuid: z
|
|
30
|
+
.string()
|
|
31
|
+
.nullable()
|
|
32
|
+
.describe("The UUID of the lockdown plan. Required for 'activate-lockdown' and 'deactivate-lockdown'."),
|
|
33
|
+
includeFields: INCLUDE_FIELDS_ARG,
|
|
34
|
+
filterBy: FILTER_BY_ARG,
|
|
35
|
+
};
|
|
36
|
+
const TOOL_ARGS_SCHEMA = z.object(TOOL_ARGS);
|
|
37
|
+
export const OUTPUT_SCHEMA = z.object({
|
|
38
|
+
unlockResult: z
|
|
39
|
+
.object({
|
|
40
|
+
success: z.boolean().optional(),
|
|
41
|
+
doorUuid: z.string().optional(),
|
|
42
|
+
})
|
|
43
|
+
.optional()
|
|
44
|
+
.describe("Result of unlocking a door"),
|
|
45
|
+
accessControlGroups: z
|
|
46
|
+
.array(z.object({
|
|
47
|
+
uuid: z.string().optional(),
|
|
48
|
+
name: z.string().optional(),
|
|
49
|
+
description: z.string().optional(),
|
|
50
|
+
orgUuid: z.string().optional(),
|
|
51
|
+
userUuids: z.array(z.string()).optional(),
|
|
52
|
+
}))
|
|
53
|
+
.optional()
|
|
54
|
+
.describe("List of access control groups"),
|
|
55
|
+
credentials: z
|
|
56
|
+
.array(z.object({
|
|
57
|
+
uuid: z.string().optional(),
|
|
58
|
+
userUuid: z.string().optional(),
|
|
59
|
+
credentialType: z.string().optional(),
|
|
60
|
+
status: z.string().optional(),
|
|
61
|
+
note: z.string().optional(),
|
|
62
|
+
}))
|
|
63
|
+
.optional()
|
|
64
|
+
.describe("List of access control credentials for a user"),
|
|
65
|
+
lockdownPlans: z
|
|
66
|
+
.array(z.object({
|
|
67
|
+
uuid: z.string().optional(),
|
|
68
|
+
name: z.string().optional(),
|
|
69
|
+
locationUuid: z.string().optional(),
|
|
70
|
+
description: z.string().optional(),
|
|
71
|
+
active: z.boolean().optional(),
|
|
72
|
+
}))
|
|
73
|
+
.optional()
|
|
74
|
+
.describe("List of lockdown plans"),
|
|
75
|
+
lockdownResult: z
|
|
76
|
+
.object({
|
|
77
|
+
success: z.boolean().optional(),
|
|
78
|
+
locationUuid: z.string().optional(),
|
|
79
|
+
action: z.string().optional(),
|
|
80
|
+
})
|
|
81
|
+
.optional()
|
|
82
|
+
.describe("Result of activating or deactivating a lockdown"),
|
|
83
|
+
doorScheduleExceptions: z
|
|
84
|
+
.array(z.object({
|
|
85
|
+
uuid: z.string().optional(),
|
|
86
|
+
name: z.string().optional(),
|
|
87
|
+
startTime: z.number().optional(),
|
|
88
|
+
endTime: z.number().optional(),
|
|
89
|
+
doorUuids: z.array(z.string()).optional(),
|
|
90
|
+
}))
|
|
91
|
+
.optional()
|
|
92
|
+
.describe("Door schedule exceptions"),
|
|
93
|
+
accessGrants: z
|
|
94
|
+
.array(z.object({
|
|
95
|
+
uuid: z.string().optional(),
|
|
96
|
+
name: z.string().optional(),
|
|
97
|
+
locationUuid: z.string().optional(),
|
|
98
|
+
userUuids: z.array(z.string()).optional(),
|
|
99
|
+
groupUuids: z.array(z.string()).optional(),
|
|
100
|
+
doorUuids: z.array(z.string()).optional(),
|
|
101
|
+
scheduleUuid: z.string().optional(),
|
|
102
|
+
}))
|
|
103
|
+
.optional()
|
|
104
|
+
.describe("List of location access grants. Each grant contains userUuids and groupUuids that have access to the doorUuids in the grant."),
|
|
105
|
+
remoteUnlockUsers: z
|
|
106
|
+
.object({
|
|
107
|
+
doors: z.array(z.string()).optional().describe("Names of doors with remote unlock enabled at this location."),
|
|
108
|
+
totalUsers: z.number().optional().describe("Total number of unique users who can remotely unlock doors."),
|
|
109
|
+
groups: z.array(z.object({
|
|
110
|
+
permissionGroup: z.string().optional().describe("Name of the permission group/role."),
|
|
111
|
+
doors: z.union([z.literal("all"), z.array(z.string())]).optional()
|
|
112
|
+
.describe("Which doors users in this group can unlock. 'all' means every door at the location."),
|
|
113
|
+
users: z.array(z.string()).optional()
|
|
114
|
+
.describe("Users in this group, formatted as 'Name (email)'. Always list ALL users completely."),
|
|
115
|
+
})).optional(),
|
|
116
|
+
})
|
|
117
|
+
.optional()
|
|
118
|
+
.describe("Users who can remotely unlock doors at a location, grouped by permission group. Always present the COMPLETE list of all users to the end user."),
|
|
119
|
+
error: z.string().optional().describe("An error message if the request failed."),
|
|
120
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { INCLUDE_FIELDS_ARG, FILTER_BY_ARG } from "../util.js";
|
|
3
|
+
export var AlarmMonitoringRequestType;
|
|
4
|
+
(function (AlarmMonitoringRequestType) {
|
|
5
|
+
AlarmMonitoringRequestType["ORG_STATUS"] = "org-status";
|
|
6
|
+
AlarmMonitoringRequestType["GET_THREAT_CASES"] = "get-threat-cases";
|
|
7
|
+
AlarmMonitoringRequestType["LOCATION_STATUS"] = "location-status";
|
|
8
|
+
})(AlarmMonitoringRequestType || (AlarmMonitoringRequestType = {}));
|
|
9
|
+
export const TOOL_ARGS = {
|
|
10
|
+
requestType: z
|
|
11
|
+
.nativeEnum(AlarmMonitoringRequestType)
|
|
12
|
+
.describe("The type of alarm monitoring request to make."),
|
|
13
|
+
locationUuid: z
|
|
14
|
+
.string()
|
|
15
|
+
.nullable()
|
|
16
|
+
.describe("Location UUID. Required for 'location-status'."),
|
|
17
|
+
startTimeMs: z
|
|
18
|
+
.number()
|
|
19
|
+
.nullable()
|
|
20
|
+
.describe("Filter threat cases after this timestamp (ms since epoch). Optional for 'get-threat-cases'."),
|
|
21
|
+
endTimeMs: z
|
|
22
|
+
.number()
|
|
23
|
+
.nullable()
|
|
24
|
+
.describe("Filter threat cases before this timestamp (ms since epoch). Optional for 'get-threat-cases'."),
|
|
25
|
+
maxResults: z
|
|
26
|
+
.number()
|
|
27
|
+
.nullable()
|
|
28
|
+
.describe("Maximum number of threat cases to return. Optional for 'get-threat-cases'."),
|
|
29
|
+
includeFields: INCLUDE_FIELDS_ARG,
|
|
30
|
+
filterBy: FILTER_BY_ARG,
|
|
31
|
+
};
|
|
32
|
+
const TOOL_ARGS_SCHEMA = z.object(TOOL_ARGS);
|
|
33
|
+
const LocationMonitoringStatusSchema = z.object({
|
|
34
|
+
locationUuid: z.string().optional(),
|
|
35
|
+
enabled: z.boolean().optional(),
|
|
36
|
+
enabledOnMs: z.number().optional(),
|
|
37
|
+
disabledOnMs: z.number().optional(),
|
|
38
|
+
notEnabledReason: z.string().optional(),
|
|
39
|
+
});
|
|
40
|
+
const ThreatCaseSchema = z.object({
|
|
41
|
+
uuid: z.string().optional(),
|
|
42
|
+
status: z.string().optional(),
|
|
43
|
+
locationName: z.string().optional(),
|
|
44
|
+
locationUuid: z.string().optional(),
|
|
45
|
+
deviceUuid: z.string().optional(),
|
|
46
|
+
createdAtMillis: z.number().optional(),
|
|
47
|
+
promptTitle: z.string().optional(),
|
|
48
|
+
});
|
|
49
|
+
export const OUTPUT_SCHEMA = z.object({
|
|
50
|
+
locationStatuses: z
|
|
51
|
+
.array(LocationMonitoringStatusSchema)
|
|
52
|
+
.optional()
|
|
53
|
+
.describe("Alarm monitoring status for each location"),
|
|
54
|
+
threatCases: z
|
|
55
|
+
.array(ThreatCaseSchema)
|
|
56
|
+
.optional()
|
|
57
|
+
.describe("List of alarm monitoring threat cases"),
|
|
58
|
+
locationDetail: LocationMonitoringStatusSchema.optional().describe("Detailed alarm monitoring status for a single location"),
|
|
59
|
+
error: z.string().optional().describe("An error message if the request failed."),
|
|
60
|
+
});
|