rhombus-node-mcp 0.1.43 → 0.1.45

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.
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Keystone correlation helper for the OnGuard feature set.
3
+ *
4
+ * Given a badge-event-like entry (a camera `deviceUuid` + a `timestampMs`), produce the media hints
5
+ * the agent needs to *show* the moment: a still (camera-tool `image`) and a short clip window
6
+ * (clips-tool `createClip`). Pure and reusable — the follow-the-badge timeline, anomaly review, and
7
+ * lost-badge tracking all turn badge events into "here's the picture/video" the same way.
8
+ *
9
+ * Kept deliberately small and side-effect-free. Richer correlation (co-located face events,
10
+ * people-count in the window) layers on top of this when those features land.
11
+ */
12
+ export const DEFAULT_CLIP_PADDING_SECONDS = 15;
13
+ /**
14
+ * Build still + clip hints for a single badge event. Returns empty hints (no clip/still) when the
15
+ * event lacks a camera or timestamp, so callers can render those events without media gracefully.
16
+ */
17
+ export function buildMediaHints(event, clipPaddingSeconds = DEFAULT_CLIP_PADDING_SECONDS) {
18
+ if (!event.deviceUuid || event.timestampMs == null) {
19
+ return {};
20
+ }
21
+ const padMs = Math.max(0, clipPaddingSeconds) * 1000;
22
+ return {
23
+ clipHint: {
24
+ deviceUuid: event.deviceUuid,
25
+ startTimeMs: event.timestampMs - padMs,
26
+ endTimeMs: event.timestampMs + padMs,
27
+ },
28
+ stillHint: {
29
+ deviceUuid: event.deviceUuid,
30
+ timestampMs: event.timestampMs,
31
+ },
32
+ };
33
+ }
@@ -0,0 +1,56 @@
1
+ import { buildMediaHints } from "./badge-correlation.js";
2
+ import { searchOnGuardEvents } from "./onguard-tool-api.js";
3
+ /**
4
+ * Reconstructs a single cardholder's movements as a chronological timeline of OnGuard badge taps,
5
+ * each with the still/clip hints needed to show what happened. Pure orchestration over
6
+ * searchOnGuardEvents + the shared correlation helper.
7
+ */
8
+ export async function getBadgeTimeline(args, timeZone, requestModifiers, sessionId) {
9
+ const { events } = await searchOnGuardEvents({
10
+ cardholderQuery: args.cardholderQuery,
11
+ locationUuids: args.locationUuids,
12
+ afterMs: args.afterMs,
13
+ beforeMs: args.beforeMs,
14
+ limit: args.limit ?? 200,
15
+ }, timeZone, requestModifiers, sessionId);
16
+ // searchOnGuardEvents returns newest-first; a timeline reads chronologically.
17
+ const ordered = events
18
+ .filter((e) => e.timestampMs != null)
19
+ .sort((a, b) => (a.timestampMs ?? 0) - (b.timestampMs ?? 0));
20
+ const stops = ordered.map((e, i) => {
21
+ const next = ordered[i + 1];
22
+ const gapToNextSeconds = next?.timestampMs != null && e.timestampMs != null
23
+ ? Math.round((next.timestampMs - e.timestampMs) / 1000)
24
+ : undefined;
25
+ const { clipHint, stillHint } = buildMediaHints({ deviceUuid: e.deviceUuid, timestampMs: e.timestampMs }, args.clipPaddingSeconds);
26
+ return {
27
+ timestampMs: e.timestampMs,
28
+ datetime: e.datetime,
29
+ deviceUuid: e.deviceUuid,
30
+ area: e.areaEntering ?? e.areaExiting,
31
+ label: e.label,
32
+ isAnomaly: e.isAnomaly,
33
+ clipHint,
34
+ stillHint,
35
+ gapToNextSeconds,
36
+ };
37
+ });
38
+ // The sequence of areas traversed, collapsing consecutive repeats.
39
+ const path = [];
40
+ for (const stop of stops) {
41
+ if (stop.area && stop.area !== path[path.length - 1]) {
42
+ path.push(stop.area);
43
+ }
44
+ }
45
+ // cardholderQuery is full-text, so it can match more than one person — surface that so the agent
46
+ // can disambiguate rather than silently merge two people's movements.
47
+ const distinctCardholders = [
48
+ ...new Set(ordered.map((e) => e.cardholderName).filter((n) => !!n)),
49
+ ];
50
+ return {
51
+ cardholderName: distinctCardholders[0],
52
+ ambiguousCardholders: distinctCardholders.length > 1 ? distinctCardholders : undefined,
53
+ stops,
54
+ path,
55
+ };
56
+ }
@@ -0,0 +1,44 @@
1
+ import { postApi } from "../network/network.js";
2
+ import { formatTimestamp } from "../util.js";
3
+ /**
4
+ * Calls the webservice OnGuard event search (POST /eventSearchV2/searchOnGuardEvents) and maps the
5
+ * raw seekpoints to an agent-friendly shape. Typed against the generated public OpenAPI schema.
6
+ */
7
+ export async function searchOnGuardEvents(args, timeZone, requestModifiers, sessionId) {
8
+ const body = {
9
+ deviceUuids: args.deviceUuids,
10
+ locationUuids: args.locationUuids,
11
+ afterMs: args.afterMs,
12
+ beforeMs: args.beforeMs,
13
+ cardholderQuery: args.cardholderQuery,
14
+ badgeStatus: args.badgeStatus,
15
+ badgeType: args.badgeType,
16
+ area: args.area,
17
+ anomalyOnly: args.anomalyOnly,
18
+ entryMade: args.entryMade,
19
+ limit: args.limit ?? 200,
20
+ };
21
+ const res = await postApi({
22
+ route: "/eventSearchV2/searchOnGuardEvents",
23
+ body,
24
+ modifiers: requestModifiers,
25
+ sessionId,
26
+ });
27
+ if (res.error) {
28
+ throw new Error(res.status ?? res.errorMsg ?? "OnGuard event search failed");
29
+ }
30
+ const events = (res.events ?? []).map((e) => ({
31
+ timestampMs: e.timestampMs ?? undefined,
32
+ datetime: e.timestampMs != null ? formatTimestamp(e.timestampMs, timeZone) : undefined,
33
+ deviceUuid: e.deviceUuid ?? undefined,
34
+ label: e.customDisplayName ?? undefined,
35
+ cardholderName: e.customDescription ?? undefined,
36
+ badgeStatus: e.badgeStatus ?? undefined,
37
+ badgeType: e.badgeType ?? undefined,
38
+ areaEntering: e.areaEntering ?? undefined,
39
+ areaExiting: e.areaExiting ?? undefined,
40
+ entryMade: e.entryMade ?? undefined,
41
+ isAnomaly: e.alert ?? undefined,
42
+ }));
43
+ return { events };
44
+ }
@@ -0,0 +1,50 @@
1
+ import { getBadgeTimeline } from "../api/badge-timeline-tool-api.js";
2
+ import { OUTPUT_SCHEMA, TOOL_ARGS } from "../types/badge-timeline-tool-types.js";
3
+ import { createToolStructuredContent, extractFromToolExtra } from "../util.js";
4
+ const TOOL_NAME = "badge-timeline-tool";
5
+ const TOOL_DESCRIPTION = `
6
+ Reconstructs one person's movements through a building from their Honeywell OnGuard (Lenel) badge taps.
7
+ Use this for incident reconstruction / "follow the badge" requests, e.g. "reconstruct Eve's movements
8
+ yesterday" or "where did this cardholder go".
9
+
10
+ Returns the cardholder's badge taps in CHRONOLOGICAL order (oldest first), each with:
11
+ - datetime / timestampMs and the area entered
12
+ - deviceUuid: the camera at that door
13
+ - clipHint (camera + start/end window) and stillHint (camera + timestamp)
14
+ - gapToNextSeconds: time until the next tap (a large gap = unobserved movement between doors)
15
+ plus a "path" array summarizing the areas traversed in order.
16
+
17
+ Resolve relative times like "yesterday" to ISO 8601 first (use the timestamp tool), then pass
18
+ startTime/endTime. cardholderQuery is a full-text name match; if "ambiguousCardholders" is returned the
19
+ query matched more than one person — ask the user which one before trusting the timeline.
20
+
21
+ IMPORTANT — to show the movement visually: for each stop (or the key transitions), call the camera-tool
22
+ (requestType "image", cameraUuid = stop.deviceUuid, timestamp = stop.timestampMs) for a still you can see,
23
+ and/or the clips-tool (requestType "createClip", using stop.clipHint) for video. Issue those per-stop
24
+ media calls in PARALLEL, then present the timeline as a chronological narrative.
25
+ `;
26
+ const TOOL_HANDLER = async (args, _extra) => {
27
+ const { requestModifiers, sessionId } = extractFromToolExtra(_extra);
28
+ try {
29
+ const result = await getBadgeTimeline({
30
+ cardholderQuery: args.cardholderQuery,
31
+ locationUuids: args.locationUuids ?? undefined,
32
+ afterMs: args.startTime ? new Date(args.startTime).getTime() : undefined,
33
+ beforeMs: args.endTime ? new Date(args.endTime).getTime() : undefined,
34
+ clipPaddingSeconds: args.clipPaddingSeconds ?? undefined,
35
+ limit: args.limit ?? undefined,
36
+ }, args.timeZone ?? "UTC", requestModifiers, sessionId);
37
+ return createToolStructuredContent(result);
38
+ }
39
+ catch (error) {
40
+ const message = error instanceof Error ? error.message : "Unknown error";
41
+ return createToolStructuredContent({ error: message });
42
+ }
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
+ }
@@ -0,0 +1,55 @@
1
+ import { searchOnGuardEvents } from "../api/onguard-tool-api.js";
2
+ import { OUTPUT_SCHEMA, TOOL_ARGS } from "../types/onguard-tool-types.js";
3
+ import { createToolStructuredContent, extractFromToolExtra } from "../util.js";
4
+ const TOOL_NAME = "onguard-events-tool";
5
+ const TOOL_DESCRIPTION = `
6
+ Searches Honeywell OnGuard (Lenel) badge / access-control events for the organization. Use this to answer
7
+ "who entered WHERE and WHEN" questions, e.g. "who entered the back office yesterday".
8
+
9
+ Each returned event includes:
10
+ - cardholderName: the person's name
11
+ - deviceUuid: the camera that saw the event
12
+ - timestampMs / datetime: when it happened
13
+ - label: e.g. "OnGuard: Badge Authorized" (a grant) or an anomaly label
14
+ - badgeStatus, badgeType, areaEntering, areaExiting, entryMade, isAnomaly
15
+
16
+ Filters (all optional): area, locationUuids, deviceUuids, cardholderQuery, badgeStatus, badgeType,
17
+ anomalyOnly, entryMade, startTime, endTime, limit. Resolve relative times like "yesterday" to ISO 8601
18
+ first (use the timestamp tool), then pass startTime/endTime.
19
+
20
+ IMPORTANT — to show pictures and video of each person so the user can visually identify them: after this
21
+ returns, for each event (or the most relevant ones) call the camera-tool (requestType "image",
22
+ cameraUuid = the event's deviceUuid, timestamp = the event's time) to get a still you can see, and/or the
23
+ clips-tool (requestType "createClip") with a short window around the timestamp for video. Issue those
24
+ per-event media calls in PARALLEL.
25
+ `;
26
+ const TOOL_HANDLER = async (args, _extra) => {
27
+ const { requestModifiers, sessionId } = extractFromToolExtra(_extra);
28
+ try {
29
+ const result = await searchOnGuardEvents({
30
+ area: args.area ?? undefined,
31
+ locationUuids: args.locationUuids ?? undefined,
32
+ deviceUuids: args.deviceUuids ?? undefined,
33
+ cardholderQuery: args.cardholderQuery ?? undefined,
34
+ badgeStatus: args.badgeStatus ?? undefined,
35
+ badgeType: args.badgeType ?? undefined,
36
+ anomalyOnly: args.anomalyOnly ?? undefined,
37
+ entryMade: args.entryMade ?? undefined,
38
+ afterMs: args.startTime ? new Date(args.startTime).getTime() : undefined,
39
+ beforeMs: args.endTime ? new Date(args.endTime).getTime() : undefined,
40
+ limit: args.limit ?? undefined,
41
+ }, args.timeZone ?? "UTC", requestModifiers, sessionId);
42
+ return createToolStructuredContent(result);
43
+ }
44
+ catch (error) {
45
+ const message = error instanceof Error ? error.message : "Unknown error";
46
+ return createToolStructuredContent({ error: message });
47
+ }
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,75 @@
1
+ import { z } from "zod";
2
+ import { createUuidSchema } from "../types.js";
3
+ import { ISOTimestampFormatDescription } from "../utils/timestampInput.js";
4
+ export const TOOL_ARGS = {
5
+ cardholderQuery: z
6
+ .string()
7
+ .describe('The cardholder (person) to reconstruct, full-text name match, e.g. "Eve" or "Eve Adams".'),
8
+ locationUuids: z
9
+ .array(createUuidSchema())
10
+ .nullable()
11
+ .describe("Optional: restrict to these Rhombus location UUIDs. Use the location-tool to resolve names."),
12
+ startTime: z
13
+ .string()
14
+ .datetime({ message: "Invalid datetime string. Expected ISO 8601 format.", offset: true })
15
+ .nullable()
16
+ .describe("Start of the window (inclusive). " + ISOTimestampFormatDescription),
17
+ endTime: z
18
+ .string()
19
+ .datetime({ message: "Invalid datetime string. Expected ISO 8601 format.", offset: true })
20
+ .nullable()
21
+ .describe("End of the window (inclusive). " + ISOTimestampFormatDescription),
22
+ clipPaddingSeconds: z
23
+ .number()
24
+ .nullable()
25
+ .describe("Seconds of video before/after each badge tap to include in the clip hint (default 15)."),
26
+ limit: z.number().nullable().describe("Maximum badge taps to include (default 200)."),
27
+ timeZone: z
28
+ .string()
29
+ .nullable()
30
+ .describe("IANA timezone used to format times, e.g. America/New_York. Defaults to UTC."),
31
+ };
32
+ const TOOL_ARGS_SCHEMA = z.object(TOOL_ARGS);
33
+ const ClipHintSchema = z
34
+ .object({
35
+ deviceUuid: z.string(),
36
+ startTimeMs: z.number(),
37
+ endTimeMs: z.number(),
38
+ })
39
+ .describe("Pass to clips-tool createClip to get video of this tap.");
40
+ const StillHintSchema = z
41
+ .object({
42
+ deviceUuid: z.string(),
43
+ timestampMs: z.number(),
44
+ })
45
+ .describe("Pass to camera-tool (requestType image) to get a still of this tap.");
46
+ export const BadgeTimelineStopSchema = z.object({
47
+ timestampMs: z.number().optional(),
48
+ datetime: z.string().optional().describe("Human-readable tap time in the requested timezone."),
49
+ deviceUuid: z
50
+ .string()
51
+ .optional()
52
+ .describe("The camera at this door. Pass to camera-tool (image) or clips-tool (createClip)."),
53
+ area: z.string().optional().describe("The area entered (or exited) at this tap."),
54
+ label: z.string().optional(),
55
+ isAnomaly: z.boolean().optional().describe("True if this tap was an alerting/anomalous event."),
56
+ clipHint: ClipHintSchema.optional(),
57
+ stillHint: StillHintSchema.optional(),
58
+ gapToNextSeconds: z
59
+ .number()
60
+ .optional()
61
+ .describe("Seconds until the next tap — large gaps are unobserved movement between doors."),
62
+ });
63
+ export const OUTPUT_SCHEMA = z.object({
64
+ cardholderName: z.string().optional().describe("The resolved cardholder name."),
65
+ ambiguousCardholders: z
66
+ .array(z.string())
67
+ .optional()
68
+ .describe("Set when the query matched more than one person — disambiguate with the user before trusting the timeline."),
69
+ stops: z
70
+ .array(BadgeTimelineStopSchema)
71
+ .optional()
72
+ .describe("The cardholder's badge taps in chronological order (oldest first)."),
73
+ path: z.array(z.string()).optional().describe("Areas traversed, in order (consecutive repeats collapsed)."),
74
+ error: z.string().optional(),
75
+ });
@@ -0,0 +1,70 @@
1
+ import { z } from "zod";
2
+ import { createUuidSchema } from "../types.js";
3
+ import { ISOTimestampFormatDescription } from "../utils/timestampInput.js";
4
+ export const TOOL_ARGS = {
5
+ area: z
6
+ .string()
7
+ .nullable()
8
+ .describe('Filter to events whose entered area matches this, e.g. "back office". Full-text match.'),
9
+ locationUuids: z
10
+ .array(createUuidSchema())
11
+ .nullable()
12
+ .describe("Filter to these Rhombus location UUIDs. Use the location-tool to resolve names to UUIDs."),
13
+ deviceUuids: z
14
+ .array(createUuidSchema())
15
+ .nullable()
16
+ .describe("Filter to these camera UUIDs (the camera that saw the badge event)."),
17
+ cardholderQuery: z
18
+ .string()
19
+ .nullable()
20
+ .describe("Match the cardholder's name (full-text), e.g. a person you are looking for."),
21
+ badgeStatus: z.string().nullable().describe("Filter by badge status, e.g. Active or Lost."),
22
+ badgeType: z.string().nullable().describe("Filter by badge type."),
23
+ anomalyOnly: z
24
+ .boolean()
25
+ .nullable()
26
+ .describe("Only return anomaly events: an inactive/lost badge was used, or access was granted but no entry was made (possible tailgating)."),
27
+ entryMade: z
28
+ .boolean()
29
+ .nullable()
30
+ .describe("Filter by whether access was granted AND entry was actually made."),
31
+ startTime: z
32
+ .string()
33
+ .datetime({ message: "Invalid datetime string. Expected ISO 8601 format.", offset: true })
34
+ .nullable()
35
+ .describe("Only events at or after this time (inclusive). " + ISOTimestampFormatDescription),
36
+ endTime: z
37
+ .string()
38
+ .datetime({ message: "Invalid datetime string. Expected ISO 8601 format.", offset: true })
39
+ .nullable()
40
+ .describe("Only events at or before this time (inclusive). " + ISOTimestampFormatDescription),
41
+ limit: z
42
+ .number()
43
+ .nullable()
44
+ .describe("Maximum number of events to return (default 200; the server caps at 1000)."),
45
+ timeZone: z
46
+ .string()
47
+ .nullable()
48
+ .describe("IANA timezone used to format event times, e.g. America/New_York. Defaults to UTC."),
49
+ };
50
+ const TOOL_ARGS_SCHEMA = z.object(TOOL_ARGS);
51
+ export const OnGuardEventSchema = z.object({
52
+ timestampMs: z.number().optional(),
53
+ datetime: z.string().optional().describe("Human-readable event time in the requested timezone."),
54
+ deviceUuid: z
55
+ .string()
56
+ .optional()
57
+ .describe("The camera that saw this event. Pass to camera-tool (requestType image) or clips-tool (createClip) to get a still/video."),
58
+ label: z.string().optional().describe('Event label, e.g. "OnGuard: Badge Authorized" or an anomaly label.'),
59
+ cardholderName: z.string().optional().describe("The cardholder (person) name."),
60
+ badgeStatus: z.string().optional(),
61
+ badgeType: z.string().optional(),
62
+ areaEntering: z.string().optional(),
63
+ areaExiting: z.string().optional(),
64
+ entryMade: z.boolean().optional(),
65
+ isAnomaly: z.boolean().optional().describe("True if this is an alerting/anomalous event."),
66
+ });
67
+ export const OUTPUT_SCHEMA = z.object({
68
+ events: z.array(OnGuardEventSchema).optional(),
69
+ error: z.string().optional(),
70
+ });
@@ -5855,6 +5855,12 @@ const Camera_GetCustomFootageSeekpointsV2WSRequest = z.object({
5855
5855
  });
5856
5856
  const SeekpointType = z.string();
5857
5857
  const SeekpointIndexType = z.object({
5858
+ alert: z.boolean().optional(),
5859
+ areaEntering: z.string().optional(),
5860
+ areaExiting: z.string().optional(),
5861
+ badgeStatus: z.string().optional(),
5862
+ badgeType: z.string().optional(),
5863
+ entryMade: z.boolean().optional(),
5858
5864
  compositComponentUuid: z.string().optional(),
5859
5865
  customDescription: z.string().optional(),
5860
5866
  customDisplayName: z.string().optional(),
@@ -10818,6 +10824,25 @@ const Eventsearch_GetEventSeekpointsWSResponse = z.object({
10818
10824
  errorMsg: z.string().optional(),
10819
10825
  warningMsg: z.string().optional()
10820
10826
  });
10827
+ const Eventsearch_SearchOnGuardEventsWSRequest = z.object({
10828
+ afterMs: z.number().int().optional(),
10829
+ anomalyOnly: z.boolean().optional(),
10830
+ area: z.string().optional(),
10831
+ badgeStatus: z.string().optional(),
10832
+ badgeType: z.string().optional(),
10833
+ beforeMs: z.number().int().optional(),
10834
+ cardholderQuery: z.string().optional(),
10835
+ deviceUuids: z.array(z.string()).optional(),
10836
+ entryMade: z.boolean().optional(),
10837
+ limit: z.number().int().optional(),
10838
+ locationUuids: z.array(z.string()).optional()
10839
+ });
10840
+ const Eventsearch_SearchOnGuardEventsWSResponse = z.object({
10841
+ error: z.boolean().optional(),
10842
+ errorMsg: z.string().optional(),
10843
+ events: z.array(SeekpointIndexType).optional(),
10844
+ warningMsg: z.string().optional()
10845
+ });
10821
10846
  const Export_ExportAuditEventsWSRequest = z.object({
10822
10847
  endInterval: z.number().int().optional(),
10823
10848
  excludeActions: z.array(z.string()).optional(),
@@ -21985,6 +22010,8 @@ export const schemas = {
21985
22010
  Eventsearch_VideoFootageWSRequest,
21986
22011
  Eventsearch_GetEventSeekpointsWSRequest,
21987
22012
  Eventsearch_GetEventSeekpointsWSResponse,
22013
+ Eventsearch_SearchOnGuardEventsWSRequest,
22014
+ Eventsearch_SearchOnGuardEventsWSResponse,
21988
22015
  Export_ExportAuditEventsWSRequest,
21989
22016
  Export_ExportClimateEventsWSRequest,
21990
22017
  Export_ExportCountReportsWSRequest,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rhombus-node-mcp",
3
- "version": "0.1.43",
3
+ "version": "0.1.45",
4
4
  "description": "MCP server for Rhombus API",
5
5
  "keywords": [
6
6
  "ai",