@snokam/mcp-salesforce 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.releaserc.cjs ADDED
@@ -0,0 +1,41 @@
1
+ module.exports = {
2
+ branches: [{ name: "main" }],
3
+ tagFormat: "mcp-salesforce-v${version}",
4
+ plugins: [
5
+ [
6
+ "@semantic-release/commit-analyzer",
7
+ {
8
+ releaseRules: [
9
+ { type: "feat", release: "minor" },
10
+ { type: "fix", release: "patch" },
11
+ { type: "perf", release: "patch" },
12
+ { type: "refactor", release: "patch" },
13
+ ],
14
+ },
15
+ ],
16
+ "@semantic-release/release-notes-generator",
17
+ [
18
+ "@semantic-release/npm",
19
+ {
20
+ pkgRoot: ".",
21
+ npmPublish: true,
22
+ access: "public",
23
+ },
24
+ ],
25
+ [
26
+ "@semantic-release/git",
27
+ {
28
+ assets: ["package.json"],
29
+ message:
30
+ "chore(release): mcp-salesforce-v${nextRelease.version} [skip ci]\n\n${nextRelease.notes}",
31
+ },
32
+ ],
33
+ [
34
+ "@semantic-release/github",
35
+ {
36
+ successComment: false,
37
+ releasedLabels: false,
38
+ },
39
+ ],
40
+ ],
41
+ };
package/README.md ADDED
@@ -0,0 +1,69 @@
1
+ # @snokam/salesforce-mcp
2
+
3
+ MCP (Model Context Protocol) server for Salesforce CRM integration. Enables AI agents like Olaf to interact with Salesforce data directly.
4
+
5
+ ## Features
6
+
7
+ - 🔍 **Search contacts, leads, and accounts**
8
+ - 📊 **View opportunities pipeline**
9
+ - 📝 **Create tasks**
10
+ - 🔧 **Execute custom SOQL queries** (read-only)
11
+ - 📋 **Get recent activities**
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ cd backend/apps/salesforce-mcp
17
+ pnpm install
18
+ pnpm build
19
+ ```
20
+
21
+ ## Configuration
22
+
23
+ Uses the same environment variables as the existing `chatgpt-function`:
24
+
25
+ ```env
26
+ SALESFORCE_CONSUMER_KEY=your_connected_app_consumer_key
27
+ SALESFORCE_CONSUMER_SECRET=your_connected_app_consumer_secret
28
+ ```
29
+
30
+ Uses `client_credentials` OAuth flow against `https://snokam.my.salesforce.com`.
31
+
32
+ ## Usage with mcporter
33
+
34
+ Add to your `~/.mcporter/config.yaml`:
35
+
36
+ ```yaml
37
+ servers:
38
+ salesforce:
39
+ command: node
40
+ args:
41
+ - /path/to/monorepo/backend/apps/salesforce-mcp/dist/index.js
42
+ env:
43
+ SALESFORCE_CONSUMER_KEY: ${SALESFORCE_CONSUMER_KEY}
44
+ SALESFORCE_CONSUMER_SECRET: ${SALESFORCE_CONSUMER_SECRET}
45
+ ```
46
+
47
+ ## Available Tools
48
+
49
+ | Tool | Description |
50
+ | --------------------------- | --------------------------------------------------------------- |
51
+ | `search_contacts` | Search contacts by name, email, or company |
52
+ | `search_leads` | Search leads with optional status filter |
53
+ | `search_accounts` | Search accounts/companies with type filter |
54
+ | `get_account` | Get detailed account info with contacts and opportunities |
55
+ | `get_opportunities` | View pipeline with stage/account filters |
56
+ | `get_strategic_contacts` | Get contacts from strategic priority accounts needing follow-up |
57
+ | `get_contact_activities` | Get recent events and tasks for a contact |
58
+ | `get_comprehensive_contact` | Full contact info with account details and activities |
59
+ | `soql_query` | Execute custom SOQL queries (SELECT only) |
60
+
61
+ ## Development
62
+
63
+ ```bash
64
+ pnpm dev # Run with tsx
65
+ ```
66
+
67
+ ## Jira
68
+
69
+ - SNO-3449: Sette opp MCP-server for Salesforce-integrasjon
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,312 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { z } from "zod";
5
+ // Environment variables - same as existing chatgpt-function
6
+ const SF_CONSUMER_KEY = process.env.SALESFORCE_CONSUMER_KEY;
7
+ const SF_CONSUMER_SECRET = process.env.SALESFORCE_CONSUMER_SECRET;
8
+ const SF_INSTANCE_URL = "https://snokam.my.salesforce.com";
9
+ const SF_API_VERSION = "v62.0";
10
+ let accessToken = null;
11
+ async function getAccessToken() {
12
+ if (accessToken) {
13
+ return accessToken;
14
+ }
15
+ if (!SF_CONSUMER_KEY || !SF_CONSUMER_SECRET) {
16
+ throw new Error("SALESFORCE_CONSUMER_KEY and SALESFORCE_CONSUMER_SECRET environment variables are required");
17
+ }
18
+ const params = new URLSearchParams({
19
+ client_id: SF_CONSUMER_KEY,
20
+ client_secret: SF_CONSUMER_SECRET,
21
+ grant_type: "client_credentials",
22
+ });
23
+ const response = await fetch(`${SF_INSTANCE_URL}/services/oauth2/token`, {
24
+ method: "POST",
25
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
26
+ body: params,
27
+ });
28
+ if (!response.ok) {
29
+ const error = await response.text();
30
+ throw new Error(`Failed to get access token: ${response.status} - ${error}`);
31
+ }
32
+ const data = await response.json();
33
+ accessToken = data.access_token;
34
+ return accessToken;
35
+ }
36
+ async function sfQuery(soql) {
37
+ const token = await getAccessToken();
38
+ const url = `${SF_INSTANCE_URL}/services/data/${SF_API_VERSION}/query?q=${encodeURIComponent(soql)}`;
39
+ const response = await fetch(url, {
40
+ headers: { Authorization: `Bearer ${token}` },
41
+ });
42
+ if (!response.ok) {
43
+ const error = await response.text();
44
+ throw new Error(`Salesforce query failed: ${response.status} - ${error}`);
45
+ }
46
+ const data = await response.json();
47
+ return data.records;
48
+ }
49
+ const server = new McpServer({
50
+ name: "salesforce-mcp",
51
+ version: "1.0.0",
52
+ });
53
+ // Tool: Search Contacts
54
+ server.tool("search_contacts", "Search for contacts in Salesforce by name, email, or company", {
55
+ query: z.string().describe("Search query (name, email, or company)"),
56
+ limit: z.number().default(10).describe("Maximum number of results"),
57
+ }, async ({ query, limit }) => {
58
+ const escapedQuery = query.replace(/'/g, "\\'");
59
+ const soql = `
60
+ SELECT Id, Name, Email, Phone, Title, Account.Name, Account.Strategic_Priority__c, LastActivityDate
61
+ FROM Contact
62
+ WHERE Name LIKE '%${escapedQuery}%' OR Email LIKE '%${escapedQuery}%' OR Account.Name LIKE '%${escapedQuery}%'
63
+ ORDER BY LastActivityDate DESC NULLS LAST
64
+ LIMIT ${limit}
65
+ `;
66
+ const records = await sfQuery(soql);
67
+ return {
68
+ content: [{ type: "text", text: JSON.stringify(records, null, 2) }],
69
+ };
70
+ });
71
+ // Tool: Search Leads
72
+ server.tool("search_leads", "Search for leads in Salesforce", {
73
+ query: z.string().describe("Search query"),
74
+ status: z
75
+ .string()
76
+ .optional()
77
+ .describe("Lead status filter (e.g., Open, Working, Closed)"),
78
+ limit: z.number().default(10).describe("Maximum number of results"),
79
+ }, async ({ query, status, limit }) => {
80
+ const escapedQuery = query.replace(/'/g, "\\'");
81
+ let whereClause = `Name LIKE '%${escapedQuery}%' OR Email LIKE '%${escapedQuery}%' OR Company LIKE '%${escapedQuery}%'`;
82
+ if (status) {
83
+ whereClause = `(${whereClause}) AND Status = '${status}'`;
84
+ }
85
+ const soql = `
86
+ SELECT Id, Name, Email, Phone, Company, Status, LeadSource, CreatedDate
87
+ FROM Lead
88
+ WHERE ${whereClause}
89
+ ORDER BY CreatedDate DESC
90
+ LIMIT ${limit}
91
+ `;
92
+ const records = await sfQuery(soql);
93
+ return {
94
+ content: [{ type: "text", text: JSON.stringify(records, null, 2) }],
95
+ };
96
+ });
97
+ // Tool: Get Opportunities
98
+ server.tool("get_opportunities", "Get opportunities from Salesforce pipeline", {
99
+ stage: z
100
+ .string()
101
+ .optional()
102
+ .describe("Filter by stage (e.g., Prospecting, Negotiation, Closed Won)"),
103
+ accountId: z.string().optional().describe("Filter by account ID"),
104
+ limit: z.number().default(20).describe("Maximum number of results"),
105
+ }, async ({ stage, accountId, limit }) => {
106
+ const conditions = [];
107
+ if (stage)
108
+ conditions.push(`StageName = '${stage}'`);
109
+ if (accountId)
110
+ conditions.push(`AccountId = '${accountId}'`);
111
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
112
+ const soql = `
113
+ SELECT Id, Name, StageName, Amount, CloseDate, Account.Name, Owner.Name, Probability
114
+ FROM Opportunity
115
+ ${whereClause}
116
+ ORDER BY CloseDate ASC
117
+ LIMIT ${limit}
118
+ `;
119
+ const records = await sfQuery(soql);
120
+ return {
121
+ content: [{ type: "text", text: JSON.stringify(records, null, 2) }],
122
+ };
123
+ });
124
+ // Tool: Get Account Details
125
+ server.tool("get_account", "Get detailed information about a Salesforce account", {
126
+ accountId: z.string().describe("Salesforce Account ID"),
127
+ }, async ({ accountId }) => {
128
+ const soql = `
129
+ SELECT Id, Name, Industry, Website, Phone, BillingCity, BillingCountry,
130
+ Description, NumberOfEmployees, AnnualRevenue, Owner.Name, Type,
131
+ Strategic_Priority__c, Technology__c, LastActivityDate,
132
+ (SELECT Id, Name, Email, Title FROM Contacts LIMIT 10),
133
+ (SELECT Id, Name, StageName, Amount, CloseDate FROM Opportunities ORDER BY CloseDate DESC LIMIT 5)
134
+ FROM Account
135
+ WHERE Id = '${accountId}'
136
+ `;
137
+ const records = await sfQuery(soql);
138
+ return {
139
+ content: [{ type: "text", text: JSON.stringify(records[0], null, 2) }],
140
+ };
141
+ });
142
+ // Tool: Search Accounts
143
+ server.tool("search_accounts", "Search for accounts/companies in Salesforce", {
144
+ query: z.string().describe("Search query (company name)"),
145
+ type: z
146
+ .string()
147
+ .optional()
148
+ .describe("Account type filter (e.g., Prospect, Customer)"),
149
+ limit: z.number().default(10).describe("Maximum number of results"),
150
+ }, async ({ query, type, limit }) => {
151
+ const escapedQuery = query.replace(/'/g, "\\'");
152
+ let whereClause = `Name LIKE '%${escapedQuery}%'`;
153
+ if (type) {
154
+ whereClause += ` AND Type = '${type}'`;
155
+ }
156
+ const soql = `
157
+ SELECT Id, Name, Industry, Website, Phone, BillingCity, Owner.Name, Type,
158
+ Strategic_Priority__c, Technology__c, LastActivityDate
159
+ FROM Account
160
+ WHERE ${whereClause}
161
+ ORDER BY Name ASC
162
+ LIMIT ${limit}
163
+ `;
164
+ const records = await sfQuery(soql);
165
+ return {
166
+ content: [{ type: "text", text: JSON.stringify(records, null, 2) }],
167
+ };
168
+ });
169
+ // Tool: Get Strategic Contacts (for follow-up)
170
+ server.tool("get_strategic_contacts", "Get contacts from strategic priority accounts that need follow-up", {
171
+ priority: z
172
+ .enum(["A", "B", "C", "D"])
173
+ .default("D")
174
+ .describe("Strategic priority filter"),
175
+ daysInactive: z.number().default(90).describe("Days since last activity"),
176
+ limit: z.number().default(20).describe("Maximum number of results"),
177
+ }, async ({ priority, daysInactive, limit }) => {
178
+ const cutoffDate = new Date(Date.now() - daysInactive * 24 * 60 * 60 * 1000)
179
+ .toISOString()
180
+ .split("T")[0];
181
+ const soql = `
182
+ SELECT Id, Name, Title, Email, Account.Name, Account.Strategic_Priority__c,
183
+ Account.Technology__c, Account.Description, Account.LastActivityDate
184
+ FROM Contact
185
+ WHERE Account.Type = 'Prospect'
186
+ AND Account.Strategic_Priority__c = '${priority}'
187
+ AND Email != null
188
+ AND (Account.LastActivityDate < ${cutoffDate} OR Account.LastActivityDate = null)
189
+ LIMIT ${limit}
190
+ `;
191
+ const records = await sfQuery(soql);
192
+ return {
193
+ content: [{ type: "text", text: JSON.stringify(records, null, 2) }],
194
+ };
195
+ });
196
+ // Tool: Execute SOQL Query
197
+ server.tool("soql_query", "Execute a custom SOQL query against Salesforce (read-only)", {
198
+ query: z.string().describe("SOQL query to execute"),
199
+ }, async ({ query }) => {
200
+ // Basic safety check - only allow SELECT queries
201
+ if (!query.trim().toUpperCase().startsWith("SELECT")) {
202
+ return {
203
+ content: [
204
+ { type: "text", text: "Error: Only SELECT queries are allowed" },
205
+ ],
206
+ isError: true,
207
+ };
208
+ }
209
+ const records = await sfQuery(query);
210
+ return {
211
+ content: [{ type: "text", text: JSON.stringify(records, null, 2) }],
212
+ };
213
+ });
214
+ // Tool: Get Recent Activities for Contact
215
+ server.tool("get_contact_activities", "Get recent activities (events and tasks) for a specific contact", {
216
+ contactId: z.string().describe("Salesforce Contact ID"),
217
+ daysBack: z.number().default(90).describe("Number of days to look back"),
218
+ }, async ({ contactId, daysBack }) => {
219
+ const cutoffDate = new Date(Date.now() - daysBack * 24 * 60 * 60 * 1000).toISOString();
220
+ // Get events
221
+ const eventsSoql = `
222
+ SELECT Id, Subject, Description, ActivityDateTime
223
+ FROM Event
224
+ WHERE WhoId = '${contactId}'
225
+ AND ActivityDateTime > ${cutoffDate}
226
+ ORDER BY ActivityDateTime DESC
227
+ `;
228
+ // Get tasks
229
+ const tasksSoql = `
230
+ SELECT Id, Subject, Description, ActivityDate, Status
231
+ FROM Task
232
+ WHERE WhoId = '${contactId}'
233
+ AND ActivityDate > ${cutoffDate.split("T")[0]}
234
+ ORDER BY ActivityDate DESC
235
+ `;
236
+ const [events, tasks] = await Promise.all([
237
+ sfQuery(eventsSoql),
238
+ sfQuery(tasksSoql),
239
+ ]);
240
+ return {
241
+ content: [
242
+ {
243
+ type: "text",
244
+ text: JSON.stringify({ events, tasks }, null, 2),
245
+ },
246
+ ],
247
+ };
248
+ });
249
+ // Tool: Get Comprehensive Contact Info (like existing chatgpt-function)
250
+ server.tool("get_comprehensive_contact", "Get comprehensive contact information including account details and recent activities", {
251
+ contactId: z.string().describe("Salesforce Contact ID"),
252
+ daysBack: z
253
+ .number()
254
+ .default(90)
255
+ .describe("Number of days to look back for activities"),
256
+ }, async ({ contactId, daysBack }) => {
257
+ // Get contact details
258
+ const contactSoql = `
259
+ SELECT Id, Name, Title, Email, Phone, Description,
260
+ Account.Name, Account.Strategic_Priority__c, Account.Technology__c,
261
+ Account.Description, Account.Industry, Account.Website
262
+ FROM Contact
263
+ WHERE Id = '${contactId}'
264
+ `;
265
+ const contacts = await sfQuery(contactSoql);
266
+ const contact = contacts[0];
267
+ if (!contact) {
268
+ return {
269
+ content: [
270
+ { type: "text", text: `No contact found with ID: ${contactId}` },
271
+ ],
272
+ isError: true,
273
+ };
274
+ }
275
+ const cutoffDate = new Date(Date.now() - daysBack * 24 * 60 * 60 * 1000).toISOString();
276
+ // Get events and tasks
277
+ const eventsSoql = `
278
+ SELECT Id, Subject, Description, ActivityDateTime
279
+ FROM Event
280
+ WHERE WhoId = '${contactId}'
281
+ AND ActivityDateTime > ${cutoffDate}
282
+ ORDER BY ActivityDateTime DESC
283
+ LIMIT 20
284
+ `;
285
+ const tasksSoql = `
286
+ SELECT Id, Subject, Description, ActivityDate, Status
287
+ FROM Task
288
+ WHERE WhoId = '${contactId}'
289
+ AND ActivityDate > ${cutoffDate.split("T")[0]}
290
+ ORDER BY ActivityDate DESC
291
+ LIMIT 20
292
+ `;
293
+ const [events, tasks] = await Promise.all([
294
+ sfQuery(eventsSoql),
295
+ sfQuery(tasksSoql),
296
+ ]);
297
+ return {
298
+ content: [
299
+ {
300
+ type: "text",
301
+ text: JSON.stringify({ contact, events, tasks }, null, 2),
302
+ },
303
+ ],
304
+ };
305
+ });
306
+ // Start the server
307
+ async function main() {
308
+ const transport = new StdioServerTransport();
309
+ await server.connect(transport);
310
+ console.error("Salesforce MCP server running on stdio");
311
+ }
312
+ main().catch(console.error);
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@snokam/mcp-salesforce",
3
+ "version": "1.1.0",
4
+ "description": "MCP server for Salesforce CRM integration",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "salesforce-mcp": "dist/index.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "start": "node dist/index.js",
13
+ "dev": "tsx src/index.ts"
14
+ },
15
+ "dependencies": {
16
+ "@modelcontextprotocol/sdk": "^1.18.0",
17
+ "zod": "^3.23.0"
18
+ },
19
+ "devDependencies": {
20
+ "@types/node": "^22.0.0",
21
+ "tsx": "^4.19.0",
22
+ "typescript": "^5.7.0"
23
+ },
24
+ "engines": {
25
+ "node": ">=18"
26
+ },
27
+ "publishConfig": {
28
+ "access": "public"
29
+ }
30
+ }
package/src/index.ts ADDED
@@ -0,0 +1,400 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
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
+ }
65
+
66
+ const server = new McpServer({
67
+ name: "salesforce-mcp",
68
+ version: "1.0.0",
69
+ });
70
+
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
+ );
392
+
393
+ // Start the server
394
+ async function main() {
395
+ const transport = new StdioServerTransport();
396
+ await server.connect(transport);
397
+ console.error("Salesforce MCP server running on stdio");
398
+ }
399
+
400
+ main().catch(console.error);
package/tsconfig.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "declaration": true,
13
+ "resolveJsonModule": true
14
+ },
15
+ "include": ["src/**/*"],
16
+ "exclude": ["node_modules", "dist"]
17
+ }