@aaron-pienza/mcp-server-salesforce 1.0.0 → 1.0.2
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.
|
@@ -45,6 +45,16 @@ Notes:
|
|
|
45
45
|
required: ["apexCode"]
|
|
46
46
|
}
|
|
47
47
|
};
|
|
48
|
+
async function resolveCurrentUserId(conn) {
|
|
49
|
+
const directId = conn?.userInfo?.id || conn?.userInfo?.user_id;
|
|
50
|
+
if (directId)
|
|
51
|
+
return directId;
|
|
52
|
+
if (typeof conn?.identity === 'function') {
|
|
53
|
+
const identity = await conn.identity();
|
|
54
|
+
return identity?.user_id || identity?.id;
|
|
55
|
+
}
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
48
58
|
/**
|
|
49
59
|
* Handles executing anonymous Apex code in Salesforce
|
|
50
60
|
* @param conn Active Salesforce connection
|
|
@@ -93,10 +103,22 @@ export async function handleExecuteAnonymous(conn, args) {
|
|
|
93
103
|
// Get debug logs if available
|
|
94
104
|
if (result.compiled) {
|
|
95
105
|
try {
|
|
106
|
+
const currentUserId = await resolveCurrentUserId(conn);
|
|
107
|
+
if (!currentUserId) {
|
|
108
|
+
responseText += `\n**Debug Log:** Unable to determine the current Salesforce user, so logs were not retrieved safely.`;
|
|
109
|
+
return {
|
|
110
|
+
content: [{
|
|
111
|
+
type: "text",
|
|
112
|
+
text: responseText
|
|
113
|
+
}],
|
|
114
|
+
isError: !result.success,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
96
117
|
// Query for the most recent debug log
|
|
97
118
|
const logs = await conn.query(`
|
|
98
119
|
SELECT Id, LogUserId, Operation, Application, Status, LogLength, LastModifiedDate, Request
|
|
99
120
|
FROM ApexLog
|
|
121
|
+
WHERE LogUserId = '${currentUserId}'
|
|
100
122
|
ORDER BY LastModifiedDate DESC
|
|
101
123
|
LIMIT 1
|
|
102
124
|
`);
|
|
@@ -86,6 +86,15 @@ Notes:
|
|
|
86
86
|
required: ["operation", "username"]
|
|
87
87
|
}
|
|
88
88
|
};
|
|
89
|
+
function ensureSaveResultSuccess(result, action) {
|
|
90
|
+
if (result?.success !== false) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const errors = Array.isArray(result.errors)
|
|
94
|
+
? result.errors.map((error) => typeof error === 'string' ? error : error?.message || JSON.stringify(error))
|
|
95
|
+
: result?.errors ? [String(result.errors)] : [];
|
|
96
|
+
throw new Error(`${action} failed${errors.length > 0 ? `: ${errors.join(', ')}` : ''}`);
|
|
97
|
+
}
|
|
89
98
|
/**
|
|
90
99
|
* Handles managing debug logs for Salesforce users
|
|
91
100
|
* @param conn Active Salesforce connection
|
|
@@ -193,12 +202,13 @@ export async function handleManageDebugLogs(conn, args) {
|
|
|
193
202
|
// Update existing trace flag
|
|
194
203
|
traceFlagId = existingTraceFlag.records[0].Id;
|
|
195
204
|
debugLevelId = existingTraceFlag.records[0].DebugLevelId;
|
|
196
|
-
await conn.tooling.sobject('TraceFlag').update({
|
|
205
|
+
const updateResult = await conn.tooling.sobject('TraceFlag').update({
|
|
197
206
|
Id: traceFlagId,
|
|
198
207
|
LogType: 'USER_DEBUG',
|
|
199
208
|
StartDate: new Date().toISOString(),
|
|
200
209
|
ExpirationDate: expirationDate.toISOString()
|
|
201
210
|
});
|
|
211
|
+
ensureSaveResultSuccess(updateResult, 'Updating trace flag');
|
|
202
212
|
operation = 'updated';
|
|
203
213
|
}
|
|
204
214
|
else {
|
|
@@ -215,6 +225,7 @@ export async function handleManageDebugLogs(conn, args) {
|
|
|
215
225
|
Visualforce: args.logLevel,
|
|
216
226
|
Workflow: args.logLevel
|
|
217
227
|
});
|
|
228
|
+
ensureSaveResultSuccess(debugLevelResult, 'Creating debug level');
|
|
218
229
|
debugLevelId = debugLevelResult.id;
|
|
219
230
|
// Create a new trace flag
|
|
220
231
|
const traceFlagResult = await conn.tooling.sobject('TraceFlag').create({
|
|
@@ -224,6 +235,7 @@ export async function handleManageDebugLogs(conn, args) {
|
|
|
224
235
|
StartDate: new Date().toISOString(),
|
|
225
236
|
ExpirationDate: expirationDate.toISOString()
|
|
226
237
|
});
|
|
238
|
+
ensureSaveResultSuccess(traceFlagResult, 'Creating trace flag');
|
|
227
239
|
traceFlagId = traceFlagResult.id;
|
|
228
240
|
operation = 'enabled';
|
|
229
241
|
}
|
|
@@ -254,6 +266,7 @@ export async function handleManageDebugLogs(conn, args) {
|
|
|
254
266
|
// Delete trace flags instead of updating expiration date
|
|
255
267
|
const traceFlagIds = traceFlags.records.map((tf) => tf.Id);
|
|
256
268
|
const deleteResults = await Promise.all(traceFlagIds.map((id) => conn.tooling.sobject('TraceFlag').delete(id)));
|
|
269
|
+
deleteResults.forEach((result) => ensureSaveResultSuccess(result, 'Deleting trace flag'));
|
|
257
270
|
return {
|
|
258
271
|
content: [{
|
|
259
272
|
type: "text",
|
|
@@ -273,6 +286,7 @@ export async function handleManageDebugLogs(conn, args) {
|
|
|
273
286
|
Id: id,
|
|
274
287
|
ExpirationDate: nearFutureExpiration.toISOString()
|
|
275
288
|
})));
|
|
289
|
+
updateResults.forEach((result) => ensureSaveResultSuccess(result, 'Updating trace flag expiration'));
|
|
276
290
|
return {
|
|
277
291
|
content: [{
|
|
278
292
|
type: "text",
|
|
@@ -297,12 +311,13 @@ export async function handleManageDebugLogs(conn, args) {
|
|
|
297
311
|
SELECT Id, LogUserId, Operation, Application, Status, LogLength, LastModifiedDate, Request
|
|
298
312
|
FROM ApexLog
|
|
299
313
|
WHERE Id = '${escapeSoqlValue(args.logId)}'
|
|
314
|
+
AND LogUserId = '${escapeSoqlValue(user.Id)}'
|
|
300
315
|
`);
|
|
301
316
|
if (logQuery.records.length === 0) {
|
|
302
317
|
return {
|
|
303
318
|
content: [{
|
|
304
319
|
type: "text",
|
|
305
|
-
text: `No log found with ID '${args.logId}'.`
|
|
320
|
+
text: `No log found with ID '${args.logId}' for user '${args.username}'.`
|
|
306
321
|
}]
|
|
307
322
|
};
|
|
308
323
|
}
|
package/dist/tools/searchAll.js
CHANGED
|
@@ -35,7 +35,7 @@ Notes:
|
|
|
35
35
|
- Use * and ? for wildcards in search terms
|
|
36
36
|
- Each object can have its own WHERE, ORDER BY, and LIMIT clauses
|
|
37
37
|
- Support for WITH clauses: DATA CATEGORY, DIVISION, METADATA, NETWORK, PRICEBOOKID, SNIPPET, SECURITY_ENFORCED
|
|
38
|
-
-
|
|
38
|
+
- The updateable/viewable filters are reserved for future support and currently return a clear error if requested`,
|
|
39
39
|
inputSchema: {
|
|
40
40
|
type: "object",
|
|
41
41
|
properties: {
|
|
@@ -112,12 +112,12 @@ Notes:
|
|
|
112
112
|
},
|
|
113
113
|
updateable: {
|
|
114
114
|
type: "boolean",
|
|
115
|
-
description: "
|
|
115
|
+
description: "Reserved for future support. If set, the tool returns an error instead of generating invalid SOSL.",
|
|
116
116
|
optional: true
|
|
117
117
|
},
|
|
118
118
|
viewable: {
|
|
119
119
|
type: "boolean",
|
|
120
|
-
description: "
|
|
120
|
+
description: "Reserved for future support. If set, the tool returns an error instead of generating invalid SOSL.",
|
|
121
121
|
optional: true
|
|
122
122
|
}
|
|
123
123
|
},
|
|
@@ -147,6 +147,15 @@ export async function handleSearchAll(conn, args) {
|
|
|
147
147
|
if (!searchTerm.trim()) {
|
|
148
148
|
throw new Error('Search term cannot be empty');
|
|
149
149
|
}
|
|
150
|
+
if (updateable || viewable) {
|
|
151
|
+
return {
|
|
152
|
+
content: [{
|
|
153
|
+
type: "text",
|
|
154
|
+
text: 'The updateable/viewable filters are not currently supported by this tool. Remove those flags and retry the SOSL search.'
|
|
155
|
+
}],
|
|
156
|
+
isError: true,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
150
159
|
// Validate object names
|
|
151
160
|
for (const obj of objects) {
|
|
152
161
|
const objValidation = validateIdentifier(obj.name);
|
|
@@ -172,19 +181,10 @@ export async function handleSearchAll(conn, args) {
|
|
|
172
181
|
const withClausesStr = withClauses
|
|
173
182
|
? withClauses.map(buildWithClause).join(' ')
|
|
174
183
|
: '';
|
|
175
|
-
// Add updateable/viewable flags if specified
|
|
176
|
-
const accessFlags = [];
|
|
177
|
-
if (updateable)
|
|
178
|
-
accessFlags.push('UPDATEABLE');
|
|
179
|
-
if (viewable)
|
|
180
|
-
accessFlags.push('VIEWABLE');
|
|
181
|
-
const accessClause = accessFlags.length > 0 ?
|
|
182
|
-
` RETURNING ${accessFlags.join(',')}` : '';
|
|
183
184
|
// Construct complete SOSL query
|
|
184
185
|
const soslQuery = `FIND {${escapeSoslSearchTerm(searchTerm)}} IN ${searchIn}
|
|
185
186
|
${withClausesStr}
|
|
186
|
-
RETURNING ${returningClause}
|
|
187
|
-
${accessClause}`.trim();
|
|
187
|
+
RETURNING ${returningClause}`.trim();
|
|
188
188
|
// Execute search
|
|
189
189
|
const result = await conn.search(soslQuery);
|
|
190
190
|
// Format results by object
|
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
import { ConnectionConfig } from '../types/connection.js';
|
|
1
|
+
import { ConnectionConfig, SalesforceCLIResponse } from '../types/connection.js';
|
|
2
|
+
/**
|
|
3
|
+
* Executes the Salesforce CLI command to get org information
|
|
4
|
+
* @returns Parsed response from sf org display --json command
|
|
5
|
+
*/
|
|
6
|
+
export declare function getSalesforceOrgInfo(execSfOrgDisplay?: () => Promise<{
|
|
7
|
+
stdout: string;
|
|
8
|
+
stderr: string;
|
|
9
|
+
}>): Promise<SalesforceCLIResponse>;
|
|
2
10
|
/**
|
|
3
11
|
* Creates a Salesforce connection using either username/password or OAuth 2.0 Client Credentials Flow
|
|
4
12
|
* @param config Optional connection configuration
|
package/dist/utils/connection.js
CHANGED
|
@@ -12,22 +12,22 @@ const OAUTH_TIMEOUT = 30000; // 30 seconds for OAuth token request
|
|
|
12
12
|
* Executes the Salesforce CLI command to get org information
|
|
13
13
|
* @returns Parsed response from sf org display --json command
|
|
14
14
|
*/
|
|
15
|
-
async function getSalesforceOrgInfo() {
|
|
15
|
+
export async function getSalesforceOrgInfo(execSfOrgDisplay = () => execFileAsync('sf', ['org', 'display', '--json'])) {
|
|
16
16
|
try {
|
|
17
17
|
console.error(`Executing Salesforce CLI: sf org display --json`);
|
|
18
18
|
let stdout = '';
|
|
19
|
-
let stderr = '';
|
|
20
19
|
let execError = null;
|
|
21
20
|
try {
|
|
22
21
|
// Use execFile instead of exec to avoid shell injection surface
|
|
23
|
-
const result = await
|
|
22
|
+
const result = await execSfOrgDisplay();
|
|
24
23
|
stdout = result.stdout;
|
|
25
|
-
stderr = result.stderr;
|
|
26
24
|
}
|
|
27
25
|
catch (err) {
|
|
28
26
|
execError = err;
|
|
27
|
+
if (err?.code === 'ENOENT' || err?.message?.includes('command not found') || err?.message?.includes('not recognized')) {
|
|
28
|
+
throw err;
|
|
29
|
+
}
|
|
29
30
|
stdout = 'stdout' in err ? err.stdout || '' : '';
|
|
30
|
-
stderr = 'stderr' in err ? err.stderr || '' : '';
|
|
31
31
|
}
|
|
32
32
|
// Parse JSON — log redacted version only
|
|
33
33
|
let response;
|
package/dist/utils/logging.js
CHANGED
|
@@ -6,8 +6,7 @@
|
|
|
6
6
|
* Logs to stderr (stdout is reserved for MCP JSON-RPC).
|
|
7
7
|
*/
|
|
8
8
|
export function logApexExecution(code) {
|
|
9
|
-
|
|
10
|
-
console.error(`[AUDIT] Execute Anonymous Apex — ${code.length} chars — Preview: ${preview.replace(/\n/g, ' ')}`);
|
|
9
|
+
console.error(`[AUDIT] Execute Anonymous Apex — ${code.length} chars`);
|
|
11
10
|
}
|
|
12
11
|
/**
|
|
13
12
|
* Returns a shallow copy of an object with specified fields replaced by "[REDACTED]".
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aaron-pienza/mcp-server-salesforce",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "MCP server for Salesforce — query, analytics, Apex, REST API passthrough, and more.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -36,6 +36,9 @@
|
|
|
36
36
|
"url": "https://github.com/aaron-pienza/mcp-server-salesforce.git"
|
|
37
37
|
},
|
|
38
38
|
"homepage": "https://github.com/aaron-pienza/mcp-server-salesforce#readme",
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=18.0.0"
|
|
41
|
+
},
|
|
39
42
|
"publishConfig": {
|
|
40
43
|
"access": "public"
|
|
41
44
|
},
|