@mindstone/mcp-server-salesforce 0.2.0 → 0.2.1
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/tools/auth-tools.js +47 -12
- 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.1](./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/tools/auth-tools.js
CHANGED
|
@@ -1,8 +1,19 @@
|
|
|
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';
|
|
5
6
|
import { listConnectedAccounts, removeAccount } from '../client.js';
|
|
7
|
+
// Disconnect needs a raw handle the model can copy verbatim into
|
|
8
|
+
// salesforce_disconnect_account, but the storage id is username-derived
|
|
9
|
+
// (sanitizeFilename) — passing it through raw would re-open the very channel
|
|
10
|
+
// the envelopes below close. Instead the account list exposes a
|
|
11
|
+
// connector-authored `ref` (truncated hash of the storage id): stable across
|
|
12
|
+
// calls, shaped like data, and containing zero external text.
|
|
13
|
+
function accountRef(accountId) {
|
|
14
|
+
return `acct_${createHash('sha256').update(String(accountId)).digest('hex').slice(0, 12)}`;
|
|
15
|
+
}
|
|
16
|
+
const ACCOUNT_REF_SHAPE = /^acct_[0-9a-f]{12}$/;
|
|
6
17
|
export function registerAuthTools(server) {
|
|
7
18
|
server.registerTool('salesforce_connect_account', {
|
|
8
19
|
description: `Connect a Salesforce account via OAuth. Takes no parameters — call with {}.
|
|
@@ -41,34 +52,40 @@ After connecting, verify with salesforce_list_connected_accounts.`,
|
|
|
41
52
|
if (mode === 'bridge') {
|
|
42
53
|
const result = await bridgeRequest(process.env.MCP_BRIDGE_CONFIGURE_ENDPOINT || '/mcp/configure', {});
|
|
43
54
|
if (result.success) {
|
|
55
|
+
// The bridge username is host/org-authored external text: envelope
|
|
56
|
+
// it before it reaches model-visible output (AGENTS.md invariant #6).
|
|
57
|
+
const username = result.username ? sanitizeExternalData(result.username, 'salesforce:auth') : undefined;
|
|
44
58
|
return JSON.stringify({
|
|
45
59
|
ok: true,
|
|
46
60
|
status: 'authenticated',
|
|
47
|
-
message: `Successfully connected Salesforce account${
|
|
48
|
-
username
|
|
61
|
+
message: `Successfully connected Salesforce account${username ? `: ${username}` : ''}`,
|
|
62
|
+
username,
|
|
49
63
|
next_step: 'You can now use Salesforce tools. Try salesforce_list_connected_accounts to verify.',
|
|
50
64
|
});
|
|
51
65
|
}
|
|
52
66
|
return JSON.stringify({
|
|
53
67
|
ok: false,
|
|
54
|
-
error: result.error
|
|
68
|
+
error: result.error
|
|
69
|
+
? sanitizeExternalData(result.error, 'salesforce:auth')
|
|
70
|
+
: 'Failed to authenticate with Salesforce',
|
|
55
71
|
action_required: 'Please try calling salesforce_connect_account again.',
|
|
56
72
|
});
|
|
57
73
|
}
|
|
58
74
|
// standalone_oauth
|
|
59
75
|
const result = await startStandaloneOAuth();
|
|
60
76
|
if (result.success) {
|
|
77
|
+
const username = result.username ? sanitizeExternalData(result.username, 'salesforce:auth') : undefined;
|
|
61
78
|
return JSON.stringify({
|
|
62
79
|
ok: true,
|
|
63
80
|
status: 'authenticated',
|
|
64
|
-
message: `Successfully connected Salesforce account${
|
|
65
|
-
username
|
|
81
|
+
message: `Successfully connected Salesforce account${username ? `: ${username}` : ''}`,
|
|
82
|
+
username,
|
|
66
83
|
next_step: 'You can now use Salesforce tools. Try salesforce_list_connected_accounts to verify.',
|
|
67
84
|
});
|
|
68
85
|
}
|
|
69
86
|
return JSON.stringify({
|
|
70
87
|
ok: false,
|
|
71
|
-
error: result.error
|
|
88
|
+
error: result.error ? sanitizeExternalData(result.error, 'salesforce:auth') : 'Failed to authenticate',
|
|
72
89
|
action_required: 'Please try calling salesforce_connect_account again.',
|
|
73
90
|
});
|
|
74
91
|
}));
|
|
@@ -127,11 +144,15 @@ This MCP instance operates on a single Salesforce account. If no account is conn
|
|
|
127
144
|
const hasValidToken = token && (token.expires_at ?? 0) > Date.now();
|
|
128
145
|
const hasRefresh = token && !!token.refresh_token;
|
|
129
146
|
return {
|
|
130
|
-
|
|
131
|
-
|
|
147
|
+
// Connector-authored raw handle for salesforce_disconnect_account
|
|
148
|
+
// (the storage id stays internal — it is username-derived and would
|
|
149
|
+
// re-open the channel the envelopes below close).
|
|
150
|
+
ref: accountRef(a.id),
|
|
151
|
+
username: a.username ? sanitizeExternalData(a.username, 'salesforce:auth') : a.username,
|
|
152
|
+
instance_url: a.instance_url ? sanitizeExternalData(a.instance_url, 'salesforce:auth') : a.instance_url,
|
|
132
153
|
is_sandbox: a.is_sandbox,
|
|
133
154
|
status: hasValidToken || hasRefresh ? 'active' : 'expired',
|
|
134
|
-
connected_at: a.connected_at,
|
|
155
|
+
connected_at: a.connected_at ? sanitizeExternalData(a.connected_at, 'salesforce:auth') : a.connected_at,
|
|
135
156
|
};
|
|
136
157
|
});
|
|
137
158
|
return JSON.stringify({
|
|
@@ -145,9 +166,10 @@ This MCP instance operates on a single Salesforce account. If no account is conn
|
|
|
145
166
|
server.registerTool('salesforce_disconnect_account', {
|
|
146
167
|
description: `Disconnect a Salesforce account. Example: { "username": "user@company.com" }
|
|
147
168
|
|
|
169
|
+
Accepts the account \`ref\` from salesforce_list_connected_accounts as well as the raw username.
|
|
148
170
|
Permanently removes stored credentials. Use when switching accounts or troubleshooting.`,
|
|
149
171
|
inputSchema: z.object({
|
|
150
|
-
username: z.string().min(1).describe('Username of the account to disconnect'),
|
|
172
|
+
username: z.string().min(1).describe('Username or account ref of the account to disconnect'),
|
|
151
173
|
}),
|
|
152
174
|
annotations: {
|
|
153
175
|
readOnlyHint: false,
|
|
@@ -166,7 +188,20 @@ Permanently removes stored credentials. Use when switching accounts or troublesh
|
|
|
166
188
|
: 'No accounts to disconnect — set up authentication first.',
|
|
167
189
|
});
|
|
168
190
|
}
|
|
169
|
-
|
|
191
|
+
// Resolve a connector-authored `ref` back to the storage id.
|
|
192
|
+
let usernameOrId = args.username;
|
|
193
|
+
if (ACCOUNT_REF_SHAPE.test(usernameOrId)) {
|
|
194
|
+
const match = listConnectedAccounts().find((a) => accountRef(a.id) === usernameOrId);
|
|
195
|
+
if (!match) {
|
|
196
|
+
return JSON.stringify({
|
|
197
|
+
ok: false,
|
|
198
|
+
error: `Account not found: ${usernameOrId}`,
|
|
199
|
+
resolution: 'Use salesforce_list_connected_accounts to see connected accounts.',
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
usernameOrId = match.id;
|
|
203
|
+
}
|
|
204
|
+
removeAccount(usernameOrId);
|
|
170
205
|
return JSON.stringify({
|
|
171
206
|
ok: true,
|
|
172
207
|
message: `Disconnected Salesforce account: ${args.username}`,
|
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.1",
|
|
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",
|