Skip to main content
Glama
validation.tsβ€’14.5 kB
/** * JSON Schema Validation Middleware * * Provides strict JSON-schema validation for all MCP tools * to prevent runtime type errors from reaching business logic. * * This middleware validates all tool parameters against their schemas * and returns proper 4xx ValidationError for schema violations. */ import { UniversalValidationError, ErrorType, HttpStatusCode, InputSanitizer, SanitizedValue, SanitizedObject, } from '../handlers/tool-configs/universal/schemas.js'; /** * Schema validation result */ export interface ValidationResult { valid: boolean; sanitizedParams?: SanitizedObject; errors?: ValidationError[]; } /** * Validation error details */ export interface ValidationError { field: string; message: string; value?: unknown; expected?: string; suggestion?: string; } /** * Schema definition for validation */ export interface SchemaDefinition { type: string; properties?: Record<string, unknown>; required?: string[]; additionalProperties?: boolean; enum?: string[]; minimum?: number; maximum?: number; minLength?: number; maxLength?: number; pattern?: string; format?: string; default?: unknown; } /** * Validates a value against a JSON schema */ export class JsonSchemaValidator { /** * Validate parameters against a JSON schema */ static validate(params: unknown, schema: SchemaDefinition): ValidationResult { // First sanitize the input const sanitized = InputSanitizer.sanitizeObject(params); if ( !sanitized || typeof sanitized !== 'object' || Array.isArray(sanitized) ) { return { valid: false, errors: [ { field: 'root', message: 'Parameters must be an object', value: params, expected: 'object', }, ], }; } const sanitizedParams = sanitized as SanitizedObject; // Validate against schema const validationErrors = this.validateObject(sanitizedParams, schema, ''); if (validationErrors.length > 0) { return { valid: false, errors: validationErrors, }; } return { valid: true, sanitizedParams, }; } /** * Validate an object against a schema */ private static validateObject( obj: SanitizedObject, schema: SchemaDefinition, path: string ): ValidationError[] { const errors: ValidationError[] = []; // Check type if (schema.type === 'object') { // Check required fields if (schema.required) { for (const field of schema.required) { if ( !(field in obj) || obj[field] === null || obj[field] === undefined ) { errors.push({ field: path ? `${path}.${field}` : field, message: `Missing required field: ${field}`, expected: 'required field to be present', }); } } } // Check properties if (schema.properties) { for (const [key, value] of Object.entries(obj)) { if (schema.properties[key]) { const fieldPath = path ? `${path}.${key}` : key; errors.push( ...this.validateValue( value, schema.properties[key] as SchemaDefinition, fieldPath ) ); } else if (schema.additionalProperties === false) { errors.push({ field: path ? `${path}.${key}` : key, message: `Unknown field: ${key}`, value, suggestion: 'Remove this field or check the field name', }); } } } } return errors; } /** * Validate a single value against a schema */ private static validateValue( value: SanitizedValue, schema: SchemaDefinition, path: string ): ValidationError[] { const errors: ValidationError[] = []; // Handle null/undefined if (value === null || value === undefined) { if (!schema.default) { return errors; // Allow null/undefined if no default specified } } // Check type const actualType = Array.isArray(value) ? 'array' : typeof value; switch (schema.type) { case 'string': if (typeof value !== 'string') { errors.push({ field: path, message: `Expected string, got ${actualType}`, value, expected: 'string', }); } else { // Check string constraints if (schema.minLength && value.length < schema.minLength) { errors.push({ field: path, message: `String must be at least ${schema.minLength} characters`, value, expected: `minimum length: ${schema.minLength}`, }); } if (schema.maxLength && value.length > schema.maxLength) { errors.push({ field: path, message: `String must be at most ${schema.maxLength} characters`, value, expected: `maximum length: ${schema.maxLength}`, }); } if (schema.pattern) { const regex = new RegExp(schema.pattern); if (!regex.test(value)) { errors.push({ field: path, message: `String does not match pattern: ${schema.pattern}`, value, expected: `pattern: ${schema.pattern}`, }); } } if (schema.enum && !schema.enum.includes(value)) { errors.push({ field: path, message: `Invalid value: ${value}`, value, expected: `one of: ${schema.enum.join(', ')}`, }); } } break; case 'number': if (typeof value !== 'number') { errors.push({ field: path, message: `Expected number, got ${actualType}`, value, expected: 'number', }); } else { // Check number constraints if (schema.minimum !== undefined && value < schema.minimum) { errors.push({ field: path, message: `Value must be at least ${schema.minimum}`, value, expected: `minimum: ${schema.minimum}`, }); } if (schema.maximum !== undefined && value > schema.maximum) { errors.push({ field: path, message: `Value must be at most ${schema.maximum}`, value, expected: `maximum: ${schema.maximum}`, }); } } break; case 'boolean': if (typeof value !== 'boolean') { errors.push({ field: path, message: `Expected boolean, got ${actualType}`, value, expected: 'boolean', }); } break; case 'array': if (!Array.isArray(value)) { errors.push({ field: path, message: `Expected array, got ${actualType}`, value, expected: 'array', }); } break; case 'object': if (typeof value !== 'object' || Array.isArray(value)) { errors.push({ field: path, message: `Expected object, got ${actualType}`, value, expected: 'object', }); } else if (value !== null) { // Recursively validate nested object errors.push( ...this.validateObject(value as SanitizedObject, schema, path) ); } break; } return errors; } } /** * Parameter validation middleware */ export class ParameterValidationMiddleware { /** * Validate parameters for universal tools */ static validateUniversalParams( toolName: string, params: unknown, schema: SchemaDefinition ): SanitizedObject { // First validate against JSON schema const result = JsonSchemaValidator.validate(params, schema); if (!result.valid) { const errorMessages = result.errors!.map( (e) => `${e.field}: ${e.message}${e.suggestion ? ` (${e.suggestion})` : ''}` ); throw new UniversalValidationError( `Validation failed for ${toolName}:\n${errorMessages.join('\n')}`, ErrorType.USER_ERROR, { httpStatusCode: HttpStatusCode.UNPROCESSABLE_ENTITY, suggestion: 'Fix the validation errors and try again', } ); } const sanitizedParams = result.sanitizedParams!; // Additional specific validations for universal tools this.validatePaginationParams(sanitizedParams); this.validateIdFormat(sanitizedParams); return sanitizedParams; } /** * Validate pagination parameters (limit, offset) */ private static validatePaginationParams(params: SanitizedObject): void { // Validate limit if ( 'limit' in params && params.limit !== null && params.limit !== undefined ) { const limit = Number(params.limit); if (isNaN(limit) || !Number.isInteger(limit)) { throw new UniversalValidationError( 'Parameter "limit" must be an integer', ErrorType.USER_ERROR, { field: 'limit', httpStatusCode: HttpStatusCode.UNPROCESSABLE_ENTITY, suggestion: 'Provide a valid integer for limit', example: 'limit: 10', } ); } if (limit < 1) { throw new UniversalValidationError( 'Parameter "limit" must be at least 1', ErrorType.USER_ERROR, { field: 'limit', httpStatusCode: HttpStatusCode.UNPROCESSABLE_ENTITY, suggestion: 'Use a positive integer for limit', example: 'limit: 10', } ); } if (limit > 100) { throw new UniversalValidationError( 'Parameter "limit" must not exceed 100', ErrorType.USER_ERROR, { field: 'limit', httpStatusCode: HttpStatusCode.UNPROCESSABLE_ENTITY, suggestion: 'Use a value between 1 and 100', example: 'limit: 50', } ); } } // Validate offset if ( 'offset' in params && params.offset !== null && params.offset !== undefined ) { const offset = Number(params.offset); if (isNaN(offset) || !Number.isInteger(offset)) { throw new UniversalValidationError( 'Parameter "offset" must be an integer', ErrorType.USER_ERROR, { field: 'offset', httpStatusCode: HttpStatusCode.UNPROCESSABLE_ENTITY, suggestion: 'Provide a valid integer for offset', example: 'offset: 0', } ); } if (offset < 0) { throw new UniversalValidationError( 'Parameter "offset" must be non-negative', ErrorType.USER_ERROR, { field: 'offset', httpStatusCode: HttpStatusCode.UNPROCESSABLE_ENTITY, suggestion: 'Use a non-negative integer for offset', example: 'offset: 0', } ); } } } /** * Validate ID format for record_id and similar fields */ private static validateIdFormat(params: SanitizedObject): void { const idFields = [ 'record_id', 'source_id', 'target_id', 'company_id', 'person_id', ]; for (const field of idFields) { if ( field in params && params[field] !== null && params[field] !== undefined ) { const id = String(params[field]); // Basic ID format validation (alphanumeric with underscores and hyphens) const idRegex = /^[a-zA-Z0-9_-]+$/; if (!idRegex.test(id)) { throw new UniversalValidationError( `Invalid ${field} format: "${id}"`, ErrorType.USER_ERROR, { field, httpStatusCode: HttpStatusCode.UNPROCESSABLE_ENTITY, suggestion: `The ${field} should contain only letters, numbers, underscores, and hyphens`, example: `${field}: 'comp_abc123' or 'person_xyz789'`, } ); } // Check for reasonable length if (id.length < 3 || id.length > 100) { throw new UniversalValidationError( `Invalid ${field} length: ${id.length} characters`, ErrorType.USER_ERROR, { field, httpStatusCode: HttpStatusCode.UNPROCESSABLE_ENTITY, suggestion: `The ${field} should be between 3 and 100 characters`, example: `${field}: 'comp_abc123'`, } ); } } } } } /** * Create a validation error response */ export function createValidationErrorResponse( error: UniversalValidationError ): Record<string, unknown> { return { error: error.toErrorResponse().error, status: 'error', code: error.httpStatusCode, }; } /** * Wrap a handler with validation middleware */ export function withValidation<T extends (...args: unknown[]) => unknown>( handler: T, schema: SchemaDefinition, toolName: string ): T { return (async (...args: unknown[]) => { try { // Validate parameters if provided if (args[0]) { const validatedParams = ParameterValidationMiddleware.validateUniversalParams( toolName, args[0], schema ); // Replace original params with validated ones args[0] = validatedParams; } // Call the original handler return await handler(...args); } catch (error: unknown) { // If it's already a validation error, re-throw if (error instanceof UniversalValidationError) { throw error; } // Otherwise wrap in a system error throw new UniversalValidationError( `Unexpected error in ${toolName}: ${ error instanceof Error ? error.message : String(error) }`, ErrorType.SYSTEM_ERROR, { httpStatusCode: HttpStatusCode.INTERNAL_SERVER_ERROR, cause: error instanceof Error ? error : undefined, } ); } }) as T; }

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/kesslerio/attio-mcp-server'

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