@simonecoelhosfo/optimizely-mcp-server
Version:
Optimizely MCP Server for AI assistants with integrated CLI tools
257 lines • 10.9 kB
JavaScript
/**
* 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