Skip to main content
Glama
sascodiego

MCP Vibe Coding Knowledge Graph

by sascodiego
MCPInputValidator.js34.9 kB
import { logger } from '../utils/logger.js'; import Joi from 'joi'; /** * CONTEXT: Comprehensive MCP input validation and sanitization system * REASON: Secure and validate all inputs to MCP tools and handlers * CHANGE: Multi-layered validation with schema validation, security checks, and sanitization * PREVENTION: Injection attacks, data corruption, invalid inputs, and security vulnerabilities */ export class MCPInputValidator { constructor(config = {}) { this.config = { maxStringLength: config.maxStringLength || 100000, maxArrayLength: config.maxArrayLength || 10000, maxObjectDepth: config.maxObjectDepth || 10, maxObjectKeys: config.maxObjectKeys || 1000, enableSanitization: config.enableSanitization !== false, strictMode: config.strictMode !== false, enableLogging: config.enableLogging !== false, rateLimiting: { enabled: config.rateLimiting?.enabled || false, maxRequestsPerMinute: config.rateLimiting?.maxRequestsPerMinute || 100, maxRequestsPerHour: config.rateLimiting?.maxRequestsPerHour || 1000 }, ...config }; // Validation schemas for each MCP tool this.toolSchemas = new Map(); this.customValidators = new Map(); this.sanitizationRules = new Map(); // Rate limiting tracking this.requestCounts = new Map(); this.requestCountsHourly = new Map(); // Statistics this.stats = { totalValidations: 0, validInputs: 0, invalidInputs: 0, sanitizedInputs: 0, blockedInputs: 0, rateLimitedRequests: 0 }; this.initializeDefaultSchemas(); this.initializeDefaultValidators(); this.initializeDefaultSanitizers(); this.setupRateLimitCleanup(); logger.info('MCPInputValidator initialized', { strictMode: this.config.strictMode, rateLimitingEnabled: this.config.rateLimiting.enabled, schemasCount: this.toolSchemas.size }); } /** * Main validation entry point for MCP tool calls */ async validateToolInput(toolName, args, clientInfo = {}) { const startTime = Date.now(); this.stats.totalValidations++; try { // Rate limiting check if (this.config.rateLimiting.enabled) { const rateLimitResult = await this.checkRateLimit(clientInfo); if (!rateLimitResult.allowed) { this.stats.rateLimitedRequests++; this.stats.blockedInputs++; return { isValid: false, sanitized: false, errors: ['Rate limit exceeded'], warnings: [], originalArgs: args, validatedArgs: null, metadata: { validationTime: Date.now() - startTime, riskLevel: 'HIGH', rateLimited: true } }; } } const result = { isValid: true, sanitized: false, errors: [], warnings: [], originalArgs: args, validatedArgs: args, metadata: { validationTime: 0, riskLevel: 'LOW', rateLimited: false } }; // Step 1: Schema validation await this.performSchemaValidation(toolName, args, result); if (!result.isValid && this.config.strictMode) { this.stats.invalidInputs++; return result; } // Step 2: Security validation await this.performSecurityValidation(args, result); if (!result.isValid && this.config.strictMode) { this.stats.blockedInputs++; return result; } // Step 3: Custom validation await this.performCustomValidation(toolName, args, result); // Step 4: Sanitization if (this.config.enableSanitization) { await this.performSanitization(toolName, result); } // Step 5: Final validation of sanitized data if (result.sanitized) { await this.performFinalValidation(toolName, result); } // Update statistics and metadata const validationTime = Date.now() - startTime; result.metadata.validationTime = validationTime; if (result.isValid) { this.stats.validInputs++; if (result.sanitized) { this.stats.sanitizedInputs++; } } else { this.stats.invalidInputs++; } if (this.config.enableLogging) { logger.debug('Input validation completed', { toolName, isValid: result.isValid, sanitized: result.sanitized, errorCount: result.errors.length, warningCount: result.warnings.length, validationTime: `${validationTime}ms`, riskLevel: result.metadata.riskLevel }); } return result; } catch (error) { this.stats.invalidInputs++; logger.error('Input validation failed', { toolName, error: error.message, args: this.sanitizeArgsForLogging(args) }); return { isValid: false, sanitized: false, errors: [`Validation error: ${error.message}`], warnings: [], originalArgs: args, validatedArgs: null, metadata: { validationTime: Date.now() - startTime, riskLevel: 'HIGH', rateLimited: false } }; } } /** * Perform schema validation using Joi */ async performSchemaValidation(toolName, args, result) { const schema = this.toolSchemas.get(toolName); if (!schema) { result.warnings.push(`No schema defined for tool: ${toolName}`); return; } try { const { error, value, warning } = schema.validate(args, { abortEarly: false, stripUnknown: true, convert: true }); if (error) { result.errors.push(...error.details.map(detail => detail.message)); result.isValid = false; result.metadata.riskLevel = 'MEDIUM'; } else { result.validatedArgs = value; if (warning) { result.warnings.push(...warning.details.map(detail => detail.message)); } } } catch (validationError) { result.errors.push(`Schema validation failed: ${validationError.message}`); result.isValid = false; result.metadata.riskLevel = 'HIGH'; } } /** * Perform security validation */ async performSecurityValidation(args, result) { // Check for injection patterns const injectionPatterns = [ /(?:;|\|\||&&|\$\(|\`)/, // Command injection /<script|javascript:|vbscript:|onload=/i, // XSS patterns /(\%27)|(\')|(\')|(\-\-)|(\%23)|(#)/, // SQL injection /(\bEXEC\b|\bEVAL\b|\bSYSTEM\b)/i, // Dangerous functions /\${.*}|<%.*%>/, // Template injection ]; await this.validateRecursive(args, (value, path) => { if (typeof value === 'string') { // Check for suspicious patterns for (const pattern of injectionPatterns) { if (pattern.test(value)) { result.errors.push(`Potential injection detected at ${path}: ${pattern.source}`); result.isValid = false; result.metadata.riskLevel = 'CRITICAL'; } } // Check for excessively long strings if (value.length > this.config.maxStringLength) { result.errors.push(`String too long at ${path}: ${value.length} > ${this.config.maxStringLength}`); result.isValid = false; result.metadata.riskLevel = 'HIGH'; } // Check for binary data disguised as text if (this.containsBinaryData(value)) { result.warnings.push(`Potential binary data detected at ${path}`); result.metadata.riskLevel = 'MEDIUM'; } // Check for encoded payloads if (this.hasEncodedPayload(value)) { result.warnings.push(`Encoded payload detected at ${path}`); result.metadata.riskLevel = 'MEDIUM'; } } }); // Check object structure limits const structureCheck = this.validateObjectStructure(args); if (!structureCheck.valid) { result.errors.push(...structureCheck.errors); result.isValid = false; result.metadata.riskLevel = 'HIGH'; } } /** * Perform custom validation based on tool-specific rules */ async performCustomValidation(toolName, args, result) { const validator = this.customValidators.get(toolName); if (validator) { try { const customResult = await validator(args, result); if (customResult && !customResult.isValid) { result.errors.push(...(customResult.errors || [])); result.warnings.push(...(customResult.warnings || [])); result.isValid = false; if (customResult.riskLevel) { result.metadata.riskLevel = customResult.riskLevel; } } } catch (error) { result.warnings.push(`Custom validation failed: ${error.message}`); } } } /** * Perform sanitization */ async performSanitization(toolName, result) { const sanitizer = this.sanitizationRules.get(toolName) || this.sanitizationRules.get('default'); if (sanitizer) { try { const sanitizationResult = await sanitizer(result.validatedArgs); if (sanitizationResult.modified) { result.validatedArgs = sanitizationResult.sanitizedArgs; result.sanitized = true; result.warnings.push('Input was sanitized'); if (this.config.enableLogging) { logger.debug('Input sanitized', { toolName, original: this.sanitizeArgsForLogging(result.originalArgs), sanitized: this.sanitizeArgsForLogging(result.validatedArgs) }); } } } catch (error) { result.warnings.push(`Sanitization failed: ${error.message}`); } } } /** * Final validation after sanitization */ async performFinalValidation(toolName, result) { // Re-run security validation on sanitized data const finalSecurityResult = { isValid: true, errors: [], warnings: [], metadata: { riskLevel: 'LOW' } }; await this.performSecurityValidation(result.validatedArgs, finalSecurityResult); if (!finalSecurityResult.isValid) { result.errors.push('Sanitized data still contains security issues'); result.isValid = false; result.metadata.riskLevel = 'HIGH'; } } /** * Check rate limits */ async checkRateLimit(clientInfo) { const clientId = clientInfo.id || clientInfo.ip || 'anonymous'; const now = Date.now(); const minuteKey = `${clientId}:${Math.floor(now / 60000)}`; const hourKey = `${clientId}:${Math.floor(now / 3600000)}`; // Check minute limit const minuteCount = this.requestCounts.get(minuteKey) || 0; if (minuteCount >= this.config.rateLimiting.maxRequestsPerMinute) { return { allowed: false, reason: 'Rate limit exceeded (per minute)', retryAfter: 60 - (Math.floor(now / 1000) % 60) }; } // Check hourly limit const hourCount = this.requestCountsHourly.get(hourKey) || 0; if (hourCount >= this.config.rateLimiting.maxRequestsPerHour) { return { allowed: false, reason: 'Rate limit exceeded (per hour)', retryAfter: 3600 - (Math.floor(now / 1000) % 3600) }; } // Update counters this.requestCounts.set(minuteKey, minuteCount + 1); this.requestCountsHourly.set(hourKey, hourCount + 1); return { allowed: true }; } /** * Validate object structure to prevent DoS attacks */ validateObjectStructure(obj, depth = 0, path = 'root') { const errors = []; if (depth > this.config.maxObjectDepth) { errors.push(`Object nesting too deep at ${path}: ${depth} > ${this.config.maxObjectDepth}`); return { valid: false, errors }; } if (Array.isArray(obj)) { if (obj.length > this.config.maxArrayLength) { errors.push(`Array too large at ${path}: ${obj.length} > ${this.config.maxArrayLength}`); return { valid: false, errors }; } for (let i = 0; i < obj.length; i++) { const result = this.validateObjectStructure(obj[i], depth + 1, `${path}[${i}]`); if (!result.valid) { errors.push(...result.errors); } } } else if (obj && typeof obj === 'object') { const keys = Object.keys(obj); if (keys.length > this.config.maxObjectKeys) { errors.push(`Object has too many keys at ${path}: ${keys.length} > ${this.config.maxObjectKeys}`); return { valid: false, errors }; } for (const key of keys) { const result = this.validateObjectStructure(obj[key], depth + 1, `${path}.${key}`); if (!result.valid) { errors.push(...result.errors); } } } return { valid: errors.length === 0, errors }; } /** * Recursively validate values */ async validateRecursive(obj, validator, path = 'root') { if (Array.isArray(obj)) { for (let i = 0; i < obj.length; i++) { await this.validateRecursive(obj[i], validator, `${path}[${i}]`); } } else if (obj && typeof obj === 'object') { for (const [key, value] of Object.entries(obj)) { await this.validateRecursive(value, validator, `${path}.${key}`); } } else { await validator(obj, path); } } /** * Check if string contains binary data */ containsBinaryData(str) { // Check for null bytes and other binary indicators return /[\x00-\x08\x0E-\x1F\x7F-\xFF]/.test(str); } /** * Check for encoded payloads */ hasEncodedPayload(str) { // Check for various encoding patterns const encodingPatterns = [ /%[0-9A-Fa-f]{2}/, // URL encoding /&#[0-9]+;|&#x[0-9A-Fa-f]+;/, // HTML entities /\\u[0-9A-Fa-f]{4}/, // Unicode escapes /\\x[0-9A-Fa-f]{2}/, // Hex escapes /[A-Za-z0-9+/]{20,}={0,2}/ // Base64-like patterns ]; return encodingPatterns.some(pattern => pattern.test(str)); } /** * Initialize default schemas for MCP tools */ initializeDefaultSchemas() { // Schema for define_domain_ontology this.toolSchemas.set('define_domain_ontology', Joi.object({ entities: Joi.array().items( Joi.object({ name: Joi.string().pattern(/^[a-zA-Z_][a-zA-Z0-9_]*$/).max(100).required(), type: Joi.string().valid('class', 'interface', 'enum', 'function', 'variable').required(), properties: Joi.object().pattern(Joi.string(), Joi.any()).max(50).optional(), description: Joi.string().max(1000).optional() }) ).max(1000).required(), relationships: Joi.array().items( Joi.object({ from: Joi.string().max(100).required(), to: Joi.string().max(100).required(), type: Joi.string().valid('implements', 'extends', 'uses', 'depends_on', 'contains').required(), properties: Joi.object().pattern(Joi.string(), Joi.any()).max(20).optional() }) ).max(5000).optional(), businessRules: Joi.array().items( Joi.string().max(2000) ).max(100).optional(), codingStandards: Joi.object().pattern( Joi.string().max(50), Joi.alternatives().try( Joi.string().max(500), Joi.number(), Joi.boolean(), Joi.object().max(10) ) ).max(100).optional() })); // Schema for query_context_for_task this.toolSchemas.set('query_context_for_task', Joi.object({ taskDescription: Joi.string().min(5).max(5000).required(), entityTypes: Joi.array().items( Joi.string().max(50) ).max(100).optional(), depth: Joi.number().integer().min(1).max(10).default(2).optional() })); // Schema for generate_code_with_context this.toolSchemas.set('generate_code_with_context', Joi.object({ requirement: Joi.string().min(10).max(10000).required(), contextIds: Joi.array().items( Joi.string().pattern(/^[a-zA-Z0-9_\-:.]+$/).max(200) ).max(1000).optional(), patternsToApply: Joi.array().items( Joi.string().max(100) ).max(50).optional(), constraints: Joi.object().pattern( Joi.string().max(50), Joi.alternatives().try(Joi.string().max(500), Joi.number(), Joi.boolean()) ).max(50).optional() })); // Schema for validate_against_kg this.toolSchemas.set('validate_against_kg', Joi.object({ codeSnippet: Joi.string().min(1).max(50000).required(), validationTypes: Joi.array().items( Joi.string().valid('patterns', 'rules', 'standards', 'security', 'performance') ).max(10).default(['patterns', 'rules', 'standards']).optional(), strictMode: Joi.boolean().default(true).optional() })); // Schema for extract_context_from_code this.toolSchemas.set('extract_context_from_code', Joi.object({ filePath: Joi.string().pattern(/^[a-zA-Z0-9_\-./\\:]+$/).max(500).required(), codeSnippet: Joi.string().max(100000).optional() })); // Schema for detect_technical_debt this.toolSchemas.set('detect_technical_debt', Joi.object({ scope: Joi.string().valid('module', 'project', 'specific').required(), target: Joi.string().max(500).required(), debtTypes: Joi.array().items( Joi.string().valid('complexity', 'duplication', 'coupling', 'cohesion', 'naming', 'documentation') ).max(10).default(['complexity', 'duplication', 'coupling']).optional() })); // Schema for suggest_refactoring this.toolSchemas.set('suggest_refactoring', Joi.object({ codeEntity: Joi.string().max(500).required(), improvementGoals: Joi.array().items( Joi.string().valid('performance', 'maintainability', 'readability', 'testability', 'reusability') ).max(10).optional(), preserveBehavior: Joi.boolean().default(true).optional() })); // Schema for update_kg_from_code this.toolSchemas.set('update_kg_from_code', Joi.object({ codeAnalysis: Joi.object({ entities: Joi.array().items(Joi.object()).max(10000).optional(), relationships: Joi.array().items(Joi.object()).max(50000).optional(), patterns: Joi.array().items(Joi.object()).max(1000).optional(), metrics: Joi.object().optional() }).required(), decisions: Joi.array().items( Joi.object({ id: Joi.string().max(100).required(), description: Joi.string().max(2000).required(), rationale: Joi.string().max(5000).optional(), alternatives: Joi.array().items(Joi.string().max(1000)).max(10).optional() }) ).max(100).optional(), learnedPatterns: Joi.array().items( Joi.object({ name: Joi.string().max(100).required(), description: Joi.string().max(2000).required(), confidence: Joi.number().min(0).max(1).optional(), examples: Joi.array().items(Joi.string().max(1000)).max(10).optional() }) ).max(100).optional() })); // Schema for analyze_codebase this.toolSchemas.set('analyze_codebase', Joi.object({ codebasePath: Joi.string().pattern(/^[a-zA-Z0-9_\-./\\:]+$/).max(1000).required(), includeGitHistory: Joi.boolean().default(true).optional(), maxDepth: Joi.number().integer().min(1).max(20).default(10).optional() })); // Schema for get_kg_statistics this.toolSchemas.set('get_kg_statistics', Joi.object({ includeDetails: Joi.boolean().default(false).optional(), entityType: Joi.string().max(50).optional() })); // Arduino-specific schemas this.toolSchemas.set('analyze_arduino_sketch', Joi.object({ sketchPath: Joi.string().pattern(/^[a-zA-Z0-9_\-./\\:]+$/).max(500).required(), targetBoard: Joi.string().valid('uno', 'mega2560', 'nano', 'esp32').default('uno').optional(), includeLibraries: Joi.boolean().default(true).optional() })); this.toolSchemas.set('validate_hardware_config', Joi.object({ board: Joi.string().valid('uno', 'mega2560', 'nano', 'esp32').required(), components: Joi.array().items( Joi.object({ pin: Joi.string().pattern(/^[A-Za-z0-9_]+$/).max(10).required(), usage: Joi.array().items(Joi.string().max(50)).max(10).required(), type: Joi.string().max(50).required() }) ).max(100).required(), connections: Joi.array().items( Joi.object().pattern(Joi.string(), Joi.any()) ).max(500).optional() })); this.toolSchemas.set('optimize_for_arduino', Joi.object({ memoryUsage: Joi.object({ ram: Joi.number().integer().min(0).max(1000000).optional(), flash: Joi.number().integer().min(0).max(10000000).optional(), eeprom: Joi.number().integer().min(0).max(100000).optional() }).optional(), targetBoard: Joi.string().valid('uno', 'mega2560', 'nano', 'esp32').required(), complexity: Joi.number().integer().min(0).max(1000).optional(), constraints: Joi.object({ maxRAM: Joi.number().integer().min(0).max(1000000).optional(), maxFlash: Joi.number().integer().min(0).max(10000000).optional(), maxLoopTime: Joi.number().integer().min(0).max(1000000).optional() }).optional() })); this.toolSchemas.set('generate_interrupt_safe_code', Joi.object({ functionality: Joi.string().min(5).max(2000).required(), interruptType: Joi.string().valid('external', 'timer', 'serial').default('external').optional(), sharedVariables: Joi.array().items( Joi.string().pattern(/^[a-zA-Z_][a-zA-Z0-9_]*$/).max(50) ).max(50).optional() })); this.toolSchemas.set('analyze_timing_constraints', Joi.object({ codeEntity: Joi.string().max(500).required(), constraints: Joi.object({ maxExecutionTime: Joi.number().integer().min(0).max(1000000).optional(), maxLoopTime: Joi.number().integer().min(0).max(1000000).optional(), realTimeDeadline: Joi.number().integer().min(0).max(1000000).optional() }).optional() })); logger.debug('Default validation schemas initialized', { schemaCount: this.toolSchemas.size }); } /** * Initialize default custom validators */ initializeDefaultValidators() { // File path validator this.customValidators.set('extract_context_from_code', async (args, result) => { const { filePath } = args; // Check for path traversal attempts if (filePath.includes('..') || filePath.includes('~')) { return { isValid: false, errors: ['Path traversal attempt detected'], riskLevel: 'CRITICAL' }; } // Check for suspicious file extensions const suspiciousExtensions = ['.exe', '.bat', '.sh', '.cmd', '.com', '.scr']; const extension = filePath.toLowerCase().split('.').pop(); if (suspiciousExtensions.includes(`.${extension}`)) { return { isValid: false, errors: ['Suspicious file extension detected'], riskLevel: 'HIGH' }; } return { isValid: true }; }); // Code snippet validator this.customValidators.set('validate_against_kg', async (args, result) => { const { codeSnippet } = args; // Check for obfuscated code patterns const obfuscationPatterns = [ /[a-zA-Z0-9]{50,}/, // Long random strings /\\x[0-9A-Fa-f]{2}{10,}/, // Excessive hex escapes /eval\s*\(/i, // Eval usage /Function\s*\(/i, // Function constructor /setTimeout\s*\(\s*["'].*["']/i // String-based setTimeout ]; for (const pattern of obfuscationPatterns) { if (pattern.test(codeSnippet)) { return { isValid: false, errors: ['Potentially obfuscated or malicious code detected'], riskLevel: 'HIGH' }; } } return { isValid: true }; }); // Arduino hardware validator this.customValidators.set('validate_hardware_config', async (args, result) => { const { board, components } = args; // Board-specific pin validation const boardPins = { uno: { digital: 14, analog: 6, pwm: 6 }, mega2560: { digital: 54, analog: 16, pwm: 15 }, nano: { digital: 14, analog: 8, pwm: 6 }, esp32: { digital: 34, analog: 18, pwm: 16 } }; const boardConfig = boardPins[board]; if (!boardConfig) { return { isValid: false, errors: ['Unknown board type'], riskLevel: 'MEDIUM' }; } // Check for pin conflicts const usedPins = new Set(); const errors = []; for (const component of components) { const pin = component.pin; if (usedPins.has(pin)) { errors.push(`Pin conflict detected: ${pin} used by multiple components`); } usedPins.add(pin); // Validate pin number if (pin.startsWith('D') || pin.startsWith('d')) { const pinNum = parseInt(pin.substring(1)); if (pinNum >= boardConfig.digital) { errors.push(`Invalid digital pin: ${pin} (max: D${boardConfig.digital - 1})`); } } else if (pin.startsWith('A') || pin.startsWith('a')) { const pinNum = parseInt(pin.substring(1)); if (pinNum >= boardConfig.analog) { errors.push(`Invalid analog pin: ${pin} (max: A${boardConfig.analog - 1})`); } } } if (errors.length > 0) { return { isValid: false, errors, riskLevel: 'MEDIUM' }; } return { isValid: true }; }); logger.debug('Default custom validators initialized', { validatorCount: this.customValidators.size }); } /** * Initialize default sanitizers */ initializeDefaultSanitizers() { // Default sanitizer for all tools this.sanitizationRules.set('default', async (args) => { let modified = false; const sanitizedArgs = this.deepClone(args); // Sanitize all string values recursively const sanitizeValue = (obj, path = '') => { if (typeof obj === 'string') { const original = obj; let sanitized = obj; // Remove null bytes sanitized = sanitized.replace(/\x00/g, ''); // Normalize Unicode try { sanitized = sanitized.normalize('NFKC'); } catch (e) { // Ignore normalization errors } // Trim excessive whitespace sanitized = sanitized.replace(/\s{2,}/g, ' ').trim(); // Limit length to prevent DoS if (sanitized.length > 50000) { sanitized = sanitized.substring(0, 50000) + '... [truncated]'; } if (sanitized !== original) { modified = true; } return sanitized; } else if (Array.isArray(obj)) { return obj.map((item, index) => sanitizeValue(item, `${path}[${index}]`)); } else if (obj && typeof obj === 'object') { const result = {}; for (const [key, value] of Object.entries(obj)) { result[key] = sanitizeValue(value, `${path}.${key}`); } return result; } return obj; }; const result = sanitizeValue(sanitizedArgs); return { modified, sanitizedArgs: result }; }); // Specific sanitizer for code snippets this.sanitizationRules.set('validate_against_kg', async (args) => { const sanitizedArgs = { ...args }; let modified = false; if (sanitizedArgs.codeSnippet) { const original = sanitizedArgs.codeSnippet; let sanitized = original; // Remove dangerous patterns while preserving code structure sanitized = sanitized.replace(/eval\s*\(/gi, 'eval_SANITIZED('); sanitized = sanitized.replace(/Function\s*\(/gi, 'Function_SANITIZED('); sanitized = sanitized.replace(/document\s*\.\s*write/gi, 'document_write_SANITIZED'); if (sanitized !== original) { modified = true; sanitizedArgs.codeSnippet = sanitized; } } return { modified, sanitizedArgs }; }); // Sanitizer for file paths this.sanitizationRules.set('extract_context_from_code', async (args) => { const sanitizedArgs = { ...args }; let modified = false; if (sanitizedArgs.filePath) { const original = sanitizedArgs.filePath; let sanitized = original; // Normalize path separators sanitized = sanitized.replace(/\\/g, '/'); // Remove double slashes sanitized = sanitized.replace(/\/+/g, '/'); // Remove trailing slash sanitized = sanitized.replace(/\/$/, ''); if (sanitized !== original) { modified = true; sanitizedArgs.filePath = sanitized; } } return { modified, sanitizedArgs }; }); logger.debug('Default sanitization rules initialized', { sanitizerCount: this.sanitizationRules.size }); } /** * Setup rate limit cleanup */ setupRateLimitCleanup() { if (!this.config.rateLimiting.enabled) return; // Cleanup old rate limit entries every 5 minutes setInterval(() => { const now = Date.now(); const currentMinute = Math.floor(now / 60000); const currentHour = Math.floor(now / 3600000); // Clean up minute counters older than 2 minutes for (const [key] of this.requestCounts) { const keyMinute = parseInt(key.split(':')[1]); if (currentMinute - keyMinute > 2) { this.requestCounts.delete(key); } } // Clean up hour counters older than 2 hours for (const [key] of this.requestCountsHourly) { const keyHour = parseInt(key.split(':')[1]); if (currentHour - keyHour > 2) { this.requestCountsHourly.delete(key); } } }, 5 * 60 * 1000); } /** * Add custom validation schema for a tool */ addToolSchema(toolName, schema) { if (!Joi.isSchema(schema)) { throw new Error('Schema must be a Joi schema object'); } this.toolSchemas.set(toolName, schema); logger.debug('Custom schema added', { toolName }); } /** * Add custom validator for a tool */ addCustomValidator(toolName, validator) { if (typeof validator !== 'function') { throw new Error('Validator must be a function'); } this.customValidators.set(toolName, validator); logger.debug('Custom validator added', { toolName }); } /** * Add custom sanitizer for a tool */ addSanitizationRule(toolName, sanitizer) { if (typeof sanitizer !== 'function') { throw new Error('Sanitizer must be a function'); } this.sanitizationRules.set(toolName, sanitizer); logger.debug('Custom sanitizer added', { toolName }); } /** * Get validation statistics */ getStatistics() { return { ...this.stats, successRate: this.stats.totalValidations > 0 ? (this.stats.validInputs / this.stats.totalValidations * 100).toFixed(2) + '%' : '0%', sanitizationRate: this.stats.totalValidations > 0 ? (this.stats.sanitizedInputs / this.stats.totalValidations * 100).toFixed(2) + '%' : '0%', blockRate: this.stats.totalValidations > 0 ? (this.stats.blockedInputs / this.stats.totalValidations * 100).toFixed(2) + '%' : '0%', rateLimitRate: this.stats.totalValidations > 0 ? (this.stats.rateLimitedRequests / this.stats.totalValidations * 100).toFixed(2) + '%' : '0%' }; } /** * Reset statistics */ resetStatistics() { this.stats = { totalValidations: 0, validInputs: 0, invalidInputs: 0, sanitizedInputs: 0, blockedInputs: 0, rateLimitedRequests: 0 }; logger.debug('Validation statistics reset'); } /** * Update configuration */ updateConfig(newConfig) { this.config = { ...this.config, ...newConfig }; logger.debug('MCPInputValidator configuration updated', newConfig); } /** * Utility: Deep clone object */ deepClone(obj) { if (obj === null || typeof obj !== 'object') return obj; if (obj instanceof Date) return new Date(obj.getTime()); if (Array.isArray(obj)) return obj.map(item => this.deepClone(item)); const cloned = {}; for (const [key, value] of Object.entries(obj)) { cloned[key] = this.deepClone(value); } return cloned; } /** * Utility: Sanitize args for logging (remove sensitive data) */ sanitizeArgsForLogging(args) { const sanitized = this.deepClone(args); const sanitizeRecursive = (obj) => { if (typeof obj === 'string') { if (obj.length > 200) { return obj.substring(0, 200) + '... [truncated for logging]'; } return obj; } else if (Array.isArray(obj)) { return obj.slice(0, 10).map(item => sanitizeRecursive(item)); } else if (obj && typeof obj === 'object') { const result = {}; const keys = Object.keys(obj).slice(0, 20); for (const key of keys) { result[key] = sanitizeRecursive(obj[key]); } if (Object.keys(obj).length > 20) { result['...'] = `[${Object.keys(obj).length - 20} more keys]`; } return result; } return obj; }; return sanitizeRecursive(sanitized); } } export default MCPInputValidator;

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/sascodiego/KGsMCP'

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