@mindstone/mcp-server-salesforce 0.1.2
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/LICENSE +97 -0
- package/README.md +80 -0
- package/dist/auth.d.ts +19 -0
- package/dist/auth.js +331 -0
- package/dist/bridge.d.ts +14 -0
- package/dist/bridge.js +37 -0
- package/dist/client.d.ts +23 -0
- package/dist/client.js +118 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +29 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.js +21 -0
- package/dist/tools/accounts.d.ts +3 -0
- package/dist/tools/accounts.js +107 -0
- package/dist/tools/auth-tools.d.ts +3 -0
- package/dist/tools/auth-tools.js +177 -0
- package/dist/tools/contacts.d.ts +3 -0
- package/dist/tools/contacts.js +105 -0
- package/dist/tools/index.d.ts +9 -0
- package/dist/tools/index.js +9 -0
- package/dist/tools/leads.d.ts +3 -0
- package/dist/tools/leads.js +135 -0
- package/dist/tools/opportunities.d.ts +3 -0
- package/dist/tools/opportunities.js +108 -0
- package/dist/tools/query.d.ts +17 -0
- package/dist/tools/query.js +267 -0
- package/dist/tools/tasks.d.ts +3 -0
- package/dist/tools/tasks.js +134 -0
- package/dist/tools/users.d.ts +3 -0
- package/dist/tools/users.js +39 -0
- package/dist/types.d.ts +44 -0
- package/dist/types.js +19 -0
- package/dist/utils.d.ts +62 -0
- package/dist/utils.js +134 -0
- package/package.json +55 -0
package/dist/client.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import jsforce from 'jsforce';
|
|
2
|
+
import { ConnectorError, } from './types.js';
|
|
3
|
+
import { getAuthMode, getActiveToken, loadAccounts, loadToken, saveToken, deleteToken, saveAccounts, } from './auth.js';
|
|
4
|
+
// Connection cache per account
|
|
5
|
+
const connections = new Map();
|
|
6
|
+
function getLoginUrl(tokenData) {
|
|
7
|
+
if (tokenData.login_url)
|
|
8
|
+
return tokenData.login_url;
|
|
9
|
+
const isSandbox = tokenData.instance_url.includes('test.salesforce.com') ||
|
|
10
|
+
tokenData.instance_url.includes('.sandbox.') ||
|
|
11
|
+
tokenData.instance_url.includes('.scratch.');
|
|
12
|
+
return isSandbox
|
|
13
|
+
? 'https://test.salesforce.com'
|
|
14
|
+
: 'https://login.salesforce.com';
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Ensure the connector is in a mode that can make API calls.
|
|
18
|
+
*/
|
|
19
|
+
export function requireAuth() {
|
|
20
|
+
const mode = getAuthMode();
|
|
21
|
+
if (mode === 'unconfigured') {
|
|
22
|
+
throw new ConnectorError('No Salesforce authentication configured', 'UNCONFIGURED', 'Set up authentication: (1) Set SALESFORCE_CLIENT_ID and SALESFORCE_CLIENT_SECRET for OAuth, or (2) Set SALESFORCE_ACCESS_TOKEN and SALESFORCE_INSTANCE_URL for manual token mode. See README for details.');
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Get a jsforce connection for the current account.
|
|
27
|
+
*/
|
|
28
|
+
export async function getConnection(accountId) {
|
|
29
|
+
requireAuth();
|
|
30
|
+
const { accountId: resolvedId, token: tokenData } = accountId
|
|
31
|
+
? { accountId, token: loadToken(accountId) }
|
|
32
|
+
: getActiveToken();
|
|
33
|
+
if (!tokenData) {
|
|
34
|
+
throw new ConnectorError(`Account ${accountId} not found`, 'ACCOUNT_NOT_FOUND', 'Call salesforce_connect_account to connect.');
|
|
35
|
+
}
|
|
36
|
+
// Check cache
|
|
37
|
+
const cached = connections.get(resolvedId);
|
|
38
|
+
if (cached)
|
|
39
|
+
return cached;
|
|
40
|
+
const loginUrl = getLoginUrl(tokenData);
|
|
41
|
+
const clientId = process.env.SALESFORCE_CLIENT_ID;
|
|
42
|
+
const clientSecret = process.env.SALESFORCE_CLIENT_SECRET;
|
|
43
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
44
|
+
const connectionConfig = {
|
|
45
|
+
instanceUrl: tokenData.instance_url,
|
|
46
|
+
accessToken: tokenData.access_token,
|
|
47
|
+
refreshToken: tokenData.refresh_token,
|
|
48
|
+
};
|
|
49
|
+
if (clientId && clientSecret && tokenData.refresh_token) {
|
|
50
|
+
connectionConfig.oauth2 = { clientId, clientSecret, loginUrl };
|
|
51
|
+
}
|
|
52
|
+
const conn = new jsforce.Connection(connectionConfig);
|
|
53
|
+
conn.on('refresh', async (accessToken) => {
|
|
54
|
+
console.error(`[Salesforce MCP] Token refreshed for account ${resolvedId}`);
|
|
55
|
+
const updatedToken = {
|
|
56
|
+
...tokenData,
|
|
57
|
+
access_token: accessToken,
|
|
58
|
+
expires_at: Date.now() + 2 * 60 * 60 * 1000,
|
|
59
|
+
};
|
|
60
|
+
saveToken(resolvedId, updatedToken);
|
|
61
|
+
});
|
|
62
|
+
connections.set(resolvedId, conn);
|
|
63
|
+
return conn;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Execute an operation with a jsforce connection, handling session expiry.
|
|
67
|
+
*/
|
|
68
|
+
export async function withConnection(accountId, operation) {
|
|
69
|
+
requireAuth();
|
|
70
|
+
const { accountId: resolvedId } = accountId
|
|
71
|
+
? { accountId }
|
|
72
|
+
: getActiveToken();
|
|
73
|
+
const conn = await getConnection(resolvedId);
|
|
74
|
+
try {
|
|
75
|
+
return await operation(conn);
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
if (error instanceof Error &&
|
|
79
|
+
(error.message.includes('INVALID_SESSION_ID') ||
|
|
80
|
+
error.message.includes('Session expired'))) {
|
|
81
|
+
// Clear cached connection on session expiry
|
|
82
|
+
connections.delete(resolvedId);
|
|
83
|
+
throw new ConnectorError('Session expired', 'SESSION_EXPIRED', 'Call salesforce_connect_account to re-connect.');
|
|
84
|
+
}
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* List all connected accounts with their status.
|
|
90
|
+
*/
|
|
91
|
+
export function listConnectedAccounts() {
|
|
92
|
+
const config = loadAccounts();
|
|
93
|
+
return config.accounts.map((a) => {
|
|
94
|
+
const token = loadToken(a.id);
|
|
95
|
+
return {
|
|
96
|
+
...a,
|
|
97
|
+
is_sandbox: a.instance_url?.includes('test.salesforce.com') ||
|
|
98
|
+
a.instance_url?.includes('.sandbox.') ||
|
|
99
|
+
false,
|
|
100
|
+
};
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Remove a connected account.
|
|
105
|
+
*/
|
|
106
|
+
export function removeAccount(usernameOrId) {
|
|
107
|
+
const config = loadAccounts();
|
|
108
|
+
const account = config.accounts.find((a) => a.username === usernameOrId || a.id === usernameOrId);
|
|
109
|
+
if (!account) {
|
|
110
|
+
throw new ConnectorError(`Account not found: ${usernameOrId}`, 'ACCOUNT_NOT_FOUND', 'Use salesforce_list_connected_accounts to see connected accounts.');
|
|
111
|
+
}
|
|
112
|
+
connections.delete(account.id);
|
|
113
|
+
deleteToken(account.id);
|
|
114
|
+
config.accounts = config.accounts.filter((a) => a.id !== account.id);
|
|
115
|
+
saveAccounts(config);
|
|
116
|
+
console.error(`[Salesforce MCP] Removed account: ${account.username}`);
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=client.js.map
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Salesforce MCP Server
|
|
4
|
+
*
|
|
5
|
+
* Provides Salesforce CRM integration via Model Context Protocol.
|
|
6
|
+
* Accounts, contacts, opportunities, leads, tasks, users, and custom objects.
|
|
7
|
+
*
|
|
8
|
+
* Environment variables:
|
|
9
|
+
* - SALESFORCE_CLIENT_ID: OAuth client ID (for standalone OAuth mode)
|
|
10
|
+
* - SALESFORCE_CLIENT_SECRET: OAuth client secret (for standalone OAuth mode)
|
|
11
|
+
* - SALESFORCE_ACCESS_TOKEN: Static access token (for manual token mode)
|
|
12
|
+
* - SALESFORCE_INSTANCE_URL: Salesforce instance URL (for manual token mode)
|
|
13
|
+
* - SALESFORCE_CONFIG_DIR: Custom config directory (default: ~/.mcp/salesforce)
|
|
14
|
+
* - SALESFORCE_SANDBOX: Set to "true" for sandbox environments
|
|
15
|
+
* - MCP_HOST_BRIDGE_STATE: Path to host app bridge state file (optional)
|
|
16
|
+
*/
|
|
17
|
+
export {};
|
|
18
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Salesforce MCP Server
|
|
4
|
+
*
|
|
5
|
+
* Provides Salesforce CRM integration via Model Context Protocol.
|
|
6
|
+
* Accounts, contacts, opportunities, leads, tasks, users, and custom objects.
|
|
7
|
+
*
|
|
8
|
+
* Environment variables:
|
|
9
|
+
* - SALESFORCE_CLIENT_ID: OAuth client ID (for standalone OAuth mode)
|
|
10
|
+
* - SALESFORCE_CLIENT_SECRET: OAuth client secret (for standalone OAuth mode)
|
|
11
|
+
* - SALESFORCE_ACCESS_TOKEN: Static access token (for manual token mode)
|
|
12
|
+
* - SALESFORCE_INSTANCE_URL: Salesforce instance URL (for manual token mode)
|
|
13
|
+
* - SALESFORCE_CONFIG_DIR: Custom config directory (default: ~/.mcp/salesforce)
|
|
14
|
+
* - SALESFORCE_SANDBOX: Set to "true" for sandbox environments
|
|
15
|
+
* - MCP_HOST_BRIDGE_STATE: Path to host app bridge state file (optional)
|
|
16
|
+
*/
|
|
17
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
18
|
+
import { createServer } from './server.js';
|
|
19
|
+
async function main() {
|
|
20
|
+
const server = createServer();
|
|
21
|
+
const transport = new StdioServerTransport();
|
|
22
|
+
await server.connect(transport);
|
|
23
|
+
console.error('Salesforce MCP server running on stdio');
|
|
24
|
+
}
|
|
25
|
+
main().catch((error) => {
|
|
26
|
+
console.error('Fatal error:', error);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
});
|
|
29
|
+
//# sourceMappingURL=index.js.map
|
package/dist/server.d.ts
ADDED
package/dist/server.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { SERVER_NAME, SERVER_VERSION } from './types.js';
|
|
3
|
+
import { resolveAuthMode } from './auth.js';
|
|
4
|
+
import { registerAuthTools, registerAccountTools, registerContactTools, registerOpportunityTools, registerLeadTools, registerTaskTools, registerUserTools, registerQueryTools, } from './tools/index.js';
|
|
5
|
+
export function createServer() {
|
|
6
|
+
resolveAuthMode();
|
|
7
|
+
const server = new McpServer({
|
|
8
|
+
name: SERVER_NAME,
|
|
9
|
+
version: SERVER_VERSION,
|
|
10
|
+
});
|
|
11
|
+
registerAuthTools(server);
|
|
12
|
+
registerAccountTools(server);
|
|
13
|
+
registerContactTools(server);
|
|
14
|
+
registerOpportunityTools(server);
|
|
15
|
+
registerLeadTools(server);
|
|
16
|
+
registerTaskTools(server);
|
|
17
|
+
registerUserTools(server);
|
|
18
|
+
registerQueryTools(server);
|
|
19
|
+
return server;
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { withErrorHandling, escapeSOQL, escapeSOQLLike, validateFields, validateAndMergeCustomFields, checkSaveResult } from '../utils.js';
|
|
3
|
+
import { withConnection } from '../client.js';
|
|
4
|
+
export function registerAccountTools(server) {
|
|
5
|
+
server.registerTool('salesforce_get_accounts', {
|
|
6
|
+
description: `Get CRM accounts. Filters: name_contains, industry, account_type. Returns: Id, Name, Industry, Type, Phone, Website, Description. Max 200 records (default: 50).`,
|
|
7
|
+
inputSchema: z.object({
|
|
8
|
+
limit: z.number().int().min(1).max(200).optional().describe('Max results 1-200 (default: 50)'),
|
|
9
|
+
name_contains: z.string().optional().describe('Filter by name (case-insensitive)'),
|
|
10
|
+
industry: z.string().optional().describe('Filter by industry'),
|
|
11
|
+
account_type: z.string().optional().describe('Filter by type (Customer, Partner, Competitor)'),
|
|
12
|
+
fields: z.array(z.string()).optional().describe('Custom fields (must be valid API names)'),
|
|
13
|
+
}),
|
|
14
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
15
|
+
}, withErrorHandling(async (args) => {
|
|
16
|
+
return withConnection(undefined, async (conn) => {
|
|
17
|
+
const defaultFields = ['Id', 'Name', 'Industry', 'Type', 'Phone', 'Website', 'Description'];
|
|
18
|
+
const fields = validateFields(args.fields || [], defaultFields);
|
|
19
|
+
let query = `SELECT ${fields.join(', ')} FROM Account`;
|
|
20
|
+
const conditions = [];
|
|
21
|
+
if (args.name_contains)
|
|
22
|
+
conditions.push(`Name LIKE '%${escapeSOQLLike(args.name_contains)}%'`);
|
|
23
|
+
if (args.industry)
|
|
24
|
+
conditions.push(`Industry = '${escapeSOQL(args.industry)}'`);
|
|
25
|
+
if (args.account_type)
|
|
26
|
+
conditions.push(`Type = '${escapeSOQL(args.account_type)}'`);
|
|
27
|
+
if (conditions.length > 0)
|
|
28
|
+
query += ` WHERE ${conditions.join(' AND ')}`;
|
|
29
|
+
const limit = Math.min(Math.max(1, args.limit ?? 50), 200);
|
|
30
|
+
query += ` LIMIT ${limit}`;
|
|
31
|
+
const result = await conn.query(query);
|
|
32
|
+
return JSON.stringify({ ok: true, records: result.records, totalSize: result.totalSize, done: result.done });
|
|
33
|
+
});
|
|
34
|
+
}));
|
|
35
|
+
server.registerTool('salesforce_create_account', {
|
|
36
|
+
description: `Create a CRM account. Required: name. Optional: industry, type, phone, website, description, fields.`,
|
|
37
|
+
inputSchema: z.object({
|
|
38
|
+
name: z.string().min(1).describe('Account name (required)'),
|
|
39
|
+
industry: z.string().optional().describe('Industry'),
|
|
40
|
+
type: z.string().optional().describe('Account type'),
|
|
41
|
+
phone: z.string().optional().describe('Phone number'),
|
|
42
|
+
website: z.string().optional().describe('Website URL'),
|
|
43
|
+
description: z.string().optional().describe('Account description'),
|
|
44
|
+
fields: z.record(z.unknown()).optional().describe('Additional/custom fields as key-value pairs'),
|
|
45
|
+
}),
|
|
46
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
47
|
+
}, withErrorHandling(async (args) => {
|
|
48
|
+
return withConnection(undefined, async (conn) => {
|
|
49
|
+
const data = { Name: args.name };
|
|
50
|
+
if (args.industry)
|
|
51
|
+
data.Industry = args.industry;
|
|
52
|
+
if (args.type)
|
|
53
|
+
data.Type = args.type;
|
|
54
|
+
if (args.phone)
|
|
55
|
+
data.Phone = args.phone;
|
|
56
|
+
if (args.website)
|
|
57
|
+
data.Website = args.website;
|
|
58
|
+
if (args.description)
|
|
59
|
+
data.Description = args.description;
|
|
60
|
+
if (args.fields)
|
|
61
|
+
validateAndMergeCustomFields(data, args.fields);
|
|
62
|
+
const result = await conn.sobject('Account').create(data);
|
|
63
|
+
if (!result.success) {
|
|
64
|
+
const { ConnectorError } = await import('../types.js');
|
|
65
|
+
throw new ConnectorError('Failed to create account', 'CREATE_ERROR', JSON.stringify(result.errors));
|
|
66
|
+
}
|
|
67
|
+
return JSON.stringify({ ok: true, status: 'success', object: 'Account', id: result.id, name: args.name });
|
|
68
|
+
});
|
|
69
|
+
}));
|
|
70
|
+
server.registerTool('salesforce_update_account', {
|
|
71
|
+
description: `Update a CRM account. Required: id. Updatable: name, industry, type, phone, website, description, fields.`,
|
|
72
|
+
inputSchema: z.object({
|
|
73
|
+
id: z.string().min(1).describe('Salesforce Account ID (required)'),
|
|
74
|
+
name: z.string().optional().describe('Account name'),
|
|
75
|
+
industry: z.string().optional().describe('Industry'),
|
|
76
|
+
type: z.string().optional().describe('Account type'),
|
|
77
|
+
phone: z.string().optional().describe('Phone number'),
|
|
78
|
+
website: z.string().optional().describe('Website URL'),
|
|
79
|
+
description: z.string().optional().describe('Description'),
|
|
80
|
+
fields: z.record(z.unknown()).optional().describe('Additional/custom fields as key-value pairs'),
|
|
81
|
+
}),
|
|
82
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
83
|
+
}, withErrorHandling(async (args) => {
|
|
84
|
+
return withConnection(undefined, async (conn) => {
|
|
85
|
+
const updateData = { Id: args.id };
|
|
86
|
+
if (args.name)
|
|
87
|
+
updateData.Name = args.name;
|
|
88
|
+
if (args.industry)
|
|
89
|
+
updateData.Industry = args.industry;
|
|
90
|
+
if (args.type)
|
|
91
|
+
updateData.Type = args.type;
|
|
92
|
+
if (args.phone)
|
|
93
|
+
updateData.Phone = args.phone;
|
|
94
|
+
if (args.website)
|
|
95
|
+
updateData.Website = args.website;
|
|
96
|
+
if (args.description)
|
|
97
|
+
updateData.Description = args.description;
|
|
98
|
+
if (args.fields)
|
|
99
|
+
validateAndMergeCustomFields(updateData, args.fields);
|
|
100
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
101
|
+
const result = await conn.sobject('Account').update(updateData);
|
|
102
|
+
checkSaveResult(result, 'Failed to update account');
|
|
103
|
+
return JSON.stringify({ ok: true, status: 'success', object: 'Account', id: args.id });
|
|
104
|
+
});
|
|
105
|
+
}));
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=accounts.js.map
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { withErrorHandling } from '../utils.js';
|
|
3
|
+
import { getAuthMode, loadToken, startStandaloneOAuth } from '../auth.js';
|
|
4
|
+
import { bridgeRequest } from '../bridge.js';
|
|
5
|
+
import { listConnectedAccounts, removeAccount } from '../client.js';
|
|
6
|
+
export function registerAuthTools(server) {
|
|
7
|
+
server.registerTool('salesforce_connect_account', {
|
|
8
|
+
description: `Connect a Salesforce account via OAuth. Takes no parameters — call with {}.
|
|
9
|
+
|
|
10
|
+
Initiates OAuth flow — in standalone mode, opens a browser URL for Salesforce sign-in. In bridge mode, delegates to the host app.
|
|
11
|
+
|
|
12
|
+
WHEN TO USE:
|
|
13
|
+
- No Salesforce account is connected
|
|
14
|
+
- Authentication errors from other tools
|
|
15
|
+
- User asks to connect Salesforce
|
|
16
|
+
|
|
17
|
+
After connecting, verify with salesforce_list_connected_accounts.`,
|
|
18
|
+
inputSchema: z.object({}).strict(),
|
|
19
|
+
annotations: {
|
|
20
|
+
readOnlyHint: false,
|
|
21
|
+
destructiveHint: false,
|
|
22
|
+
idempotentHint: false,
|
|
23
|
+
openWorldHint: true,
|
|
24
|
+
},
|
|
25
|
+
}, withErrorHandling(async () => {
|
|
26
|
+
const mode = getAuthMode();
|
|
27
|
+
if (mode === 'unconfigured') {
|
|
28
|
+
return JSON.stringify({
|
|
29
|
+
ok: false,
|
|
30
|
+
error: 'No authentication configured',
|
|
31
|
+
action_required: 'Set SALESFORCE_CLIENT_ID and SALESFORCE_CLIENT_SECRET environment variables for OAuth, or set SALESFORCE_ACCESS_TOKEN and SALESFORCE_INSTANCE_URL for manual token mode. See README for details.',
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
if (mode === 'manual_token') {
|
|
35
|
+
return JSON.stringify({
|
|
36
|
+
ok: true,
|
|
37
|
+
message: 'Manual token mode — no connect flow needed. Your SALESFORCE_ACCESS_TOKEN is already configured.',
|
|
38
|
+
auth_mode: 'manual_token',
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
if (mode === 'bridge') {
|
|
42
|
+
const result = await bridgeRequest(process.env.MCP_BRIDGE_CONFIGURE_ENDPOINT || '/mcp/configure', {});
|
|
43
|
+
if (result.success) {
|
|
44
|
+
return JSON.stringify({
|
|
45
|
+
ok: true,
|
|
46
|
+
status: 'authenticated',
|
|
47
|
+
message: `Successfully connected Salesforce account${result.username ? `: ${result.username}` : ''}`,
|
|
48
|
+
username: result.username,
|
|
49
|
+
next_step: 'You can now use Salesforce tools. Try salesforce_list_connected_accounts to verify.',
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
return JSON.stringify({
|
|
53
|
+
ok: false,
|
|
54
|
+
error: result.error || 'Failed to authenticate with Salesforce',
|
|
55
|
+
action_required: 'Please try calling salesforce_connect_account again.',
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
// standalone_oauth
|
|
59
|
+
const result = await startStandaloneOAuth();
|
|
60
|
+
if (result.success) {
|
|
61
|
+
return JSON.stringify({
|
|
62
|
+
ok: true,
|
|
63
|
+
status: 'authenticated',
|
|
64
|
+
message: `Successfully connected Salesforce account${result.username ? `: ${result.username}` : ''}`,
|
|
65
|
+
username: result.username,
|
|
66
|
+
next_step: 'You can now use Salesforce tools. Try salesforce_list_connected_accounts to verify.',
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
return JSON.stringify({
|
|
70
|
+
ok: false,
|
|
71
|
+
error: result.error || 'Failed to authenticate',
|
|
72
|
+
action_required: 'Please try calling salesforce_connect_account again.',
|
|
73
|
+
});
|
|
74
|
+
}));
|
|
75
|
+
server.registerTool('salesforce_list_connected_accounts', {
|
|
76
|
+
description: `List connected Salesforce accounts. Takes no parameters — call with {}.
|
|
77
|
+
|
|
78
|
+
Call this FIRST before any Salesforce CRM operations to verify authentication.
|
|
79
|
+
This MCP instance operates on a single Salesforce account. If no account is connected, use salesforce_connect_account.`,
|
|
80
|
+
inputSchema: z.object({}).strict(),
|
|
81
|
+
annotations: {
|
|
82
|
+
readOnlyHint: true,
|
|
83
|
+
destructiveHint: false,
|
|
84
|
+
idempotentHint: true,
|
|
85
|
+
openWorldHint: false,
|
|
86
|
+
},
|
|
87
|
+
}, withErrorHandling(async () => {
|
|
88
|
+
const mode = getAuthMode();
|
|
89
|
+
if (mode === 'unconfigured') {
|
|
90
|
+
return JSON.stringify({
|
|
91
|
+
ok: false,
|
|
92
|
+
auth_mode: 'unconfigured',
|
|
93
|
+
accounts: [],
|
|
94
|
+
count: 0,
|
|
95
|
+
message: 'No Salesforce authentication configured.',
|
|
96
|
+
action_required: 'Set SALESFORCE_CLIENT_ID and SALESFORCE_CLIENT_SECRET for OAuth, or SALESFORCE_ACCESS_TOKEN for manual token mode.',
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
if (mode === 'manual_token') {
|
|
100
|
+
const hasToken = !!process.env.SALESFORCE_ACCESS_TOKEN;
|
|
101
|
+
return JSON.stringify({
|
|
102
|
+
ok: true,
|
|
103
|
+
auth_mode: 'manual_token',
|
|
104
|
+
accounts: hasToken
|
|
105
|
+
? [{ username: 'manual-token', status: 'active', instance_url: process.env.SALESFORCE_INSTANCE_URL || '' }]
|
|
106
|
+
: [],
|
|
107
|
+
count: hasToken ? 1 : 0,
|
|
108
|
+
message: hasToken
|
|
109
|
+
? 'Manual token mode — using SALESFORCE_ACCESS_TOKEN.'
|
|
110
|
+
: 'Manual token mode but SALESFORCE_ACCESS_TOKEN is empty.',
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
const accounts = listConnectedAccounts();
|
|
114
|
+
if (accounts.length === 0) {
|
|
115
|
+
return JSON.stringify({
|
|
116
|
+
ok: true,
|
|
117
|
+
auth_mode: mode,
|
|
118
|
+
accounts: [],
|
|
119
|
+
count: 0,
|
|
120
|
+
message: 'No Salesforce accounts connected.',
|
|
121
|
+
action_required: 'Call salesforce_connect_account to connect.',
|
|
122
|
+
next_step: 'salesforce_connect_account',
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
const accountList = accounts.map((a) => {
|
|
126
|
+
const token = loadToken(a.id);
|
|
127
|
+
const hasValidToken = token && (token.expires_at ?? 0) > Date.now();
|
|
128
|
+
const hasRefresh = token && !!token.refresh_token;
|
|
129
|
+
return {
|
|
130
|
+
username: a.username,
|
|
131
|
+
instance_url: a.instance_url,
|
|
132
|
+
is_sandbox: a.is_sandbox,
|
|
133
|
+
status: hasValidToken || hasRefresh ? 'active' : 'expired',
|
|
134
|
+
connected_at: a.connected_at,
|
|
135
|
+
};
|
|
136
|
+
});
|
|
137
|
+
return JSON.stringify({
|
|
138
|
+
ok: true,
|
|
139
|
+
auth_mode: mode,
|
|
140
|
+
accounts: accountList,
|
|
141
|
+
count: accountList.length,
|
|
142
|
+
note: 'This MCP instance operates on a single Salesforce account.',
|
|
143
|
+
});
|
|
144
|
+
}));
|
|
145
|
+
server.registerTool('salesforce_disconnect_account', {
|
|
146
|
+
description: `Disconnect a Salesforce account. Example: { "username": "user@company.com" }
|
|
147
|
+
|
|
148
|
+
Permanently removes stored credentials. Use when switching accounts or troubleshooting.`,
|
|
149
|
+
inputSchema: z.object({
|
|
150
|
+
username: z.string().min(1).describe('Username of the account to disconnect'),
|
|
151
|
+
}),
|
|
152
|
+
annotations: {
|
|
153
|
+
readOnlyHint: false,
|
|
154
|
+
destructiveHint: true,
|
|
155
|
+
idempotentHint: true,
|
|
156
|
+
openWorldHint: false,
|
|
157
|
+
},
|
|
158
|
+
}, withErrorHandling(async (args) => {
|
|
159
|
+
const mode = getAuthMode();
|
|
160
|
+
if (mode === 'unconfigured' || mode === 'manual_token') {
|
|
161
|
+
return JSON.stringify({
|
|
162
|
+
ok: false,
|
|
163
|
+
error: `Cannot disconnect in ${mode} mode.`,
|
|
164
|
+
resolution: mode === 'manual_token'
|
|
165
|
+
? 'Remove the SALESFORCE_ACCESS_TOKEN environment variable to disconnect.'
|
|
166
|
+
: 'No accounts to disconnect — set up authentication first.',
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
removeAccount(args.username);
|
|
170
|
+
return JSON.stringify({
|
|
171
|
+
ok: true,
|
|
172
|
+
message: `Disconnected Salesforce account: ${args.username}`,
|
|
173
|
+
note: 'To reconnect, use salesforce_connect_account.',
|
|
174
|
+
});
|
|
175
|
+
}));
|
|
176
|
+
}
|
|
177
|
+
//# sourceMappingURL=auth-tools.js.map
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { withErrorHandling, escapeSOQL, escapeSOQLLike, validateFields, validateAndMergeCustomFields, checkSaveResult } from '../utils.js';
|
|
3
|
+
import { withConnection } from '../client.js';
|
|
4
|
+
import { ConnectorError } from '../types.js';
|
|
5
|
+
export function registerContactTools(server) {
|
|
6
|
+
server.registerTool('salesforce_get_contacts', {
|
|
7
|
+
description: `Get contacts. Filters: name_contains, email_contains, related_account_id. Returns: Id, FirstName, LastName, Email, Phone, Title, AccountId. Max 200 (default: 50).`,
|
|
8
|
+
inputSchema: z.object({
|
|
9
|
+
limit: z.number().int().min(1).max(200).optional().describe('Max results 1-200 (default: 50)'),
|
|
10
|
+
name_contains: z.string().optional().describe('Filter by first or last name'),
|
|
11
|
+
email_contains: z.string().optional().describe('Filter by email'),
|
|
12
|
+
related_account_id: z.string().optional().describe('Filter by Account ID'),
|
|
13
|
+
fields: z.array(z.string()).optional().describe('Custom fields (valid API names)'),
|
|
14
|
+
}),
|
|
15
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
16
|
+
}, withErrorHandling(async (args) => {
|
|
17
|
+
return withConnection(undefined, async (conn) => {
|
|
18
|
+
const defaultFields = ['Id', 'FirstName', 'LastName', 'Email', 'Phone', 'Title', 'AccountId'];
|
|
19
|
+
const fields = validateFields(args.fields || [], defaultFields);
|
|
20
|
+
let query = `SELECT ${fields.join(', ')} FROM Contact`;
|
|
21
|
+
const conditions = [];
|
|
22
|
+
if (args.name_contains) {
|
|
23
|
+
const escaped = escapeSOQLLike(args.name_contains);
|
|
24
|
+
conditions.push(`(FirstName LIKE '%${escaped}%' OR LastName LIKE '%${escaped}%')`);
|
|
25
|
+
}
|
|
26
|
+
if (args.email_contains)
|
|
27
|
+
conditions.push(`Email LIKE '%${escapeSOQLLike(args.email_contains)}%'`);
|
|
28
|
+
if (args.related_account_id)
|
|
29
|
+
conditions.push(`AccountId = '${escapeSOQL(args.related_account_id)}'`);
|
|
30
|
+
if (conditions.length > 0)
|
|
31
|
+
query += ` WHERE ${conditions.join(' AND ')}`;
|
|
32
|
+
const limit = Math.min(Math.max(1, args.limit ?? 50), 200);
|
|
33
|
+
query += ` LIMIT ${limit}`;
|
|
34
|
+
const result = await conn.query(query);
|
|
35
|
+
return JSON.stringify({ ok: true, records: result.records, totalSize: result.totalSize });
|
|
36
|
+
});
|
|
37
|
+
}));
|
|
38
|
+
server.registerTool('salesforce_create_contact', {
|
|
39
|
+
description: `Create a contact. Required: last_name. Optional: first_name, email, phone, title, related_account_id, fields.`,
|
|
40
|
+
inputSchema: z.object({
|
|
41
|
+
first_name: z.string().optional().describe('First name'),
|
|
42
|
+
last_name: z.string().min(1).describe('Last name (required)'),
|
|
43
|
+
email: z.string().optional().describe('Email address'),
|
|
44
|
+
phone: z.string().optional().describe('Phone number'),
|
|
45
|
+
title: z.string().optional().describe('Job title'),
|
|
46
|
+
related_account_id: z.string().optional().describe('Account ID to associate with'),
|
|
47
|
+
fields: z.record(z.unknown()).optional().describe('Additional/custom fields as key-value pairs'),
|
|
48
|
+
}),
|
|
49
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
50
|
+
}, withErrorHandling(async (args) => {
|
|
51
|
+
return withConnection(undefined, async (conn) => {
|
|
52
|
+
const data = { LastName: args.last_name };
|
|
53
|
+
if (args.first_name)
|
|
54
|
+
data.FirstName = args.first_name;
|
|
55
|
+
if (args.email)
|
|
56
|
+
data.Email = args.email;
|
|
57
|
+
if (args.phone)
|
|
58
|
+
data.Phone = args.phone;
|
|
59
|
+
if (args.title)
|
|
60
|
+
data.Title = args.title;
|
|
61
|
+
if (args.related_account_id)
|
|
62
|
+
data.AccountId = args.related_account_id;
|
|
63
|
+
if (args.fields)
|
|
64
|
+
validateAndMergeCustomFields(data, args.fields);
|
|
65
|
+
const result = await conn.sobject('Contact').create(data);
|
|
66
|
+
if (!result.success)
|
|
67
|
+
throw new ConnectorError('Failed to create contact', 'CREATE_ERROR', JSON.stringify(result.errors));
|
|
68
|
+
return JSON.stringify({ ok: true, status: 'success', object: 'Contact', id: result.id, name: `${args.first_name || ''} ${args.last_name}`.trim() });
|
|
69
|
+
});
|
|
70
|
+
}));
|
|
71
|
+
server.registerTool('salesforce_update_contact', {
|
|
72
|
+
description: `Update a contact. Required: id. Updatable: first_name, last_name, email, phone, title, fields.`,
|
|
73
|
+
inputSchema: z.object({
|
|
74
|
+
id: z.string().min(1).describe('Salesforce Contact ID (required)'),
|
|
75
|
+
first_name: z.string().optional().describe('First name'),
|
|
76
|
+
last_name: z.string().optional().describe('Last name'),
|
|
77
|
+
email: z.string().optional().describe('Email address'),
|
|
78
|
+
phone: z.string().optional().describe('Phone number'),
|
|
79
|
+
title: z.string().optional().describe('Job title'),
|
|
80
|
+
fields: z.record(z.unknown()).optional().describe('Additional/custom fields as key-value pairs'),
|
|
81
|
+
}),
|
|
82
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
83
|
+
}, withErrorHandling(async (args) => {
|
|
84
|
+
return withConnection(undefined, async (conn) => {
|
|
85
|
+
const updateData = { Id: args.id };
|
|
86
|
+
if (args.first_name)
|
|
87
|
+
updateData.FirstName = args.first_name;
|
|
88
|
+
if (args.last_name)
|
|
89
|
+
updateData.LastName = args.last_name;
|
|
90
|
+
if (args.email)
|
|
91
|
+
updateData.Email = args.email;
|
|
92
|
+
if (args.phone)
|
|
93
|
+
updateData.Phone = args.phone;
|
|
94
|
+
if (args.title)
|
|
95
|
+
updateData.Title = args.title;
|
|
96
|
+
if (args.fields)
|
|
97
|
+
validateAndMergeCustomFields(updateData, args.fields);
|
|
98
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
99
|
+
const result = await conn.sobject('Contact').update(updateData);
|
|
100
|
+
checkSaveResult(result, 'Failed to update contact');
|
|
101
|
+
return JSON.stringify({ ok: true, status: 'success', object: 'Contact', id: args.id });
|
|
102
|
+
});
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=contacts.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { registerAuthTools } from './auth-tools.js';
|
|
2
|
+
export { registerAccountTools } from './accounts.js';
|
|
3
|
+
export { registerContactTools } from './contacts.js';
|
|
4
|
+
export { registerOpportunityTools } from './opportunities.js';
|
|
5
|
+
export { registerLeadTools } from './leads.js';
|
|
6
|
+
export { registerTaskTools } from './tasks.js';
|
|
7
|
+
export { registerUserTools } from './users.js';
|
|
8
|
+
export { registerQueryTools } from './query.js';
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { registerAuthTools } from './auth-tools.js';
|
|
2
|
+
export { registerAccountTools } from './accounts.js';
|
|
3
|
+
export { registerContactTools } from './contacts.js';
|
|
4
|
+
export { registerOpportunityTools } from './opportunities.js';
|
|
5
|
+
export { registerLeadTools } from './leads.js';
|
|
6
|
+
export { registerTaskTools } from './tasks.js';
|
|
7
|
+
export { registerUserTools } from './users.js';
|
|
8
|
+
export { registerQueryTools } from './query.js';
|
|
9
|
+
//# sourceMappingURL=index.js.map
|