UNPKG

@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
// 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}`); } } }