@mindstone/mcp-server-salesforce 0.1.2 → 0.2.0

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/dist/utils.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { ConnectorError } from './types.js';
2
+ import { wrapUntrusted } from './untrusted-content.js';
2
3
  /**
3
4
  * Wraps a tool handler with standard error handling.
4
5
  */
@@ -25,9 +26,44 @@ export function withErrorHandling(fn) {
25
26
  isError: true,
26
27
  };
27
28
  }
28
- const errorMessage = error instanceof Error ? error.message : String(error);
29
+ // Unexpected errors: log the raw detail locally for diagnostics, then
30
+ // decide what the model may see.
31
+ console.error('[Salesforce MCP] Unexpected error while handling tool call:', error);
32
+ // jsforce API errors (HttpApiError shape: string errorCode): the message
33
+ // is authored by the vendor API (validation-rule text, error bodies) and
34
+ // stays actionable for the caller, but it is org-controlled text, so it
35
+ // MUST be enveloped before it reaches model-visible output (invariant #6).
36
+ const vendorCode = error.errorCode;
37
+ if (error instanceof Error && typeof vendorCode === 'string') {
38
+ return {
39
+ content: [
40
+ {
41
+ type: 'text',
42
+ text: JSON.stringify({
43
+ ok: false,
44
+ error: wrapUntrusted(error.message, 'salesforce:vendor_errors'),
45
+ code: 'VENDOR_ERROR',
46
+ vendor_code: vendorCode,
47
+ }),
48
+ },
49
+ ],
50
+ isError: true,
51
+ };
52
+ }
53
+ // Anything else (runtime failures, network errors, …): return a
54
+ // sanitised message — ad-hoc error text can embed environment details
55
+ // such as tokens or file paths.
29
56
  return {
30
- content: [{ type: 'text', text: JSON.stringify({ ok: false, error: errorMessage }) }],
57
+ content: [
58
+ {
59
+ type: 'text',
60
+ text: JSON.stringify({
61
+ ok: false,
62
+ error: 'Unexpected internal error while handling the request',
63
+ code: 'INTERNAL_ERROR',
64
+ }),
65
+ },
66
+ ],
31
67
  isError: true,
32
68
  };
33
69
  }
@@ -57,6 +93,19 @@ export function escapeSOQL(value) {
57
93
  .replace(/%/g, '\\%')
58
94
  .replace(/_/g, '\\_');
59
95
  }
96
+ /**
97
+ * Escape a user-supplied search term for interpolation inside a SOSL
98
+ * `FIND {term}` clause. SOSL gives special meaning to a different character
99
+ * set than SOQL — every reserved character is backslash-escaped so the term
100
+ * is always a single literal token and can never break out of the braces or
101
+ * inject operators (AND/OR groupings, wildcards, field scoping).
102
+ */
103
+ export function escapeSOSL(term) {
104
+ if (!term)
105
+ return '';
106
+ // Backslash MUST be escaped first to avoid double-escaping.
107
+ return term.replace(/\\/g, '\\\\').replace(/([?&|!{}[\]()^~*:"+-])/g, '\\$1');
108
+ }
60
109
  /**
61
110
  * Escape a user-supplied substring for interpolation inside a SOQL
62
111
  * `LIKE '%...%'` clause. Identical to `escapeSOQL` semantically (both
@@ -96,6 +145,24 @@ export function formatSOQLDate(dateStr, paramName) {
96
145
  }
97
146
  return match[1];
98
147
  }
148
+ /**
149
+ * Format a date or datetime string as a SOQL datetime literal (UTC).
150
+ * Accepts a plain date ("2026-01-09", treated as midnight UTC) or an ISO 8601
151
+ * datetime ("2026-01-09T14:30:00Z", offsets supported).
152
+ */
153
+ export function formatSOQLDateTime(value, paramName) {
154
+ const dateOnly = value.match(/^(\d{4}-\d{2}-\d{2})$/);
155
+ if (dateOnly)
156
+ return `${dateOnly[1]}T00:00:00Z`;
157
+ if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2})?(\.\d{1,3})?(Z|[+-]\d{2}:?\d{2})?$/.test(value)) {
158
+ throw new ConnectorError(`Invalid ${paramName}: "${value}"`, 'INVALID_DATE_FORMAT', 'Use ISO 8601 (e.g., "2026-01-09T14:30:00Z") or a plain date ("2026-01-09", treated as midnight UTC)');
159
+ }
160
+ const parsed = new Date(value);
161
+ if (Number.isNaN(parsed.getTime())) {
162
+ throw new ConnectorError(`Invalid ${paramName}: "${value}"`, 'INVALID_DATE_FORMAT', 'Use ISO 8601 (e.g., "2026-01-09T14:30:00Z") or a plain date ("2026-01-09", treated as midnight UTC)');
163
+ }
164
+ return parsed.toISOString();
165
+ }
99
166
  /**
100
167
  * Validate field names and return validated or default fields.
101
168
  */
