UNPKG

rhombus-node-mcp

Version:
189 lines (188 loc) 6.66 kB
import { z } from "zod"; import fs from "fs"; import path from "path"; import { DateTime } from "luxon"; import { filterIncludedFields, applyFilterBy } from "./filtering-utils.js"; export { INCLUDE_FIELDS_ARG, FILTER_BY_ARG, filterIncludedFields, applyFilterBy, zodToDotNotationPaths, createFilteringProxy } from "./filtering-utils.js"; export function generateRandomString(length) { const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; let result = ""; const charactersLength = characters.length; for (let i = 0; i < length; i++) { result += characters.charAt(Math.floor(Math.random() * charactersLength)); } return result; } export const RequestModifiers = z .object({ headers: z.optional(z.record(z.string(), z.string())), query: z.optional(z.record(z.string(), z.string())), }) .optional(); export function extractFromToolExtra(_extra) { const extra = _extra; return { requestModifiers: extra._meta?.requestModifiers, sessionId: extra.sessionId, }; } // --------------------------------------------------------------------------- // File utilities // --------------------------------------------------------------------------- /** * Get all file paths in a directory in a directory * * i.e. * foo: * - folder1: * - har * - gar * - bar * - lar * * returns: ["folder1/har", "folder1/gar", "bar", "lar"] */ export function getFilePathsInDirectory(dirPath) { const filePaths = []; const fileNames = fs.readdirSync(dirPath); for (const fileName of fileNames) { const pathToAdd = path.join(dirPath, fileName); const stats = fs.lstatSync(pathToAdd); if (stats.isDirectory()) { filePaths.push(...getFilePathsInDirectory(pathToAdd)); } else if (stats.isFile()) { filePaths.push(pathToAdd); } } return filePaths; } // --------------------------------------------------------------------------- // Tool content helpers // --------------------------------------------------------------------------- /** * Returns an object in the form expected by `server.tool` */ export function createToolTextContent(content) { return { content: [ { type: "text", text: content, }, ], }; } /** * Returns structured content expected by `server.tool`. * Optionally applies programmatic field projection (includeFields) and * row filtering (filterBy) before serialising. * * The generic type T allows for type safety. Set T to the output schema of the * tool and your content will be type-checked. * * filterBy is applied first (prune rows), then includeFields (prune columns), * which minimises output size optimally. */ export function createToolStructuredContent(content, opts) { // biome-ignore lint/suspicious/noExplicitAny: intentional runtime manipulation let result = content; if (opts?.filterBy?.length) { result = applyFilterBy(result, opts.filterBy) ?? result; } if (opts?.includeFields?.length) { result = filterIncludedFields(result, opts.includeFields) ?? result; } return { content: [ { type: "text", text: JSON.stringify(result), }, ], structuredContent: result, }; } // --------------------------------------------------------------------------- // removeNullFields // --------------------------------------------------------------------------- /** * Recursively removes fields with null values from a JavaScript object. * If a nested object becomes empty after removing nulls, it will also be removed. * * @param {unknown} obj The object or array to clean. * @returns {object | unknown[] | undefined} The cleaned object/array, or undefined if the input was null/undefined or an empty object/array resulted. */ export function removeNullFields(obj) { if (obj === null || obj === undefined) { return undefined; } if (Array.isArray(obj)) { const cleanedArray = []; for (const item of obj) { const cleanedItem = removeNullFields(item); if (cleanedItem !== undefined) { cleanedArray.push(cleanedItem); } } return cleanedArray.length > 0 ? cleanedArray : undefined; } if (typeof obj !== "object") { return obj; // Cast to any for primitive types } const cleanedObject = {}; for (const key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { const value = obj[key]; if (value === null) { continue; } if (typeof value === "object") { const cleanedValue = removeNullFields(value); if (cleanedValue !== undefined) { cleanedObject[key] = cleanedValue; } } else { cleanedObject[key] = value; } } } return Object.keys(cleanedObject).length > 0 ? cleanedObject : undefined; } // --------------------------------------------------------------------------- // formatTimestamp // --------------------------------------------------------------------------- /** * Formats a timestamp in milliseconds to a human-readable date string * Format: "February 24, 2025 at 3:23 PM" * * @param timestampMs - Timestamp in milliseconds * @param timeZone - Optional IANA timezone string (defaults to "America/Los_Angeles") * @returns Formatted date string */ export function formatTimestamp(timestampMs, timeZone) { return DateTime.fromMillis(timestampMs) .setZone(timeZone || "America/Los_Angeles") .toFormat("MMMM d, yyyy 'at' h:mm:ss a", { locale: "en-US", }); } /** * Formats a timestamp in milliseconds to an ISO 8601 string with timezone offset. * Format: "2025-04-21T10:57:00.000-07:00" * * Use this when returning timestamps in tool output schemas so the offset is preserved * (rather than the bare "Z" produced by Date.prototype.toISOString()). * * @param timestampMs - Timestamp in milliseconds * @param timeZone - Optional IANA timezone string (defaults to "America/Los_Angeles") * @returns ISO 8601 string with offset, or undefined if input is null/undefined */ export function formatIsoWithOffset(timestampMs, timeZone) { if (timestampMs === null || timestampMs === undefined) return undefined; return (DateTime.fromMillis(timestampMs) .setZone(timeZone || "America/Los_Angeles") .toISO() ?? undefined); }