rhombus-node-mcp 0.1.54 → 0.1.56

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.
Files changed (48) hide show
  1. package/dist/api/person-tracking-tool-api.js +128 -143
  2. package/dist/api/reid-tool-api.js +62 -0
  3. package/dist/tools/entity-lookup-tool.js +2 -0
  4. package/dist/tools/get-entity-tool.js +6 -1
  5. package/dist/tools/get-org-information-tool.js +6 -1
  6. package/dist/tools/time-conversion-tool.js +6 -1
  7. package/dist/tools/time-tool.js +6 -1
  8. package/dist/tools/user-tool.js +2 -0
  9. package/dist/tools-console/access-anomaly-tool.js +2 -0
  10. package/dist/tools-console/access-control-tool.js +2 -0
  11. package/dist/tools-console/alarm-monitoring-tool.js +2 -0
  12. package/dist/tools-console/analytics-tool.js +2 -0
  13. package/dist/tools-console/automated-prompts-tool.js +2 -0
  14. package/dist/tools-console/badge-timeline-tool.js +2 -0
  15. package/dist/tools-console/camera-tool.js +2 -0
  16. package/dist/tools-console/camera-uptime-tool.js +2 -0
  17. package/dist/tools-console/clips-tool.js +2 -0
  18. package/dist/tools-console/count-tool.js +11 -6
  19. package/dist/tools-console/create-camera-policy-tool.js +2 -0
  20. package/dist/tools-console/door-schedule-exception-tool.js +2 -0
  21. package/dist/tools-console/door-tool.js +2 -0
  22. package/dist/tools-console/elements-access-anomaly-tool.js +2 -0
  23. package/dist/tools-console/elements-badge-timeline-tool.js +2 -0
  24. package/dist/tools-console/elements-lost-badge-tool.js +2 -0
  25. package/dist/tools-console/elements-tool.js +2 -0
  26. package/dist/tools-console/events-tool.js +2 -0
  27. package/dist/tools-console/faces-tool.js +2 -0
  28. package/dist/tools-console/guest-management-tool.js +2 -0
  29. package/dist/tools-console/location-tool.js +6 -1
  30. package/dist/tools-console/lost-badge-tool.js +2 -0
  31. package/dist/tools-console/lpr-tool.js +2 -0
  32. package/dist/tools-console/netbox-access-anomaly-tool.js +2 -0
  33. package/dist/tools-console/netbox-badge-timeline-tool.js +2 -0
  34. package/dist/tools-console/netbox-lost-badge-tool.js +2 -0
  35. package/dist/tools-console/netbox-tool.js +2 -0
  36. package/dist/tools-console/onguard-tool.js +2 -0
  37. package/dist/tools-console/person-tracking-tool.js +20 -22
  38. package/dist/tools-console/policy-alerts-tool.js +2 -0
  39. package/dist/tools-console/reboot-cameras-tool.js +6 -1
  40. package/dist/tools-console/report-tool.js +2 -0
  41. package/dist/tools-console/rules-tool.js +2 -0
  42. package/dist/tools-console/search-tool.js +2 -0
  43. package/dist/tools-console/update-tool.js +2 -0
  44. package/dist/tools-console/user-access-trail-tool.js +2 -0
  45. package/dist/tools-console/user-audit-tool.js +2 -0
  46. package/dist/tools-console/video-walls-tool.js +2 -0
  47. package/dist/types/person-tracking-tool-types.js +36 -47
  48. package/package.json +1 -1
@@ -1,134 +1,139 @@
1
1
  import { buildMediaHints } from "./badge-correlation.js";
2
- import { getFaceEvents, getRegisteredFaces, searchSimilarFaces } from "./faces-tool-api.js";
2
+ import { searchElementsEvents } from "./elements-tool-api.js";
3
+ import { getCameraList } from "./get-entity-tool-api.js";
4
+ import { searchNetboxEvents } from "./netbox-tool-api.js";
5
+ import { searchOnGuardEvents } from "./onguard-tool-api.js";
6
+ import { listReidentificationEmbeddings, searchReidentificationMatchesByEmbedding } from "./reid-tool-api.js";
3
7
  import { formatTimestamp } from "../util.js";
