rhombus-node-mcp 0.1.0

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 (3) hide show
  1. package/README.md +91 -0
  2. package/dist/index.js +273 -0
  3. package/package.json +46 -0
package/README.md ADDED
@@ -0,0 +1,91 @@
1
+ # Rhombus MCP Server
2
+
3
+ An MCP server implementation that integrates the Rhombus API to provide Chatbot tools.
4
+
5
+ ## Configuration
6
+
7
+ ### Step 1:
8
+
9
+ Clone this repository:
10
+
11
+ ```bash
12
+ git clone git@github.com:RhombusSystems/rhombus-node-mcp.git
13
+ ```
14
+
15
+ Navigate to the `rhombus-node-mcp` directory and install the necessary dependencies:
16
+
17
+ ```bash
18
+ cd rhombus-node-mcp && npm install
19
+ ```
20
+
21
+ ### Step 2: Get a Rhombus API Key
22
+
23
+ 1. Sign up for a [Rhombus Account](https://console.rhombus.com).
24
+ 2. Follow the account setup instructions and generate your API key from the developer dashboard.
25
+ 3. Set the API key in your environment as `RHOMBUS_API_KEY`.
26
+
27
+ ### Step 3: Configure Claude Desktop
28
+
29
+ 1. Download Claude desktop [here](https://claude.ai/download).
30
+
31
+ 2. Add this to your `claude_desktop_config.json`:
32
+
33
+ ```json
34
+ {
35
+ "mcpServers": {
36
+ "rhombus": {
37
+ "command": "docker",
38
+ "args": ["run", "-i", "--rm", "-e", "RHOMBUS-API-KEY", "mcp/rhombus"],
39
+ "env": {
40
+ "RHOMBUS_API_KEY": "YOUR_API_KEY_HERE"
41
+ }
42
+ }
43
+ }
44
+ }
45
+ ```
46
+
47
+ ### NPX
48
+
49
+ ```json
50
+ {
51
+ "mcpServers": {
52
+ "rhombus": {
53
+ "command": "npx",
54
+ "args": ["-y", "server-rhombus"],
55
+ "env": {
56
+ "RHOMBUS_API_KEY": "YOUR_API_KEY_HERE"
57
+ }
58
+ }
59
+ }
60
+ }
61
+ ```
62
+
63
+ You can access the file using:
64
+
65
+ ```bash
66
+ vim ~/Library/Application\ Support/Claude/claude_desktop_config.json
67
+ ```
68
+
69
+ ### Step 4: Build the Docker Image
70
+
71
+ Docker build:
72
+
73
+ ```bash
74
+ docker build -t mcp/rhombus:latest -f Dockerfile .
75
+ ```
76
+
77
+ ### Step 5: Testing
78
+
79
+ Let's make sure Claude for Desktop is picking up the tools we've exposed in our `rhombus` server. You can do this by looking for the hammer icon:
80
+
81
+ After clicking on the hammer icon, you should see the tools that come with the Filesystem MCP Server:
82
+
83
+ If you see both of these this means that the integration is active. Congratulations! This means Claude can now ask Rhombus questions. You can then simply use it as you would use the Rhombus web app.
84
+
85
+ ### Troubleshooting
86
+
87
+ The Claude documentation provides an excellent [troubleshooting guide](https://modelcontextprotocol.io/docs/tools/debugging) you can refer to. However, you can still reach out to us at api@rhombus.com for any additional support or [file a bug](https://github.com/ppl-ai/api-discussion/issues).
88
+
89
+ ## License
90
+
91
+ This MCP server is licensed under the MIT License. This means you are free to use, modify, and distribute the software, subject to the terms and conditions of the MIT License. For more details, please see the LICENSE file in the project repository.
package/dist/index.js ADDED
@@ -0,0 +1,273 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { z } from "zod";
5
+ const THREE_HOURS_MS = 3 * 60 * 60 * 1000;
6
+ const FIVE_SECONDS_MS = 5 * 1000;
7
+ const RHOMBUS_API_KEY = process.env.RHOMBUS_API_KEY;
8
+ if (!RHOMBUS_API_KEY) {
9
+ console.error("Missing RHOMBUS_API_KEY");
10
+ process.exit(1);
11
+ }
12
+ const BASE_URL = "https://api2.rhombussystems.com/api";
13
+ const headers = {
14
+ "Content-Type": "application/json",
15
+ "x-auth-apikey": RHOMBUS_API_KEY,
16
+ "x-auth-scheme": "api-token",
17
+ accept: "application/json",
18
+ };
19
+ const server = new McpServer({
20
+ name: "rhombus",
21
+ version: "1.0.0",
22
+ capabilities: {
23
+ resources: {},
24
+ tools: {},
25
+ },
26
+ });
27
+ async function postApi(url, body) {
28
+ try {
29
+ const response = await fetch(url, { method: "POST", headers, body });
30
+ if (!response.ok) {
31
+ throw new Error(`HTTP error! status: ${response.status}`);
32
+ }
33
+ return await response.json();
34
+ }
35
+ catch (error) {
36
+ return {
37
+ error: true,
38
+ status: `Request Error: ${error}`,
39
+ };
40
+ }
41
+ }
42
+ async function getOrg() {
43
+ const url = BASE_URL + "/org/getOrgV2";
44
+ return await postApi(url, "{}");
45
+ }
46
+ async function getLocations() {
47
+ const url = BASE_URL + "/location/getLocationsV2";
48
+ return await postApi(url, "{}");
49
+ }
50
+ async function getCameraList() {
51
+ const url = BASE_URL + "/camera/getMinimalCameraStateList";
52
+ return await postApi(url, "{}").then(response => {
53
+ return {
54
+ cameraStates: response.cameraStates.filter((camera) => !!camera.locationUuid),
55
+ };
56
+ });
57
+ }
58
+ async function getAccessControlledDoors() {
59
+ const url = BASE_URL + "/component/findAccessControlledDoors";
60
+ return await postApi(url, "{}");
61
+ }
62
+ async function getFaceEvents(_locationUuid) {
63
+ const nowMs = Date.now();
64
+ const rangeStartMs = nowMs - THREE_HOURS_MS;
65
+ const rangeEndMs = nowMs - FIVE_SECONDS_MS;
66
+ const body = JSON.stringify({
67
+ pageRequest: {
68
+ lastEvaluatedKey: undefined,
69
+ maxPageSize: 75,
70
+ },
71
+ searchFilter: {
72
+ deviceUuids: [],
73
+ faceNames: [],
74
+ labels: [],
75
+ locationUuids: [],
76
+ personUuids: [],
77
+ timestampFilter: {
78
+ rangeStart: rangeStartMs,
79
+ rangeEnd: rangeEndMs,
80
+ },
81
+ },
82
+ });
83
+ const response = await postApi(BASE_URL + "/faceRecognition/faceEvent/findFaceEventsByOrg", body).then(response => {
84
+ return {
85
+ faceEvents: (response.faceEvents || []).map((event) => ({
86
+ ...event,
87
+ eventTimestamp: new Date(event.eventTimestamp).toString(),
88
+ })),
89
+ };
90
+ });
91
+ return response;
92
+ }
93
+ async function getAccessControlEvents(doorUuid) {
94
+ const url = BASE_URL + "/component/findComponentEventsByAccessControlledDoor";
95
+ const body = JSON.stringify({
96
+ limit: 50,
97
+ accessControlledDoorUuid: doorUuid,
98
+ });
99
+ const response = await postApi(url, body).then(response => ({
100
+ componentEvents: (response.componentEvents || []).map((event) => ({
101
+ ...event,
102
+ timestamp: new Date(event.timestampMs).toString(),
103
+ })),
104
+ }));
105
+ return response;
106
+ }
107
+ async function rebootCameras(cameraUuids) {
108
+ const url = BASE_URL + "/camera/reboot";
109
+ let successCount = 0;
110
+ let errorCount = 0;
111
+ for (const cameraUuid in cameraUuids) {
112
+ try {
113
+ const body = JSON.stringify({ cameraUuid: cameraUuid });
114
+ const response = await postApi(url, body);
115
+ if (response.error) {
116
+ errorCount++;
117
+ }
118
+ else {
119
+ successCount++;
120
+ }
121
+ }
122
+ catch (error) {
123
+ const ret = `Error rebooting cameras: ${error}`;
124
+ return { error: true, status: ret };
125
+ }
126
+ let status;
127
+ if (successCount === cameraUuids.length)
128
+ status = "SUCCESS";
129
+ else if (successCount > 0 && successCount < cameraUuids.length)
130
+ status = "PARTIAL_SUCCESS";
131
+ else
132
+ status = "ERROR";
133
+ return { status, successCount, errorCount };
134
+ }
135
+ }
136
+ server.tool("get-org-information", "Get general information about the organization including org name, camera configuration defaults, contact information, and org settings.", {}, async ({}) => {
137
+ const org = await getOrg();
138
+ return {
139
+ content: [
140
+ {
141
+ type: "text",
142
+ text: JSON.stringify(org),
143
+ },
144
+ ],
145
+ };
146
+ });
147
+ server.tool("get-entity-tool", "get a list of entities like cameras, access controlled doors, sensors, etc.", {
148
+ entityType: z
149
+ .enum(["camera", "access-controlled-doors"])
150
+ .describe("The entity type to retreive. Example: cameras."),
151
+ }, async ({ entityType }) => {
152
+ let ret;
153
+ switch (entityType) {
154
+ case "camera":
155
+ ret = await getCameraList();
156
+ break;
157
+ case "access-controlled-doors":
158
+ ret = await getAccessControlledDoors();
159
+ break;
160
+ default:
161
+ ret = {};
162
+ break;
163
+ }
164
+ return {
165
+ content: [
166
+ {
167
+ type: "text",
168
+ text: JSON.stringify(ret),
169
+ },
170
+ ],
171
+ };
172
+ });
173
+ server.tool("events-tool", "event data for certain types of information like faces and license plates", {
174
+ eventType: z.enum(["faces", "people", "access-control"]),
175
+ locationUuid: z.optional(z.string()),
176
+ accessControlledDoorUuid: z.optional(z.string()),
177
+ }, async ({ eventType, locationUuid, accessControlledDoorUuid }) => {
178
+ if (eventType === "faces" || eventType === "people") {
179
+ const response = await getFaceEvents(locationUuid);
180
+ return {
181
+ content: [
182
+ {
183
+ type: "text",
184
+ text: JSON.stringify(response),
185
+ },
186
+ ],
187
+ };
188
+ }
189
+ if (eventType === "access-control") {
190
+ if (!accessControlledDoorUuid) {
191
+ return {
192
+ content: [
193
+ {
194
+ type: "text",
195
+ text: JSON.stringify({
196
+ needUserInput: true,
197
+ commandForUser: "Which door are you asking about?",
198
+ }),
199
+ },
200
+ ],
201
+ };
202
+ }
203
+ else {
204
+ const events = await getAccessControlEvents(accessControlledDoorUuid);
205
+ return {
206
+ content: [
207
+ {
208
+ type: "text",
209
+ text: JSON.stringify(events),
210
+ },
211
+ ],
212
+ };
213
+ }
214
+ }
215
+ return {
216
+ content: [
217
+ {
218
+ type: "text",
219
+ text: JSON.stringify({}),
220
+ },
221
+ ],
222
+ };
223
+ });
224
+ server.tool("location-tool", "contains basic operations for locations and response in JSON format.", {
225
+ action: z.enum(["get"]),
226
+ locationUpdate: z.optional(z.object({ uuid: z.string(), name: z.optional(z.string()) })),
227
+ }, async ({ action, locationUpdate }) => {
228
+ let ret;
229
+ switch (action) {
230
+ case "get":
231
+ ret = await getLocations();
232
+ break;
233
+ default:
234
+ ret = { error: true, status: `unsupported location tool call: ${action}` };
235
+ break;
236
+ }
237
+ return {
238
+ content: [{ type: "text", text: JSON.stringify(ret) }],
239
+ };
240
+ });
241
+ 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", {
242
+ cameraUuids: z
243
+ .array(z.string())
244
+ .describe("An array of camera UUID strings which are unique identifiers for cameras"),
245
+ }, async ({ cameraUuids }) => {
246
+ const cameraRebootData = await rebootCameras(cameraUuids);
247
+ if (!cameraRebootData) {
248
+ return {
249
+ content: [
250
+ {
251
+ type: "text",
252
+ text: "Failed to reboot cameras",
253
+ },
254
+ ],
255
+ };
256
+ }
257
+ return {
258
+ content: [
259
+ {
260
+ type: "text",
261
+ text: JSON.stringify(cameraRebootData),
262
+ },
263
+ ],
264
+ };
265
+ });
266
+ async function main() {
267
+ const transport = new StdioServerTransport();
268
+ await server.connect(transport);
269
+ }
270
+ main().catch(error => {
271
+ console.error("Fatal error in main():", error);
272
+ process.exit(1);
273
+ });
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "rhombus-node-mcp",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for Rhombus API",
5
+ "keywords": [
6
+ "ai",
7
+ "rhombus",
8
+ "mcp",
9
+ "modelcontextprotocol"
10
+ ],
11
+ "homepage": "https://modelcontextprotocol.io",
12
+ "bugs": {
13
+ "url": "https://github.com/modelcontextprotocol/servers/issues"
14
+ },
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/modelcontextprotocol/servers.git"
18
+ },
19
+ "license": "MIT",
20
+ "author": "Model Context Protocol (https://modelcontextprotocol.io)",
21
+ "type": "module",
22
+ "main": "dist/index.js",
23
+ "bin": {
24
+ "mcp-server-rhombus": "dist/index.js"
25
+ },
26
+ "scripts": {
27
+ "build": "tsc && chmod +x dist/*.js",
28
+ "prepare": "npm run build",
29
+ "watch": "tsc --watch"
30
+ },
31
+ "files": [
32
+ "dist"
33
+ ],
34
+ "dependencies": {
35
+ "@modelcontextprotocol/sdk": "^1.9.0",
36
+ "zod": "^3.24.2"
37
+ },
38
+ "devDependencies": {
39
+ "@types/node": "^22.14.0",
40
+ "typescript": "^5.8.3",
41
+ "prettier": "3.5.3"
42
+ },
43
+ "engines": {
44
+ "node": ">=18"
45
+ }
46
+ }