@rohithvemulapally/mcp-server-salesforce 0.0.6
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 +357 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +312 -0
- package/dist/tools/aggregateQuery.d.ts +18 -0
- package/dist/tools/aggregateQuery.js +250 -0
- package/dist/tools/describe.d.ts +9 -0
- package/dist/tools/describe.js +33 -0
- package/dist/tools/dml.d.ts +15 -0
- package/dist/tools/dml.js +105 -0
- package/dist/tools/executeAnonymous.d.ts +25 -0
- package/dist/tools/executeAnonymous.js +130 -0
- package/dist/tools/manageDebugLogs.d.ts +30 -0
- package/dist/tools/manageDebugLogs.js +424 -0
- package/dist/tools/manageField.d.ts +32 -0
- package/dist/tools/manageField.js +349 -0
- package/dist/tools/manageFieldPermissions.d.ts +17 -0
- package/dist/tools/manageFieldPermissions.js +247 -0
- package/dist/tools/manageObject.d.ts +20 -0
- package/dist/tools/manageObject.js +138 -0
- package/dist/tools/metadata.d.ts +9 -0
- package/dist/tools/metadata.js +66 -0
- package/dist/tools/query.d.ts +16 -0
- package/dist/tools/query.js +114 -0
- package/dist/tools/readApex.d.ts +26 -0
- package/dist/tools/readApex.js +165 -0
- package/dist/tools/readApexTrigger.d.ts +26 -0
- package/dist/tools/readApexTrigger.js +165 -0
- package/dist/tools/search.d.ts +9 -0
- package/dist/tools/search.js +45 -0
- package/dist/tools/searchAll.d.ts +29 -0
- package/dist/tools/searchAll.js +250 -0
- package/dist/tools/writeApex.d.ts +27 -0
- package/dist/tools/writeApex.js +154 -0
- package/dist/tools/writeApexTrigger.d.ts +28 -0
- package/dist/tools/writeApexTrigger.js +178 -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 +172 -0
- package/dist/utils/errorHandler.d.ts +15 -0
- package/dist/utils/errorHandler.js +23 -0
- package/package.json +39 -0
|
@@ -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,250 @@
|
|
|
1
|
+
export const AGGREGATE_QUERY = {
|
|
2
|
+
name: "salesforce_aggregate_query",
|
|
3
|
+
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.
|
|
4
|
+
|
|
5
|
+
NOTE: For regular queries without GROUP BY or aggregates, use salesforce_query_records instead.
|
|
6
|
+
|
|
7
|
+
This tool handles:
|
|
8
|
+
1. GROUP BY queries (single/multiple fields, related objects, date functions)
|
|
9
|
+
2. Aggregate functions: COUNT(), COUNT_DISTINCT(), SUM(), AVG(), MIN(), MAX()
|
|
10
|
+
3. HAVING clauses for filtering grouped results
|
|
11
|
+
4. Date/time grouping: CALENDAR_YEAR(), CALENDAR_MONTH(), CALENDAR_QUARTER(), FISCAL_YEAR(), FISCAL_QUARTER()
|
|
12
|
+
|
|
13
|
+
Examples:
|
|
14
|
+
1. Count opportunities by stage:
|
|
15
|
+
- objectName: "Opportunity"
|
|
16
|
+
- selectFields: ["StageName", "COUNT(Id) OpportunityCount"]
|
|
17
|
+
- groupByFields: ["StageName"]
|
|
18
|
+
|
|
19
|
+
2. Analyze cases by priority and status:
|
|
20
|
+
- objectName: "Case"
|
|
21
|
+
- selectFields: ["Priority", "Status", "COUNT(Id) CaseCount", "AVG(Days_Open__c) AvgDaysOpen"]
|
|
22
|
+
- groupByFields: ["Priority", "Status"]
|
|
23
|
+
|
|
24
|
+
3. Count contacts by account industry:
|
|
25
|
+
- objectName: "Contact"
|
|
26
|
+
- selectFields: ["Account.Industry", "COUNT(Id) ContactCount"]
|
|
27
|
+
- groupByFields: ["Account.Industry"]
|
|
28
|
+
|
|
29
|
+
4. Quarterly opportunity analysis:
|
|
30
|
+
- objectName: "Opportunity"
|
|
31
|
+
- selectFields: ["CALENDAR_YEAR(CloseDate) Year", "CALENDAR_QUARTER(CloseDate) Quarter", "SUM(Amount) Revenue"]
|
|
32
|
+
- groupByFields: ["CALENDAR_YEAR(CloseDate)", "CALENDAR_QUARTER(CloseDate)"]
|
|
33
|
+
|
|
34
|
+
5. Find accounts with more than 10 opportunities:
|
|
35
|
+
- objectName: "Opportunity"
|
|
36
|
+
- selectFields: ["Account.Name", "COUNT(Id) OpportunityCount"]
|
|
37
|
+
- groupByFields: ["Account.Name"]
|
|
38
|
+
- havingClause: "COUNT(Id) > 10"
|
|
39
|
+
|
|
40
|
+
Important Rules:
|
|
41
|
+
- All non-aggregate fields in selectFields MUST be included in groupByFields
|
|
42
|
+
- Use whereClause to filter rows BEFORE grouping
|
|
43
|
+
- Use havingClause to filter AFTER grouping (for aggregate conditions)
|
|
44
|
+
- ORDER BY can only use fields from groupByFields or aggregate functions
|
|
45
|
+
- OFFSET is not supported with GROUP BY in Salesforce`,
|
|
46
|
+
inputSchema: {
|
|
47
|
+
type: "object",
|
|
48
|
+
properties: {
|
|
49
|
+
objectName: {
|
|
50
|
+
type: "string",
|
|
51
|
+
description: "API name of the object to query"
|
|
52
|
+
},
|
|
53
|
+
selectFields: {
|
|
54
|
+
type: "array",
|
|
55
|
+
items: { type: "string" },
|
|
56
|
+
description: "Fields to select - mix of group fields and aggregates. Format: 'FieldName' or 'COUNT(Id) AliasName'"
|
|
57
|
+
},
|
|
58
|
+
groupByFields: {
|
|
59
|
+
type: "array",
|
|
60
|
+
items: { type: "string" },
|
|
61
|
+
description: "Fields to group by - must include all non-aggregate fields from selectFields"
|
|
62
|
+
},
|
|
63
|
+
whereClause: {
|
|
64
|
+
type: "string",
|
|
65
|
+
description: "WHERE clause to filter rows BEFORE grouping (cannot contain aggregate functions)",
|
|
66
|
+
optional: true
|
|
67
|
+
},
|
|
68
|
+
havingClause: {
|
|
69
|
+
type: "string",
|
|
70
|
+
description: "HAVING clause to filter results AFTER grouping (use for aggregate conditions)",
|
|
71
|
+
optional: true
|
|
72
|
+
},
|
|
73
|
+
orderBy: {
|
|
74
|
+
type: "string",
|
|
75
|
+
description: "ORDER BY clause - can only use grouped fields or aggregate functions",
|
|
76
|
+
optional: true
|
|
77
|
+
},
|
|
78
|
+
limit: {
|
|
79
|
+
type: "number",
|
|
80
|
+
description: "Maximum number of grouped results to return",
|
|
81
|
+
optional: true
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
required: ["objectName", "selectFields", "groupByFields"]
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
// Aggregate functions that don't need to be in GROUP BY
|
|
88
|
+
const AGGREGATE_FUNCTIONS = ['COUNT', 'COUNT_DISTINCT', 'SUM', 'AVG', 'MIN', 'MAX'];
|
|
89
|
+
const DATE_FUNCTIONS = ['CALENDAR_YEAR', 'CALENDAR_MONTH', 'CALENDAR_QUARTER', 'FISCAL_YEAR', 'FISCAL_QUARTER'];
|
|
90
|
+
// Helper function to detect if a field contains an aggregate function
|
|
91
|
+
function isAggregateField(field) {
|
|
92
|
+
const upperField = field.toUpperCase();
|
|
93
|
+
return AGGREGATE_FUNCTIONS.some(func => upperField.includes(`${func}(`));
|
|
94
|
+
}
|
|
95
|
+
// Helper function to extract the base field from a select field (removing alias)
|
|
96
|
+
function extractBaseField(field) {
|
|
97
|
+
// Remove alias if present (e.g., "COUNT(Id) OpportunityCount" -> "COUNT(Id)")
|
|
98
|
+
const parts = field.trim().split(/\s+/);
|
|
99
|
+
return parts[0];
|
|
100
|
+
}
|
|
101
|
+
// Helper function to extract non-aggregate fields from select fields
|
|
102
|
+
function extractNonAggregateFields(selectFields) {
|
|
103
|
+
return selectFields
|
|
104
|
+
.filter(field => !isAggregateField(field))
|
|
105
|
+
.map(field => extractBaseField(field));
|
|
106
|
+
}
|
|
107
|
+
// Helper function to validate that all non-aggregate fields are in GROUP BY
|
|
108
|
+
function validateGroupByFields(selectFields, groupByFields) {
|
|
109
|
+
const nonAggregateFields = extractNonAggregateFields(selectFields);
|
|
110
|
+
const groupBySet = new Set(groupByFields.map(f => f.trim()));
|
|
111
|
+
const missingFields = nonAggregateFields.filter(field => !groupBySet.has(field));
|
|
112
|
+
return {
|
|
113
|
+
isValid: missingFields.length === 0,
|
|
114
|
+
missingFields
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
// Helper function to validate WHERE clause doesn't contain aggregates
|
|
118
|
+
function validateWhereClause(whereClause) {
|
|
119
|
+
if (!whereClause)
|
|
120
|
+
return { isValid: true };
|
|
121
|
+
const upperWhere = whereClause.toUpperCase();
|
|
122
|
+
for (const func of AGGREGATE_FUNCTIONS) {
|
|
123
|
+
if (upperWhere.includes(`${func}(`)) {
|
|
124
|
+
return {
|
|
125
|
+
isValid: false,
|
|
126
|
+
error: `WHERE clause cannot contain aggregate functions. Use HAVING clause instead for aggregate conditions like ${func}()`
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return { isValid: true };
|
|
131
|
+
}
|
|
132
|
+
// Helper function to validate ORDER BY fields
|
|
133
|
+
function validateOrderBy(orderBy, groupByFields, selectFields) {
|
|
134
|
+
if (!orderBy)
|
|
135
|
+
return { isValid: true };
|
|
136
|
+
// Extract fields from ORDER BY (handling DESC/ASC)
|
|
137
|
+
const orderByParts = orderBy.split(',').map(part => {
|
|
138
|
+
return part.trim().replace(/ (DESC|ASC)$/i, '').trim();
|
|
139
|
+
});
|
|
140
|
+
const groupBySet = new Set(groupByFields);
|
|
141
|
+
const aggregateFields = selectFields.filter(field => isAggregateField(field)).map(field => extractBaseField(field));
|
|
142
|
+
for (const orderField of orderByParts) {
|
|
143
|
+
// Check if it's in GROUP BY or is an aggregate
|
|
144
|
+
if (!groupBySet.has(orderField) && !aggregateFields.some(agg => agg === orderField) && !isAggregateField(orderField)) {
|
|
145
|
+
return {
|
|
146
|
+
isValid: false,
|
|
147
|
+
error: `ORDER BY field '${orderField}' must be in GROUP BY clause or be an aggregate function`
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return { isValid: true };
|
|
152
|
+
}
|
|
153
|
+
export async function handleAggregateQuery(conn, args) {
|
|
154
|
+
const { objectName, selectFields, groupByFields, whereClause, havingClause, orderBy, limit } = args;
|
|
155
|
+
try {
|
|
156
|
+
// Validate GROUP BY contains all non-aggregate fields
|
|
157
|
+
const groupByValidation = validateGroupByFields(selectFields, groupByFields);
|
|
158
|
+
if (!groupByValidation.isValid) {
|
|
159
|
+
return {
|
|
160
|
+
content: [{
|
|
161
|
+
type: "text",
|
|
162
|
+
text: `Error: The following non-aggregate fields must be included in GROUP BY clause: ${groupByValidation.missingFields.join(', ')}\n\n` +
|
|
163
|
+
`All fields in SELECT that are not aggregate functions (COUNT, SUM, AVG, etc.) must be included in GROUP BY.`
|
|
164
|
+
}],
|
|
165
|
+
isError: true,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
// Validate WHERE clause doesn't contain aggregates
|
|
169
|
+
const whereValidation = validateWhereClause(whereClause);
|
|
170
|
+
if (!whereValidation.isValid) {
|
|
171
|
+
return {
|
|
172
|
+
content: [{
|
|
173
|
+
type: "text",
|
|
174
|
+
text: whereValidation.error
|
|
175
|
+
}],
|
|
176
|
+
isError: true,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
// Validate ORDER BY fields
|
|
180
|
+
const orderByValidation = validateOrderBy(orderBy, groupByFields, selectFields);
|
|
181
|
+
if (!orderByValidation.isValid) {
|
|
182
|
+
return {
|
|
183
|
+
content: [{
|
|
184
|
+
type: "text",
|
|
185
|
+
text: orderByValidation.error
|
|
186
|
+
}],
|
|
187
|
+
isError: true,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
// Construct SOQL query
|
|
191
|
+
let soql = `SELECT ${selectFields.join(', ')} FROM ${objectName}`;
|
|
192
|
+
if (whereClause)
|
|
193
|
+
soql += ` WHERE ${whereClause}`;
|
|
194
|
+
soql += ` GROUP BY ${groupByFields.join(', ')}`;
|
|
195
|
+
if (havingClause)
|
|
196
|
+
soql += ` HAVING ${havingClause}`;
|
|
197
|
+
if (orderBy)
|
|
198
|
+
soql += ` ORDER BY ${orderBy}`;
|
|
199
|
+
if (limit)
|
|
200
|
+
soql += ` LIMIT ${limit}`;
|
|
201
|
+
const result = await conn.query(soql);
|
|
202
|
+
// Format the output
|
|
203
|
+
const formattedRecords = result.records.map((record, index) => {
|
|
204
|
+
const recordStr = selectFields.map(field => {
|
|
205
|
+
const baseField = extractBaseField(field);
|
|
206
|
+
const fieldParts = field.trim().split(/\s+/);
|
|
207
|
+
const displayName = fieldParts.length > 1 ? fieldParts[fieldParts.length - 1] : baseField;
|
|
208
|
+
// Handle nested fields in results
|
|
209
|
+
if (baseField.includes('.')) {
|
|
210
|
+
const parts = baseField.split('.');
|
|
211
|
+
let value = record;
|
|
212
|
+
for (const part of parts) {
|
|
213
|
+
value = value?.[part];
|
|
214
|
+
}
|
|
215
|
+
return ` ${displayName}: ${value !== null && value !== undefined ? value : 'null'}`;
|
|
216
|
+
}
|
|
217
|
+
const value = record[baseField] || record[displayName];
|
|
218
|
+
return ` ${displayName}: ${value !== null && value !== undefined ? value : 'null'}`;
|
|
219
|
+
}).join('\n');
|
|
220
|
+
return `Group ${index + 1}:\n${recordStr}`;
|
|
221
|
+
}).join('\n\n');
|
|
222
|
+
return {
|
|
223
|
+
content: [{
|
|
224
|
+
type: "text",
|
|
225
|
+
text: `Aggregate query returned ${result.records.length} grouped results:\n\n${formattedRecords}`
|
|
226
|
+
}],
|
|
227
|
+
isError: false,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
232
|
+
// Provide more helpful error messages for common issues
|
|
233
|
+
let enhancedError = errorMessage;
|
|
234
|
+
if (errorMessage.includes('MALFORMED_QUERY')) {
|
|
235
|
+
if (errorMessage.includes('GROUP BY')) {
|
|
236
|
+
enhancedError = `Query error: ${errorMessage}\n\nCommon issues:\n` +
|
|
237
|
+
`1. Ensure all non-aggregate fields in SELECT are in GROUP BY\n` +
|
|
238
|
+
`2. Check that date functions match exactly between SELECT and GROUP BY\n` +
|
|
239
|
+
`3. Verify field names and relationships are correct`;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return {
|
|
243
|
+
content: [{
|
|
244
|
+
type: "text",
|
|
245
|
+
text: `Error executing aggregate query: ${enhancedError}`
|
|
246
|
+
}],
|
|
247
|
+
isError: true,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Tool } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
+
export declare const DESCRIBE_OBJECT: Tool;
|
|
3
|
+
export declare function handleDescribeObject(conn: any, objectName: string): Promise<{
|
|
4
|
+
content: {
|
|
5
|
+
type: string;
|
|
6
|
+
text: string;
|
|
7
|
+
}[];
|
|
8
|
+
isError: boolean;
|
|
9
|
+
}>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export const DESCRIBE_OBJECT = {
|
|
2
|
+
name: "salesforce_describe_object",
|
|
3
|
+
description: "Get detailed schema metadata including all fields, relationships, and field properties of any Salesforce object. Examples: 'Account' shows all Account fields including custom fields; 'Case' shows all Case fields including relationships to Account, Contact etc.",
|
|
4
|
+
inputSchema: {
|
|
5
|
+
type: "object",
|
|
6
|
+
properties: {
|
|
7
|
+
objectName: {
|
|
8
|
+
type: "string",
|
|
9
|
+
description: "API name of the object (e.g., 'Account', 'Contact', 'Custom_Object__c')"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
required: ["objectName"]
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
export async function handleDescribeObject(conn, objectName) {
|
|
16
|
+
const describe = await conn.describe(objectName);
|
|
17
|
+
// Format the output
|
|
18
|
+
const formattedDescription = `
|
|
19
|
+
Object: ${describe.name} (${describe.label})${describe.custom ? ' (Custom Object)' : ''}
|
|
20
|
+
Fields:
|
|
21
|
+
${describe.fields.map((field) => ` - ${field.name} (${field.label})
|
|
22
|
+
Type: ${field.type}${field.length ? `, Length: ${field.length}` : ''}
|
|
23
|
+
Required: ${!field.nillable}
|
|
24
|
+
${field.referenceTo && field.referenceTo.length > 0 ? `References: ${field.referenceTo.join(', ')}` : ''}
|
|
25
|
+
${field.picklistValues && field.picklistValues.length > 0 ? `Picklist Values: ${field.picklistValues.map((v) => v.value).join(', ')}` : ''}`).join('\n')}`;
|
|
26
|
+
return {
|
|
27
|
+
content: [{
|
|
28
|
+
type: "text",
|
|
29
|
+
text: formattedDescription
|
|
30
|
+
}],
|
|
31
|
+
isError: false,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Tool } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
+
export declare const DML_RECORDS: Tool;
|
|
3
|
+
export interface DMLArgs {
|
|
4
|
+
operation: 'insert' | 'update' | 'delete' | 'upsert';
|
|
5
|
+
objectName: string;
|
|
6
|
+
records: Record<string, any>[];
|
|
7
|
+
externalIdField?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function handleDMLRecords(conn: any, args: DMLArgs): Promise<{
|
|
10
|
+
content: {
|
|
11
|
+
type: string;
|
|
12
|
+
text: string;
|
|
13
|
+
}[];
|
|
14
|
+
isError: boolean;
|
|
15
|
+
}>;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
export const DML_RECORDS = {
|
|
2
|
+
name: "salesforce_dml_records",
|
|
3
|
+
description: `Perform data manipulation operations on Salesforce records:
|
|
4
|
+
- insert: Create new records
|
|
5
|
+
- update: Modify existing records (requires Id)
|
|
6
|
+
- delete: Remove records (requires Id)
|
|
7
|
+
- upsert: Insert or update based on external ID field
|
|
8
|
+
Examples: Insert new Accounts, Update Case status, Delete old records, Upsert based on custom external ID`,
|
|
9
|
+
inputSchema: {
|
|
10
|
+
type: "object",
|
|
11
|
+
properties: {
|
|
12
|
+
operation: {
|
|
13
|
+
type: "string",
|
|
14
|
+
enum: ["insert", "update", "delete", "upsert"],
|
|
15
|
+
description: "Type of DML operation to perform"
|
|
16
|
+
},
|
|
17
|
+
objectName: {
|
|
18
|
+
type: "string",
|
|
19
|
+
description: "API name of the object"
|
|
20
|
+
},
|
|
21
|
+
records: {
|
|
22
|
+
type: "array",
|
|
23
|
+
items: { type: "object" },
|
|
24
|
+
description: "Array of records to process"
|
|
25
|
+
},
|
|
26
|
+
externalIdField: {
|
|
27
|
+
type: "string",
|
|
28
|
+
description: "External ID field name for upsert operations",
|
|
29
|
+
optional: true
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
required: ["operation", "objectName", "records"]
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
export async function handleDMLRecords(conn, args) {
|
|
36
|
+
const { operation, objectName, records, externalIdField } = args;
|
|
37
|
+
let result;
|
|
38
|
+
switch (operation) {
|
|
39
|
+
case 'insert':
|
|
40
|
+
result = await conn.sobject(objectName).create(records);
|
|
41
|
+
break;
|
|
42
|
+
case 'update':
|
|
43
|
+
result = await conn.sobject(objectName).update(records);
|
|
44
|
+
break;
|
|
45
|
+
case 'delete':
|
|
46
|
+
result = await conn.sobject(objectName).destroy(records.map(r => r.Id));
|
|
47
|
+
break;
|
|
48
|
+
case 'upsert':
|
|
49
|
+
if (!externalIdField) {
|
|
50
|
+
throw new Error('externalIdField is required for upsert operations');
|
|
51
|
+
}
|
|
52
|
+
result = await conn.sobject(objectName).upsert(records, externalIdField);
|
|
53
|
+
break;
|
|
54
|
+
default:
|
|
55
|
+
throw new Error(`Unsupported operation: ${operation}`);
|
|
56
|
+
}
|
|
57
|
+
// Format DML results
|
|
58
|
+
const results = Array.isArray(result) ? result : [result];
|
|
59
|
+
const successCount = results.filter(r => r.success).length;
|
|
60
|
+
const failureCount = results.length - successCount;
|
|
61
|
+
let responseText = `${operation.toUpperCase()} operation completed.\n`;
|
|
62
|
+
responseText += `Processed ${results.length} records:\n`;
|
|
63
|
+
responseText += `- Successful: ${successCount}\n`;
|
|
64
|
+
responseText += `- Failed: ${failureCount}\n\n`;
|
|
65
|
+
if (failureCount > 0) {
|
|
66
|
+
responseText += 'Errors:\n';
|
|
67
|
+
results.forEach((r, idx) => {
|
|
68
|
+
if (!r.success && r.errors) {
|
|
69
|
+
responseText += `Record ${idx + 1}:\n`;
|
|
70
|
+
if (Array.isArray(r.errors)) {
|
|
71
|
+
r.errors.forEach((error) => {
|
|
72
|
+
responseText += ` - ${error.message}`;
|
|
73
|
+
if (error.statusCode) {
|
|
74
|
+
responseText += ` [${error.statusCode}]`;
|
|
75
|
+
}
|
|
76
|
+
if (error.fields && error.fields.length > 0) {
|
|
77
|
+
responseText += `\n Fields: ${error.fields.join(', ')}`;
|
|
78
|
+
}
|
|
79
|
+
responseText += '\n';
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
// Single error object
|
|
84
|
+
const error = r.errors;
|
|
85
|
+
responseText += ` - ${error.message}`;
|
|
86
|
+
if (error.statusCode) {
|
|
87
|
+
responseText += ` [${error.statusCode}]`;
|
|
88
|
+
}
|
|
89
|
+
if (error.fields) {
|
|
90
|
+
const fields = Array.isArray(error.fields) ? error.fields.join(', ') : error.fields;
|
|
91
|
+
responseText += `\n Fields: ${fields}`;
|
|
92
|
+
}
|
|
93
|
+
responseText += '\n';
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
content: [{
|
|
100
|
+
type: "text",
|
|
101
|
+
text: responseText
|
|
102
|
+
}],
|
|
103
|
+
isError: false,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Tool } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
+
export declare const EXECUTE_ANONYMOUS: Tool;
|
|
3
|
+
export interface ExecuteAnonymousArgs {
|
|
4
|
+
apexCode: string;
|
|
5
|
+
logLevel?: 'NONE' | 'ERROR' | 'WARN' | 'INFO' | 'DEBUG' | 'FINE' | 'FINER' | 'FINEST';
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Handles executing anonymous Apex code in Salesforce
|
|
9
|
+
* @param conn Active Salesforce connection
|
|
10
|
+
* @param args Arguments for executing anonymous Apex
|
|
11
|
+
* @returns Tool response with execution results and debug logs
|
|
12
|
+
*/
|
|
13
|
+
export declare function handleExecuteAnonymous(conn: any, args: ExecuteAnonymousArgs): Promise<{
|
|
14
|
+
content: {
|
|
15
|
+
type: string;
|
|
16
|
+
text: string;
|
|
17
|
+
}[];
|
|
18
|
+
isError?: undefined;
|
|
19
|
+
} | {
|
|
20
|
+
content: {
|
|
21
|
+
type: string;
|
|
22
|
+
text: string;
|
|
23
|
+
}[];
|
|
24
|
+
isError: boolean;
|
|
25
|
+
}>;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
export const EXECUTE_ANONYMOUS = {
|
|
2
|
+
name: "salesforce_execute_anonymous",
|
|
3
|
+
description: `Execute anonymous Apex code in Salesforce.
|
|
4
|
+
|
|
5
|
+
Examples:
|
|
6
|
+
1. Execute simple Apex code:
|
|
7
|
+
{
|
|
8
|
+
"apexCode": "System.debug('Hello World');"
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
2. Execute Apex code with variables:
|
|
12
|
+
{
|
|
13
|
+
"apexCode": "List<Account> accounts = [SELECT Id, Name FROM Account LIMIT 5]; for(Account a : accounts) { System.debug(a.Name); }"
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
3. Execute Apex with debug logs:
|
|
17
|
+
{
|
|
18
|
+
"apexCode": "System.debug(LoggingLevel.INFO, 'Processing accounts...'); List<Account> accounts = [SELECT Id FROM Account LIMIT 10]; System.debug(LoggingLevel.INFO, 'Found ' + accounts.size() + ' accounts');",
|
|
19
|
+
"logLevel": "DEBUG"
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
Notes:
|
|
23
|
+
- The apexCode parameter is required and must contain valid Apex code
|
|
24
|
+
- The code is executed in an anonymous context and does not persist
|
|
25
|
+
- The logLevel parameter is optional (defaults to 'DEBUG')
|
|
26
|
+
- Execution results include compilation success/failure, execution success/failure, and debug logs
|
|
27
|
+
- For security reasons, some operations may be restricted based on user permissions
|
|
28
|
+
- This tool can be used for data operations or updates when there are no other specific tools available
|
|
29
|
+
- When users request data queries or updates that aren't directly supported by other tools, this tool can be used if the operation is achievable using Apex code
|
|
30
|
+
`,
|
|
31
|
+
inputSchema: {
|
|
32
|
+
type: "object",
|
|
33
|
+
properties: {
|
|
34
|
+
apexCode: {
|
|
35
|
+
type: "string",
|
|
36
|
+
description: "Apex code to execute anonymously"
|
|
37
|
+
},
|
|
38
|
+
logLevel: {
|
|
39
|
+
type: "string",
|
|
40
|
+
enum: ["NONE", "ERROR", "WARN", "INFO", "DEBUG", "FINE", "FINER", "FINEST"],
|
|
41
|
+
description: "Log level for debug logs (optional, defaults to DEBUG)"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
required: ["apexCode"]
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Handles executing anonymous Apex code in Salesforce
|
|
49
|
+
* @param conn Active Salesforce connection
|
|
50
|
+
* @param args Arguments for executing anonymous Apex
|
|
51
|
+
* @returns Tool response with execution results and debug logs
|
|
52
|
+
*/
|
|
53
|
+
export async function handleExecuteAnonymous(conn, args) {
|
|
54
|
+
try {
|
|
55
|
+
// Validate inputs
|
|
56
|
+
if (!args.apexCode || args.apexCode.trim() === '') {
|
|
57
|
+
throw new Error('apexCode is required and cannot be empty');
|
|
58
|
+
}
|
|
59
|
+
console.error(`Executing anonymous Apex code`);
|
|
60
|
+
// Set default log level if not provided
|
|
61
|
+
const logLevel = args.logLevel || 'DEBUG';
|
|
62
|
+
// Execute the anonymous Apex code
|
|
63
|
+
const result = await conn.tooling.executeAnonymous(args.apexCode);
|
|
64
|
+
// Format the response
|
|
65
|
+
let responseText = '';
|
|
66
|
+
// Add compilation and execution status
|
|
67
|
+
if (result.compiled) {
|
|
68
|
+
responseText += `**Compilation:** Success\n`;
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
responseText += `**Compilation:** Failed\n`;
|
|
72
|
+
responseText += `**Line:** ${result.line}\n`;
|
|
73
|
+
responseText += `**Column:** ${result.column}\n`;
|
|
74
|
+
responseText += `**Error:** ${result.compileProblem}\n\n`;
|
|
75
|
+
}
|
|
76
|
+
if (result.compiled && result.success) {
|
|
77
|
+
responseText += `**Execution:** Success\n`;
|
|
78
|
+
}
|
|
79
|
+
else if (result.compiled) {
|
|
80
|
+
responseText += `**Execution:** Failed\n`;
|
|
81
|
+
responseText += `**Error:** ${result.exceptionMessage}\n`;
|
|
82
|
+
if (result.exceptionStackTrace) {
|
|
83
|
+
responseText += `**Stack Trace:**\n\`\`\`\n${result.exceptionStackTrace}\n\`\`\`\n\n`;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Get debug logs if available
|
|
87
|
+
if (result.compiled) {
|
|
88
|
+
try {
|
|
89
|
+
// Query for the most recent debug log
|
|
90
|
+
const logs = await conn.query(`
|
|
91
|
+
SELECT Id, LogUserId, Operation, Application, Status, LogLength, LastModifiedDate, Request
|
|
92
|
+
FROM ApexLog
|
|
93
|
+
ORDER BY LastModifiedDate DESC
|
|
94
|
+
LIMIT 1
|
|
95
|
+
`);
|
|
96
|
+
if (logs.records.length > 0) {
|
|
97
|
+
const logId = logs.records[0].Id;
|
|
98
|
+
// Retrieve the log body
|
|
99
|
+
const logBody = await conn.tooling.request({
|
|
100
|
+
method: 'GET',
|
|
101
|
+
url: `${conn.instanceUrl}/services/data/v58.0/tooling/sobjects/ApexLog/${logId}/Body`
|
|
102
|
+
});
|
|
103
|
+
responseText += `\n**Debug Log:**\n\`\`\`\n${logBody}\n\`\`\``;
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
responseText += `\n**Debug Log:** No logs available. Ensure debug logs are enabled for your user.`;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
catch (logError) {
|
|
110
|
+
responseText += `\n**Debug Log:** Unable to retrieve debug logs: ${logError instanceof Error ? logError.message : String(logError)}`;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
content: [{
|
|
115
|
+
type: "text",
|
|
116
|
+
text: responseText
|
|
117
|
+
}]
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
console.error('Error executing anonymous Apex:', error);
|
|
122
|
+
return {
|
|
123
|
+
content: [{
|
|
124
|
+
type: "text",
|
|
125
|
+
text: `Error executing anonymous Apex: ${error instanceof Error ? error.message : String(error)}`
|
|
126
|
+
}],
|
|
127
|
+
isError: true,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Tool } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
+
export declare const MANAGE_DEBUG_LOGS: Tool;
|
|
3
|
+
export interface ManageDebugLogsArgs {
|
|
4
|
+
operation: 'enable' | 'disable' | 'retrieve';
|
|
5
|
+
username: string;
|
|
6
|
+
logLevel?: 'NONE' | 'ERROR' | 'WARN' | 'INFO' | 'DEBUG' | 'FINE' | 'FINER' | 'FINEST';
|
|
7
|
+
expirationTime?: number;
|
|
8
|
+
limit?: number;
|
|
9
|
+
logId?: string;
|
|
10
|
+
includeBody?: boolean;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Handles managing debug logs for Salesforce users
|
|
14
|
+
* @param conn Active Salesforce connection
|
|
15
|
+
* @param args Arguments for managing debug logs
|
|
16
|
+
* @returns Tool response with operation results
|
|
17
|
+
*/
|
|
18
|
+
export declare function handleManageDebugLogs(conn: any, args: ManageDebugLogsArgs): Promise<{
|
|
19
|
+
content: {
|
|
20
|
+
type: string;
|
|
21
|
+
text: string;
|
|
22
|
+
}[];
|
|
23
|
+
isError: boolean;
|
|
24
|
+
} | {
|
|
25
|
+
content: {
|
|
26
|
+
type: string;
|
|
27
|
+
text: string;
|
|
28
|
+
}[];
|
|
29
|
+
isError?: undefined;
|
|
30
|
+
}>;
|