UNPKG

survey-mcp-server

Version:

Survey management server handling survey creation, response collection, analysis, and reporting with database access for data management

331 lines 12.9 kB
import { logger } from '../utils/logger.js'; import { toolDefinitions } from '../tools/schema.js'; export class ValidationMiddleware { constructor() { } static getInstance() { if (!ValidationMiddleware.instance) { ValidationMiddleware.instance = new ValidationMiddleware(); } return ValidationMiddleware.instance; } validateToolInput(toolName, input) { const tool = toolDefinitions.find(t => t.name === toolName); if (!tool) { return { isValid: false, errors: [{ field: 'toolName', message: `Tool '${toolName}' not found` }] }; } const validationResult = this.validateAgainstSchema(input, tool.inputSchema); if (!validationResult.isValid) { logger.warn(`Validation failed for tool ${toolName}:`, validationResult.errors); } return validationResult; } validateAgainstSchema(input, schema) { const errors = []; const sanitizedInput = {}; // Check required fields if (schema.required && Array.isArray(schema.required)) { for (const requiredField of schema.required) { if (!(requiredField in input)) { errors.push({ field: requiredField, message: `Required field '${requiredField}' is missing` }); } } } // Validate properties if (schema.properties) { for (const [fieldName, fieldSchema] of Object.entries(schema.properties)) { const fieldValue = input[fieldName]; const fieldResult = this.validateField(fieldName, fieldValue, fieldSchema); if (!fieldResult.isValid) { errors.push(...fieldResult.errors); } else if (fieldResult.sanitizedValue !== undefined) { sanitizedInput[fieldName] = fieldResult.sanitizedValue; } } } // Check for additional properties if not allowed if (schema.additionalProperties === false) { for (const fieldName in input) { if (!schema.properties || !(fieldName in schema.properties)) { errors.push({ field: fieldName, message: `Additional property '${fieldName}' is not allowed` }); } } } return { isValid: errors.length === 0, errors, sanitizedInput: Object.keys(sanitizedInput).length > 0 ? sanitizedInput : input }; } validateField(fieldName, value, fieldSchema) { const errors = []; let sanitizedValue = value; // Skip validation if value is undefined and field is not required if (value === undefined) { return { isValid: true, errors: [], sanitizedValue: undefined }; } // Type validation if (fieldSchema.type) { const typeValidation = this.validateType(fieldName, value, fieldSchema.type); if (!typeValidation.isValid) { errors.push(...typeValidation.errors); return { isValid: false, errors, sanitizedValue: value }; } sanitizedValue = typeValidation.sanitizedValue; } // Enum validation if (fieldSchema.enum && Array.isArray(fieldSchema.enum)) { if (!fieldSchema.enum.includes(value)) { errors.push({ field: fieldName, message: `Value '${value}' is not one of allowed values: ${fieldSchema.enum.join(', ')}`, value }); } } // String validations if (fieldSchema.type === 'string' && typeof value === 'string') { if (fieldSchema.minLength && value.length < fieldSchema.minLength) { errors.push({ field: fieldName, message: `String length must be at least ${fieldSchema.minLength}`, value }); } if (fieldSchema.maxLength && value.length > fieldSchema.maxLength) { errors.push({ field: fieldName, message: `String length must be at most ${fieldSchema.maxLength}`, value }); } if (fieldSchema.pattern) { const regex = new RegExp(fieldSchema.pattern); if (!regex.test(value)) { errors.push({ field: fieldName, message: `String does not match required pattern: ${fieldSchema.pattern}`, value }); } } } // Number validations if (fieldSchema.type === 'number' && typeof value === 'number') { if (fieldSchema.minimum !== undefined && value < fieldSchema.minimum) { errors.push({ field: fieldName, message: `Number must be at least ${fieldSchema.minimum}`, value }); } if (fieldSchema.maximum !== undefined && value > fieldSchema.maximum) { errors.push({ field: fieldName, message: `Number must be at most ${fieldSchema.maximum}`, value }); } } // Array validations if (fieldSchema.type === 'array' && Array.isArray(value)) { if (fieldSchema.minItems !== undefined && value.length < fieldSchema.minItems) { errors.push({ field: fieldName, message: `Array must have at least ${fieldSchema.minItems} items`, value }); } if (fieldSchema.maxItems !== undefined && value.length > fieldSchema.maxItems) { errors.push({ field: fieldName, message: `Array must have at most ${fieldSchema.maxItems} items`, value }); } // Validate array items if (fieldSchema.items) { const sanitizedArray = []; for (let i = 0; i < value.length; i++) { const itemResult = this.validateField(`${fieldName}[${i}]`, value[i], fieldSchema.items); if (!itemResult.isValid) { errors.push(...itemResult.errors); } else { sanitizedArray.push(itemResult.sanitizedValue !== undefined ? itemResult.sanitizedValue : value[i]); } } sanitizedValue = sanitizedArray; } } // Object validations if (fieldSchema.type === 'object' && typeof value === 'object' && value !== null) { const objectResult = this.validateAgainstSchema(value, fieldSchema); if (!objectResult.isValid) { errors.push(...objectResult.errors); } else { sanitizedValue = objectResult.sanitizedInput; } } return { isValid: errors.length === 0, errors, sanitizedValue }; } validateType(fieldName, value, expectedType) { const errors = []; let sanitizedValue = value; switch (expectedType) { case 'string': if (typeof value !== 'string') { if (typeof value === 'number' || typeof value === 'boolean') { sanitizedValue = String(value); } else { errors.push({ field: fieldName, message: `Expected string, got ${typeof value}`, value }); } } break; case 'number': if (typeof value !== 'number') { if (typeof value === 'string' && !isNaN(Number(value))) { sanitizedValue = Number(value); } else { errors.push({ field: fieldName, message: `Expected number, got ${typeof value}`, value }); } } break; case 'integer': if (typeof value !== 'number' || !Number.isInteger(value)) { if (typeof value === 'string' && !isNaN(Number(value)) && Number.isInteger(Number(value))) { sanitizedValue = Number(value); } else { errors.push({ field: fieldName, message: `Expected integer, got ${typeof value}`, value }); } } break; case 'boolean': if (typeof value !== 'boolean') { if (value === 'true' || value === 'false') { sanitizedValue = value === 'true'; } else { errors.push({ field: fieldName, message: `Expected boolean, got ${typeof value}`, value }); } } break; case 'array': if (!Array.isArray(value)) { errors.push({ field: fieldName, message: `Expected array, got ${typeof value}`, value }); } break; case 'object': if (typeof value !== 'object' || value === null || Array.isArray(value)) { errors.push({ field: fieldName, message: `Expected object, got ${typeof value}`, value }); } break; default: // Unknown type, skip validation break; } return { isValid: errors.length === 0, errors, sanitizedValue }; } validateMongoQuery(query) { const errors = []; const sanitizedQuery = this.sanitizeMongoQuery(query); // Check for dangerous operators const dangerousOperators = ['$where', '$eval', '$function', '$accumulator', '$facet']; const containsDangerousOp = this.containsDangerousOperators(query, dangerousOperators); if (containsDangerousOp) { errors.push({ field: 'query', message: 'Query contains dangerous operators', value: query }); } return { isValid: errors.length === 0, errors, sanitizedInput: sanitizedQuery }; } sanitizeMongoQuery(query) { if (typeof query !== 'object' || query === null) { return query; } const sanitized = {}; for (const [key, value] of Object.entries(query)) { // Remove dangerous operators if (key.startsWith('$') && ['$where', '$eval', '$function', '$accumulator', '$facet'].includes(key)) { continue; } // Recursively sanitize nested objects if (typeof value === 'object' && value !== null) { sanitized[key] = this.sanitizeMongoQuery(value); } else { sanitized[key] = value; } } return sanitized; } containsDangerousOperators(obj, operators) { if (typeof obj !== 'object' || obj === null) { return false; } for (const [key, value] of Object.entries(obj)) { if (operators.includes(key)) { return true; } if (typeof value === 'object' && value !== null) { if (this.containsDangerousOperators(value, operators)) { return true; } } } return false; } } export const validationMiddleware = ValidationMiddleware.getInstance(); //# sourceMappingURL=validation.js.map