@mindstone/mcp-server-salesforce 0.2.0 → 0.2.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/README.md +1 -1
- package/dist/bridge.d.ts +6 -1
- package/dist/bridge.js +7 -2
- package/dist/tools/auth-tools.js +53 -13
- package/dist/types.d.ts +11 -0
- package/dist/types.js +11 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ Salesforce CRM MCP server — accounts, contacts, opportunities, leads, tasks, u
|
|
|
9
9
|
|
|
10
10
|
## Status
|
|
11
11
|
|
|
12
|
-
- **Version:** [0.2.
|
|
12
|
+
- **Version:** [0.2.2](./CHANGELOG.md) · [npm](https://www.npmjs.com/package/@mindstone/mcp-server-salesforce)
|
|
13
13
|
- **Auth:** OAuth (local 127.0.0.1 callback) or static access token ([`SALESFORCE_CLIENT_SECRET`](./server.json), [`SALESFORCE_ACCESS_TOKEN`](./server.json))
|
|
14
14
|
- **Tools:** [37](./src/tools/) (accounts, contacts, opportunities, leads, tasks, cases, events, search, notes, campaigns, reports, query)
|
|
15
15
|
- **Surface:** cloud-api
|
package/dist/bridge.d.ts
CHANGED
|
@@ -4,8 +4,13 @@
|
|
|
4
4
|
export declare const BRIDGE_STATE_PATH: string;
|
|
5
5
|
/**
|
|
6
6
|
* Send a request to the host app bridge.
|
|
7
|
+
*
|
|
8
|
+
* `timeoutMs` defaults to the ordinary per-call budget (REQUEST_TIMEOUT_MS);
|
|
9
|
+
* callers that trigger long host-side flows (e.g. interactive OAuth) can opt
|
|
10
|
+
* into a longer one. The abort itself is never removed — only widened — so a
|
|
11
|
+
* dead bridge still fails within the chosen bound.
|
|
7
12
|
*/
|
|
8
|
-
export declare const bridgeRequest: (urlPath: string, body?: Record<string, unknown
|
|
13
|
+
export declare const bridgeRequest: (urlPath: string, body?: Record<string, unknown>, timeoutMs?: number) => Promise<{
|
|
9
14
|
success: boolean;
|
|
10
15
|
username?: string;
|
|
11
16
|
warning?: string;
|
package/dist/bridge.js
CHANGED
|
@@ -17,15 +17,20 @@ const loadBridgeState = () => {
|
|
|
17
17
|
};
|
|
18
18
|
/**
|
|
19
19
|
* Send a request to the host app bridge.
|
|
20
|
+
*
|
|
21
|
+
* `timeoutMs` defaults to the ordinary per-call budget (REQUEST_TIMEOUT_MS);
|
|
22
|
+
* callers that trigger long host-side flows (e.g. interactive OAuth) can opt
|
|
23
|
+
* into a longer one. The abort itself is never removed — only widened — so a
|
|
24
|
+
* dead bridge still fails within the chosen bound.
|
|
20
25
|
*/
|
|
21
|
-
export const bridgeRequest = async (urlPath, body = {}) => {
|
|
26
|
+
export const bridgeRequest = async (urlPath, body = {}, timeoutMs = REQUEST_TIMEOUT_MS) => {
|
|
22
27
|
const bridge = loadBridgeState();
|
|
23
28
|
if (!bridge) {
|
|
24
29
|
return { success: false, error: 'Bridge not available' };
|
|
25
30
|
}
|
|
26
31
|
const response = await fetch(`http://127.0.0.1:${bridge.port}${urlPath}`, {
|
|
27
32
|
method: 'POST',
|
|
28
|
-
signal: AbortSignal.timeout(
|
|
33
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
29
34
|
headers: {
|
|
30
35
|
'Content-Type': 'application/json',
|
|
31
36
|
Authorization: `Bearer ${bridge.token}`,
|
package/dist/tools/auth-tools.js
CHANGED
|
@@ -1,8 +1,20 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
1
2
|
import { z } from 'zod';
|
|
2
|
-
import { withErrorHandling } from '../utils.js';
|
|
3
|
+
import { withErrorHandling, sanitizeExternalData } from '../utils.js';
|
|
3
4
|
import { getAuthMode, loadToken, startStandaloneOAuth } from '../auth.js';
|
|
4
5
|
import { bridgeRequest } from '../bridge.js';
|
|
6
|
+
import { BRIDGE_OAUTH_TIMEOUT_MS } from '../types.js';
|
|
5
7
|
import { listConnectedAccounts, removeAccount } from '../client.js';
|
|
8
|
+
// Disconnect needs a raw handle the model can copy verbatim into
|
|
9
|
+
// salesforce_disconnect_account, but the storage id is username-derived
|
|
10
|
+
// (sanitizeFilename) — passing it through raw would re-open the very channel
|
|
11
|
+
// the envelopes below close. Instead the account list exposes a
|
|
12
|
+
// connector-authored `ref` (truncated hash of the storage id): stable across
|
|
13
|
+
// calls, shaped like data, and containing zero external text.
|
|
14
|
+
function accountRef(accountId) {
|
|
15
|
+
return `acct_${createHash('sha256').update(String(accountId)).digest('hex').slice(0, 12)}`;
|
|
16
|
+
}
|
|
17
|
+
const ACCOUNT_REF_SHAPE = /^acct_[0-9a-f]{12}$/;
|
|
6
18
|
export function registerAuthTools(server) {
|
|
7
19
|
server.registerTool('salesforce_connect_account', {
|
|
8
20
|
description: `Connect a Salesforce account via OAuth. Takes no parameters — call with {}.
|
|
@@ -39,36 +51,46 @@ After connecting, verify with salesforce_list_connected_accounts.`,
|
|
|
39
51
|
});
|
|
40
52
|
}
|
|
41
53
|
if (mode === 'bridge') {
|
|
42
|
-
|
|
54
|
+
// The host holds this response open until the user completes the
|
|
55
|
+
// interactive OAuth flow in their browser (up to 5 minutes), so this
|
|
56
|
+
// call needs the long budget — the default 30s abort would kill the
|
|
57
|
+
// connect mid-flow while the human is still signing in.
|
|
58
|
+
const result = await bridgeRequest(process.env.MCP_BRIDGE_CONFIGURE_ENDPOINT || '/mcp/configure', {}, BRIDGE_OAUTH_TIMEOUT_MS);
|
|
43
59
|
if (result.success) {
|
|
60
|
+
// The bridge username is host/org-authored external text: envelope
|
|
61
|
+
// it before it reaches model-visible output (AGENTS.md invariant #6).
|
|
62
|
+
const username = result.username ? sanitizeExternalData(result.username, 'salesforce:auth') : undefined;
|
|
44
63
|
return JSON.stringify({
|
|
45
64
|
ok: true,
|
|
46
65
|
status: 'authenticated',
|
|
47
|
-
message: `Successfully connected Salesforce account${
|
|
48
|
-
username
|
|
66
|
+
message: `Successfully connected Salesforce account${username ? `: ${username}` : ''}`,
|
|
67
|
+
username,
|
|
49
68
|
next_step: 'You can now use Salesforce tools. Try salesforce_list_connected_accounts to verify.',
|
|
50
69
|
});
|
|
51
70
|
}
|
|
52
71
|
return JSON.stringify({
|
|
53
72
|
ok: false,
|
|
54
|
-
error: result.error
|
|
73
|
+
error: result.error
|
|
74
|
+
? sanitizeExternalData(result.error, 'salesforce:auth')
|
|
75
|
+
: 'Failed to authenticate with Salesforce',
|
|
55
76
|
action_required: 'Please try calling salesforce_connect_account again.',
|
|
56
77
|
});
|
|
57
78
|
}
|
|
58
79
|
// standalone_oauth
|
|
59
80
|
const result = await startStandaloneOAuth();
|
|
60
81
|
if (result.success) {
|
|
82
|
+
const username = result.username ? sanitizeExternalData(result.username, 'salesforce:auth') : undefined;
|
|
61
83
|
return JSON.stringify({
|
|
62
84
|
ok: true,
|
|
63
85
|
status: 'authenticated',
|
|
64
|
-
message: `Successfully connected Salesforce account${
|
|
65
|
-
username
|
|
86
|
+
message: `Successfully connected Salesforce account${username ? `: ${username}` : ''}`,
|
|
87
|
+
username,
|
|
66
88
|
next_step: 'You can now use Salesforce tools. Try salesforce_list_connected_accounts to verify.',
|
|
67
89
|
});
|
|
68
90
|
}
|
|
69
91
|
return JSON.stringify({
|
|
70
92
|
ok: false,
|
|
71
|
-
error: result.error
|
|
93
|
+
error: result.error ? sanitizeExternalData(result.error, 'salesforce:auth') : 'Failed to authenticate',
|
|
72
94
|
action_required: 'Please try calling salesforce_connect_account again.',
|
|
73
95
|
});
|
|
74
96
|
}));
|
|
@@ -127,11 +149,15 @@ This MCP instance operates on a single Salesforce account. If no account is conn
|
|
|
127
149
|
const hasValidToken = token && (token.expires_at ?? 0) > Date.now();
|
|
128
150
|
const hasRefresh = token && !!token.refresh_token;
|
|
129
151
|
return {
|
|
130
|
-
|
|
131
|
-
|
|
152
|
+
// Connector-authored raw handle for salesforce_disconnect_account
|
|
153
|
+
// (the storage id stays internal — it is username-derived and would
|
|
154
|
+
// re-open the channel the envelopes below close).
|
|
155
|
+
ref: accountRef(a.id),
|
|
156
|
+
username: a.username ? sanitizeExternalData(a.username, 'salesforce:auth') : a.username,
|
|
157
|
+
instance_url: a.instance_url ? sanitizeExternalData(a.instance_url, 'salesforce:auth') : a.instance_url,
|
|
132
158
|
is_sandbox: a.is_sandbox,
|
|
133
159
|
status: hasValidToken || hasRefresh ? 'active' : 'expired',
|
|
134
|
-
connected_at: a.connected_at,
|
|
160
|
+
connected_at: a.connected_at ? sanitizeExternalData(a.connected_at, 'salesforce:auth') : a.connected_at,
|
|
135
161
|
};
|
|
136
162
|
});
|
|
137
163
|
return JSON.stringify({
|
|
@@ -145,9 +171,10 @@ This MCP instance operates on a single Salesforce account. If no account is conn
|
|
|
145
171
|
server.registerTool('salesforce_disconnect_account', {
|
|
146
172
|
description: `Disconnect a Salesforce account. Example: { "username": "user@company.com" }
|
|
147
173
|
|
|
174
|
+
Accepts the account \`ref\` from salesforce_list_connected_accounts as well as the raw username.
|
|
148
175
|
Permanently removes stored credentials. Use when switching accounts or troubleshooting.`,
|
|
149
176
|
inputSchema: z.object({
|
|
150
|
-
username: z.string().min(1).describe('Username of the account to disconnect'),
|
|
177
|
+
username: z.string().min(1).describe('Username or account ref of the account to disconnect'),
|
|
151
178
|
}),
|
|
152
179
|
annotations: {
|
|
153
180
|
readOnlyHint: false,
|
|
@@ -166,7 +193,20 @@ Permanently removes stored credentials. Use when switching accounts or troublesh
|
|
|
166
193
|
: 'No accounts to disconnect — set up authentication first.',
|
|
167
194
|
});
|
|
168
195
|
}
|
|
169
|
-
|
|
196
|
+
// Resolve a connector-authored `ref` back to the storage id.
|
|
197
|
+
let usernameOrId = args.username;
|
|
198
|
+
if (ACCOUNT_REF_SHAPE.test(usernameOrId)) {
|
|
199
|
+
const match = listConnectedAccounts().find((a) => accountRef(a.id) === usernameOrId);
|
|
200
|
+
if (!match) {
|
|
201
|
+
return JSON.stringify({
|
|
202
|
+
ok: false,
|
|
203
|
+
error: `Account not found: ${usernameOrId}`,
|
|
204
|
+
resolution: 'Use salesforce_list_connected_accounts to see connected accounts.',
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
usernameOrId = match.id;
|
|
208
|
+
}
|
|
209
|
+
removeAccount(usernameOrId);
|
|
170
210
|
return JSON.stringify({
|
|
171
211
|
ok: true,
|
|
172
212
|
message: `Disconnected Salesforce account: ${args.username}`,
|
package/dist/types.d.ts
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
export declare const REQUEST_TIMEOUT_MS = 30000;
|
|
2
|
+
/**
|
|
3
|
+
* Bridge budget for the OAuth-initiating `/mcp/configure` call. The host
|
|
4
|
+
* holds that HTTP response open until the user finishes the interactive
|
|
5
|
+
* browser sign-in — both the host's own OAuth flow and this connector's
|
|
6
|
+
* standalone flow cap the wait at 5 minutes. The ordinary 30s
|
|
7
|
+
* REQUEST_TIMEOUT_MS therefore aborts mid-flow on any human-paced connect
|
|
8
|
+
* and reports a spurious failure. 5 minutes + 30s slack for the host's
|
|
9
|
+
* post-auth work (token exchange, config write, reload) — deliberately
|
|
10
|
+
* bounded so a genuinely dead bridge still fails instead of hanging forever.
|
|
11
|
+
*/
|
|
12
|
+
export declare const BRIDGE_OAUTH_TIMEOUT_MS: number;
|
|
2
13
|
export declare const SERVER_NAME = "salesforce-mcp-server";
|
|
3
14
|
/**
|
|
4
15
|
* Salesforce REST API version the connector pins every request to. Pinned
|
package/dist/types.js
CHANGED
|
@@ -2,6 +2,17 @@ import { createRequire } from 'node:module';
|
|
|
2
2
|
const require = createRequire(import.meta.url);
|
|
3
3
|
const pkg = require('../package.json');
|
|
4
4
|
export const REQUEST_TIMEOUT_MS = 30_000;
|
|
5
|
+
/**
|
|
6
|
+
* Bridge budget for the OAuth-initiating `/mcp/configure` call. The host
|
|
7
|
+
* holds that HTTP response open until the user finishes the interactive
|
|
8
|
+
* browser sign-in — both the host's own OAuth flow and this connector's
|
|
9
|
+
* standalone flow cap the wait at 5 minutes. The ordinary 30s
|
|
10
|
+
* REQUEST_TIMEOUT_MS therefore aborts mid-flow on any human-paced connect
|
|
11
|
+
* and reports a spurious failure. 5 minutes + 30s slack for the host's
|
|
12
|
+
* post-auth work (token exchange, config write, reload) — deliberately
|
|
13
|
+
* bounded so a genuinely dead bridge still fails instead of hanging forever.
|
|
14
|
+
*/
|
|
15
|
+
export const BRIDGE_OAUTH_TIMEOUT_MS = 5 * 60 * 1000 + REQUEST_TIMEOUT_MS;
|
|
5
16
|
export const SERVER_NAME = 'salesforce-mcp-server';
|
|
6
17
|
/**
|
|
7
18
|
* Salesforce REST API version the connector pins every request to. Pinned
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mindstone/mcp-server-salesforce",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"mcpName": "io.github.mindstone/mcp-server-salesforce",
|
|
5
5
|
"description": "Salesforce CRM MCP server — accounts, contacts, opportunities, leads, tasks, and custom objects via Salesforce API",
|
|
6
6
|
"license": "FSL-1.1-MIT",
|