UNPKG

sfcc-dev-mcp

Version:

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

278 lines 12.3 kB
/** * SFCC Best Practices Client * * Provides access to SFCC development best practices documentation including * cartridge creation, ISML templates, job framework, LocalServiceRegistry, * OCAPI hooks, SCAPI hooks, SCAPI custom endpoints, SFRA controllers, and SFRA models. */ import * as fs from 'fs/promises'; import * as path from 'path'; import { PathResolver } from '../utils/path-resolver.js'; import { CacheManager } from '../utils/cache.js'; import { Logger } from '../utils/logger.js'; /** * Client for accessing SFCC best practices documentation */ export class SFCCBestPracticesClient { cache; docsPath; logger; constructor() { this.cache = new CacheManager(); this.docsPath = PathResolver.getBestPracticesPath(); this.logger = Logger.getChildLogger('BestPracticesClient'); } /** * Get all available best practice guides */ async getAvailableGuides() { const cacheKey = 'best-practices:available-guides'; const cached = this.cache.getSearchResults(cacheKey); if (cached) { return cached; } const guides = [ { name: 'cartridge_creation', title: 'Cartridge Creation Best Practices', description: 'Instructions and best practices for creating, configuring, and deploying custom SFRA cartridges', }, { name: 'isml_templates', title: 'ISML Templates Best Practices', description: 'Comprehensive best practices for developing ISML templates within the SFRA framework, including security, performance, and maintainability guidelines', }, { name: 'job_framework', title: 'Job Framework Best Practices', description: 'Comprehensive guide for developing custom jobs in the SFCC Job Framework, covering both task-oriented and chunk-oriented approaches with performance optimization and debugging strategies', }, { name: 'localserviceregistry', title: 'LocalServiceRegistry Best Practices', description: 'Comprehensive guide for creating server-to-server integrations in SFCC using dw.svc.LocalServiceRegistry, including configuration patterns, callback implementation, OAuth flows, and reusable service module patterns', }, { name: 'ocapi_hooks', title: 'OCAPI Hooks Best Practices', description: 'Best practices for implementing OCAPI hooks in Salesforce B2C Commerce Cloud', }, { name: 'scapi_hooks', title: 'SCAPI Hooks Best Practices', description: 'Essential best practices for implementing SCAPI hooks with AI development assistance', }, { name: 'scapi_custom_endpoint', title: 'Custom SCAPI Endpoint Best Practices', description: 'Best practices for creating custom SCAPI endpoints in B2C Commerce Cloud', }, { name: 'sfra_controllers', title: 'SFRA Controllers Best Practices', description: 'Best practices and code patterns for developing SFRA controllers', }, { name: 'sfra_models', title: 'SFRA Models Best Practices', description: 'Best practices for developing SFRA models in Salesforce B2C Commerce Cloud', }, { name: 'performance', title: 'Performance and Stability Best Practices', description: 'Comprehensive performance optimization strategies, coding standards, and stability guidelines for SFCC development including caching, index-friendly APIs, and job development', }, { name: 'security', title: 'Security Best Practices', description: 'Comprehensive security best practices for SFCC development covering SFRA Controllers, OCAPI/SCAPI Hooks, and Custom SCAPI Endpoints with OWASP compliance guidelines', }, ]; this.cache.setSearchResults(cacheKey, guides); return guides; } /** * Get a specific best practice guide */ async getBestPracticeGuide(guideName) { const cacheKey = `best-practices:guide:${guideName}`; const cached = this.cache.getFileContent(cacheKey); if (cached) { return JSON.parse(cached); } try { // Enhanced security validation - validate guideName before path construction if (!guideName || typeof guideName !== 'string') { throw new Error('Invalid guide name: must be a non-empty string'); } // Prevent null bytes and dangerous characters in the guide name itself if (guideName.includes('\0') || guideName.includes('\x00')) { throw new Error('Invalid guide name: contains null bytes'); } // Prevent path traversal sequences in the guide name if (guideName.includes('..') || guideName.includes('/') || guideName.includes('\\')) { throw new Error('Invalid guide name: contains path traversal sequences'); } // Only allow alphanumeric characters, underscores, and hyphens if (!/^[a-zA-Z0-9_-]+$/.test(guideName)) { throw new Error('Invalid guide name: contains invalid characters'); } const filePath = path.join(this.docsPath, `${guideName}.md`); // Additional security validation - ensure the resolved path is within the docs directory const resolvedPath = path.resolve(filePath); const resolvedDocsPath = path.resolve(this.docsPath); if (!resolvedPath.startsWith(resolvedDocsPath)) { throw new Error('Invalid guide name: path outside allowed directory'); } // Ensure the file still ends with .md after path resolution if (!resolvedPath.toLowerCase().endsWith('.md')) { throw new Error('Invalid guide name: must reference a markdown file'); } const content = await fs.readFile(resolvedPath, 'utf-8'); // Basic content validation if (!content.trim()) { throw new Error(`Empty best practice guide: ${guideName}`); } // Check for binary content if (content.includes('\0')) { throw new Error(`Invalid content in best practice guide: ${guideName}`); } const lines = content.split('\n'); const title = lines.find(line => line.startsWith('#'))?.replace('#', '').trim() ?? guideName; // Extract sections (## headers) const sections = lines .filter(line => line.startsWith('##')) .map(line => line.replace('##', '').trim()); // Extract description (first paragraph after title) const descriptionStart = lines.findIndex(line => line.startsWith('#')) + 1; const descriptionEnd = lines.findIndex((line, index) => index > descriptionStart && (line.startsWith('#') || line.trim() === '')); const description = lines .slice(descriptionStart, descriptionEnd > -1 ? descriptionEnd : descriptionStart + 3) .join(' ') .trim(); const guide = { title, description, sections, content, }; this.cache.setFileContent(cacheKey, JSON.stringify(guide)); return guide; } catch (error) { this.logger.error(`Error reading best practice guide ${guideName}:`, error); return null; } } /** * Search across all best practices for specific terms */ async searchBestPractices(query) { const cacheKey = `best-practices:search:${query.toLowerCase()}`; const cached = this.cache.getSearchResults(cacheKey); if (cached) { return cached; } const guides = await this.getAvailableGuides(); const results = []; for (const guide of guides) { const guideContent = await this.getBestPracticeGuide(guide.name); if (!guideContent) { continue; } const matches = []; const lines = guideContent.content.split('\n'); let currentSection = ''; for (let i = 0; i < lines.length; i++) { const line = lines[i]; if (line.startsWith('##')) { currentSection = line.replace('##', '').trim(); } if (line.toLowerCase().includes(query.toLowerCase())) { // Get context around the match const start = Math.max(0, i - 2); const end = Math.min(lines.length, i + 3); const context = lines.slice(start, end).join('\n'); matches.push({ section: currentSection || 'Introduction', content: context, }); } } if (matches.length > 0) { results.push({ guide: guide.name, title: guide.title, matches, }); } } this.cache.setSearchResults(cacheKey, results); return results; } /** * Get hook reference tables for OCAPI/SCAPI hooks */ async getHookReference(guideName) { if (!guideName.includes('hooks')) { return []; } const cacheKey = `best-practices:hook-reference:${guideName}`; const cached = this.cache.getSearchResults(cacheKey); if (cached) { return cached; } const guide = await this.getBestPracticeGuide(guideName); if (!guide) { return []; } const reference = []; const lines = guide.content.split('\n'); let currentCategory = ''; let inTable = false; let hooks = []; for (const line of lines) { // Look for hook reference sections if (line.match(/^###?\s+(Shop API Hooks|Data API Hooks|Shopper.*Hooks|.*API Hooks)/i)) { if (currentCategory && hooks.length > 0) { reference.push({ category: currentCategory, hooks: [...hooks] }); } currentCategory = line.replace(/^#+\s*/, ''); hooks = []; inTable = false; } // Detect table headers if (line.includes('API Endpoint') && line.includes('Hook')) { inTable = true; continue; } // Skip separator line if (line.match(/^\|[\s\-|]+\|$/)) { continue; } // Parse table rows if (inTable && line.startsWith('|') && !line.includes('**')) { const parts = line.split('|').map(p => p.trim()).filter(p => p); if (parts.length >= 2) { const endpoint = parts[0].replace(/`/g, ''); const hookPoints = parts[1].split(',').map(h => h.replace(/`/g, '').trim()); const signature = parts[2] ? parts[2].replace(/`/g, '') : undefined; if (endpoint && hookPoints.length > 0) { hooks.push({ endpoint, hookPoints, signature }); } } } // End table when we hit a new section if (inTable && line.startsWith('#')) { inTable = false; } } // Add last category if (currentCategory && hooks.length > 0) { reference.push({ category: currentCategory, hooks }); } this.cache.setSearchResults(cacheKey, reference); return reference; } } //# sourceMappingURL=best-practices-client.js.map