4
- /**
5
- * Fuzzy-match a free-text name against the registered-faces directory. Mirrors faces-tool's scoring
6
- * (4 = exact full name, 3 = first name, 2 = last name, 1 = substring ≥3 chars) but returns every person
7
- * tied at the best score so the caller can flag ambiguity instead of silently tracking the wrong person.
8
- */
9
- function matchRegisteredPeople(query, people) {
10
- const input = query.toLowerCase().trim();
11
- if (!input)
12
- return [];
13
- let bestScore = 0;
14
- const scored = [];
15
- for (const person of people) {
16
- if (!person.name || !person.uuid)
17
- continue;
18
- const fullName = person.name.toLowerCase().trim();
19
- const parts = fullName.split(/\s+/);
20
- let score = 0;
21
- if (fullName === input)
22
- score = 4;
23
- else if (parts[0] === input)
24
- score = 3;
25
- else if (parts.length > 1 && parts[parts.length - 1] === input)
26
- score = 2;
27
- else if (input.length >= 3 && fullName.includes(input))
28
- score = 1;
29
- if (score > 0) {
30
- scored.push({ name: person.name, uuid: person.uuid, score });
31
- if (score > bestScore)
32
- bestScore = score;
33
- }
34
- }
35
- // Dedupe by uuid among the best-scoring tier.
36
- const seen = new Set();
37
- const top = [];
38
- for (const s of scored) {
39
- if (s.score !== bestScore || seen.has(s.uuid))
40
- continue;
41
- seen.add(s.uuid);
42
- top.push({ name: s.name, personUuid: s.uuid });
43
- }
44
- return top;
8
+ const DEFAULT_BADGE_MATCH_WINDOW_S = 30;
9
+ const DEFAULT_TRACK_FORWARD_MS = 6 * 60 * 60 * 1000; // track 6h forward from the badge tap if no endTime
10
+ /** RUUID without any `.vN` facet suffix, so a badge event's camera matches the camera-state list. */
11
+ function stripFacet(uuid) {
12
+ return (uuid ?? "").split(".")[0];
45
13
  }
46
14
  /**
47
- * Reconstructs one person's movements across cameras as a chronological track of face sightings, each
48
- * with the still/clip hints needed to show the moment. Pure orchestration over the face-recognition
49
- * APIs:
50
- * - faceEventUuid -> searchSimilarFaces (appearance match; works for unrecognized people)
51
- * - personUuid -> getFaceEvents filtered to that person
52
- * - personQuery -> resolve name against registered faces, then getFaceEvents
15
+ * Reconstructs where a named person went, grounded in access control + person re-identification:
16
+ * 1. find the person's badge tap(s) (OnGuard / Elements / NetBox) a camera + time we KNOW is them,
17
+ * 2. pull the re-id embedding recorded on that camera nearest the badge time (the person at the door),
18
+ * 3. re-id-search that embedding across cameras over the window → their cross-camera movement track.
19
+ *
20
+ * Identity comes from the badge (not face recognition); the track is appearance/re-id based.
53
21
  */
54
22
  export async function getPersonTrack(args, timeZone, requestModifiers, sessionId) {
55
- let resolvedPerson;
56
- let raw = [];
57
- if (args.faceEventUuid) {
58
- // Appearance-based track from a seed sighting — handles people who aren't registered/named.
59
- const similar = await searchSimilarFaces(args.faceEventUuid, timeZone, requestModifiers, sessionId);
60
- raw = similar.map((s) => ({
61
- deviceUuid: s.deviceUuid,
62
- timestampMs: s.eventTimestampMs,
63
- datetime: s.eventTimestamp,
64
- similarity: s.similarity,
65
- faceName: undefined,
66
- }));
23
+ // 1. Anchor on access-control events. Check all three badge integrations; an org may use any.
24
+ const badgeArgs = {
25
+ cardholderQuery: args.personQuery,
26
+ locationUuids: args.locationUuids,
27
+ afterMs: args.afterMs,
28
+ beforeMs: args.beforeMs,
29
+ limit: args.limit ?? 200,
30
+ };
31
+ const empty = { events: [] };
32
+ const [og, el, nb] = await Promise.all([
33
+ searchOnGuardEvents(badgeArgs, timeZone, requestModifiers, sessionId).catch(() => empty),
34
+ searchElementsEvents(badgeArgs, timeZone, requestModifiers, sessionId).catch(() => empty),
35
+ searchNetboxEvents(badgeArgs, timeZone, requestModifiers, sessionId).catch(() => empty),
36
+ ]);
37
+ const badgeEvents = [
38
+ ...og.events.map((e) => ({ ...e, integration: "OnGuard" })),
39
+ ...el.events.map((e) => ({ ...e, integration: "Elements" })),
40
+ ...nb.events.map((e) => ({ ...e, integration: "NetBox" })),
41
+ ]
42
+ .filter((e) => e.deviceUuid && e.timestampMs != null)
43
+ .sort((a, b) => (a.timestampMs ?? 0) - (b.timestampMs ?? 0));
44
+ if (badgeEvents.length === 0) {
45
+ return {
46
+ sightings: [],
47
+ path: [],
48
+ count: 0,
49
+ note: `No access-control (badge) events found for "${args.personQuery}" in the window — can't anchor a re-id track. Try a wider time range, or confirm the name as it appears on the badge.`,
50
+ };
51
+ }
52
+ // Earliest badge tap in the window — track forward from there.
53
+ const anchor = badgeEvents[0];
54
+ const resolvedPerson = anchor.cardholderName ? { name: anchor.cardholderName } : undefined;
55
+ const anchorOut = {
56
+ deviceUuid: anchor.deviceUuid,
57
+ timestampMs: anchor.timestampMs,
58
+ datetime: anchor.datetime,
59
+ integration: anchor.integration,
60
+ area: anchor.areaEntering ?? anchor.areaExiting,
61
+ };
62
+ // 2. Resolve the badge camera's location (re-id list is scoped by location).
63
+ let anchorLocationUuid;
64
+ try {
65
+ const { cameras } = await getCameraList(requestModifiers, sessionId);
66
+ const dev = stripFacet(anchor.deviceUuid);
67
+ anchorLocationUuid = cameras.find((c) => stripFacet(c.uuid) === dev)?.locationUuid;
68
+ }
69
+ catch {
70
+ // best-effort; the re-id list can still run device-scoped without a location
71
+ }
72
+ // 3. Ground the re-id embedding: the person detected on that camera nearest the badge tap.
73
+ const winMs = (args.badgeMatchWindowSeconds ?? DEFAULT_BADGE_MATCH_WINDOW_S) * 1000;
74
+ const anchorMs = anchor.timestampMs;
75
+ const embeddings = await listReidentificationEmbeddings({
76
+ deviceUuids: [anchor.deviceUuid],
77
+ locationUuid: anchorLocationUuid,
78
+ startTimestampMs: anchorMs - winMs,
79
+ endTimestampMs: anchorMs + winMs,
80
+ limit: 100,
81
+ }, requestModifiers, sessionId);
82
+ if (embeddings.length === 0) {
83
+ return {
84
+ resolvedPerson,
85
+ anchor: anchorOut,
86
+ sightings: [],
87
+ path: [],
88
+ count: 0,
89
+ note: `Found ${anchor.cardholderName ?? "the person"}'s badge tap at ${anchor.datetime}, but no person re-identification embedding was recorded on that camera within ±${args.badgeMatchWindowSeconds ?? DEFAULT_BADGE_MATCH_WINDOW_S}s — can't build a re-id track. (Re-id needs human-detection coverage on the door camera.)`,
90
+ };
67
91
  }
68
- else {
69
- // Resolve to a person UUID (directly or by name) before pulling their face events.
70
- let personUuid = args.personUuid;
71
- if (!personUuid && args.personQuery) {
72
- const peopleResponse = await getRegisteredFaces({}, requestModifiers, sessionId);
73
- const matches = matchRegisteredPeople(args.personQuery, peopleResponse.people ?? []);
74
- if (matches.length > 1) {
75
- return { ambiguousPeople: matches, sightings: [], path: [], count: 0 };
76
- }
77
- if (matches.length === 0) {
78
- return { sightings: [], path: [], count: 0 };
79
- }
80
- resolvedPerson = matches[0];
81
- personUuid = matches[0].personUuid;
82
- }
83
- else if (personUuid) {
84
- resolvedPerson = { personUuid };
85
- }
86
- if (!personUuid) {
87
- return { sightings: [], path: [], count: 0 };
88
- }
89
- const faceEventArgs = {
90
- pageRequest: { maxPageSize: args.limit ?? 200, lastEvaluatedKey: null },
91
- searchFilter: {
92
- faceNameContains: null,
93
- faceNames: [],
94
- hasEmbedding: null,
95
- hasName: null,
96
- labels: [],
97
- locationUuids: args.locationUuids ?? [],
98
- personUuids: [personUuid],
99
- timestampFilter: args.afterMs != null || args.beforeMs != null
100
- ? {
101
- rangeStart: args.afterMs != null ? new Date(args.afterMs).toISOString() : null,
102
- rangeEnd: args.beforeMs != null ? new Date(args.beforeMs).toISOString() : null,
103
- }
104
- : null,
105
- },
92
+ // The detection closest in time to the badge tap is the best proxy for the badge holder.
93
+ const seed = embeddings.reduce((best, e) => Math.abs((e.timestamp ?? 0) - anchorMs) < Math.abs((best.timestamp ?? 0) - anchorMs) ? e : best);
94
+ if (!seed.embedding || seed.embedding.length === 0) {
95
+ return {
96
+ resolvedPerson,
97
+ anchor: anchorOut,
98
+ sightings: [],
99
+ path: [],
100
+ count: 0,
101
+ note: "The re-id detection at the door had no embedding vector; can't search for matches.",
106
102
  };
107
- const { faceEvents } = await getFaceEvents(faceEventArgs, timeZone, requestModifiers, sessionId);
108
- raw = faceEvents.map((e) => ({
109
- deviceUuid: e.deviceUuid,
110
- timestampMs: e.eventTimestampMs,
111
- datetime: e.eventTimestamp,
112
- locationUuid: e.locationUuid,
113
- faceName: e.faceName,
114
- thumbnailS3Key: e.thumbnailS3Key,
115
- }));
116
- if (!resolvedPerson?.name) {
117
- const named = raw.find((r) => r.faceName);
118
- if (named?.faceName)
119
- resolvedPerson = { name: named.faceName, personUuid };
120
- }
121
103
  }
122
- // Always enforce the time window client-side (searchSimilarFaces has no time filter, and the face-event
123
- // time filter defaults to the last 7 days if unset) and order chronologically.
124
- const ordered = raw
125
- .filter((r) => r.timestampMs != null)
126
- .filter((r) => (args.afterMs == null || r.timestampMs >= args.afterMs))
127
- .filter((r) => (args.beforeMs == null || r.timestampMs <= args.beforeMs))
128
- .sort((a, b) => (a.timestampMs ?? 0) - (b.timestampMs ?? 0))
129
- .slice(0, args.limit ?? 200);
130
- const sightings = buildSightings(ordered, timeZone, args.clipPaddingSeconds);
131
- // Camera sequence the person moved through, collapsing consecutive repeats.
104
+ // 4. Re-id search that appearance across cameras over the window.
105
+ const searchStart = args.afterMs ?? anchorMs;
106
+ const searchEnd = args.beforeMs ?? anchorMs + DEFAULT_TRACK_FORWARD_MS;
107
+ const matches = await searchReidentificationMatchesByEmbedding({
108
+ searchEmbedding: seed.embedding,
109
+ locationUuid: args.locationUuids?.[0],
110
+ startTimestampMs: searchStart,
111
+ endTimestampMs: searchEnd,
112
+ limit: args.limit ?? 200,
113
+ }, requestModifiers, sessionId);
114
+ // 5. Build the chronological track with media hints.
115
+ const ordered = matches
116
+ .filter((m) => m.timestamp != null)
117
+ .sort((a, b) => (a.timestamp ?? 0) - (b.timestamp ?? 0));
118
+ const sightings = ordered.map((m, i) => {
119
+ const next = ordered[i + 1];
120
+ const gapToNextSeconds = next?.timestamp != null && m.timestamp != null
121
+ ? Math.round((next.timestamp - m.timestamp) / 1000)
122
+ : undefined;
123
+ const { clipHint, stillHint } = buildMediaHints({ deviceUuid: m.deviceUuid, timestampMs: m.timestamp }, args.clipPaddingSeconds);
124
+ return {
125
+ timestampMs: m.timestamp,
126
+ datetime: m.timestamp != null ? formatTimestamp(m.timestamp, timeZone) : undefined,
127
+ deviceUuid: m.deviceUuid,
128
+ locationUuid: m.locationUuid,
129
+ distance: m.distance,
130
+ stableTrackId: m.stableTrackId,
131
+ thumbnailUri: m.thumbnailUri,
132
+ clipHint,
133
+ stillHint,
134
+ gapToNextSeconds,
135
+ };
136
+ });
132
137
  const path = [];
133
138
  for (const s of sightings) {
134
139
  if (s.deviceUuid && s.deviceUuid !== path[path.length - 1])
@@ -136,30 +141,10 @@ export async function getPersonTrack(args, timeZone, requestModifiers, sessionId
136
141
  }
137
142
  return {
138
143
  resolvedPerson,
144
+ anchor: anchorOut,
139
145
  sightings,
140
146
  path,
141
147
  lastKnownSighting: sightings[sightings.length - 1],
142
148
  count: sightings.length,
143
149
  };
144
150
  }
145
- function buildSightings(ordered, timeZone, clipPaddingSeconds) {
146
- return ordered.map((s, i) => {
147
- const next = ordered[i + 1];
148
- const gapToNextSeconds = next?.timestampMs != null && s.timestampMs != null
149
- ? Math.round((next.timestampMs - s.timestampMs) / 1000)
150
- : undefined;
151
- const { clipHint, stillHint } = buildMediaHints({ deviceUuid: s.deviceUuid, timestampMs: s.timestampMs }, clipPaddingSeconds);
152
- return {
153
- timestampMs: s.timestampMs,
154
- datetime: s.datetime ?? (s.timestampMs != null ? formatTimestamp(s.timestampMs, timeZone) : undefined),
155
- deviceUuid: s.deviceUuid,
156
- locationUuid: s.locationUuid,
157
- faceName: s.faceName,
158
- similarity: s.similarity,
159
- thumbnailS3Key: s.thumbnailS3Key,
160
- clipHint,
161
- stillHint,
162
- gapToNextSeconds,
163
- };
164
- });
165
- }
@@ -0,0 +1,62 @@
1
+ import { postApi } from "../network/network.js";
2
+ function mapEmbedding(e) {
3
+ return {
4
+ deviceUuid: e.deviceUuid ?? undefined,
5
+ locationUuid: e.locationUuid ?? undefined,
6
+ timestamp: e.timestamp ?? undefined,
7
+ embedding: (e.embedding ?? []).filter((n) => n != null),
8
+ embeddingId: e.embeddingId ?? undefined,
9
+ stableTrackId: e.stableTrackId ?? undefined,
10
+ thumbnailUri: e.thumbnailUri ?? undefined,
11
+ };
12
+ }
13
+ /**
14
+ * Lists the person re-identification embeddings recorded on the given camera(s) in a time window —
15
+ * i.e. the people the camera's human-detection AI saw. Used to grab the embedding of the person standing
16
+ * at a door at a known moment (e.g. a badge tap), so it can be tracked across other cameras.
17
+ */
18
+ export async function listReidentificationEmbeddings(args, requestModifiers, sessionId) {
19
+ const body = {
20
+ deviceUuids: args.deviceUuids,
21
+ locationUuid: args.locationUuid,
22
+ startTimestampMs: args.startTimestampMs,
23
+ endTimestampMs: args.endTimestampMs,
24
+ limit: args.limit ?? 100,
25
+ };
26
+ const res = await postApi({
27
+ route: "/search/listReidentificationEmbeddings",
28
+ body,
29
+ modifiers: requestModifiers,
30
+ sessionId,
31
+ });
32
+ if (res.error)
33
+ throw new Error(res.errorMsg ?? "listReidentificationEmbeddings failed");
34
+ return (res.embeddings ?? []).map(mapEmbedding);
35
+ }
36
+ /**
37
+ * Searches person re-identification matches for a given appearance embedding across cameras and time —
38
+ * "find this same person elsewhere". Returns sightings ordered by similarity (distance asc; lower is a
39
+ * closer match).
40
+ */
41
+ export async function searchReidentificationMatchesByEmbedding(args, requestModifiers, sessionId) {
42
+ const body = {
43
+ searchEmbedding: args.searchEmbedding,
44
+ deviceUuids: args.deviceUuids,
45
+ locationUuid: args.locationUuid,
46
+ startTimestampMs: args.startTimestampMs,
47
+ endTimestampMs: args.endTimestampMs,
48
+ limit: args.limit ?? 100,
49
+ };
50
+ const res = await postApi({
51
+ route: "/search/searchReidentificationMatchesByEmbedding",
52
+ body,
53
+ modifiers: requestModifiers,
54
+ sessionId,
55
+ });
56
+ if (res.error)
57
+ throw new Error(res.errorMsg ?? "searchReidentificationMatchesByEmbedding failed");
58
+ return (res.matches ?? []).map((m) => ({
59
+ ...mapEmbedding(m.embedding ?? {}),
60
+ distance: m.distance ?? undefined,
61
+ }));
62
+ }
@@ -29,8 +29,10 @@ const TOOL_HANDLER = async (args, extra) => {
29
29
  };
30
30
  export function createTool(server) {
31
31
  server.registerTool(TOOL_NAME, {
32
+ title: "Entity Lookup",
32
33
  description: TOOL_DESCRIPTION,
33
34
  inputSchema: TOOL_ARGS,
34
35
  outputSchema: OUTPUT_SCHEMA.shape,
36
+ annotations: { readOnlyHint: true },
35
37
  }, TOOL_HANDLER);
36
38
  }
@@ -82,5 +82,10 @@ const TOOL_HANDLER = async (args, extra) => {
82
82
  return createToolTextContent(JSON.stringify(ret));
83
83
  };
84
84
  export function createTool(server) {
85
- server.tool(TOOL_NAME, TOOL_DESCRIPTION, TOOL_ARGS, TOOL_HANDLER);
85
+ server.registerTool(TOOL_NAME, {
86
+ title: "Get Entities",
87
+ description: TOOL_DESCRIPTION,
88
+ inputSchema: TOOL_ARGS,
89
+ annotations: { readOnlyHint: true },
90
+ }, TOOL_HANDLER);
86
91
  }
@@ -14,5 +14,10 @@ const TOOL_HANDLER = async (_, extra) => {
14
14
  };
15
15
  };
16
16
  export function createTool(server) {
17
- server.tool(TOOL_NAME, TOOL_DESCRIPTION, TOOL_ARGS, TOOL_HANDLER);
17
+ server.registerTool(TOOL_NAME, {
18
+ title: "Organization Information",
19
+ description: TOOL_DESCRIPTION,
20
+ inputSchema: TOOL_ARGS,
21
+ annotations: { readOnlyHint: true },
22
+ }, TOOL_HANDLER);
18
23
  }
@@ -39,5 +39,10 @@ const TOOL_HANDLER = async (args, extra) => {
39
39
  }
40
40
  };
41
41
  export function createTool(server) {
42
- server.tool(TOOL_NAME, TOOL_DESCRIPTION, TOOL_ARGS, TOOL_HANDLER);
42
+ server.registerTool(TOOL_NAME, {
43
+ title: "Time Conversion",
44
+ description: TOOL_DESCRIPTION,
45
+ inputSchema: TOOL_ARGS,
46
+ annotations: { readOnlyHint: true },
47
+ }, TOOL_HANDLER);
43
48
  }
@@ -19,5 +19,10 @@ const TOOL_HANDLER = async (args, extra) => {
19
19
  };
20
20
  };
21
21
  export function createTool(server) {
22
- server.tool(TOOL_NAME, TOOL_DESCRIPTION, TOOL_ARGS, TOOL_HANDLER);
22
+ server.registerTool(TOOL_NAME, {
23
+ title: "Current Time",
24
+ description: TOOL_DESCRIPTION,
25
+ inputSchema: TOOL_ARGS,
26
+ annotations: { readOnlyHint: true },
27
+ }, TOOL_HANDLER);
23
28
  }
@@ -55,8 +55,10 @@ const TOOL_HANDLER = async (args, _extra) => {
55
55
  };
56
56
  export function createTool(server) {
57
57
  server.registerTool(TOOL_NAME, {
58
+ title: "Users",
58
59
  description: TOOL_DESCRIPTION,
59
60
  inputSchema: TOOL_ARGS,
60
61
  outputSchema: OUTPUT_SCHEMA.shape,
62
+ annotations: { readOnlyHint: true },
61
63
  }, TOOL_HANDLER);
62
64
  }
@@ -46,8 +46,10 @@ const TOOL_HANDLER = async (args, _extra) => {
46
46
  };
47
47
  export function createTool(server) {
48
48
  server.registerTool(TOOL_NAME, {
49
+ title: "Access Anomaly Detection",
49
50
  description: TOOL_DESCRIPTION,
50
51
  inputSchema: TOOL_ARGS,
51
52
  outputSchema: OUTPUT_SCHEMA.shape,
53
+ annotations: { readOnlyHint: true },
52
54
  }, TOOL_HANDLER);
53
55
  }
@@ -90,8 +90,10 @@ const TOOL_HANDLER = async (args, _extra) => {
90
90
  };
91
91
  export function createTool(server) {
92
92
  server.registerTool(TOOL_NAME, {
93
+ title: "Access Control",
93
94
  description: TOOL_DESCRIPTION,
94
95
  inputSchema: TOOL_ARGS,
95
96
  outputSchema: OUTPUT_SCHEMA.shape,
97
+ annotations: { readOnlyHint: false, destructiveHint: true },
96
98
  }, TOOL_HANDLER);
97
99
  }
@@ -43,8 +43,10 @@ const TOOL_HANDLER = async (args, _extra) => {
43
43
  };
44
44
  export function createTool(server) {
45
45
  server.registerTool(TOOL_NAME, {
46
+ title: "Alarm Monitoring",
46
47
  description: TOOL_DESCRIPTION,
47
48
  inputSchema: TOOL_ARGS,
48
49
  outputSchema: OUTPUT_SCHEMA.shape,
50
+ annotations: { readOnlyHint: true },
49
51
  }, TOOL_HANDLER);
50
52
  }
@@ -376,8 +376,10 @@ const TOOL_HANDLER = async (args, _extra) => {
376
376
  };
377
377
  export function createTool(server) {
378
378
  server.registerTool(TOOL_NAME, {
379
+ title: "Analytics",
379
380
  description: TOOL_DESCRIPTION,
380
381
  inputSchema: TOOL_ARGS,
381
382
  outputSchema: OUTPUT_SCHEMA.shape,
383
+ annotations: { readOnlyHint: true },
382
384
  }, TOOL_HANDLER);
383
385
  }
@@ -138,8 +138,10 @@ const TOOL_HANDLER = async (args, _extra) => {
138
138
  };
139
139
  export function createTool(server) {
140
140
  server.registerTool(TOOL_NAME, {
141
+ title: "Automated Prompts",
141
142
  description: TOOL_DESCRIPTION,
142
143
  inputSchema: TOOL_ARGS,
143
144
  outputSchema: OUTPUT_SCHEMA.shape,
145
+ annotations: { readOnlyHint: false, destructiveHint: true },
144
146
  }, TOOL_HANDLER);
145
147
  }
@@ -43,8 +43,10 @@ const TOOL_HANDLER = async (args, _extra) => {
43
43
  };
44
44
  export function createTool(server) {
45
45
  server.registerTool(TOOL_NAME, {
46
+ title: "Badge Timeline",
46
47
  description: TOOL_DESCRIPTION,
47
48
  inputSchema: TOOL_ARGS,
48
49
  outputSchema: OUTPUT_SCHEMA.shape,
50
+ annotations: { readOnlyHint: true },
49
51
  }, TOOL_HANDLER);
50
52
  }
@@ -143,7 +143,9 @@ const TOOL_HANDLER = async (args, extra) => {
143
143
  };
144
144
  export function createTool(server) {
145
145
  server.registerTool(TOOL_NAME, {
146
+ title: "Cameras",
146
147
  description: TOOL_DESCRIPTION,
147
148
  inputSchema: TOOL_ARGS,
149
+ annotations: { readOnlyHint: true },
148
150
  }, TOOL_HANDLER);
149
151
  }
@@ -43,8 +43,10 @@ const TOOL_HANDLER = async (args, _extra) => {
43
43
  };
44
44
  export function createTool(server) {
45
45
  server.registerTool(TOOL_NAME, {
46
+ title: "Camera Uptime",
46
47
  description: TOOL_DESCRIPTION,
47
48
  inputSchema: TOOL_ARGS,
48
49
  outputSchema: OUTPUT_SCHEMA.shape,
50
+ annotations: { readOnlyHint: true },
49
51
  }, TOOL_HANDLER);
50
52
  }
@@ -72,8 +72,10 @@ const TOOL_HANDLER = async (args, extra) => {
72
72
  };
73
73
  export function createTool(server) {
74
74
  server.registerTool(TOOL_NAME, {
75
+ title: "Clips",
75
76
  description: TOOL_DESCRIPTION,
76
77
  inputSchema: TOOL_ARGS,
77
78
  outputSchema: OUTPUT_SCHEMA.shape,
79
+ annotations: { readOnlyHint: false, destructiveHint: true },
78
80
  }, TOOL_HANDLER);
79
81
  }
@@ -1,13 +1,18 @@
1
1
  import { z } from "zod";
2
2
  import { logger } from "../logger.js";
3
3
  export function createTool(server) {
4
- server.tool("count-tool", `
5
- This tool counts the number of items by accepting an array of UUIDs. It can count anything that has UUIDs - users, devices,
4
+ server.registerTool("count-tool", {
5
+ title: "Count Items",
6
+ description: `
7
+ This tool counts the number of items by accepting an array of UUIDs. It can count anything that has UUIDs - users, devices,
6
8
  records, or any other entities. Simply provide an array of UUID strings and it will return the precise count.
7
- `, {
8
- uuids: z
9
- .array(z.string().describe("UUID string of an individual item"))
10
- .describe("An array of UUID strings representing the items to count. Each string should be a valid UUID."),
9
+ `,
10
+ inputSchema: {
11
+ uuids: z
12
+ .array(z.string().describe("UUID string of an individual item"))
13
+ .describe("An array of UUID strings representing the items to count. Each string should be a valid UUID."),
14
+ },
15
+ annotations: { readOnlyHint: true },
11
16
  }, async ({ uuids }) => {
12
17
  try {
13
18
  logger.info("Counting UUIDs", uuids);
@@ -207,8 +207,10 @@ const TOOL_HANDLER = async (args, extra) => {
207
207
  };
208
208
  export function createTool(server) {
209
209
  server.registerTool(TOOL_NAME, {
210
+ title: "Create Camera Policy",
210
211
  description: TOOL_DESCRIPTION,
211
212
  inputSchema: TOOL_ARGS,
212
213
  outputSchema: OUTPUT_SCHEMA.shape,
214
+ annotations: { readOnlyHint: false, destructiveHint: false },
213
215
  }, TOOL_HANDLER);
214
216
  }
@@ -137,8 +137,10 @@ const TOOL_HANDLER = async (args, _extra) => {
137
137
  };
138
138
  export function createTool(server) {
139
139
  server.registerTool(TOOL_NAME, {
140
+ title: "Door Schedule Exceptions",
140
141
  description: TOOL_DESCRIPTION,
141
142
  inputSchema: TOOL_ARGS,
142
143
  outputSchema: OUTPUT_SCHEMA.shape,
144
+ annotations: { readOnlyHint: false, destructiveHint: true },
143
145
  }, TOOL_HANDLER);
144
146
  }
@@ -59,8 +59,10 @@ const TOOL_HANDLER = async (args, _extra) => {
59
59
  };
60
60
  export function createTool(server) {
61
61
  server.registerTool(TOOL_NAME, {
62
+ title: "Doors",
62
63
  description: TOOL_DESCRIPTION,
63
64
  inputSchema: TOOL_ARGS,
64
65
  outputSchema: OUTPUT_SCHEMA.shape,
66
+ annotations: { readOnlyHint: false, destructiveHint: false },
65
67
  }, TOOL_HANDLER);
66
68
  }
@@ -46,8 +46,10 @@ const TOOL_HANDLER = async (args, _extra) => {
46
46
  };
47
47
  export function createTool(server) {
48
48
  server.registerTool(TOOL_NAME, {
49
+ title: "Elements Access Anomaly Detection",
49
50
  description: TOOL_DESCRIPTION,
50
51
  inputSchema: TOOL_ARGS,
51
52
  outputSchema: OUTPUT_SCHEMA.shape,
53
+ annotations: { readOnlyHint: true },
52
54
  }, TOOL_HANDLER);
53
55
  }
@@ -43,8 +43,10 @@ const TOOL_HANDLER = async (args, _extra) => {
43
43
  };
44
44
  export function createTool(server) {
45
45
  server.registerTool(TOOL_NAME, {
46
+ title: "Elements Badge Timeline",
46
47
  description: TOOL_DESCRIPTION,
47
48
  inputSchema: TOOL_ARGS,
48
49
  outputSchema: OUTPUT_SCHEMA.shape,
50
+ annotations: { readOnlyHint: true },
49
51
  }, TOOL_HANDLER);
50
52
  }
@@ -40,8 +40,10 @@ const TOOL_HANDLER = async (args, _extra) => {
40
40
  };
41
41
  export function createTool(server) {
42
42
  server.registerTool(TOOL_NAME, {
43
+ title: "Elements Lost Badge",
43
44
  description: TOOL_DESCRIPTION,
44
45
  inputSchema: TOOL_ARGS,
45
46
  outputSchema: OUTPUT_SCHEMA.shape,
47
+ annotations: { readOnlyHint: true },
46
48
  }, TOOL_HANDLER);
47
49
  }
@@ -56,8 +56,10 @@ const TOOL_HANDLER = async (args, _extra) => {
56
56
  };
57
57
  export function createTool(server) {
58
58
  server.registerTool(TOOL_NAME, {
59
+ title: "Elements Events",
59
60
  description: TOOL_DESCRIPTION,
60
61
  inputSchema: TOOL_ARGS,
61
62
  outputSchema: OUTPUT_SCHEMA.shape,
63
+ annotations: { readOnlyHint: true },
62
64
  }, TOOL_HANDLER);
63
65
  }
@@ -240,8 +240,10 @@ const TOOL_HANDLER = async (args, extra) => {
240
240
  };
241
241
  export function createTool(server) {
242
242
  server.registerTool(TOOL_NAME, {
243
+ title: "Events",
243
244
  description: TOOL_DESCRIPTION,
244
245
  inputSchema: TOOL_ARGS,
245
246
  outputSchema: OUTPUT_SCHEMA.shape,
247
+ annotations: { readOnlyHint: true },
246
248
  }, TOOL_HANDLER);
247
249
  }
@@ -202,8 +202,10 @@ const TOOL_HANDLER = async (args, extra) => {
202
202
  };
203
203
  export function createTool(server) {
204
204
  server.registerTool(TOOL_NAME, {
205
+ title: "Faces",
205
206
  description: TOOL_DESCRIPTION,
206
207
  inputSchema: TOOL_ARGS,
207
208
  outputSchema: OUTPUT_SCHEMA.shape,
209
+ annotations: { readOnlyHint: true },
208
210
  }, TOOL_HANDLER);
209
211
  }
@@ -43,8 +43,10 @@ const TOOL_HANDLER = async (args, _extra) => {
43
43
  };
44
44
  export function createTool(server) {
45
45
  server.registerTool(TOOL_NAME, {
46
+ title: "Guest Management",
46
47
  description: TOOL_DESCRIPTION,
47
48
  inputSchema: TOOL_ARGS,
48
49
  outputSchema: OUTPUT_SCHEMA.shape,
50
+ annotations: { readOnlyHint: true },
49
51
  }, TOOL_HANDLER);
50
52
  }
@@ -41,5 +41,10 @@ const TOOL_HANDLER = async (args, extra) => {
41
41
  };
42
42
  };
43
43
  export function createTool(server) {
44
- server.tool(TOOL_NAME, TOOL_DESCRIPTION, TOOL_ARGS, TOOL_HANDLER);
44
+ server.registerTool(TOOL_NAME, {
45
+ title: "Locations",
46
+ description: TOOL_DESCRIPTION,
47
+ inputSchema: TOOL_ARGS,
48
+ annotations: { readOnlyHint: false, destructiveHint: false },
49
+ }, TOOL_HANDLER);
45
50
  }
@@ -40,8 +40,10 @@ const TOOL_HANDLER = async (args, _extra) => {
40
40
  };
41
41
  export function createTool(server) {
42
42
  server.registerTool(TOOL_NAME, {
43
+ title: "Lost Badge",
43
44
  description: TOOL_DESCRIPTION,
44
45
  inputSchema: TOOL_ARGS,
45
46
  outputSchema: OUTPUT_SCHEMA.shape,
47
+ annotations: { readOnlyHint: true },
46
48
  }, TOOL_HANDLER);
47
49
  }
@@ -80,8 +80,10 @@ const TOOL_HANDLER = async (args, _extra) => {
80
80
  };
81
81
  export function createTool(server) {
82
82
  server.registerTool(TOOL_NAME, {
83
+ title: "License Plate Recognition",
83
84
  description: TOOL_DESCRIPTION,
84
85
  inputSchema: TOOL_ARGS,
85
86
  outputSchema: OUTPUT_SCHEMA.shape,
87
+ annotations: { readOnlyHint: false, destructiveHint: false },
86
88
  }, TOOL_HANDLER);
87
89
  }
@@ -46,8 +46,10 @@ const TOOL_HANDLER = async (args, _extra) => {
46
46
  };
47
47
  export function createTool(server) {
48
48
  server.registerTool(TOOL_NAME, {
49
+ title: "NetBox Access Anomaly Detection",
49
50
  description: TOOL_DESCRIPTION,
50
51
  inputSchema: TOOL_ARGS,
51
52
  outputSchema: OUTPUT_SCHEMA.shape,
53
+ annotations: { readOnlyHint: true },
52
54
  }, TOOL_HANDLER);
53
55
  }
@@ -43,8 +43,10 @@ const TOOL_HANDLER = async (args, _extra) => {
43
43
  };
44
44
  export function createTool(server) {
45
45
  server.registerTool(TOOL_NAME, {
46
+ title: "NetBox Badge Timeline",
46
47
  description: TOOL_DESCRIPTION,
47
48
  inputSchema: TOOL_ARGS,
48
49
  outputSchema: OUTPUT_SCHEMA.shape,
50
+ annotations: { readOnlyHint: true },
49
51
  }, TOOL_HANDLER);
50
52
  }
@@ -40,8 +40,10 @@ const TOOL_HANDLER = async (args, _extra) => {
40
40
  };
41
41
  export function createTool(server) {
42
42
  server.registerTool(TOOL_NAME, {
43
+ title: "NetBox Lost Badge",
43
44
  description: TOOL_DESCRIPTION,
44
45
  inputSchema: TOOL_ARGS,
45
46
  outputSchema: OUTPUT_SCHEMA.shape,
47
+ annotations: { readOnlyHint: true },
46
48
  }, TOOL_HANDLER);
47
49
  }
@@ -56,8 +56,10 @@ const TOOL_HANDLER = async (args, _extra) => {
56
56
  };
57
57
  export function createTool(server) {
58
58
  server.registerTool(TOOL_NAME, {
59
+ title: "NetBox Events",
59
60
  description: TOOL_DESCRIPTION,
60
61
  inputSchema: TOOL_ARGS,
61
62
  outputSchema: OUTPUT_SCHEMA.shape,
63
+ annotations: { readOnlyHint: true },
62
64
  }, TOOL_HANDLER);
63
65
  }
@@ -56,8 +56,10 @@ const TOOL_HANDLER = async (args, _extra) => {
56
56
  };
57
57
  export function createTool(server) {
58
58
  server.registerTool(TOOL_NAME, {
59
+ title: "OnGuard Events",
59
60
  description: TOOL_DESCRIPTION,
60
61
  inputSchema: TOOL_ARGS,
61
62
  outputSchema: OUTPUT_SCHEMA.shape,
63
+ annotations: { readOnlyHint: true },
62
64
  }, TOOL_HANDLER);
63
65
  }
@@ -3,28 +3,25 @@ import { OUTPUT_SCHEMA, TOOL_ARGS } from "../types/person-tracking-tool-types.js
3
3
  import { createToolStructuredContent, extractFromToolExtra } from "../util.js";
4
4
  const TOOL_NAME = "person-tracking-tool";
5
5
  const TOOL_DESCRIPTION = `
6
- Tracks one person's movements across cameras using face recognition. Use this for "track this person",
7
- "where did X go", "follow this person across the building", or "where was X last seen" the camera-based
8
- counterpart to the badge-timeline tools (which follow door taps).
6
+ Reconstructs where a named person went across cameras, e.g. "show me where Brandon Salzberg went" or
7
+ "track Eve through the building yesterday". Identity is grounded in ACCESS CONTROL and the track uses
8
+ person RE-IDENTIFICATION (appearance), NOT face recognition:
9
9
 
10
- Identify the person ONE of three ways (in priority order):
11
- - faceEventUuid: track by APPEARANCE from a specific sighting works even for an unrecognized/unnamed
12
- person (e.g. follow the person in this face event). Get a faceEventUuid from the faces-tool.
13
- - personUuid: the exact registered-person UUID (from faces-tool get-registered-faces).
14
- - personQuery: a full-text name (e.g. "Eve" or "Eve Adams"); it is resolved against the registered-faces
15
- directory. If "ambiguousPeople" is returned the name matched more than one person — ask the user which
16
- one before trusting the track.
10
+ 1. Finds the person's badge tap(s) (OnGuard / Elements / NetBox) in the window — a camera + time we KNOW
11
+ is them. (Pass the name as it appears on the badge.)
12
+ 2. Pulls the person re-id embedding recorded on that door camera nearest the badge tap (the person at the
13
+ door).
14
+ 3. Re-id-searches that appearance across all cameras over the window to reconstruct their movement.
17
15
 
18
- Returns the person's sightings in CHRONOLOGICAL order (oldest first), each with:
19
- - datetime / timestampMs, deviceUuid (the camera), and locationUuid
20
- - similarity (only on appearance/faceEventUuid tracks) and a face thumbnail key
21
- - clipHint (camera + start/end window) and stillHint (camera + timestamp)
22
- - gapToNextSeconds: time until the next sighting (a large gap = unobserved movement between cameras)
23
- plus a "path" array (the camera sequence, consecutive repeats collapsed) and "lastKnownSighting" (the
24
- person's last-known location).
16
+ Returns:
17
+ - resolvedPerson and "anchor" (the badge tap that grounded the track: door camera, time, integration).
18
+ - sightings: chronological re-id hits, each with deviceUuid (camera), timestampMs/datetime, distance
19
+ (LOWER = closer appearance match), a thumbnail, clipHint/stillHint, and gapToNextSeconds.
20
+ - path (camera sequence) and lastKnownSighting (last-known location).
21
+ - note: set when no badge tap was found, or no re-id embedding existed on the door camera.
25
22
 
26
23
  Resolve relative times like "yesterday" to ISO 8601 first (use the timestamp tool), then pass
27
- startTime/endTime. Face tracking depends on face-recognition coverage, so treat the track as investigative.
24
+ startTime/endTime. Re-id depends on human-detection coverage, so treat the track as investigative, not proof.
28
25
 
29
26
  IMPORTANT — to show the movement visually: for each sighting (or the key transitions), call the camera-tool
30
27
  (requestType "image", cameraUuid = sighting.deviceUuid, timestamp = sighting.timestampMs) for a still, and/or
@@ -35,12 +32,11 @@ const TOOL_HANDLER = async (args, _extra) => {
35
32
  const { requestModifiers, sessionId } = extractFromToolExtra(_extra);
36
33
  try {
37
34
  const result = await getPersonTrack({
38
- personQuery: args.personQuery ?? undefined,
39
- personUuid: args.personUuid ?? undefined,
40
- faceEventUuid: args.faceEventUuid ?? undefined,
41
- locationUuids: args.locationUuids ?? undefined,
35
+ personQuery: args.personQuery,
42
36
  afterMs: args.startTime ? new Date(args.startTime).getTime() : undefined,
43
37
  beforeMs: args.endTime ? new Date(args.endTime).getTime() : undefined,
38
+ locationUuids: args.locationUuids ?? undefined,
39
+ badgeMatchWindowSeconds: args.badgeMatchWindowSeconds ?? undefined,
44
40
  clipPaddingSeconds: args.clipPaddingSeconds ?? undefined,
45
41
  limit: args.limit ?? undefined,
46
42
  }, args.timeZone ?? "UTC", requestModifiers, sessionId);
@@ -53,8 +49,10 @@ const TOOL_HANDLER = async (args, _extra) => {
53
49
  };
54
50
  export function createTool(server) {
55
51
  server.registerTool(TOOL_NAME, {
52
+ title: "Person Tracking",
56
53
  description: TOOL_DESCRIPTION,
57
54
  inputSchema: TOOL_ARGS,
58
55
  outputSchema: OUTPUT_SCHEMA.shape,
56
+ annotations: { readOnlyHint: true },
59
57
  }, TOOL_HANDLER);
60
58
  }
@@ -71,8 +71,10 @@ const TOOL_HANDLER = async (args, extra) => {
71
71
  };
72
72
  export function createTool(server) {
73
73
  server.registerTool(TOOL_NAME, {
74
+ title: "Policy Alerts",
74
75
  description: TOOL_DESCRIPTION,
75
76
  inputSchema: TOOL_ARGS,
76
77
  outputSchema: OUTPUT_SCHEMA.shape,
78
+ annotations: { readOnlyHint: false, destructiveHint: false },
77
79
  }, TOOL_HANDLER);
78
80
  }
@@ -30,5 +30,10 @@ const TOOL_HANDLER = async (args, extra) => {
30
30
  };
31
31
  };
32
32
  export function createTool(server) {
33
- server.tool(TOOL_NAME, TOOL_DESCRIPTION, TOOL_ARGS, TOOL_HANDLER);
33
+ server.registerTool(TOOL_NAME, {
34
+ title: "Reboot Cameras",
35
+ description: TOOL_DESCRIPTION,
36
+ inputSchema: TOOL_ARGS,
37
+ annotations: { readOnlyHint: false, destructiveHint: true },
38
+ }, TOOL_HANDLER);
34
39
  }
@@ -322,8 +322,10 @@ const TOOL_HANDLER = async (args, extra) => {
322
322
  };
323
323
  export function createTool(server) {
324
324
  server.registerTool(TOOL_NAME, {
325
+ title: "Reports",
325
326
  description: TOOL_DESCRIPTION,
326
327
  inputSchema: TOOL_ARGS.shape,
327
328
  outputSchema: OUTPUT_SCHEMA.shape,
329
+ annotations: { readOnlyHint: true },
328
330
  }, TOOL_HANDLER);
329
331
  }
@@ -69,8 +69,10 @@ const TOOL_HANDLER = async (args, _extra) => {
69
69
  };
70
70
  export function createTool(server) {
71
71
  server.registerTool(TOOL_NAME, {
72
+ title: "Automation Rules",
72
73
  description: TOOL_DESCRIPTION,
73
74
  inputSchema: TOOL_ARGS,
74
75
  outputSchema: OUTPUT_SCHEMA.shape,
76
+ annotations: { readOnlyHint: false, destructiveHint: true },
75
77
  }, TOOL_HANDLER);
76
78
  }
@@ -62,8 +62,10 @@ const TOOL_HANDLER = async (args, _extra) => {
62
62
  };
63
63
  export function createTool(server) {
64
64
  server.registerTool(TOOL_NAME, {
65
+ title: "Search",
65
66
  description: TOOL_DESCRIPTION,
66
67
  inputSchema: TOOL_ARGS,
67
68
  outputSchema: OUTPUT_SCHEMA.shape,
69
+ annotations: { readOnlyHint: true },
68
70
  }, TOOL_HANDLER);
69
71
  }
@@ -331,8 +331,10 @@ function hasCapabilitySensitiveSettings(payload) {
331
331
  }
332
332
  export function createTool(server) {
333
333
  server.registerTool(TOOL_NAME, {
334
+ title: "Update Settings",
334
335
  description: TOOL_DESCRIPTION,
335
336
  inputSchema: TOOL_ARGS,
336
337
  outputSchema: OUTPUT_SCHEMA.shape,
338
+ annotations: { readOnlyHint: false, destructiveHint: false },
337
339
  }, TOOL_HANDLER);
338
340
  }
@@ -57,8 +57,10 @@ const TOOL_HANDLER = async (args, _extra) => {
57
57
  };
58
58
  export function createTool(server) {
59
59
  server.registerTool(TOOL_NAME, {
60
+ title: "User Access Trail",
60
61
  description: TOOL_DESCRIPTION,
61
62
  inputSchema: TOOL_ARGS,
62
63
  outputSchema: OUTPUT_SCHEMA.shape,
64
+ annotations: { readOnlyHint: true },
63
65
  }, TOOL_HANDLER);
64
66
  }
@@ -46,8 +46,10 @@ const TOOL_HANDLER = async (args, _extra) => {
46
46
  };
47
47
  export function createTool(server) {
48
48
  server.registerTool(TOOL_NAME, {
49
+ title: "User Audit Log",
49
50
  description: TOOL_DESCRIPTION,
50
51
  inputSchema: TOOL_ARGS,
51
52
  outputSchema: OUTPUT_SCHEMA.shape,
53
+ annotations: { readOnlyHint: true },
52
54
  }, TOOL_HANDLER);
53
55
  }
@@ -34,8 +34,10 @@ const TOOL_HANDLER = async (args, extra) => {
34
34
  };
35
35
  export function createTool(server) {
36
36
  server.registerTool(TOOL_NAME, {
37
+ title: "Video Walls",
37
38
  description: TOOL_DESCRIPTION,
38
39
  inputSchema: TOOL_ARGS,
39
40
  outputSchema: OUTPUT_SCHEMA.shape,
41
+ annotations: { readOnlyHint: false, destructiveHint: false },
40
42
  }, TOOL_HANDLER);
41
43
  }
@@ -4,32 +4,25 @@ import { ISOTimestampFormatDescription } from "../utils/timestampInput.js";
4
4
  export const TOOL_ARGS = {
5
5
  personQuery: z
6
6
  .string()
7
- .nullable()
8
- .describe('The recognized person to track, full-text name match against registered faces, e.g. "Eve" or "Eve Adams". ' +
9
- "Provide this OR personUuid OR faceEventUuid."),
10
- personUuid: z
11
- .string()
12
- .nullable()
13
- .describe("The exact person UUID to track (from faces-tool get-registered-faces). Takes precedence over personQuery."),
14
- faceEventUuid: z
15
- .string()
16
- .nullable()
17
- .describe("Track by appearance from a specific face sighting (a faceEvent UUID), even when the person isn't a registered/named face. " +
18
- "Use this for 'track THIS person' from a sighting. Takes precedence over personQuery/personUuid."),
19
- locationUuids: z
20
- .array(createUuidSchema())
21
- .nullable()
22
- .describe("Optional: restrict to these Rhombus location UUIDs. Use the location-tool to resolve names."),
7
+ .describe('The person to track, full-text name match against their access-control (badge) records, e.g. "Brandon" or "Brandon Salzberg".'),
23
8
  startTime: z
24
9
  .string()
25
10
  .datetime({ message: "Invalid datetime string. Expected ISO 8601 format.", offset: true })
26
11
  .nullable()
27
- .describe("Start of the window (inclusive). " + ISOTimestampFormatDescription),
12
+ .describe("Start of the window to search badge taps and track over (inclusive). " + ISOTimestampFormatDescription),
28
13
  endTime: z
29
14
  .string()
30
15
  .datetime({ message: "Invalid datetime string. Expected ISO 8601 format.", offset: true })
31
16
  .nullable()
32
17
  .describe("End of the window (inclusive). " + ISOTimestampFormatDescription),
18
+ locationUuids: z
19
+ .array(createUuidSchema())
20
+ .nullable()
21
+ .describe("Optional: restrict badge search and the re-id track to these Rhombus location UUIDs."),
22
+ badgeMatchWindowSeconds: z
23
+ .number()
24
+ .nullable()
25
+ .describe("± seconds around the badge tap to look on the door camera for the person's re-id embedding (default 30)."),
33
26
  clipPaddingSeconds: z
34
27
  .number()
35
28
  .nullable()
@@ -42,36 +35,22 @@ export const TOOL_ARGS = {
42
35
  };
43
36
  const TOOL_ARGS_SCHEMA = z.object(TOOL_ARGS);
44
37
  const ClipHintSchema = z
45
- .object({
46
- deviceUuid: z.string(),
47
- startTimeMs: z.number(),
48
- endTimeMs: z.number(),
49
- })
38
+ .object({ deviceUuid: z.string(), startTimeMs: z.number(), endTimeMs: z.number() })
50
39
  .describe("Pass to clips-tool createClip to get video of this sighting.");
51
40
  const StillHintSchema = z
52
- .object({
53
- deviceUuid: z.string(),
54
- timestampMs: z.number(),
55
- })
41
+ .object({ deviceUuid: z.string(), timestampMs: z.number() })
56
42
  .describe("Pass to camera-tool (requestType image) to get a still of this sighting.");
57
- const PersonRefSchema = z.object({
58
- name: z.string().optional(),
59
- personUuid: z.string().optional(),
60
- });
61
43
  export const SightingSchema = z.object({
62
44
  timestampMs: z.number().optional(),
63
45
  datetime: z.string().optional().describe("Human-readable sighting time in the requested timezone."),
64
- deviceUuid: z
65
- .string()
66
- .optional()
67
- .describe("The camera that saw the person. Pass to camera-tool (image) or clips-tool (createClip)."),
68
- locationUuid: z.string().optional().describe("The location where the sighting occurred."),
69
- faceName: z.string().optional().describe("The recognized name on this sighting, if any."),
70
- similarity: z
46
+ deviceUuid: z.string().optional().describe("The camera that re-identified the person."),
47
+ locationUuid: z.string().optional(),
48
+ distance: z
71
49
  .number()
72
50
  .optional()
73
- .describe("Appearance-match similarity (0-1) when tracking from a faceEventUuid seed."),
74
- thumbnailS3Key: z.string().optional().describe("Thumbnail key for the detected face on this sighting."),
51
+ .describe("Re-id match distance to the door embedding — LOWER means a closer appearance match."),
52
+ stableTrackId: z.number().optional().describe("Per-camera track-consolidation id for this detection."),
53
+ thumbnailUri: z.string().optional().describe("Thumbnail of the detected person."),
75
54
  clipHint: ClipHintSchema.optional(),
76
55
  stillHint: StillHintSchema.optional(),
77
56
  gapToNextSeconds: z
@@ -79,21 +58,31 @@ export const SightingSchema = z.object({
79
58
  .optional()
80
59
  .describe("Seconds until the next sighting — large gaps mean the person was unobserved between cameras."),
81
60
  });
61
+ const AnchorSchema = z
62
+ .object({
63
+ deviceUuid: z.string().optional().describe("The door camera where the badge tap happened."),
64
+ timestampMs: z.number().optional(),
65
+ datetime: z.string().optional(),
66
+ integration: z.string().optional().describe("Which badge system the tap came from (OnGuard / Elements / NetBox)."),
67
+ area: z.string().optional(),
68
+ })
69
+ .describe("The access-control badge tap used to ground the re-id track (the known identity moment).");
82
70
  export const OUTPUT_SCHEMA = z.object({
83
- resolvedPerson: PersonRefSchema.optional().describe("The person actually tracked (name + UUID), when resolved."),
84
- ambiguousPeople: z
85
- .array(PersonRefSchema)
71
+ resolvedPerson: z
72
+ .object({ name: z.string().optional() })
86
73
  .optional()
87
- .describe("Set when personQuery matched more than one registered person disambiguate with the user before trusting the track."),
74
+ .describe("The person resolved from the badge record."),
75
+ anchor: AnchorSchema.optional(),
88
76
  sightings: z
89
77
  .array(SightingSchema)
90
78
  .optional()
91
- .describe("The person's camera sightings in chronological order (oldest first)."),
79
+ .describe("Re-id sightings of the person across cameras, in chronological order (oldest first)."),
92
80
  path: z
93
81
  .array(z.string())
94
82
  .optional()
95
- .describe("Camera UUIDs the person passed through, in order (consecutive repeats collapsed)."),
96
- lastKnownSighting: SightingSchema.optional().describe("The most recent sighting — the person's last-known location."),
97
- count: z.number().optional().describe("Number of sightings returned."),
83
+ .describe("Camera UUIDs the person was re-identified at, in order (consecutive repeats collapsed)."),
84
+ lastKnownSighting: SightingSchema.optional().describe("The most recent re-id sighting — last-known location."),
85
+ count: z.number().optional().describe("Number of re-id sightings returned."),
86
+ note: z.string().optional().describe("Set when the track couldn't be built (no badge tap, or no re-id at the door)."),
98
87
  error: z.string().optional(),
99
88
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rhombus-node-mcp",
3
- "version": "0.1.54",
3
+ "version": "0.1.56",
4
4
  "description": "MCP server for Rhombus API",
5
5
  "keywords": [
6
6
  "ai",