@simonecoelhosfo/optimizely-mcp-server
Version:
Optimizely MCP Server for AI assistants with integrated CLI tools
399 lines • 15.3 kB
JavaScript
/**
* Reference Resolver
*
* Handles entity reference resolution with multiple strategies:
* - By ID (direct lookup)
* - By name (search and match)
* - By key (for entities that support keys)
* - Auto-create (if not found and configured)
* - Step references (${step_id.field})
*/
import { LRUCache } from 'lru-cache';
export class ReferenceResolver {
entityCache;
lookupStrategies;
apiClient;
constructor(apiClient) {
// Initialize cache with 5-minute TTL
this.entityCache = new LRUCache({
max: 1000,
ttl: 1000 * 60 * 5, // 5 minutes
});
// Mock API client for now
this.apiClient = apiClient || this.createMockApiClient();
this.lookupStrategies = new Map();
this.initializeStrategies();
}
/**
* Resolves an entity reference with multiple fallback strategies
*/
async resolveReference(ref, context) {
// Handle step references first
if (typeof ref === 'string' && ref.startsWith('${') && ref.endsWith('}')) {
return this.resolveStepReference(ref, context);
}
// Convert to EntityReference if needed
const entityRef = typeof ref === 'string'
? { ref: { id: ref } }
: ref;
// Check cache first
const cacheKey = this.getCacheKey(entityRef, context);
const cached = this.entityCache.get(cacheKey);
if (cached)
return cached;
try {
// Try resolution strategies in order
const strategies = this.getStrategiesFor(context.entityType);
for (const strategy of strategies) {
const result = await strategy.resolve(entityRef, context);
if (result.found) {
this.entityCache.set(cacheKey, result);
return result;
}
}
// Not found - handle based on ref configuration
if (entityRef.ref.auto_create && entityRef.ref.create_template) {
return {
method: 'will_create',
template: entityRef.ref.create_template,
reason: `${context.entityType} not found, will be created`,
found: false
};
}
const error = `Unable to resolve ${context.entityType} reference: ${JSON.stringify(entityRef)}`;
return {
method: 'error',
error,
suggestion: this.getSuggestion(error, context.entityType),
found: false
};
}
catch (error) {
return {
method: 'error',
error: error instanceof Error ? error.message : String(error),
suggestion: this.getSuggestion(error instanceof Error ? error.message : String(error), context.entityType),
found: false
};
}
}
/**
* Resolve a step reference like ${step_id.field}
*/
resolveStepReference(ref, context) {
const refPattern = /^\$\{([^.]+)\.([^}]+)\}$/;
const match = ref.match(refPattern);
if (!match) {
return {
method: 'error',
error: `Invalid step reference format: ${ref}`,
suggestion: 'Use format: ${step_id.field_name}',
found: false
};
}
const [, stepId, fieldName] = match;
if (!context.availableSteps?.has(stepId)) {
return {
method: 'error',
error: `Referenced step "${stepId}" not found`,
suggestion: `Available steps: ${Array.from(context.availableSteps?.keys() || []).join(', ')}`,
found: false
};
}
const step = context.availableSteps.get(stepId);
const value = step.outputs?.[fieldName] || step.template?.outputs?.[fieldName];
if (value === undefined) {
return {
method: 'error',
error: `Field "${fieldName}" not found in step "${stepId}" outputs`,
suggestion: `Available fields: ${Object.keys(step.outputs || {}).join(', ')}`,
found: false
};
}
return {
method: 'step_reference',
id: value,
matchedBy: `${stepId}.${fieldName}`,
found: true
};
}
/**
* Initialize lookup strategies
*/
initializeStrategies() {
// ID lookup strategy
this.lookupStrategies.set('id', {
name: 'ID Lookup',
resolve: async (ref, context) => {
if (!ref.ref.id)
return { found: false, method: 'id' };
try {
const entity = await this.apiClient.getEntity(context.entityType, ref.ref.id, context.projectId);
return {
method: 'id',
entity,
id: ref.ref.id,
found: true
};
}
catch (error) {
return { found: false, method: 'id' };
}
}
});
// Name lookup strategy
this.lookupStrategies.set('name', {
name: 'Name Lookup',
resolve: async (ref, context) => {
if (!ref.ref.name)
return { found: false, method: 'name' };
const entities = await this.apiClient.listEntities(context.entityType, context.projectId, { name: ref.ref.name });
if (entities.length === 1) {
return {
method: 'name',
entity: entities[0],
id: entities[0].id,
matchedBy: 'exact_name',
found: true
};
}
if (entities.length > 1) {
// Multiple matches - try exact match
const exactMatch = entities.find(e => e.name === ref.ref.name);
if (exactMatch) {
return {
method: 'name',
entity: exactMatch,
id: exactMatch.id,
matchedBy: 'exact_name_multiple',
found: true
};
}
}
// Try fuzzy match
const fuzzyMatch = await this.fuzzyMatchByName(entities, ref.ref.name, context);
if (fuzzyMatch) {
return {
method: 'name_fuzzy',
entity: fuzzyMatch,
id: fuzzyMatch.id,
matchedBy: 'fuzzy_name',
confidence: 0.8,
found: true
};
}
return { found: false, method: 'name' };
}
});
// Key lookup strategy
this.lookupStrategies.set('key', {
name: 'Key Lookup',
resolve: async (ref, context) => {
if (!ref.ref.key)
return { found: false, method: 'key' };
// Only certain entity types support keys
const keySupported = ['experiment', 'feature', 'flag', 'event', 'attribute', 'page', 'audience'].includes(context.entityType);
if (!keySupported)
return { found: false, method: 'key' };
const entities = await this.apiClient.listEntities(context.entityType, context.projectId, { key: ref.ref.key });
if (entities.length > 0) {
return {
method: 'key',
entity: entities[0],
id: entities[0].id,
matchedBy: 'key',
found: true
};
}
return { found: false, method: 'name' };
}
});
}
/**
* Get strategies for a specific entity type
*/
getStrategiesFor(entityType) {
// Order matters - try most specific first
const order = ['id', 'key', 'name'];
return order
.map(name => this.lookupStrategies.get(name))
.filter((s) => s !== undefined);
}
/**
* Fuzzy match by name
*/
async fuzzyMatchByName(entities, targetName, context) {
if (entities.length === 0)
return null;
const normalized = targetName.toLowerCase().trim();
// Try various matching strategies
const matches = entities.filter(e => {
const entityName = (e.name || '').toLowerCase().trim();
// Exact match (case-insensitive)
if (entityName === normalized)
return true;
// Contains match
if (entityName.includes(normalized) || normalized.includes(entityName))
return true;
// Remove common prefixes/suffixes
const cleanTarget = normalized.replace(/^(test|prod|dev|staging)[-_\s]*/i, '');
const cleanEntity = entityName.replace(/^(test|prod|dev|staging)[-_\s]*/i, '');
if (cleanEntity === cleanTarget)
return true;
return false;
});
// Return best match (shortest name that matches)
if (matches.length > 0) {
return matches.sort((a, b) => a.name.length - b.name.length)[0];
}
return null;
}
/**
* Get cache key for a reference
*/
getCacheKey(ref, context) {
const parts = [
context.projectId,
context.entityType,
ref.ref.id || '',
ref.ref.name || '',
ref.ref.key || ''
];
return parts.join(':');
}
/**
* Get helpful suggestion for resolution errors
*/
getSuggestion(error, entityType) {
if (error.includes('not found')) {
return `Try using 'auto_create: true' to create the ${entityType} automatically`;
}
if (error.includes('multiple')) {
return `Multiple ${entityType}s found. Try using a more specific name or use ID instead`;
}
if (error.includes('Invalid')) {
return `Check the reference format. Use { ref: { id: "123" } } or { ref: { name: "My ${entityType}" } }`;
}
return `Verify the ${entityType} exists in the project`;
}
/**
* Resolve all references in a template
*/
async resolveTemplateReferences(template, projectId, platform = 'web') {
const resolutions = new Map();
const availableSteps = new Map();
// First pass - collect all steps
for (const step of template.steps || []) {
availableSteps.set(step.id, step);
}
// Second pass - resolve references
for (const step of template.steps || []) {
if (step.type !== 'template')
continue;
const entityType = this.extractEntityType(step.template?.system_template_id);
if (!entityType)
continue;
const context = {
projectId,
entityType,
platform,
availableSteps
};
// Check all fields that might contain references
const inputs = step.template?.inputs || {};
const referenceFields = this.getReferenceFieldsForEntity(entityType);
for (const field of referenceFields) {
const value = inputs[field];
if (!value)
continue;
const key = `${step.id}.${field}`;
if (typeof value === 'string' && value.startsWith('${')) {
// Step reference
resolutions.set(key, await this.resolveReference(value, context));
}
else if (typeof value === 'object' && value.ref) {
// Entity reference
resolutions.set(key, await this.resolveReference(value, context));
}
else if (Array.isArray(value)) {
// Array of references
for (let i = 0; i < value.length; i++) {
const item = value[i];
if (typeof item === 'object' && item.ref) {
resolutions.set(`${key}[${i}]`, await this.resolveReference(item, context));
}
}
}
}
}
return resolutions;
}
/**
* Get fields that typically contain references for an entity type
*/
getReferenceFieldsForEntity(entityType) {
const referenceFields = {
experiment: ['campaign_id', 'audience', 'audiences', 'page_ids', 'event_id', 'metrics'],
campaign: ['page_ids', 'metrics', 'audiences'],
page: [],
audience: [],
event: ['page_id'],
flag: ['environment_id'],
ruleset: ['flag_id', 'environment_id'],
rule: ['ruleset_id', 'flag_id']
};
return referenceFields[entityType] || [];
}
/**
* Extract entity type from system template ID
*/
extractEntityType(systemTemplateId) {
if (!systemTemplateId)
return null;
const patterns = [
{ regex: /optimizely_experiment_/, type: 'experiment' },
{ regex: /optimizely_campaign_/, type: 'campaign' },
{ regex: /optimizely_audience_/, type: 'audience' },
{ regex: /optimizely_event_/, type: 'event' },
{ regex: /optimizely_flag_/, type: 'flag' },
{ regex: /optimizely_ruleset_/, type: 'ruleset' },
{ regex: /optimizely_rule_/, type: 'rule' },
{ regex: /optimizely_page_/, type: 'page' }
];
for (const pattern of patterns) {
if (pattern.regex.test(systemTemplateId)) {
return pattern.type;
}
}
return null;
}
/**
* Create a mock API client for testing
*/
createMockApiClient() {
return {
getEntity: async (entityType, id, projectId) => {
// Mock implementation
return {
id,
name: `Mock ${entityType} ${id}`,
type: entityType,
project_id: projectId
};
},
listEntities: async (entityType, projectId, filters) => {
// Mock implementation
if (filters?.name) {
return [{
id: Math.floor(Math.random() * 10000),
name: filters.name,
type: entityType,
project_id: projectId
}];
}
return [];
}
};
}
}
//# sourceMappingURL=ReferenceResolver.js.map