rhombus-node-mcp 0.1.12 → 0.1.15
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 +2 -2
- package/dist/createServer.js +44 -0
- package/dist/index.js +27 -32
- package/dist/logger.js +1 -1
- package/dist/network.js +6 -2
- package/dist/tools/clips-tool.js +3 -4
- package/dist/tools/create-tool.js +11 -12
- package/dist/tools/devices/camera-tool/camera-tool.js +6 -6
- package/dist/tools/devices/get-entity-tool.js +4 -3
- package/dist/tools/events-tool.js +5 -6
- package/dist/tools/faces-tool.js +4 -5
- package/dist/tools/get-org-information.js +2 -3
- package/dist/tools/location-tool.js +3 -4
- package/dist/tools/policy-alerts-tool.js +3 -4
- package/dist/tools/reboot-cameras.js +3 -4
- package/dist/tools/time-tool.js +4 -2
- package/dist/transports/stdio.js +10 -0
- package/dist/transports/streamable-http.js +98 -0
- package/dist/util.js +4 -14
- package/dist/utils/confirmation.js +0 -6
- package/package.json +6 -1
package/README.md
CHANGED
|
@@ -49,7 +49,7 @@ Your insights will directly influence our development roadmap and help us create
|
|
|
49
49
|
"--rm",
|
|
50
50
|
"-e",
|
|
51
51
|
"RHOMBUS_API_KEY=YOUR_API_KEY_HERE",
|
|
52
|
-
"
|
|
52
|
+
"rhombussystems/mcp-server-rhombus"
|
|
53
53
|
],
|
|
54
54
|
"env": {
|
|
55
55
|
"RHOMBUS_API_KEY": "YOUR_API_KEY_HERE"
|
|
@@ -106,7 +106,7 @@ docker build -t mcp-server-rhombus .
|
|
|
106
106
|
### 2. Update Your Claude Config for Local Use ⚡
|
|
107
107
|
Now, you'll need to adjust your `claude_desktop_config.json` to point to your newly built local Docker image.
|
|
108
108
|
|
|
109
|
-
> ***Note:*** When running locally, the Docker image name changes to `mcp-server-rhombus` from `
|
|
109
|
+
> ***Note:*** When running locally, the Docker image name changes to `mcp-server-rhombus` from `rhombussystems/mcp-server-rhombus`. Make sure to update this in your configuration!
|
|
110
110
|
|
|
111
111
|
```json
|
|
112
112
|
{
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { logger } from "./logger.js";
|
|
3
|
+
import getResources from "./resources/getResources.js";
|
|
4
|
+
import getTools from "./tools/getTools.js";
|
|
5
|
+
let initiated = false;
|
|
6
|
+
let resources;
|
|
7
|
+
let tools;
|
|
8
|
+
export async function serverInit() {
|
|
9
|
+
resources = await getResources();
|
|
10
|
+
logger.info(`📚 Found ${resources.length} resources`);
|
|
11
|
+
for (const resource of resources) {
|
|
12
|
+
logger.debug(`📕 - ${resource.name}`);
|
|
13
|
+
}
|
|
14
|
+
tools = await getTools();
|
|
15
|
+
logger.info(`🛠️ Found ${tools.length} tools`);
|
|
16
|
+
for (const tool of tools) {
|
|
17
|
+
logger.debug(`🔧 - ${tool.name}`);
|
|
18
|
+
}
|
|
19
|
+
initiated = true;
|
|
20
|
+
}
|
|
21
|
+
export default async function createServer() {
|
|
22
|
+
if (!initiated) {
|
|
23
|
+
await serverInit();
|
|
24
|
+
}
|
|
25
|
+
logger.info(`🖥️ Creating Server`);
|
|
26
|
+
const server = new McpServer({
|
|
27
|
+
name: "rhombus-node-mcp",
|
|
28
|
+
version: "1.0.0",
|
|
29
|
+
capabilities: {
|
|
30
|
+
resources: {},
|
|
31
|
+
tools: {},
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
for (const resource of resources) {
|
|
35
|
+
resource.create(server);
|
|
36
|
+
}
|
|
37
|
+
logger.info(`🛠️ Registered ${resources.length} resources`);
|
|
38
|
+
for (const tool of tools) {
|
|
39
|
+
tool.create(server);
|
|
40
|
+
}
|
|
41
|
+
logger.info(`🛠️ Registered ${tools.length} tools`);
|
|
42
|
+
logger.info(`✅ Server created`);
|
|
43
|
+
return server;
|
|
44
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,43 +1,38 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
3
|
import "dotenv/config";
|
|
5
|
-
import
|
|
4
|
+
import { serverInit } from "./createServer.js";
|
|
6
5
|
import { logger } from "./logger.js";
|
|
7
|
-
import
|
|
6
|
+
import stdioTransport from "./transports/stdio.js";
|
|
7
|
+
import streamableHttpTransport from "./transports/streamable-http.js";
|
|
8
8
|
const RHOMBUS_API_KEY = process.env.RHOMBUS_API_KEY;
|
|
9
|
-
|
|
10
|
-
logger.info("Missing RHOMBUS_API_KEY");
|
|
11
|
-
}
|
|
12
|
-
const serverUrl = process.env.RHOMBUS_API_SERVER || "api2.rhombussystems.com";
|
|
13
|
-
logger.info(`Using API_KEY: ${RHOMBUS_API_KEY}`);
|
|
14
|
-
logger.info(`To hit API server: ${serverUrl}`);
|
|
15
|
-
logger.info("🌐 Using server url", serverUrl);
|
|
16
|
-
export const server = new McpServer({
|
|
17
|
-
name: "rhombus",
|
|
18
|
-
version: "1.0.0",
|
|
19
|
-
capabilities: {
|
|
20
|
-
resources: {},
|
|
21
|
-
tools: {},
|
|
22
|
-
},
|
|
23
|
-
});
|
|
9
|
+
const TRANSPORT_TYPE = process.env.TRANSPORT_TYPE || "stdio";
|
|
24
10
|
async function main() {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
11
|
+
if (!RHOMBUS_API_KEY) {
|
|
12
|
+
logger.info("Missing RHOMBUS_API_KEY");
|
|
13
|
+
}
|
|
14
|
+
const serverUrl = process.env.RHOMBUS_API_SERVER || "api2.rhombussystems.com";
|
|
15
|
+
logger.info(`Using API_KEY: ${RHOMBUS_API_KEY}`);
|
|
16
|
+
logger.info(`To hit API server: ${serverUrl}`);
|
|
17
|
+
logger.info("🌐 Using server url", serverUrl);
|
|
18
|
+
await serverInit();
|
|
19
|
+
const server = new McpServer({
|
|
20
|
+
name: "rhombus",
|
|
21
|
+
version: "1.0.0",
|
|
22
|
+
capabilities: {
|
|
23
|
+
resources: {},
|
|
24
|
+
tools: {},
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
if (TRANSPORT_TYPE === "stdio") {
|
|
28
|
+
await stdioTransport();
|
|
29
|
+
}
|
|
30
|
+
else if (TRANSPORT_TYPE === "streamable-http") {
|
|
31
|
+
await streamableHttpTransport();
|
|
30
32
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
for (const tool of tools) {
|
|
34
|
-
tool.create(server);
|
|
35
|
-
logger.debug(`🔧 Registered tool ${tool.name}`);
|
|
33
|
+
else {
|
|
34
|
+
throw new Error(`Invalid transport type: ${TRANSPORT_TYPE}`);
|
|
36
35
|
}
|
|
37
|
-
const transport = new StdioServerTransport();
|
|
38
|
-
logger.info(`🚙 Starting stdio transport`);
|
|
39
|
-
await server.connect(transport);
|
|
40
|
-
logger.info(`🚙🌬️ Connected.`);
|
|
41
36
|
}
|
|
42
37
|
main().catch(error => {
|
|
43
38
|
console.error("Fatal error in main():", error);
|
package/dist/logger.js
CHANGED
package/dist/network.js
CHANGED
|
@@ -34,15 +34,19 @@ export const appendQueryParams = (url, params) => {
|
|
|
34
34
|
const queryString = existingSearchParams.toString();
|
|
35
35
|
return queryString ? `${baseUrl}?${queryString}` : baseUrl;
|
|
36
36
|
};
|
|
37
|
-
export async function postApi(route, body, modifiers
|
|
37
|
+
export async function postApi(route, body, modifiers) {
|
|
38
|
+
// merge headers
|
|
38
39
|
let requestHeaders = {
|
|
39
|
-
...(modifiers?.headers ?? AUTH_HEADERS),
|
|
40
40
|
...STATIC_HEADERS,
|
|
41
|
+
...AUTH_HEADERS,
|
|
42
|
+
...(modifiers?.headers ?? {}),
|
|
41
43
|
};
|
|
42
44
|
let url = BASE_URL + route;
|
|
45
|
+
// attach query params if provided
|
|
43
46
|
if (modifiers?.query) {
|
|
44
47
|
url = appendQueryParams(url, modifiers.query);
|
|
45
48
|
}
|
|
49
|
+
// stringify body if it's not already a string
|
|
46
50
|
if (typeof body === "object") {
|
|
47
51
|
body = JSON.stringify(body);
|
|
48
52
|
}
|
package/dist/tools/clips-tool.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { postApi } from "../network.js";
|
|
3
|
-
import { createToolArgs } from "../util.js";
|
|
4
3
|
const ClipsArgs = z.object({
|
|
5
4
|
deviceUuidFilters: z
|
|
6
5
|
.array(z.string())
|
|
@@ -50,11 +49,11 @@ The tool returns a JSON object with the following structure and important fields
|
|
|
50
49
|
* **status (string):** The current processing status of the clip, with possible values such as INITIATING, UPLOADING, RENDERING, FAILED, COMPLETE, OFFLINE, or UNKNOWN.
|
|
51
50
|
* **userUuid (string | null):** The UUID of the user associated with the clip, if applicable.
|
|
52
51
|
* **sourceAlertUuid (string | null):** The UUID of the alert that triggered the creation of this clip, if any.
|
|
53
|
-
`,
|
|
52
|
+
`, {
|
|
54
53
|
...ClipsArgs.shape,
|
|
55
|
-
}
|
|
54
|
+
}, async ({ ...args }, extra) => {
|
|
56
55
|
let ret;
|
|
57
|
-
ret = await getSavedClips(args, requestModifiers);
|
|
56
|
+
ret = await getSavedClips(args, extra._meta?.requestModifiers);
|
|
58
57
|
return {
|
|
59
58
|
content: [{ type: "text", text: JSON.stringify(ret) }],
|
|
60
59
|
};
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { createToolArgs } from "../util.js";
|
|
3
|
-
import { CreateVideoWallOptions } from "../types.js";
|
|
4
|
-
import { postApi } from "../network.js";
|
|
5
2
|
import { logger } from "../logger.js";
|
|
3
|
+
import { postApi } from "../network.js";
|
|
4
|
+
import { CreateVideoWallOptions } from "../types.js";
|
|
6
5
|
import { addConfirmationParams, requireConfirmation } from "../utils/confirmation.js";
|
|
7
|
-
async function createVideoWall(options,
|
|
6
|
+
async function createVideoWall(options, requestModifiers) {
|
|
8
7
|
const body = {
|
|
9
8
|
videoWall: {
|
|
10
9
|
displayName: options?.displayName,
|
|
@@ -19,12 +18,12 @@ async function createVideoWall(options, headers) {
|
|
|
19
18
|
},
|
|
20
19
|
},
|
|
21
20
|
};
|
|
22
|
-
const response = await postApi("/camera/createVideoWall", body,
|
|
21
|
+
const response = await postApi("/camera/createVideoWall", body, requestModifiers);
|
|
23
22
|
return response;
|
|
24
23
|
}
|
|
25
|
-
async function handleCreateVideoWallRequest(videoWallCreateOptions,
|
|
24
|
+
async function handleCreateVideoWallRequest(videoWallCreateOptions, requestModifiers) {
|
|
26
25
|
let text = "Unable to create video wall!";
|
|
27
|
-
logger.
|
|
26
|
+
logger.info("🔨 Creating video wall");
|
|
28
27
|
if (!videoWallCreateOptions?.displayName) {
|
|
29
28
|
text = JSON.stringify({
|
|
30
29
|
needUserInput: true,
|
|
@@ -38,8 +37,8 @@ async function handleCreateVideoWallRequest(videoWallCreateOptions, headers) {
|
|
|
38
37
|
});
|
|
39
38
|
}
|
|
40
39
|
else {
|
|
41
|
-
logger.
|
|
42
|
-
text = JSON.stringify(await createVideoWall(videoWallCreateOptions,
|
|
40
|
+
logger.info("Creating video wall with options: ", JSON.stringify(videoWallCreateOptions));
|
|
41
|
+
text = JSON.stringify(await createVideoWall(videoWallCreateOptions, requestModifiers));
|
|
43
42
|
}
|
|
44
43
|
return Promise.resolve({
|
|
45
44
|
content: [
|
|
@@ -51,17 +50,17 @@ async function handleCreateVideoWallRequest(videoWallCreateOptions, headers) {
|
|
|
51
50
|
});
|
|
52
51
|
}
|
|
53
52
|
export function createTool(server) {
|
|
54
|
-
server.tool("create-tool", "Tool for creating many entity types such as video walls.", addConfirmationParams(
|
|
53
|
+
server.tool("create-tool", "Tool for creating many entity types such as video walls.", addConfirmationParams({
|
|
55
54
|
entityType: z
|
|
56
55
|
.enum(["video-wall"])
|
|
57
56
|
.describe("The entity type to create. Example: video wall."),
|
|
58
57
|
videoWallCreateOptions: CreateVideoWallOptions,
|
|
59
|
-
})
|
|
58
|
+
}), async ({ entityType, videoWallCreateOptions, confirmationId }, extra) => {
|
|
60
59
|
const confirmation = requireConfirmation(confirmationId);
|
|
61
60
|
if (confirmation === true) {
|
|
62
61
|
switch (entityType) {
|
|
63
62
|
case "video-wall":
|
|
64
|
-
return await handleCreateVideoWallRequest(videoWallCreateOptions, requestModifiers);
|
|
63
|
+
return await handleCreateVideoWallRequest(videoWallCreateOptions, extra._meta?.requestModifiers);
|
|
65
64
|
default:
|
|
66
65
|
}
|
|
67
66
|
return {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { getLogger } from "../../../logger.js";
|
|
3
3
|
import { appendQueryParams, AUTH_HEADERS, postApi, STATIC_HEADERS } from "../../../network.js";
|
|
4
|
-
import {
|
|
4
|
+
import { createToolTextContent, removeNullFields } from "../../../util.js";
|
|
5
5
|
import { addConfirmationParams, isConfirmed, requireConfirmation, } from "../../../utils/confirmation.js";
|
|
6
6
|
import { ExternalUpdateableFacetedUserConfigSchema, } from "./types.js";
|
|
7
7
|
const logger = getLogger("camera-tool");
|
|
@@ -130,14 +130,14 @@ Please make sure you only update the necessary fields, since any unnecessary cha
|
|
|
130
130
|
It may be a good idea to call "image" on this tool again after updating settings to make sure the new settings were effective in fulfilling
|
|
131
131
|
the user's request.
|
|
132
132
|
Use Cases: Adjust streaming parameters for Camera E.
|
|
133
|
-
`, addConfirmationParams(
|
|
133
|
+
`, addConfirmationParams({
|
|
134
134
|
requestType: z.enum(["image", "get-settings", "update-settings"]),
|
|
135
135
|
timestampMs: z.optional(z.number()).describe(`
|
|
136
136
|
the timestamp in milliseconds. You can default to the current time if the user didn't specify a time, or you can call time-tool to parse the user's time description
|
|
137
137
|
`),
|
|
138
138
|
cameraUuid: z.optional(z.string()).describe("the camera uuid requested"),
|
|
139
139
|
configUpdate: ExternalUpdateableFacetedUserConfigSchema.optional().describe('the config update that would be applied to the camera if the requestType is "update-settings"'),
|
|
140
|
-
})
|
|
140
|
+
}), async ({ cameraUuid, timestampMs, requestType, configUpdate, confirmationId }, extra) => {
|
|
141
141
|
if (!cameraUuid) {
|
|
142
142
|
return {
|
|
143
143
|
content: [
|
|
@@ -167,7 +167,7 @@ Use Cases: Adjust streaming parameters for Camera E.
|
|
|
167
167
|
],
|
|
168
168
|
};
|
|
169
169
|
}
|
|
170
|
-
response = await getImageForCameraAtTime(cameraUuid, timestampMs, requestModifiers);
|
|
170
|
+
response = await getImageForCameraAtTime(cameraUuid, timestampMs, extra._meta?.requestModifiers);
|
|
171
171
|
if (!response.success || !response.imageData) {
|
|
172
172
|
return {
|
|
173
173
|
content: [{ type: "text", text: JSON.stringify(response) }],
|
|
@@ -183,7 +183,7 @@ Use Cases: Adjust streaming parameters for Camera E.
|
|
|
183
183
|
],
|
|
184
184
|
};
|
|
185
185
|
case "get-settings":
|
|
186
|
-
response = await getCameraSettings(cameraUuid, requestModifiers);
|
|
186
|
+
response = await getCameraSettings(cameraUuid, extra._meta?.requestModifiers);
|
|
187
187
|
return {
|
|
188
188
|
content: [{ type: "text", text: JSON.stringify(response) }],
|
|
189
189
|
};
|
|
@@ -195,7 +195,7 @@ Use Cases: Adjust streaming parameters for Camera E.
|
|
|
195
195
|
if (!configUpdate) {
|
|
196
196
|
return createToolTextContent("Missing configUpdate");
|
|
197
197
|
}
|
|
198
|
-
response = await updateCameraSettings(cameraUuid, configUpdate, requestModifiers);
|
|
198
|
+
response = await updateCameraSettings(cameraUuid, configUpdate, extra._meta?.requestModifiers);
|
|
199
199
|
return {
|
|
200
200
|
content: [{ type: "text", text: JSON.stringify(response) }],
|
|
201
201
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { postApi } from "../../network.js";
|
|
3
|
-
import { createToolArgs, createToolTextContent } from "../../util.js";
|
|
4
3
|
import DeviceType from "../../types/deviceType.js";
|
|
4
|
+
import { createToolTextContent } from "../../util.js";
|
|
5
5
|
async function getCameraList(requestModifiers) {
|
|
6
6
|
return {
|
|
7
7
|
cameras: (await postApi("/camera/getMinimalCameraStateList", "{}", requestModifiers)).cameraStates.filter((camera) => !!camera.locationUuid),
|
|
@@ -69,11 +69,12 @@ export function createTool(server) {
|
|
|
69
69
|
Retrieves entities (or devices) of certain types.
|
|
70
70
|
Can request multiple entity types at once.
|
|
71
71
|
The return structure is a JSON string that continues the states of the requested entities.
|
|
72
|
-
This data is exact. Whatever entities exist will be returned here.`,
|
|
72
|
+
This data is exact. Whatever entities exist will be returned here.`, {
|
|
73
73
|
entityTypes: z
|
|
74
74
|
.array(z.nativeEnum(DeviceType).describe("The entity type to retreive"))
|
|
75
75
|
.describe("What type of entities to retrieve."),
|
|
76
|
-
}
|
|
76
|
+
}, async ({ entityTypes }, extra) => {
|
|
77
|
+
const requestModifiers = extra._meta?.requestModifiers;
|
|
77
78
|
const promises = [];
|
|
78
79
|
if (entityTypes.includes(DeviceType.CAMERA)) {
|
|
79
80
|
promises.push(getCameraList(requestModifiers));
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
2
|
+
import { FIVE_SECONDS_MS, THREE_HOURS_MS } from "../constants.js";
|
|
3
3
|
import { postApi } from "../network.js";
|
|
4
|
-
import { THREE_HOURS_MS, FIVE_SECONDS_MS } from "../constants.js";
|
|
5
4
|
async function getFaceEvents(_locationUuid, requestModifiers) {
|
|
6
5
|
const nowMs = Date.now();
|
|
7
6
|
const rangeStartMs = nowMs - THREE_HOURS_MS;
|
|
@@ -47,13 +46,13 @@ async function getAccessControlEvents(doorUuid, requestModifiers) {
|
|
|
47
46
|
return response;
|
|
48
47
|
}
|
|
49
48
|
export function createTool(server) {
|
|
50
|
-
server.tool("events-tool", "event data for certain types of information like faces, license plates, and access-control events",
|
|
49
|
+
server.tool("events-tool", "event data for certain types of information like faces, license plates, and access-control events", {
|
|
51
50
|
eventType: z.enum(["faces", "people", "access-control"]),
|
|
52
51
|
locationUuid: z.optional(z.string()),
|
|
53
52
|
accessControlledDoorUuid: z.optional(z.string()),
|
|
54
|
-
}
|
|
53
|
+
}, async ({ eventType, locationUuid, accessControlledDoorUuid }, extra) => {
|
|
55
54
|
if (eventType === "faces" || eventType === "people") {
|
|
56
|
-
const response = await getFaceEvents(locationUuid, requestModifiers);
|
|
55
|
+
const response = await getFaceEvents(locationUuid, extra._meta?.requestModifiers);
|
|
57
56
|
return {
|
|
58
57
|
content: [
|
|
59
58
|
{
|
|
@@ -78,7 +77,7 @@ export function createTool(server) {
|
|
|
78
77
|
};
|
|
79
78
|
}
|
|
80
79
|
else {
|
|
81
|
-
const events = await getAccessControlEvents(accessControlledDoorUuid, requestModifiers);
|
|
80
|
+
const events = await getAccessControlEvents(accessControlledDoorUuid, extra._meta?.requestModifiers);
|
|
82
81
|
return {
|
|
83
82
|
content: [
|
|
84
83
|
{
|
package/dist/tools/faces-tool.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { postApi } from "../network.js";
|
|
3
|
-
import { createToolArgs } from "../util.js";
|
|
4
3
|
var RequestType;
|
|
5
4
|
(function (RequestType) {
|
|
6
5
|
RequestType["GET_FACE_EVENTS"] = "get-face-events";
|
|
@@ -112,7 +111,7 @@ If the requestType is "get-registered-faces":
|
|
|
112
111
|
|
|
113
112
|
`,
|
|
114
113
|
// FacesToolArgs directly passed here
|
|
115
|
-
|
|
114
|
+
{
|
|
116
115
|
requestType: z.nativeEnum(RequestType),
|
|
117
116
|
args: z.union([
|
|
118
117
|
z.object({
|
|
@@ -123,15 +122,15 @@ If the requestType is "get-registered-faces":
|
|
|
123
122
|
args: GetRegisteredFacesArgsSchema,
|
|
124
123
|
}),
|
|
125
124
|
]),
|
|
126
|
-
}
|
|
125
|
+
}, async ({ requestType, args }, extra) => {
|
|
127
126
|
let ret;
|
|
128
127
|
if (requestType === "get-face-events") {
|
|
129
128
|
// Pass the args directly, as requested
|
|
130
|
-
ret = await getFaceEvents(args, requestModifiers);
|
|
129
|
+
ret = await getFaceEvents(args, extra._meta?.requestModifiers);
|
|
131
130
|
}
|
|
132
131
|
else if (requestType === "get-registered-faces") {
|
|
133
132
|
// Pass the args (will effectively be an empty object for this call)
|
|
134
|
-
ret = await getRegisteredFaces(args, requestModifiers);
|
|
133
|
+
ret = await getRegisteredFaces(args, extra._meta?.requestModifiers);
|
|
135
134
|
}
|
|
136
135
|
return {
|
|
137
136
|
content: [{ type: "text", text: JSON.stringify(ret) }],
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import { createToolArgs } from "../util.js";
|
|
2
1
|
import { postApi } from "../network.js";
|
|
3
2
|
async function getOrg(requestModifiers) {
|
|
4
3
|
return await postApi("/org/getOrgV2", {}, requestModifiers);
|
|
5
4
|
}
|
|
6
5
|
export function createTool(server) {
|
|
7
|
-
server.tool("get-org-information", "Get general information about the organization including org name, camera configuration defaults, contact information, and org settings.",
|
|
8
|
-
const org = await getOrg(requestModifiers);
|
|
6
|
+
server.tool("get-org-information", "Get general information about the organization including org name, camera configuration defaults, contact information, and org settings.", {}, async (_, extra) => {
|
|
7
|
+
const org = await getOrg(extra._meta?.requestModifiers);
|
|
9
8
|
return {
|
|
10
9
|
content: [
|
|
11
10
|
{
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { createToolArgs } from "../util.js";
|
|
3
2
|
import { postApi } from "../network.js";
|
|
4
3
|
async function getLocations(requestModifiers) {
|
|
5
4
|
return await postApi("/location/getLocationsV2", {}, requestModifiers);
|
|
6
5
|
}
|
|
7
6
|
export function createTool(server) {
|
|
8
7
|
server.tool("location-tool", `This tool performs operations on locations.
|
|
9
|
-
- 'get': Retrieves all locations.`,
|
|
8
|
+
- 'get': Retrieves all locations.`, {
|
|
10
9
|
action: z.enum(["get", "update"]),
|
|
11
10
|
locationUpdate: z
|
|
12
11
|
.object({
|
|
@@ -14,11 +13,11 @@ export function createTool(server) {
|
|
|
14
13
|
name: z.string(),
|
|
15
14
|
})
|
|
16
15
|
.optional(),
|
|
17
|
-
}
|
|
16
|
+
}, async ({ action }, extra) => {
|
|
18
17
|
let ret;
|
|
19
18
|
switch (action) {
|
|
20
19
|
case "get":
|
|
21
|
-
ret = await getLocations(requestModifiers);
|
|
20
|
+
ret = await getLocations(extra._meta?.requestModifiers);
|
|
22
21
|
break;
|
|
23
22
|
case "update":
|
|
24
23
|
ret = { error: true, status: "not implemented" };
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { postApi } from "../network.js";
|
|
3
|
-
import { createToolArgs } from "../util.js";
|
|
4
3
|
const PolicyAlertsArgs = z.object({
|
|
5
4
|
afterTimestampMs: z
|
|
6
5
|
.number()
|
|
@@ -45,11 +44,11 @@ policy alerts within the Rhombus system.
|
|
|
45
44
|
This tool allows you to filter existing alerts by a specific time range (before or after a timestamp in milliseconds),
|
|
46
45
|
by a list of device UUIDs, or by a list of location UUIDs.
|
|
47
46
|
You can also specify the maximum number of results to return.
|
|
48
|
-
The output is provided in JSON format.`,
|
|
47
|
+
The output is provided in JSON format.`, {
|
|
49
48
|
...PolicyAlertsArgs.shape,
|
|
50
|
-
}
|
|
49
|
+
}, async ({ ...args }, extra) => {
|
|
51
50
|
let ret;
|
|
52
|
-
ret = await getPolicyAlerts(args, requestModifiers);
|
|
51
|
+
ret = await getPolicyAlerts(args, extra._meta?.requestModifiers);
|
|
53
52
|
return {
|
|
54
53
|
content: [{ type: "text", text: JSON.stringify(ret) }],
|
|
55
54
|
};
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { createToolArgs } from "../util.js";
|
|
3
2
|
import { postApi } from "../network.js";
|
|
4
3
|
import { addConfirmationParams, isConfirmed, requireConfirmation } from "../utils/confirmation.js";
|
|
5
4
|
async function rebootCameras(cameraUuids, requestModifiers) {
|
|
@@ -31,16 +30,16 @@ async function rebootCameras(cameraUuids, requestModifiers) {
|
|
|
31
30
|
}
|
|
32
31
|
}
|
|
33
32
|
export function createTool(server) {
|
|
34
|
-
server.tool("reboot-cameras", "this tool is for rebooting one or more cameras causing them to reconnect to the server, this is a helpful option when a camera is experiencing connectivity issues or is in need of troubleshooting. THIS TOOL PERFORMS AN ACTION.", addConfirmationParams(
|
|
33
|
+
server.tool("reboot-cameras", "this tool is for rebooting one or more cameras causing them to reconnect to the server, this is a helpful option when a camera is experiencing connectivity issues or is in need of troubleshooting. THIS TOOL PERFORMS AN ACTION.", addConfirmationParams({
|
|
35
34
|
cameraUuids: z
|
|
36
35
|
.array(z.string())
|
|
37
36
|
.describe("An array of camera UUID strings which are unique identifiers for cameras"),
|
|
38
|
-
})
|
|
37
|
+
}), async ({ cameraUuids, confirmationId }, extra) => {
|
|
39
38
|
const confirmation = requireConfirmation(confirmationId);
|
|
40
39
|
if (!isConfirmed(confirmation)) {
|
|
41
40
|
return confirmation;
|
|
42
41
|
}
|
|
43
|
-
const cameraRebootData = await rebootCameras(cameraUuids, requestModifiers);
|
|
42
|
+
const cameraRebootData = await rebootCameras(cameraUuids, extra._meta?.requestModifiers);
|
|
44
43
|
if (!cameraRebootData) {
|
|
45
44
|
return {
|
|
46
45
|
content: [
|
package/dist/tools/time-tool.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
1
|
import { parse } from "chrono-node";
|
|
3
2
|
import { DateTime } from "luxon";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { logger } from "../logger.js";
|
|
4
5
|
function nullToUndefined(value) {
|
|
5
6
|
return value === null ? undefined : value;
|
|
6
7
|
}
|
|
@@ -13,7 +14,8 @@ export function createTool(server) {
|
|
|
13
14
|
.string()
|
|
14
15
|
.optional()
|
|
15
16
|
.describe("Optional IANA timezone string (e.g., 'America/Los_Angeles', 'UTC'). Will default to system timezone if not provided."),
|
|
16
|
-
}, async ({ time_description, timezone }) => {
|
|
17
|
+
}, async ({ time_description, timezone }, extra) => {
|
|
18
|
+
logger.info("EXTRA", extra);
|
|
17
19
|
// console.error(`🕛 handling tool call for time ${time_description} using timezone ${timezone}`);
|
|
18
20
|
const now = DateTime.now()
|
|
19
21
|
.setZone(timezone || undefined)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
2
|
+
import createServer from "../createServer.js";
|
|
3
|
+
import { logger } from "../logger.js";
|
|
4
|
+
export default async function stdioTransport() {
|
|
5
|
+
const server = await createServer();
|
|
6
|
+
const transport = new StdioServerTransport();
|
|
7
|
+
logger.info(`🚙 Starting stdio transport`);
|
|
8
|
+
await server.connect(transport);
|
|
9
|
+
logger.info(`🚙🌬️ Connected.`);
|
|
10
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
2
|
+
import express from "express";
|
|
3
|
+
import jwt from "jsonwebtoken";
|
|
4
|
+
import createServer from "../createServer.js";
|
|
5
|
+
import { logger } from "../logger.js";
|
|
6
|
+
import { RequestModifiers } from "../util.js";
|
|
7
|
+
const JWT_SECRET = process.env.SECRET;
|
|
8
|
+
export default function streamableHttpTransport() {
|
|
9
|
+
if (!JWT_SECRET) {
|
|
10
|
+
throw new Error("SECRET is not set");
|
|
11
|
+
}
|
|
12
|
+
const app = express();
|
|
13
|
+
app.use(express.json());
|
|
14
|
+
app.post("/mcp", async (req, res) => {
|
|
15
|
+
logger.info(`Received MCP request`, req.body);
|
|
16
|
+
// check JWT
|
|
17
|
+
const token = req.headers.authorization?.split(" ")[1];
|
|
18
|
+
let authRequestModifiers;
|
|
19
|
+
if (token) {
|
|
20
|
+
try {
|
|
21
|
+
const decoded = RequestModifiers.parse(jwt.verify(token, JWT_SECRET));
|
|
22
|
+
// if successful (did not throw)
|
|
23
|
+
authRequestModifiers = decoded;
|
|
24
|
+
// inject auth into request body's meta object
|
|
25
|
+
// to be made available to MCP tools
|
|
26
|
+
if (req.body.params) {
|
|
27
|
+
req.body.params._meta = {
|
|
28
|
+
...(req.body.params._meta ?? {}),
|
|
29
|
+
requestModifiers: authRequestModifiers,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
logger.info(`🔒 MCP request authenticated with x-auth-session: ${authRequestModifiers?.headers?.["x-auth-session"]} and x-auth-chat: ${authRequestModifiers?.headers?.["x-auth-chat"]}`);
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
logger.error(`Error verifying JWT: ${token}, err: ${err}`);
|
|
36
|
+
res.status(401).json({
|
|
37
|
+
jsonrpc: "2.0",
|
|
38
|
+
error: { code: -32000, message: "Unauthorized" },
|
|
39
|
+
id: null,
|
|
40
|
+
});
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const server = await createServer();
|
|
45
|
+
try {
|
|
46
|
+
const transport = new StreamableHTTPServerTransport({
|
|
47
|
+
sessionIdGenerator: undefined,
|
|
48
|
+
});
|
|
49
|
+
await server.connect(transport);
|
|
50
|
+
await transport.handleRequest(req, res, req.body);
|
|
51
|
+
res.on("close", () => {
|
|
52
|
+
logger.log("Request closed");
|
|
53
|
+
transport.close();
|
|
54
|
+
server.close();
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
logger.error("Error handling MCP request:", error);
|
|
59
|
+
if (!res.headersSent) {
|
|
60
|
+
res.status(500).json({
|
|
61
|
+
jsonrpc: "2.0",
|
|
62
|
+
error: {
|
|
63
|
+
code: -32603,
|
|
64
|
+
message: "Internal server error",
|
|
65
|
+
},
|
|
66
|
+
id: null,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
app.get("/mcp", async (req, res) => {
|
|
72
|
+
logger.warn("Received Not Allowed GET MCP request");
|
|
73
|
+
res.writeHead(405).end(JSON.stringify({
|
|
74
|
+
jsonrpc: "2.0",
|
|
75
|
+
error: {
|
|
76
|
+
code: -32000,
|
|
77
|
+
message: "Method not allowed.",
|
|
78
|
+
},
|
|
79
|
+
id: null,
|
|
80
|
+
}));
|
|
81
|
+
});
|
|
82
|
+
app.delete("/mcp", async (req, res) => {
|
|
83
|
+
logger.warn("Received Not Allowed DELETE MCP request");
|
|
84
|
+
res.writeHead(405).end(JSON.stringify({
|
|
85
|
+
jsonrpc: "2.0",
|
|
86
|
+
error: {
|
|
87
|
+
code: -32000,
|
|
88
|
+
message: "Method not allowed.",
|
|
89
|
+
},
|
|
90
|
+
id: null,
|
|
91
|
+
}));
|
|
92
|
+
});
|
|
93
|
+
// Start the server
|
|
94
|
+
const PORT = process.env.PORT || 3000;
|
|
95
|
+
app.listen(PORT, () => {
|
|
96
|
+
logger.info(`rhombus-node-mcp listening on port ${PORT}`);
|
|
97
|
+
});
|
|
98
|
+
}
|
package/dist/util.js
CHANGED
|
@@ -10,14 +10,10 @@ export function generateRandomString(length) {
|
|
|
10
10
|
}
|
|
11
11
|
return result;
|
|
12
12
|
}
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
query: z.nullable(z.record(z.string(), z.string())),
|
|
18
|
-
}))
|
|
19
|
-
.describe("Optional headers accepted by tools. LLM should never ever use this. 😅"),
|
|
20
|
-
};
|
|
13
|
+
export const RequestModifiers = z.object({
|
|
14
|
+
headers: z.optional(z.record(z.string(), z.string())),
|
|
15
|
+
query: z.optional(z.record(z.string(), z.string())),
|
|
16
|
+
}).optional();
|
|
21
17
|
/**
|
|
22
18
|
* Get all file paths in a directory in a directory
|
|
23
19
|
*
|
|
@@ -46,12 +42,6 @@ export function getFilePathsInDirectory(dirPath) {
|
|
|
46
42
|
}
|
|
47
43
|
return filePaths;
|
|
48
44
|
}
|
|
49
|
-
export function createToolArgs(args) {
|
|
50
|
-
return {
|
|
51
|
-
...args,
|
|
52
|
-
...STATIC_ARGS,
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
45
|
/**
|
|
56
46
|
* Returns an object in the form expected by `server.tool`
|
|
57
47
|
*/
|
|
@@ -4,12 +4,6 @@
|
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { createToolTextContent, generateRandomString } from "../util.js";
|
|
6
6
|
import { logger } from "../logger.js";
|
|
7
|
-
// export function createToolArgs<TArgs extends object>(args: TArgs): TArgs & typeof STATIC_ARGS {
|
|
8
|
-
// return {
|
|
9
|
-
// ...args,
|
|
10
|
-
// ...STATIC_ARGS,
|
|
11
|
-
// };
|
|
12
|
-
// }
|
|
13
7
|
export const CONFIRMATION_ARGS = {
|
|
14
8
|
confirmationId: z.string().nullable().optional(),
|
|
15
9
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rhombus-node-mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15",
|
|
4
4
|
"description": "MCP server for Rhombus API",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -35,11 +35,16 @@
|
|
|
35
35
|
"@modelcontextprotocol/sdk": "^1.9.0",
|
|
36
36
|
"chrono-node": "^2.8.0",
|
|
37
37
|
"dotenv": "^16.5.0",
|
|
38
|
+
"express": "^5.1.0",
|
|
39
|
+
"express-jwt": "^8.5.1",
|
|
40
|
+
"jsonwebtoken": "^9.0.2",
|
|
38
41
|
"log4js": "^6.9.1",
|
|
39
42
|
"luxon": "^3.6.1",
|
|
40
43
|
"zod": "^3.24.2"
|
|
41
44
|
},
|
|
42
45
|
"devDependencies": {
|
|
46
|
+
"@types/express": "^5.0.3",
|
|
47
|
+
"@types/jwt-express": "^1.1.6",
|
|
43
48
|
"@types/luxon": "^3.6.2",
|
|
44
49
|
"@types/node": "^22.14.0",
|
|
45
50
|
"prettier": "3.5.3",
|