UNPKG

@boundless-oss/atlas

Version:

Atlas - MCP Server for comprehensive startup project management

474 lines (425 loc) 11.9 kB
/** * Schema Validation Utility * Implements MCP Design Guide Section 2.3 principles for robust tool input validation */ import { ErrorHandler } from './error-handler.js'; export interface ValidationRule { field: string; type: string; required?: boolean; min?: number; max?: number; pattern?: string; enum?: any[]; custom?: (value: any) => boolean | string; } export interface SchemaDefinition { type: 'object'; properties: Record<string, any>; required?: string[]; additionalProperties?: boolean; } export class SchemaValidator { /** * Enhanced schema patterns for common MCP tool parameters */ static readonly COMMON_SCHEMAS = { // Identifiers ID: { type: 'string', pattern: '^[a-zA-Z0-9\\-_]+$', minLength: 1, maxLength: 50, description: 'Alphanumeric identifier with hyphens and underscores' }, UUID: { type: 'string', pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', description: 'UUID v4 format' }, // Names and titles NAME: { type: 'string', minLength: 1, maxLength: 100, pattern: '^[a-zA-Z0-9\\s\\-_\\.]+$', description: 'Name with alphanumeric characters, spaces, hyphens, underscores, and periods' }, TITLE: { type: 'string', minLength: 1, maxLength: 150, description: 'Title or summary text' }, // Descriptions SHORT_DESCRIPTION: { type: 'string', minLength: 5, maxLength: 500, description: 'Short description text' }, LONG_DESCRIPTION: { type: 'string', minLength: 10, maxLength: 2000, description: 'Detailed description text' }, // Dates and times DATE: { type: 'string', format: 'date', description: 'Date in YYYY-MM-DD format' }, DATETIME: { type: 'string', format: 'date-time', description: 'Date and time in ISO 8601 format' }, // Priority levels PRIORITY: { type: 'string', enum: ['low', 'medium', 'high', 'critical'], description: 'Priority level' }, // Status values AGILE_STATUS: { type: 'string', enum: ['planned', 'active', 'completed', 'cancelled'], description: 'Agile item status' }, TASK_STATUS: { type: 'string', enum: ['todo', 'in_progress', 'review', 'testing', 'done', 'blocked'], description: 'Task status' }, // Story points (Fibonacci sequence) STORY_POINTS: { type: 'number', enum: [0, 1, 2, 3, 5, 8, 13, 21], description: 'Story points using Fibonacci sequence' }, // Duration constraints SPRINT_DURATION: { type: 'number', minimum: 1, maximum: 30, description: 'Sprint duration in days' }, // Email format EMAIL: { type: 'string', format: 'email', description: 'Valid email address' }, // URL format URL: { type: 'string', format: 'uri', description: 'Valid URL' }, // Arrays with constraints TAG_ARRAY: { type: 'array', items: { type: 'string', minLength: 1, maxLength: 30, pattern: '^[a-zA-Z0-9\\-_]+$' }, maxItems: 10, uniqueItems: true, description: 'Array of tags (max 10, alphanumeric with hyphens/underscores)' }, STRING_ARRAY: { type: 'array', items: { type: 'string', minLength: 1, maxLength: 200 }, maxItems: 20, description: 'Array of strings (max 20 items)' } }; /** * Create enhanced schema for tool definitions */ static createToolSchema(fields: Record<string, string | object>, required: string[] = []): SchemaDefinition { const properties: Record<string, any> = {}; for (const [fieldName, schemaType] of Object.entries(fields)) { if (typeof schemaType === 'string') { // Use predefined schema if (this.COMMON_SCHEMAS[schemaType as keyof typeof this.COMMON_SCHEMAS]) { properties[fieldName] = { ...this.COMMON_SCHEMAS[schemaType as keyof typeof this.COMMON_SCHEMAS] }; } else { throw new Error(`Unknown schema type: ${schemaType}`); } } else { // Use custom schema object properties[fieldName] = schemaType; } } return { type: 'object', properties, required, additionalProperties: false }; } /** * Validate parameters against schema with detailed error reporting */ static validateParameters( params: any, schema: SchemaDefinition, context: { tool: string; module: string } ): void { if (!params || typeof params !== 'object') { throw ErrorHandler.createValidationError( 'params', params, 'must be an object', context ); } // Check required fields if (schema.required) { for (const field of schema.required) { if (!(field in params) || params[field] === undefined || params[field] === null) { throw ErrorHandler.createValidationError( field, params[field], 'is required', context ); } } } // Validate each property for (const [fieldName, value] of Object.entries(params)) { if (value === undefined || value === null) { continue; // Skip undefined/null values for optional fields } const fieldSchema = schema.properties[fieldName]; if (!fieldSchema && !schema.additionalProperties) { throw ErrorHandler.createValidationError( fieldName, value, 'is not allowed (additional properties forbidden)', context ); } if (fieldSchema) { this.validateField(fieldName, value, fieldSchema, context); } } } /** * Validate individual field against its schema */ private static validateField( fieldName: string, value: any, schema: any, context: { tool: string; module: string } ): void { // Type validation if (schema.type) { if (!this.validateType(value, schema.type)) { throw ErrorHandler.createValidationError( fieldName, value, `must be of type ${schema.type}`, context ); } } // String validations if (schema.type === 'string' && typeof value === 'string') { if (schema.minLength && value.length < schema.minLength) { throw ErrorHandler.createValidationError( fieldName, value, `must be at least ${schema.minLength} characters long`, context ); } if (schema.maxLength && value.length > schema.maxLength) { throw ErrorHandler.createValidationError( fieldName, value, `must be no more than ${schema.maxLength} characters long`, context ); } if (schema.pattern && !new RegExp(schema.pattern).test(value)) { throw ErrorHandler.createValidationError( fieldName, value, `must match pattern: ${schema.pattern}`, context ); } if (schema.format) { this.validateFormat(fieldName, value, schema.format, context); } } // Number validations if (schema.type === 'number' && typeof value === 'number') { if (schema.minimum !== undefined && value < schema.minimum) { throw ErrorHandler.createValidationError( fieldName, value, `must be at least ${schema.minimum}`, context ); } if (schema.maximum !== undefined && value > schema.maximum) { throw ErrorHandler.createValidationError( fieldName, value, `must be no more than ${schema.maximum}`, context ); } } // Array validations if (schema.type === 'array' && Array.isArray(value)) { if (schema.minItems && value.length < schema.minItems) { throw ErrorHandler.createValidationError( fieldName, value, `must have at least ${schema.minItems} items`, context ); } if (schema.maxItems && value.length > schema.maxItems) { throw ErrorHandler.createValidationError( fieldName, value, `must have no more than ${schema.maxItems} items`, context ); } if (schema.uniqueItems) { const uniqueValues = new Set(value); if (uniqueValues.size !== value.length) { throw ErrorHandler.createValidationError( fieldName, value, 'must contain unique items only', context ); } } // Validate each array item if (schema.items) { value.forEach((item, index) => { this.validateField(`${fieldName}[${index}]`, item, schema.items, context); }); } } // Enum validation if (schema.enum && !schema.enum.includes(value)) { throw ErrorHandler.createValidationError( fieldName, value, `must be one of: ${schema.enum.join(', ')}`, context ); } } /** * Validate data type */ private static validateType(value: any, expectedType: string): boolean { switch (expectedType) { case 'string': return typeof value === 'string'; case 'number': return typeof value === 'number' && !isNaN(value); case 'boolean': return typeof value === 'boolean'; case 'array': return Array.isArray(value); case 'object': return typeof value === 'object' && value !== null && !Array.isArray(value); default: return true; } } /** * Validate string formats */ private static validateFormat( fieldName: string, value: string, format: string, context: { tool: string; module: string } ): void { switch (format) { case 'email': const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(value)) { throw ErrorHandler.createValidationError( fieldName, value, 'must be a valid email address', context ); } break; case 'uri': try { new URL(value); } catch { throw ErrorHandler.createValidationError( fieldName, value, 'must be a valid URL', context ); } break; case 'date': const dateRegex = /^\d{4}-\d{2}-\d{2}$/; if (!dateRegex.test(value) || isNaN(Date.parse(value))) { throw ErrorHandler.createValidationError( fieldName, value, 'must be a valid date in YYYY-MM-DD format', context ); } break; case 'date-time': if (isNaN(Date.parse(value))) { throw ErrorHandler.createValidationError( fieldName, value, 'must be a valid ISO 8601 date-time', context ); } break; } } /** * Create a decorator for automatic parameter validation */ static validateParams(schema: SchemaDefinition) { return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { const originalMethod = descriptor.value; descriptor.value = async function (...args: any[]) { const params = args[0]; const context = { tool: propertyKey, module: target.constructor.name, }; SchemaValidator.validateParameters(params, schema, context); return await originalMethod.apply(this, args); }; return descriptor; }; } }