@rohithvemulapally/mcp-server-salesforce 0.0.6 → 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 +3 -3
- package/dist/tools/metadata.d.ts +1 -1
- package/dist/tools/metadata.js +7 -32
- package/dist/tools/query.d.ts +12 -0
- package/dist/tools/query.js +75 -36
- package/package.json +18 -19
package/dist/index.js
CHANGED
|
@@ -86,6 +86,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
86
86
|
};
|
|
87
87
|
return await handleQueryRecords(conn, validatedArgs);
|
|
88
88
|
}
|
|
89
|
+
case "salesforce_metadata_query": {
|
|
90
|
+
return await handleMetadataQuery(conn, args);
|
|
91
|
+
}
|
|
89
92
|
case "salesforce_aggregate_query": {
|
|
90
93
|
const aggregateArgs = args;
|
|
91
94
|
if (!aggregateArgs.objectName || !Array.isArray(aggregateArgs.selectFields) || !Array.isArray(aggregateArgs.groupByFields)) {
|
|
@@ -252,9 +255,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
252
255
|
};
|
|
253
256
|
return await handleWriteApexTrigger(conn, validatedArgs);
|
|
254
257
|
}
|
|
255
|
-
case "salesforce_metadata_query": {
|
|
256
|
-
return await handleMetadataQuery(conn, args);
|
|
257
|
-
}
|
|
258
258
|
case "salesforce_execute_anonymous": {
|
|
259
259
|
const executeArgs = args;
|
|
260
260
|
if (!executeArgs.apexCode) {
|
package/dist/tools/metadata.d.ts
CHANGED
package/dist/tools/metadata.js
CHANGED
|
@@ -1,21 +1,12 @@
|
|
|
1
1
|
export const METADATA_QUERY = {
|
|
2
2
|
name: "salesforce_metadata_query",
|
|
3
|
-
description: "Fetch
|
|
3
|
+
description: "Fetch validation rules or lightning pages",
|
|
4
4
|
inputSchema: {
|
|
5
5
|
type: "object",
|
|
6
6
|
properties: {
|
|
7
|
-
metadataType: {
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
}
|
|
7
|
+
metadataType: { type: "string" },
|
|
8
|
+
objectName: { type: "string" },
|
|
9
|
+
name: { type: "string" }
|
|
19
10
|
},
|
|
20
11
|
required: ["metadataType"]
|
|
21
12
|
}
|
|
@@ -24,42 +15,26 @@ export async function handleMetadataQuery(conn, args) {
|
|
|
24
15
|
const { metadataType, objectName, name } = args;
|
|
25
16
|
try {
|
|
26
17
|
if (metadataType === "ValidationRule") {
|
|
27
|
-
if (!objectName) {
|
|
28
|
-
throw new Error("objectName is required for ValidationRule");
|
|
29
|
-
}
|
|
30
18
|
const result = await conn.tooling.query(`SELECT Id, ValidationName, ErrorMessage, Active
|
|
31
19
|
FROM ValidationRule
|
|
32
20
|
WHERE EntityDefinition.QualifiedApiName = '${objectName}'`);
|
|
33
21
|
return {
|
|
34
|
-
content: [{
|
|
35
|
-
type: "text",
|
|
36
|
-
text: JSON.stringify(result.records, null, 2)
|
|
37
|
-
}],
|
|
22
|
+
content: [{ type: "text", text: JSON.stringify(result.records, null, 2) }],
|
|
38
23
|
isError: false
|
|
39
24
|
};
|
|
40
25
|
}
|
|
41
26
|
if (metadataType === "FlexiPage") {
|
|
42
|
-
if (!name) {
|
|
43
|
-
throw new Error("name is required for FlexiPage");
|
|
44
|
-
}
|
|
45
27
|
const result = await conn.metadata.read("FlexiPage", name);
|
|
46
28
|
return {
|
|
47
|
-
content: [{
|
|
48
|
-
type: "text",
|
|
49
|
-
text: JSON.stringify(result, null, 2)
|
|
50
|
-
}],
|
|
29
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
51
30
|
isError: false
|
|
52
31
|
};
|
|
53
32
|
}
|
|
54
33
|
throw new Error("Unsupported metadata type");
|
|
55
34
|
}
|
|
56
35
|
catch (error) {
|
|
57
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
58
36
|
return {
|
|
59
|
-
content: [{
|
|
60
|
-
type: "text",
|
|
61
|
-
text: `Error fetching metadata: ${errorMessage}`
|
|
62
|
-
}],
|
|
37
|
+
content: [{ type: "text", text: error.message }],
|
|
63
38
|
isError: true
|
|
64
39
|
};
|
|
65
40
|
}
|
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,16 +1,31 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
.on("error", (err) => reject(err));
|
|
9
|
-
});
|
|
10
|
-
}
|
|
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
|
+
*/
|
|
11
8
|
export const QUERY_RECORDS = {
|
|
12
9
|
name: "salesforce_query_records",
|
|
13
|
-
description:
|
|
10
|
+
description: `Query records from any Salesforce object using SOQL, including relationship queries.
|
|
11
|
+
|
|
12
|
+
NOTE:
|
|
13
|
+
- For aggregate queries (COUNT, SUM, GROUP BY), standard query is used.
|
|
14
|
+
- For large datasets, Bulk API is used automatically.
|
|
15
|
+
|
|
16
|
+
Examples:
|
|
17
|
+
1. Parent-to-child query:
|
|
18
|
+
- objectName: "Account"
|
|
19
|
+
- fields: ["Name", "(SELECT Id, FirstName FROM Contacts)"]
|
|
20
|
+
|
|
21
|
+
2. Child-to-parent query:
|
|
22
|
+
- objectName: "Contact"
|
|
23
|
+
- fields: ["FirstName", "Account.Name"]
|
|
24
|
+
|
|
25
|
+
3. Multi-level relationship:
|
|
26
|
+
- objectName: "Contact"
|
|
27
|
+
- fields: ["Account.Owner.Name"]
|
|
28
|
+
`,
|
|
14
29
|
inputSchema: {
|
|
15
30
|
type: "object",
|
|
16
31
|
properties: {
|
|
@@ -23,40 +38,52 @@ export const QUERY_RECORDS = {
|
|
|
23
38
|
required: ["objectName", "fields"]
|
|
24
39
|
}
|
|
25
40
|
};
|
|
41
|
+
/**
|
|
42
|
+
* Validates relationship fields syntax.
|
|
43
|
+
*/
|
|
26
44
|
function validateRelationshipFields(fields) {
|
|
27
45
|
for (const field of fields) {
|
|
28
|
-
if (field.includes(
|
|
29
|
-
const parts = field.split(
|
|
30
|
-
if (parts.some(
|
|
31
|
-
return { isValid: false, error: `Invalid
|
|
46
|
+
if (field.includes('.')) {
|
|
47
|
+
const parts = field.split('.');
|
|
48
|
+
if (parts.some(p => !p)) {
|
|
49
|
+
return { isValid: false, error: `Invalid field: ${field}` };
|
|
32
50
|
}
|
|
33
51
|
if (parts.length > 5) {
|
|
34
|
-
return { isValid: false, error: `
|
|
52
|
+
return { isValid: false, error: `Too deep relationship: ${field}` };
|
|
35
53
|
}
|
|
36
54
|
}
|
|
37
|
-
if (field.includes(
|
|
38
|
-
return { isValid: false, error: `Invalid subquery
|
|
55
|
+
if (field.includes('SELECT') && !field.match(/^\(SELECT.*FROM.*\)$/)) {
|
|
56
|
+
return { isValid: false, error: `Invalid subquery: ${field}` };
|
|
39
57
|
}
|
|
40
58
|
}
|
|
41
59
|
return { isValid: true };
|
|
42
60
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Formats relationship fields for readable output.
|
|
63
|
+
*/
|
|
64
|
+
function formatRelationshipResults(record, field, prefix = '') {
|
|
65
|
+
if (field.includes('.')) {
|
|
66
|
+
const [rel, ...rest] = field.split('.');
|
|
67
|
+
const related = record[rel];
|
|
68
|
+
if (!related)
|
|
48
69
|
return `${prefix}${field}: null`;
|
|
49
|
-
return formatRelationshipResults(
|
|
70
|
+
return formatRelationshipResults(related, rest.join('.'), `${prefix}${rel}.`);
|
|
50
71
|
}
|
|
51
72
|
const value = record[field];
|
|
52
73
|
if (Array.isArray(value)) {
|
|
53
74
|
return `${prefix}${field}: [${value.length} records]`;
|
|
54
75
|
}
|
|
55
|
-
return `${prefix}${field}: ${value ??
|
|
76
|
+
return `${prefix}${field}: ${value ?? 'null'}`;
|
|
56
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* Executes query using either:
|
|
80
|
+
* - Standard API (for aggregates, limits)
|
|
81
|
+
* - Bulk API (for large datasets)
|
|
82
|
+
*/
|
|
57
83
|
export async function handleQueryRecords(conn, args) {
|
|
58
84
|
const { objectName, fields, whereClause, orderBy, limit } = args;
|
|
59
85
|
try {
|
|
86
|
+
// Step 1: Validate fields
|
|
60
87
|
const validation = validateRelationshipFields(fields);
|
|
61
88
|
if (!validation.isValid) {
|
|
62
89
|
return {
|
|
@@ -64,7 +91,8 @@ export async function handleQueryRecords(conn, args) {
|
|
|
64
91
|
isError: true
|
|
65
92
|
};
|
|
66
93
|
}
|
|
67
|
-
|
|
94
|
+
// Step 2: Build SOQL query
|
|
95
|
+
let soql = `SELECT ${fields.join(', ')} FROM ${objectName}`;
|
|
68
96
|
if (whereClause)
|
|
69
97
|
soql += ` WHERE ${whereClause}`;
|
|
70
98
|
if (orderBy)
|
|
@@ -72,27 +100,38 @@ export async function handleQueryRecords(conn, args) {
|
|
|
72
100
|
if (limit)
|
|
73
101
|
soql += ` LIMIT ${limit}`;
|
|
74
102
|
let records = [];
|
|
103
|
+
// Step 3: Decide API (Standard vs Bulk)
|
|
75
104
|
const lower = soql.toLowerCase();
|
|
76
105
|
if (lower.includes("limit") ||
|
|
77
106
|
lower.includes("count") ||
|
|
78
107
|
lower.includes("group by")) {
|
|
108
|
+
// Use standard REST query
|
|
79
109
|
const result = await conn.query(soql);
|
|
80
110
|
records = result.records;
|
|
81
111
|
}
|
|
82
112
|
else {
|
|
83
|
-
|
|
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
|
+
});
|
|
84
121
|
}
|
|
122
|
+
// Step 4: Format results
|
|
85
123
|
const formattedRecords = records.map((record, index) => {
|
|
86
124
|
const recordStr = fields.map(field => {
|
|
87
|
-
if (field.startsWith(
|
|
88
|
-
const
|
|
89
|
-
const
|
|
90
|
-
return ` ${
|
|
125
|
+
if (field.startsWith('(SELECT')) {
|
|
126
|
+
const relName = field.match(/FROM\s+(\w+)/)?.[1];
|
|
127
|
+
const child = record[relName];
|
|
128
|
+
return ` ${relName}: [${child?.length || 0} records]`;
|
|
91
129
|
}
|
|
92
|
-
return
|
|
93
|
-
}).join(
|
|
130
|
+
return ' ' + formatRelationshipResults(record, field);
|
|
131
|
+
}).join('\n');
|
|
94
132
|
return `Record ${index + 1}:\n${recordStr}`;
|
|
95
|
-
}).join(
|
|
133
|
+
}).join('\n\n');
|
|
134
|
+
// Step 5: Return response
|
|
96
135
|
return {
|
|
97
136
|
content: [{
|
|
98
137
|
type: "text",
|
|
@@ -102,11 +141,11 @@ export async function handleQueryRecords(conn, args) {
|
|
|
102
141
|
};
|
|
103
142
|
}
|
|
104
143
|
catch (error) {
|
|
105
|
-
const
|
|
144
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
106
145
|
return {
|
|
107
146
|
content: [{
|
|
108
147
|
type: "text",
|
|
109
|
-
text: `Error executing query: ${
|
|
148
|
+
text: `Error executing query: ${msg}`
|
|
110
149
|
}],
|
|
111
150
|
isError: true
|
|
112
151
|
};
|
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
|
+
}
|