@rohithvemulapally/mcp-server-salesforce 0.0.6 → 0.0.7
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/dist/index.js +1 -6
- package/dist/tools/query.js +117 -51
- package/package.json +1 -1
- package/dist/tools/metadata.d.ts +0 -9
- package/dist/tools/metadata.js +0 -66
package/dist/index.js
CHANGED
|
@@ -19,7 +19,6 @@ import { READ_APEX_TRIGGER, handleReadApexTrigger } from "./tools/readApexTrigge
|
|
|
19
19
|
import { WRITE_APEX_TRIGGER, handleWriteApexTrigger } from "./tools/writeApexTrigger.js";
|
|
20
20
|
import { EXECUTE_ANONYMOUS, handleExecuteAnonymous } from "./tools/executeAnonymous.js";
|
|
21
21
|
import { MANAGE_DEBUG_LOGS, handleManageDebugLogs } from "./tools/manageDebugLogs.js";
|
|
22
|
-
import { METADATA_QUERY, handleMetadataQuery } from "./tools/metadata";
|
|
23
22
|
// Load environment variables (using dotenv 16.x which has no stdout tips)
|
|
24
23
|
// MCP servers require stdout to contain ONLY JSON-RPC messages
|
|
25
24
|
dotenv.config();
|
|
@@ -48,8 +47,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
48
47
|
READ_APEX_TRIGGER,
|
|
49
48
|
WRITE_APEX_TRIGGER,
|
|
50
49
|
EXECUTE_ANONYMOUS,
|
|
51
|
-
MANAGE_DEBUG_LOGS
|
|
52
|
-
METADATA_QUERY
|
|
50
|
+
MANAGE_DEBUG_LOGS
|
|
53
51
|
],
|
|
54
52
|
}));
|
|
55
53
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
@@ -252,9 +250,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
252
250
|
};
|
|
253
251
|
return await handleWriteApexTrigger(conn, validatedArgs);
|
|
254
252
|
}
|
|
255
|
-
case "salesforce_metadata_query": {
|
|
256
|
-
return await handleMetadataQuery(conn, args);
|
|
257
|
-
}
|
|
258
253
|
case "salesforce_execute_anonymous": {
|
|
259
254
|
const executeArgs = args;
|
|
260
255
|
if (!executeArgs.apexCode) {
|
package/dist/tools/query.js
CHANGED
|
@@ -1,114 +1,180 @@
|
|
|
1
|
-
// Bulk API query for large datasets
|
|
2
|
-
async function bulkQuery(conn, soql) {
|
|
3
|
-
return new Promise((resolve, reject) => {
|
|
4
|
-
const records = [];
|
|
5
|
-
conn.bulk.query(soql)
|
|
6
|
-
.on("record", (record) => records.push(record))
|
|
7
|
-
.on("end", () => resolve(records))
|
|
8
|
-
.on("error", (err) => reject(err));
|
|
9
|
-
});
|
|
10
|
-
}
|
|
11
1
|
export const QUERY_RECORDS = {
|
|
12
2
|
name: "salesforce_query_records",
|
|
13
|
-
description:
|
|
3
|
+
description: `Query records from any Salesforce object using SOQL, including relationship queries.
|
|
4
|
+
|
|
5
|
+
NOTE: For queries with GROUP BY, aggregate functions (COUNT, SUM, AVG, etc.), or HAVING clauses, use salesforce_aggregate_query instead.
|
|
6
|
+
|
|
7
|
+
Examples:
|
|
8
|
+
1. Parent-to-child query (e.g., Account with Contacts):
|
|
9
|
+
- objectName: "Account"
|
|
10
|
+
- fields: ["Name", "(SELECT Id, FirstName, LastName FROM Contacts)"]
|
|
11
|
+
|
|
12
|
+
2. Child-to-parent query (e.g., Contact with Account details):
|
|
13
|
+
- objectName: "Contact"
|
|
14
|
+
- fields: ["FirstName", "LastName", "Account.Name", "Account.Industry"]
|
|
15
|
+
|
|
16
|
+
3. Multiple level query (e.g., Contact -> Account -> Owner):
|
|
17
|
+
- objectName: "Contact"
|
|
18
|
+
- fields: ["Name", "Account.Name", "Account.Owner.Name"]
|
|
19
|
+
|
|
20
|
+
4. Related object filtering:
|
|
21
|
+
- objectName: "Contact"
|
|
22
|
+
- fields: ["Name", "Account.Name"]
|
|
23
|
+
- whereClause: "Account.Industry = 'Technology'"
|
|
24
|
+
|
|
25
|
+
Note: When using relationship fields:
|
|
26
|
+
- Use dot notation for parent relationships (e.g., "Account.Name")
|
|
27
|
+
- Use subqueries in parentheses for child relationships (e.g., "(SELECT Id FROM Contacts)")
|
|
28
|
+
- Custom relationship fields end in "__r" (e.g., "CustomObject__r.Name")`,
|
|
14
29
|
inputSchema: {
|
|
15
30
|
type: "object",
|
|
16
31
|
properties: {
|
|
17
|
-
objectName: {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
32
|
+
objectName: {
|
|
33
|
+
type: "string",
|
|
34
|
+
description: "API name of the object to query"
|
|
35
|
+
},
|
|
36
|
+
fields: {
|
|
37
|
+
type: "array",
|
|
38
|
+
items: { type: "string" },
|
|
39
|
+
description: "List of fields to retrieve, including relationship fields"
|
|
40
|
+
},
|
|
41
|
+
whereClause: {
|
|
42
|
+
type: "string",
|
|
43
|
+
description: "WHERE clause, can include conditions on related objects",
|
|
44
|
+
optional: true
|
|
45
|
+
},
|
|
46
|
+
orderBy: {
|
|
47
|
+
type: "string",
|
|
48
|
+
description: "ORDER BY clause, can include fields from related objects",
|
|
49
|
+
optional: true
|
|
50
|
+
},
|
|
51
|
+
limit: {
|
|
52
|
+
type: "number",
|
|
53
|
+
description: "Maximum number of records to return",
|
|
54
|
+
optional: true
|
|
55
|
+
}
|
|
22
56
|
},
|
|
23
57
|
required: ["objectName", "fields"]
|
|
24
58
|
}
|
|
25
59
|
};
|
|
60
|
+
// Helper function to validate relationship field syntax
|
|
26
61
|
function validateRelationshipFields(fields) {
|
|
27
62
|
for (const field of fields) {
|
|
28
|
-
|
|
29
|
-
|
|
63
|
+
// Check for parent relationship syntax (dot notation)
|
|
64
|
+
if (field.includes('.')) {
|
|
65
|
+
const parts = field.split('.');
|
|
66
|
+
// Check for empty parts
|
|
30
67
|
if (parts.some(part => !part)) {
|
|
31
|
-
return {
|
|
68
|
+
return {
|
|
69
|
+
isValid: false,
|
|
70
|
+
error: `Invalid relationship field format: "${field}". Relationship fields should use proper dot notation (e.g., "Account.Name")`
|
|
71
|
+
};
|
|
32
72
|
}
|
|
73
|
+
// Check for too many levels (Salesforce typically limits to 5)
|
|
33
74
|
if (parts.length > 5) {
|
|
34
|
-
return {
|
|
75
|
+
return {
|
|
76
|
+
isValid: false,
|
|
77
|
+
error: `Relationship field "${field}" exceeds maximum depth of 5 levels`
|
|
78
|
+
};
|
|
35
79
|
}
|
|
36
80
|
}
|
|
37
|
-
|
|
38
|
-
|
|
81
|
+
// Check for child relationship syntax (subqueries)
|
|
82
|
+
if (field.includes('SELECT') && !field.match(/^\(SELECT.*FROM.*\)$/)) {
|
|
83
|
+
return {
|
|
84
|
+
isValid: false,
|
|
85
|
+
error: `Invalid subquery format: "${field}". Child relationship queries should be wrapped in parentheses`
|
|
86
|
+
};
|
|
39
87
|
}
|
|
40
88
|
}
|
|
41
89
|
return { isValid: true };
|
|
42
90
|
}
|
|
43
|
-
function
|
|
44
|
-
|
|
45
|
-
|
|
91
|
+
// Helper function to format relationship query results
|
|
92
|
+
function formatRelationshipResults(record, field, prefix = '') {
|
|
93
|
+
if (field.includes('.')) {
|
|
94
|
+
const [relationship, ...rest] = field.split('.');
|
|
46
95
|
const relatedRecord = record[relationship];
|
|
47
|
-
if (relatedRecord === null)
|
|
96
|
+
if (relatedRecord === null) {
|
|
48
97
|
return `${prefix}${field}: null`;
|
|
49
|
-
|
|
98
|
+
}
|
|
99
|
+
return formatRelationshipResults(relatedRecord, rest.join('.'), `${prefix}${relationship}.`);
|
|
50
100
|
}
|
|
51
101
|
const value = record[field];
|
|
52
102
|
if (Array.isArray(value)) {
|
|
103
|
+
// Handle child relationship arrays
|
|
53
104
|
return `${prefix}${field}: [${value.length} records]`;
|
|
54
105
|
}
|
|
55
|
-
return `${prefix}${field}: ${value
|
|
106
|
+
return `${prefix}${field}: ${value !== null && value !== undefined ? value : 'null'}`;
|
|
56
107
|
}
|
|
57
108
|
export async function handleQueryRecords(conn, args) {
|
|
58
109
|
const { objectName, fields, whereClause, orderBy, limit } = args;
|
|
59
110
|
try {
|
|
111
|
+
// Validate relationship field syntax
|
|
60
112
|
const validation = validateRelationshipFields(fields);
|
|
61
113
|
if (!validation.isValid) {
|
|
62
114
|
return {
|
|
63
|
-
content: [{
|
|
64
|
-
|
|
115
|
+
content: [{
|
|
116
|
+
type: "text",
|
|
117
|
+
text: validation.error
|
|
118
|
+
}],
|
|
119
|
+
isError: true,
|
|
65
120
|
};
|
|
66
121
|
}
|
|
67
|
-
|
|
122
|
+
// Construct SOQL query
|
|
123
|
+
let soql = `SELECT ${fields.join(', ')} FROM ${objectName}`;
|
|
68
124
|
if (whereClause)
|
|
69
125
|
soql += ` WHERE ${whereClause}`;
|
|
70
126
|
if (orderBy)
|
|
71
127
|
soql += ` ORDER BY ${orderBy}`;
|
|
72
128
|
if (limit)
|
|
73
129
|
soql += ` LIMIT ${limit}`;
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
lower.includes("count") ||
|
|
78
|
-
lower.includes("group by")) {
|
|
79
|
-
const result = await conn.query(soql);
|
|
80
|
-
records = result.records;
|
|
81
|
-
}
|
|
82
|
-
else {
|
|
83
|
-
records = await bulkQuery(conn, soql);
|
|
84
|
-
}
|
|
85
|
-
const formattedRecords = records.map((record, index) => {
|
|
130
|
+
const result = await conn.query(soql);
|
|
131
|
+
// Format the output
|
|
132
|
+
const formattedRecords = result.records.map((record, index) => {
|
|
86
133
|
const recordStr = fields.map(field => {
|
|
87
|
-
|
|
134
|
+
// Handle special case for subqueries (child relationships)
|
|
135
|
+
if (field.startsWith('(SELECT')) {
|
|
88
136
|
const relationshipName = field.match(/FROM\s+(\w+)/)?.[1];
|
|
137
|
+
if (!relationshipName)
|
|
138
|
+
return ` ${field}: Invalid subquery format`;
|
|
89
139
|
const childRecords = record[relationshipName];
|
|
90
140
|
return ` ${relationshipName}: [${childRecords?.length || 0} records]`;
|
|
91
141
|
}
|
|
92
|
-
return
|
|
93
|
-
}).join(
|
|
142
|
+
return ' ' + formatRelationshipResults(record, field);
|
|
143
|
+
}).join('\n');
|
|
94
144
|
return `Record ${index + 1}:\n${recordStr}`;
|
|
95
|
-
}).join(
|
|
145
|
+
}).join('\n\n');
|
|
96
146
|
return {
|
|
97
147
|
content: [{
|
|
98
148
|
type: "text",
|
|
99
|
-
text: `Query returned ${records.length} records:\n\n${formattedRecords}`
|
|
149
|
+
text: `Query returned ${result.records.length} records:\n\n${formattedRecords}`
|
|
100
150
|
}],
|
|
101
|
-
isError: false
|
|
151
|
+
isError: false,
|
|
102
152
|
};
|
|
103
153
|
}
|
|
104
154
|
catch (error) {
|
|
155
|
+
// Enhanced error handling for relationship queries
|
|
105
156
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
157
|
+
let enhancedError = errorMessage;
|
|
158
|
+
if (errorMessage.includes('INVALID_FIELD')) {
|
|
159
|
+
// Try to identify which relationship field caused the error
|
|
160
|
+
const fieldMatch = errorMessage.match(/(?:No such column |Invalid field: )['"]?([^'")\s]+)/);
|
|
161
|
+
if (fieldMatch) {
|
|
162
|
+
const invalidField = fieldMatch[1];
|
|
163
|
+
if (invalidField.includes('.')) {
|
|
164
|
+
enhancedError = `Invalid relationship field "${invalidField}". Please check:\n` +
|
|
165
|
+
`1. The relationship name is correct\n` +
|
|
166
|
+
`2. The field exists on the related object\n` +
|
|
167
|
+
`3. You have access to the field\n` +
|
|
168
|
+
`4. For custom relationships, ensure you're using '__r' suffix`;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
106
172
|
return {
|
|
107
173
|
content: [{
|
|
108
174
|
type: "text",
|
|
109
|
-
text: `Error executing query: ${
|
|
175
|
+
text: `Error executing query: ${enhancedError}`
|
|
110
176
|
}],
|
|
111
|
-
isError: true
|
|
177
|
+
isError: true,
|
|
112
178
|
};
|
|
113
179
|
}
|
|
114
180
|
}
|
package/package.json
CHANGED
package/dist/tools/metadata.d.ts
DELETED
package/dist/tools/metadata.js
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
export const METADATA_QUERY = {
|
|
2
|
-
name: "salesforce_metadata_query",
|
|
3
|
-
description: "Fetch Salesforce metadata such as validation rules and Lightning record pages",
|
|
4
|
-
inputSchema: {
|
|
5
|
-
type: "object",
|
|
6
|
-
properties: {
|
|
7
|
-
metadataType: {
|
|
8
|
-
type: "string",
|
|
9
|
-
description: "Type of metadata: ValidationRule or FlexiPage"
|
|
10
|
-
},
|
|
11
|
-
objectName: {
|
|
12
|
-
type: "string",
|
|
13
|
-
description: "Object name (required for ValidationRule)"
|
|
14
|
-
},
|
|
15
|
-
name: {
|
|
16
|
-
type: "string",
|
|
17
|
-
description: "Metadata name (required for FlexiPage)"
|
|
18
|
-
}
|
|
19
|
-
},
|
|
20
|
-
required: ["metadataType"]
|
|
21
|
-
}
|
|
22
|
-
};
|
|
23
|
-
export async function handleMetadataQuery(conn, args) {
|
|
24
|
-
const { metadataType, objectName, name } = args;
|
|
25
|
-
try {
|
|
26
|
-
if (metadataType === "ValidationRule") {
|
|
27
|
-
if (!objectName) {
|
|
28
|
-
throw new Error("objectName is required for ValidationRule");
|
|
29
|
-
}
|
|
30
|
-
const result = await conn.tooling.query(`SELECT Id, ValidationName, ErrorMessage, Active
|
|
31
|
-
FROM ValidationRule
|
|
32
|
-
WHERE EntityDefinition.QualifiedApiName = '${objectName}'`);
|
|
33
|
-
return {
|
|
34
|
-
content: [{
|
|
35
|
-
type: "text",
|
|
36
|
-
text: JSON.stringify(result.records, null, 2)
|
|
37
|
-
}],
|
|
38
|
-
isError: false
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
if (metadataType === "FlexiPage") {
|
|
42
|
-
if (!name) {
|
|
43
|
-
throw new Error("name is required for FlexiPage");
|
|
44
|
-
}
|
|
45
|
-
const result = await conn.metadata.read("FlexiPage", name);
|
|
46
|
-
return {
|
|
47
|
-
content: [{
|
|
48
|
-
type: "text",
|
|
49
|
-
text: JSON.stringify(result, null, 2)
|
|
50
|
-
}],
|
|
51
|
-
isError: false
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
throw new Error("Unsupported metadata type");
|
|
55
|
-
}
|
|
56
|
-
catch (error) {
|
|
57
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
58
|
-
return {
|
|
59
|
-
content: [{
|
|
60
|
-
type: "text",
|
|
61
|
-
text: `Error fetching metadata: ${errorMessage}`
|
|
62
|
-
}],
|
|
63
|
-
isError: true
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
}
|