@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/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 { z } from "zod";
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
- // Tool: Search Contacts
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
+ }