Skip to main content
Glama
validation.ts10.9 kB
// Centralized validation utilities for MCP Sigmund // This module consolidates all validation logic to reduce duplication import { ValidationError } from './error-handler.js'; // Common validation patterns export class ValidationPatterns { static readonly IDENTIFIER = /^[a-zA-Z0-9_-]+$/; static readonly DATE_ISO = /^\d{4}-\d{2}-\d{2}$/; static readonly HOSTNAME = /^[a-zA-Z0-9.-]+$/; static readonly CATEGORY = /^[a-zA-Z0-9\s&-_]+$/; static readonly POSTGRES_URL = /^postgres(ql)?:\/\/([^:]+):([^@]+)@([^:]+):(\d+)\/(.+)$/; } // Base validation class export abstract class BaseValidator { protected static validateRequired(value: unknown, fieldName: string): void { if (value === undefined || value === null || value === '') { throw new ValidationError(`${fieldName} is required`); } } protected static validateType( value: unknown, expectedType: string, fieldName: string ): void { if (typeof value !== expectedType) { throw new ValidationError(`${fieldName} must be a ${expectedType}`); } } protected static validatePattern( value: string, pattern: RegExp, fieldName: string, description: string ): void { if (!pattern.test(value)) { throw new ValidationError(`${fieldName} ${description}`); } } protected static validateRange( value: number, min: number, max: number, fieldName: string ): void { if (value < min || value > max) { throw new ValidationError( `${fieldName} must be between ${min} and ${max}` ); } } } // String validation utilities export class StringValidator extends BaseValidator { static validateString( value: unknown, fieldName: string, maxLength: number = 255 ): string { this.validateRequired(value, fieldName); this.validateType(value, 'string', fieldName); const stringValue = value as string; const sanitized = stringValue .replace(/[<>"'&]/g, '') // Remove HTML/XML characters .replace(/[;()]/g, '') // Remove SQL injection characters .trim() .substring(0, maxLength); if (sanitized.length === 0) { throw new ValidationError( `${fieldName} cannot be empty after sanitization` ); } return sanitized; } static validateIdentifier(value: unknown, fieldName: string): string { const sanitized = this.validateString(value, fieldName, 100); this.validatePattern( sanitized, ValidationPatterns.IDENTIFIER, fieldName, 'can only contain letters, numbers, hyphens, and underscores' ); return sanitized; } static validateCategory(value: unknown, fieldName: string): string { const sanitized = this.validateString(value, fieldName, 100); this.validatePattern( sanitized, ValidationPatterns.CATEGORY, fieldName, 'can only contain letters, numbers, spaces, ampersands, hyphens, and underscores' ); return sanitized; } static validateHostname(value: unknown, fieldName: string): string { const sanitized = this.validateString(value, fieldName, 255); this.validatePattern( sanitized, ValidationPatterns.HOSTNAME, fieldName, 'has invalid format' ); return sanitized; } } // Numeric validation utilities export class NumericValidator extends BaseValidator { static validateNumber( value: unknown, fieldName: string, min?: number, max?: number ): number { this.validateRequired(value, fieldName); const num = Number(value); if (isNaN(num)) { throw new ValidationError(`${fieldName} must be a valid number`); } if (min !== undefined && max !== undefined) { this.validateRange(num, min, max, fieldName); } else if (min !== undefined) { if (num < min) { throw new ValidationError(`${fieldName} must be at least ${min}`); } } else if (max !== undefined) { if (num > max) { throw new ValidationError(`${fieldName} must be at most ${max}`); } } return num; } static validateInteger( value: unknown, fieldName: string, min?: number, max?: number ): number { const num = this.validateNumber(value, fieldName, min, max); if (!Number.isInteger(num)) { throw new ValidationError(`${fieldName} must be an integer`); } return num; } static validatePort(value: unknown, fieldName: string): number { return this.validateInteger(value, fieldName, 1, 65535); } } // Date validation utilities export class DateValidator extends BaseValidator { static validateDate(value: unknown, fieldName: string): string { this.validateRequired(value, fieldName); this.validateType(value, 'string', fieldName); const dateString = value as string; this.validatePattern( dateString, ValidationPatterns.DATE_ISO, fieldName, 'must be in YYYY-MM-DD format' ); const date = new Date(dateString); if (isNaN(date.getTime())) { throw new ValidationError(`${fieldName} is not a valid date`); } return dateString; } static validateDateRange(dateFrom?: string, dateTo?: string): void { if (dateFrom && dateTo) { const fromDate = new Date(this.validateDate(dateFrom, 'dateFrom')); const toDate = new Date(this.validateDate(dateTo, 'dateTo')); if (fromDate > toDate) { throw new ValidationError('dateFrom cannot be after dateTo'); } } else if (dateFrom) { this.validateDate(dateFrom, 'dateFrom'); } else if (dateTo) { this.validateDate(dateTo, 'dateTo'); } } } // URL validation utilities export class URLValidator extends BaseValidator { static validatePostgreSQLURL(value: unknown, fieldName: string): string { this.validateRequired(value, fieldName); this.validateType(value, 'string', fieldName); const urlString = value as string; this.validatePattern( urlString, ValidationPatterns.POSTGRES_URL, fieldName, 'must be a valid PostgreSQL connection string' ); try { const url = new URL(urlString); // Validate protocol if (!url.protocol.startsWith('postgres')) { throw new ValidationError( `${fieldName} must use postgresql:// protocol` ); } // Validate required components if (!url.hostname) { throw new ValidationError(`${fieldName} must include hostname`); } if (!url.port) { throw new ValidationError(`${fieldName} must include port`); } NumericValidator.validatePort(url.port, 'port'); if (!url.pathname || url.pathname === '/') { throw new ValidationError(`${fieldName} must include database name`); } if (!url.username) { throw new ValidationError(`${fieldName} must include username`); } if (!url.password) { throw new ValidationError(`${fieldName} must include password`); } // Validate hostname and database name StringValidator.validateHostname(url.hostname, 'hostname'); const dbName = url.pathname.substring(1); StringValidator.validateIdentifier(dbName, 'database name'); StringValidator.validateIdentifier(url.username, 'username'); } catch (error) { if (error instanceof ValidationError) { throw error; } throw new ValidationError( `Invalid ${fieldName}: ${error instanceof Error ? error.message : 'Unknown error'}` ); } return urlString; } } // Banking-specific validation utilities export class BankingValidator extends BaseValidator { static validateQuery(query: string): void { const validQueries = [ 'accounts', 'transactions', 'balance', 'overview', 'spending_analysis', 'cashflow_analysis', 'providers', ]; if (!validQueries.includes(query)) { throw new ValidationError( `Invalid query: ${query}. Valid queries are: ${validQueries.join(', ')}` ); } } static validateDataOperation(operation: string): void { const validOperations = [ 'get_stats', 'validate_data', 'export_data', 'cleanup_data', ]; if (!validOperations.includes(operation)) { throw new ValidationError( `Invalid operation: ${operation}. Valid operations are: ${validOperations.join(', ')}` ); } } static validateLimit(limit?: number): void { if (limit !== undefined) { NumericValidator.validateNumber(limit, 'limit', 1, 10000); } } static validateMonths(months?: number): void { if (months !== undefined) { NumericValidator.validateNumber(months, 'months', 1, 60); } } static validateProvider(provider?: string): void { if (provider !== undefined) { StringValidator.validateIdentifier(provider, 'provider'); } } } // Composite validation functions for common use cases export class CompositeValidator { static validateBankingQueryParams(params: { limit?: number; dateFrom?: string; dateTo?: string; months?: number; category?: string; accountId?: string; userId?: string; provider?: string; }): { limit?: number; dateFrom?: string; dateTo?: string; months?: number; category?: string; accountId?: string; userId?: string; provider?: string; } { const validated: any = {}; if (params.limit !== undefined) { validated.limit = NumericValidator.validateNumber( params.limit, 'limit', 1, 10000 ); } if (params.dateFrom !== undefined) { validated.dateFrom = DateValidator.validateDate( params.dateFrom, 'dateFrom' ); } if (params.dateTo !== undefined) { validated.dateTo = DateValidator.validateDate(params.dateTo, 'dateTo'); } if (params.months !== undefined) { validated.months = NumericValidator.validateNumber( params.months, 'months', 1, 60 ); } if (params.category !== undefined) { validated.category = StringValidator.validateCategory( params.category, 'category' ); } if (params.accountId !== undefined) { validated.accountId = StringValidator.validateIdentifier( params.accountId, 'accountId' ); } if (params.userId !== undefined) { validated.userId = StringValidator.validateIdentifier( params.userId, 'userId' ); } if (params.provider !== undefined) { validated.provider = StringValidator.validateIdentifier( params.provider, 'provider' ); } // Validate date range if both dates are provided DateValidator.validateDateRange(validated.dateFrom, validated.dateTo); return validated; } }

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/radup/mcp-sigmund'

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