@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 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.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>) => Promise<{
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(REQUEST_TIMEOUT_MS),
33
+ signal: AbortSignal.timeout(timeoutMs),
29
34
  headers: {
30
35
  'Content-Type': 'application/json',
31
36
  Authorization: `Bearer ${bridge.token}`,
@@ -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
- const result = await bridgeRequest(process.env.MCP_BRIDGE_CONFIGURE_ENDPOINT || '/mcp/configure', {});
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${result.username ? `: ${result.username}` : ''}`,
48
- username: result.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 || 'Failed to authenticate with Salesforce',
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${result.username ? `: ${result.username}` : ''}`,
65
- username: result.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 || 'Failed to authenticate',
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
- username: a.username,
131
- instance_url: a.instance_url,
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
- removeAccount(args.username);
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.0",
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",