@@ -125,10 +192,70 @@ export function validateAndMergeCustomFields(updateData, fields) {
125
192
  }
126
193
  }
127
194
  export const ALLOWED_FILTER_OPERATORS = new Set(['=', '!=', '<', '>', '<=', '>=', 'LIKE']);
195
+ /**
196
+ * Vendor error payloads (e.g. `SaveResult.errors`) are org-authored text:
197
+ * validation-rule messages and error bodies are defined in the customer's
198
+ * Salesforce org and can carry arbitrary content. Envelope them before they
199
+ * reach model-visible output (AGENTS.md invariant #6, FOX-3490).
200
+ */
201
+ export function formatVendorErrors(errors) {
202
+ const detail = typeof errors === 'string' ? errors : JSON.stringify(errors);
203
+ return wrapUntrusted(detail ?? 'Unknown vendor error', 'salesforce:vendor_errors');
204
+ }
205
+ /**
206
+ * 15- or 18-character alphanumeric Salesforce record ID.
207
+ */
208
+ const SALESFORCE_ID_SHAPE = /^[a-zA-Z0-9]{15}(?:[a-zA-Z0-9]{3})?$/;
209
+ /**
210
+ * The structural exemption applies ONLY when a `*Id`-keyed value actually has
211
+ * the shape of a Salesforce ID: record IDs are copied verbatim into follow-up
212
+ * tool calls (update, convert, link), so enveloping them would corrupt that
213
+ * flow. An org-authored string under an Id-named key that is not a valid ID
214
+ * is external text and is enveloped like any other. `attributes` is not
215
+ * exempt as a whole either — its nested values (type, url) are sanitized
216
+ * recursively. Everything else reachable inside a Salesforce record (names,
217
+ * emails, descriptions, subjects, …) is authored in the external system and
218
+ * MUST be enveloped (AGENTS.md invariant #6, FOX-3490).
219
+ */
220
+ function isStructuralRecordValue(key, value) {
221
+ if (key !== 'Id' && !key.endsWith('Id'))
222
+ return false;
223
+ return typeof value === 'string' && SALESFORCE_ID_SHAPE.test(value);
224
+ }
225
+ function wrapRecordValue(value, source) {
226
+ if (typeof value === 'string')
227
+ return wrapUntrusted(value, source);
228
+ if (Array.isArray(value))
229
+ return value.map((item) => wrapRecordValue(item, source));
230
+ if (value && typeof value === 'object') {
231
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [
232
+ key,
233
+ isStructuralRecordValue(key, item) ? item : wrapRecordValue(item, source),
234
+ ]));
235
+ }
236
+ return value;
237
+ }
238
+ /**
239
+ * Envelope every external-text field in a list of Salesforce records before
240
+ * they are returned to the LLM. Only values under `Id`/`*Id` keys that are
241
+ * actually shaped like Salesforce IDs pass through raw, so downstream tool
242
+ * calls can use them as identifiers.
243
+ */
244
+ export function sanitizeRecords(records, source) {
245
+ return records.map((record) => wrapRecordValue(record, source));
246
+ }
247
+ /**
248
+ * Envelope every string inside an arbitrary external-data blob (report
249
+ * results, metadata payloads), leaving only shape-validated Salesforce IDs
250
+ * raw. Use for non-record response shapes where every value is org-authored.
251
+ */
252
+ export function sanitizeExternalData(value, source) {
253
+ return wrapRecordValue(value, source);
254
+ }
128
255
  export function checkSaveResult(result, errorMessage) {
129
256
  const res = Array.isArray(result) ? result[0] : result;
130
257
  if (!res.success) {
131
- throw new ConnectorError(errorMessage, 'UPDATE_ERROR', JSON.stringify(res.errors));
258
+ throw new ConnectorError(errorMessage, 'UPDATE_ERROR', formatVendorErrors(res.errors));
132
259
  }
133
260
  }
134
261
  //# sourceMappingURL=utils.js.map
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@mindstone/mcp-server-salesforce",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "mcpName": "io.github.mindstone/mcp-server-salesforce",
5
- "description": "Salesforce CRM MCP server \u2014 accounts, contacts, opportunities, leads, tasks, and custom objects via Salesforce API",
5
+ "description": "Salesforce CRM MCP server — accounts, contacts, opportunities, leads, tasks, and custom objects via Salesforce API",
6
6
  "license": "FSL-1.1-MIT",
7
7
  "type": "module",
8
8
  "bin": {