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