UNPKG

@aiondadotcom/mcp-salesforce

Version:

Model Context Protocol (MCP) server for Salesforce integration using OAuth authentication

348 lines (313 loc) 10 kB
import fs from 'fs'; import path from 'path'; import { logger } from '../utils/debug.js'; class SalesforceTimeMachine { constructor(backupRootDirectory = './backups') { this.backupRootDirectory = backupRootDirectory; } /** * Find all available backups sorted by timestamp (newest first) */ async getAllBackups() { try { if (!fs.existsSync(this.backupRootDirectory)) { return []; } const items = fs.readdirSync(this.backupRootDirectory); const backups = []; for (const item of items) { const itemPath = path.join(this.backupRootDirectory, item); if (fs.statSync(itemPath).isDirectory() && item.startsWith('salesforce-backup-')) { const manifestPath = path.join(itemPath, 'backup-manifest.json'); if (fs.existsSync(manifestPath)) { try { const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); backups.push({ path: itemPath, timestamp: manifest.backupInfo.timestamp, date: new Date(manifest.backupInfo.timestamp), manifest: manifest }); } catch (err) { logger.warn(`Could not read manifest for backup ${item}:`, err.message); } } } } // Sort by timestamp (newest first) return backups.sort((a, b) => b.date - a.date); } catch (error) { throw new Error(`Failed to get backups: ${error.message}`); } } /** * Find backup closest to a specific date */ async getBackupAtDate(targetDate) { const backups = await this.getAllBackups(); if (backups.length === 0) { return null; } const target = new Date(targetDate); // Find the backup with timestamp <= target date (most recent backup before target) for (const backup of backups) { if (backup.date <= target) { return backup; } } // If no backup before target date, return the oldest backup return backups[backups.length - 1]; } /** * Query historical data from a specific backup */ async queryBackupData(backupPath, objectType, filters = {}) { try { const dataPath = path.join(backupPath, 'data', `${objectType}.json`); if (!fs.existsSync(dataPath)) { return { success: false, error: `No data found for object type '${objectType}' in backup`, availableObjects: this.getAvailableObjectTypes(backupPath) }; } const data = JSON.parse(fs.readFileSync(dataPath, 'utf8')); let filteredData = data; // Apply filters if (Object.keys(filters).length > 0) { filteredData = data.filter(record => { return Object.entries(filters).every(([field, value]) => { if (typeof value === 'string' && value.includes('*')) { // Wildcard matching const regex = new RegExp(value.replace(/\*/g, '.*'), 'i'); return regex.test(record[field]); } return record[field] === value; }); }); } return { success: true, data: filteredData, count: filteredData.length, backupTimestamp: this.getBackupTimestamp(backupPath), objectType: objectType }; } catch (error) { return { success: false, error: `Failed to query backup data: ${error.message}` }; } } /** * Time Machine query: Find data as it existed at a specific point in time */ async queryAtPointInTime(targetDate, objectType, filters = {}) { try { const backup = await this.getBackupAtDate(targetDate); if (!backup) { return { success: false, error: 'No backups found for the specified date range' }; } const result = await this.queryBackupData(backup.path, objectType, filters); if (result.success) { result.snapshotDate = backup.timestamp; result.message = `Data as it existed on ${backup.timestamp}`; } return result; } catch (error) { return { success: false, error: `Time Machine query failed: ${error.message}` }; } } /** * Compare data between two points in time */ async compareDataOverTime(startDate, endDate, objectType, filters = {}) { try { const startBackup = await this.getBackupAtDate(startDate); const endBackup = await this.getBackupAtDate(endDate); if (!startBackup || !endBackup) { return { success: false, error: 'Could not find backups for the specified date range' }; } const startResult = await this.queryBackupData(startBackup.path, objectType, filters); const endResult = await this.queryBackupData(endBackup.path, objectType, filters); if (!startResult.success || !endResult.success) { return { success: false, error: 'Failed to query data from one or both backups' }; } // Simple comparison - can be enhanced with more sophisticated diff logic const comparison = { startSnapshot: { date: startBackup.timestamp, count: startResult.count, data: startResult.data }, endSnapshot: { date: endBackup.timestamp, count: endResult.count, data: endResult.data }, changes: { countDifference: endResult.count - startResult.count, // Add more sophisticated change detection here } }; return { success: true, comparison: comparison, objectType: objectType }; } catch (error) { return { success: false, error: `Data comparison failed: ${error.message}` }; } } /** * Search across all backups for historical changes to a specific record */ async getRecordHistory(recordId, objectType) { try { const backups = await this.getAllBackups(); const history = []; for (const backup of backups) { const result = await this.queryBackupData(backup.path, objectType, { Id: recordId }); if (result.success && result.data.length > 0) { history.push({ timestamp: backup.timestamp, data: result.data[0] // Should be only one record with specific ID }); } } return { success: true, recordId: recordId, objectType: objectType, history: history, changesCount: history.length }; } catch (error) { return { success: false, error: `Record history query failed: ${error.message}` }; } } /** * Get available object types from a backup */ getAvailableObjectTypes(backupPath) { try { const dataPath = path.join(backupPath, 'data'); if (!fs.existsSync(dataPath)) { return []; } return fs.readdirSync(dataPath) .filter(file => file.endsWith('.json')) .map(file => file.replace('.json', '')); } catch (error) { return []; } } /** * Get backup timestamp from manifest */ getBackupTimestamp(backupPath) { try { const manifestPath = path.join(backupPath, 'backup-manifest.json'); const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); return manifest.backupInfo.timestamp; } catch (error) { return null; } } /** * List all available Time Machine operations */ getAvailableOperations() { return [ { operation: 'query_at_point_in_time', description: 'Query data as it existed at a specific date', parameters: ['targetDate', 'objectType', 'filters (optional)'] }, { operation: 'compare_over_time', description: 'Compare data between two points in time', parameters: ['startDate', 'endDate', 'objectType', 'filters (optional)'] }, { operation: 'get_record_history', description: 'Get complete history of changes for a specific record', parameters: ['recordId', 'objectType'] }, { operation: 'list_backups', description: 'List all available backup snapshots', parameters: [] } ]; } } // MCP Tool implementations const TIME_MACHINE_TOOLS = [ { name: 'salesforce_time_machine_query', description: 'Query historical Salesforce data from backups using Time Machine functionality. Supports point-in-time queries, comparisons, and record history tracking.', inputSchema: { type: 'object', properties: { operation: { type: 'string', enum: ['query_at_point_in_time', 'compare_over_time', 'get_record_history', 'list_backups'], description: 'The Time Machine operation to perform' }, targetDate: { type: 'string', description: 'Target date for point-in-time queries (ISO 8601 format)' }, startDate: { type: 'string', description: 'Start date for comparison queries (ISO 8601 format)' }, endDate: { type: 'string', description: 'End date for comparison queries (ISO 8601 format)' }, objectType: { type: 'string', description: 'Salesforce object type to query (e.g., Account, Contact, ContentVersion)' }, recordId: { type: 'string', description: 'Specific record ID for history tracking' }, filters: { type: 'object', description: 'Optional filters to apply to the query (field-value pairs, supports wildcards with *)' }, backupDirectory: { type: 'string', description: 'Path to backup directory (defaults to ./backups)' } }, required: ['operation'] } } ]; export { SalesforceTimeMachine, TIME_MACHINE_TOOLS };