delegate-sf-mcp 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.json +20 -0
- package/LICENSE +24 -0
- package/README.md +76 -0
- package/auth.js +148 -0
- package/bin/config-helper.js +51 -0
- package/bin/mcp-salesforce.js +12 -0
- package/bin/setup.js +266 -0
- package/bin/status.js +134 -0
- package/docs/README.md +52 -0
- package/docs/step1.png +0 -0
- package/docs/step2.png +0 -0
- package/docs/step3.png +0 -0
- package/docs/step4.png +0 -0
- package/examples/README.md +35 -0
- package/package.json +16 -0
- package/scripts/README.md +30 -0
- package/src/auth/file-storage.js +447 -0
- package/src/auth/oauth.js +417 -0
- package/src/auth/token-manager.js +207 -0
- package/src/backup/manager.js +949 -0
- package/src/index.js +168 -0
- package/src/salesforce/client.js +388 -0
- package/src/sf-client.js +79 -0
- package/src/tools/auth.js +190 -0
- package/src/tools/backup.js +486 -0
- package/src/tools/create.js +109 -0
- package/src/tools/delegate-hygiene.js +268 -0
- package/src/tools/delegate-validate.js +212 -0
- package/src/tools/delegate-verify.js +143 -0
- package/src/tools/delete.js +72 -0
- package/src/tools/describe.js +132 -0
- package/src/tools/installation-info.js +656 -0
- package/src/tools/learn-context.js +1077 -0
- package/src/tools/learn.js +351 -0
- package/src/tools/query.js +82 -0
- package/src/tools/repair-credentials.js +77 -0
- package/src/tools/setup.js +120 -0
- package/src/tools/time_machine.js +347 -0
- package/src/tools/update.js +138 -0
- package/src/tools.js +214 -0
- package/src/utils/cache.js +120 -0
- package/src/utils/debug.js +52 -0
- package/src/utils/logger.js +19 -0
- package/tokens.json +8 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { TokenManager } from '../auth/token-manager.js';
|
|
2
|
+
import { FileStorageManager } from '../auth/file-storage.js';
|
|
3
|
+
|
|
4
|
+
export const reauth = {
|
|
5
|
+
name: 'salesforce_auth',
|
|
6
|
+
description: 'Authenticate with Salesforce. Automatically detects if authentication is needed and handles OAuth flow. Call this tool if any Salesforce operations fail due to authentication issues.',
|
|
7
|
+
inputSchema: {
|
|
8
|
+
type: 'object',
|
|
9
|
+
properties: {
|
|
10
|
+
force: {
|
|
11
|
+
type: 'boolean',
|
|
12
|
+
description: 'Force re-authentication even if current tokens appear valid',
|
|
13
|
+
default: false
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
additionalProperties: false
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export async function handleReauth(args) {
|
|
21
|
+
const { force = false } = args;
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
// Get stored credentials
|
|
25
|
+
const fileStorage = new FileStorageManager();
|
|
26
|
+
const credentials = await fileStorage.getCredentials();
|
|
27
|
+
|
|
28
|
+
// Check if we have existing tokens even without credentials
|
|
29
|
+
if (!credentials) {
|
|
30
|
+
const tokens = await fileStorage.getTokens();
|
|
31
|
+
if (tokens && tokens.access_token && tokens.refresh_token) {
|
|
32
|
+
// We have tokens but no credentials, try to use tokens directly
|
|
33
|
+
const tokenManager = new TokenManager(null, null, tokens.instance_url);
|
|
34
|
+
tokenManager.currentTokens = tokens;
|
|
35
|
+
|
|
36
|
+
// Test if tokens are still valid
|
|
37
|
+
const testResult = await tokenManager.testTokens();
|
|
38
|
+
if (testResult.valid) {
|
|
39
|
+
return {
|
|
40
|
+
success: true,
|
|
41
|
+
message: 'Already authenticated with valid tokens',
|
|
42
|
+
details: {
|
|
43
|
+
authenticated: true,
|
|
44
|
+
instanceUrl: tokens.instance_url,
|
|
45
|
+
storedAt: tokens.stored_at
|
|
46
|
+
},
|
|
47
|
+
nextSteps: [
|
|
48
|
+
'Your MCP Salesforce tools should now work normally',
|
|
49
|
+
'Tokens are stored securely in your home directory with 600 permissions',
|
|
50
|
+
'They will auto-refresh before expiration'
|
|
51
|
+
]
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
success: false,
|
|
58
|
+
error: 'No Salesforce credentials found. Please run the salesforce_setup tool first to configure your credentials.',
|
|
59
|
+
details: {
|
|
60
|
+
setupRequired: true,
|
|
61
|
+
requiredCredentials: ['clientId', 'clientSecret', 'instanceUrl']
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const tokenManager = new TokenManager(
|
|
67
|
+
credentials.clientId,
|
|
68
|
+
credentials.clientSecret,
|
|
69
|
+
credentials.instanceUrl
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
// Check current token status if not forcing
|
|
73
|
+
if (!force) {
|
|
74
|
+
await tokenManager.initialize();
|
|
75
|
+
const tokenInfo = tokenManager.getTokenInfo();
|
|
76
|
+
|
|
77
|
+
if (tokenInfo.authenticated) {
|
|
78
|
+
// Test if tokens are actually working
|
|
79
|
+
const testResult = await tokenManager.testTokens();
|
|
80
|
+
if (testResult.valid) {
|
|
81
|
+
return {
|
|
82
|
+
success: true,
|
|
83
|
+
message: 'Current tokens are still valid. Use force=true to re-authenticate anyway.',
|
|
84
|
+
tokenInfo: {
|
|
85
|
+
authenticated: true,
|
|
86
|
+
expiresInMinutes: tokenInfo.expires_in_minutes,
|
|
87
|
+
instanceUrl: tokenInfo.instance_url
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Clear existing tokens if forcing or if they're invalid
|
|
95
|
+
await tokenManager.clearTokens();
|
|
96
|
+
|
|
97
|
+
// Start OAuth flow
|
|
98
|
+
const tokens = await tokenManager.authenticateWithOAuth();
|
|
99
|
+
|
|
100
|
+
// Verify the new tokens work
|
|
101
|
+
const testResult = await tokenManager.testTokens();
|
|
102
|
+
|
|
103
|
+
if (!testResult.valid) {
|
|
104
|
+
return {
|
|
105
|
+
success: false,
|
|
106
|
+
error: 'Authentication completed but token validation failed',
|
|
107
|
+
details: testResult
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const tokenInfo = tokenManager.getTokenInfo();
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
success: true,
|
|
115
|
+
message: 'Successfully authenticated with Salesforce! New tokens have been stored securely.',
|
|
116
|
+
authenticationFlow: 'OAuth completed successfully',
|
|
117
|
+
tokenInfo: {
|
|
118
|
+
authenticated: true,
|
|
119
|
+
instanceUrl: tokenInfo.instance_url,
|
|
120
|
+
expiresInMinutes: tokenInfo.expires_in_minutes,
|
|
121
|
+
storedAt: tokenInfo.stored_at
|
|
122
|
+
},
|
|
123
|
+
nextSteps: [
|
|
124
|
+
'Your MCP Salesforce tools should now work normally',
|
|
125
|
+
'Tokens are stored securely in your home directory with 600 permissions',
|
|
126
|
+
'They will auto-refresh before expiration'
|
|
127
|
+
]
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
} catch (error) {
|
|
131
|
+
// Handle specific error types
|
|
132
|
+
if (error.message.includes('OAuth flow timed out')) {
|
|
133
|
+
return {
|
|
134
|
+
success: false,
|
|
135
|
+
error: 'Authentication timed out after 5 minutes',
|
|
136
|
+
details: {
|
|
137
|
+
reason: 'User did not complete OAuth flow in browser',
|
|
138
|
+
suggestion: 'Please try again and complete the authentication in your browser quickly'
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (error.message.includes('User denied')) {
|
|
144
|
+
return {
|
|
145
|
+
success: false,
|
|
146
|
+
error: 'Authentication was denied by user',
|
|
147
|
+
details: {
|
|
148
|
+
reason: 'User cancelled or denied the OAuth authorization',
|
|
149
|
+
suggestion: 'Please try again and accept the OAuth authorization'
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (error.message.includes('invalid_client')) {
|
|
155
|
+
return {
|
|
156
|
+
success: false,
|
|
157
|
+
error: 'Invalid Salesforce Connected App credentials',
|
|
158
|
+
details: {
|
|
159
|
+
reason: 'Client ID or Client Secret is incorrect',
|
|
160
|
+
suggestion: 'Please verify your Client ID and Client Secret in ~/.mcp-salesforce.json configuration'
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (error.message.includes('EADDRINUSE')) {
|
|
166
|
+
return {
|
|
167
|
+
success: false,
|
|
168
|
+
error: 'Cannot start OAuth callback server - port conflict',
|
|
169
|
+
details: {
|
|
170
|
+
reason: 'OAuth callback port is already in use',
|
|
171
|
+
suggestion: 'Please close any other applications using ports 8080-9000 and try again'
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
success: false,
|
|
178
|
+
error: `Authentication failed: ${error.message}`,
|
|
179
|
+
details: {
|
|
180
|
+
errorType: error.constructor.name,
|
|
181
|
+
troubleshooting: [
|
|
182
|
+
'Verify your Salesforce Connected App configuration',
|
|
183
|
+
'Check that callback URLs include http://localhost:8080/callback',
|
|
184
|
+
'Ensure your Salesforce credentials are correct in MCP config',
|
|
185
|
+
'Try running with force=true parameter'
|
|
186
|
+
]
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
}
|
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { SalesforceBackupManager } from '../backup/manager.js';
|
|
5
|
+
import { debug as logger } from '../utils/debug.js';
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Salesforce Backup MCP Tool
|
|
12
|
+
*
|
|
13
|
+
* Creates comprehensive backups of Salesforce data including all file attachments
|
|
14
|
+
* from ContentDocument/ContentVersion, Attachments, and Documents
|
|
15
|
+
*/
|
|
16
|
+
export async function handleSalesforceBackup(args, client) {
|
|
17
|
+
const {
|
|
18
|
+
backup_type = 'incremental',
|
|
19
|
+
include_files = true,
|
|
20
|
+
include_attachments = true,
|
|
21
|
+
include_documents = true,
|
|
22
|
+
objects_filter = [],
|
|
23
|
+
since_date = null,
|
|
24
|
+
compression = false,
|
|
25
|
+
parallel_downloads = 5
|
|
26
|
+
} = args;
|
|
27
|
+
|
|
28
|
+
// Resolve backup directory relative to project root, not current working directory
|
|
29
|
+
const projectRoot = path.resolve(__dirname, '../..');
|
|
30
|
+
const output_directory = path.join(projectRoot, 'backups');
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
// Ensure backup directory exists
|
|
34
|
+
await fs.mkdir(output_directory, { recursive: true });
|
|
35
|
+
|
|
36
|
+
logger.log(`đī¸ Starting Salesforce ${backup_type} backup...`);
|
|
37
|
+
|
|
38
|
+
// Validate parameters
|
|
39
|
+
const validBackupTypes = ['full', 'incremental', 'files_only'];
|
|
40
|
+
if (!validBackupTypes.includes(backup_type)) {
|
|
41
|
+
throw new Error(`Invalid backup_type. Must be one of: ${validBackupTypes.join(', ')}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (parallel_downloads < 1 || parallel_downloads > 10) {
|
|
45
|
+
throw new Error('parallel_downloads must be between 1 and 10');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Parse since_date if provided
|
|
49
|
+
let parsedSinceDate = null;
|
|
50
|
+
if (since_date) {
|
|
51
|
+
try {
|
|
52
|
+
parsedSinceDate = new Date(since_date).toISOString();
|
|
53
|
+
} catch (error) {
|
|
54
|
+
throw new Error(`Invalid since_date format. Use ISO format: YYYY-MM-DDTHH:mm:ss.sssZ`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Create backup manager with options
|
|
59
|
+
const backupManager = new SalesforceBackupManager(client, {
|
|
60
|
+
outputDirectory: output_directory,
|
|
61
|
+
includeFiles: include_files,
|
|
62
|
+
includeAttachments: include_attachments,
|
|
63
|
+
includeDocuments: include_documents,
|
|
64
|
+
compression: compression,
|
|
65
|
+
parallelDownloads: parallel_downloads,
|
|
66
|
+
objectsFilter: objects_filter
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Execute async backup - returns immediately
|
|
70
|
+
const jobResult = await backupManager.startAsyncBackup(backup_type, parsedSinceDate);
|
|
71
|
+
|
|
72
|
+
// Return immediate response with job information
|
|
73
|
+
let successMessage = `đ **Salesforce ${backup_type} backup started successfully!**\n\n`;
|
|
74
|
+
successMessage += `đ **Job ID**: \`${jobResult.jobId}\`\n`;
|
|
75
|
+
successMessage += `đ **Backup Location**: \`${jobResult.backupDirectory}\`\n`;
|
|
76
|
+
successMessage += `⥠**Status**: ${jobResult.status} - Running in background\n\n`;
|
|
77
|
+
successMessage += `âšī¸ **Monitor Progress**: Use the \`salesforce_backup_status\` tool to check job progress\n`;
|
|
78
|
+
successMessage += `đ **View All Jobs**: Use \`salesforce_backup_status\` without parameters to see all jobs\n\n`;
|
|
79
|
+
|
|
80
|
+
successMessage += `đ **What happens next?**\n`;
|
|
81
|
+
successMessage += `- The backup is now running in the background\n`;
|
|
82
|
+
successMessage += `- You can continue using other tools immediately\n`;
|
|
83
|
+
successMessage += `- Check progress anytime with \`salesforce_backup_status\`\n`;
|
|
84
|
+
successMessage += `- You'll find the completed backup in the specified directory\n\n`;
|
|
85
|
+
|
|
86
|
+
successMessage += `đ **Expected Directory Structure**:\n`;
|
|
87
|
+
successMessage += `\`\`\`\n`;
|
|
88
|
+
successMessage += `${path.basename(jobResult.backupDirectory)}/\n`;
|
|
89
|
+
successMessage += `âââ metadata/ # Schemas and manifest\n`;
|
|
90
|
+
successMessage += `âââ data/ # Object records (JSON)\n`;
|
|
91
|
+
successMessage += `âââ files/\n`;
|
|
92
|
+
successMessage += `â âââ content-versions/ # Modern files\n`;
|
|
93
|
+
successMessage += `â âââ attachments/ # Legacy attachments\n`;
|
|
94
|
+
successMessage += `â âââ documents/ # Document objects\n`;
|
|
95
|
+
successMessage += `âââ logs/ # Backup logs\n`;
|
|
96
|
+
successMessage += `\`\`\`\n\n`;
|
|
97
|
+
|
|
98
|
+
// Add next steps suggestions
|
|
99
|
+
successMessage += `đ **Next Steps**:\n`;
|
|
100
|
+
successMessage += `- Monitor progress: Use \`salesforce_backup_status ${jobResult.jobId}\` for detailed status\n`;
|
|
101
|
+
successMessage += `- Check all jobs: Use \`salesforce_backup_status\` to see all running/completed jobs\n`;
|
|
102
|
+
|
|
103
|
+
if (backup_type === 'full') {
|
|
104
|
+
successMessage += `- Schedule incremental backups using \`since_date\` parameter\n`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (compression) {
|
|
108
|
+
successMessage += `- Archive will be created with compression enabled\n`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
content: [
|
|
113
|
+
{
|
|
114
|
+
type: "text",
|
|
115
|
+
text: successMessage
|
|
116
|
+
}
|
|
117
|
+
]
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
} catch (error) {
|
|
121
|
+
let errorMessage = `â **Salesforce backup failed**\n\n`;
|
|
122
|
+
errorMessage += `**Error**: ${error.message}\n\n`;
|
|
123
|
+
|
|
124
|
+
errorMessage += `đ§ **Troubleshooting**:\n`;
|
|
125
|
+
|
|
126
|
+
if (error.message.includes('Authentication')) {
|
|
127
|
+
errorMessage += `- Run \`salesforce_auth\` to refresh your authentication\n`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (error.message.includes('permission') || error.message.includes('access')) {
|
|
131
|
+
errorMessage += `- Check your Salesforce user permissions for objects and files\n`;
|
|
132
|
+
errorMessage += `- Ensure you have \`View All Data\` or appropriate object permissions\n`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (error.message.includes('API')) {
|
|
136
|
+
errorMessage += `- Check your Salesforce API limits in Setup > System Overview\n`;
|
|
137
|
+
errorMessage += `- Consider reducing \`parallel_downloads\` parameter\n`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (error.message.includes('since_date')) {
|
|
141
|
+
errorMessage += `- Use ISO date format: \`2024-01-15T10:30:00.000Z\`\n`;
|
|
142
|
+
errorMessage += `- Check that the date is not in the future\n`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
errorMessage += `\nđĄ **Suggestions**:\n`;
|
|
146
|
+
errorMessage += `- Try a smaller backup first with \`backup_type: "files_only"\`\n`;
|
|
147
|
+
errorMessage += `- Use \`objects_filter\` to backup specific objects only\n`;
|
|
148
|
+
errorMessage += `- Check available disk space\n`;
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
content: [
|
|
152
|
+
{
|
|
153
|
+
type: "text",
|
|
154
|
+
text: errorMessage
|
|
155
|
+
}
|
|
156
|
+
]
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Salesforce Backup List MCP Tool
|
|
163
|
+
*
|
|
164
|
+
* Lists available backups and their information
|
|
165
|
+
*/
|
|
166
|
+
export async function handleSalesforceBackupList(args) {
|
|
167
|
+
// Resolve backup directory relative to project root, not current working directory
|
|
168
|
+
const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
|
|
169
|
+
const backup_directory = path.join(projectRoot, 'backups');
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
// Check if backup directory exists
|
|
173
|
+
try {
|
|
174
|
+
await fs.access(backup_directory);
|
|
175
|
+
} catch (error) {
|
|
176
|
+
return {
|
|
177
|
+
content: [
|
|
178
|
+
{
|
|
179
|
+
type: "text",
|
|
180
|
+
text: `đ No backups found.\n\nCreate your first backup using the \`salesforce_backup\` tool.`
|
|
181
|
+
}
|
|
182
|
+
]
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Read backup directory
|
|
187
|
+
const entries = await fs.readdir(backup_directory, { withFileTypes: true });
|
|
188
|
+
const backupDirs = entries
|
|
189
|
+
.filter(entry => entry.isDirectory() && entry.name.startsWith('salesforce-backup-'))
|
|
190
|
+
.sort((a, b) => b.name.localeCompare(a.name)); // Most recent first
|
|
191
|
+
|
|
192
|
+
if (backupDirs.length === 0) {
|
|
193
|
+
return {
|
|
194
|
+
content: [
|
|
195
|
+
{
|
|
196
|
+
type: "text",
|
|
197
|
+
text: `đ No Salesforce backups found.\n\nCreate your first backup using the \`salesforce_backup\` tool.`
|
|
198
|
+
}
|
|
199
|
+
]
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
let response = `đ **Available Salesforce Backups** (${backupDirs.length} found)\n\n`;
|
|
204
|
+
|
|
205
|
+
// Analyze each backup
|
|
206
|
+
for (const [index, backupDir] of backupDirs.slice(0, 10).entries()) { // Show max 10
|
|
207
|
+
const backupPath = path.join(backup_directory, backupDir.name);
|
|
208
|
+
const manifestPath = path.join(backupPath, 'backup-manifest.json');
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
const manifestData = await fs.readFile(manifestPath, 'utf-8');
|
|
212
|
+
const manifest = JSON.parse(manifestData);
|
|
213
|
+
|
|
214
|
+
const backupInfo = manifest.backupInfo;
|
|
215
|
+
const stats = manifest.downloadStats || {};
|
|
216
|
+
const totalFiles = (stats.contentVersions || 0) + (stats.attachments || 0) + (stats.documents || 0);
|
|
217
|
+
const sizeMB = Math.round((stats.totalBytes || 0) / (1024 * 1024) * 100) / 100;
|
|
218
|
+
|
|
219
|
+
response += `**${index + 1}. ${backupDir.name}**\n`;
|
|
220
|
+
response += ` đ
Date: ${new Date(backupInfo.timestamp).toLocaleString()}\n`;
|
|
221
|
+
response += ` âąī¸ Duration: ${backupInfo.duration} seconds\n`;
|
|
222
|
+
response += ` đĻ Files: ${totalFiles} (${sizeMB} MB)\n`;
|
|
223
|
+
response += ` đĸ Instance: ${backupInfo.salesforceInstance}\n`;
|
|
224
|
+
response += ` đ Path: \`${backupPath}\`\n\n`;
|
|
225
|
+
|
|
226
|
+
} catch (error) {
|
|
227
|
+
response += `**${index + 1}. ${backupDir.name}**\n`;
|
|
228
|
+
response += ` â ī¸ Manifest not found or corrupted\n`;
|
|
229
|
+
response += ` đ Path: \`${backupPath}\`\n\n`;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (backupDirs.length > 10) {
|
|
234
|
+
response += `... and ${backupDirs.length - 10} more backups\n\n`;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
response += `đ§ **Management**:\n`;
|
|
238
|
+
response += `- View backup details: Check \`backup-manifest.json\` in each directory\n`;
|
|
239
|
+
response += `- Clean old backups: Manually delete directories you no longer need\n`;
|
|
240
|
+
response += `- Restore from backup: Use the data files to recreate records\n`;
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
content: [
|
|
244
|
+
{
|
|
245
|
+
type: "text",
|
|
246
|
+
text: response
|
|
247
|
+
}
|
|
248
|
+
]
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
} catch (error) {
|
|
252
|
+
return {
|
|
253
|
+
content: [
|
|
254
|
+
{
|
|
255
|
+
type: "text",
|
|
256
|
+
text: `â **Failed to list backups**: ${error.message}\n\nPlease check your backup directory access.`
|
|
257
|
+
}
|
|
258
|
+
]
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Salesforce Backup Status MCP Tool
|
|
265
|
+
*
|
|
266
|
+
* Check the status of backup jobs (running, completed, failed)
|
|
267
|
+
*/
|
|
268
|
+
export async function handleSalesforceBackupStatus(args, client) {
|
|
269
|
+
const { job_id = null } = args;
|
|
270
|
+
|
|
271
|
+
// Resolve backup directory relative to project root
|
|
272
|
+
const projectRoot = path.resolve(__dirname, '../..');
|
|
273
|
+
const backupDirectory = path.join(projectRoot, 'backups');
|
|
274
|
+
|
|
275
|
+
try {
|
|
276
|
+
// Ensure backup directory exists
|
|
277
|
+
await fs.mkdir(backupDirectory, { recursive: true });
|
|
278
|
+
|
|
279
|
+
// Create backup manager to access status methods
|
|
280
|
+
const backupManager = new SalesforceBackupManager(client, {
|
|
281
|
+
outputDirectory: backupDirectory
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
if (job_id) {
|
|
285
|
+
// Get status of specific job
|
|
286
|
+
const jobStatus = await backupManager.getBackupJobStatus(job_id);
|
|
287
|
+
|
|
288
|
+
if (!jobStatus) {
|
|
289
|
+
return {
|
|
290
|
+
content: [{
|
|
291
|
+
type: "text",
|
|
292
|
+
text: `â **Job not found**: No backup job found with ID \`${job_id}\`\n\nUse \`salesforce_backup_status\` without parameters to see all jobs.`
|
|
293
|
+
}]
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Format single job status
|
|
298
|
+
let statusMessage = `đ **Backup Job Status**\n\n`;
|
|
299
|
+
statusMessage += `đ **Job ID**: \`${jobStatus.jobId}\`\n`;
|
|
300
|
+
statusMessage += `⥠**Status**: ${getStatusEmoji(jobStatus.status)} ${jobStatus.status.toUpperCase()}\n`;
|
|
301
|
+
statusMessage += `đ
**Started**: ${new Date(jobStatus.startTime).toLocaleString()}\n`;
|
|
302
|
+
|
|
303
|
+
if (jobStatus.endTime) {
|
|
304
|
+
statusMessage += `đ **Completed**: ${new Date(jobStatus.endTime).toLocaleString()}\n`;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
statusMessage += `đ **Backup Directory**: \`${jobStatus.backupDirectory}\`\n`;
|
|
308
|
+
statusMessage += `đ **Progress**: ${jobStatus.progress}%\n`;
|
|
309
|
+
statusMessage += `đŦ **Message**: ${jobStatus.message}\n`;
|
|
310
|
+
|
|
311
|
+
if (jobStatus.result) {
|
|
312
|
+
const stats = jobStatus.result.stats;
|
|
313
|
+
const totalFiles = stats.contentVersions + stats.attachments + stats.documents;
|
|
314
|
+
const sizeMB = Math.round(stats.totalBytes / (1024 * 1024) * 100) / 100;
|
|
315
|
+
|
|
316
|
+
statusMessage += `\nđ **Backup Results**:\n`;
|
|
317
|
+
statusMessage += `- âąī¸ **Duration**: ${jobStatus.result.duration} seconds\n`;
|
|
318
|
+
statusMessage += `- đ **ContentVersion files**: ${stats.contentVersions}\n`;
|
|
319
|
+
statusMessage += `- đ **Attachment files**: ${stats.attachments}\n`;
|
|
320
|
+
statusMessage += `- đ **Document files**: ${stats.documents}\n`;
|
|
321
|
+
statusMessage += `- đĻ **Total files**: ${totalFiles}\n`;
|
|
322
|
+
statusMessage += `- đž **Total size**: ${sizeMB} MB\n`;
|
|
323
|
+
|
|
324
|
+
if (stats.errors > 0) {
|
|
325
|
+
statusMessage += `- â ī¸ **Errors**: ${stats.errors} failed downloads\n`;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (jobStatus.error) {
|
|
330
|
+
statusMessage += `\nâ **Error**: ${jobStatus.error}\n`;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return {
|
|
334
|
+
content: [{
|
|
335
|
+
type: "text",
|
|
336
|
+
text: statusMessage
|
|
337
|
+
}]
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
} else {
|
|
341
|
+
// Get status of all jobs
|
|
342
|
+
const allJobs = await backupManager.getBackupJobStatuses();
|
|
343
|
+
|
|
344
|
+
if (allJobs.length === 0) {
|
|
345
|
+
return {
|
|
346
|
+
content: [{
|
|
347
|
+
type: "text",
|
|
348
|
+
text: `đ **No backup jobs found**\n\nNo backup jobs have been started yet. Use the \`salesforce_backup\` tool to start a backup.`
|
|
349
|
+
}]
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Format all jobs status
|
|
354
|
+
let statusMessage = `đ **All Backup Jobs** (${allJobs.length} total)\n\n`;
|
|
355
|
+
|
|
356
|
+
allJobs.forEach((job, index) => {
|
|
357
|
+
statusMessage += `**${index + 1}. ${job.jobId}**\n`;
|
|
358
|
+
statusMessage += ` ${getStatusEmoji(job.status)} Status: ${job.status.toUpperCase()}\n`;
|
|
359
|
+
statusMessage += ` đ
Started: ${new Date(job.startTime).toLocaleString()}\n`;
|
|
360
|
+
statusMessage += ` đ Progress: ${job.progress}%\n`;
|
|
361
|
+
statusMessage += ` đŦ ${job.message}\n`;
|
|
362
|
+
|
|
363
|
+
if (job.endTime) {
|
|
364
|
+
statusMessage += ` đ Completed: ${new Date(job.endTime).toLocaleString()}\n`;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (job.error) {
|
|
368
|
+
statusMessage += ` â Error: ${job.error}\n`;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
statusMessage += `\n`;
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
statusMessage += `âšī¸ **Tip**: Use \`salesforce_backup_status\` with a specific \`job_id\` to get detailed information about a single job.`;
|
|
375
|
+
|
|
376
|
+
return {
|
|
377
|
+
content: [{
|
|
378
|
+
type: "text",
|
|
379
|
+
text: statusMessage
|
|
380
|
+
}]
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
} catch (error) {
|
|
385
|
+
logger.log('Backup status error:', error);
|
|
386
|
+
|
|
387
|
+
return {
|
|
388
|
+
content: [{
|
|
389
|
+
type: "text",
|
|
390
|
+
text: `â **Error checking backup status**: ${error.message}\n\nPlease check that the backup system is properly configured.`
|
|
391
|
+
}]
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Helper function to get status emoji
|
|
398
|
+
*/
|
|
399
|
+
function getStatusEmoji(status) {
|
|
400
|
+
const emojis = {
|
|
401
|
+
'starting': 'đ',
|
|
402
|
+
'running': 'âĄ',
|
|
403
|
+
'completed': 'â
',
|
|
404
|
+
'failed': 'â'
|
|
405
|
+
};
|
|
406
|
+
return emojis[status] || 'â';
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
export const BACKUP_TOOLS = {
|
|
410
|
+
salesforce_backup: {
|
|
411
|
+
name: "salesforce_backup",
|
|
412
|
+
description: "Start comprehensive backups of Salesforce data including all file attachments. Runs asynchronously in background - returns immediately with job ID. Check progress with salesforce_backup_status.",
|
|
413
|
+
inputSchema: {
|
|
414
|
+
type: "object",
|
|
415
|
+
properties: {
|
|
416
|
+
backup_type: {
|
|
417
|
+
type: "string",
|
|
418
|
+
enum: ["full", "incremental", "files_only"],
|
|
419
|
+
description: "Type of backup to perform: 'full' backs up everything, 'incremental' backs up changes since a date, 'files_only' backs up just attachments",
|
|
420
|
+
default: "incremental"
|
|
421
|
+
},
|
|
422
|
+
include_files: {
|
|
423
|
+
type: "boolean",
|
|
424
|
+
description: "Include modern Files (ContentDocument/ContentVersion) in backup",
|
|
425
|
+
default: true
|
|
426
|
+
},
|
|
427
|
+
include_attachments: {
|
|
428
|
+
type: "boolean",
|
|
429
|
+
description: "Include legacy Attachment files in backup",
|
|
430
|
+
default: true
|
|
431
|
+
},
|
|
432
|
+
include_documents: {
|
|
433
|
+
type: "boolean",
|
|
434
|
+
description: "Include Document object files in backup",
|
|
435
|
+
default: true
|
|
436
|
+
},
|
|
437
|
+
objects_filter: {
|
|
438
|
+
type: "array",
|
|
439
|
+
items: { type: "string" },
|
|
440
|
+
description: "Specific objects to backup (empty array = all objects). Example: ['Account', 'Contact', 'CustomObject__c']",
|
|
441
|
+
default: []
|
|
442
|
+
},
|
|
443
|
+
since_date: {
|
|
444
|
+
type: "string",
|
|
445
|
+
description: "ISO date for incremental backup - only backup records modified after this date. Format: YYYY-MM-DDTHH:mm:ss.sssZ",
|
|
446
|
+
default: null
|
|
447
|
+
},
|
|
448
|
+
compression: {
|
|
449
|
+
type: "boolean",
|
|
450
|
+
description: "Compress backup files with gzip to save space",
|
|
451
|
+
default: false
|
|
452
|
+
},
|
|
453
|
+
parallel_downloads: {
|
|
454
|
+
type: "number",
|
|
455
|
+
description: "Number of parallel file downloads (1-10). Higher values = faster but more API usage",
|
|
456
|
+
default: 5,
|
|
457
|
+
minimum: 1,
|
|
458
|
+
maximum: 10
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
},
|
|
463
|
+
|
|
464
|
+
salesforce_backup_list: {
|
|
465
|
+
name: "salesforce_backup_list",
|
|
466
|
+
description: "List all available Salesforce backups with their details including timestamp, duration, file counts, and sizes",
|
|
467
|
+
inputSchema: {
|
|
468
|
+
type: "object",
|
|
469
|
+
properties: {}
|
|
470
|
+
}
|
|
471
|
+
},
|
|
472
|
+
|
|
473
|
+
salesforce_backup_status: {
|
|
474
|
+
name: "salesforce_backup_status",
|
|
475
|
+
description: "Check the status of Salesforce backup jobs. Monitor running, completed, or failed backup operations. Use without parameters to see all jobs, or specify a job_id to get detailed status of a specific backup job.",
|
|
476
|
+
inputSchema: {
|
|
477
|
+
type: "object",
|
|
478
|
+
properties: {
|
|
479
|
+
job_id: {
|
|
480
|
+
type: "string",
|
|
481
|
+
description: "Optional: Specific backup job ID to check status for. If not provided, shows status of all backup jobs."
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
};
|