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