rhombus-node-mcp 0.1.13 → 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 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
- "johnrhombusdocker/mcp-server-rhombus"
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 `johnrhombusdocker/mcp-server-rhombus`. Make sure to update this in your configuration!
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 getTools from "./tools/getTools.js";
4
+ import { serverInit } from "./createServer.js";
6
5
  import { logger } from "./logger.js";
7
- import getResources from "./resources/getResources.js";
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
- if (!RHOMBUS_API_KEY) {
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
- const resources = await getResources();
26
- logger.info(`🛠️ Registering ${resources.length} resources`);
27
- for (const resource of resources) {
28
- resource.create(server);
29
- logger.debug(`🔧 Registered resource ${resource.name}`);
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
- const tools = await getTools();
32
- logger.info(`🛠️ Registering ${tools.length} tools`);
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
@@ -5,7 +5,7 @@ log4js.configure({
5
5
  mcp: {
6
6
  type: "file",
7
7
  filename: path.resolve(process.env.LOG_FOLDER ?? process.cwd(), "rhombus-node-mcp.log"),
8
- maxLogSize: "1K",
8
+ maxLogSize: "1M",
9
9
  layout: {
10
10
  type: "basic",
11
11
  },
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 = undefined) {
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
  }
@@ -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
- `, createToolArgs({
52
+ `, {
54
53
  ...ClipsArgs.shape,
55
- }), async ({ requestModifiers, ...args }) => {
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, headers) {
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, headers);
21
+ const response = await postApi("/camera/createVideoWall", body, requestModifiers);
23
22
  return response;
24
23
  }
25
- async function handleCreateVideoWallRequest(videoWallCreateOptions, headers) {
24
+ async function handleCreateVideoWallRequest(videoWallCreateOptions, requestModifiers) {
26
25
  let text = "Unable to create video wall!";
27
- logger.error("🔨 Creating video wall");
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.error("Creating video wall with options: ", JSON.stringify(videoWallCreateOptions));
42
- text = JSON.stringify(await createVideoWall(videoWallCreateOptions, headers));
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(createToolArgs({
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
- })), async ({ entityType, videoWallCreateOptions, requestModifiers, confirmationId }) => {
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 { createToolArgs, createToolTextContent, removeNullFields, } from "../../../util.js";
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(createToolArgs({
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
- })), async ({ cameraUuid, timestampMs, requestType, configUpdate, requestModifiers, confirmationId, }) => {
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.`, createToolArgs({
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
- }), async ({ entityTypes, requestModifiers }) => {
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 { createToolArgs } from "../util.js";
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", createToolArgs({
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
- }), async ({ eventType, locationUuid, accessControlledDoorUuid, requestModifiers }) => {
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
  {
@@ -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
- createToolArgs({
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
- }), async ({ requestModifiers, requestType, args }) => {
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.", createToolArgs({}), async ({ requestModifiers }) => {
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.`, createToolArgs({
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
- }), async ({ action, requestModifiers }) => {
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.`, createToolArgs({
47
+ The output is provided in JSON format.`, {
49
48
  ...PolicyAlertsArgs.shape,
50
- }), async ({ requestModifiers, ...args }) => {
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(createToolArgs({
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
- })), async ({ cameraUuids, requestModifiers, confirmationId }) => {
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: [
@@ -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 STATIC_ARGS = {
14
- requestModifiers: z
15
- .optional(z.object({
16
- headers: z.nullable(z.record(z.string(), z.string())),
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.13",
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",