@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 +28 -3
- package/dist/client.js +2 -1
- package/dist/server.js +7 -1
- package/dist/tools/accounts.js +4 -4
- package/dist/tools/auth-tools.js +47 -12
- package/dist/tools/campaigns.d.ts +3 -0
- package/dist/tools/campaigns.js +78 -0
- package/dist/tools/cases.d.ts +3 -0
- package/dist/tools/cases.js +121 -0
- package/dist/tools/contacts.js +4 -4
- package/dist/tools/events.d.ts +3 -0
- package/dist/tools/events.js +90 -0
- package/dist/tools/index.d.ts +6 -0
- package/dist/tools/index.js +6 -0
- package/dist/tools/leads.js +16 -8
- package/dist/tools/notes.d.ts +3 -0
- package/dist/tools/notes.js +111 -0
- package/dist/tools/opportunities.js +4 -4
- package/dist/tools/query.js +13 -10
- package/dist/tools/reports.d.ts +3 -0
- package/dist/tools/reports.js +39 -0
- package/dist/tools/search.d.ts +3 -0
- package/dist/tools/search.js +50 -0
- package/dist/tools/tasks.js +4 -4
- package/dist/tools/users.js +2 -2
- package/dist/types.d.ts +8 -0
- package/dist/types.js +8 -0
- package/dist/untrusted-content.d.ts +37 -0
- package/dist/untrusted-content.js +70 -0
- package/dist/utils.d.ts +34 -0
- package/dist/utils.js +130 -3
- package/package.json +1 -1
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
|
|
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:** [
|
|
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 (
|
|
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;
|
package/dist/tools/accounts.js
CHANGED
|
@@ -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:
|
|
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',
|
|
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
|
});
|
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}`,
|
|
@@ -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,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
|
package/dist/tools/contacts.js
CHANGED
|
@@ -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:
|
|
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',
|
|
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,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
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -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
|
package/dist/tools/index.js
CHANGED
|
@@ -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
|