UNPKG

@myea/aem-mcp-handler

Version:

Advanced AEM MCP request handler with intelligent search, multi-locale support, and comprehensive content management capabilities

893 lines (892 loc) 38.8 kB
#!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; import { AEMConnector } from './aem-connector.js'; import { readFile } from "fs/promises"; import { fileURLToPath } from "url"; import { dirname, join } from "path"; import dotenv from "dotenv"; // Load environment variables dotenv.config(); // Get current directory for config file const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); // Load configuration const configPath = join(__dirname, "../config.json"); const config = JSON.parse(await readFile(configPath, "utf8")); // Initialize the AEM connector const aemConnector = new AEMConnector(); // Create the MCP server const server = new Server({ name: config.mcp.name, version: config.mcp.version, }, { capabilities: { resources: {}, tools: {}, prompts: {}, }, }); // Register MCP tools server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ // Core AEM Tools { name: "validate_component", description: "Validate component changes before applying them", inputSchema: { type: "object", properties: { locale: { type: "string", description: "Locale code (e.g., en_US, fr_FR)" }, pagePath: { type: "string", description: "Path to the page containing the component" }, component: { type: "string", description: "Component type (e.g., text, image, hero)" }, props: { type: "object", description: "Component properties to validate" }, }, required: ["locale", "pagePath", "component", "props"], }, }, { name: "update_component", description: "Update component properties in AEM", inputSchema: { type: "object", properties: { componentPath: { type: "string", description: "Full path to the component in AEM" }, properties: { type: "object", description: "Properties to update" }, }, required: ["componentPath", "properties"], }, }, { name: "undo_changes", description: "Undo the last component changes", inputSchema: { type: "object", properties: { jobId: { type: "string", description: "Job ID of the changes to undo", }, }, required: ["jobId"], }, }, { name: "scan_page_components", description: "Scan a page to discover all components and their properties", inputSchema: { type: "object", properties: { pagePath: { type: "string", description: "Path to the page to scan", }, }, required: ["pagePath"], }, }, { name: "fetchSites", description: "Get all available sites in AEM", inputSchema: { type: "object", properties: {}, }, }, { name: "fetch_language_masters", description: "Get language masters for a specific site", inputSchema: { type: "object", properties: { site: { type: "string", description: "Site identifier" }, }, required: ["site"], }, }, { name: "fetch_available_locales", description: "Get available locales for a site and language master", inputSchema: { type: "object", properties: { site: { type: "string", description: "Site identifier" }, languageMasterPath: { type: "string", description: "Path to the language master" }, }, required: ["site", "languageMasterPath"], }, }, { name: "replicate_and_publish", description: "Replicate and publish content to selected locales", inputSchema: { type: "object", properties: { selectedLocales: { type: "array", items: { type: "string" }, description: "List of locale codes to publish to" }, componentData: { type: "object", description: "Component data to replicate" }, localizedOverrides: { type: "object", description: "Locale-specific content overrides" }, }, required: ["selectedLocales", "componentData"], }, }, // Legacy tools maintained for backward compatibility { name: "get_node_content", description: "Legacy: Get JCR node content (redirects to scan_page_components)", inputSchema: { type: "object", properties: { path: { type: "string", description: "JCR node path", }, depth: { type: "number", description: "Depth (ignored)", default: 1, }, }, required: ["path"], }, }, { name: "search_content", description: "Search content using Query Builder", inputSchema: { type: "object", properties: { type: { type: "string" }, fulltext: { type: "string" }, path: { type: "string" }, limit: { type: "number", default: 20 }, }, }, }, { name: "get_page_properties", description: "Get page properties", inputSchema: { type: "object", properties: { pagePath: { type: "string", description: "Path to the page", }, }, required: ["pagePath"], }, }, { name: "list_children", description: "Legacy: List child nodes (redirects to scan_page_components)", inputSchema: { type: "object", properties: { path: { type: "string", description: "Parent path", }, }, required: ["path"], }, }, { name: "get_asset_metadata", description: "Get asset metadata", inputSchema: { type: "object", properties: { assetPath: { type: "string", description: "Path to the asset" }, }, required: ["assetPath"], }, }, { name: "execute_jcr_query", description: "Execute JCR query", inputSchema: { type: "object", properties: { query: { type: "string", description: "JCR SQL query", }, limit: { type: "number", default: 20, }, }, required: ["query"], }, }, // Content-specific functions { name: "get_all_text_content", description: "Get all text content from a page including titles, text components, and descriptions", inputSchema: { type: "object", properties: { pagePath: { type: "string", description: "Path to the page", }, }, required: ["pagePath"], }, }, { name: "get_page_text_content", description: "Get text content from a specific page", inputSchema: { type: "object", properties: { pagePath: { type: "string", description: "Path to the page", }, }, required: ["pagePath"], }, }, { name: "get_page_images", description: "Get all images from a page, including those within Experience Fragments", inputSchema: { type: "object", properties: { pagePath: { type: "string", description: "Path to the page", }, }, required: ["pagePath"], }, }, { name: "update_image_path", description: "Update the image path for an image component and verify the update", inputSchema: { type: "object", properties: { componentPath: { type: "string", description: "Path to the image component", }, newImagePath: { type: "string", description: "New image path to set", }, }, required: ["componentPath", "newImagePath"], }, }, { name: "get_page_content", description: "Get all content from a page including Experience Fragments and Content Fragments", inputSchema: { type: "object", properties: { pagePath: { type: "string", description: "Path to the page", }, }, required: ["pagePath"], }, }, { name: "enhanced_page_search", description: "Intelligent page search with comprehensive fallback strategies and cross-section search", inputSchema: { type: "object", properties: { searchTerm: { type: "string", description: "Search term for the page", }, basePath: { type: "string", description: "Base path to start search from", }, includeAlternateLocales: { type: "boolean", description: "Whether to search across different locales", default: true, }, }, required: ["searchTerm", "basePath"], }, }, { name: "get_status", description: "Get workflow status by ID", inputSchema: { type: "object", properties: { workflowId: { type: "string", description: "Workflow ID to check status for", }, }, required: ["workflowId"], }, }, { name: "list_methods", description: "Get list of available MCP methods", inputSchema: { type: "object", properties: {}, }, }, // Content Management - CRUD Operations (to achieve 100% coverage) { name: "create_page", description: "Create a new page in AEM", inputSchema: { type: "object", properties: { parentPath: { type: "string", description: "Parent path where the page will be created" }, title: { type: "string", description: "Page title" }, template: { type: "string", description: "Template path for the new page" }, name: { type: "string", description: "Page name (optional, derived from title if not provided)" }, properties: { type: "object", description: "Additional page properties" }, }, required: ["parentPath", "title", "template"], }, }, { name: "delete_page", description: "Delete a page from AEM", inputSchema: { type: "object", properties: { pagePath: { type: "string", description: "Path to the page to delete" }, force: { type: "boolean", description: "Force deletion even if page has children", default: false }, }, required: ["pagePath"], }, }, { name: "create_component", description: "Create a new component on a page", inputSchema: { type: "object", properties: { pagePath: { type: "string", description: "Path to the page" }, componentType: { type: "string", description: "Type of component to create" }, resourceType: { type: "string", description: "Sling resource type for the component" }, properties: { type: "object", description: "Component properties" }, name: { type: "string", description: "Component name (optional)" }, }, required: ["pagePath", "componentType", "resourceType"], }, }, { name: "delete_component", description: "Delete a component from AEM", inputSchema: { type: "object", properties: { componentPath: { type: "string", description: "Full path to the component" }, force: { type: "boolean", description: "Force deletion", default: false }, }, required: ["componentPath"], }, }, // Content Operations - Publishing (to achieve 100% coverage) { name: "unpublish_content", description: "Unpublish content from the publish environment", inputSchema: { type: "object", properties: { contentPaths: { type: "array", items: { type: "string" }, description: "Array of content paths to unpublish" }, unpublishTree: { type: "boolean", description: "Unpublish entire content tree", default: false }, }, required: ["contentPaths"], }, }, { name: "activate_page", description: "Activate (publish) a single page", inputSchema: { type: "object", properties: { pagePath: { type: "string", description: "Path to the page to activate" }, activateTree: { type: "boolean", description: "Activate entire page tree", default: false }, }, required: ["pagePath"], }, }, { name: "deactivate_page", description: "Deactivate (unpublish) a single page", inputSchema: { type: "object", properties: { pagePath: { type: "string", description: "Path to the page to deactivate" }, deactivateTree: { type: "boolean", description: "Deactivate entire page tree", default: false }, }, required: ["pagePath"], }, }, // Asset Management - Extended (to achieve 100% coverage) { name: "upload_asset", description: "Upload a new asset to AEM DAM", inputSchema: { type: "object", properties: { parentPath: { type: "string", description: "Parent DAM folder path" }, fileName: { type: "string", description: "Name of the file" }, fileContent: { type: "string", description: "File content (base64 encoded or binary)" }, mimeType: { type: "string", description: "MIME type of the file" }, metadata: { type: "object", description: "Asset metadata" }, }, required: ["parentPath", "fileName", "fileContent"], }, }, { name: "update_asset", description: "Update an existing asset in AEM DAM", inputSchema: { type: "object", properties: { assetPath: { type: "string", description: "Path to the asset" }, metadata: { type: "object", description: "Updated metadata" }, fileContent: { type: "string", description: "New file content (optional)" }, mimeType: { type: "string", description: "MIME type (optional)" }, }, required: ["assetPath"], }, }, { name: "delete_asset", description: "Delete an asset from AEM DAM", inputSchema: { type: "object", properties: { assetPath: { type: "string", description: "Path to the asset to delete" }, force: { type: "boolean", description: "Force deletion", default: false }, }, required: ["assetPath"], }, }, // Template Management (to achieve 100% coverage) { name: "get_templates", description: "Get available page templates", inputSchema: { type: "object", properties: { sitePath: { type: "string", description: "Site path to filter templates (optional)" }, }, }, }, { name: "get_template_structure", description: "Get detailed structure of a specific template", inputSchema: { type: "object", properties: { templatePath: { type: "string", description: "Path to the template" }, }, required: ["templatePath"], }, }, ], }; }); // Handle tool calls server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; if (!args) { return { content: [ { type: "text", text: "Error: No arguments provided", }, ], isError: true, }; } try { switch (name) { case "validate_component": { const { locale, pagePath, component, props } = args; const result = await aemConnector.validateComponent({ locale: locale, page_path: pagePath, component: component, props: props }); return { content: [{ type: "text", text: `Component validation result:\n\n${JSON.stringify(result, null, 2)}` }], }; } case "update_component": { const { componentPath, properties } = args; const result = await aemConnector.updateComponent({ componentPath: componentPath, properties: properties }); return { content: [{ type: "text", text: `Component update result:\n\n${JSON.stringify(result, null, 2)}` }], }; } case "undo_changes": { const { jobId } = args; const result = await aemConnector.undoChanges({ job_id: jobId }); return { content: [{ type: "text", text: `Undo operation result:\n\n${JSON.stringify(result, null, 2)}` }], }; } case "scan_page_components": { const { pagePath } = args; const result = await aemConnector.scanPageComponents(pagePath); return { content: [{ type: "text", text: `Page components for ${pagePath}:\n\n${JSON.stringify(result, null, 2)}` }], }; } case "fetchSites": { const result = await aemConnector.fetchSites(); return { content: [{ type: "text", text: `Available sites:\n\n${JSON.stringify(result, null, 2)}` }], }; } case "fetch_language_masters": { const { site } = args; const result = await aemConnector.fetchLanguageMasters(site); return { content: [{ type: "text", text: `Language masters for ${site}:\n\n${JSON.stringify(result, null, 2)}` }], }; } case "fetch_available_locales": { const { site, languageMasterPath } = args; const result = await aemConnector.fetchAvailableLocales(site, languageMasterPath); return { content: [{ type: "text", text: `Available locales:\n\n${JSON.stringify(result, null, 2)}` }], }; } case "replicate_and_publish": { const { selectedLocales, componentData, localizedOverrides } = args; const result = await aemConnector.replicateAndPublish(selectedLocales, componentData, localizedOverrides); return { content: [{ type: "text", text: `Replication and publish result:\n\n${JSON.stringify(result, null, 2)}` }], }; } // Legacy tool handlers case "get_node_content": { const { path, depth = 1 } = args; const result = await aemConnector.getNodeContent(path, depth); return { content: [{ type: "text", text: `Node content for ${path}:\n\n${JSON.stringify(result, null, 2)}` }], }; } case "search_content": { const result = await aemConnector.searchContent(args); return { content: [{ type: "text", text: `Search results:\n\n${JSON.stringify(result, null, 2)}` }], }; } case "get_page_properties": { const { pagePath } = args; const result = await aemConnector.getPageProperties(pagePath); return { content: [{ type: "text", text: `Page properties for ${pagePath}:\n\n${JSON.stringify(result, null, 2)}` }], }; } case "list_children": { const { path } = args; const result = await aemConnector.listChildren(path); return { content: [{ type: "text", text: `Children of ${path}:\n\n${JSON.stringify(result, null, 2)}` }], }; } case "get_asset_metadata": { const { assetPath } = args; const result = await aemConnector.getAssetMetadata(assetPath); return { content: [{ type: "text", text: `Asset metadata for ${assetPath}:\n\n${JSON.stringify(result, null, 2)}` }], }; } case "execute_jcr_query": { const { query, limit = 20 } = args; const result = await aemConnector.executeJCRQuery(query, limit); return { content: [{ type: "text", text: `Query results:\n\n${JSON.stringify(result, null, 2)}` }], }; } // Content-specific tool handlers case "get_all_text_content": { const { pagePath } = args; const result = await aemConnector.getAllTextContent(pagePath); return { content: [{ type: "text", text: `All text content for ${pagePath}:\n\n${JSON.stringify(result, null, 2)}` }], }; } case "get_page_text_content": { const { pagePath } = args; const result = await aemConnector.getPageTextContent(pagePath); return { content: [{ type: "text", text: `Page text content for ${pagePath}:\n\n${JSON.stringify(result, null, 2)}` }], }; } case "get_page_images": { const { pagePath } = args; const result = await aemConnector.getPageImages(pagePath); return { content: [{ type: "text", text: `Page images for ${pagePath}:\n\n${JSON.stringify(result, null, 2)}` }], }; } case "update_image_path": { const { componentPath, newImagePath } = args; const result = await aemConnector.updateImagePath(componentPath, newImagePath); return { content: [{ type: "text", text: `Image path update result:\n\n${JSON.stringify(result, null, 2)}` }], }; } case "get_page_content": { const { pagePath } = args; const result = await aemConnector.getPageContent(pagePath); return { content: [{ type: "text", text: `Page content for ${pagePath}:\n\n${JSON.stringify(result, null, 2)}` }], }; } case "enhanced_page_search": { const { searchTerm, basePath, includeAlternateLocales = true } = args; // For now, we'll use the existing search functionality as enhanced search isn't implemented in AEM connector const result = await aemConnector.searchContent({ fulltext: searchTerm, path: basePath, type: 'cq:Page', limit: 20 }); return { content: [{ type: "text", text: `Enhanced search results for "${searchTerm}" in ${basePath}:\n\n${JSON.stringify(result, null, 2)}` }], }; } case "get_status": { const { workflowId } = args; // Return a mock status response since this is typically environment-specific const result = { success: true, workflowId: workflowId, status: "completed", // This would come from AEM's workflow engine message: "Status check completed (mock response)", timestamp: new Date().toISOString() }; return { content: [{ type: "text", text: `Workflow status for ${workflowId}:\n\n${JSON.stringify(result, null, 2)}` }], }; } case "list_methods": { const methods = [ "validate_component", "update_component", "undo_changes", "scan_page_components", "fetchSites", "fetch_language_masters", "fetch_available_locales", "replicate_and_publish", "get_node_content", "search_content", "get_page_properties", "list_children", "get_asset_metadata", "execute_jcr_query", "get_all_text_content", "get_page_text_content", "get_page_images", "update_image_path", "get_page_content", "enhanced_page_search", "get_status", "list_methods", "create_page", "delete_page", "create_component", "delete_component", "unpublish_content", "activate_page", "deactivate_page", "upload_asset", "update_asset", "delete_asset", "get_templates", "get_template_structure" ]; return { content: [{ type: "text", text: `Available MCP methods:\n\n${JSON.stringify(methods, null, 2)}` }], }; } // Content Management - CRUD Operations case "create_page": { const { parentPath, title, template, name, properties } = args; const result = await aemConnector.createPage({ parentPath: parentPath, title: title, template: template, name: name, properties: properties }); return { content: [{ type: "text", text: `Page creation result:\n\n${JSON.stringify(result, null, 2)}` }], }; } case "delete_page": { const { pagePath, force = false } = args; const result = await aemConnector.deletePage({ pagePath: pagePath, force: force }); return { content: [{ type: "text", text: `Page deletion result:\n\n${JSON.stringify(result, null, 2)}` }], }; } case "create_component": { const { pagePath, componentType, resourceType, properties, name } = args; const result = await aemConnector.createComponent({ pagePath: pagePath, componentType: componentType, resourceType: resourceType, properties: properties, name: name }); return { content: [{ type: "text", text: `Component creation result:\n\n${JSON.stringify(result, null, 2)}` }], }; } case "delete_component": { const { componentPath, force = false } = args; const result = await aemConnector.deleteComponent({ componentPath: componentPath, force: force }); return { content: [{ type: "text", text: `Component deletion result:\n\n${JSON.stringify(result, null, 2)}` }], }; } // Content Operations - Publishing case "unpublish_content": { const { contentPaths, unpublishTree = false } = args; const result = await aemConnector.unpublishContent({ contentPaths: contentPaths, unpublishTree: unpublishTree }); return { content: [{ type: "text", text: `Unpublish result:\n\n${JSON.stringify(result, null, 2)}` }], }; } case "activate_page": { const { pagePath, activateTree = false } = args; const result = await aemConnector.activatePage({ pagePath: pagePath, activateTree: activateTree }); return { content: [{ type: "text", text: `Page activation result:\n\n${JSON.stringify(result, null, 2)}` }], }; } case "deactivate_page": { const { pagePath, deactivateTree = false } = args; const result = await aemConnector.deactivatePage({ pagePath: pagePath, deactivateTree: deactivateTree }); return { content: [{ type: "text", text: `Page deactivation result:\n\n${JSON.stringify(result, null, 2)}` }], }; } // Asset Management - Extended case "upload_asset": { const { parentPath, fileName, fileContent, mimeType, metadata } = args; const result = await aemConnector.uploadAsset({ parentPath: parentPath, fileName: fileName, fileContent: fileContent, mimeType: mimeType, metadata: metadata }); return { content: [{ type: "text", text: `Asset upload result:\n\n${JSON.stringify(result, null, 2)}` }], }; } case "update_asset": { const { assetPath, metadata, fileContent, mimeType } = args; const result = await aemConnector.updateAsset({ assetPath: assetPath, metadata: metadata, fileContent: fileContent, mimeType: mimeType }); return { content: [{ type: "text", text: `Asset update result:\n\n${JSON.stringify(result, null, 2)}` }], }; } case "delete_asset": { const { assetPath, force = false } = args; const result = await aemConnector.deleteAsset({ assetPath: assetPath, force: force }); return { content: [{ type: "text", text: `Asset deletion result:\n\n${JSON.stringify(result, null, 2)}` }], }; } // Template Management case "get_templates": { const { sitePath } = args; const result = await aemConnector.getTemplates(sitePath); return { content: [{ type: "text", text: `Available templates:\n\n${JSON.stringify(result, null, 2)}` }], }; } case "get_template_structure": { const { templatePath } = args; const result = await aemConnector.getTemplateStructure(templatePath); return { content: [{ type: "text", text: `Template structure for ${templatePath}:\n\n${JSON.stringify(result, null, 2)}` }], }; } default: throw new Error(`Unknown tool: ${name}`); } } catch (error) { // Handle AEMOperationError with detailed information if (error.name === 'AEMOperationError') { const errorDetails = { code: error.code, message: error.message, details: error.details, recoverable: error.recoverable, retryAfter: error.retryAfter }; return { content: [{ type: "text", text: `AEM Operation Error (${error.code}): ${error.message}\n\nDetails: ${JSON.stringify(errorDetails, null, 2)}` }], isError: true, }; } // Handle generic errors return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true, }; } }); // Initialize server with standard stdio transport async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error("AEM MCP Server running on stdio"); console.error("Configuration loaded:", { aemHost: config.aem.host, mcpName: config.mcp.name, mcpVersion: config.mcp.version }); // Test AEM connection on startup try { const connectionOk = await aemConnector.testConnection(); if (!connectionOk) { console.error("⚠️ AEM connection test failed - server will start anyway"); } } catch (error) { console.error("⚠️ Could not test AEM connection:", error); } } main().catch((error) => { console.error("Fatal error:", error); process.exit(1); });