Skip to main content
Glama
BaseToolManager.ts8.44 kB
/** * Base utility class for tool managers with enhanced type safety */ import { getErrorMessage } from "@/utils/error.js"; import { WordPressId, createWordPressId, DeepReadonly, Result, createSuccess, createError } from "@/types/enhanced.js"; interface EnhancedError extends Error { originalError?: unknown; operation?: string; context?: Record<string, unknown> | undefined; timestamp?: Date; } export interface ParameterValidationRule<T = unknown> { readonly key: string; readonly required: boolean; readonly type?: string; readonly validator?: (value: unknown) => value is T; readonly transformer?: (value: unknown) => T; readonly errorMessage?: string; } export interface CacheKeyOptions { readonly namespace?: string; readonly includeTimestamp?: boolean; readonly customHasher?: (params: Record<string, unknown>) => string; } export class BaseToolUtils { /** * Validate required parameters with enhanced type safety */ static validateParams<T extends Record<string, unknown>>( params: unknown, rules: readonly ParameterValidationRule[], ): Result<T, Error> { if (!params || typeof params !== "object" || Array.isArray(params)) { return createError(new ValidationError("Parameters must be a non-null object", "params", params)); } const typedParams = params as Record<string, unknown>; const errors: Error[] = []; for (const rule of rules) { const value = typedParams[rule.key]; const exists = rule.key in typedParams; // Check required fields if (rule.required && (!exists || value === undefined || value === null)) { errors.push( new ValidationError(rule.errorMessage || `Missing required parameter: ${rule.key}`, rule.key, value), ); continue; } // Skip optional fields that don't exist if (!rule.required && !exists) { continue; } // Type validation if (rule.type && typeof value !== rule.type) { errors.push( new ValidationError( `Parameter ${rule.key} must be of type ${rule.type}, got ${typeof value}`, rule.key, value, ), ); continue; } // Custom validation if (rule.validator && !rule.validator(value)) { errors.push( new ValidationError(rule.errorMessage || `Parameter ${rule.key} failed validation`, rule.key, value), ); continue; } // Transform value if transformer provided if (rule.transformer) { try { typedParams[rule.key] = rule.transformer(value); } catch (_error) { errors.push( new ValidationError( `Failed to transform parameter ${rule.key}: ${getErrorMessage(_error)}`, rule.key, value, ), ); } } } if (errors.length > 0) { return createError(errors[0]); // Return first error for simplicity } return createSuccess(typedParams as T); } /** * Validate ID parameter with WordPress ID branding */ static validateId(id: unknown, name = "id"): Result<WordPressId, Error> { try { const numId = Number(id); if (!Number.isInteger(numId) || numId <= 0) { return createError(new ValidationError(`Invalid ${name}: must be a positive integer`, name, id)); } return createSuccess(createWordPressId(numId)); } catch (_error) { return createError(new ValidationError(`Invalid ${name}: ${getErrorMessage(_error)}`, name, id)); } } /** * Validate multiple IDs at once */ static validateIds(ids: unknown[], name = "ids"): Result<readonly WordPressId[], Error> { const validatedIds: WordPressId[] = []; for (let i = 0; i < ids.length; i++) { const result = this.validateId(ids[i], `${name}[${i}]`); if (!result.success) { return result; } validatedIds.push(result.data); } return createSuccess(validatedIds as readonly WordPressId[]); } /** * Handle errors consistently with enhanced error context */ static handleError(error: unknown, operation: string, context?: Record<string, unknown> | undefined): Error { const errorMessage = getErrorMessage(error); const enhancedMessage = `Error in ${operation}: ${errorMessage}`; const enhancedError = new Error(enhancedMessage) as EnhancedError; enhancedError.originalError = error; enhancedError.operation = operation; enhancedError.context = context; enhancedError.timestamp = new Date(); return enhancedError; } /** * Generate cache keys with enhanced options */ static generateCacheKey( operation: string, params: DeepReadonly<Record<string, unknown>>, options: CacheKeyOptions = {}, ): string { const { namespace = "wp", includeTimestamp = false, customHasher } = options; const site = params.site || "default"; let paramStr: string; if (customHasher) { paramStr = customHasher(params as Record<string, unknown>); } else { paramStr = Object.entries(params) .filter(([key]) => key !== "site") .sort(([a], [b]) => a.localeCompare(b)) .map(([key, value]) => `${key}:${JSON.stringify(value)}`) .join("|"); } let cacheKey = `${namespace}:${site}:${operation}:${paramStr}`; if (includeTimestamp) { const timestamp = Math.floor(Date.now() / 1000); cacheKey += `:${timestamp}`; } return cacheKey; } /** * Format consistent success response messages */ static formatSuccessMessage(operation: string, details?: string, count?: number): string { let message = operation; if (count !== undefined) { message += ` (${count} ${count === 1 ? "item" : "items"})`; } if (details) { message += `: ${details}`; } else { message += " completed successfully"; } return message; } /** * Validate string parameters with enhanced checks */ static validateString( value: unknown, name: string, options: { readonly required?: boolean; readonly minLength?: number; readonly maxLength?: number; readonly pattern?: RegExp; readonly allowEmpty?: boolean; } = {}, ): Result<string, Error> { const { required = true, minLength, maxLength, pattern, allowEmpty = false } = options; if (value === undefined || value === null) { if (required) { return createError(new ValidationError(`${name} is required`, name, value)); } return createSuccess(""); } if (typeof value !== "string") { return createError(new ValidationError(`${name} must be a string`, name, value)); } if (!allowEmpty && value.trim().length === 0) { return createError(new ValidationError(`${name} cannot be empty`, name, value)); } if (minLength !== undefined && value.length < minLength) { return createError(new ValidationError(`${name} must be at least ${minLength} characters long`, name, value)); } if (maxLength !== undefined && value.length > maxLength) { return createError(new ValidationError(`${name} must be no more than ${maxLength} characters long`, name, value)); } if (pattern && !pattern.test(value)) { return createError(new ValidationError(`${name} does not match required pattern`, name, value)); } return createSuccess(value); } /** * Type-safe parameter extraction */ static extractParam<T>(params: DeepReadonly<Record<string, unknown>>, key: string, defaultValue?: T): T | undefined { const value = params[key]; return value !== undefined ? (value as T) : defaultValue; } /** * Safe array extraction with type validation */ static extractArray<T>( params: DeepReadonly<Record<string, unknown>>, key: string, itemValidator?: (item: unknown) => item is T, ): readonly T[] { const value = params[key]; if (!Array.isArray(value)) { return []; } if (itemValidator) { return value.filter(itemValidator); } return value as readonly T[]; } } // Custom ValidationError class for enhanced error handling class ValidationError extends Error { constructor( message: string, public readonly field: string, public readonly value: unknown, ) { super(message); this.name = "ValidationError"; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/docdyhr/mcp-wordpress'

If you have feedback or need assistance with the MCP directory API, please join our Discord server