UNPKG

@simonecoelhosfo/optimizely-mcp-server

Version:

Optimizely MCP Server for AI assistants with integrated CLI tools

257 lines 10.9 kB
/** * Template Schema Integration * @description Bridges the gap between parser and rich template schemas * * Purpose: Ensure the intelligent parser uses the same validation rules, * enums, and constraints that AI agents are primed with. * * @author Optimizely MCP Server * @version 1.0.0 */ import { getLogger } from '../logging/Logger.js'; import { MODEL_FRIENDLY_TEMPLATES } from '../templates/ModelFriendlyTemplates.js'; /** * Integrates rich template schemas with the parser */ export class TemplateSchemaIntegration { logger = getLogger(); templateCache = new Map(); constructor() { this.logger.debug('TemplateSchemaIntegration initialized'); } /** * Get rich template schema for entity type */ getTemplateSchema(entityType, operation) { const cacheKey = `${entityType}:${operation || 'default'}`; if (this.templateCache.has(cacheKey)) { return this.templateCache.get(cacheKey); } // Find matching template const templates = Object.values(MODEL_FRIENDLY_TEMPLATES); const template = templates.find((t) => t.entity_type === entityType && (!operation || t.template_id.includes(operation))); if (template) { this.templateCache.set(cacheKey, template); } return template; } /** * Extract field definitions with full validation rules */ extractFieldDefinitions(template) { const fieldMap = new Map(); const extractFields = (fields, prefix = '') => { for (const [fieldName, fieldSchema] of Object.entries(fields)) { const fullFieldName = prefix ? `${prefix}.${fieldName}` : fieldName; fieldMap.set(fullFieldName, fieldSchema); // Recursively extract nested fields if (fieldSchema.properties) { extractFields(fieldSchema.properties, fullFieldName); } } }; extractFields(template.fields); return fieldMap; } /** * Validate field value against schema constraints */ validateFieldValue(fieldName, value, schema) { const errors = []; const warnings = []; const suggestions = []; // Required field check if (schema.required && (value === undefined || value === null || value === '')) { errors.push(`Required field '${fieldName}' is missing or empty`); if (schema.default !== undefined) { suggestions.push(`Consider using default value: ${JSON.stringify(schema.default)}`); } } // Type validation if (value !== undefined && value !== null) { const actualType = Array.isArray(value) ? 'array' : typeof value; const expectedType = schema.type; if (expectedType && actualType !== expectedType) { errors.push(`Field '${fieldName}' type mismatch: expected ${expectedType}, got ${actualType}`); } // Enum validation if (schema.enum && !schema.enum.includes(value)) { errors.push(`Field '${fieldName}' value '${value}' not in allowed values: ${schema.enum.join(', ')}`); // Suggest closest match const closestMatch = this.findClosestEnumMatch(value, schema.enum); if (closestMatch) { suggestions.push(`Did you mean '${closestMatch}'?`); } } // Min/Max validation if (schema.min !== undefined && typeof value === 'number' && value < schema.min) { errors.push(`Field '${fieldName}' value ${value} is below minimum ${schema.min}`); } if (schema.max !== undefined && typeof value === 'number' && value > schema.max) { errors.push(`Field '${fieldName}' value ${value} exceeds maximum ${schema.max}`); } } return { isValid: errors.length === 0, errors, warnings, suggestions }; } /** * Find closest enum match using string similarity */ findClosestEnumMatch(value, enumValues) { if (typeof value !== 'string') return null; const valueLower = value.toLowerCase(); // Exact match (case insensitive) const exactMatch = enumValues.find(e => e.toLowerCase() === valueLower); if (exactMatch) return exactMatch; // Partial match const partialMatch = enumValues.find(e => e.toLowerCase().includes(valueLower) || valueLower.includes(e.toLowerCase())); if (partialMatch) return partialMatch; return null; } /** * Apply template constraints to a payload */ async applyTemplateConstraints(payload, entityType, operation) { const template = this.getTemplateSchema(entityType, operation); if (!template) { return { constrainedPayload: payload, validationResult: { isValid: false, errors: [`No template schema found for ${entityType}:${operation}`], warnings: [], suggestions: [] }, appliedConstraints: [] }; } // CRITICAL FIX: Start with complete payload to preserve all fields // The original logic only preserved fields in template schema, causing // complex fields like ab_test to be stripped during CREATE operations const constrainedPayload = { ...payload }; const appliedConstraints = []; const allErrors = []; const allWarnings = []; const allSuggestions = []; const fieldDefinitions = this.extractFieldDefinitions(template); // Validate and constrain each field that's in the template schema for (const [fieldPath, fieldSchema] of fieldDefinitions) { const value = this.getFieldValue(payload, fieldPath); const validation = this.validateFieldValue(fieldPath, value, fieldSchema); allErrors.push(...validation.errors); allWarnings.push(...validation.warnings); allSuggestions.push(...validation.suggestions); // Apply constraints ONLY if the field exists or has a default if (value !== undefined) { // Apply enum constraint if (fieldSchema.enum && !fieldSchema.enum.includes(value)) { const closestMatch = this.findClosestEnumMatch(value, fieldSchema.enum); if (closestMatch) { this.setFieldValue(constrainedPayload, fieldPath, closestMatch); appliedConstraints.push(`Corrected ${fieldPath}: '${value}' → '${closestMatch}'`); } } // Apply type coercion if (fieldSchema.type) { const coercedValue = this.coerceType(value, fieldSchema.type); if (coercedValue !== value) { this.setFieldValue(constrainedPayload, fieldPath, coercedValue); appliedConstraints.push(`Type coercion ${fieldPath}: ${typeof value}${fieldSchema.type}`); } } } // Apply defaults for missing required fields if (fieldSchema.required && value === undefined && fieldSchema.default !== undefined) { this.setFieldValue(constrainedPayload, fieldPath, fieldSchema.default); appliedConstraints.push(`Applied default for ${fieldPath}: ${JSON.stringify(fieldSchema.default)}`); } } // CRITICAL: Log field preservation for debugging const originalKeys = Object.keys(payload); const constrainedKeys = Object.keys(constrainedPayload); const preservedComplexFields = originalKeys.filter(key => !fieldDefinitions.has(key) && constrainedPayload[key] !== undefined); if (preservedComplexFields.length > 0) { this.logger.debug({ entityType, operation, preservedComplexFields, templateFields: Array.from(fieldDefinitions.keys()), hasAbTest: !!constrainedPayload.ab_test, abTestVariations: constrainedPayload.ab_test?.variations?.length || 0 }, 'TemplateSchemaIntegration: Preserved complex fields not in template schema'); } return { constrainedPayload, validationResult: { isValid: allErrors.length === 0, errors: allErrors, warnings: allWarnings, suggestions: allSuggestions }, appliedConstraints }; } /** * Get field value from payload using dot notation */ getFieldValue(payload, fieldPath) { const parts = fieldPath.split('.'); let current = payload; for (const part of parts) { if (current && typeof current === 'object' && part in current) { current = current[part]; } else { return undefined; } } return current; } /** * Set field value in payload using dot notation */ setFieldValue(payload, fieldPath, value) { const parts = fieldPath.split('.'); let current = payload; for (let i = 0; i < parts.length - 1; i++) { const part = parts[i]; if (!(part in current) || typeof current[part] !== 'object') { current[part] = {}; } current = current[part]; } current[parts[parts.length - 1]] = value; } /** * Coerce value to expected type */ coerceType(value, expectedType) { switch (expectedType) { case 'string': return String(value); case 'number': case 'integer': case 'double': const num = Number(value); return isNaN(num) ? value : num; case 'boolean': if (typeof value === 'string') { const lower = value.toLowerCase(); if (['true', '1', 'yes', 'on'].includes(lower)) return true; if (['false', '0', 'no', 'off'].includes(lower)) return false; } return value; case 'array': return Array.isArray(value) ? value : [value]; default: return value; } } } export const templateSchemaIntegration = new TemplateSchemaIntegration(); //# sourceMappingURL=TemplateSchemaIntegration.js.map