UNPKG

sfcc-dev-mcp

Version:

MCP server for Salesforce B2C Commerce Cloud development assistance including logs, debugging, and development tools

549 lines 24.4 kB
/** * SFCC Documentation Client * * This module provides functionality to query and retrieve SFCC class documentation * from the converted Markdown files. It enables AI assistants to access detailed * information about SFCC classes, methods, properties, and usage examples. */ import fs from 'fs/promises'; import path from 'path'; import { PathResolver } from '../utils/path-resolver.js'; import { CacheManager } from '../utils/cache.js'; import { Logger } from '../utils/logger.js'; // Create a logger instance for this module const logger = new Logger('DocsClient'); export class SFCCDocumentationClient { docsPath; classCache = new Map(); cacheManager; initialized = false; constructor() { this.docsPath = PathResolver.getDocsPath(); this.cacheManager = new CacheManager(); } /** * Initialize the documentation client by scanning all available classes */ async initialize() { if (this.initialized) { return; } try { await this.scanDocumentation(); this.initialized = true; } catch (error) { throw new Error(`Failed to initialize SFCC documentation: ${error}`); } } /** * Scan the docs directory and index all SFCC classes */ async scanDocumentation() { const packages = await fs.readdir(this.docsPath, { withFileTypes: true }); for (const packageDir of packages) { if (!packageDir.isDirectory()) { continue; } const packageName = packageDir.name; const packagePath = path.join(this.docsPath, packageName); try { const files = await fs.readdir(packagePath); for (const file of files) { if (file.endsWith('.md')) { const className = file.replace('.md', ''); const filePath = path.join(packagePath, file); try { // Enhanced security validation - validate file name before path operations if (!file || typeof file !== 'string') { logger.warn(`Warning: Invalid file name type: ${file}`); continue; } // Prevent null bytes and dangerous characters in the file name itself if (file.includes('\0') || file.includes('\x00')) { logger.warn(`Warning: File name contains null bytes: ${file}`); continue; } // Prevent path traversal sequences in the file name if (file.includes('..') || file.includes('/') || file.includes('\\')) { logger.warn(`Warning: File name contains path traversal sequences: ${file}`); continue; } // Only allow alphanumeric characters, underscores, hyphens, and dots for file names if (!/^[a-zA-Z0-9_.-]+$/.test(file)) { logger.warn(`Warning: File name contains invalid characters: ${file}`); continue; } // Additional security validation - ensure the resolved path is within the package directory const resolvedPath = path.resolve(filePath); const resolvedPackagePath = path.resolve(packagePath); const resolvedDocsPath = path.resolve(this.docsPath); // Ensure the file is within the package directory and docs directory if (!resolvedPath.startsWith(resolvedPackagePath) || !resolvedPath.startsWith(resolvedDocsPath)) { logger.warn(`Warning: File path outside allowed directory: ${file}`); continue; } // Ensure the file still ends with .md after path resolution if (!resolvedPath.toLowerCase().endsWith('.md')) { logger.warn(`Warning: File does not reference a markdown file: ${file}`); continue; } const content = await fs.readFile(resolvedPath, 'utf-8'); // Basic content validation if (!content.trim()) { logger.warn(`Warning: Empty documentation file: ${file}`); continue; } // Check for binary content if (content.includes('\0')) { logger.warn(`Warning: Binary content detected in: ${file}`); continue; } this.classCache.set(`${packageName}.${className}`, { className, packageName, filePath, content, }); } catch (fileError) { logger.warn(`Warning: Could not read file ${file}: ${fileError}`); } } } } catch (error) { logger.warn(`Warning: Could not read package ${packageName}: ${error}`); } } } /** * Get a list of all available SFCC classes */ async getAvailableClasses() { await this.initialize(); return Array.from(this.classCache.keys()).sort(); } /** * Search for classes by name (partial matching) */ async searchClasses(query) { await this.initialize(); // Check cache first const cacheKey = `search:classes:${query.toLowerCase()}`; const cachedResult = this.cacheManager.getSearchResults(cacheKey); if (cachedResult) { return cachedResult; } const lowercaseQuery = query.toLowerCase(); const results = Array.from(this.classCache.keys()) .filter(className => className.toLowerCase().includes(lowercaseQuery)) .sort() // Return the official . notation for class names (e.g., dw.content.ContentMgr) .map(className => className.replace(/_/g, '.')); // Cache the results this.cacheManager.setSearchResults(cacheKey, results); return results; } /** * Get the raw documentation content for a class */ async getClassDocumentation(className) { await this.initialize(); // Normalize class name to support both formats (dw.content.ContentMgr -> dw_content.ContentMgr) const normalizedClassName = this.normalizeClassName(className); // Check cache first const cacheKey = `content:${normalizedClassName}`; const cachedContent = this.cacheManager.getFileContent(cacheKey); if (cachedContent !== undefined) { return cachedContent || null; } // Try exact match first with normalized name let classInfo = this.classCache.get(normalizedClassName); // If not found, try to find by class name only (without package) if (!classInfo) { const simpleClassName = this.extractSimpleClassName(normalizedClassName); const matches = Array.from(this.classCache.entries()) .filter(([, info]) => info.className === simpleClassName); if (matches.length === 1) { classInfo = matches[0][1]; } else if (matches.length > 1) { throw new Error(`Multiple classes found with name "${simpleClassName}": ${matches.map(([key]) => key).join(', ')}`); } } const content = classInfo ? classInfo.content : null; // Cache the result (including null results to avoid repeated lookups) this.cacheManager.setFileContent(cacheKey, content ?? ''); return content; } /** * Normalize class name to handle both dot and underscore formats * Examples: * - dw.content.ContentMgr -> dw_content.ContentMgr * - dw_content.ContentMgr -> dw_content.ContentMgr (unchanged) * - ContentMgr -> ContentMgr (unchanged) */ normalizeClassName(className) { // If it contains dots but not underscores in the package part, convert dots to underscores if (className.includes('.') && !className.includes('_')) { // Split by dots and convert package parts (all but last) to use underscores const parts = className.split('.'); if (parts.length > 1) { const packageParts = parts.slice(0, -1); const simpleClassName = parts[parts.length - 1]; return `${packageParts.join('_')}.${simpleClassName}`; } } return className; } /** * Extract simple class name from full class name * Examples: * - dw_content.ContentMgr -> ContentMgr * - ContentMgr -> ContentMgr */ extractSimpleClassName(className) { const parts = className.split('.'); return parts[parts.length - 1]; } /** * Parse class documentation and extract structured information */ async getClassDetails(className) { // Check cache first const cacheKey = `details:${className}`; const cachedDetails = this.cacheManager.getClassDetails(cacheKey); if (cachedDetails !== undefined) { return cachedDetails; } const content = await this.getClassDocumentation(className); if (!content) { // Cache null results to avoid repeated parsing attempts this.cacheManager.setClassDetails(cacheKey, null); return null; } const details = this.parseClassContent(content); // Cache the parsed details this.cacheManager.setClassDetails(cacheKey, details); return details; } /** * Parse markdown content and extract structured class information */ parseClassContent(content) { const lines = content.split('\n'); let currentSection = ''; let className = ''; let packageName = ''; let description = ''; const properties = []; const methods = []; const inheritance = []; let constructorInfo = ''; for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); // Extract package name if (line.startsWith('## Package:')) { packageName = line.replace('## Package:', '').trim(); } // Extract class name if (line.startsWith('# ') && !line.startsWith('## ')) { className = line.replace('# ', '').replace('Class ', '').trim(); } // Track current section if (line.startsWith('## ')) { currentSection = line.replace('## ', '').trim(); } // Extract description if (currentSection === 'Description' && line && !line.startsWith('#')) { description += `${line} `; } // Extract inheritance hierarchy if (currentSection === 'Inheritance Hierarchy' && line.includes('-')) { const hierarchyItem = line.replace(/^[\s-]*/, '').trim(); if (hierarchyItem) { inheritance.push(hierarchyItem); } } // Extract properties if (currentSection === 'Properties' && line.startsWith('### ')) { const propName = line.replace('### ', '').trim(); let propType = ''; let propDesc = ''; const modifiers = []; let deprecated = false; let deprecationMessage = ''; // Look for type and description in following lines for (let j = i + 1; j < lines.length && !lines[j].startsWith('#'); j++) { const nextLine = lines[j].trim(); if (nextLine.startsWith('**Type:**')) { const typeMatch = nextLine.match(/\*\*Type:\*\*\s*(.+)/); if (typeMatch) { const typeInfo = typeMatch[1]; propType = typeInfo.split(' ')[0]; if (typeInfo.includes('(Read Only)')) { modifiers.push('Read Only'); } if (typeInfo.includes('(Static)')) { modifiers.push('Static'); } } } else if (nextLine.startsWith('**Deprecated:**')) { deprecated = true; // Check if there's a message on the same line const sameLineMessage = nextLine.replace('**Deprecated:**', '').trim(); if (sameLineMessage) { deprecationMessage = sameLineMessage; } else { // Look for the deprecation message on subsequent lines until next ** marker const depLines = []; for (let k = j + 1; k < lines.length && !lines[k].startsWith('#'); k++) { const depLine = lines[k].trim(); if (depLine.startsWith('**') && !depLine.startsWith('**Deprecated:**')) { break; // Stop at next ** marker } if (depLine && !depLine.startsWith('---')) { depLines.push(depLine); } } deprecationMessage = depLines.join(' ').trim(); } } else if (nextLine && !nextLine.startsWith('**') && !nextLine.startsWith('#')) { propDesc += `${nextLine} `; } } properties.push({ name: propName, type: propType, description: propDesc.trim(), modifiers: modifiers.length > 0 ? modifiers : undefined, deprecated: deprecated || undefined, deprecationMessage: deprecationMessage || undefined, }); } // Extract methods if ((currentSection === 'Method Summary' || currentSection === 'Method Details') && line.startsWith('### ')) { const methodName = line.replace('### ', '').trim(); let signature = ''; let methodDesc = ''; let deprecated = false; let deprecationMessage = ''; // Look for signature and description in following lines for (let j = i + 1; j < lines.length && !lines[j].startsWith('#'); j++) { const nextLine = lines[j].trim(); if (nextLine.startsWith('**Signature:**')) { const sigMatch = nextLine.match(/\*\*Signature:\*\*\s*`(.+)`/); if (sigMatch) { signature = sigMatch[1]; } } else if (nextLine.startsWith('**Description:**')) { methodDesc = nextLine.replace('**Description:**', '').trim(); } else if (nextLine.startsWith('**Deprecated:**')) { deprecated = true; // Check if there's a message on the same line const sameLineMessage = nextLine.replace('**Deprecated:**', '').trim(); if (sameLineMessage) { deprecationMessage = sameLineMessage; } else { // Look for the deprecation message on subsequent lines until next ** marker const depLines = []; for (let k = j + 1; k < lines.length && !lines[k].startsWith('#'); k++) { const depLine = lines[k].trim(); if (depLine.startsWith('**') && !depLine.startsWith('**Deprecated:**')) { break; // Stop at next ** marker } if (depLine && !depLine.startsWith('---')) { depLines.push(depLine); } } deprecationMessage = depLines.join(' ').trim(); } } else if (nextLine && !nextLine.startsWith('**') && !nextLine.startsWith('#') && !nextLine.startsWith('---')) { if (!methodDesc && !nextLine.includes('Signature:')) { methodDesc += `${nextLine} `; } } } methods.push({ name: methodName, signature: signature || methodName, description: methodDesc.trim(), deprecated: deprecated || undefined, deprecationMessage: deprecationMessage || undefined, }); } // Extract constructor info if (currentSection === 'Constructor Summary' && line && !line.startsWith('#')) { constructorInfo += `${line} `; } } return { className: className.trim(), packageName: packageName.trim(), description: description.trim(), properties, methods, inheritance: inheritance.length > 0 ? inheritance : undefined, constructorInfo: constructorInfo.trim() || undefined, }; } /** * Parse markdown content and extract referenced types from a class */ extractReferencedTypes(content) { const referencedTypes = new Set(); const lines = content.split('\n'); for (const line of lines) { // Extract types from property definitions const propTypeMatch = line.match(/\*\*Type:\*\*\s*([A-Za-z][A-Za-z0-9.]*)/); if (propTypeMatch) { const type = propTypeMatch[1]; // Only include SFCC types (those that start with uppercase or contain dots) if (/^[A-Z]/.test(type) || type.includes('.')) { referencedTypes.add(type); } } // Extract return types from method signatures const methodReturnMatch = line.match(/:\s*([A-Za-z][A-Za-z0-9.]*)\s*$/); if (methodReturnMatch) { const type = methodReturnMatch[1]; if (/^[A-Z]/.test(type) || type.includes('.')) { referencedTypes.add(type); } } // Extract parameter types from method signatures const paramMatches = line.match(/\(\s*([^)]+)\s*\)/); if (paramMatches) { const params = paramMatches[1]; const typeMatches = params.match(/:\s*([A-Za-z][A-Za-z0-9.]*)/g); if (typeMatches) { typeMatches.forEach(match => { const type = match.replace(/:\s*/, ''); if (/^[A-Z]/.test(type) || type.includes('.')) { referencedTypes.add(type); } }); } } } return Array.from(referencedTypes); } /** * Get class details with optional expansion of referenced types */ async getClassDetailsExpanded(className, expand = false) { // Check cache first for expanded details const cacheKey = `details-expanded:${className}:${expand}`; const cachedResult = this.cacheManager.getClassDetails(cacheKey); if (cachedResult !== undefined) { return cachedResult; } const classDetails = await this.getClassDetails(className); if (!classDetails || !expand) { const result = classDetails; this.cacheManager.setClassDetails(cacheKey, result); return result; } // Get the raw content to extract referenced types const content = await this.getClassDocumentation(className); if (!content) { this.cacheManager.setClassDetails(cacheKey, classDetails); return classDetails; } const referencedTypeNames = this.extractReferencedTypes(content); const referencedTypes = []; // Get details for each referenced type for (const typeName of referencedTypeNames) { // Skip if it's the same class to avoid circular references if (typeName === className || typeName.endsWith(`.${className}`)) { continue; } try { const typeDetails = await this.getClassDetails(typeName); if (typeDetails) { referencedTypes.push(typeDetails); } // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { // Silently skip types that can't be found logger.warn(`Could not find details for referenced type: ${typeName}`); } } const result = { ...classDetails, referencedTypes: referencedTypes.length > 0 ? referencedTypes : undefined, }; // Cache the result this.cacheManager.setClassDetails(cacheKey, result); return result; } /** * Get methods for a specific class */ async getClassMethods(className) { const details = await this.getClassDetails(className); return details?.methods ?? []; } /** * Get properties for a specific class */ async getClassProperties(className) { const details = await this.getClassDetails(className); return details?.properties ?? []; } /** * Search for methods across all classes */ async searchMethods(methodName) { await this.initialize(); // Check cache first const cacheKey = `search:methods:${methodName.toLowerCase()}`; const cachedResult = this.cacheManager.getMethodSearch(cacheKey); if (cachedResult) { return cachedResult; } const results = []; for (const [fullClassName] of this.classCache) { const methods = await this.getClassMethods(fullClassName); for (const method of methods) { if (method.name.toLowerCase().includes(methodName.toLowerCase())) { results.push({ className: fullClassName, method, }); } } } // Cache the search results this.cacheManager.setMethodSearch(cacheKey, results); return results; } /** * Get cache statistics for monitoring performance */ getCacheStats() { return this.cacheManager.getAllStats(); } /** * Clear all caches */ clearCache() { this.cacheManager.clearAll(); } /** * Cleanup resources and destroy caches */ destroy() { this.cacheManager.destroy(); } } //# sourceMappingURL=docs-client.js.map