@aaron-pienza/mcp-server-salesforce 1.0.1 → 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
  }
@@ -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
- - "updateable" and "viewable" options control record access filtering`,
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: "Return only updateable records",
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: "Return only viewable records",
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
@@ -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 execFileAsync('sf', ['org', 'display', '--json']);
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;
@@ -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
- const preview = code.length > 200 ? code.substring(0, 200) + '...' : code;
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.1",
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",