@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 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) {
@@ -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: "Query records from any Salesforce object using SOQL",
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: { type: "string" },
18
- fields: { type: "array", items: { type: "string" } },
19
- whereClause: { type: "string", optional: true },
20
- orderBy: { type: "string", optional: true },
21
- limit: { type: "number", optional: true }
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
- if (field.includes(".")) {
29
- const parts = field.split(".");
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 { isValid: false, error: `Invalid relationship field format: "${field}"` };
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 { isValid: false, error: `Relationship field "${field}" exceeds maximum depth` };
75
+ return {
76
+ isValid: false,
77
+ error: `Relationship field "${field}" exceeds maximum depth of 5 levels`
78
+ };
35
79
  }
36
80
  }
37
- if (field.includes("SELECT") && !field.match(/^\(SELECT.*FROM.*\)$/)) {
38
- return { isValid: false, error: `Invalid subquery format: "${field}"` };
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 formatRelationshipResults(record, field, prefix = "") {
44
- if (field.includes(".")) {
45
- const [relationship, ...rest] = field.split(".");
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
- return formatRelationshipResults(relatedRecord, rest.join("."), `${prefix}${relationship}.`);
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 ?? "null"}`;
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: [{ type: "text", text: validation.error }],
64
- isError: true
115
+ content: [{
116
+ type: "text",
117
+ text: validation.error
118
+ }],
119
+ isError: true,
65
120
  };
66
121
  }
67
- let soql = `SELECT ${fields.join(", ")} FROM ${objectName}`;
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
- let records = [];
75
- const lower = soql.toLowerCase();
76
- if (lower.includes("limit") ||
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
- if (field.startsWith("(SELECT")) {
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 " " + formatRelationshipResults(record, field);
93
- }).join("\n");
142
+ return ' ' + formatRelationshipResults(record, field);
143
+ }).join('\n');
94
144
  return `Record ${index + 1}:\n${recordStr}`;
95
- }).join("\n\n");
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: ${errorMessage}`
175
+ text: `Error executing query: ${enhancedError}`
110
176
  }],
111
- isError: true
177
+ isError: true,
112
178
  };
113
179
  }
114
180
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rohithvemulapally/mcp-server-salesforce",
3
- "version": "0.0.6",
3
+ "version": "0.0.7",
4
4
  "description": "A Salesforce connector MCP Server.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -1,9 +0,0 @@
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: string;
7
- }[];
8
- isError: boolean;
9
- }>;
@@ -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
- }