@mindstone/mcp-server-salesforce 0.1.3 → 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,9 +9,9 @@ Salesforce CRM MCP server — accounts, contacts, opportunities, leads, tasks, u
9
9
 
10
10
  ## Status
11
11
 
12
- - **Version:** [0.1.2](./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
- - **Tools:** [26](./src/tools/) (accounts, contacts, opportunities, leads, tasks, query)
14
+ - **Tools:** [37](./src/tools/) (accounts, contacts, opportunities, leads, tasks, cases, events, search, notes, campaigns, reports, query)
15
15
  - **Surface:** cloud-api
16
16
  - **Machine-readable:** [`STATUS.json`](./STATUS.json)
17
17
 
@@ -135,7 +135,7 @@ Then call `salesforce_connect_account` to start the OAuth flow.
135
135
  - `SALESFORCE_OAUTH_PORT` — OAuth callback port (`0` = OS-assigned; default: `0`)
136
136
  - `SALESFORCE_OAUTH_SCOPES` — Space-separated OAuth scopes. Leave unset to use the connector default.
137
137
 
138
- ## Available Tools (26)
138
+ ## Available Tools (37)
139
139
 
140
140
  ### Account Management
141
141
  - `salesforce_connect_account` — Connect a Salesforce account via OAuth
@@ -168,6 +168,29 @@ Then call `salesforce_connect_account` to start the OAuth flow.
168
168
  - `salesforce_create_task` — Create a task
169
169
  - `salesforce_update_task` — Update a task
170
170
 
171
+ ### Cases
172
+ - `salesforce_get_cases` — Get support cases with filters
173
+ - `salesforce_create_case` — Create a support case
174
+ - `salesforce_update_case` — Update a support case
175
+
176
+ ### Events
177
+ - `salesforce_get_events` — Get calendar events with filters
178
+ - `salesforce_create_event` — Create a calendar event
179
+
180
+ ### Search
181
+ - `salesforce_search` — Cross-object full-text search (SOSL); the response's `truncated` flag tells you whether more matches exist beyond the limit
182
+
183
+ ### Notes
184
+ - `salesforce_get_notes` — Get notes attached to a record
185
+ - `salesforce_create_note` — Create a note, optionally attached to a record (a failed attach rolls the note back, or reports the orphaned note ID)
186
+
187
+ ### Campaigns
188
+ - `salesforce_get_campaigns` — Get marketing campaigns with filters
189
+ - `salesforce_get_campaign_members` — Get leads/contacts in a campaign
190
+
191
+ ### Reports
192
+ - `salesforce_run_report` — Run an existing Salesforce report (Analytics REST API)
193
+
171
194
  ### Users
172
195
  - `salesforce_get_users` — Get Salesforce users
173
196
 
@@ -187,6 +210,8 @@ Then call `salesforce_connect_account` to start the OAuth flow.
187
210
  - OAuth credentials are stored under `SALESFORCE_CONFIG_DIR` (default `~/.mcp/salesforce`) with restrictive directory and file permissions.
188
211
  - Write and disconnect tools are marked so capable hosts can ask for confirmation before changing Salesforce data.
189
212
  - SOQL helper paths escape string and `LIKE` values, strip comments quote-safely before applying the query limit cap, and enforce a maximum of 200 records for raw SOQL queries.
213
+ - Record text returned by read tools is wrapped in `<untrusted-content>` envelopes so Salesforce-authored field values are presented to the model as data, not instructions.
214
+ - `salesforce_convert_lead` reports a conversion as failed when Salesforce rejects it at the record level (e.g. a validation rule), instead of claiming success; the org-authored error detail is enveloped the same way.
190
215
 
191
216
  ## Licence
192
217
 
package/dist/client.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import jsforce from 'jsforce';
2
- import { ConnectorError, } from './types.js';
2
+ import { ConnectorError, SALESFORCE_API_VERSION, } from './types.js';
3
3
  import { getAuthMode, getActiveToken, loadAccounts, loadToken, saveToken, deleteToken, saveAccounts, } from './auth.js';
4
4
  // Connection cache per account
5
5
  const connections = new Map();
@@ -44,6 +44,7 @@ export async function getConnection(accountId) {
44
44
  const connectionConfig = {
45
45
  instanceUrl: tokenData.instance_url,
46
46
  accessToken: tokenData.access_token,
47
+ version: SALESFORCE_API_VERSION,
47
48
  };
48
49
  // Only hand jsforce a refresh token when we can also give it a way to USE
49
50
  // that token — i.e. the OAuth2 client info to build a refresh delegate.
package/dist/server.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { SERVER_NAME, SERVER_VERSION } from './types.js';
3
3
  import { resolveAuthMode } from './auth.js';
4
- import { registerAuthTools, registerAccountTools, registerContactTools, registerOpportunityTools, registerLeadTools, registerTaskTools, registerUserTools, registerQueryTools, } from './tools/index.js';
4
+ import { registerAuthTools, registerAccountTools, registerContactTools, registerOpportunityTools, registerLeadTools, registerTaskTools, registerCaseTools, registerEventTools, registerSearchTools, registerNoteTools, registerCampaignTools, registerReportTools, registerUserTools, registerQueryTools, } from './tools/index.js';
5
5
  export function createServer() {
6
6
  resolveAuthMode();
7
7
  const server = new McpServer({
@@ -14,6 +14,12 @@ export function createServer() {
14
14
  registerOpportunityTools(server);
15
15
  registerLeadTools(server);
16
16
  registerTaskTools(server);
17
+ registerCaseTools(server);
18
+ registerEventTools(server);
19
+ registerSearchTools(server);
20
+ registerNoteTools(server);
21
+ registerCampaignTools(server);
22
+ registerReportTools(server);
17
23
  registerUserTools(server);
18
24
  registerQueryTools(server);
19
25
  return server;
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { withErrorHandling, escapeSOQL, escapeSOQLLike, validateFields, validateAndMergeCustomFields, checkSaveResult } from '../utils.js';
2
+ import { withErrorHandling, escapeSOQL, escapeSOQLLike, validateFields, validateAndMergeCustomFields, checkSaveResult, formatVendorErrors, sanitizeRecords } from '../utils.js';
3
3
  import { withConnection } from '../client.js';
4
4
  export function registerAccountTools(server) {
5
5
  server.registerTool('salesforce_get_accounts', {
@@ -29,7 +29,7 @@ export function registerAccountTools(server) {
29
29
  const limit = Math.min(Math.max(1, args.limit ?? 50), 200);
30
30
  query += ` LIMIT ${limit}`;
31
31
  const result = await conn.query(query);
32
- return JSON.stringify({ ok: true, records: result.records, totalSize: result.totalSize, done: result.done });
32
+ return JSON.stringify({ ok: true, records: sanitizeRecords(result.records, 'salesforce:get_accounts:records'), totalSize: result.totalSize, done: result.done });
33
33
  });
34
34
  }));
35
35
  server.registerTool('salesforce_create_account', {
@@ -43,7 +43,7 @@ export function registerAccountTools(server) {
43
43
  description: z.string().optional().describe('Account description'),
44
44
  fields: z.record(z.unknown()).optional().describe('Additional/custom fields as key-value pairs'),
45
45
  }),
46
- annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
46
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
47
47
  }, withErrorHandling(async (args) => {
48
48
  return withConnection(undefined, async (conn) => {
49
49
  const data = { Name: args.name };
@@ -62,7 +62,7 @@ export function registerAccountTools(server) {
62
62
  const result = await conn.sobject('Account').create(data);
63
63
  if (!result.success) {
64
64
  const { ConnectorError } = await import('../types.js');
65
- throw new ConnectorError('Failed to create account', 'CREATE_ERROR', JSON.stringify(result.errors));
65
+ throw new ConnectorError('Failed to create account', 'CREATE_ERROR', formatVendorErrors(result.errors));
66
66
  }
67
67
  return JSON.stringify({ ok: true, status: 'success', object: 'Account', id: result.id, name: args.name });
68
68
  });
@@ -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}`,
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerCampaignTools(server: McpServer): void;
3
+ //# sourceMappingURL=campaigns.d.ts.map
@@ -0,0 +1,78 @@
1
+ import { z } from 'zod';
2
+ import { withErrorHandling, escapeSOQL, escapeSOQLLike, validateFields, formatSOQLDate, sanitizeRecords } from '../utils.js';
3
+ import { withConnection } from '../client.js';
4
+ export function registerCampaignTools(server) {
5
+ server.registerTool('salesforce_get_campaigns', {
6
+ description: `Get marketing campaigns. Filters: name_contains, campaign_type, status, is_active, start_date_from, start_date_to. Returns: Id, Name, Type, Status, IsActive, StartDate, EndDate, Description. Max 200 (default: 50).`,
7
+ inputSchema: z.object({
8
+ limit: z.number().int().min(1).max(200).optional().describe('Max results 1-200 (default: 50)'),
9
+ name_contains: z.string().optional().describe('Filter by name (case-insensitive)'),
10
+ campaign_type: z.string().optional().describe('Filter by type (Email, Webinar, Advertisement)'),
11
+ status: z.string().optional().describe('Filter by status (Planned, In Progress, Completed)'),
12
+ is_active: z.boolean().optional().describe('Filter by active status'),
13
+ start_date_from: z.string().optional().describe('Campaigns starting on/after date (YYYY-MM-DD)'),
14
+ start_date_to: z.string().optional().describe('Campaigns starting on/before date (YYYY-MM-DD)'),
15
+ fields: z.array(z.string()).optional().describe('Custom fields (must be valid API names)'),
16
+ }),
17
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
18
+ }, withErrorHandling(async (args) => {
19
+ return withConnection(undefined, async (conn) => {
20
+ const defaultFields = ['Id', 'Name', 'Type', 'Status', 'IsActive', 'StartDate', 'EndDate', 'Description'];
21
+ const fields = validateFields(args.fields || [], defaultFields);
22
+ let query = `SELECT ${fields.join(', ')} FROM Campaign`;
23
+ const conditions = [];
24
+ if (args.name_contains)
25
+ conditions.push(`Name LIKE '%${escapeSOQLLike(args.name_contains)}%'`);
26
+ if (args.campaign_type)
27
+ conditions.push(`Type = '${escapeSOQL(args.campaign_type)}'`);
28
+ if (args.status)
29
+ conditions.push(`Status = '${escapeSOQL(args.status)}'`);
30
+ if (args.is_active !== undefined)
31
+ conditions.push(`IsActive = ${args.is_active}`);
32
+ if (args.start_date_from)
33
+ conditions.push(`StartDate >= ${formatSOQLDate(args.start_date_from, 'start_date_from')}`);
34
+ if (args.start_date_to)
35
+ conditions.push(`StartDate <= ${formatSOQLDate(args.start_date_to, 'start_date_to')}`);
36
+ if (conditions.length > 0)
37
+ query += ` WHERE ${conditions.join(' AND ')}`;
38
+ const limit = Math.min(Math.max(1, args.limit ?? 50), 200);
39
+ query += ` LIMIT ${limit}`;
40
+ const result = await conn.query(query);
41
+ return JSON.stringify({ ok: true, records: sanitizeRecords(result.records, 'salesforce:get_campaigns:records'), totalSize: result.totalSize });
42
+ });
43
+ }));
44
+ server.registerTool('salesforce_get_campaign_members', {
45
+ description: `Get campaign members (leads/contacts in a campaign). Filters: campaign_id, lead_id, contact_id, status. Returns: Id, CampaignId, LeadId, ContactId, Status, CreatedDate. Max 200 (default: 50).`,
46
+ inputSchema: z.object({
47
+ limit: z.number().int().min(1).max(200).optional().describe('Max results 1-200 (default: 50)'),
48
+ campaign_id: z.string().optional().describe('Filter by Campaign ID'),
49
+ lead_id: z.string().optional().describe('Filter by Lead ID'),
50
+ contact_id: z.string().optional().describe('Filter by Contact ID'),
51
+ status: z.string().optional().describe('Filter by member status (Sent, Responded)'),
52
+ fields: z.array(z.string()).optional().describe('Custom fields (must be valid API names)'),
53
+ }),
54
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
55
+ }, withErrorHandling(async (args) => {
56
+ return withConnection(undefined, async (conn) => {
57
+ const defaultFields = ['Id', 'CampaignId', 'LeadId', 'ContactId', 'Status', 'CreatedDate'];
58
+ const fields = validateFields(args.fields || [], defaultFields);
59
+ let query = `SELECT ${fields.join(', ')} FROM CampaignMember`;
60
+ const conditions = [];
61
+ if (args.campaign_id)
62
+ conditions.push(`CampaignId = '${escapeSOQL(args.campaign_id)}'`);
63
+ if (args.lead_id)
64
+ conditions.push(`LeadId = '${escapeSOQL(args.lead_id)}'`);
65
+ if (args.contact_id)
66
+ conditions.push(`ContactId = '${escapeSOQL(args.contact_id)}'`);
67
+ if (args.status)
68
+ conditions.push(`Status = '${escapeSOQL(args.status)}'`);
69
+ if (conditions.length > 0)
70
+ query += ` WHERE ${conditions.join(' AND ')}`;
71
+ const limit = Math.min(Math.max(1, args.limit ?? 50), 200);
72
+ query += ` LIMIT ${limit}`;
73
+ const result = await conn.query(query);
74
+ return JSON.stringify({ ok: true, records: sanitizeRecords(result.records, 'salesforce:get_campaign_members:records'), totalSize: result.totalSize });
75
+ });
76
+ }));
77
+ }
78
+ //# sourceMappingURL=campaigns.js.map
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerCaseTools(server: McpServer): void;
3
+ //# sourceMappingURL=cases.d.ts.map
@@ -0,0 +1,121 @@
1
+ import { z } from 'zod';
2
+ import { withErrorHandling, escapeSOQL, escapeSOQLLike, validateFields, validateAndMergeCustomFields, checkSaveResult, formatVendorErrors, sanitizeRecords } from '../utils.js';
3
+ import { withConnection } from '../client.js';
4
+ import { ConnectorError } from '../types.js';
5
+ export function registerCaseTools(server) {
6
+ server.registerTool('salesforce_get_cases', {
7
+ description: `Get support cases. Filters: subject_contains, status, priority, related_account_id, related_contact_id, is_closed. Returns: Id, CaseNumber, Subject, Status, Priority, Origin, Description, AccountId, ContactId, OwnerId, CreatedDate, ClosedDate. Max 200 (default: 50).`,
8
+ inputSchema: z.object({
9
+ limit: z.number().int().min(1).max(200).optional().describe('Max results 1-200 (default: 50)'),
10
+ subject_contains: z.string().optional().describe('Filter by subject (case-insensitive)'),
11
+ status: z.string().optional().describe('Filter by status (New, Working, Escalated, Closed)'),
12
+ priority: z.string().optional().describe('Filter by priority (High, Medium, Low)'),
13
+ related_account_id: z.string().optional().describe('Filter by Account ID'),
14
+ related_contact_id: z.string().optional().describe('Filter by Contact ID'),
15
+ is_closed: z.boolean().optional().describe('Filter by closed status'),
16
+ fields: z.array(z.string()).optional().describe('Custom fields (must be valid API names)'),
17
+ }),
18
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
19
+ }, withErrorHandling(async (args) => {
20
+ return withConnection(undefined, async (conn) => {
21
+ const defaultFields = ['Id', 'CaseNumber', 'Subject', 'Status', 'Priority', 'Origin', 'Description', 'AccountId', 'ContactId', 'OwnerId', 'CreatedDate', 'ClosedDate'];
22
+ const fields = validateFields(args.fields || [], defaultFields);
23
+ let query = `SELECT ${fields.join(', ')} FROM Case`;
24
+ const conditions = [];
25
+ if (args.subject_contains)
26
+ conditions.push(`Subject LIKE '%${escapeSOQLLike(args.subject_contains)}%'`);
27
+ if (args.status)
28
+ conditions.push(`Status = '${escapeSOQL(args.status)}'`);
29
+ if (args.priority)
30
+ conditions.push(`Priority = '${escapeSOQL(args.priority)}'`);
31
+ if (args.related_account_id)
32
+ conditions.push(`AccountId = '${escapeSOQL(args.related_account_id)}'`);
33
+ if (args.related_contact_id)
34
+ conditions.push(`ContactId = '${escapeSOQL(args.related_contact_id)}'`);
35
+ if (args.is_closed !== undefined)
36
+ conditions.push(`IsClosed = ${args.is_closed}`);
37
+ if (conditions.length > 0)
38
+ query += ` WHERE ${conditions.join(' AND ')}`;
39
+ const limit = Math.min(Math.max(1, args.limit ?? 50), 200);
40
+ query += ` LIMIT ${limit}`;
41
+ const result = await conn.query(query);
42
+ return JSON.stringify({ ok: true, records: sanitizeRecords(result.records, 'salesforce:get_cases:records'), totalSize: result.totalSize });
43
+ });
44
+ }));
45
+ server.registerTool('salesforce_create_case', {
46
+ description: `Create a support case. Required: subject. Optional: status, priority, origin, description, related_account_id, related_contact_id, fields.`,
47
+ inputSchema: z.object({
48
+ subject: z.string().min(1).describe('Case subject (required)'),
49
+ status: z.string().optional().describe('Case status (default: New)'),
50
+ priority: z.string().optional().describe('Priority (High, Medium, Low)'),
51
+ origin: z.string().optional().describe('Case origin (Phone, Email, Web)'),
52
+ description: z.string().optional().describe('Case description'),
53
+ related_account_id: z.string().optional().describe('Account ID to associate with'),
54
+ related_contact_id: z.string().optional().describe('Contact ID to associate with'),
55
+ fields: z.record(z.unknown()).optional().describe('Additional/custom fields as key-value pairs'),
56
+ }),
57
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
58
+ }, withErrorHandling(async (args) => {
59
+ return withConnection(undefined, async (conn) => {
60
+ const data = { Subject: args.subject };
61
+ if (args.status)
62
+ data.Status = args.status;
63
+ if (args.priority)
64
+ data.Priority = args.priority;
65
+ if (args.origin)
66
+ data.Origin = args.origin;
67
+ if (args.description)
68
+ data.Description = args.description;
69
+ if (args.related_account_id)
70
+ data.AccountId = args.related_account_id;
71
+ if (args.related_contact_id)
72
+ data.ContactId = args.related_contact_id;
73
+ if (args.fields)
74
+ validateAndMergeCustomFields(data, args.fields);
75
+ const result = await conn.sobject('Case').create(data);
76
+ if (!result.success)
77
+ throw new ConnectorError('Failed to create case', 'CREATE_ERROR', formatVendorErrors(result.errors));
78
+ return JSON.stringify({ ok: true, status: 'success', object: 'Case', id: result.id, subject: args.subject });
79
+ });
80
+ }));
81
+ server.registerTool('salesforce_update_case', {
82
+ description: `Update a support case. Required: id. Updatable: subject, status, priority, origin, description, related_account_id, related_contact_id, fields.`,
83
+ inputSchema: z.object({
84
+ id: z.string().min(1).describe('Salesforce Case ID (required)'),
85
+ subject: z.string().optional().describe('Case subject'),
86
+ status: z.string().optional().describe('Case status (e.g., Working, Escalated, Closed)'),
87
+ priority: z.string().optional().describe('Priority (High, Medium, Low)'),
88
+ origin: z.string().optional().describe('Case origin'),
89
+ description: z.string().optional().describe('Description'),
90
+ related_account_id: z.string().optional().describe('Account ID to associate with'),
91
+ related_contact_id: z.string().optional().describe('Contact ID to associate with'),
92
+ fields: z.record(z.unknown()).optional().describe('Additional/custom fields as key-value pairs'),
93
+ }),
94
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
95
+ }, withErrorHandling(async (args) => {
96
+ return withConnection(undefined, async (conn) => {
97
+ const updateData = { Id: args.id };
98
+ if (args.subject)
99
+ updateData.Subject = args.subject;
100
+ if (args.status)
101
+ updateData.Status = args.status;
102
+ if (args.priority)
103
+ updateData.Priority = args.priority;
104
+ if (args.origin)
105
+ updateData.Origin = args.origin;
106
+ if (args.description)
107
+ updateData.Description = args.description;
108
+ if (args.related_account_id)
109
+ updateData.AccountId = args.related_account_id;
110
+ if (args.related_contact_id)
111
+ updateData.ContactId = args.related_contact_id;
112
+ if (args.fields)
113
+ validateAndMergeCustomFields(updateData, args.fields);
114
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
115
+ const result = await conn.sobject('Case').update(updateData);
116
+ checkSaveResult(result, 'Failed to update case');
117
+ return JSON.stringify({ ok: true, status: 'success', object: 'Case', id: args.id });
118
+ });
119
+ }));
120
+ }
121
+ //# sourceMappingURL=cases.js.map
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { withErrorHandling, escapeSOQL, escapeSOQLLike, validateFields, validateAndMergeCustomFields, checkSaveResult } from '../utils.js';
2
+ import { withErrorHandling, escapeSOQL, escapeSOQLLike, validateFields, validateAndMergeCustomFields, checkSaveResult, formatVendorErrors, sanitizeRecords } from '../utils.js';
3
3
  import { withConnection } from '../client.js';
4
4
  import { ConnectorError } from '../types.js';
5
5
  export function registerContactTools(server) {
@@ -32,7 +32,7 @@ export function registerContactTools(server) {
32
32
  const limit = Math.min(Math.max(1, args.limit ?? 50), 200);
33
33
  query += ` LIMIT ${limit}`;
34
34
  const result = await conn.query(query);
35
- return JSON.stringify({ ok: true, records: result.records, totalSize: result.totalSize });
35
+ return JSON.stringify({ ok: true, records: sanitizeRecords(result.records, 'salesforce:get_contacts:records'), totalSize: result.totalSize });
36
36
  });
37
37
  }));
38
38
  server.registerTool('salesforce_create_contact', {
@@ -46,7 +46,7 @@ export function registerContactTools(server) {
46
46
  related_account_id: z.string().optional().describe('Account ID to associate with'),
47
47
  fields: z.record(z.unknown()).optional().describe('Additional/custom fields as key-value pairs'),
48
48
  }),
49
- annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
49
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
50
50
  }, withErrorHandling(async (args) => {
51
51
  return withConnection(undefined, async (conn) => {
52
52
  const data = { LastName: args.last_name };
@@ -64,7 +64,7 @@ export function registerContactTools(server) {
64
64
  validateAndMergeCustomFields(data, args.fields);
65
65
  const result = await conn.sobject('Contact').create(data);
66
66
  if (!result.success)
67
- throw new ConnectorError('Failed to create contact', 'CREATE_ERROR', JSON.stringify(result.errors));
67
+ throw new ConnectorError('Failed to create contact', 'CREATE_ERROR', formatVendorErrors(result.errors));
68
68
  return JSON.stringify({ ok: true, status: 'success', object: 'Contact', id: result.id, name: `${args.first_name || ''} ${args.last_name}`.trim() });
69
69
  });
70
70
  }));
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerEventTools(server: McpServer): void;
3
+ //# sourceMappingURL=events.d.ts.map
@@ -0,0 +1,90 @@
1
+ import { z } from 'zod';
2
+ import { withErrorHandling, escapeSOQL, escapeSOQLLike, validateFields, validateAndMergeCustomFields, formatSOQLDateTime, formatVendorErrors, sanitizeRecords } from '../utils.js';
3
+ import { withConnection } from '../client.js';
4
+ import { ConnectorError } from '../types.js';
5
+ export function registerEventTools(server) {
6
+ server.registerTool('salesforce_get_events', {
7
+ description: `Get calendar events. Filters: subject_contains, who_id, what_id, start_from, start_to. Dates accept YYYY-MM-DD (midnight UTC) or ISO 8601 datetimes. Returns: Id, Subject, StartDateTime, EndDateTime, Location, Description, WhoId, WhatId, OwnerId, IsAllDayEvent. Max 200 (default: 50).`,
8
+ inputSchema: z.object({
9
+ limit: z.number().int().min(1).max(200).optional().describe('Max results 1-200 (default: 50)'),
10
+ subject_contains: z.string().optional().describe('Filter by subject (case-insensitive)'),
11
+ who_id: z.string().optional().describe('Filter by WhoId (Contact or Lead ID)'),
12
+ what_id: z.string().optional().describe('Filter by WhatId (Account or Opportunity ID)'),
13
+ start_from: z.string().optional().describe('Events starting on/after this date or datetime (YYYY-MM-DD or ISO 8601; see tool description for an example)'),
14
+ start_to: z.string().optional().describe('Events starting on/before this date or datetime (YYYY-MM-DD or ISO 8601)'),
15
+ fields: z.array(z.string()).optional().describe('Custom fields (must be valid API names)'),
16
+ }),
17
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
18
+ }, withErrorHandling(async (args) => {
19
+ return withConnection(undefined, async (conn) => {
20
+ const defaultFields = ['Id', 'Subject', 'StartDateTime', 'EndDateTime', 'Location', 'Description', 'WhoId', 'WhatId', 'OwnerId', 'IsAllDayEvent'];
21
+ const fields = validateFields(args.fields || [], defaultFields);
22
+ let query = `SELECT ${fields.join(', ')} FROM Event`;
23
+ const conditions = [];
24
+ if (args.subject_contains)
25
+ conditions.push(`Subject LIKE '%${escapeSOQLLike(args.subject_contains)}%'`);
26
+ if (args.who_id)
27
+ conditions.push(`WhoId = '${escapeSOQL(args.who_id)}'`);
28
+ if (args.what_id)
29
+ conditions.push(`WhatId = '${escapeSOQL(args.what_id)}'`);
30
+ if (args.start_from)
31
+ conditions.push(`StartDateTime >= ${formatSOQLDateTime(args.start_from, 'start_from')}`);
32
+ if (args.start_to)
33
+ conditions.push(`StartDateTime <= ${formatSOQLDateTime(args.start_to, 'start_to')}`);
34
+ if (conditions.length > 0)
35
+ query += ` WHERE ${conditions.join(' AND ')}`;
36
+ query += ' ORDER BY StartDateTime ASC';
37
+ const limit = Math.min(Math.max(1, args.limit ?? 50), 200);
38
+ query += ` LIMIT ${limit}`;
39
+ const result = await conn.query(query);
40
+ return JSON.stringify({ ok: true, records: sanitizeRecords(result.records, 'salesforce:get_events:records'), totalSize: result.totalSize });
41
+ });
42
+ }));
43
+ server.registerTool('salesforce_create_event', {
44
+ description: `Create a calendar event. Required: subject, start_date_time, end_date_time. Optional: location, description, who_id, what_id, is_all_day_event, owner_id, fields. Datetimes must be ISO 8601 (e.g. "2026-01-09T14:30:00Z"); all-day events accept plain dates (YYYY-MM-DD).`,
45
+ inputSchema: z.object({
46
+ subject: z.string().min(1).describe('Event subject (required)'),
47
+ start_date_time: z.string().min(1).describe('Start (required) — ISO 8601 datetime (see tool description for an example), or YYYY-MM-DD for all-day events'),
48
+ end_date_time: z.string().min(1).describe('End (required) — ISO 8601 datetime, or YYYY-MM-DD for all-day events'),
49
+ location: z.string().optional().describe('Event location'),
50
+ description: z.string().optional().describe('Event description'),
51
+ who_id: z.string().optional().describe('Contact or Lead ID to invite/relate'),
52
+ what_id: z.string().optional().describe('Account or Opportunity ID to relate'),
53
+ is_all_day_event: z.boolean().optional().describe('All-day event (default: false)'),
54
+ owner_id: z.string().optional().describe('Owner User ID'),
55
+ fields: z.record(z.unknown()).optional().describe('Additional/custom fields as key-value pairs'),
56
+ }),
57
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
58
+ }, withErrorHandling(async (args) => {
59
+ // Validate formats up front for actionable errors; the validated values
60
+ // go into the create body verbatim (JSON body — no SOQL injection surface).
61
+ formatSOQLDateTime(args.start_date_time, 'start_date_time');
62
+ formatSOQLDateTime(args.end_date_time, 'end_date_time');
63
+ return withConnection(undefined, async (conn) => {
64
+ const data = {
65
+ Subject: args.subject,
66
+ StartDateTime: args.start_date_time,
67
+ EndDateTime: args.end_date_time,
68
+ };
69
+ if (args.location)
70
+ data.Location = args.location;
71
+ if (args.description)
72
+ data.Description = args.description;
73
+ if (args.who_id)
74
+ data.WhoId = args.who_id;
75
+ if (args.what_id)
76
+ data.WhatId = args.what_id;
77
+ if (args.is_all_day_event !== undefined)
78
+ data.IsAllDayEvent = args.is_all_day_event;
79
+ if (args.owner_id)
80
+ data.OwnerId = args.owner_id;
81
+ if (args.fields)
82
+ validateAndMergeCustomFields(data, args.fields);
83
+ const result = await conn.sobject('Event').create(data);
84
+ if (!result.success)
85
+ throw new ConnectorError('Failed to create event', 'CREATE_ERROR', formatVendorErrors(result.errors));
86
+ return JSON.stringify({ ok: true, status: 'success', object: 'Event', id: result.id, subject: args.subject });
87
+ });
88
+ }));
89
+ }
90
+ //# sourceMappingURL=events.js.map
@@ -4,6 +4,12 @@ export { registerContactTools } from './contacts.js';
4
4
  export { registerOpportunityTools } from './opportunities.js';
5
5
  export { registerLeadTools } from './leads.js';
6
6
  export { registerTaskTools } from './tasks.js';
7
+ export { registerCaseTools } from './cases.js';
8
+ export { registerEventTools } from './events.js';
9
+ export { registerSearchTools } from './search.js';
10
+ export { registerNoteTools } from './notes.js';
11
+ export { registerCampaignTools } from './campaigns.js';
12
+ export { registerReportTools } from './reports.js';
7
13
  export { registerUserTools } from './users.js';
8
14
  export { registerQueryTools } from './query.js';
9
15
  //# sourceMappingURL=index.d.ts.map
@@ -4,6 +4,12 @@ export { registerContactTools } from './contacts.js';
4
4
  export { registerOpportunityTools } from './opportunities.js';
5
5
  export { registerLeadTools } from './leads.js';
6
6
  export { registerTaskTools } from './tasks.js';
7
+ export { registerCaseTools } from './cases.js';
8
+ export { registerEventTools } from './events.js';
9
+ export { registerSearchTools } from './search.js';
10
+ export { registerNoteTools } from './notes.js';
11
+ export { registerCampaignTools } from './campaigns.js';
12
+ export { registerReportTools } from './reports.js';
7
13
  export { registerUserTools } from './users.js';
8
14
  export { registerQueryTools } from './query.js';
9
15
  //# sourceMappingURL=index.js.map