Skip to main content
Glama
utility-operations.ts13.4 kB
/** * Utility Operations Module * Handles utility operations like node content access, children listing, and content extraction */ import { AxiosInstance } from 'axios'; import { IAEMConnector, NodeContentResponse, ChildrenResponse, TextContentResponse, ImagesResponse, SitesResponse, LanguageMastersResponse, LocalesResponse, ILogger, AEMConfig } from '../interfaces/index.js'; import { AEMOperationError, createAEMError, handleAEMHttpError, safeExecute, createSuccessResponse, AEM_ERROR_CODES } from '../error-handler.js'; export class UtilityOperations implements Partial<IAEMConnector> { constructor( private httpClient: AxiosInstance, private logger: ILogger, private config: AEMConfig ) {} /** * Get JCR node content as raw JSON for a given path and depth */ async getNodeContent(path: string, depth = 1): Promise<NodeContentResponse> { return safeExecute<NodeContentResponse>(async () => { const response = await this.httpClient.get(`${path}.json`, { params: { ':depth': depth.toString() } }); return { path, depth, content: response.data, timestamp: new Date().toISOString() }; }, 'getNodeContent'); } /** * List direct children under a path using AEM's JSON API */ async listChildren(path: string): Promise<ChildrenResponse> { return safeExecute<ChildrenResponse>(async () => { // First try direct JSON API approach try { const response = await this.httpClient.get(`${path}.1.json`); const children: Array<{ name: string; path: string; primaryType: string; title: string; lastModified?: string; resourceType?: string; }> = []; if (response.data && typeof response.data === 'object') { Object.entries(response.data).forEach(([key, value]: [string, any]) => { // Skip JCR system properties and metadata if (key.startsWith('jcr:') || key.startsWith('sling:') || key.startsWith('cq:') || key.startsWith('rep:') || key.startsWith('oak:') || key === 'jcr:content') { return; } if (value && typeof value === 'object') { const childPath = `${path}/${key}`; children.push({ name: key, path: childPath, primaryType: value['jcr:primaryType'] || 'nt:unstructured', title: value['jcr:content']?.['jcr:title'] || value['jcr:title'] || key, lastModified: value['jcr:content']?.['cq:lastModified'] || value['cq:lastModified'], resourceType: value['jcr:content']?.['sling:resourceType'] || value['sling:resourceType'] }); } }); } return { children }; } catch (error: any) { // Fallback to QueryBuilder for cq:Page nodes specifically if (error.response?.status === 404 || error.response?.status === 403) { const response = await this.httpClient.get('/bin/querybuilder.json', { params: { path: path, type: 'cq:Page', 'p.nodedepth': '1', 'p.limit': '1000', 'p.hits': 'full' }, }); const children = (response.data.hits || []).map((hit: any) => ({ name: hit.name || hit.path?.split('/').pop(), path: hit.path, primaryType: hit['jcr:primaryType'] || 'cq:Page', title: hit['jcr:content/jcr:title'] || hit.title || hit.name, lastModified: hit['jcr:content/cq:lastModified'], resourceType: hit['jcr:content/sling:resourceType'] })); return { children }; } throw error; } }, 'listChildren'); } /** * Get all available sites in AEM */ async fetchSites(): Promise<SitesResponse> { return safeExecute<SitesResponse>(async () => { const response = await this.httpClient.get('/content.json', { params: { ':depth': '2' } }); const sites: Array<{ name: string; path: string; title: string; template?: string; lastModified?: string; }> = []; Object.entries(response.data).forEach(([key, value]: [string, any]) => { if (key.startsWith('jcr:') || key.startsWith('sling:')) return; if (value && typeof value === 'object' && value['jcr:content']) { sites.push({ name: key, path: `/content/${key}`, title: value['jcr:content']['jcr:title'] || key, template: value['jcr:content']['cq:template'], lastModified: value['jcr:content']['cq:lastModified'], }); } }); return createSuccessResponse({ sites, totalCount: sites.length, }, 'fetchSites') as SitesResponse; }, 'fetchSites'); } /** * Get language masters for a specific site */ async fetchLanguageMasters(site: string): Promise<LanguageMastersResponse> { return safeExecute<LanguageMastersResponse>(async () => { const response = await this.httpClient.get(`/content/${site}.json`, { params: { ':depth': '3' } }); const masters: Array<{ name: string; path: string; title: string; language: string; }> = []; Object.entries(response.data).forEach(([key, value]: [string, any]) => { if (key.startsWith('jcr:') || key.startsWith('sling:')) return; if (value && typeof value === 'object' && value['jcr:content']) { masters.push({ name: key, path: `/content/${key}`, title: value['jcr:content']['jcr:title'] || key, language: value['jcr:content']['jcr:language'] || 'en', }); } }); return createSuccessResponse({ site, languageMasters: masters, }, 'fetchLanguageMasters') as LanguageMastersResponse; }, 'fetchLanguageMasters'); } /** * Get available locales for a site and language master */ async fetchAvailableLocales(site: string, languageMasterPath: string): Promise<LocalesResponse> { return safeExecute<LocalesResponse>(async () => { const response = await this.httpClient.get(`${languageMasterPath}.json`, { params: { ':depth': '2' } }); const locales: Array<{ name: string; title: string; language: string; }> = []; Object.entries(response.data).forEach(([key, value]: [string, any]) => { if (key.startsWith('jcr:') || key.startsWith('sling:')) return; if (value && typeof value === 'object') { locales.push({ name: key, title: value['jcr:content']?.['jcr:title'] || key, language: value['jcr:content']?.['jcr:language'] || key, }); } }); return createSuccessResponse({ site, languageMasterPath, availableLocales: locales, }, 'fetchAvailableLocales') as LocalesResponse; }, 'fetchAvailableLocales'); } /** * Get all text content from a page including titles, text components, and descriptions */ async getAllTextContent(pagePath: string): Promise<TextContentResponse> { return safeExecute<TextContentResponse>(async () => { const response = await this.httpClient.get(`${pagePath}.infinity.json`); const textContent: Array<{ path: string; title?: string; text?: string; description?: string; }> = []; const processNode = (node: any, nodePath: string) => { if (!node || typeof node !== 'object') return; if (node['text'] || node['jcr:title'] || node['jcr:description']) { textContent.push({ path: nodePath, title: node['jcr:title'], text: node['text'], description: node['jcr:description'], }); } Object.entries(node).forEach(([key, value]) => { if (typeof value === 'object' && value !== null && !key.startsWith('rep:') && !key.startsWith('oak:')) { const childPath = nodePath ? `${nodePath}/${key}` : key; processNode(value, childPath); } }); }; if (response.data['jcr:content']) { processNode(response.data['jcr:content'], 'jcr:content'); } else { processNode(response.data, pagePath); } return createSuccessResponse({ pagePath, textContent, }, 'getAllTextContent') as TextContentResponse; }, 'getAllTextContent'); } /** * Get text content from a specific page (alias for getAllTextContent) */ async getPageTextContent(pagePath: string): Promise<TextContentResponse> { return this.getAllTextContent(pagePath); } /** * Get all images from a page, including those within Experience Fragments */ async getPageImages(pagePath: string): Promise<ImagesResponse> { return safeExecute<ImagesResponse>(async () => { const response = await this.httpClient.get(`${pagePath}.infinity.json`); const images: Array<{ path: string; fileReference?: string; src?: string; alt?: string; title?: string; }> = []; const processNode = (node: any, nodePath: string) => { if (!node || typeof node !== 'object') return; if (node['fileReference'] || node['src']) { images.push({ path: nodePath, fileReference: node['fileReference'], src: node['src'], alt: node['alt'] || node['altText'], title: node['jcr:title'] || node['title'], }); } Object.entries(node).forEach(([key, value]) => { if (typeof value === 'object' && value !== null && !key.startsWith('rep:') && !key.startsWith('oak:')) { const childPath = nodePath ? `${nodePath}/${key}` : key; processNode(value, childPath); } }); }; if (response.data['jcr:content']) { processNode(response.data['jcr:content'], 'jcr:content'); } else { processNode(response.data, pagePath); } return createSuccessResponse({ pagePath, images, }, 'getPageImages') as ImagesResponse; }, 'getPageImages'); } /** * Get page content including Experience Fragments and Content Fragments */ async getPageContent(pagePath: string): Promise<{ success: boolean; operation: string; timestamp: string; data: { pagePath: string; content: Record<string, unknown>; }; }> { return safeExecute(async () => { const response = await this.httpClient.get(`${pagePath}.infinity.json`); return createSuccessResponse({ pagePath, content: response.data, }, 'getPageContent'); }, 'getPageContent'); } /** * Update the image path for an image component and verify the update */ async updateImagePath(componentPath: string, newImagePath: string): Promise<{ success: boolean; operation: string; timestamp: string; data: { message: string; path: string; properties: Record<string, unknown>; updatedProperties: Record<string, unknown>; verification: { success: boolean; propertiesChanged: number; timestamp: string; }; }; }> { return safeExecute(async () => { // This would typically call the component operations module // For now, we'll implement a simplified version const formData = new URLSearchParams(); formData.append('fileReference', newImagePath); const response = await this.httpClient.post(componentPath, formData, { headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, }); // Verify the update const verificationResponse = await this.httpClient.get(`${componentPath}.json`); return createSuccessResponse({ message: 'Image path updated successfully', path: componentPath, properties: { fileReference: newImagePath }, updatedProperties: verificationResponse.data, verification: { success: true, propertiesChanged: 1, timestamp: new Date().toISOString(), }, }, 'updateImagePath'); }, 'updateImagePath'); } /** * Undo changes (placeholder implementation) * Note: AEM MCP does not support undo/rollback. Use AEM version history. */ async undoChanges(request: { jobId: string }): Promise<{ success: boolean; operation: string; timestamp: string; data: { message: string; request: { jobId: string }; timestamp: string; }; }> { return createSuccessResponse({ message: 'undoChanges is not implemented. Please use AEM version history for undo/rollback.', request, timestamp: new Date().toISOString(), }, 'undoChanges'); } }

Implementation Reference

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/indrasishbanerjee/aem-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server