@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/LICENSE
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Functional Source License, Version 1.1, MIT Future License
|
|
2
|
+
|
|
3
|
+
## Abbreviation
|
|
4
|
+
|
|
5
|
+
FSL-1.1-MIT
|
|
6
|
+
|
|
7
|
+
## Notice
|
|
8
|
+
|
|
9
|
+
Copyright 2026 Mindstone Learning Limited
|
|
10
|
+
|
|
11
|
+
## Terms and Conditions
|
|
12
|
+
|
|
13
|
+
### Licensor ("We")
|
|
14
|
+
|
|
15
|
+
The party offering the Software under these Terms and Conditions.
|
|
16
|
+
|
|
17
|
+
**Licensor**: Mindstone Learning Limited
|
|
18
|
+
|
|
19
|
+
### The Software
|
|
20
|
+
|
|
21
|
+
The "Software" is each version of the software that we make available under
|
|
22
|
+
these Terms and Conditions, as indicated by our inclusion of these Terms and
|
|
23
|
+
Conditions with the Software.
|
|
24
|
+
|
|
25
|
+
**Software**: Salesforce MCP Server
|
|
26
|
+
|
|
27
|
+
### License Grant
|
|
28
|
+
|
|
29
|
+
Subject to your compliance with this License Grant and the Patents,
|
|
30
|
+
Redistribution and Trademark clauses below, we hereby grant you the right to
|
|
31
|
+
use, copy, modify, create derivative works, publicly perform, publicly display
|
|
32
|
+
and redistribute the Software for any Permitted Purpose identified below.
|
|
33
|
+
|
|
34
|
+
### Permitted Purpose
|
|
35
|
+
|
|
36
|
+
A Permitted Purpose is any purpose other than a Competing Use. A "Competing
|
|
37
|
+
Use" means making the Software available to third parties as a commercial
|
|
38
|
+
hosted service that directly competes with any product or service provided by
|
|
39
|
+
the Licensor.
|
|
40
|
+
|
|
41
|
+
### Patents
|
|
42
|
+
|
|
43
|
+
To the extent your use for a Permitted Purpose would necessarily infringe our
|
|
44
|
+
patents, the license grant above includes a license under our patents. If you
|
|
45
|
+
make a claim against any party that the Software infringes or contributes to
|
|
46
|
+
the infringement of any patent, then your patent license to the Software ends
|
|
47
|
+
immediately.
|
|
48
|
+
|
|
49
|
+
### Redistribution
|
|
50
|
+
|
|
51
|
+
The Terms and Conditions apply to all copies, modifications and derivatives of
|
|
52
|
+
the Software.
|
|
53
|
+
|
|
54
|
+
If you redistribute any copies, modifications or derivatives of the Software,
|
|
55
|
+
you must include a copy of or a link to these Terms and Conditions and not
|
|
56
|
+
remove any copyright notices provided in or with the Software.
|
|
57
|
+
|
|
58
|
+
### Disclaimer
|
|
59
|
+
|
|
60
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR
|
|
61
|
+
IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF FITNESS FOR A PARTICULAR
|
|
62
|
+
PURPOSE, MERCHANTABILITY, TITLE OR NON-INFRINGEMENT.
|
|
63
|
+
|
|
64
|
+
IN NO EVENT WILL WE HAVE ANY LIABILITY TO YOU ARISING OUT OF OR RELATED TO THE
|
|
65
|
+
SOFTWARE, INCLUDING INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES, OF
|
|
66
|
+
ANY CHARACTER INCLUDING DAMAGES FOR LOSS OF GOODWILL, LOST PROFITS, LOST SALES
|
|
67
|
+
OR BUSINESS, WORK STOPPAGE, COMPUTER FAILURE OR MALFUNCTION, LOST CONTENT,
|
|
68
|
+
DATA OR DATA USE, BREACH OF DUTY OF GOOD FAITH, OR ANY AND ALL OTHER DAMAGES
|
|
69
|
+
OR LOSSES OF ANY KIND OR NATURE WHATSOEVER (WHETHER DIRECT, INDIRECT, SPECIAL,
|
|
70
|
+
COLLATERAL, INCIDENTAL, CONSEQUENTIAL OR OTHERWISE) ARISING OUT OF OR IN
|
|
71
|
+
CONNECTION WITH THE SOFTWARE OR THIS LICENSE, EVEN IF SUCH PARTY SHALL HAVE
|
|
72
|
+
BEEN INFORMED OF THE POSSIBILITY OF SUCH DAMAGES.
|
|
73
|
+
|
|
74
|
+
### Trademark
|
|
75
|
+
|
|
76
|
+
Except for displaying the License Details and identifying us as the origin of
|
|
77
|
+
the Software, you have no right under these Terms and Conditions to use our
|
|
78
|
+
trademarks, trade names, service marks or product names.
|
|
79
|
+
|
|
80
|
+
## Change Date
|
|
81
|
+
|
|
82
|
+
Four years from the date the Software is made available under these Terms and
|
|
83
|
+
Conditions: **2030-04-08**
|
|
84
|
+
|
|
85
|
+
## Change License
|
|
86
|
+
|
|
87
|
+
MIT License
|
|
88
|
+
|
|
89
|
+
## License Details
|
|
90
|
+
|
|
91
|
+
| Parameter | Value |
|
|
92
|
+
|---|---|
|
|
93
|
+
| Licensor | Mindstone Learning Limited |
|
|
94
|
+
| Software | Salesforce MCP Server |
|
|
95
|
+
| Use Limitation | Competing Use |
|
|
96
|
+
| Change Date | 2030-04-08 |
|
|
97
|
+
| Change License | MIT |
|
package/README.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# @mindstone/mcp-server-salesforce
|
|
2
|
+
|
|
3
|
+
Salesforce CRM MCP server — accounts, contacts, opportunities, leads, tasks, users, and custom objects via the Salesforce API.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx -y @mindstone/mcp-server-salesforce
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Configuration
|
|
12
|
+
|
|
13
|
+
### OAuth (Recommended)
|
|
14
|
+
|
|
15
|
+
Set these environment variables:
|
|
16
|
+
|
|
17
|
+
- `SALESFORCE_CLIENT_ID` — Your Salesforce Connected App client ID
|
|
18
|
+
- `SALESFORCE_CLIENT_SECRET` — Your Salesforce Connected App client secret
|
|
19
|
+
- `SALESFORCE_SANDBOX` — Set to `"true"` for sandbox environments (optional)
|
|
20
|
+
|
|
21
|
+
Then call `salesforce_connect_account` to start the OAuth flow.
|
|
22
|
+
|
|
23
|
+
### Manual Token
|
|
24
|
+
|
|
25
|
+
- `SALESFORCE_ACCESS_TOKEN` — A valid Salesforce access token
|
|
26
|
+
- `SALESFORCE_INSTANCE_URL` — Your Salesforce instance URL (e.g., `https://mycompany.my.salesforce.com`)
|
|
27
|
+
|
|
28
|
+
### Additional Options
|
|
29
|
+
|
|
30
|
+
- `SALESFORCE_CONFIG_DIR` — Custom config directory (default: `~/.mcp/salesforce`)
|
|
31
|
+
|
|
32
|
+
## Available Tools (26)
|
|
33
|
+
|
|
34
|
+
### Account Management
|
|
35
|
+
- `salesforce_connect_account` — Connect a Salesforce account via OAuth
|
|
36
|
+
- `salesforce_list_connected_accounts` — List connected accounts
|
|
37
|
+
- `salesforce_disconnect_account` — Disconnect an account
|
|
38
|
+
|
|
39
|
+
### CRM Accounts
|
|
40
|
+
- `salesforce_get_accounts` — Get CRM accounts with filters
|
|
41
|
+
- `salesforce_create_account` — Create a CRM account
|
|
42
|
+
- `salesforce_update_account` — Update a CRM account
|
|
43
|
+
|
|
44
|
+
### Contacts
|
|
45
|
+
- `salesforce_get_contacts` — Get contacts with filters
|
|
46
|
+
- `salesforce_create_contact` — Create a contact
|
|
47
|
+
- `salesforce_update_contact` — Update a contact
|
|
48
|
+
|
|
49
|
+
### Opportunities
|
|
50
|
+
- `salesforce_get_opportunities` — Get opportunities with filters
|
|
51
|
+
- `salesforce_create_opportunity` — Create an opportunity
|
|
52
|
+
- `salesforce_update_opportunity` — Update an opportunity
|
|
53
|
+
|
|
54
|
+
### Leads
|
|
55
|
+
- `salesforce_get_leads` — Get leads with filters
|
|
56
|
+
- `salesforce_create_lead` — Create a lead
|
|
57
|
+
- `salesforce_convert_lead` — Convert a lead to Account + Contact
|
|
58
|
+
- `salesforce_update_lead` — Update a lead
|
|
59
|
+
|
|
60
|
+
### Tasks
|
|
61
|
+
- `salesforce_get_tasks` — Get tasks with filters
|
|
62
|
+
- `salesforce_create_task` — Create a task
|
|
63
|
+
- `salesforce_update_task` — Update a task
|
|
64
|
+
|
|
65
|
+
### Users
|
|
66
|
+
- `salesforce_get_users` — Get Salesforce users
|
|
67
|
+
|
|
68
|
+
### Query & Schema
|
|
69
|
+
- `salesforce_query` — Execute raw SOQL queries
|
|
70
|
+
- `salesforce_describe_object` — Get object metadata and fields
|
|
71
|
+
- `salesforce_list_objects` — List available Salesforce objects
|
|
72
|
+
|
|
73
|
+
### Generic CRUD
|
|
74
|
+
- `salesforce_create_record` — Create any Salesforce record
|
|
75
|
+
- `salesforce_update_record` — Update any Salesforce record
|
|
76
|
+
- `salesforce_get_records` — Query any Salesforce object
|
|
77
|
+
|
|
78
|
+
## License
|
|
79
|
+
|
|
80
|
+
FSL-1.1-MIT
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type AuthMode, type SalesforceTokenData, type SalesforceAccountsConfig } from './types.js';
|
|
2
|
+
export declare function resolveAuthMode(): AuthMode;
|
|
3
|
+
export declare function getAuthMode(): AuthMode;
|
|
4
|
+
export declare function _resetAuthMode(): void;
|
|
5
|
+
export declare function loadAccounts(): SalesforceAccountsConfig;
|
|
6
|
+
export declare function saveAccounts(config: SalesforceAccountsConfig): void;
|
|
7
|
+
export declare function loadToken(accountId: string): SalesforceTokenData | null;
|
|
8
|
+
export declare function saveToken(accountId: string, token: SalesforceTokenData): void;
|
|
9
|
+
export declare function deleteToken(accountId: string): void;
|
|
10
|
+
export declare function getActiveToken(): {
|
|
11
|
+
accountId: string;
|
|
12
|
+
token: SalesforceTokenData;
|
|
13
|
+
};
|
|
14
|
+
export declare function startStandaloneOAuth(): Promise<{
|
|
15
|
+
success: boolean;
|
|
16
|
+
username?: string;
|
|
17
|
+
error?: string;
|
|
18
|
+
}>;
|
|
19
|
+
//# sourceMappingURL=auth.d.ts.map
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import * as os from 'node:os';
|
|
4
|
+
import * as crypto from 'node:crypto';
|
|
5
|
+
import * as http from 'node:http';
|
|
6
|
+
import { ConnectorError, } from './types.js';
|
|
7
|
+
import { BRIDGE_STATE_PATH } from './bridge.js';
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Auth Mode Detection (resolved ONCE at startup)
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
let resolvedMode = null;
|
|
12
|
+
export function resolveAuthMode() {
|
|
13
|
+
if (resolvedMode !== null)
|
|
14
|
+
return resolvedMode;
|
|
15
|
+
const hasBridge = !!BRIDGE_STATE_PATH;
|
|
16
|
+
const hasClientId = !!process.env.SALESFORCE_CLIENT_ID;
|
|
17
|
+
const hasClientSecret = !!process.env.SALESFORCE_CLIENT_SECRET;
|
|
18
|
+
const hasManualToken = !!process.env.SALESFORCE_ACCESS_TOKEN;
|
|
19
|
+
if (hasBridge) {
|
|
20
|
+
resolvedMode = 'bridge';
|
|
21
|
+
if (hasClientId || hasManualToken) {
|
|
22
|
+
console.error('[Salesforce MCP] WARNING: Bridge state detected alongside other auth env vars. Using bridge mode (highest precedence).');
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
else if (hasClientId && hasClientSecret) {
|
|
26
|
+
resolvedMode = 'standalone_oauth';
|
|
27
|
+
}
|
|
28
|
+
else if (hasManualToken) {
|
|
29
|
+
resolvedMode = 'manual_token';
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
resolvedMode = 'unconfigured';
|
|
33
|
+
}
|
|
34
|
+
console.error(`[Salesforce MCP] Auth mode: ${resolvedMode}`);
|
|
35
|
+
return resolvedMode;
|
|
36
|
+
}
|
|
37
|
+
export function getAuthMode() {
|
|
38
|
+
if (resolvedMode === null)
|
|
39
|
+
return resolveAuthMode();
|
|
40
|
+
return resolvedMode;
|
|
41
|
+
}
|
|
42
|
+
export function _resetAuthMode() {
|
|
43
|
+
resolvedMode = null;
|
|
44
|
+
}
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Config Directory & Token Persistence
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
function getConfigDir() {
|
|
49
|
+
return process.env.SALESFORCE_CONFIG_DIR || path.join(os.homedir(), '.mcp', 'salesforce');
|
|
50
|
+
}
|
|
51
|
+
function getCredentialsDir() {
|
|
52
|
+
return path.join(getConfigDir(), 'credentials');
|
|
53
|
+
}
|
|
54
|
+
function sanitizeFilename(name) {
|
|
55
|
+
return name.replace(/[^a-zA-Z0-9._-]/g, '-');
|
|
56
|
+
}
|
|
57
|
+
function ensureDirectories() {
|
|
58
|
+
const configDir = getConfigDir();
|
|
59
|
+
const credDir = getCredentialsDir();
|
|
60
|
+
if (!fs.existsSync(configDir)) {
|
|
61
|
+
fs.mkdirSync(configDir, { recursive: true, mode: 0o700 });
|
|
62
|
+
}
|
|
63
|
+
if (!fs.existsSync(credDir)) {
|
|
64
|
+
fs.mkdirSync(credDir, { recursive: true, mode: 0o700 });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function getAccountsPath() {
|
|
68
|
+
return path.join(getConfigDir(), 'accounts.json');
|
|
69
|
+
}
|
|
70
|
+
function getTokenPath(accountId) {
|
|
71
|
+
return path.join(getCredentialsDir(), `${sanitizeFilename(accountId)}.token.json`);
|
|
72
|
+
}
|
|
73
|
+
function atomicWriteFile(filePath, data) {
|
|
74
|
+
const dir = path.dirname(filePath);
|
|
75
|
+
if (!fs.existsSync(dir)) {
|
|
76
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
77
|
+
}
|
|
78
|
+
const tmpPath = `${filePath}.${crypto.randomBytes(6).toString('hex')}.tmp`;
|
|
79
|
+
const fd = fs.openSync(tmpPath, 'w', 0o600);
|
|
80
|
+
try {
|
|
81
|
+
fs.writeSync(fd, data);
|
|
82
|
+
fs.fsyncSync(fd);
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
fs.closeSync(fd);
|
|
86
|
+
}
|
|
87
|
+
fs.renameSync(tmpPath, filePath);
|
|
88
|
+
}
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
// Account Management
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
export function loadAccounts() {
|
|
93
|
+
ensureDirectories();
|
|
94
|
+
try {
|
|
95
|
+
const data = fs.readFileSync(getAccountsPath(), 'utf-8');
|
|
96
|
+
return JSON.parse(data);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return { accounts: [] };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
export function saveAccounts(config) {
|
|
103
|
+
ensureDirectories();
|
|
104
|
+
atomicWriteFile(getAccountsPath(), JSON.stringify(config, null, 2));
|
|
105
|
+
}
|
|
106
|
+
export function loadToken(accountId) {
|
|
107
|
+
try {
|
|
108
|
+
const data = fs.readFileSync(getTokenPath(accountId), 'utf-8');
|
|
109
|
+
return JSON.parse(data);
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
export function saveToken(accountId, token) {
|
|
116
|
+
ensureDirectories();
|
|
117
|
+
atomicWriteFile(getTokenPath(accountId), JSON.stringify(token, null, 2));
|
|
118
|
+
}
|
|
119
|
+
export function deleteToken(accountId) {
|
|
120
|
+
try {
|
|
121
|
+
fs.unlinkSync(getTokenPath(accountId));
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
// Token file may not exist
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
// Active Token Resolution
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
export function getActiveToken() {
|
|
131
|
+
const mode = getAuthMode();
|
|
132
|
+
if (mode === 'manual_token') {
|
|
133
|
+
const accessToken = process.env.SALESFORCE_ACCESS_TOKEN || '';
|
|
134
|
+
const instanceUrl = process.env.SALESFORCE_INSTANCE_URL || 'https://login.salesforce.com';
|
|
135
|
+
return {
|
|
136
|
+
accountId: 'manual',
|
|
137
|
+
token: {
|
|
138
|
+
access_token: accessToken,
|
|
139
|
+
instance_url: instanceUrl,
|
|
140
|
+
expires_at: Number.MAX_SAFE_INTEGER,
|
|
141
|
+
username: 'manual-token',
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
if (mode === 'unconfigured') {
|
|
146
|
+
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.');
|
|
147
|
+
}
|
|
148
|
+
const config = loadAccounts();
|
|
149
|
+
if (config.accounts.length === 0) {
|
|
150
|
+
throw new ConnectorError('No Salesforce accounts connected', 'NO_ACCOUNT', 'Call salesforce_connect_account to connect your Salesforce account.');
|
|
151
|
+
}
|
|
152
|
+
const accountId = config.accounts[0].id;
|
|
153
|
+
const token = loadToken(accountId);
|
|
154
|
+
if (!token) {
|
|
155
|
+
throw new ConnectorError('Salesforce credentials not found', 'NO_CREDENTIALS', 'Call salesforce_connect_account to reconnect.');
|
|
156
|
+
}
|
|
157
|
+
return { accountId, token };
|
|
158
|
+
}
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
// Standalone OAuth Flow
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
let authInProgress = false;
|
|
163
|
+
export async function startStandaloneOAuth() {
|
|
164
|
+
if (authInProgress) {
|
|
165
|
+
return { success: false, error: 'Authentication already in progress. Please complete the current flow first.' };
|
|
166
|
+
}
|
|
167
|
+
const clientId = process.env.SALESFORCE_CLIENT_ID;
|
|
168
|
+
const clientSecret = process.env.SALESFORCE_CLIENT_SECRET;
|
|
169
|
+
if (!clientId || !clientSecret) {
|
|
170
|
+
return {
|
|
171
|
+
success: false,
|
|
172
|
+
error: 'SALESFORCE_CLIENT_ID and SALESFORCE_CLIENT_SECRET must be set for standalone OAuth.',
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
authInProgress = true;
|
|
176
|
+
try {
|
|
177
|
+
const result = await runOAuthCallbackServer(clientId, clientSecret);
|
|
178
|
+
return result;
|
|
179
|
+
}
|
|
180
|
+
finally {
|
|
181
|
+
authInProgress = false;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
async function runOAuthCallbackServer(clientId, clientSecret) {
|
|
185
|
+
const state = crypto.randomBytes(32).toString('hex');
|
|
186
|
+
const codeVerifier = crypto.randomBytes(32).toString('base64url');
|
|
187
|
+
const codeChallenge = crypto.createHash('sha256').update(codeVerifier).digest('base64url');
|
|
188
|
+
const port = parseInt(process.env.SALESFORCE_OAUTH_PORT || '0', 10);
|
|
189
|
+
// Hard-coded loopback bind. We deliberately do NOT honour an env override
|
|
190
|
+
// (previously `MCP_OAUTH_BIND_HOST`): an OAuth callback server with a
|
|
191
|
+
// short-lived auth code is a privilege-bearing local service and must
|
|
192
|
+
// never be exposed beyond loopback. (M3.2 — VAL-SALESFORCE-017..019.)
|
|
193
|
+
const bindHost = '127.0.0.1';
|
|
194
|
+
const isSandbox = process.env.SALESFORCE_SANDBOX === 'true';
|
|
195
|
+
const loginBase = isSandbox ? 'https://test.salesforce.com' : 'https://login.salesforce.com';
|
|
196
|
+
const timeoutMs = 5 * 60 * 1000;
|
|
197
|
+
return new Promise((resolve) => {
|
|
198
|
+
const server = http.createServer(async (req, res) => {
|
|
199
|
+
if (!req.url?.startsWith('/callback')) {
|
|
200
|
+
res.writeHead(404);
|
|
201
|
+
res.end('Not found');
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const url = new URL(req.url, `http://${bindHost}:${actualPort}`);
|
|
205
|
+
const code = url.searchParams.get('code');
|
|
206
|
+
const returnedState = url.searchParams.get('state');
|
|
207
|
+
const error = url.searchParams.get('error');
|
|
208
|
+
if (error) {
|
|
209
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
210
|
+
res.end('<html><body><h2>Authentication failed</h2><p>You can close this window.</p></body></html>');
|
|
211
|
+
cleanup();
|
|
212
|
+
resolve({ success: false, error: `OAuth error: ${error}` });
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (returnedState !== state) {
|
|
216
|
+
res.writeHead(400, { 'Content-Type': 'text/html' });
|
|
217
|
+
res.end('<html><body><h2>Invalid state parameter</h2><p>Please try again.</p></body></html>');
|
|
218
|
+
cleanup();
|
|
219
|
+
resolve({ success: false, error: 'OAuth state mismatch — possible CSRF attack.' });
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (!code) {
|
|
223
|
+
res.writeHead(400, { 'Content-Type': 'text/html' });
|
|
224
|
+
res.end('<html><body><h2>Missing authorization code</h2><p>Please try again.</p></body></html>');
|
|
225
|
+
cleanup();
|
|
226
|
+
resolve({ success: false, error: 'No authorization code received.' });
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
try {
|
|
230
|
+
const redirectUri = `http://localhost:${actualPort}/callback`;
|
|
231
|
+
const tokenResponse = await fetch(`${loginBase}/services/oauth2/token`, {
|
|
232
|
+
method: 'POST',
|
|
233
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
234
|
+
body: new URLSearchParams({
|
|
235
|
+
grant_type: 'authorization_code',
|
|
236
|
+
client_id: clientId,
|
|
237
|
+
client_secret: clientSecret,
|
|
238
|
+
redirect_uri: redirectUri,
|
|
239
|
+
code,
|
|
240
|
+
code_verifier: codeVerifier,
|
|
241
|
+
}).toString(),
|
|
242
|
+
});
|
|
243
|
+
if (!tokenResponse.ok) {
|
|
244
|
+
const errText = await tokenResponse.text();
|
|
245
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
246
|
+
res.end('<html><body><h2>Token exchange failed</h2><p>You can close this window.</p></body></html>');
|
|
247
|
+
cleanup();
|
|
248
|
+
resolve({ success: false, error: `Token exchange failed: ${errText}` });
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const tokenData = (await tokenResponse.json());
|
|
252
|
+
const username = tokenData.id?.split('/').pop() || `user-${Date.now()}`;
|
|
253
|
+
const accountId = sanitizeFilename(username);
|
|
254
|
+
const instanceUrl = tokenData.instance_url;
|
|
255
|
+
const token = {
|
|
256
|
+
access_token: tokenData.access_token,
|
|
257
|
+
refresh_token: tokenData.refresh_token,
|
|
258
|
+
instance_url: instanceUrl,
|
|
259
|
+
issued_at: tokenData.issued_at,
|
|
260
|
+
username,
|
|
261
|
+
expires_at: Date.now() + 2 * 60 * 60 * 1000,
|
|
262
|
+
login_url: loginBase,
|
|
263
|
+
};
|
|
264
|
+
saveToken(accountId, token);
|
|
265
|
+
const config = loadAccounts();
|
|
266
|
+
const existingIdx = config.accounts.findIndex((a) => a.id === accountId);
|
|
267
|
+
const account = {
|
|
268
|
+
id: accountId,
|
|
269
|
+
username,
|
|
270
|
+
instance_url: instanceUrl,
|
|
271
|
+
is_sandbox: isSandbox,
|
|
272
|
+
connected_at: new Date().toISOString(),
|
|
273
|
+
};
|
|
274
|
+
if (existingIdx >= 0) {
|
|
275
|
+
config.accounts[existingIdx] = account;
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
config.accounts.push(account);
|
|
279
|
+
}
|
|
280
|
+
saveAccounts(config);
|
|
281
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
282
|
+
res.end('<html><body><h2>Authentication successful!</h2><p>You can close this window.</p></body></html>');
|
|
283
|
+
cleanup();
|
|
284
|
+
resolve({ success: true, username });
|
|
285
|
+
}
|
|
286
|
+
catch (err) {
|
|
287
|
+
res.writeHead(500, { 'Content-Type': 'text/html' });
|
|
288
|
+
res.end('<html><body><h2>Authentication error</h2><p>You can close this window.</p></body></html>');
|
|
289
|
+
cleanup();
|
|
290
|
+
resolve({
|
|
291
|
+
success: false,
|
|
292
|
+
error: `Token exchange error: ${err instanceof Error ? err.message : String(err)}`,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
let actualPort = port;
|
|
297
|
+
let timeoutHandle;
|
|
298
|
+
const cleanup = () => {
|
|
299
|
+
clearTimeout(timeoutHandle);
|
|
300
|
+
server.close();
|
|
301
|
+
};
|
|
302
|
+
server.listen(port, bindHost, () => {
|
|
303
|
+
const addr = server.address();
|
|
304
|
+
if (addr && typeof addr !== 'string') {
|
|
305
|
+
actualPort = addr.port;
|
|
306
|
+
}
|
|
307
|
+
const redirectUri = `http://localhost:${actualPort}/callback`;
|
|
308
|
+
const scopes = process.env.SALESFORCE_OAUTH_SCOPES || 'api refresh_token';
|
|
309
|
+
const authorizeUrl = `${loginBase}/services/oauth2/authorize?` +
|
|
310
|
+
new URLSearchParams({
|
|
311
|
+
client_id: clientId,
|
|
312
|
+
redirect_uri: redirectUri,
|
|
313
|
+
response_type: 'code',
|
|
314
|
+
scope: scopes,
|
|
315
|
+
state,
|
|
316
|
+
code_challenge: codeChallenge,
|
|
317
|
+
code_challenge_method: 'S256',
|
|
318
|
+
}).toString();
|
|
319
|
+
console.error(`[Salesforce MCP] Open this URL to authenticate:\n${authorizeUrl}`);
|
|
320
|
+
});
|
|
321
|
+
timeoutHandle = setTimeout(() => {
|
|
322
|
+
cleanup();
|
|
323
|
+
resolve({ success: false, error: 'OAuth flow timed out after 5 minutes.' });
|
|
324
|
+
}, timeoutMs);
|
|
325
|
+
server.on('error', (err) => {
|
|
326
|
+
cleanup();
|
|
327
|
+
resolve({ success: false, error: `OAuth server error: ${err.message}` });
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
//# sourceMappingURL=auth.js.map
|
package/dist/bridge.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path to bridge state file, supporting both current and legacy env vars.
|
|
3
|
+
*/
|
|
4
|
+
export declare const BRIDGE_STATE_PATH: string;
|
|
5
|
+
/**
|
|
6
|
+
* Send a request to the host app bridge.
|
|
7
|
+
*/
|
|
8
|
+
export declare const bridgeRequest: (urlPath: string, body?: Record<string, unknown>) => Promise<{
|
|
9
|
+
success: boolean;
|
|
10
|
+
username?: string;
|
|
11
|
+
warning?: string;
|
|
12
|
+
error?: string;
|
|
13
|
+
}>;
|
|
14
|
+
//# sourceMappingURL=bridge.d.ts.map
|
package/dist/bridge.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import { REQUEST_TIMEOUT_MS } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Path to bridge state file, supporting both current and legacy env vars.
|
|
5
|
+
*/
|
|
6
|
+
export const BRIDGE_STATE_PATH = process.env.MCP_HOST_BRIDGE_STATE || process.env.MINDSTONE_REBEL_BRIDGE_STATE || '';
|
|
7
|
+
const loadBridgeState = () => {
|
|
8
|
+
if (!BRIDGE_STATE_PATH)
|
|
9
|
+
return null;
|
|
10
|
+
try {
|
|
11
|
+
const raw = fs.readFileSync(BRIDGE_STATE_PATH, 'utf8');
|
|
12
|
+
return JSON.parse(raw);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Send a request to the host app bridge.
|
|
20
|
+
*/
|
|
21
|
+
export const bridgeRequest = async (urlPath, body = {}) => {
|
|
22
|
+
const bridge = loadBridgeState();
|
|
23
|
+
if (!bridge) {
|
|
24
|
+
return { success: false, error: 'Bridge not available' };
|
|
25
|
+
}
|
|
26
|
+
const response = await fetch(`http://127.0.0.1:${bridge.port}${urlPath}`, {
|
|
27
|
+
method: 'POST',
|
|
28
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
29
|
+
headers: {
|
|
30
|
+
'Content-Type': 'application/json',
|
|
31
|
+
Authorization: `Bearer ${bridge.token}`,
|
|
32
|
+
},
|
|
33
|
+
body: JSON.stringify(body),
|
|
34
|
+
});
|
|
35
|
+
return response.json();
|
|
36
|
+
};
|
|
37
|
+
//# sourceMappingURL=bridge.js.map
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import jsforce from 'jsforce';
|
|
2
|
+
import { type SalesforceAccount } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Ensure the connector is in a mode that can make API calls.
|
|
5
|
+
*/
|
|
6
|
+
export declare function requireAuth(): void;
|
|
7
|
+
/**
|
|
8
|
+
* Get a jsforce connection for the current account.
|
|
9
|
+
*/
|
|
10
|
+
export declare function getConnection(accountId?: string): Promise<jsforce.Connection>;
|
|
11
|
+
/**
|
|
12
|
+
* Execute an operation with a jsforce connection, handling session expiry.
|
|
13
|
+
*/
|
|
14
|
+
export declare function withConnection<T>(accountId: string | undefined, operation: (conn: jsforce.Connection) => Promise<T>): Promise<T>;
|
|
15
|
+
/**
|
|
16
|
+
* List all connected accounts with their status.
|
|
17
|
+
*/
|
|
18
|
+
export declare function listConnectedAccounts(): SalesforceAccount[];
|
|
19
|
+
/**
|
|
20
|
+
* Remove a connected account.
|
|
21
|
+
*/
|
|
22
|
+
export declare function removeAccount(usernameOrId: string): void;
|
|
23
|
+
//# sourceMappingURL=client.d.ts.map
|