@mindstone/mcp-server-salesforce 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,135 @@
1
+ import { z } from 'zod';
2
+ import { withErrorHandling, escapeSOQL, escapeSOQLLike, validateFields, validateAndMergeCustomFields, checkSaveResult } from '../utils.js';
3
+ import { withConnection } from '../client.js';
4
+ import { ConnectorError } from '../types.js';
5
+ export function registerLeadTools(server) {
6
+ server.registerTool('salesforce_get_leads', {
7
+ description: `Get leads. Filters: name_contains, company_contains, email_contains, status. 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
+ name_contains: z.string().optional().describe('Filter by name'),
11
+ company_contains: z.string().optional().describe('Filter by company'),
12
+ email_contains: z.string().optional().describe('Filter by email'),
13
+ status: z.string().optional().describe('Filter by status'),
14
+ fields: z.array(z.string()).optional().describe('Custom fields'),
15
+ }),
16
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
17
+ }, withErrorHandling(async (args) => {
18
+ return withConnection(undefined, async (conn) => {
19
+ const defaultFields = ['Id', 'FirstName', 'LastName', 'Company', 'Email', 'Phone', 'Status', 'Title'];
20
+ const fields = validateFields(args.fields || [], defaultFields);
21
+ let query = `SELECT ${fields.join(', ')} FROM Lead`;
22
+ const conditions = [];
23
+ if (args.name_contains) {
24
+ const escaped = escapeSOQLLike(args.name_contains);
25
+ conditions.push(`(FirstName LIKE '%${escaped}%' OR LastName LIKE '%${escaped}%')`);
26
+ }
27
+ if (args.company_contains)
28
+ conditions.push(`Company LIKE '%${escapeSOQLLike(args.company_contains)}%'`);
29
+ if (args.email_contains)
30
+ conditions.push(`Email LIKE '%${escapeSOQLLike(args.email_contains)}%'`);
31
+ if (args.status)
32
+ conditions.push(`Status = '${escapeSOQL(args.status)}'`);
33
+ if (conditions.length > 0)
34
+ query += ` WHERE ${conditions.join(' AND ')}`;
35
+ const limit = Math.min(Math.max(1, args.limit ?? 50), 200);
36
+ query += ` LIMIT ${limit}`;
37
+ const result = await conn.query(query);
38
+ return JSON.stringify({ ok: true, records: result.records, totalSize: result.totalSize });
39
+ });
40
+ }));
41
+ server.registerTool('salesforce_create_lead', {
42
+ description: `Create a lead. Required: last_name, company. Optional: first_name, email, phone, title, status, fields.`,
43
+ inputSchema: z.object({
44
+ first_name: z.string().optional().describe('First name'),
45
+ last_name: z.string().min(1).describe('Last name (required)'),
46
+ company: z.string().min(1).describe('Company name (required)'),
47
+ email: z.string().optional().describe('Email address'),
48
+ phone: z.string().optional().describe('Phone number'),
49
+ title: z.string().optional().describe('Job title'),
50
+ status: z.string().optional().describe('Lead status'),
51
+ fields: z.record(z.unknown()).optional().describe('Additional/custom fields'),
52
+ }),
53
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
54
+ }, withErrorHandling(async (args) => {
55
+ return withConnection(undefined, async (conn) => {
56
+ const data = { LastName: args.last_name, Company: args.company };
57
+ if (args.first_name)
58
+ data.FirstName = args.first_name;
59
+ if (args.email)
60
+ data.Email = args.email;
61
+ if (args.phone)
62
+ data.Phone = args.phone;
63
+ if (args.title)
64
+ data.Title = args.title;
65
+ if (args.status)
66
+ data.Status = args.status;
67
+ if (args.fields)
68
+ validateAndMergeCustomFields(data, args.fields);
69
+ const result = await conn.sobject('Lead').create(data);
70
+ if (!result.success)
71
+ throw new ConnectorError('Failed to create lead', 'CREATE_ERROR', JSON.stringify(result.errors));
72
+ return JSON.stringify({ ok: true, status: 'success', object: 'Lead', id: result.id, name: `${args.first_name || ''} ${args.last_name}`.trim() });
73
+ });
74
+ }));
75
+ server.registerTool('salesforce_convert_lead', {
76
+ description: `Convert a lead into Account + Contact, and optionally an Opportunity. Required: lead_id. Optional: create_opportunity, opportunity_name.`,
77
+ inputSchema: z.object({
78
+ lead_id: z.string().min(1).describe('Lead ID to convert (required)'),
79
+ create_opportunity: z.boolean().optional().describe('Create opportunity (default: true)'),
80
+ opportunity_name: z.string().optional().describe('Name for new opportunity'),
81
+ }),
82
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
83
+ }, withErrorHandling(async (args) => {
84
+ return withConnection(undefined, async (conn) => {
85
+ const leadConvert = {
86
+ leadId: args.lead_id,
87
+ convertedStatus: 'Closed - Converted',
88
+ doNotCreateOpportunity: args.create_opportunity === false,
89
+ };
90
+ const result = await conn
91
+ .soap.convertLead([leadConvert]);
92
+ return JSON.stringify({ ok: true, status: 'success', result: result[0] });
93
+ });
94
+ }));
95
+ server.registerTool('salesforce_update_lead', {
96
+ description: `Update a lead. Required: lead_id. Updatable: first_name, last_name, company, email, phone, title, status, fields.`,
97
+ inputSchema: z.object({
98
+ lead_id: z.string().min(1).describe('Salesforce Lead ID (required)'),
99
+ first_name: z.string().optional().describe('First name'),
100
+ last_name: z.string().optional().describe('Last name'),
101
+ company: z.string().optional().describe('Company name'),
102
+ email: z.string().optional().describe('Email address'),
103
+ phone: z.string().optional().describe('Phone number'),
104
+ title: z.string().optional().describe('Job title'),
105
+ status: z.string().optional().describe('Lead status'),
106
+ fields: z.record(z.unknown()).optional().describe('Additional/custom fields'),
107
+ }),
108
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
109
+ }, withErrorHandling(async (args) => {
110
+ return withConnection(undefined, async (conn) => {
111
+ const updateData = { Id: args.lead_id };
112
+ if (args.first_name)
113
+ updateData.FirstName = args.first_name;
114
+ if (args.last_name)
115
+ updateData.LastName = args.last_name;
116
+ if (args.company)
117
+ updateData.Company = args.company;
118
+ if (args.email)
119
+ updateData.Email = args.email;
120
+ if (args.phone)
121
+ updateData.Phone = args.phone;
122
+ if (args.title)
123
+ updateData.Title = args.title;
124
+ if (args.status)
125
+ updateData.Status = args.status;
126
+ if (args.fields)
127
+ validateAndMergeCustomFields(updateData, args.fields);
128
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
129
+ const result = await conn.sobject('Lead').update(updateData);
130
+ checkSaveResult(result, 'Failed to update lead');
131
+ return JSON.stringify({ ok: true, status: 'success', object: 'Lead', id: args.lead_id });
132
+ });
133
+ }));
134
+ }
135
+ //# sourceMappingURL=leads.js.map
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerOpportunityTools(server: McpServer): void;
3
+ //# sourceMappingURL=opportunities.d.ts.map
@@ -0,0 +1,108 @@
1
+ import { z } from 'zod';
2
+ import { withErrorHandling, escapeSOQL, escapeSOQLLike, validateFields, validateAndMergeCustomFields, formatSOQLDate, checkSaveResult } from '../utils.js';
3
+ import { withConnection } from '../client.js';
4
+ import { ConnectorError } from '../types.js';
5
+ export function registerOpportunityTools(server) {
6
+ server.registerTool('salesforce_get_opportunities', {
7
+ description: `Get opportunities. Filters: name_contains, stage, related_account_id, close_date_from, close_date_to. Dates must be YYYY-MM-DD. 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
+ name_contains: z.string().optional().describe('Filter by name'),
11
+ stage: z.string().optional().describe('Filter by stage'),
12
+ related_account_id: z.string().optional().describe('Filter by Account ID'),
13
+ close_date_from: z.string().optional().describe('Closing on/after date (YYYY-MM-DD)'),
14
+ close_date_to: z.string().optional().describe('Closing on/before date (YYYY-MM-DD)'),
15
+ fields: z.array(z.string()).optional().describe('Custom fields'),
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', 'StageName', 'Amount', 'CloseDate', 'AccountId', 'Probability'];
21
+ const fields = validateFields(args.fields || [], defaultFields);
22
+ let query = `SELECT ${fields.join(', ')} FROM Opportunity`;
23
+ const conditions = [];
24
+ if (args.name_contains)
25
+ conditions.push(`Name LIKE '%${escapeSOQLLike(args.name_contains)}%'`);
26
+ if (args.stage)
27
+ conditions.push(`StageName = '${escapeSOQL(args.stage)}'`);
28
+ if (args.related_account_id)
29
+ conditions.push(`AccountId = '${escapeSOQL(args.related_account_id)}'`);
30
+ if (args.close_date_from)
31
+ conditions.push(`CloseDate >= ${formatSOQLDate(args.close_date_from, 'close_date_from')}`);
32
+ if (args.close_date_to)
33
+ conditions.push(`CloseDate <= ${formatSOQLDate(args.close_date_to, 'close_date_to')}`);
34
+ if (conditions.length > 0)
35
+ query += ` WHERE ${conditions.join(' AND ')}`;
36
+ const limit = Math.min(Math.max(1, args.limit ?? 50), 200);
37
+ query += ` LIMIT ${limit}`;
38
+ const result = await conn.query(query);
39
+ return JSON.stringify({ ok: true, records: result.records, totalSize: result.totalSize });
40
+ });
41
+ }));
42
+ server.registerTool('salesforce_create_opportunity', {
43
+ description: `Create an opportunity. Required: name, stage_name, close_date (YYYY-MM-DD). Optional: amount, related_account_id, description, fields.`,
44
+ inputSchema: z.object({
45
+ name: z.string().min(1).describe('Opportunity name (required)'),
46
+ stage_name: z.string().min(1).describe('Stage name (required)'),
47
+ close_date: z.string().min(1).describe('Close date YYYY-MM-DD (required)'),
48
+ amount: z.number().optional().describe('Opportunity amount'),
49
+ related_account_id: z.string().optional().describe('Account ID to associate with'),
50
+ description: z.string().optional().describe('Description'),
51
+ fields: z.record(z.unknown()).optional().describe('Additional/custom fields as key-value pairs'),
52
+ }),
53
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
54
+ }, withErrorHandling(async (args) => {
55
+ formatSOQLDate(args.close_date, 'close_date');
56
+ return withConnection(undefined, async (conn) => {
57
+ const data = { Name: args.name, StageName: args.stage_name, CloseDate: args.close_date };
58
+ if (args.amount !== undefined)
59
+ data.Amount = args.amount;
60
+ if (args.related_account_id)
61
+ data.AccountId = args.related_account_id;
62
+ if (args.description)
63
+ data.Description = args.description;
64
+ if (args.fields)
65
+ validateAndMergeCustomFields(data, args.fields);
66
+ const result = await conn.sobject('Opportunity').create(data);
67
+ if (!result.success)
68
+ throw new ConnectorError('Failed to create opportunity', 'CREATE_ERROR', JSON.stringify(result.errors));
69
+ return JSON.stringify({ ok: true, status: 'success', object: 'Opportunity', id: result.id, name: args.name });
70
+ });
71
+ }));
72
+ server.registerTool('salesforce_update_opportunity', {
73
+ description: `Update an opportunity. Required: id. Updatable: name, stage_name, close_date (YYYY-MM-DD), amount, description, fields.`,
74
+ inputSchema: z.object({
75
+ id: z.string().min(1).describe('Salesforce Opportunity ID (required)'),
76
+ name: z.string().optional().describe('Opportunity name'),
77
+ stage_name: z.string().optional().describe('Stage name'),
78
+ close_date: z.string().optional().describe('Close date YYYY-MM-DD'),
79
+ amount: z.number().optional().describe('Amount'),
80
+ description: z.string().optional().describe('Description'),
81
+ fields: z.record(z.unknown()).optional().describe('Additional/custom fields'),
82
+ }),
83
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
84
+ }, withErrorHandling(async (args) => {
85
+ if (args.close_date)
86
+ formatSOQLDate(args.close_date, 'close_date');
87
+ return withConnection(undefined, async (conn) => {
88
+ const updateData = { Id: args.id };
89
+ if (args.name)
90
+ updateData.Name = args.name;
91
+ if (args.stage_name)
92
+ updateData.StageName = args.stage_name;
93
+ if (args.close_date)
94
+ updateData.CloseDate = args.close_date;
95
+ if (args.amount !== undefined)
96
+ updateData.Amount = args.amount;
97
+ if (args.description)
98
+ updateData.Description = args.description;
99
+ if (args.fields)
100
+ validateAndMergeCustomFields(updateData, args.fields);
101
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
102
+ const result = await conn.sobject('Opportunity').update(updateData);
103
+ checkSaveResult(result, 'Failed to update opportunity');
104
+ return JSON.stringify({ ok: true, status: 'success', object: 'Opportunity', id: args.id });
105
+ });
106
+ }));
107
+ }
108
+ //# sourceMappingURL=opportunities.js.map
@@ -0,0 +1,17 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ /**
3
+ * Enforce a hard cap on the LIMIT clause of a caller-supplied SOQL query.
4
+ *
5
+ * The cap is applied AFTER stripping line and block comments and trailing
6
+ * whitespace, so trailing-comment/whitespace bypass attempts (LIMIT 5000
7
+ * followed by trailing whitespace, line comments, or block comments) are
8
+ * neutralised. OFFSET clauses are preserved on the output side, and a
9
+ * caller-supplied OFFSET-without-LIMIT triggers an inserted cap.
10
+ *
11
+ * The result is a single, syntactically valid SOQL statement with exactly
12
+ * one `LIMIT <n>` clause where `n <= maxLimit`, and at most one trailing
13
+ * `OFFSET <n>` clause.
14
+ */
15
+ export declare function applyQueryLimitCap(rawQuery: string, maxLimit: number): string;
16
+ export declare function registerQueryTools(server: McpServer): void;
17
+ //# sourceMappingURL=query.d.ts.map
@@ -0,0 +1,267 @@
1
+ import { z } from 'zod';
2
+ import { withErrorHandling, validateObjectName, validateFields, isValidQueryFieldName, isValidFieldName, escapeSOQL, ALLOWED_FILTER_OPERATORS, validateAndMergeCustomFields, checkSaveResult } from '../utils.js';
3
+ import { withConnection } from '../client.js';
4
+ import { ConnectorError } from '../types.js';
5
+ // Strip SQL-style line ("// ...") and block ("/* ... ") comments from a
6
+ // SOQL query. SOQL itself does not officially support either form, but
7
+ // we defensively strip both because callers (or attackers crafting
8
+ // tool-arguments) commonly use them to evade naive trailing-LIMIT
9
+ // regexes.
10
+ //
11
+ // The walker is QUOTE-AWARE: characters inside a SOQL single-quoted string
12
+ // literal are preserved verbatim. SOQL string literals support two escape
13
+ // forms for an embedded apostrophe: doubled-quote `''` and backslash-quote
14
+ // `\'`. We honour both, plus generic backslash-escapes (`\\`, `\n`, etc.)
15
+ // which simply consume the next character. Without quote-awareness, a
16
+ // query like `WHERE Website = 'https://example.com/path'` would be
17
+ // corrupted to `WHERE Website = 'https:` (unterminated literal), since
18
+ // the naive global regex would treat the `//` inside the quoted span as
19
+ // a line comment.
20
+ function stripSoqlComments(query) {
21
+ let out = '';
22
+ let i = 0;
23
+ const n = query.length;
24
+ while (i < n) {
25
+ const ch = query[i];
26
+ // Inside a single-quoted SOQL string literal: copy verbatim until the
27
+ // matching close-quote, honouring backslash escapes and the doubled-
28
+ // apostrophe escape ('').
29
+ if (ch === "'") {
30
+ out += ch;
31
+ i++;
32
+ while (i < n) {
33
+ const c = query[i];
34
+ if (c === '\\' && i + 1 < n) {
35
+ // Generic backslash escape: keep both characters verbatim.
36
+ out += c + query[i + 1];
37
+ i += 2;
38
+ continue;
39
+ }
40
+ if (c === "'") {
41
+ // Doubled-apostrophe = literal apostrophe inside the literal.
42
+ if (i + 1 < n && query[i + 1] === "'") {
43
+ out += "''";
44
+ i += 2;
45
+ continue;
46
+ }
47
+ // Closing quote.
48
+ out += "'";
49
+ i++;
50
+ break;
51
+ }
52
+ out += c;
53
+ i++;
54
+ }
55
+ continue;
56
+ }
57
+ // Outside any quoted literal: strip block + line comments.
58
+ if (ch === '/' && i + 1 < n) {
59
+ const next = query[i + 1];
60
+ if (next === '*') {
61
+ // Block comment: consume up to the closing */ (or EOF).
62
+ i += 2;
63
+ while (i < n && !(query[i] === '*' && i + 1 < n && query[i + 1] === '/')) {
64
+ i++;
65
+ }
66
+ if (i < n)
67
+ i += 2; // skip the closing */
68
+ out += ' ';
69
+ continue;
70
+ }
71
+ if (next === '/') {
72
+ // Line comment: consume up to (but not including) the newline.
73
+ i += 2;
74
+ while (i < n && query[i] !== '\n')
75
+ i++;
76
+ out += ' ';
77
+ continue;
78
+ }
79
+ }
80
+ out += ch;
81
+ i++;
82
+ }
83
+ return out;
84
+ }
85
+ /**
86
+ * Enforce a hard cap on the LIMIT clause of a caller-supplied SOQL query.
87
+ *
88
+ * The cap is applied AFTER stripping line and block comments and trailing
89
+ * whitespace, so trailing-comment/whitespace bypass attempts (LIMIT 5000
90
+ * followed by trailing whitespace, line comments, or block comments) are
91
+ * neutralised. OFFSET clauses are preserved on the output side, and a
92
+ * caller-supplied OFFSET-without-LIMIT triggers an inserted cap.
93
+ *
94
+ * The result is a single, syntactically valid SOQL statement with exactly
95
+ * one `LIMIT <n>` clause where `n <= maxLimit`, and at most one trailing
96
+ * `OFFSET <n>` clause.
97
+ */
98
+ export function applyQueryLimitCap(rawQuery, maxLimit) {
99
+ const cleaned = stripSoqlComments(rawQuery).trim();
100
+ let base = cleaned;
101
+ let offsetClause = '';
102
+ const offsetMatch = base.match(/^([\s\S]*?)\s+OFFSET\s+(\d+)\s*$/i);
103
+ if (offsetMatch) {
104
+ base = offsetMatch[1];
105
+ offsetClause = ` OFFSET ${offsetMatch[2]}`;
106
+ }
107
+ const limitMatch = base.match(/^([\s\S]*?)\s+LIMIT\s+(\d+)\s*$/i);
108
+ let head;
109
+ let limit;
110
+ if (limitMatch) {
111
+ head = limitMatch[1].trimEnd();
112
+ const current = parseInt(limitMatch[2], 10);
113
+ limit = Math.min(current, maxLimit);
114
+ }
115
+ else {
116
+ head = base.trimEnd();
117
+ limit = maxLimit;
118
+ }
119
+ return `${head} LIMIT ${limit}${offsetClause}`;
120
+ }
121
+ export function registerQueryTools(server) {
122
+ server.registerTool('salesforce_query', {
123
+ description: `Execute a raw SOQL query. For advanced queries only — prefer dedicated tools for standard operations. Max 200 records enforced.`,
124
+ inputSchema: z.object({
125
+ query: z.string().min(1).describe('SOQL query string'),
126
+ }),
127
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
128
+ }, withErrorHandling(async (args) => {
129
+ return withConnection(undefined, async (conn) => {
130
+ const MAX_LIMIT = 200;
131
+ const query = applyQueryLimitCap(args.query, MAX_LIMIT);
132
+ const result = await conn.query(query);
133
+ return JSON.stringify({ ok: true, records: result.records, totalSize: result.totalSize, done: result.done });
134
+ });
135
+ }));
136
+ server.registerTool('salesforce_describe_object', {
137
+ description: `Get object metadata and field definitions. Returns field names, types, and required flags. Common objects: Account, Contact, Opportunity, Lead, Case, Task.`,
138
+ inputSchema: z.object({
139
+ object_name: z.string().min(1).describe('Object API name (Account, Contact, Opportunity, Lead, CustomObject__c)'),
140
+ }),
141
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
142
+ }, withErrorHandling(async (args) => {
143
+ return withConnection(undefined, async (conn) => {
144
+ const metadata = await conn.sobject(args.object_name).describe();
145
+ return JSON.stringify({
146
+ ok: true,
147
+ name: metadata.name,
148
+ label: metadata.label,
149
+ labelPlural: metadata.labelPlural,
150
+ fields: metadata.fields.map((f) => ({
151
+ name: f.name,
152
+ label: f.label,
153
+ type: f.type,
154
+ required: !f.nillable && !f.defaultedOnCreate,
155
+ updateable: f.updateable,
156
+ createable: f.createable,
157
+ })),
158
+ recordTypeInfos: metadata.recordTypeInfos,
159
+ });
160
+ });
161
+ }));
162
+ server.registerTool('salesforce_list_objects', {
163
+ description: `List available Salesforce sObjects. Default shows custom objects only. Set custom_only=false to include standard objects.`,
164
+ inputSchema: z.object({
165
+ custom_only: z.boolean().optional().describe('Show only custom objects (default: true)'),
166
+ }),
167
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
168
+ }, withErrorHandling(async (args) => {
169
+ return withConnection(undefined, async (conn) => {
170
+ const result = await conn.describeGlobal();
171
+ let objects = result.sobjects.map((s) => ({
172
+ name: s.name,
173
+ label: s.label,
174
+ queryable: s.queryable,
175
+ createable: s.createable,
176
+ updateable: s.updateable,
177
+ custom: s.custom,
178
+ }));
179
+ const customOnly = args.custom_only !== false;
180
+ if (customOnly)
181
+ objects = objects.filter((o) => o.custom);
182
+ return JSON.stringify({ ok: true, objects, count: objects.length });
183
+ });
184
+ }));
185
+ server.registerTool('salesforce_create_record', {
186
+ description: `Generic record creation for any Salesforce object. For standard objects, prefer dedicated tools.`,
187
+ inputSchema: z.object({
188
+ object_name: z.string().min(1).describe('sObject API name (e.g., Invoice__c, Case)'),
189
+ fields: z.record(z.unknown()).describe('Field-value pairs to set on the new record'),
190
+ }),
191
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
192
+ }, withErrorHandling(async (args) => {
193
+ validateObjectName(args.object_name);
194
+ return withConnection(undefined, async (conn) => {
195
+ const data = {};
196
+ validateAndMergeCustomFields(data, args.fields);
197
+ const result = await conn.sobject(args.object_name).create(data);
198
+ if (!result.success)
199
+ throw new ConnectorError(`Failed to create ${args.object_name} record`, 'CREATE_ERROR', JSON.stringify(result.errors));
200
+ return JSON.stringify({ ok: true, status: 'success', object: args.object_name, id: result.id });
201
+ });
202
+ }));
203
+ server.registerTool('salesforce_update_record', {
204
+ description: `Generic record update for any Salesforce object. For standard objects, prefer dedicated tools.`,
205
+ inputSchema: z.object({
206
+ object_name: z.string().min(1).describe('sObject API name'),
207
+ id: z.string().min(1).describe('Salesforce record ID to update'),
208
+ fields: z.record(z.unknown()).describe('Field-value pairs to update'),
209
+ }),
210
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
211
+ }, withErrorHandling(async (args) => {
212
+ validateObjectName(args.object_name);
213
+ return withConnection(undefined, async (conn) => {
214
+ const updateData = { Id: args.id };
215
+ validateAndMergeCustomFields(updateData, args.fields);
216
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
217
+ const result = await conn.sobject(args.object_name).update(updateData);
218
+ checkSaveResult(result, `Failed to update ${args.object_name} record`);
219
+ return JSON.stringify({ ok: true, status: 'success', object: args.object_name, id: args.id });
220
+ });
221
+ }));
222
+ server.registerTool('salesforce_get_records', {
223
+ description: `Generic record query for any Salesforce object. For standard objects, prefer dedicated get tools.`,
224
+ inputSchema: z.object({
225
+ object_name: z.string().min(1).describe('sObject API name'),
226
+ fields: z.array(z.string()).optional().describe('Fields to SELECT (defaults to Id)'),
227
+ filters: z.array(z.object({
228
+ field: z.string().describe('Field API name'),
229
+ operator: z.enum(['=', '!=', '<', '>', '<=', '>=', 'LIKE']).describe('Comparison operator'),
230
+ value: z.union([z.string(), z.number(), z.boolean()]).describe('Filter value'),
231
+ })).optional().describe('WHERE conditions (AND-joined)'),
232
+ limit: z.number().int().min(1).max(200).optional().describe('Max results 1-200 (default: 50)'),
233
+ }),
234
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
235
+ }, withErrorHandling(async (args) => {
236
+ validateObjectName(args.object_name);
237
+ return withConnection(undefined, async (conn) => {
238
+ const fields = validateFields(args.fields || [], ['Id'], isValidQueryFieldName);
239
+ let soql = `SELECT ${fields.join(', ')} FROM ${args.object_name}`;
240
+ if (args.filters && args.filters.length > 0) {
241
+ const conditions = [];
242
+ for (const filter of args.filters) {
243
+ if (!isValidFieldName(filter.field)) {
244
+ throw new ConnectorError(`Invalid filter field name: "${filter.field}"`, 'INVALID_FIELD_NAMES', 'Field names must be valid API names');
245
+ }
246
+ if (!ALLOWED_FILTER_OPERATORS.has(filter.operator)) {
247
+ throw new ConnectorError(`Invalid filter operator: "${filter.operator}"`, 'INVALID_OPERATOR', `Allowed: ${[...ALLOWED_FILTER_OPERATORS].join(', ')}`);
248
+ }
249
+ let formattedValue;
250
+ if (typeof filter.value === 'string') {
251
+ formattedValue = `'${escapeSOQL(filter.value)}'`;
252
+ }
253
+ else {
254
+ formattedValue = `${filter.value}`;
255
+ }
256
+ conditions.push(`${filter.field} ${filter.operator} ${formattedValue}`);
257
+ }
258
+ soql += ` WHERE ${conditions.join(' AND ')}`;
259
+ }
260
+ const limit = Math.min(Math.max(1, args.limit ?? 50), 200);
261
+ soql += ` LIMIT ${limit}`;
262
+ const result = await conn.query(soql);
263
+ return JSON.stringify({ ok: true, records: result.records, totalSize: result.totalSize, done: result.done });
264
+ });
265
+ }));
266
+ }
267
+ //# sourceMappingURL=query.js.map
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerTaskTools(server: McpServer): void;
3
+ //# sourceMappingURL=tasks.d.ts.map