@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 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,9 @@
1
+ import { Tool } from "@modelcontextprotocol/sdk/types.js";
2
+ export declare const METADATA_QUERY: Tool;
3
+ export declare function handleMetadataQuery(conn: any, args: any): Promise<{
4
+ content: {
5
+ type: string;
6
+ text: any;
7
+ }[];
8
+ isError: boolean;
9
+ }>;
@@ -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
+ }
@@ -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;
@@ -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: For queries with GROUP BY, aggregate functions (COUNT, SUM, AVG, etc.), or HAVING clauses, use salesforce_aggregate_query instead.
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 (e.g., Account with Contacts):
17
+ 1. Parent-to-child query:
9
18
  - objectName: "Account"
10
- - fields: ["Name", "(SELECT Id, FirstName, LastName FROM Contacts)"]
19
+ - fields: ["Name", "(SELECT Id, FirstName FROM Contacts)"]
11
20
 
12
- 2. Child-to-parent query (e.g., Contact with Account details):
21
+ 2. Child-to-parent query:
13
22
  - objectName: "Contact"
14
- - fields: ["FirstName", "LastName", "Account.Name", "Account.Industry"]
23
+ - fields: ["FirstName", "Account.Name"]
15
24
 
16
- 3. Multiple level query (e.g., Contact -> Account -> Owner):
25
+ 3. Multi-level relationship:
17
26
  - 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")`,
27
+ - fields: ["Account.Owner.Name"]
28
+ `,
29
29
  inputSchema: {
30
30
  type: "object",
31
31
  properties: {
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
- }
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
- // Helper function to validate relationship field syntax
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
- // Check for empty parts
67
- if (parts.some(part => !part)) {
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
- // Helper function to format relationship query results
61
+ /**
62
+ * Formats relationship fields for readable output.
63
+ */
92
64
  function formatRelationshipResults(record, field, prefix = '') {
93
65
  if (field.includes('.')) {
94
- const [relationship, ...rest] = field.split('.');
95
- const relatedRecord = record[relationship];
96
- if (relatedRecord === null) {
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 !== null && value !== undefined ? value : 'null'}`;
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
- // Validate relationship field syntax
86
+ // Step 1: Validate fields
112
87
  const validation = validateRelationshipFields(fields);
113
88
  if (!validation.isValid) {
114
89
  return {
115
- content: [{
116
- type: "text",
117
- text: validation.error
118
- }],
119
- isError: true,
90
+ content: [{ type: "text", text: validation.error }],
91
+ isError: true
120
92
  };
121
93
  }
122
- // Construct SOQL query
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
- const result = await conn.query(soql);
131
- // Format the output
132
- const formattedRecords = result.records.map((record, index) => {
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 relationshipName = field.match(/FROM\s+(\w+)/)?.[1];
137
- if (!relationshipName)
138
- return ` ${field}: Invalid subquery format`;
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 ${result.records.length} records:\n\n${formattedRecords}`
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
- // Enhanced error handling for relationship queries
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: ${enhancedError}`
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.7",
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
- "salesforce-connector": "dist/index.js"
9
+ "salesforce-connector": "dist/index.js"
10
10
  },
11
11
  "files": [
12
- "dist"
12
+ "dist"
13
13
  ],
14
14
  "scripts": {
15
- "build": "tsc && shx chmod +x dist/*.js",
16
- "prepare": "npm run build",
17
- "watch": "tsc --watch",
18
- "test": "node --test"
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
- "mcp",
22
- "salesforce",
23
- "claude",
24
- "ai"
21
+ "mcp",
22
+ "salesforce",
23
+ "claude",
24
+ "ai"
25
25
  ],
26
26
  "author": "rohithvemulapally",
27
27
  "license": "MIT",
28
28
  "dependencies": {
29
- "@modelcontextprotocol/sdk": "1.22.0",
30
- "dotenv": "16.4.7",
31
- "jsforce": "^3.10.3"
29
+ "@modelcontextprotocol/sdk": "1.22.0",
30
+ "dotenv": "16.4.7",
31
+ "jsforce": "^3.10.3"
32
32
  },
33
33
  "devDependencies": {
34
- "@types/node": "^24.3.0",
35
- "typescript": "^5.7.2",
36
- "shx": "^0.4.0"
34
+ "@types/node": "^24.3.0",
35
+ "typescript": "^5.7.2",
36
+ "shx": "^0.4.0"
37
37
  }
38
- }
39
-
38
+ }