/**
* Utility functions for NDB MCP Server
*/
/**
* Format individual values appropriately
*/
function formatValue(value: any): string {
if (value === null || value === undefined) {
return 'N/A';
}
if (typeof value === 'boolean') {
return value ? 'Yes' : 'No';
}
if (typeof value === 'string') {
// Format timestamps
if (value.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/)) {
try {
return new Date(value).toLocaleString();
} catch {
return value;
}
}
return value;
}
if (Array.isArray(value)) {
if (value.length === 0) {
return '[]';
}
// If array contains only primitives, join as before
if (value.every(item => (typeof item !== 'object' || item === null))) {
if (value.length <= 3) {
return `[${value.join(', ')}]`;
}
return `[${value.length} items: ${value.slice(0, 2).join(', ')}...]`;
}
// If array contains objects, pretty print each item
return '[\n' + value.map((item, idx) => {
if (typeof item === 'object' && item !== null) {
// Indent nested objects for readability
const formatted = JSON.stringify(item, null, 2).replace(/^/gm, ' ');
return ` [${idx}]:\n${formatted}`;
}
return ` [${idx}]: ${String(item)}`;
}).join(',\n') + '\n]';
}
if (typeof value === 'object') {
const keys = Object.keys(value);
if (keys.length === 0) {
return '{}';
}
if (keys.length <= 3) {
return `{${keys.join(', ')}}`;
}
return `{${keys.length} properties}`;
}
return String(value);
}
/**
* Parse JSON argument from tool call
*/
export function parseJsonArgument(arg: any): any {
if (typeof arg === 'string') {
try {
return JSON.parse(arg);
} catch {
return arg;
}
}
return arg;
}
/**
* Validate UUID format
*/
export function isValidUUID(uuid: string): boolean {
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
return uuidRegex.test(uuid);
}
/**
* Sanitize string for logging (remove sensitive data)
*/
export function sanitizeForLogging(str: string): string {
// Remove potential passwords, tokens, etc.
return str
.replace(/password['":\s]*['"](.*?)['"]/, 'password":"***"')
.replace(/token['":\s]*['"](.*?)['"]/, 'token":"***"')
.replace(/secret['":\s]*['"](.*?)['"]/, 'secret":"***"')
.replace(/key['":\s]*['"](.*?)['"]/, 'key":"***"');
}
/**
* Convert bytes to human readable format
*/
export function formatBytes(bytes: number): string {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
/**
* Convert duration to human readable format
*/
export function formatDuration(startTime: string, endTime?: string): string {
const start = new Date(startTime);
const end = endTime ? new Date(endTime) : new Date();
const durationMs = end.getTime() - start.getTime();
if (durationMs < 1000) {
return `${durationMs}ms`;
}
const seconds = Math.floor(durationMs / 1000);
if (seconds < 60) {
return `${seconds}s`;
}
const minutes = Math.floor(seconds / 60);
if (minutes < 60) {
return `${minutes}m ${seconds % 60}s`;
}
const hours = Math.floor(minutes / 60);
if (hours < 24) {
return `${hours}h ${minutes % 60}m`;
}
const days = Math.floor(hours / 24);
return `${days}d ${hours % 24}h`;
}
/**
* Extract error message from various error types
*/
export function extractErrorMessage(error: any): string {
if (typeof error === 'string') {
return error;
}
if (error?.message) {
return error.message;
}
if (error?.response?.data?.message) {
return error.response.data.message;
}
if (error?.response?.statusText) {
return `HTTP ${error.response.status}: ${error.response.statusText}`;
}
return 'Unknown error occurred';
}
/**
* Advanced filtering utility for arrays of objects, supporting nested properties (e.g. versions.name, databases.length)
* Used by list_dbservers, list_profiles, and can be reused elsewhere.
*
* @param {Array} arr - Array of objects to filter
* @param {string} valueType - Comma-separated list of fields (e.g. "name,versions.length,versions.published")
* @param {string} value - Comma-separated list of values (e.g. "*prod*,>=2,true")
* @returns {Array} Filtered array
*/
interface NestedObject {
[key: string]: any;
name?: string;
value?: any;
properties?: NestedObject[];
tags?: { tagName: string; value: any }[];
}
function parseValue(value: string): string[][] {
return value.split('&').map(g => g.trim().split('|').map(v => v.trim()));
}
function compareValue(itemVal: any, orVal: string): boolean {
if (orVal.startsWith('!')) {
return String(itemVal) !== orVal.substring(1);
}
if (orVal.startsWith('>=')) {
return itemVal >= Number(orVal.substring(2));
}
if (orVal.startsWith('<=')) {
return itemVal <= Number(orVal.substring(2));
}
if (orVal.startsWith('>')) {
return itemVal > Number(orVal.substring(1));
}
if (orVal.startsWith('<')) {
return itemVal < Number(orVal.substring(1));
}
if (orVal.startsWith('*') && orVal.endsWith('*')) {
const search = orVal.slice(1, -1).toLowerCase();
return String(itemVal).toLowerCase().includes(search);
}
return String(itemVal) === orVal;
}
function matchNested(item: any, path: string[], value: string): boolean {
if (path.length === 0) return false;
const key = path[0];
const rest = path.slice(1);
const andGroups = parseValue(value);
// Si la clé n'existe pas, on retourne true (car la propriété ne peut pas correspondre aux valeurs exclues)
if (item[key] === undefined) return true;
if (Array.isArray(item)) {
return item.some((el: any) => matchNested(el, path, value));
}
if ((key === 'properties' || key === 'tags') && rest.length > 0) {
const nameKey = key === 'properties' ? 'name' : 'tagName';
const nameToFind = rest[0];
const valuePath = rest.slice(1);
if (!Array.isArray(item[key])) return true; // Si ce n'est pas un tableau, on ne peut pas filtrer, donc on garde
// Si la propriété imbriquée n'existe pas, on retourne true
const found = item[key].find((obj: any) => obj[nameKey]?.toLowerCase() === nameToFind.toLowerCase());
if (!found) return true;
if (valuePath.length === 0) {
const objVal = found.value;
return andGroups.every(andGroup => andGroup.some(orVal => compareValue(objVal, orVal)));
} else {
return matchNested(found, valuePath, value);
}
}
if ((rest.length === 1 && rest[0] === 'length' && Array.isArray(item[key])) || (rest.length === 0 && Array.isArray(item[key]))) {
const count = item[key].length;
return andGroups.every(andGroup => andGroup.some(orVal => compareValue(count, orVal)));
}
if (rest.length === 0) {
const itemVal = item?.[key];
return andGroups.every(andGroup => andGroup.some(orVal => compareValue(itemVal, orVal)));
} else {
return matchNested(item?.[key], rest, value);
}
}
export function advancedFilter(arr: any[], valueType?: string, value?: string): any[] {
if (!Array.isArray(arr) || !valueType || !value) return arr;
const keys = valueType.split(',').map((k: string) => k.trim());
const values = value.split(',').map((v: string) => v.trim());
return arr.filter((item: any) => {
let keep = true;
keys.forEach((key: string, idx: number) => {
const val = values[idx];
if (key.includes('.')) {
const path = key.split('.');
if (!matchNested(item, path, val)) keep = false;
} else {
const andGroups = parseValue(val);
if (item[key] === undefined) return; // Si la clé n'existe pas, on ne filtre pas
if (Array.isArray(item[key])) {
const count = item[key].length;
if (!andGroups.every(andGroup => andGroup.some(orVal => compareValue(count, orVal)))) keep = false;
} else {
const itemVal = item[key];
if (!andGroups.every(andGroup => andGroup.some(orVal => compareValue(itemVal, orVal)))) keep = false;
}
}
});
return keep;
});
}
/**
* Validate and collect missing parameters using Zod schema
*
* @param args - The arguments to validate
* @param inputFile - The input file containing engine-specific properties
* @param schema - The Zod schema to validate against
* @returns An array of missing parameters
*/
export async function validateAndCollectMissingParams(args: any, inputFile: any, schema: any) {
const missing = [];
// Use the provided schema to check required fields
const result = schema.safeParse(args);
if (!result.success) {
for (const err of result.error.errors) {
missing.push({
parameter: err.path.join('.'),
description: err.message,
type: 'required'
});
}
}
// Custom cross-field validation: nodeCount === nodes.length
if (
typeof args.nodeCount === 'number' &&
Array.isArray(args.nodes)
) {
if (args.nodeCount !== args.nodes.length) {
missing.push({
parameter: 'nodeCount',
description: `nodeCount (${args.nodeCount}) does not match number of nodes (${args.nodes.length})`,
type: 'cross_field'
});
}
}
// Check specific engine properties from inputFile
if (inputFile?.properties) {
for (const prop of inputFile.properties) {
if (prop.required === 'Y') {
const hasValue = args.actionArguments?.some((arg: any) => arg.name === prop.name);
if (!hasValue) {
missing.push({
parameter: prop.name,
description: prop.description || prop.display_name,
type: 'engine_specific',
engineProperty: true,
defaultValue: prop.default_value
});
}
}
}
}
return missing;
}