@rohithvemulapally/mcp-server-salesforce 0.0.7 → 0.0.8
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 +6 -1
- package/dist/tools/metadata.d.ts +9 -0
- package/dist/tools/metadata.js +41 -0
- package/dist/tools/query.d.ts +12 -0
- package/dist/tools/query.js +77 -104
- package/package.json +18 -19
package/dist/index.js
CHANGED
|
@@ -19,6 +19,7 @@ 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";
|
|
22
23
|
// Load environment variables (using dotenv 16.x which has no stdout tips)
|
|
23
24
|
// MCP servers require stdout to contain ONLY JSON-RPC messages
|
|
24
25
|
dotenv.config();
|
|
@@ -47,7 +48,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
47
48
|
READ_APEX_TRIGGER,
|
|
48
49
|
WRITE_APEX_TRIGGER,
|
|
49
50
|
EXECUTE_ANONYMOUS,
|
|
50
|
-
MANAGE_DEBUG_LOGS
|
|
51
|
+
MANAGE_DEBUG_LOGS,
|
|
52
|
+
METADATA_QUERY
|
|
51
53
|
],
|
|
52
54
|
}));
|
|
53
55
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
@@ -84,6 +86,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
84
86
|
};
|
|
85
87
|
return await handleQueryRecords(conn, validatedArgs);
|
|
86
88
|
}
|
|
89
|
+
case "salesforce_metadata_query": {
|
|
90
|
+
return await handleMetadataQuery(conn, args);
|
|
91
|
+
}
|
|
87
92
|
case "salesforce_aggregate_query": {
|
|
88
93
|
const aggregateArgs = args;
|
|
89
94
|
if (!aggregateArgs.objectName || !Array.isArray(aggregateArgs.selectFields) || !Array.isArray(aggregateArgs.groupByFields)) {
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export const METADATA_QUERY = {
|
|
2
|
+
name: "salesforce_metadata_query",
|
|
3
|
+
description: "Fetch validation rules or lightning pages",
|
|
4
|
+
inputSchema: {
|
|
5
|
+
type: "object",
|
|
6
|
+
properties: {
|
|
7
|
+
metadataType: { type: "string" },
|
|
8
|
+
objectName: { type: "string" },
|
|
9
|
+
name: { type: "string" }
|
|
10
|
+
},
|
|
11
|
+
required: ["metadataType"]
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
export async function handleMetadataQuery(conn, args) {
|
|
15
|
+
const { metadataType, objectName, name } = args;
|
|
16
|
+
try {
|
|
17
|
+
if (metadataType === "ValidationRule") {
|
|
18
|
+
const result = await conn.tooling.query(`SELECT Id, ValidationName, ErrorMessage, Active
|
|
19
|
+
FROM ValidationRule
|
|
20
|
+
WHERE EntityDefinition.QualifiedApiName = '${objectName}'`);
|
|
21
|
+
return {
|
|
22
|
+
content: [{ type: "text", text: JSON.stringify(result.records, null, 2) }],
|
|
23
|
+
isError: false
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
if (metadataType === "FlexiPage") {
|
|
27
|
+
const result = await conn.metadata.read("FlexiPage", name);
|
|
28
|
+
return {
|
|
29
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
30
|
+
isError: false
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
throw new Error("Unsupported metadata type");
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
return {
|
|
37
|
+
content: [{ type: "text", text: error.message }],
|
|
38
|
+
isError: true
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
}
|
package/dist/tools/query.d.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import { Tool } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Tool definition for querying Salesforce records using SOQL.
|
|
4
|
+
* Supports:
|
|
5
|
+
* - Standard queries
|
|
6
|
+
* - Relationship queries
|
|
7
|
+
* - Automatic switch to Bulk API for large datasets
|
|
8
|
+
*/
|
|
2
9
|
export declare const QUERY_RECORDS: Tool;
|
|
3
10
|
export interface QueryArgs {
|
|
4
11
|
objectName: string;
|
|
@@ -7,6 +14,11 @@ export interface QueryArgs {
|
|
|
7
14
|
orderBy?: string;
|
|
8
15
|
limit?: number;
|
|
9
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* Executes query using either:
|
|
19
|
+
* - Standard API (for aggregates, limits)
|
|
20
|
+
* - Bulk API (for large datasets)
|
|
21
|
+
*/
|
|
10
22
|
export declare function handleQueryRecords(conn: any, args: QueryArgs): Promise<{
|
|
11
23
|
content: {
|
|
12
24
|
type: string;
|
package/dist/tools/query.js
CHANGED
|
@@ -1,125 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool definition for querying Salesforce records using SOQL.
|
|
3
|
+
* Supports:
|
|
4
|
+
* - Standard queries
|
|
5
|
+
* - Relationship queries
|
|
6
|
+
* - Automatic switch to Bulk API for large datasets
|
|
7
|
+
*/
|
|
1
8
|
export const QUERY_RECORDS = {
|
|
2
9
|
name: "salesforce_query_records",
|
|
3
10
|
description: `Query records from any Salesforce object using SOQL, including relationship queries.
|
|
4
11
|
|
|
5
|
-
NOTE:
|
|
12
|
+
NOTE:
|
|
13
|
+
- For aggregate queries (COUNT, SUM, GROUP BY), standard query is used.
|
|
14
|
+
- For large datasets, Bulk API is used automatically.
|
|
6
15
|
|
|
7
16
|
Examples:
|
|
8
|
-
1. Parent-to-child query
|
|
17
|
+
1. Parent-to-child query:
|
|
9
18
|
- objectName: "Account"
|
|
10
|
-
- fields: ["Name", "(SELECT Id, FirstName
|
|
19
|
+
- fields: ["Name", "(SELECT Id, FirstName FROM Contacts)"]
|
|
11
20
|
|
|
12
|
-
2. Child-to-parent query
|
|
21
|
+
2. Child-to-parent query:
|
|
13
22
|
- objectName: "Contact"
|
|
14
|
-
- fields: ["FirstName", "
|
|
23
|
+
- fields: ["FirstName", "Account.Name"]
|
|
15
24
|
|
|
16
|
-
3.
|
|
25
|
+
3. Multi-level relationship:
|
|
17
26
|
- objectName: "Contact"
|
|
18
|
-
- fields: ["
|
|
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")`,
|
|
27
|
+
- fields: ["Account.Owner.Name"]
|
|
28
|
+
`,
|
|
29
29
|
inputSchema: {
|
|
30
30
|
type: "object",
|
|
31
31
|
properties: {
|
|
32
|
-
objectName: {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
},
|
|
36
|
-
|
|
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
|
-
}
|
|
32
|
+
objectName: { type: "string" },
|
|
33
|
+
fields: { type: "array", items: { type: "string" } },
|
|
34
|
+
whereClause: { type: "string", optional: true },
|
|
35
|
+
orderBy: { type: "string", optional: true },
|
|
36
|
+
limit: { type: "number", optional: true }
|
|
56
37
|
},
|
|
57
38
|
required: ["objectName", "fields"]
|
|
58
39
|
}
|
|
59
40
|
};
|
|
60
|
-
|
|
41
|
+
/**
|
|
42
|
+
* Validates relationship fields syntax.
|
|
43
|
+
*/
|
|
61
44
|
function validateRelationshipFields(fields) {
|
|
62
45
|
for (const field of fields) {
|
|
63
|
-
// Check for parent relationship syntax (dot notation)
|
|
64
46
|
if (field.includes('.')) {
|
|
65
47
|
const parts = field.split('.');
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
return {
|
|
69
|
-
isValid: false,
|
|
70
|
-
error: `Invalid relationship field format: "${field}". Relationship fields should use proper dot notation (e.g., "Account.Name")`
|
|
71
|
-
};
|
|
48
|
+
if (parts.some(p => !p)) {
|
|
49
|
+
return { isValid: false, error: `Invalid field: ${field}` };
|
|
72
50
|
}
|
|
73
|
-
// Check for too many levels (Salesforce typically limits to 5)
|
|
74
51
|
if (parts.length > 5) {
|
|
75
|
-
return {
|
|
76
|
-
isValid: false,
|
|
77
|
-
error: `Relationship field "${field}" exceeds maximum depth of 5 levels`
|
|
78
|
-
};
|
|
52
|
+
return { isValid: false, error: `Too deep relationship: ${field}` };
|
|
79
53
|
}
|
|
80
54
|
}
|
|
81
|
-
// Check for child relationship syntax (subqueries)
|
|
82
55
|
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
|
-
};
|
|
56
|
+
return { isValid: false, error: `Invalid subquery: ${field}` };
|
|
87
57
|
}
|
|
88
58
|
}
|
|
89
59
|
return { isValid: true };
|
|
90
60
|
}
|
|
91
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Formats relationship fields for readable output.
|
|
63
|
+
*/
|
|
92
64
|
function formatRelationshipResults(record, field, prefix = '') {
|
|
93
65
|
if (field.includes('.')) {
|
|
94
|
-
const [
|
|
95
|
-
const
|
|
96
|
-
if (
|
|
66
|
+
const [rel, ...rest] = field.split('.');
|
|
67
|
+
const related = record[rel];
|
|
68
|
+
if (!related)
|
|
97
69
|
return `${prefix}${field}: null`;
|
|
98
|
-
}
|
|
99
|
-
return formatRelationshipResults(relatedRecord, rest.join('.'), `${prefix}${relationship}.`);
|
|
70
|
+
return formatRelationshipResults(related, rest.join('.'), `${prefix}${rel}.`);
|
|
100
71
|
}
|
|
101
72
|
const value = record[field];
|
|
102
73
|
if (Array.isArray(value)) {
|
|
103
|
-
// Handle child relationship arrays
|
|
104
74
|
return `${prefix}${field}: [${value.length} records]`;
|
|
105
75
|
}
|
|
106
|
-
return `${prefix}${field}: ${value
|
|
76
|
+
return `${prefix}${field}: ${value ?? 'null'}`;
|
|
107
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* Executes query using either:
|
|
80
|
+
* - Standard API (for aggregates, limits)
|
|
81
|
+
* - Bulk API (for large datasets)
|
|
82
|
+
*/
|
|
108
83
|
export async function handleQueryRecords(conn, args) {
|
|
109
84
|
const { objectName, fields, whereClause, orderBy, limit } = args;
|
|
110
85
|
try {
|
|
111
|
-
//
|
|
86
|
+
// Step 1: Validate fields
|
|
112
87
|
const validation = validateRelationshipFields(fields);
|
|
113
88
|
if (!validation.isValid) {
|
|
114
89
|
return {
|
|
115
|
-
content: [{
|
|
116
|
-
|
|
117
|
-
text: validation.error
|
|
118
|
-
}],
|
|
119
|
-
isError: true,
|
|
90
|
+
content: [{ type: "text", text: validation.error }],
|
|
91
|
+
isError: true
|
|
120
92
|
};
|
|
121
93
|
}
|
|
122
|
-
//
|
|
94
|
+
// Step 2: Build SOQL query
|
|
123
95
|
let soql = `SELECT ${fields.join(', ')} FROM ${objectName}`;
|
|
124
96
|
if (whereClause)
|
|
125
97
|
soql += ` WHERE ${whereClause}`;
|
|
@@ -127,54 +99,55 @@ export async function handleQueryRecords(conn, args) {
|
|
|
127
99
|
soql += ` ORDER BY ${orderBy}`;
|
|
128
100
|
if (limit)
|
|
129
101
|
soql += ` LIMIT ${limit}`;
|
|
130
|
-
|
|
131
|
-
//
|
|
132
|
-
const
|
|
102
|
+
let records = [];
|
|
103
|
+
// Step 3: Decide API (Standard vs Bulk)
|
|
104
|
+
const lower = soql.toLowerCase();
|
|
105
|
+
if (lower.includes("limit") ||
|
|
106
|
+
lower.includes("count") ||
|
|
107
|
+
lower.includes("group by")) {
|
|
108
|
+
// Use standard REST query
|
|
109
|
+
const result = await conn.query(soql);
|
|
110
|
+
records = result.records;
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
// Use Bulk API for large data
|
|
114
|
+
records = await new Promise((resolve, reject) => {
|
|
115
|
+
const recs = [];
|
|
116
|
+
conn.bulk.query(soql)
|
|
117
|
+
.on("record", (r) => recs.push(r))
|
|
118
|
+
.on("end", () => resolve(recs))
|
|
119
|
+
.on("error", reject);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
// Step 4: Format results
|
|
123
|
+
const formattedRecords = records.map((record, index) => {
|
|
133
124
|
const recordStr = fields.map(field => {
|
|
134
|
-
// Handle special case for subqueries (child relationships)
|
|
135
125
|
if (field.startsWith('(SELECT')) {
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const childRecords = record[relationshipName];
|
|
140
|
-
return ` ${relationshipName}: [${childRecords?.length || 0} records]`;
|
|
126
|
+
const relName = field.match(/FROM\s+(\w+)/)?.[1];
|
|
127
|
+
const child = record[relName];
|
|
128
|
+
return ` ${relName}: [${child?.length || 0} records]`;
|
|
141
129
|
}
|
|
142
130
|
return ' ' + formatRelationshipResults(record, field);
|
|
143
131
|
}).join('\n');
|
|
144
132
|
return `Record ${index + 1}:\n${recordStr}`;
|
|
145
133
|
}).join('\n\n');
|
|
134
|
+
// Step 5: Return response
|
|
146
135
|
return {
|
|
147
136
|
content: [{
|
|
148
137
|
type: "text",
|
|
149
|
-
text: `Query returned ${
|
|
138
|
+
text: `Query returned ${records.length} records:\n\n${formattedRecords}`
|
|
150
139
|
}],
|
|
151
|
-
isError: false
|
|
140
|
+
isError: false
|
|
152
141
|
};
|
|
153
142
|
}
|
|
154
143
|
catch (error) {
|
|
155
|
-
|
|
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
|
-
}
|
|
144
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
172
145
|
return {
|
|
173
146
|
content: [{
|
|
174
147
|
type: "text",
|
|
175
|
-
text: `Error executing query: ${
|
|
148
|
+
text: `Error executing query: ${msg}`
|
|
176
149
|
}],
|
|
177
|
-
isError: true
|
|
150
|
+
isError: true
|
|
178
151
|
};
|
|
179
152
|
}
|
|
180
153
|
}
|
package/package.json
CHANGED
|
@@ -1,39 +1,38 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rohithvemulapally/mcp-server-salesforce",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.8",
|
|
4
4
|
"description": "A Salesforce connector MCP Server.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"bin": {
|
|
9
|
-
|
|
9
|
+
"salesforce-connector": "dist/index.js"
|
|
10
10
|
},
|
|
11
11
|
"files": [
|
|
12
|
-
|
|
12
|
+
"dist"
|
|
13
13
|
],
|
|
14
14
|
"scripts": {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
"build": "tsc && shx chmod +x dist/*.js",
|
|
16
|
+
"prepare": "npm run build",
|
|
17
|
+
"watch": "tsc --watch",
|
|
18
|
+
"test": "node --test"
|
|
19
19
|
},
|
|
20
20
|
"keywords": [
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
"mcp",
|
|
22
|
+
"salesforce",
|
|
23
|
+
"claude",
|
|
24
|
+
"ai"
|
|
25
25
|
],
|
|
26
26
|
"author": "rohithvemulapally",
|
|
27
27
|
"license": "MIT",
|
|
28
28
|
"dependencies": {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
"@modelcontextprotocol/sdk": "1.22.0",
|
|
30
|
+
"dotenv": "16.4.7",
|
|
31
|
+
"jsforce": "^3.10.3"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
"@types/node": "^24.3.0",
|
|
35
|
+
"typescript": "^5.7.2",
|
|
36
|
+
"shx": "^0.4.0"
|
|
37
37
|
}
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
}
|