@mindstone/mcp-server-salesforce 0.1.3 → 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/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/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.
|
|
12
|
+
- **Version:** [0.2.0](./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
|
});
|
|
@@ -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
|
package/dist/tools/leads.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, sanitizeExternalData } from '../utils.js';
|
|
3
3
|
import { withConnection } from '../client.js';
|
|
4
4
|
import { ConnectorError } from '../types.js';
|
|
5
5
|
export function registerLeadTools(server) {
|
|
@@ -35,7 +35,7 @@ export function registerLeadTools(server) {
|
|
|
35
35
|
const limit = Math.min(Math.max(1, args.limit ?? 50), 200);
|
|
36
36
|
query += ` LIMIT ${limit}`;
|
|
37
37
|
const result = await conn.query(query);
|
|
38
|
-
return JSON.stringify({ ok: true, records: result.records, totalSize: result.totalSize });
|
|
38
|
+
return JSON.stringify({ ok: true, records: sanitizeRecords(result.records, 'salesforce:get_leads:records'), totalSize: result.totalSize });
|
|
39
39
|
});
|
|
40
40
|
}));
|
|
41
41
|
server.registerTool('salesforce_create_lead', {
|
|
@@ -50,7 +50,7 @@ export function registerLeadTools(server) {
|
|
|
50
50
|
status: z.string().optional().describe('Lead status'),
|
|
51
51
|
fields: z.record(z.unknown()).optional().describe('Additional/custom fields'),
|
|
52
52
|
}),
|
|
53
|
-
annotations: { readOnlyHint: false, destructiveHint:
|
|
53
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
54
54
|
}, withErrorHandling(async (args) => {
|
|
55
55
|
return withConnection(undefined, async (conn) => {
|
|
56
56
|
const data = { LastName: args.last_name, Company: args.company };
|
|
@@ -68,7 +68,7 @@ export function registerLeadTools(server) {
|
|
|
68
68
|
validateAndMergeCustomFields(data, args.fields);
|
|
69
69
|
const result = await conn.sobject('Lead').create(data);
|
|
70
70
|
if (!result.success)
|
|
71
|
-
throw new ConnectorError('Failed to create lead', 'CREATE_ERROR',
|
|
71
|
+
throw new ConnectorError('Failed to create lead', 'CREATE_ERROR', formatVendorErrors(result.errors));
|
|
72
72
|
return JSON.stringify({ ok: true, status: 'success', object: 'Lead', id: result.id, name: `${args.first_name || ''} ${args.last_name}`.trim() });
|
|
73
73
|
});
|
|
74
74
|
}));
|
|
@@ -79,7 +79,7 @@ export function registerLeadTools(server) {
|
|
|
79
79
|
create_opportunity: z.boolean().optional().describe('Create opportunity (default: true)'),
|
|
80
80
|
opportunity_name: z.string().optional().describe('Name for new opportunity'),
|
|
81
81
|
}),
|
|
82
|
-
annotations: { readOnlyHint: false, destructiveHint:
|
|
82
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
83
83
|
}, withErrorHandling(async (args) => {
|
|
84
84
|
return withConnection(undefined, async (conn) => {
|
|
85
85
|
const leadConvert = {
|
|
@@ -87,9 +87,17 @@ export function registerLeadTools(server) {
|
|
|
87
87
|
convertedStatus: 'Closed - Converted',
|
|
88
88
|
doNotCreateOpportunity: args.create_opportunity === false,
|
|
89
89
|
};
|
|
90
|
-
const result = await conn
|
|
91
|
-
|
|
92
|
-
|
|
90
|
+
const result = await conn.soap.convertLead([leadConvert]);
|
|
91
|
+
const convertResult = result[0];
|
|
92
|
+
// SOAP convertLead reports record-level failures IN THE RESULT
|
|
93
|
+
// (success:false + errors[]), not as a fault — checking success is the
|
|
94
|
+
// only way to avoid reporting a conversion that never happened. The
|
|
95
|
+
// error messages are org-authored validation-rule text, so they are
|
|
96
|
+
// enveloped like every other vendor error (invariant #6).
|
|
97
|
+
if (!convertResult.success) {
|
|
98
|
+
throw new ConnectorError('Failed to convert lead', 'CONVERT_ERROR', formatVendorErrors(convertResult.errors));
|
|
99
|
+
}
|
|
100
|
+
return JSON.stringify({ ok: true, status: 'success', result: sanitizeExternalData(convertResult, 'salesforce:convert_lead:result') });
|
|
93
101
|
});
|
|
94
102
|
}));
|
|
95
103
|
server.registerTool('salesforce_update_lead', {
|