@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 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.0](./CHANGELOG.md) · [npm](https://www.npmjs.com/package/@mindstone/mcp-server-salesforce)
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
@@ -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${result.username ? `: ${result.username}` : ''}`,
48
- username: result.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 || 'Failed to authenticate with Salesforce',
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${result.username ? `: ${result.username}` : ''}`,
65
- username: result.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 || 'Failed to authenticate',
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
- username: a.username,
131
- instance_url: a.instance_url,
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
- removeAccount(args.username);
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.0",
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",