@myea/aem-mcp-handler
Version:
Advanced AEM MCP request handler with intelligent search, multi-locale support, and comprehensive content management capabilities
1,076 lines (1,075 loc) • 48.2 kB
JavaScript
// Enhanced search utilities for MCP integration
class MCPSearchUtils {
/**
* Generate variations of search terms for better matching
*/
static generateSearchVariations(term) {
const variations = new Set();
const cleanTerm = term.trim().toLowerCase();
variations.add(cleanTerm);
variations.add(cleanTerm.replace(/\s+/g, '-'));
variations.add(cleanTerm.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase());
variations.add(cleanTerm.replace(/[-_]/g, ' '));
variations.add(cleanTerm.replace(/([a-z])([A-Z])/g, '$1 $2').toLowerCase());
variations.add(cleanTerm.replace(/[-\s_]/g, ''));
variations.add(cleanTerm.replace(/[-\s]/g, '_'));
return Array.from(variations);
}
/**
* Calculate similarity between strings for fuzzy matching
*/
static calculateSimilarity(str1, str2) {
const s1 = str1.toLowerCase();
const s2 = str2.toLowerCase();
if (s1 === s2)
return 1.0;
if (s1.includes(s2) || s2.includes(s1))
return 0.8;
const longer = s1.length > s2.length ? s1 : s2;
const shorter = s1.length > s2.length ? s2 : s1;
if (longer.length === 0)
return 1.0;
const distance = this.levenshteinDistance(longer, shorter);
return (longer.length - distance) / longer.length;
}
static levenshteinDistance(str1, str2) {
const matrix = [];
for (let i = 0; i <= str2.length; i++) {
matrix[i] = [i];
}
for (let j = 0; j <= str1.length; j++) {
matrix[0][j] = j;
}
for (let i = 1; i <= str2.length; i++) {
for (let j = 1; j <= str1.length; j++) {
if (str2.charAt(i - 1) === str1.charAt(j - 1)) {
matrix[i][j] = matrix[i - 1][j - 1];
}
else {
matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j] + 1);
}
}
}
return matrix[str2.length][str1.length];
}
/**
* Generate cross-section paths for comprehensive search - UNIVERSAL for any AEM instance
*/
static generateCrossSectionPaths(basePath) {
const paths = new Set();
// Always add the original path first
paths.add(basePath);
// Dynamic site root detection
const pathSegments = basePath.split('/').filter(s => s);
if (pathSegments.length >= 2 && pathSegments[0] === 'content') {
const siteRoot = `/${pathSegments[0]}/${pathSegments[1]}`; // e.g., /content/mysite
// Try to discover the structure dynamically by exploring common patterns
// Common locale patterns found in real AEM instances
const commonLocalePatterns = [
'language-masters/en',
'language-masters/de',
'language-masters/fr',
'language-masters/es',
'language-masters/it',
'language-masters/ja',
'language-masters/zh',
'us/en',
'ca/en',
'ca/fr',
'uk/en',
'de/de',
'fr/fr',
'es/es',
'it/it',
'jp/ja',
'cn/zh',
'au/en',
'br/pt',
'mx/es',
'en',
'de',
'fr',
'es',
'it',
'ja',
'zh',
'pt',
'ko',
'nl',
'sv',
'da',
'no',
'fi',
'pl',
'ru',
'ar',
'he',
'hi',
'th',
'vi'
];
// Add site root combinations with common patterns
for (const pattern of commonLocalePatterns) {
paths.add(`${siteRoot}/${pattern}`);
}
// Try to infer structure from the current path
if (pathSegments.length > 2) {
// Extract potential locale/language structure from current path
const remainingPath = pathSegments.slice(2).join('/');
// If path contains language-masters, try other locales
if (remainingPath.includes('language-masters')) {
const afterLanguageMasters = remainingPath.split('language-masters/')[1];
if (afterLanguageMasters) {
const currentLocale = afterLanguageMasters.split('/')[0];
// Try other common locales with same structure
const commonLocales = ['en', 'de', 'fr', 'es', 'it', 'ja', 'zh', 'pt', 'ko'];
for (const locale of commonLocales) {
if (locale !== currentLocale) {
const newPath = `${siteRoot}/language-masters/${locale}`;
paths.add(newPath);
// Also try with the rest of the path
const pathRemainder = afterLanguageMasters.substring(currentLocale.length);
if (pathRemainder) {
paths.add(`${newPath}${pathRemainder}`);
}
}
}
}
}
// If path contains country/locale pattern like us/en, try others
const countryLocaleMatch = remainingPath.match(/^([a-z]{2})\/([a-z]{2})/);
if (countryLocaleMatch) {
const [, country, locale] = countryLocaleMatch;
// Try other country/locale combinations
const commonCombos = [
'us/en', 'ca/en', 'ca/fr', 'uk/en', 'au/en',
'de/de', 'fr/fr', 'es/es', 'it/it', 'jp/ja',
'br/pt', 'mx/es', 'cn/zh', 'kr/ko', 'nl/nl'
];
for (const combo of commonCombos) {
if (combo !== `${country}/${locale}`) {
const newPath = `${siteRoot}/${combo}`;
paths.add(newPath);
// Also try with the rest of the path
const pathRemainder = remainingPath.substring(`${country}/${locale}`.length);
if (pathRemainder) {
paths.add(`${newPath}${pathRemainder}`);
}
}
}
}
// Try direct locale patterns (single language codes)
const directLocaleMatch = remainingPath.match(/^([a-z]{2})(\/|$)/);
if (directLocaleMatch) {
const [, currentLocale] = directLocaleMatch;
const commonLocales = ['en', 'de', 'fr', 'es', 'it', 'ja', 'zh', 'pt', 'ko', 'nl', 'sv', 'da', 'no'];
for (const locale of commonLocales) {
if (locale !== currentLocale) {
const newPath = `${siteRoot}/${locale}`;
paths.add(newPath);
// Also try with the rest of the path
const pathRemainder = remainingPath.substring(currentLocale.length);
if (pathRemainder) {
paths.add(`${newPath}${pathRemainder}`);
}
}
}
}
}
// Also add the site root itself for broader search
paths.add(siteRoot);
// Try common content areas that might exist
const commonContentAreas = [
'/experience-fragments',
'/content-fragments',
'/dam',
'/conf'
];
for (const area of commonContentAreas) {
if (!basePath.includes(area)) {
paths.add(`/content${area}`);
// Also try under the specific site
paths.add(`${siteRoot}${area}`);
}
}
}
// If the path doesn't follow standard /content/site structure, try to be more creative
if (!basePath.startsWith('/content/')) {
// User might have given a partial path, try to construct full paths
paths.add(`/content/${basePath}`);
// Try with common site names if it looks like a locale path
if (basePath.match(/^(language-masters|[a-z]{2})(\/|$)/)) {
// This looks like it could be a locale path, try with /content/[site]/
// We'll have to discover sites dynamically in the actual search
paths.add(`/content/*/${basePath}`); // Wildcard placeholder for dynamic discovery
}
}
return Array.from(paths);
}
}
// Enhanced search with comprehensive feedback loop
class SearchFeedbackLoop {
aemConnector;
constructor(aemConnector) {
this.aemConnector = aemConnector;
}
/**
* Comprehensive search with multiple validation rounds and feedback loops
*/
async comprehensiveSearch(searchTerm, basePath) {
const searchReport = {
strategiesUsed: [],
pathsExplored: [],
validationPasses: 0,
totalAttempts: 0,
confidence: 0,
coverage: 'none'
};
let allResults = [];
let needsRetry = false;
console.error(`[Feedback Loop] Starting comprehensive search for "${searchTerm}" at "${basePath}"`);
// Round 1: Enhanced search with variations
const enhancedResults = await this.enhancedSearchRound(searchTerm, basePath, searchReport);
allResults.push(...enhancedResults.results);
// Round 2: Deep nested search if no results
if (allResults.length === 0) {
console.error(`[Feedback Loop] No results from enhanced search, starting deep nested search`);
const deepResults = await this.deepNestedSearch(searchTerm, basePath, searchReport);
allResults.push(...deepResults.results);
}
// Round 3: Exhaustive site-wide search if still no results
if (allResults.length === 0) {
console.error(`[Feedback Loop] No results from deep search, starting exhaustive site-wide search`);
const exhaustiveResults = await this.exhaustiveSiteSearch(searchTerm, basePath, searchReport);
allResults.push(...exhaustiveResults.results);
}
// Round 4: Fallback to all pages listing and fuzzy matching
if (allResults.length === 0) {
console.error(`[Feedback Loop] No results from exhaustive search, starting fallback all-pages search`);
const fallbackResults = await this.fallbackAllPagesSearch(searchTerm, basePath, searchReport);
allResults.push(...fallbackResults.results);
}
// Validation and confidence scoring
const validationResult = await this.validateSearchResults(allResults, searchTerm, searchReport);
// Determine if retry is needed
needsRetry = this.shouldRetrySearch(validationResult, searchReport);
return {
results: validationResult.validatedResults,
searchReport,
needsRetry
};
}
/**
* Enhanced search round with multiple variations - UNIVERSAL with improved path discovery
*/
async enhancedSearchRound(searchTerm, basePath, report) {
report.strategiesUsed.push('enhanced_search_universal');
const variations = MCPSearchUtils.generateSearchVariations(searchTerm);
let paths = MCPSearchUtils.generateCrossSectionPaths(basePath);
const results = [];
// Improve path discovery for partial paths
const enhancedPaths = await this.discoverActualPaths(basePath, paths);
paths = [...new Set([...paths, ...enhancedPaths])];
// Resolve wildcard paths by discovering actual sites
const resolvedPaths = await this.resolveWildcardPaths(paths);
paths = [...new Set([...paths.filter(p => !p.includes('*')), ...resolvedPaths])];
console.log(`[Feedback Loop] Universal search across ${paths.length} dynamic paths for "${searchTerm}"`);
for (const path of paths) {
report.pathsExplored.push(path);
for (const variation of variations) {
try {
report.totalAttempts++;
const searchResult = await this.aemConnector.searchContent({
path,
type: 'cq:Page',
fulltext: variation,
limit: 100
});
if (searchResult.results && searchResult.results.length > 0) {
results.push(...searchResult.results);
console.error(`[Feedback Loop] Found ${searchResult.results.length} results with variation "${variation}" at path "${path}"`);
}
}
catch (error) {
console.error(`[Feedback Loop] Enhanced search failed for ${path} with "${variation}":`, error);
}
}
}
return { results: this.deduplicateResults(results) };
}
/**
* Discover actual paths by exploring /content structure for partial paths
*/
async discoverActualPaths(basePath, generatedPaths) {
const discoveredPaths = [];
// If the basePath doesn't start with /content/, it's likely a partial path
if (!basePath.startsWith('/content/')) {
try {
console.error(`[Feedback Loop] Discovering sites for partial path: ${basePath}`);
// List all sites under /content/
const contentChildren = await this.aemConnector.listChildren('/content');
for (const child of contentChildren) {
if (child.primaryType === 'cq:Page' || child.primaryType === 'sling:Folder') {
const potentialPath = `/content/${child.name}/${basePath}`;
discoveredPaths.push(potentialPath);
console.error(`[Feedback Loop] Discovered potential path: ${potentialPath}`);
// Also try without the leading slash if basePath has one
if (basePath.startsWith('/')) {
const cleanBasePath = basePath.substring(1);
const altPath = `/content/${child.name}/${cleanBasePath}`;
discoveredPaths.push(altPath);
}
}
}
}
catch (error) {
console.error(`[Feedback Loop] Failed to discover sites from /content:`, error);
}
}
// Also try to discover paths from partially incorrect paths like /content/language-masters/en
if (basePath.startsWith('/content/') && basePath.split('/').length === 4) {
// This might be a path like /content/language-masters/en that's missing the site name
const pathParts = basePath.split('/');
if (pathParts.length === 4) { // ['', 'content', 'language-masters', 'en']
try {
const contentChildren = await this.aemConnector.listChildren('/content');
for (const child of contentChildren) {
if (child.primaryType === 'cq:Page' || child.primaryType === 'sling:Folder') {
// Try inserting the site name: /content/language-masters/en -> /content/sitename/language-masters/en
const correctedPath = `/content/${child.name}/${pathParts[2]}/${pathParts[3]}`;
discoveredPaths.push(correctedPath);
console.error(`[Feedback Loop] Generated corrected path: ${correctedPath}`);
}
}
}
catch (error) {
console.error(`[Feedback Loop] Failed to correct malformed path:`, error);
}
}
}
return discoveredPaths;
}
/**
* Resolve wildcard paths by discovering actual sites in the AEM instance
*/
async resolveWildcardPaths(paths) {
const resolvedPaths = [];
for (const path of paths) {
if (path.includes('*')) {
try {
// Extract the pattern
const [beforeWildcard, afterWildcard] = path.split('*');
if (beforeWildcard === '/content/') {
// Discover all sites under /content/
const contentChildren = await this.aemConnector.listChildren('/content');
for (const child of contentChildren) {
if (child.primaryType === 'cq:Page' || child.primaryType === 'sling:Folder') {
const sitePath = `${beforeWildcard}${child.name}${afterWildcard}`;
resolvedPaths.push(sitePath);
}
}
}
}
catch (error) {
console.error(`[Feedback Loop] Failed to resolve wildcard path ${path}:`, error);
}
}
}
return resolvedPaths;
}
/**
* Deep nested search exploring all levels
*/
async deepNestedSearch(searchTerm, basePath, report) {
report.strategiesUsed.push('deep_nested_search');
const results = [];
const maxDepth = 5;
// Get all possible nested paths
const nestedPaths = await this.getAllNestedPaths(basePath, maxDepth);
for (const path of nestedPaths) {
report.pathsExplored.push(path);
try {
report.totalAttempts++;
// Search at each nested level
const searchResult = await this.aemConnector.searchContent({
path,
type: 'cq:Page',
limit: 50
});
if (searchResult.results && searchResult.results.length > 0) {
// Apply fuzzy matching to all found pages
const fuzzyMatches = this.applyFuzzyMatching(searchResult.results, searchTerm);
results.push(...fuzzyMatches);
console.error(`[Feedback Loop] Deep search found ${fuzzyMatches.length} fuzzy matches at "${path}"`);
}
// Also try listing all children and fuzzy matching
const children = await this.aemConnector.listChildren(path);
const pageChildren = children.filter(child => child.primaryType === 'cq:Page');
if (pageChildren.length > 0) {
const fuzzyMatches = this.applyFuzzyMatching(pageChildren, searchTerm);
results.push(...fuzzyMatches);
}
}
catch (error) {
console.error(`[Feedback Loop] Deep nested search failed for ${path}:`, error);
}
}
return { results: this.deduplicateResults(results) };
}
/**
* Exhaustive site-wide search across all known content areas - UNIVERSAL
*/
async exhaustiveSiteSearch(searchTerm, basePath, report) {
report.strategiesUsed.push('exhaustive_universal_search');
const results = [];
// Dynamic site root detection
const pathSegments = basePath.split('/').filter(s => s);
let siteRoot = '/content';
if (pathSegments.length >= 2 && pathSegments[0] === 'content') {
siteRoot = `/${pathSegments[0]}/${pathSegments[1]}`;
}
// Discover actual content areas dynamically
let contentAreas = [];
try {
// First, try to discover what's actually in the site
const siteChildren = await this.aemConnector.listChildren(siteRoot);
contentAreas = siteChildren
.filter(child => child.primaryType === 'cq:Page' || child.primaryType === 'sling:Folder')
.map(child => `${siteRoot}/${child.name}`);
// Also add the site root itself
contentAreas.unshift(siteRoot);
// Try common global areas
const globalAreas = ['/content/experience-fragments', '/content/content-fragments', '/content/dam'];
contentAreas.push(...globalAreas);
}
catch (error) {
console.error(`[Feedback Loop] Failed to discover site structure, using fallback areas:`, error);
// Fallback to some reasonable defaults
contentAreas = [
siteRoot,
'/content/experience-fragments',
'/content/content-fragments',
'/content/dam'
];
}
for (const area of contentAreas) {
report.pathsExplored.push(area);
try {
report.totalAttempts++;
// Search without filters to get everything
const searchResult = await this.aemConnector.searchContent({
path: area,
type: 'cq:Page',
limit: 200
});
if (searchResult.results && searchResult.results.length > 0) {
const fuzzyMatches = this.applyFuzzyMatching(searchResult.results, searchTerm);
results.push(...fuzzyMatches);
console.error(`[Feedback Loop] Exhaustive search found ${fuzzyMatches.length} matches in "${area}"`);
}
// Also try JCR SQL query for this area
const sqlQuery = `SELECT * FROM [cq:Page] WHERE ISDESCENDANTNODE('${area}') AND NAME() LIKE '%${searchTerm}%'`;
try {
const sqlResult = await this.aemConnector.executeJCRQuery(sqlQuery, 100);
if (sqlResult.results && sqlResult.results.length > 0) {
results.push(...sqlResult.results);
console.error(`[Feedback Loop] SQL query found ${sqlResult.results.length} additional matches in "${area}"`);
}
}
catch (sqlError) {
console.error(`[Feedback Loop] SQL query failed for ${area}:`, sqlError);
}
}
catch (error) {
console.error(`[Feedback Loop] Exhaustive search failed for ${area}:`, error);
}
}
return { results: this.deduplicateResults(results) };
}
/**
* Fallback search that lists all pages and applies fuzzy matching - UNIVERSAL
*/
async fallbackAllPagesSearch(searchTerm, basePath, report) {
report.strategiesUsed.push('fallback_all_pages_universal');
const results = [];
// Dynamic site root detection
const pathSegments = basePath.split('/').filter(s => s);
let siteRoot = '/content';
if (pathSegments.length >= 2 && pathSegments[0] === 'content') {
siteRoot = `/${pathSegments[0]}/${pathSegments[1]}`;
}
try {
report.totalAttempts++;
// List ALL pages in the site recursively
const allPages = await this.getAllPagesRecursively(siteRoot, 10); // Max depth 10
report.pathsExplored.push(`${siteRoot} (recursive, ${allPages.length} pages found)`);
console.error(`[Feedback Loop] Fallback search found ${allPages.length} total pages in site`);
// Apply comprehensive fuzzy matching
const fuzzyMatches = this.applyAdvancedFuzzyMatching(allPages, searchTerm);
results.push(...fuzzyMatches);
console.error(`[Feedback Loop] Fallback fuzzy matching produced ${fuzzyMatches.length} matches`);
}
catch (error) {
console.error(`[Feedback Loop] Fallback all-pages search failed:`, error);
}
return { results: this.deduplicateResults(results) };
}
/**
* Get all nested paths up to a certain depth
*/
async getAllNestedPaths(basePath, maxDepth) {
const paths = new Set();
const queue = [{ path: basePath, depth: 0 }];
while (queue.length > 0) {
const { path, depth } = queue.shift();
if (depth >= maxDepth)
continue;
paths.add(path);
try {
const children = await this.aemConnector.listChildren(path);
for (const child of children) {
if (child.primaryType === 'sling:Folder' || child.primaryType === 'cq:Page') {
queue.push({ path: child.path, depth: depth + 1 });
}
}
}
catch (error) {
console.error(`[Feedback Loop] Failed to get children for ${path}:`, error);
}
}
return Array.from(paths);
}
/**
* Get all pages recursively
*/
async getAllPagesRecursively(basePath, maxDepth) {
const allPages = [];
const visited = new Set();
const explore = async (path, depth) => {
if (depth >= maxDepth || visited.has(path))
return;
visited.add(path);
try {
const children = await this.aemConnector.listChildren(path);
for (const child of children) {
if (child.primaryType === 'cq:Page') {
allPages.push(child);
}
// Recurse into folders and page containers
if (child.primaryType === 'sling:Folder' ||
child.primaryType === 'cq:Page' ||
child.primaryType === 'sling:OrderedFolder') {
await explore(child.path, depth + 1);
}
}
}
catch (error) {
console.error(`[Feedback Loop] Failed to explore ${path}:`, error);
}
};
await explore(basePath, 0);
return allPages;
}
/**
* Apply fuzzy matching to results
*/
applyFuzzyMatching(results, searchTerm) {
const variations = MCPSearchUtils.generateSearchVariations(searchTerm);
const matches = [];
for (const result of results) {
const pageName = result.name || result.path.split('/').pop() || '';
let bestScore = 0;
for (const variation of variations) {
const score = MCPSearchUtils.calculateSimilarity(pageName, variation);
bestScore = Math.max(bestScore, score);
}
if (bestScore > 0.2) { // Lower threshold for comprehensive search
matches.push({
...result,
score: bestScore,
matchType: 'fuzzy'
});
}
}
return matches.sort((a, b) => b.score - a.score);
}
/**
* Apply advanced fuzzy matching with multiple criteria
*/
applyAdvancedFuzzyMatching(results, searchTerm) {
const variations = MCPSearchUtils.generateSearchVariations(searchTerm);
const matches = [];
for (const result of results) {
const pageName = result.name || result.path.split('/').pop() || '';
const fullPath = result.path || '';
let bestScore = 0;
let matchType = 'none';
// Check name similarity
for (const variation of variations) {
const nameScore = MCPSearchUtils.calculateSimilarity(pageName, variation);
const pathScore = MCPSearchUtils.calculateSimilarity(fullPath, variation) * 0.5; // Weight path matches less
const totalScore = Math.max(nameScore, pathScore);
if (totalScore > bestScore) {
bestScore = totalScore;
matchType = nameScore > pathScore ? 'name_match' : 'path_match';
}
}
// Also check if search term appears anywhere in the path
if (fullPath.toLowerCase().includes(searchTerm.toLowerCase())) {
bestScore = Math.max(bestScore, 0.6);
matchType = 'path_contains';
}
if (bestScore > 0.15) { // Very low threshold for comprehensive search
matches.push({
...result,
score: bestScore,
matchType
});
}
}
return matches.sort((a, b) => b.score - a.score);
}
/**
* Validate search results and calculate confidence
*/
async validateSearchResults(results, searchTerm, report) {
report.validationPasses++;
if (results.length === 0) {
report.confidence = 0;
report.coverage = 'none';
return { validatedResults: [], confidence: 0 };
}
// Validate each result exists and is accessible
const validatedResults = [];
for (const result of results.slice(0, 20)) { // Validate top 20 results
try {
const validation = await this.aemConnector.getPageProperties(result.path);
if (validation && validation.title !== undefined) {
validatedResults.push({
...result,
validated: true,
title: validation.title || result.title
});
}
}
catch (error) {
console.error(`[Feedback Loop] Validation failed for ${result.path}:`, error);
}
}
// Calculate confidence based on multiple factors
const exactMatches = validatedResults.filter(r => r.score > 0.8).length;
const goodMatches = validatedResults.filter(r => r.score > 0.5).length;
const totalValid = validatedResults.length;
let confidence = 0;
if (exactMatches > 0) {
confidence = 0.9;
report.coverage = 'excellent';
}
else if (goodMatches > 0) {
confidence = 0.7;
report.coverage = 'good';
}
else if (totalValid > 0) {
confidence = 0.4;
report.coverage = 'partial';
}
else {
confidence = 0;
report.coverage = 'none';
}
report.confidence = confidence;
return { validatedResults, confidence };
}
/**
* Determine if search should be retried
*/
shouldRetrySearch(validationResult, report) {
// Retry if confidence is low and we haven't tried all strategies
if (report.confidence < 0.5 && report.strategiesUsed.length < 4) {
return true;
}
// Retry if we found no results and haven't done exhaustive search
if (validationResult.validatedResults.length === 0 && !report.strategiesUsed.includes('exhaustive_universal_search')) {
return true;
}
return false;
}
/**
* Remove duplicate results
*/
deduplicateResults(results) {
const seen = new Set();
return results.filter(result => {
const key = result.path || JSON.stringify(result);
if (seen.has(key))
return false;
seen.add(key);
return true;
});
}
}
export class MCPRequestHandler {
aemConnector;
searchFeedbackLoop;
constructor(aemConnector) {
this.aemConnector = aemConnector;
this.searchFeedbackLoop = new SearchFeedbackLoop(aemConnector);
}
/**
* Handle incoming JSON-RPC method calls
*/
async handleRequest(method, params) {
console.error(`[MCP Handler] Processing method: ${method}`);
try {
switch (method) {
// Core AEM operations
case 'validateComponent':
return await this.aemConnector.validateComponent(params);
case 'updateComponent':
return await this.aemConnector.updateComponent(params);
case 'undoChanges':
return await this.aemConnector.undoChanges(params);
case 'scanPageComponents':
return await this.aemConnector.scanPageComponents(params.pagePath);
case 'fetchSites':
return await this.aemConnector.fetchSites();
case 'fetchLanguageMasters':
return await this.aemConnector.fetchLanguageMasters(params.site);
case 'fetchAvailableLocales':
return await this.aemConnector.fetchAvailableLocales(params.site, params.languageMasterPath);
case 'replicateAndPublish':
return await this.aemConnector.replicateAndPublish(params.selectedLocales, params.componentData, params.localizedOverrides);
// Text content methods
case 'getAllTextContent':
return await this.aemConnector.getAllTextContent(params.pagePath);
case 'getPageTextContent':
return await this.aemConnector.getPageTextContent(params.pagePath);
// Image content methods
case 'getPageImages':
return await this.aemConnector.getPageImages(params.pagePath);
case 'updateImagePath':
if (!params.componentPath || !params.newImagePath) {
return {
success: false,
error: {
code: 400,
message: 'Both componentPath and newImagePath are required'
}
};
}
const updateResult = await this.aemConnector.updateImagePath(params.componentPath, params.newImagePath);
return {
...updateResult,
method: 'updateImagePath',
params: {
componentPath: params.componentPath,
newImagePath: params.newImagePath
}
};
// New comprehensive content scanning method
case 'getPageContent':
return await this.aemConnector.getPageContent(params.pagePath);
// Legacy/compatibility methods
case 'listPages':
// Map to listChildren for pages with depth support
return await this.listPages(params.siteRoot || params.path || '/content', params.depth || 1, params.limit || 20);
case 'getNodeContent':
return await this.aemConnector.getNodeContent(params.path, params.depth || 1);
case 'listChildren':
return await this.aemConnector.listChildren(params.path);
case 'getPageProperties':
return await this.aemConnector.getPageProperties(params.pagePath);
case 'searchContent':
return await this.searchContent(params.path, params.type, params.fulltext, params.limit);
case 'executeJCRQuery':
return await this.aemConnector.executeJCRQuery(params.query, params.limit);
case 'getAssetMetadata':
return await this.aemConnector.getAssetMetadata(params.assetPath);
// Status and info methods
case 'getStatus':
return this.getWorkflowStatus(params.workflowId);
case 'listMethods':
return { methods: this.getAvailableMethods() };
case 'enhancedPageSearch':
return await this.enhancedPageSearch(params.searchTerm, params.basePath);
default:
throw new Error(`Unknown method: ${method}`);
}
}
catch (error) {
console.error(`[MCP] Error handling ${method}:`, error);
return { error: error.message, method, params };
}
}
/**
* Get list of pages under a site root with optional depth
*/
async listPages(siteRoot, depth = 1, limit = 20) {
try {
const children = await this.aemConnector.listChildren(siteRoot, depth);
// Filter to only pages (nodes with jcr:content)
const pages = children
.filter(child => child.primaryType === 'cq:Page')
.map(child => child.path)
.slice(0, limit);
const result = {
success: true,
siteRoot: siteRoot,
pages: pages,
totalCount: pages.length
};
// If no pages found with listChildren, try searchContent as fallback
if (pages.length === 0) {
try {
const searchResult = await this.aemConnector.searchContent({
path: siteRoot,
type: 'cq:Page',
limit: limit
});
if (searchResult.results && searchResult.results.length > 0) {
result.pages = searchResult.results.map((hit) => hit.path);
result.totalCount = searchResult.results.length;
}
}
catch (searchError) {
console.error('Fallback search failed:', searchError);
// Keep original empty result if search fails
}
}
return result;
}
catch (error) {
throw new Error(`Failed to list pages: ${error.message}`);
}
}
/**
* Mock workflow status (since we don't have real workflow tracking)
*/
getWorkflowStatus(workflowId) {
return {
success: true,
workflowId: workflowId,
status: 'completed',
message: 'Mock workflow status - always returns completed',
timestamp: new Date().toISOString()
};
}
/**
* Get list of available MCP methods
*/
getAvailableMethods() {
return [
{
name: 'validateComponent',
description: 'Validate component changes before applying them',
parameters: ['locale', 'page_path', 'component', 'props']
},
{
name: 'updateComponent',
description: 'Update component properties in AEM',
parameters: ['componentPath', 'properties']
},
{
name: 'undoChanges',
description: 'Undo the last component changes',
parameters: ['job_id']
},
{
name: 'scanPageComponents',
description: 'Scan a page to discover all components and their properties',
parameters: ['pagePath']
},
{
name: 'fetchSites',
description: 'Get all available sites in AEM',
parameters: []
},
{
name: 'fetchLanguageMasters',
description: 'Get language masters for a specific site',
parameters: ['site']
},
{
name: 'fetchAvailableLocales',
description: 'Get available locales for a site and language master',
parameters: ['site', 'languageMasterPath']
},
{
name: 'replicateAndPublish',
description: 'Replicate and publish content to selected locales',
parameters: ['selectedLocales', 'componentData', 'localizedOverrides']
},
{
name: 'listPages',
description: 'List all pages under a site root',
parameters: ['siteRoot']
},
{
name: 'getNodeContent',
description: 'Get JCR node content by path',
parameters: ['path', 'depth?']
},
{
name: 'listChildren',
description: 'List all child nodes of a path',
parameters: ['path']
},
{
name: 'getPageProperties',
description: 'Get page properties and metadata',
parameters: ['pagePath']
},
{
name: 'searchContent',
description: 'Search content using QueryBuilder',
parameters: ['type?', 'fulltext?', 'path?', 'property?', 'property.value?', 'limit?']
},
{
name: 'executeJCRQuery',
description: 'Execute JCR SQL query',
parameters: ['query', 'limit?']
},
{
name: 'getAssetMetadata',
description: 'Get asset metadata from DAM',
parameters: ['assetPath']
},
{
name: 'getStatus',
description: 'Get workflow status by ID',
parameters: ['workflowId']
},
{
name: 'listMethods',
description: 'Get list of available methods',
parameters: []
},
{
name: 'getAllTextContent',
description: 'Get all text content from a page including titles, text components, and descriptions',
parameters: ['pagePath']
},
{
name: 'getPageTextContent',
description: 'Get text content from a specific page',
parameters: ['pagePath']
},
{
name: 'getPageImages',
description: 'Get all images from a page, including those within Experience Fragments',
parameters: ['pagePath']
},
{
name: 'getPageContent',
description: 'Get all content from a page including Experience Fragments and Content Fragments',
parameters: ['pagePath']
},
{
name: 'updateImagePath',
description: 'Update the image path for an image component and verify the update',
parameters: ['componentPath', 'newImagePath']
}
];
}
/**
* Enhanced page search with comprehensive feedback loop
*/
async enhancedPageSearch(searchTerm, basePath) {
try {
console.error(`[MCP Enhanced Search] Starting comprehensive search for "${searchTerm}" at "${basePath}"`);
// Run comprehensive search with feedback loop
const searchResult = await this.searchFeedbackLoop.comprehensiveSearch(searchTerm, basePath);
// If retry is needed and confidence is low, run one more comprehensive round
if (searchResult.needsRetry && searchResult.searchReport.confidence < 0.3) {
console.error(`[MCP Enhanced Search] Running retry round due to low confidence (${searchResult.searchReport.confidence})`);
// Try with simplified/modified search terms
const simplifiedTerms = this.generateAlternativeSearchTerms(searchTerm);
for (const term of simplifiedTerms) {
const retryResult = await this.searchFeedbackLoop.comprehensiveSearch(term, basePath);
if (retryResult.results.length > 0) {
// Merge results
searchResult.results.push(...retryResult.results);
searchResult.searchReport.strategiesUsed.push(`retry_with_term_${term}`);
searchResult.searchReport.confidence = Math.max(searchResult.searchReport.confidence, retryResult.searchReport.confidence);
break;
}
}
}
return {
success: true,
results: searchResult.results,
searchStrategy: searchResult.searchReport.strategiesUsed.join(' + '),
pathsSearched: searchResult.searchReport.pathsExplored,
variationsUsed: MCPSearchUtils.generateSearchVariations(searchTerm),
totalResults: searchResult.results.length,
searchReport: searchResult.searchReport,
confidence: searchResult.searchReport.confidence,
coverage: searchResult.searchReport.coverage
};
}
catch (error) {
throw new Error(`Enhanced search with feedback loop failed: ${error.message}`);
}
}
/**
* Generate alternative search terms for retry attempts
*/
generateAlternativeSearchTerms(searchTerm) {
const alternatives = [
searchTerm.substring(0, Math.ceil(searchTerm.length * 0.7)), // 70% of term
searchTerm.split(' ')[0], // First word only
searchTerm.replace(/[^a-zA-Z]/g, ''), // Letters only
searchTerm.split('-')[0], // Before hyphen
searchTerm.split('_')[0], // Before underscore
].filter(term => term.length > 2);
return [...new Set(alternatives)]; // Remove duplicates
}
/**
* Enhanced search content with comprehensive fallback strategies
*/
async searchContent(path, type = 'cq:Page', fulltext, limit = 50) {
try {
// If fulltext is provided, try enhanced search first
if (fulltext && fulltext.trim()) {
try {
const enhancedResult = await this.enhancedPageSearch(fulltext, path);
if (enhancedResult.results.length > 0) {
return {
success: true,
results: enhancedResult.results,
query: { path, type, fulltext, limit },
searchMetadata: {
strategy: enhancedResult.searchStrategy,
pathsSearched: enhancedResult.pathsSearched,
variationsUsed: enhancedResult.variationsUsed
}
};
}
}
catch (enhancedError) {
console.error('Enhanced search failed, falling back to standard search:', enhancedError);
}
}
// Fall back to standard search
const result = await this.aemConnector.searchContent({
path,
type,
fulltext,
limit
});
return {
success: true,
results: result.results || [],
query: { path, type, fulltext, limit },
searchMetadata: {
strategy: 'standard_search',
pathsSearched: [path],
variationsUsed: fulltext ? [fulltext] : []
}
};
}
catch (error) {
throw new Error(`Search failed: ${error.message}`);
}
}
}