@snokam/mcp-salesforce 2.2.0 → 2.4.0

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/.releaserc.cjs CHANGED
@@ -26,8 +26,7 @@ module.exports = {
26
26
  "@semantic-release/git",
27
27
  {
28
28
  assets: ["package.json"],
29
- message:
30
- "chore(release): mcp-salesforce-v${nextRelease.version} [skip ci]\n\n${nextRelease.notes}",
29
+ message: "chore(release): ${nextRelease.gitTag} [skip ci]",
31
30
  },
32
31
  ],
33
32
  [
package/dist/index.js CHANGED
@@ -1,309 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
- import { z } from "zod";
5
- // Environment variables - same as existing chatgpt-function
6
- const SF_CONSUMER_KEY = process.env.SALESFORCE_CONSUMER_KEY;
7
- const SF_CONSUMER_SECRET = process.env.SALESFORCE_CONSUMER_SECRET;
8
- const SF_INSTANCE_URL = "https://snokam.my.salesforce.com";
9
- const SF_API_VERSION = "v62.0";
10
- let accessToken = null;
11
- async function getAccessToken() {
12
- if (accessToken) {
13
- return accessToken;
14
- }
15
- if (!SF_CONSUMER_KEY || !SF_CONSUMER_SECRET) {
16
- throw new Error("SALESFORCE_CONSUMER_KEY and SALESFORCE_CONSUMER_SECRET environment variables are required");
17
- }
18
- const params = new URLSearchParams({
19
- client_id: SF_CONSUMER_KEY,
20
- client_secret: SF_CONSUMER_SECRET,
21
- grant_type: "client_credentials",
22
- });
23
- const response = await fetch(`${SF_INSTANCE_URL}/services/oauth2/token`, {
24
- method: "POST",
25
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
26
- body: params,
27
- });
28
- if (!response.ok) {
29
- const error = await response.text();
30
- throw new Error(`Failed to get access token: ${response.status} - ${error}`);
31
- }
32
- const data = await response.json();
33
- accessToken = data.access_token;
34
- return accessToken;
35
- }
36
- async function sfQuery(soql) {
37
- const token = await getAccessToken();
38
- const url = `${SF_INSTANCE_URL}/services/data/${SF_API_VERSION}/query?q=${encodeURIComponent(soql)}`;
39
- const response = await fetch(url, {
40
- headers: { Authorization: `Bearer ${token}` },
41
- });
42
- if (!response.ok) {
43
- const error = await response.text();
44
- throw new Error(`Salesforce query failed: ${response.status} - ${error}`);
45
- }
46
- const data = await response.json();
47
- return data.records;
48
- }
4
+ import { registerTools } from "./tools/index.js";
49
5
  const server = new McpServer({
50
6
  name: "salesforce-mcp",
51
7
  version: "1.0.0",
52
8
  });
53
- // Tool: Search Contacts
54
- server.tool("search_contacts", "Search for contacts in Salesforce by name, email, or company", {
55
- query: z.string().describe("Search query (name, email, or company)"),
56
- limit: z.number().default(10).describe("Maximum number of results"),
57
- }, async ({ query, limit }) => {
58
- const escapedQuery = query.replace(/'/g, "\\'");
59
- const soql = `
60
- SELECT Id, Name, Email, Phone, Title, Account.Name, Account.Strategic_Priority__c, LastActivityDate
61
- FROM Contact
62
- WHERE Name LIKE '%${escapedQuery}%' OR Email LIKE '%${escapedQuery}%' OR Account.Name LIKE '%${escapedQuery}%'
63
- ORDER BY LastActivityDate DESC NULLS LAST
64
- LIMIT ${limit}
65
- `;
66
- const records = await sfQuery(soql);
67
- return {
68
- content: [{ type: "text", text: JSON.stringify(records, null, 2) }],
69
- };
70
- });
71
- // Tool: Search Leads
72
- server.tool("search_leads", "Search for leads in Salesforce", {
73
- query: z.string().describe("Search query"),
74
- status: z
75
- .string()
76
- .optional()
77
- .describe("Lead status filter (e.g., Open, Working, Closed)"),
78
- limit: z.number().default(10).describe("Maximum number of results"),
79
- }, async ({ query, status, limit }) => {
80
- const escapedQuery = query.replace(/'/g, "\\'");
81
- let whereClause = `Name LIKE '%${escapedQuery}%' OR Email LIKE '%${escapedQuery}%' OR Company LIKE '%${escapedQuery}%'`;
82
- if (status) {
83
- whereClause = `(${whereClause}) AND Status = '${status}'`;
84
- }
85
- const soql = `
86
- SELECT Id, Name, Email, Phone, Company, Status, LeadSource, CreatedDate
87
- FROM Lead
88
- WHERE ${whereClause}
89
- ORDER BY CreatedDate DESC
90
- LIMIT ${limit}
91
- `;
92
- const records = await sfQuery(soql);
93
- return {
94
- content: [{ type: "text", text: JSON.stringify(records, null, 2) }],
95
- };
96
- });
97
- // Tool: Get Opportunities
98
- server.tool("get_opportunities", "Get opportunities from Salesforce pipeline", {
99
- stage: z
100
- .string()
101
- .optional()
102
- .describe("Filter by stage (e.g., Prospecting, Negotiation, Closed Won)"),
103
- accountId: z.string().optional().describe("Filter by account ID"),
104
- limit: z.number().default(20).describe("Maximum number of results"),
105
- }, async ({ stage, accountId, limit }) => {
106
- const conditions = [];
107
- if (stage)
108
- conditions.push(`StageName = '${stage}'`);
109
- if (accountId)
110
- conditions.push(`AccountId = '${accountId}'`);
111
- const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
112
- const soql = `
113
- SELECT Id, Name, StageName, Amount, CloseDate, Account.Name, Owner.Name, Probability
114
- FROM Opportunity
115
- ${whereClause}
116
- ORDER BY CloseDate ASC
117
- LIMIT ${limit}
118
- `;
119
- const records = await sfQuery(soql);
120
- return {
121
- content: [{ type: "text", text: JSON.stringify(records, null, 2) }],
122
- };
123
- });
124
- // Tool: Get Account Details
125
- server.tool("get_account", "Get detailed information about a Salesforce account", {
126
- accountId: z.string().describe("Salesforce Account ID"),
127
- }, async ({ accountId }) => {
128
- const soql = `
129
- SELECT Id, Name, Industry, Website, Phone, BillingCity, BillingCountry,
130
- Description, NumberOfEmployees, AnnualRevenue, Owner.Name, Type,
131
- Strategic_Priority__c, Technology__c, LastActivityDate,
132
- (SELECT Id, Name, Email, Title FROM Contacts LIMIT 10),
133
- (SELECT Id, Name, StageName, Amount, CloseDate FROM Opportunities ORDER BY CloseDate DESC LIMIT 5)
134
- FROM Account
135
- WHERE Id = '${accountId}'
136
- `;
137
- const records = await sfQuery(soql);
138
- return {
139
- content: [{ type: "text", text: JSON.stringify(records[0], null, 2) }],
140
- };
141
- });
142
- // Tool: Search Accounts
143
- server.tool("search_accounts", "Search for accounts/companies in Salesforce", {
144
- query: z.string().describe("Search query (company name)"),
145
- type: z
146
- .string()
147
- .optional()
148
- .describe("Account type filter (e.g., Prospect, Customer)"),
149
- limit: z.number().default(10).describe("Maximum number of results"),
150
- }, async ({ query, type, limit }) => {
151
- const escapedQuery = query.replace(/'/g, "\\'");
152
- let whereClause = `Name LIKE '%${escapedQuery}%'`;
153
- if (type) {
154
- whereClause += ` AND Type = '${type}'`;
155
- }
156
- const soql = `
157
- SELECT Id, Name, Industry, Website, Phone, BillingCity, Owner.Name, Type,
158
- Strategic_Priority__c, Technology__c, LastActivityDate
159
- FROM Account
160
- WHERE ${whereClause}
161
- ORDER BY Name ASC
162
- LIMIT ${limit}
163
- `;
164
- const records = await sfQuery(soql);
165
- return {
166
- content: [{ type: "text", text: JSON.stringify(records, null, 2) }],
167
- };
168
- });
169
- // Tool: Get Strategic Contacts (for follow-up)
170
- server.tool("get_strategic_contacts", "Get contacts from strategic priority accounts that need follow-up", {
171
- priority: z
172
- .enum(["A", "B", "C", "D"])
173
- .default("D")
174
- .describe("Strategic priority filter"),
175
- daysInactive: z.number().default(90).describe("Days since last activity"),
176
- limit: z.number().default(20).describe("Maximum number of results"),
177
- }, async ({ priority, daysInactive, limit }) => {
178
- const cutoffDate = new Date(Date.now() - daysInactive * 24 * 60 * 60 * 1000)
179
- .toISOString()
180
- .split("T")[0];
181
- const soql = `
182
- SELECT Id, Name, Title, Email, Account.Name, Account.Strategic_Priority__c,
183
- Account.Technology__c, Account.Description, Account.LastActivityDate
184
- FROM Contact
185
- WHERE Account.Type = 'Prospect'
186
- AND Account.Strategic_Priority__c = '${priority}'
187
- AND Email != null
188
- AND (Account.LastActivityDate < ${cutoffDate} OR Account.LastActivityDate = null)
189
- LIMIT ${limit}
190
- `;
191
- const records = await sfQuery(soql);
192
- return {
193
- content: [{ type: "text", text: JSON.stringify(records, null, 2) }],
194
- };
195
- });
196
- // Tool: Execute SOQL Query
197
- server.tool("soql_query", "Execute a custom SOQL query against Salesforce (read-only)", {
198
- query: z.string().describe("SOQL query to execute"),
199
- }, async ({ query }) => {
200
- // Basic safety check - only allow SELECT queries
201
- if (!query.trim().toUpperCase().startsWith("SELECT")) {
202
- return {
203
- content: [
204
- { type: "text", text: "Error: Only SELECT queries are allowed" },
205
- ],
206
- isError: true,
207
- };
208
- }
209
- const records = await sfQuery(query);
210
- return {
211
- content: [{ type: "text", text: JSON.stringify(records, null, 2) }],
212
- };
213
- });
214
- // Tool: Get Recent Activities for Contact
215
- server.tool("get_contact_activities", "Get recent activities (events and tasks) for a specific contact", {
216
- contactId: z.string().describe("Salesforce Contact ID"),
217
- daysBack: z.number().default(90).describe("Number of days to look back"),
218
- }, async ({ contactId, daysBack }) => {
219
- const cutoffDate = new Date(Date.now() - daysBack * 24 * 60 * 60 * 1000).toISOString();
220
- // Get events
221
- const eventsSoql = `
222
- SELECT Id, Subject, Description, ActivityDateTime
223
- FROM Event
224
- WHERE WhoId = '${contactId}'
225
- AND ActivityDateTime > ${cutoffDate}
226
- ORDER BY ActivityDateTime DESC
227
- `;
228
- // Get tasks
229
- const tasksSoql = `
230
- SELECT Id, Subject, Description, ActivityDate, Status
231
- FROM Task
232
- WHERE WhoId = '${contactId}'
233
- AND ActivityDate > ${cutoffDate.split("T")[0]}
234
- ORDER BY ActivityDate DESC
235
- `;
236
- const [events, tasks] = await Promise.all([
237
- sfQuery(eventsSoql),
238
- sfQuery(tasksSoql),
239
- ]);
240
- return {
241
- content: [
242
- {
243
- type: "text",
244
- text: JSON.stringify({ events, tasks }, null, 2),
245
- },
246
- ],
247
- };
248
- });
249
- // Tool: Get Comprehensive Contact Info (like existing chatgpt-function)
250
- server.tool("get_comprehensive_contact", "Get comprehensive contact information including account details and recent activities", {
251
- contactId: z.string().describe("Salesforce Contact ID"),
252
- daysBack: z
253
- .number()
254
- .default(90)
255
- .describe("Number of days to look back for activities"),
256
- }, async ({ contactId, daysBack }) => {
257
- // Get contact details
258
- const contactSoql = `
259
- SELECT Id, Name, Title, Email, Phone, Description,
260
- Account.Name, Account.Strategic_Priority__c, Account.Technology__c,
261
- Account.Description, Account.Industry, Account.Website
262
- FROM Contact
263
- WHERE Id = '${contactId}'
264
- `;
265
- const contacts = await sfQuery(contactSoql);
266
- const contact = contacts[0];
267
- if (!contact) {
268
- return {
269
- content: [
270
- { type: "text", text: `No contact found with ID: ${contactId}` },
271
- ],
272
- isError: true,
273
- };
274
- }
275
- const cutoffDate = new Date(Date.now() - daysBack * 24 * 60 * 60 * 1000).toISOString();
276
- // Get events and tasks
277
- const eventsSoql = `
278
- SELECT Id, Subject, Description, ActivityDateTime
279
- FROM Event
280
- WHERE WhoId = '${contactId}'
281
- AND ActivityDateTime > ${cutoffDate}
282
- ORDER BY ActivityDateTime DESC
283
- LIMIT 20
284
- `;
285
- const tasksSoql = `
286
- SELECT Id, Subject, Description, ActivityDate, Status
287
- FROM Task
288
- WHERE WhoId = '${contactId}'
289
- AND ActivityDate > ${cutoffDate.split("T")[0]}
290
- ORDER BY ActivityDate DESC
291
- LIMIT 20
292
- `;
293
- const [events, tasks] = await Promise.all([
294
- sfQuery(eventsSoql),
295
- sfQuery(tasksSoql),
296
- ]);
297
- return {
298
- content: [
299
- {
300
- type: "text",
301
- text: JSON.stringify({ contact, events, tasks }, null, 2),
302
- },
303
- ],
304
- };
305
- });
306
- // Start the server
9
+ registerTools(server);
307
10
  async function main() {
308
11
  const transport = new StdioServerTransport();
309
12
  await server.connect(transport);
@@ -0,0 +1 @@
1
+ export declare function sfQuery<T = unknown>(soql: string): Promise<T[]>;
@@ -0,0 +1,44 @@
1
+ const SF_ACCESS_TOKEN = process.env.SALESFORCE_ACCESS_TOKEN;
2
+ const SF_CONSUMER_KEY = process.env.SALESFORCE_CONSUMER_KEY;
3
+ const SF_CONSUMER_SECRET = process.env.SALESFORCE_CONSUMER_SECRET;
4
+ const SF_INSTANCE_URL = process.env.SALESFORCE_INSTANCE_URL || "https://snokam.my.salesforce.com";
5
+ const SF_API_VERSION = "v62.0";
6
+ let accessToken = SF_ACCESS_TOKEN ?? null;
7
+ async function getAccessToken() {
8
+ if (accessToken) {
9
+ return accessToken;
10
+ }
11
+ if (!SF_CONSUMER_KEY || !SF_CONSUMER_SECRET) {
12
+ throw new Error("Set SALESFORCE_ACCESS_TOKEN, or SALESFORCE_CONSUMER_KEY + SALESFORCE_CONSUMER_SECRET");
13
+ }
14
+ const params = new URLSearchParams({
15
+ client_id: SF_CONSUMER_KEY,
16
+ client_secret: SF_CONSUMER_SECRET,
17
+ grant_type: "client_credentials",
18
+ });
19
+ const response = await fetch(`${SF_INSTANCE_URL}/services/oauth2/token`, {
20
+ method: "POST",
21
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
22
+ body: params,
23
+ });
24
+ if (!response.ok) {
25
+ const error = await response.text();
26
+ throw new Error(`Failed to get access token: ${response.status} - ${error}`);
27
+ }
28
+ const data = await response.json();
29
+ accessToken = data.access_token;
30
+ return accessToken;
31
+ }
32
+ export async function sfQuery(soql) {
33
+ const token = await getAccessToken();
34
+ const url = `${SF_INSTANCE_URL}/services/data/${SF_API_VERSION}/query?q=${encodeURIComponent(soql)}`;
35
+ const response = await fetch(url, {
36
+ headers: { Authorization: `Bearer ${token}` },
37
+ });
38
+ if (!response.ok) {
39
+ const error = await response.text();
40
+ throw new Error(`Salesforce query failed: ${response.status} - ${error}`);
41
+ }
42
+ const data = await response.json();
43
+ return data.records;
44
+ }
@@ -0,0 +1,2 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerAccountTools(server: McpServer): void;
@@ -0,0 +1,47 @@
1
+ import { z } from "zod";
2
+ import { sfQuery } from "../salesforce-client.js";
3
+ export function registerAccountTools(server) {
4
+ server.tool("get_account", "Get detailed information about a Salesforce account", {
5
+ accountId: z.string().describe("Salesforce Account ID"),
6
+ }, async ({ accountId }) => {
7
+ const soql = `
8
+ SELECT Id, Name, Industry, Website, Phone, BillingCity, BillingCountry,
9
+ Description, NumberOfEmployees, AnnualRevenue, Owner.Name, Type,
10
+ Strategic_Priority__c, Technology__c, LastActivityDate,
11
+ (SELECT Id, Name, Email, Title FROM Contacts LIMIT 10),
12
+ (SELECT Id, Name, StageName, Amount, CloseDate FROM Opportunities ORDER BY CloseDate DESC LIMIT 5)
13
+ FROM Account
14
+ WHERE Id = '${accountId}'
15
+ `;
16
+ const records = await sfQuery(soql);
17
+ return {
18
+ content: [{ type: "text", text: JSON.stringify(records[0], null, 2) }],
19
+ };
20
+ });
21
+ server.tool("search_accounts", "Search for accounts/companies in Salesforce", {
22
+ query: z.string().describe("Search query (company name)"),
23
+ type: z
24
+ .string()
25
+ .optional()
26
+ .describe("Account type filter (e.g., Prospect, Customer)"),
27
+ limit: z.number().default(10).describe("Maximum number of results"),
28
+ }, async ({ query, type, limit }) => {
29
+ const escapedQuery = query.replace(/'/g, "\\'");
30
+ let whereClause = `Name LIKE '%${escapedQuery}%'`;
31
+ if (type) {
32
+ whereClause += ` AND Type = '${type}'`;
33
+ }
34
+ const soql = `
35
+ SELECT Id, Name, Industry, Website, Phone, BillingCity, Owner.Name, Type,
36
+ Strategic_Priority__c, Technology__c, LastActivityDate
37
+ FROM Account
38
+ WHERE ${whereClause}
39
+ ORDER BY Name ASC
40
+ LIMIT ${limit}
41
+ `;
42
+ const records = await sfQuery(soql);
43
+ return {
44
+ content: [{ type: "text", text: JSON.stringify(records, null, 2) }],
45
+ };
46
+ });
47
+ }
@@ -0,0 +1,2 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerContactTools(server: McpServer): void;
@@ -0,0 +1,133 @@
1
+ import { z } from "zod";
2
+ import { sfQuery } from "../salesforce-client.js";
3
+ export function registerContactTools(server) {
4
+ server.tool("search_contacts", "Search for contacts in Salesforce by name, email, or company", {
5
+ query: z.string().describe("Search query (name, email, or company)"),
6
+ limit: z.number().default(10).describe("Maximum number of results"),
7
+ }, async ({ query, limit }) => {
8
+ const escapedQuery = query.replace(/'/g, "\\'");
9
+ const soql = `
10
+ SELECT Id, Name, Email, Phone, Title, Account.Name, Account.Strategic_Priority__c, LastActivityDate
11
+ FROM Contact
12
+ WHERE Name LIKE '%${escapedQuery}%' OR Email LIKE '%${escapedQuery}%' OR Account.Name LIKE '%${escapedQuery}%'
13
+ ORDER BY LastActivityDate DESC NULLS LAST
14
+ LIMIT ${limit}
15
+ `;
16
+ const records = await sfQuery(soql);
17
+ return {
18
+ content: [{ type: "text", text: JSON.stringify(records, null, 2) }],
19
+ };
20
+ });
21
+ server.tool("get_strategic_contacts", "Get contacts from strategic priority accounts that need follow-up", {
22
+ priority: z
23
+ .enum(["A", "B", "C", "D"])
24
+ .default("D")
25
+ .describe("Strategic priority filter"),
26
+ daysInactive: z.number().default(90).describe("Days since last activity"),
27
+ limit: z.number().default(20).describe("Maximum number of results"),
28
+ }, async ({ priority, daysInactive, limit }) => {
29
+ const cutoffDate = new Date(Date.now() - daysInactive * 24 * 60 * 60 * 1000)
30
+ .toISOString()
31
+ .split("T")[0];
32
+ const soql = `
33
+ SELECT Id, Name, Title, Email, Account.Name, Account.Strategic_Priority__c,
34
+ Account.Technology__c, Account.Description, Account.LastActivityDate
35
+ FROM Contact
36
+ WHERE Account.Type = 'Prospect'
37
+ AND Account.Strategic_Priority__c = '${priority}'
38
+ AND Email != null
39
+ AND (Account.LastActivityDate < ${cutoffDate} OR Account.LastActivityDate = null)
40
+ LIMIT ${limit}
41
+ `;
42
+ const records = await sfQuery(soql);
43
+ return {
44
+ content: [{ type: "text", text: JSON.stringify(records, null, 2) }],
45
+ };
46
+ });
47
+ server.tool("get_contact_activities", "Get recent activities (events and tasks) for a specific contact", {
48
+ contactId: z.string().describe("Salesforce Contact ID"),
49
+ daysBack: z.number().default(90).describe("Number of days to look back"),
50
+ }, async ({ contactId, daysBack }) => {
51
+ const cutoffDate = new Date(Date.now() - daysBack * 24 * 60 * 60 * 1000).toISOString();
52
+ const eventsSoql = `
53
+ SELECT Id, Subject, Description, ActivityDateTime
54
+ FROM Event
55
+ WHERE WhoId = '${contactId}'
56
+ AND ActivityDateTime > ${cutoffDate}
57
+ ORDER BY ActivityDateTime DESC
58
+ `;
59
+ const tasksSoql = `
60
+ SELECT Id, Subject, Description, ActivityDate, Status
61
+ FROM Task
62
+ WHERE WhoId = '${contactId}'
63
+ AND ActivityDate > ${cutoffDate.split("T")[0]}
64
+ ORDER BY ActivityDate DESC
65
+ `;
66
+ const [events, tasks] = await Promise.all([
67
+ sfQuery(eventsSoql),
68
+ sfQuery(tasksSoql),
69
+ ]);
70
+ return {
71
+ content: [
72
+ {
73
+ type: "text",
74
+ text: JSON.stringify({ events, tasks }, null, 2),
75
+ },
76
+ ],
77
+ };
78
+ });
79
+ server.tool("get_comprehensive_contact", "Get comprehensive contact information including account details and recent activities", {
80
+ contactId: z.string().describe("Salesforce Contact ID"),
81
+ daysBack: z
82
+ .number()
83
+ .default(90)
84
+ .describe("Number of days to look back for activities"),
85
+ }, async ({ contactId, daysBack }) => {
86
+ const contactSoql = `
87
+ SELECT Id, Name, Title, Email, Phone, Description,
88
+ Account.Name, Account.Strategic_Priority__c, Account.Technology__c,
89
+ Account.Description, Account.Industry, Account.Website
90
+ FROM Contact
91
+ WHERE Id = '${contactId}'
92
+ `;
93
+ const contacts = await sfQuery(contactSoql);
94
+ const contact = contacts[0];
95
+ if (!contact) {
96
+ return {
97
+ content: [
98
+ { type: "text", text: `No contact found with ID: ${contactId}` },
99
+ ],
100
+ isError: true,
101
+ };
102
+ }
103
+ const cutoffDate = new Date(Date.now() - daysBack * 24 * 60 * 60 * 1000).toISOString();
104
+ const eventsSoql = `
105
+ SELECT Id, Subject, Description, ActivityDateTime
106
+ FROM Event
107
+ WHERE WhoId = '${contactId}'
108
+ AND ActivityDateTime > ${cutoffDate}
109
+ ORDER BY ActivityDateTime DESC
110
+ LIMIT 20
111
+ `;
112
+ const tasksSoql = `
113
+ SELECT Id, Subject, Description, ActivityDate, Status
114
+ FROM Task
115
+ WHERE WhoId = '${contactId}'
116
+ AND ActivityDate > ${cutoffDate.split("T")[0]}
117
+ ORDER BY ActivityDate DESC
118
+ LIMIT 20
119
+ `;
120
+ const [events, tasks] = await Promise.all([
121
+ sfQuery(eventsSoql),
122
+ sfQuery(tasksSoql),
123
+ ]);
124
+ return {
125
+ content: [
126
+ {
127
+ type: "text",
128
+ text: JSON.stringify({ contact, events, tasks }, null, 2),
129
+ },
130
+ ],
131
+ };
132
+ });
133
+ }
@@ -0,0 +1,2 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerTools(server: McpServer): void;
@@ -0,0 +1,8 @@
1
+ import { registerAccountTools } from "./accounts.js";
2
+ import { registerContactTools } from "./contacts.js";
3
+ import { registerPipelineTools } from "./pipeline.js";
4
+ export function registerTools(server) {
5
+ registerContactTools(server);
6
+ registerAccountTools(server);
7
+ registerPipelineTools(server);
8
+ }
@@ -0,0 +1,2 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerPipelineTools(server: McpServer): void;
@@ -0,0 +1,71 @@
1
+ import { z } from "zod";
2
+ import { sfQuery } from "../salesforce-client.js";
3
+ export function registerPipelineTools(server) {
4
+ server.tool("search_leads", "Search for leads in Salesforce", {
5
+ query: z.string().describe("Search query"),
6
+ status: z
7
+ .string()
8
+ .optional()
9
+ .describe("Lead status filter (e.g., Open, Working, Closed)"),
10
+ limit: z.number().default(10).describe("Maximum number of results"),
11
+ }, async ({ query, status, limit }) => {
12
+ const escapedQuery = query.replace(/'/g, "\\'");
13
+ let whereClause = `Name LIKE '%${escapedQuery}%' OR Email LIKE '%${escapedQuery}%' OR Company LIKE '%${escapedQuery}%'`;
14
+ if (status) {
15
+ whereClause = `(${whereClause}) AND Status = '${status}'`;
16
+ }
17
+ const soql = `
18
+ SELECT Id, Name, Email, Phone, Company, Status, LeadSource, CreatedDate
19
+ FROM Lead
20
+ WHERE ${whereClause}
21
+ ORDER BY CreatedDate DESC
22
+ LIMIT ${limit}
23
+ `;
24
+ const records = await sfQuery(soql);
25
+ return {
26
+ content: [{ type: "text", text: JSON.stringify(records, null, 2) }],
27
+ };
28
+ });
29
+ server.tool("get_opportunities", "Get opportunities from Salesforce pipeline", {
30
+ stage: z
31
+ .string()
32
+ .optional()
33
+ .describe("Filter by stage (e.g., Prospecting, Negotiation, Closed Won)"),
34
+ accountId: z.string().optional().describe("Filter by account ID"),
35
+ limit: z.number().default(20).describe("Maximum number of results"),
36
+ }, async ({ stage, accountId, limit }) => {
37
+ const conditions = [];
38
+ if (stage)
39
+ conditions.push(`StageName = '${stage}'`);
40
+ if (accountId)
41
+ conditions.push(`AccountId = '${accountId}'`);
42
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
43
+ const soql = `
44
+ SELECT Id, Name, StageName, Amount, CloseDate, Account.Name, Owner.Name, Probability
45
+ FROM Opportunity
46
+ ${whereClause}
47
+ ORDER BY CloseDate ASC
48
+ LIMIT ${limit}
49
+ `;
50
+ const records = await sfQuery(soql);
51
+ return {
52
+ content: [{ type: "text", text: JSON.stringify(records, null, 2) }],
53
+ };
54
+ });
55
+ server.tool("soql_query", "Execute a custom SOQL query against Salesforce (read-only)", {
56
+ query: z.string().describe("SOQL query to execute"),
57
+ }, async ({ query }) => {
58
+ if (!query.trim().toUpperCase().startsWith("SELECT")) {
59
+ return {
60
+ content: [
61
+ { type: "text", text: "Error: Only SELECT queries are allowed" },
62
+ ],
63
+ isError: true,
64
+ };
65
+ }
66
+ const records = await sfQuery(query);
67
+ return {
68
+ content: [{ type: "text", text: JSON.stringify(records, null, 2) }],
69
+ };
70
+ });
71
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@snokam/mcp-salesforce",
3
- "version": "2.2.0",
3
+ "version": "2.4.0",
4
4
  "description": "MCP server for Salesforce CRM integration with Claude Code",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -14,11 +14,11 @@
14
14
  },
15
15
  "dependencies": {
16
16
  "@modelcontextprotocol/sdk": "^1.29.0",
17
- "zod": "^4.3.6"
17
+ "zod": "^4.4.3"
18
18
  },
19
19
  "devDependencies": {
20
- "@types/node": "^25.5.0",
21
- "tsx": "^4.21.0",
20
+ "@types/node": "^26.1.1",
21
+ "tsx": "^4.23.1",
22
22
  "typescript": "^6.0.2"
23
23
  },
24
24
  "engines": {