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