@aaron-pienza/mcp-server-salesforce 1.0.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/LICENSE +21 -0
- package/README.md +398 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +392 -0
- package/dist/tools/aggregateQuery.d.ts +18 -0
- package/dist/tools/aggregateQuery.js +275 -0
- package/dist/tools/describe.d.ts +9 -0
- package/dist/tools/describe.js +45 -0
- package/dist/tools/describeAnalytics.d.ts +13 -0
- package/dist/tools/describeAnalytics.js +178 -0
- package/dist/tools/dml.d.ts +15 -0
- package/dist/tools/dml.js +133 -0
- package/dist/tools/executeAnonymous.d.ts +19 -0
- package/dist/tools/executeAnonymous.js +139 -0
- package/dist/tools/listAnalytics.d.ts +13 -0
- package/dist/tools/listAnalytics.js +149 -0
- package/dist/tools/manageDebugLogs.d.ts +31 -0
- package/dist/tools/manageDebugLogs.js +451 -0
- package/dist/tools/manageField.d.ts +32 -0
- package/dist/tools/manageField.js +358 -0
- package/dist/tools/manageFieldPermissions.d.ts +17 -0
- package/dist/tools/manageFieldPermissions.js +256 -0
- package/dist/tools/manageObject.d.ts +20 -0
- package/dist/tools/manageObject.js +138 -0
- package/dist/tools/query.d.ts +17 -0
- package/dist/tools/query.js +237 -0
- package/dist/tools/readApex.d.ts +28 -0
- package/dist/tools/readApex.js +188 -0
- package/dist/tools/readApexTrigger.d.ts +28 -0
- package/dist/tools/readApexTrigger.js +188 -0
- package/dist/tools/refreshDashboard.d.ts +13 -0
- package/dist/tools/refreshDashboard.js +91 -0
- package/dist/tools/restApi.d.ts +17 -0
- package/dist/tools/restApi.js +150 -0
- package/dist/tools/runAnalytics.d.ts +30 -0
- package/dist/tools/runAnalytics.js +367 -0
- package/dist/tools/search.d.ts +14 -0
- package/dist/tools/search.js +69 -0
- package/dist/tools/searchAll.d.ts +29 -0
- package/dist/tools/searchAll.js +258 -0
- package/dist/tools/writeApex.d.ts +27 -0
- package/dist/tools/writeApex.js +159 -0
- package/dist/tools/writeApexTrigger.d.ts +28 -0
- package/dist/tools/writeApexTrigger.js +187 -0
- package/dist/types/analytics.d.ts +2 -0
- package/dist/types/analytics.js +1 -0
- package/dist/types/connection.d.ts +52 -0
- package/dist/types/connection.js +21 -0
- package/dist/types/metadata.d.ts +43 -0
- package/dist/types/metadata.js +1 -0
- package/dist/types/salesforce.d.ts +33 -0
- package/dist/types/salesforce.js +1 -0
- package/dist/utils/connection.d.ts +7 -0
- package/dist/utils/connection.js +169 -0
- package/dist/utils/errorHandler.d.ts +15 -0
- package/dist/utils/errorHandler.js +23 -0
- package/dist/utils/logging.d.ts +12 -0
- package/dist/utils/logging.js +23 -0
- package/dist/utils/pagination.d.ts +14 -0
- package/dist/utils/pagination.js +26 -0
- package/dist/utils/sanitize.d.ts +44 -0
- package/dist/utils/sanitize.js +73 -0
- package/dist/utils/validate.d.ts +13 -0
- package/dist/utils/validate.js +60 -0
- package/package.json +52 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
+
import * as dotenv from "dotenv";
|
|
6
|
+
import { createSalesforceConnection } from "./utils/connection.js";
|
|
7
|
+
import { SEARCH_OBJECTS, handleSearchObjects } from "./tools/search.js";
|
|
8
|
+
import { DESCRIBE_OBJECT, handleDescribeObject } from "./tools/describe.js";
|
|
9
|
+
import { QUERY_RECORDS, handleQueryRecords } from "./tools/query.js";
|
|
10
|
+
import { AGGREGATE_QUERY, handleAggregateQuery } from "./tools/aggregateQuery.js";
|
|
11
|
+
import { DML_RECORDS, handleDMLRecords } from "./tools/dml.js";
|
|
12
|
+
import { MANAGE_OBJECT, handleManageObject } from "./tools/manageObject.js";
|
|
13
|
+
import { MANAGE_FIELD, handleManageField } from "./tools/manageField.js";
|
|
14
|
+
import { MANAGE_FIELD_PERMISSIONS, handleManageFieldPermissions } from "./tools/manageFieldPermissions.js";
|
|
15
|
+
import { SEARCH_ALL, handleSearchAll } from "./tools/searchAll.js";
|
|
16
|
+
import { READ_APEX, handleReadApex } from "./tools/readApex.js";
|
|
17
|
+
import { WRITE_APEX, handleWriteApex } from "./tools/writeApex.js";
|
|
18
|
+
import { READ_APEX_TRIGGER, handleReadApexTrigger } from "./tools/readApexTrigger.js";
|
|
19
|
+
import { WRITE_APEX_TRIGGER, handleWriteApexTrigger } from "./tools/writeApexTrigger.js";
|
|
20
|
+
import { EXECUTE_ANONYMOUS, handleExecuteAnonymous } from "./tools/executeAnonymous.js";
|
|
21
|
+
import { MANAGE_DEBUG_LOGS, handleManageDebugLogs } from "./tools/manageDebugLogs.js";
|
|
22
|
+
import { LIST_ANALYTICS, handleListAnalytics } from "./tools/listAnalytics.js";
|
|
23
|
+
import { DESCRIBE_ANALYTICS, handleDescribeAnalytics } from "./tools/describeAnalytics.js";
|
|
24
|
+
import { RUN_ANALYTICS, handleRunAnalytics } from "./tools/runAnalytics.js";
|
|
25
|
+
import { REFRESH_DASHBOARD, handleRefreshDashboard } from "./tools/refreshDashboard.js";
|
|
26
|
+
import { REST_API, handleRestApi } from "./tools/restApi.js";
|
|
27
|
+
// Load environment variables — quiet: true suppresses dotenv 17.x stderr logging
|
|
28
|
+
// MCP servers require stdout to contain ONLY JSON-RPC messages
|
|
29
|
+
dotenv.config({ quiet: true });
|
|
30
|
+
const server = new Server({
|
|
31
|
+
name: "salesforce-mcp-server",
|
|
32
|
+
version: "1.0.0",
|
|
33
|
+
}, {
|
|
34
|
+
capabilities: {
|
|
35
|
+
tools: {},
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
// Tool handlers
|
|
39
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
40
|
+
tools: [
|
|
41
|
+
SEARCH_OBJECTS,
|
|
42
|
+
DESCRIBE_OBJECT,
|
|
43
|
+
QUERY_RECORDS,
|
|
44
|
+
AGGREGATE_QUERY,
|
|
45
|
+
DML_RECORDS,
|
|
46
|
+
MANAGE_OBJECT,
|
|
47
|
+
MANAGE_FIELD,
|
|
48
|
+
MANAGE_FIELD_PERMISSIONS,
|
|
49
|
+
SEARCH_ALL,
|
|
50
|
+
READ_APEX,
|
|
51
|
+
WRITE_APEX,
|
|
52
|
+
READ_APEX_TRIGGER,
|
|
53
|
+
WRITE_APEX_TRIGGER,
|
|
54
|
+
EXECUTE_ANONYMOUS,
|
|
55
|
+
MANAGE_DEBUG_LOGS,
|
|
56
|
+
LIST_ANALYTICS,
|
|
57
|
+
DESCRIBE_ANALYTICS,
|
|
58
|
+
RUN_ANALYTICS,
|
|
59
|
+
REFRESH_DASHBOARD,
|
|
60
|
+
REST_API
|
|
61
|
+
],
|
|
62
|
+
}));
|
|
63
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
64
|
+
try {
|
|
65
|
+
const { name, arguments: args } = request.params;
|
|
66
|
+
if (!args)
|
|
67
|
+
throw new Error('Arguments are required');
|
|
68
|
+
const conn = await createSalesforceConnection();
|
|
69
|
+
switch (name) {
|
|
70
|
+
case "salesforce_search_objects": {
|
|
71
|
+
const searchObjArgs = args;
|
|
72
|
+
if (!searchObjArgs.searchPattern)
|
|
73
|
+
throw new Error('searchPattern is required');
|
|
74
|
+
const validatedArgs = {
|
|
75
|
+
searchPattern: searchObjArgs.searchPattern,
|
|
76
|
+
limit: searchObjArgs.limit,
|
|
77
|
+
offset: searchObjArgs.offset,
|
|
78
|
+
};
|
|
79
|
+
return await handleSearchObjects(conn, validatedArgs);
|
|
80
|
+
}
|
|
81
|
+
case "salesforce_describe_object": {
|
|
82
|
+
const { objectName } = args;
|
|
83
|
+
if (!objectName)
|
|
84
|
+
throw new Error('objectName is required');
|
|
85
|
+
return await handleDescribeObject(conn, objectName);
|
|
86
|
+
}
|
|
87
|
+
case "salesforce_query_records": {
|
|
88
|
+
const queryArgs = args;
|
|
89
|
+
if (!queryArgs.objectName || !Array.isArray(queryArgs.fields)) {
|
|
90
|
+
throw new Error('objectName and fields array are required for query');
|
|
91
|
+
}
|
|
92
|
+
// Type check and conversion
|
|
93
|
+
const validatedArgs = {
|
|
94
|
+
objectName: queryArgs.objectName,
|
|
95
|
+
fields: queryArgs.fields,
|
|
96
|
+
whereClause: queryArgs.whereClause,
|
|
97
|
+
orderBy: queryArgs.orderBy,
|
|
98
|
+
limit: queryArgs.limit,
|
|
99
|
+
offset: queryArgs.offset
|
|
100
|
+
};
|
|
101
|
+
return await handleQueryRecords(conn, validatedArgs);
|
|
102
|
+
}
|
|
103
|
+
case "salesforce_aggregate_query": {
|
|
104
|
+
const aggregateArgs = args;
|
|
105
|
+
if (!aggregateArgs.objectName || !Array.isArray(aggregateArgs.selectFields) || !Array.isArray(aggregateArgs.groupByFields)) {
|
|
106
|
+
throw new Error('objectName, selectFields array, and groupByFields array are required for aggregate query');
|
|
107
|
+
}
|
|
108
|
+
// Type check and conversion
|
|
109
|
+
const validatedArgs = {
|
|
110
|
+
objectName: aggregateArgs.objectName,
|
|
111
|
+
selectFields: aggregateArgs.selectFields,
|
|
112
|
+
groupByFields: aggregateArgs.groupByFields,
|
|
113
|
+
whereClause: aggregateArgs.whereClause,
|
|
114
|
+
havingClause: aggregateArgs.havingClause,
|
|
115
|
+
orderBy: aggregateArgs.orderBy,
|
|
116
|
+
limit: aggregateArgs.limit
|
|
117
|
+
};
|
|
118
|
+
return await handleAggregateQuery(conn, validatedArgs);
|
|
119
|
+
}
|
|
120
|
+
case "salesforce_dml_records": {
|
|
121
|
+
const dmlArgs = args;
|
|
122
|
+
if (!dmlArgs.operation || !dmlArgs.objectName || !Array.isArray(dmlArgs.records)) {
|
|
123
|
+
throw new Error('operation, objectName, and records array are required for DML');
|
|
124
|
+
}
|
|
125
|
+
const validatedArgs = {
|
|
126
|
+
operation: dmlArgs.operation,
|
|
127
|
+
objectName: dmlArgs.objectName,
|
|
128
|
+
records: dmlArgs.records,
|
|
129
|
+
externalIdField: dmlArgs.externalIdField
|
|
130
|
+
};
|
|
131
|
+
return await handleDMLRecords(conn, validatedArgs);
|
|
132
|
+
}
|
|
133
|
+
case "salesforce_manage_object": {
|
|
134
|
+
const objectArgs = args;
|
|
135
|
+
if (!objectArgs.operation || !objectArgs.objectName) {
|
|
136
|
+
throw new Error('operation and objectName are required for object management');
|
|
137
|
+
}
|
|
138
|
+
const validatedArgs = {
|
|
139
|
+
operation: objectArgs.operation,
|
|
140
|
+
objectName: objectArgs.objectName,
|
|
141
|
+
label: objectArgs.label,
|
|
142
|
+
pluralLabel: objectArgs.pluralLabel,
|
|
143
|
+
description: objectArgs.description,
|
|
144
|
+
nameFieldLabel: objectArgs.nameFieldLabel,
|
|
145
|
+
nameFieldType: objectArgs.nameFieldType,
|
|
146
|
+
nameFieldFormat: objectArgs.nameFieldFormat,
|
|
147
|
+
sharingModel: objectArgs.sharingModel
|
|
148
|
+
};
|
|
149
|
+
return await handleManageObject(conn, validatedArgs);
|
|
150
|
+
}
|
|
151
|
+
case "salesforce_manage_field": {
|
|
152
|
+
const fieldArgs = args;
|
|
153
|
+
if (!fieldArgs.operation || !fieldArgs.objectName || !fieldArgs.fieldName) {
|
|
154
|
+
throw new Error('operation, objectName, and fieldName are required for field management');
|
|
155
|
+
}
|
|
156
|
+
const validatedArgs = {
|
|
157
|
+
operation: fieldArgs.operation,
|
|
158
|
+
objectName: fieldArgs.objectName,
|
|
159
|
+
fieldName: fieldArgs.fieldName,
|
|
160
|
+
label: fieldArgs.label,
|
|
161
|
+
type: fieldArgs.type,
|
|
162
|
+
required: fieldArgs.required,
|
|
163
|
+
unique: fieldArgs.unique,
|
|
164
|
+
externalId: fieldArgs.externalId,
|
|
165
|
+
length: fieldArgs.length,
|
|
166
|
+
precision: fieldArgs.precision,
|
|
167
|
+
scale: fieldArgs.scale,
|
|
168
|
+
referenceTo: fieldArgs.referenceTo,
|
|
169
|
+
relationshipLabel: fieldArgs.relationshipLabel,
|
|
170
|
+
relationshipName: fieldArgs.relationshipName,
|
|
171
|
+
deleteConstraint: fieldArgs.deleteConstraint,
|
|
172
|
+
picklistValues: fieldArgs.picklistValues,
|
|
173
|
+
description: fieldArgs.description,
|
|
174
|
+
grantAccessTo: fieldArgs.grantAccessTo
|
|
175
|
+
};
|
|
176
|
+
return await handleManageField(conn, validatedArgs);
|
|
177
|
+
}
|
|
178
|
+
case "salesforce_manage_field_permissions": {
|
|
179
|
+
const permArgs = args;
|
|
180
|
+
if (!permArgs.operation || !permArgs.objectName || !permArgs.fieldName) {
|
|
181
|
+
throw new Error('operation, objectName, and fieldName are required for field permissions management');
|
|
182
|
+
}
|
|
183
|
+
const validatedArgs = {
|
|
184
|
+
operation: permArgs.operation,
|
|
185
|
+
objectName: permArgs.objectName,
|
|
186
|
+
fieldName: permArgs.fieldName,
|
|
187
|
+
profileNames: permArgs.profileNames,
|
|
188
|
+
readable: permArgs.readable,
|
|
189
|
+
editable: permArgs.editable
|
|
190
|
+
};
|
|
191
|
+
return await handleManageFieldPermissions(conn, validatedArgs);
|
|
192
|
+
}
|
|
193
|
+
case "salesforce_search_all": {
|
|
194
|
+
const searchArgs = args;
|
|
195
|
+
if (!searchArgs.searchTerm || !Array.isArray(searchArgs.objects)) {
|
|
196
|
+
throw new Error('searchTerm and objects array are required for search');
|
|
197
|
+
}
|
|
198
|
+
// Validate objects array
|
|
199
|
+
const objects = searchArgs.objects;
|
|
200
|
+
if (!objects.every(obj => obj.name && Array.isArray(obj.fields))) {
|
|
201
|
+
throw new Error('Each object must specify name and fields array');
|
|
202
|
+
}
|
|
203
|
+
// Type check and conversion
|
|
204
|
+
const validatedArgs = {
|
|
205
|
+
searchTerm: searchArgs.searchTerm,
|
|
206
|
+
searchIn: searchArgs.searchIn,
|
|
207
|
+
objects: objects.map(obj => ({
|
|
208
|
+
name: obj.name,
|
|
209
|
+
fields: obj.fields,
|
|
210
|
+
where: obj.where,
|
|
211
|
+
orderBy: obj.orderBy,
|
|
212
|
+
limit: obj.limit
|
|
213
|
+
})),
|
|
214
|
+
withClauses: searchArgs.withClauses,
|
|
215
|
+
updateable: searchArgs.updateable,
|
|
216
|
+
viewable: searchArgs.viewable
|
|
217
|
+
};
|
|
218
|
+
return await handleSearchAll(conn, validatedArgs);
|
|
219
|
+
}
|
|
220
|
+
case "salesforce_read_apex": {
|
|
221
|
+
const apexArgs = args;
|
|
222
|
+
// Type check and conversion
|
|
223
|
+
const validatedArgs = {
|
|
224
|
+
className: apexArgs.className,
|
|
225
|
+
namePattern: apexArgs.namePattern,
|
|
226
|
+
includeMetadata: apexArgs.includeMetadata,
|
|
227
|
+
limit: apexArgs.limit,
|
|
228
|
+
offset: apexArgs.offset
|
|
229
|
+
};
|
|
230
|
+
return await handleReadApex(conn, validatedArgs);
|
|
231
|
+
}
|
|
232
|
+
case "salesforce_write_apex": {
|
|
233
|
+
const apexArgs = args;
|
|
234
|
+
if (!apexArgs.operation || !apexArgs.className || !apexArgs.body) {
|
|
235
|
+
throw new Error('operation, className, and body are required for writing Apex');
|
|
236
|
+
}
|
|
237
|
+
// Type check and conversion
|
|
238
|
+
const validatedArgs = {
|
|
239
|
+
operation: apexArgs.operation,
|
|
240
|
+
className: apexArgs.className,
|
|
241
|
+
apiVersion: apexArgs.apiVersion,
|
|
242
|
+
body: apexArgs.body
|
|
243
|
+
};
|
|
244
|
+
return await handleWriteApex(conn, validatedArgs);
|
|
245
|
+
}
|
|
246
|
+
case "salesforce_read_apex_trigger": {
|
|
247
|
+
const triggerArgs = args;
|
|
248
|
+
// Type check and conversion
|
|
249
|
+
const validatedArgs = {
|
|
250
|
+
triggerName: triggerArgs.triggerName,
|
|
251
|
+
namePattern: triggerArgs.namePattern,
|
|
252
|
+
includeMetadata: triggerArgs.includeMetadata,
|
|
253
|
+
limit: triggerArgs.limit,
|
|
254
|
+
offset: triggerArgs.offset
|
|
255
|
+
};
|
|
256
|
+
return await handleReadApexTrigger(conn, validatedArgs);
|
|
257
|
+
}
|
|
258
|
+
case "salesforce_write_apex_trigger": {
|
|
259
|
+
const triggerArgs = args;
|
|
260
|
+
if (!triggerArgs.operation || !triggerArgs.triggerName || !triggerArgs.body) {
|
|
261
|
+
throw new Error('operation, triggerName, and body are required for writing Apex trigger');
|
|
262
|
+
}
|
|
263
|
+
// Type check and conversion
|
|
264
|
+
const validatedArgs = {
|
|
265
|
+
operation: triggerArgs.operation,
|
|
266
|
+
triggerName: triggerArgs.triggerName,
|
|
267
|
+
objectName: triggerArgs.objectName,
|
|
268
|
+
apiVersion: triggerArgs.apiVersion,
|
|
269
|
+
body: triggerArgs.body
|
|
270
|
+
};
|
|
271
|
+
return await handleWriteApexTrigger(conn, validatedArgs);
|
|
272
|
+
}
|
|
273
|
+
case "salesforce_execute_anonymous": {
|
|
274
|
+
const executeArgs = args;
|
|
275
|
+
if (!executeArgs.apexCode) {
|
|
276
|
+
throw new Error('apexCode is required for executing anonymous Apex');
|
|
277
|
+
}
|
|
278
|
+
// Type check and conversion
|
|
279
|
+
const validatedArgs = {
|
|
280
|
+
apexCode: executeArgs.apexCode,
|
|
281
|
+
logLevel: executeArgs.logLevel
|
|
282
|
+
};
|
|
283
|
+
return await handleExecuteAnonymous(conn, validatedArgs);
|
|
284
|
+
}
|
|
285
|
+
case "salesforce_manage_debug_logs": {
|
|
286
|
+
const debugLogsArgs = args;
|
|
287
|
+
if (!debugLogsArgs.operation || !debugLogsArgs.username) {
|
|
288
|
+
throw new Error('operation and username are required for managing debug logs');
|
|
289
|
+
}
|
|
290
|
+
// Type check and conversion
|
|
291
|
+
const validatedArgs = {
|
|
292
|
+
operation: debugLogsArgs.operation,
|
|
293
|
+
username: debugLogsArgs.username,
|
|
294
|
+
logLevel: debugLogsArgs.logLevel,
|
|
295
|
+
expirationTime: debugLogsArgs.expirationTime,
|
|
296
|
+
limit: debugLogsArgs.limit,
|
|
297
|
+
logId: debugLogsArgs.logId,
|
|
298
|
+
includeBody: debugLogsArgs.includeBody,
|
|
299
|
+
offset: debugLogsArgs.offset
|
|
300
|
+
};
|
|
301
|
+
return await handleManageDebugLogs(conn, validatedArgs);
|
|
302
|
+
}
|
|
303
|
+
case "salesforce_list_analytics": {
|
|
304
|
+
const listAnalyticsArgs = args;
|
|
305
|
+
if (!listAnalyticsArgs.type) {
|
|
306
|
+
throw new Error('type is required');
|
|
307
|
+
}
|
|
308
|
+
const validatedArgs = {
|
|
309
|
+
type: listAnalyticsArgs.type,
|
|
310
|
+
searchTerm: listAnalyticsArgs.searchTerm,
|
|
311
|
+
};
|
|
312
|
+
return await handleListAnalytics(conn, validatedArgs);
|
|
313
|
+
}
|
|
314
|
+
case "salesforce_describe_analytics": {
|
|
315
|
+
const descAnalyticsArgs = args;
|
|
316
|
+
if (!descAnalyticsArgs.type || !descAnalyticsArgs.resourceId) {
|
|
317
|
+
throw new Error('type and resourceId are required');
|
|
318
|
+
}
|
|
319
|
+
const validatedArgs = {
|
|
320
|
+
type: descAnalyticsArgs.type,
|
|
321
|
+
resourceId: descAnalyticsArgs.resourceId,
|
|
322
|
+
};
|
|
323
|
+
return await handleDescribeAnalytics(conn, validatedArgs);
|
|
324
|
+
}
|
|
325
|
+
case "salesforce_run_analytics": {
|
|
326
|
+
const runAnalyticsArgs = args;
|
|
327
|
+
if (!runAnalyticsArgs.type || !runAnalyticsArgs.resourceId) {
|
|
328
|
+
throw new Error('type and resourceId are required');
|
|
329
|
+
}
|
|
330
|
+
const validatedArgs = {
|
|
331
|
+
type: runAnalyticsArgs.type,
|
|
332
|
+
resourceId: runAnalyticsArgs.resourceId,
|
|
333
|
+
includeDetails: runAnalyticsArgs.includeDetails,
|
|
334
|
+
filters: runAnalyticsArgs.filters,
|
|
335
|
+
booleanFilter: runAnalyticsArgs.booleanFilter,
|
|
336
|
+
standardDateFilter: runAnalyticsArgs.standardDateFilter,
|
|
337
|
+
topRows: runAnalyticsArgs.topRows,
|
|
338
|
+
};
|
|
339
|
+
return await handleRunAnalytics(conn, validatedArgs);
|
|
340
|
+
}
|
|
341
|
+
case "salesforce_refresh_dashboard": {
|
|
342
|
+
const refreshArgs = args;
|
|
343
|
+
if (!refreshArgs.operation || !refreshArgs.dashboardId) {
|
|
344
|
+
throw new Error('operation and dashboardId are required');
|
|
345
|
+
}
|
|
346
|
+
const validatedArgs = {
|
|
347
|
+
operation: refreshArgs.operation,
|
|
348
|
+
dashboardId: refreshArgs.dashboardId,
|
|
349
|
+
};
|
|
350
|
+
return await handleRefreshDashboard(conn, validatedArgs);
|
|
351
|
+
}
|
|
352
|
+
case "salesforce_rest_api": {
|
|
353
|
+
const restArgs = args;
|
|
354
|
+
if (!restArgs.method || !restArgs.endpoint) {
|
|
355
|
+
throw new Error('method and endpoint are required for REST API calls');
|
|
356
|
+
}
|
|
357
|
+
const validatedArgs = {
|
|
358
|
+
method: restArgs.method,
|
|
359
|
+
endpoint: restArgs.endpoint,
|
|
360
|
+
body: restArgs.body,
|
|
361
|
+
queryParameters: restArgs.queryParameters,
|
|
362
|
+
apiVersion: restArgs.apiVersion,
|
|
363
|
+
rawPath: restArgs.rawPath
|
|
364
|
+
};
|
|
365
|
+
return await handleRestApi(conn, validatedArgs);
|
|
366
|
+
}
|
|
367
|
+
default:
|
|
368
|
+
return {
|
|
369
|
+
content: [{ type: "text", text: `Unknown tool: ${name}` }],
|
|
370
|
+
isError: true,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
catch (error) {
|
|
375
|
+
return {
|
|
376
|
+
content: [{
|
|
377
|
+
type: "text",
|
|
378
|
+
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
|
|
379
|
+
}],
|
|
380
|
+
isError: true,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
async function runServer() {
|
|
385
|
+
const transport = new StdioServerTransport();
|
|
386
|
+
await server.connect(transport);
|
|
387
|
+
console.error("Salesforce MCP Server running on stdio");
|
|
388
|
+
}
|
|
389
|
+
runServer().catch((error) => {
|
|
390
|
+
console.error("Fatal error running server:", error);
|
|
391
|
+
process.exit(1);
|
|
392
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Tool } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
+
export declare const AGGREGATE_QUERY: Tool;
|
|
3
|
+
export interface AggregateQueryArgs {
|
|
4
|
+
objectName: string;
|
|
5
|
+
selectFields: string[];
|
|
6
|
+
groupByFields: string[];
|
|
7
|
+
whereClause?: string;
|
|
8
|
+
havingClause?: string;
|
|
9
|
+
orderBy?: string;
|
|
10
|
+
limit?: number;
|
|
11
|
+
}
|
|
12
|
+
export declare function handleAggregateQuery(conn: any, args: AggregateQueryArgs): Promise<{
|
|
13
|
+
content: {
|
|
14
|
+
type: string;
|
|
15
|
+
text: string;
|
|
16
|
+
}[];
|
|
17
|
+
isError: boolean;
|
|
18
|
+
}>;
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { DEFAULT_LIMITS } from "../utils/pagination.js";
|
|
2
|
+
import { validateIdentifier } from "../utils/sanitize.js";
|
|
3
|
+
export const AGGREGATE_QUERY = {
|
|
4
|
+
name: "salesforce_aggregate_query",
|
|
5
|
+
description: `Execute SOQL queries with GROUP BY, aggregate functions, and statistical analysis. Use this tool for queries that summarize and group data rather than returning individual records.
|
|
6
|
+
|
|
7
|
+
NOTE: For regular queries without GROUP BY or aggregates, use salesforce_query_records instead.
|
|
8
|
+
|
|
9
|
+
This tool handles:
|
|
10
|
+
1. GROUP BY queries (single/multiple fields, related objects, date functions)
|
|
11
|
+
2. Aggregate functions: COUNT(), COUNT_DISTINCT(), SUM(), AVG(), MIN(), MAX()
|
|
12
|
+
3. HAVING clauses for filtering grouped results
|
|
13
|
+
4. Date/time grouping: CALENDAR_YEAR(), CALENDAR_MONTH(), CALENDAR_QUARTER(), FISCAL_YEAR(), FISCAL_QUARTER()
|
|
14
|
+
|
|
15
|
+
Examples:
|
|
16
|
+
1. Count opportunities by stage:
|
|
17
|
+
- objectName: "Opportunity"
|
|
18
|
+
- selectFields: ["StageName", "COUNT(Id) OpportunityCount"]
|
|
19
|
+
- groupByFields: ["StageName"]
|
|
20
|
+
|
|
21
|
+
2. Analyze cases by priority and status:
|
|
22
|
+
- objectName: "Case"
|
|
23
|
+
- selectFields: ["Priority", "Status", "COUNT(Id) CaseCount", "AVG(Days_Open__c) AvgDaysOpen"]
|
|
24
|
+
- groupByFields: ["Priority", "Status"]
|
|
25
|
+
|
|
26
|
+
3. Count contacts by account industry:
|
|
27
|
+
- objectName: "Contact"
|
|
28
|
+
- selectFields: ["Account.Industry", "COUNT(Id) ContactCount"]
|
|
29
|
+
- groupByFields: ["Account.Industry"]
|
|
30
|
+
|
|
31
|
+
4. Quarterly opportunity analysis:
|
|
32
|
+
- objectName: "Opportunity"
|
|
33
|
+
- selectFields: ["CALENDAR_YEAR(CloseDate) Year", "CALENDAR_QUARTER(CloseDate) Quarter", "SUM(Amount) Revenue"]
|
|
34
|
+
- groupByFields: ["CALENDAR_YEAR(CloseDate)", "CALENDAR_QUARTER(CloseDate)"]
|
|
35
|
+
|
|
36
|
+
5. Find accounts with more than 10 opportunities:
|
|
37
|
+
- objectName: "Opportunity"
|
|
38
|
+
- selectFields: ["Account.Name", "COUNT(Id) OpportunityCount"]
|
|
39
|
+
- groupByFields: ["Account.Name"]
|
|
40
|
+
- havingClause: "COUNT(Id) > 10"
|
|
41
|
+
|
|
42
|
+
Important Rules:
|
|
43
|
+
- All non-aggregate fields in selectFields MUST be included in groupByFields
|
|
44
|
+
- Use whereClause to filter rows BEFORE grouping
|
|
45
|
+
- Use havingClause to filter AFTER grouping (for aggregate conditions)
|
|
46
|
+
- ORDER BY can only use fields from groupByFields or aggregate functions
|
|
47
|
+
- OFFSET is not supported with GROUP BY in Salesforce`,
|
|
48
|
+
inputSchema: {
|
|
49
|
+
type: "object",
|
|
50
|
+
properties: {
|
|
51
|
+
objectName: {
|
|
52
|
+
type: "string",
|
|
53
|
+
description: "API name of the object to query"
|
|
54
|
+
},
|
|
55
|
+
selectFields: {
|
|
56
|
+
type: "array",
|
|
57
|
+
items: { type: "string" },
|
|
58
|
+
description: "Fields to select - mix of group fields and aggregates. Format: 'FieldName' or 'COUNT(Id) AliasName'"
|
|
59
|
+
},
|
|
60
|
+
groupByFields: {
|
|
61
|
+
type: "array",
|
|
62
|
+
items: { type: "string" },
|
|
63
|
+
description: "Fields to group by - must include all non-aggregate fields from selectFields"
|
|
64
|
+
},
|
|
65
|
+
whereClause: {
|
|
66
|
+
type: "string",
|
|
67
|
+
description: "WHERE clause to filter rows BEFORE grouping (cannot contain aggregate functions)",
|
|
68
|
+
optional: true
|
|
69
|
+
},
|
|
70
|
+
havingClause: {
|
|
71
|
+
type: "string",
|
|
72
|
+
description: "HAVING clause to filter results AFTER grouping (use for aggregate conditions)",
|
|
73
|
+
optional: true
|
|
74
|
+
},
|
|
75
|
+
orderBy: {
|
|
76
|
+
type: "string",
|
|
77
|
+
description: "ORDER BY clause - can only use grouped fields or aggregate functions",
|
|
78
|
+
optional: true
|
|
79
|
+
},
|
|
80
|
+
limit: {
|
|
81
|
+
type: "number",
|
|
82
|
+
description: "Maximum number of grouped results to return",
|
|
83
|
+
optional: true
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
required: ["objectName", "selectFields", "groupByFields"]
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
// Aggregate functions that don't need to be in GROUP BY
|
|
90
|
+
const AGGREGATE_FUNCTIONS = ['COUNT', 'COUNT_DISTINCT', 'SUM', 'AVG', 'MIN', 'MAX'];
|
|
91
|
+
const DATE_FUNCTIONS = ['CALENDAR_YEAR', 'CALENDAR_MONTH', 'CALENDAR_QUARTER', 'FISCAL_YEAR', 'FISCAL_QUARTER'];
|
|
92
|
+
// Helper function to detect if a field contains an aggregate function
|
|
93
|
+
function isAggregateField(field) {
|
|
94
|
+
const upperField = field.toUpperCase();
|
|
95
|
+
return AGGREGATE_FUNCTIONS.some(func => upperField.includes(`${func}(`));
|
|
96
|
+
}
|
|
97
|
+
// Helper function to extract the base field from a select field (removing alias)
|
|
98
|
+
function extractBaseField(field) {
|
|
99
|
+
// Remove alias if present (e.g., "COUNT(Id) OpportunityCount" -> "COUNT(Id)")
|
|
100
|
+
const parts = field.trim().split(/\s+/);
|
|
101
|
+
return parts[0];
|
|
102
|
+
}
|
|
103
|
+
// Helper function to extract non-aggregate fields from select fields
|
|
104
|
+
function extractNonAggregateFields(selectFields) {
|
|
105
|
+
return selectFields
|
|
106
|
+
.filter(field => !isAggregateField(field))
|
|
107
|
+
.map(field => extractBaseField(field));
|
|
108
|
+
}
|
|
109
|
+
// Helper function to validate that all non-aggregate fields are in GROUP BY
|
|
110
|
+
function validateGroupByFields(selectFields, groupByFields) {
|
|
111
|
+
const nonAggregateFields = extractNonAggregateFields(selectFields);
|
|
112
|
+
const groupBySet = new Set(groupByFields.map(f => f.trim()));
|
|
113
|
+
const missingFields = nonAggregateFields.filter(field => !groupBySet.has(field));
|
|
114
|
+
return {
|
|
115
|
+
isValid: missingFields.length === 0,
|
|
116
|
+
missingFields
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
// Helper function to validate WHERE clause doesn't contain aggregates
|
|
120
|
+
function validateWhereClause(whereClause) {
|
|
121
|
+
if (!whereClause)
|
|
122
|
+
return { isValid: true };
|
|
123
|
+
const upperWhere = whereClause.toUpperCase();
|
|
124
|
+
for (const func of AGGREGATE_FUNCTIONS) {
|
|
125
|
+
if (upperWhere.includes(`${func}(`)) {
|
|
126
|
+
return {
|
|
127
|
+
isValid: false,
|
|
128
|
+
error: `WHERE clause cannot contain aggregate functions. Use HAVING clause instead for aggregate conditions like ${func}()`
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return { isValid: true };
|
|
133
|
+
}
|
|
134
|
+
// Helper function to validate ORDER BY fields
|
|
135
|
+
function validateOrderBy(orderBy, groupByFields, selectFields) {
|
|
136
|
+
if (!orderBy)
|
|
137
|
+
return { isValid: true };
|
|
138
|
+
// Extract fields from ORDER BY (handling DESC/ASC)
|
|
139
|
+
const orderByParts = orderBy.split(',').map(part => {
|
|
140
|
+
return part.trim().replace(/ (DESC|ASC)$/i, '').trim();
|
|
141
|
+
});
|
|
142
|
+
const groupBySet = new Set(groupByFields);
|
|
143
|
+
const aggregateFields = selectFields.filter(field => isAggregateField(field)).map(field => extractBaseField(field));
|
|
144
|
+
for (const orderField of orderByParts) {
|
|
145
|
+
// Check if it's in GROUP BY or is an aggregate
|
|
146
|
+
if (!groupBySet.has(orderField) && !aggregateFields.some(agg => agg === orderField) && !isAggregateField(orderField)) {
|
|
147
|
+
return {
|
|
148
|
+
isValid: false,
|
|
149
|
+
error: `ORDER BY field '${orderField}' must be in GROUP BY clause or be an aggregate function`
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return { isValid: true };
|
|
154
|
+
}
|
|
155
|
+
export async function handleAggregateQuery(conn, args) {
|
|
156
|
+
const { objectName, selectFields, groupByFields, whereClause, havingClause, orderBy, limit } = args;
|
|
157
|
+
try {
|
|
158
|
+
// Validate GROUP BY contains all non-aggregate fields
|
|
159
|
+
const groupByValidation = validateGroupByFields(selectFields, groupByFields);
|
|
160
|
+
if (!groupByValidation.isValid) {
|
|
161
|
+
return {
|
|
162
|
+
content: [{
|
|
163
|
+
type: "text",
|
|
164
|
+
text: `Error: The following non-aggregate fields must be included in GROUP BY clause: ${groupByValidation.missingFields.join(', ')}\n\n` +
|
|
165
|
+
`All fields in SELECT that are not aggregate functions (COUNT, SUM, AVG, etc.) must be included in GROUP BY.`
|
|
166
|
+
}],
|
|
167
|
+
isError: true,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
// Validate WHERE clause doesn't contain aggregates
|
|
171
|
+
const whereValidation = validateWhereClause(whereClause);
|
|
172
|
+
if (!whereValidation.isValid) {
|
|
173
|
+
return {
|
|
174
|
+
content: [{
|
|
175
|
+
type: "text",
|
|
176
|
+
text: whereValidation.error
|
|
177
|
+
}],
|
|
178
|
+
isError: true,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
// Validate ORDER BY fields
|
|
182
|
+
const orderByValidation = validateOrderBy(orderBy, groupByFields, selectFields);
|
|
183
|
+
if (!orderByValidation.isValid) {
|
|
184
|
+
return {
|
|
185
|
+
content: [{
|
|
186
|
+
type: "text",
|
|
187
|
+
text: orderByValidation.error
|
|
188
|
+
}],
|
|
189
|
+
isError: true,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
const objValidation = validateIdentifier(objectName);
|
|
193
|
+
if (!objValidation.valid) {
|
|
194
|
+
return { content: [{ type: "text", text: objValidation.error }], isError: true };
|
|
195
|
+
}
|
|
196
|
+
// Construct SOQL query
|
|
197
|
+
let soql = `SELECT ${selectFields.join(', ')} FROM ${objectName}`;
|
|
198
|
+
if (whereClause)
|
|
199
|
+
soql += ` WHERE ${whereClause}`;
|
|
200
|
+
soql += ` GROUP BY ${groupByFields.join(', ')}`;
|
|
201
|
+
if (havingClause)
|
|
202
|
+
soql += ` HAVING ${havingClause}`;
|
|
203
|
+
if (orderBy)
|
|
204
|
+
soql += ` ORDER BY ${orderBy}`;
|
|
205
|
+
const effectiveLimit = limit ?? DEFAULT_LIMITS.aggregate;
|
|
206
|
+
soql += ` LIMIT ${effectiveLimit}`;
|
|
207
|
+
const result = await conn.query(soql);
|
|
208
|
+
// Format the output
|
|
209
|
+
const formattedRecords = result.records.map((record, index) => {
|
|
210
|
+
const recordStr = selectFields.map(field => {
|
|
211
|
+
const baseField = extractBaseField(field);
|
|
212
|
+
const fieldParts = field.trim().split(/\s+/);
|
|
213
|
+
const displayName = fieldParts.length > 1 ? fieldParts[fieldParts.length - 1] : baseField;
|
|
214
|
+
// Handle nested fields in results (e.g., Account.Industry in GROUP BY)
|
|
215
|
+
if (baseField.includes('.')) {
|
|
216
|
+
// Strategy 1: Walk nested object (e.g., record.Account.Industry)
|
|
217
|
+
const parts = baseField.split('.');
|
|
218
|
+
let value = record;
|
|
219
|
+
for (const part of parts) {
|
|
220
|
+
value = value?.[part];
|
|
221
|
+
}
|
|
222
|
+
// Strategy 2: Flattened key (jsforce sometimes returns flat keys)
|
|
223
|
+
if (value === null || value === undefined) {
|
|
224
|
+
value = record[baseField];
|
|
225
|
+
}
|
|
226
|
+
// Strategy 3: Alias/display name
|
|
227
|
+
if (value === null || value === undefined) {
|
|
228
|
+
value = record[displayName];
|
|
229
|
+
}
|
|
230
|
+
// Strategy 4: Salesforce expr aliases (expr0, expr1, etc.)
|
|
231
|
+
if (value === null || value === undefined) {
|
|
232
|
+
const fieldIndex = selectFields.indexOf(field);
|
|
233
|
+
value = record[`expr${fieldIndex}`];
|
|
234
|
+
}
|
|
235
|
+
return ` ${displayName}: ${value !== null && value !== undefined ? value : 'null'}`;
|
|
236
|
+
}
|
|
237
|
+
const value = record[baseField] ?? record[displayName];
|
|
238
|
+
return ` ${displayName}: ${value !== null && value !== undefined ? value : 'null'}`;
|
|
239
|
+
}).join('\n');
|
|
240
|
+
return `Group ${index + 1}:\n${recordStr}`;
|
|
241
|
+
}).join('\n\n');
|
|
242
|
+
const totalSize = result.totalSize ?? result.records.length;
|
|
243
|
+
let text = `Aggregate query returned ${result.records.length} of ${totalSize} grouped results:\n\n${formattedRecords}`;
|
|
244
|
+
if (result.records.length < totalSize) {
|
|
245
|
+
text += `\n\nNote: Results are truncated (${result.records.length} of ${totalSize}). Narrow with WHERE/HAVING to see different slices (OFFSET is not supported with GROUP BY in Salesforce).`;
|
|
246
|
+
}
|
|
247
|
+
return {
|
|
248
|
+
content: [{
|
|
249
|
+
type: "text",
|
|
250
|
+
text,
|
|
251
|
+
}],
|
|
252
|
+
isError: false,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
catch (error) {
|
|
256
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
257
|
+
// Provide more helpful error messages for common issues
|
|
258
|
+
let enhancedError = errorMessage;
|
|
259
|
+
if (errorMessage.includes('MALFORMED_QUERY')) {
|
|
260
|
+
if (errorMessage.includes('GROUP BY')) {
|
|
261
|
+
enhancedError = `Query error: ${errorMessage}\n\nCommon issues:\n` +
|
|
262
|
+
`1. Ensure all non-aggregate fields in SELECT are in GROUP BY\n` +
|
|
263
|
+
`2. Check that date functions match exactly between SELECT and GROUP BY\n` +
|
|
264
|
+
`3. Verify field names and relationships are correct`;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return {
|
|
268
|
+
content: [{
|
|
269
|
+
type: "text",
|
|
270
|
+
text: `Error executing aggregate query: ${enhancedError}`
|
|
271
|
+
}],
|
|
272
|
+
isError: true,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
}
|