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
JavaScript
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