adoptai-mcp 1.0.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 +70 -0
- package/bin/adoptai-mcp.js +2 -0
- package/dist/apps/canva.js +1 -0
- package/dist/apps/figma.js +1 -0
- package/dist/apps/github.js +2 -0
- package/dist/apps/notion.js +1 -0
- package/dist/apps/registry.js +20 -0
- package/dist/apps/salesforce.js +1 -0
- package/dist/cli/add.js +532 -0
- package/dist/cli/index.js +39 -0
- package/dist/cli/list.js +19 -0
- package/dist/cli/remove.js +37 -0
- package/dist/cli/serve.js +27 -0
- package/dist/cli/status.js +24 -0
- package/dist/config/clients.js +118 -0
- package/dist/config/credentials.js +34 -0
- package/dist/core/auth-manager.js +237 -0
- package/dist/core/config-writer.js +161 -0
- package/dist/core/doctor.js +199 -0
- package/dist/core/package.json +3 -0
- package/dist/core/server-base.js +81 -0
- package/dist/integrations/canva/.env +3 -0
- package/dist/integrations/canva/auth.js +287 -0
- package/dist/integrations/canva/env.js +9 -0
- package/dist/integrations/canva/index.js +12 -0
- package/dist/integrations/canva/package.json +31 -0
- package/dist/integrations/canva/publish-to-adoptai.js +365 -0
- package/dist/integrations/canva/setup.js +90 -0
- package/dist/integrations/canva/tools.js +1315 -0
- package/dist/integrations/canva/tools.original.js +1315 -0
- package/dist/integrations/figma/auth.js +48 -0
- package/dist/integrations/figma/index.js +11 -0
- package/dist/integrations/figma/package.json +27 -0
- package/dist/integrations/figma/publish-to-adoptai.js +384 -0
- package/dist/integrations/figma/setup.js +90 -0
- package/dist/integrations/figma/tools.js +1137 -0
- package/dist/integrations/github/auth.js +53 -0
- package/dist/integrations/github/index.js +11 -0
- package/dist/integrations/github/package.json +28 -0
- package/dist/integrations/github/publish-to-adoptai.js +240 -0
- package/dist/integrations/github/setup.js +103 -0
- package/dist/integrations/github/tools.js +78 -0
- package/dist/integrations/github-actions/auth.js +53 -0
- package/dist/integrations/github-actions/index.js +11 -0
- package/dist/integrations/github-actions/package.json +27 -0
- package/dist/integrations/github-actions/setup.js +103 -0
- package/dist/integrations/github-actions/tools.js +5642 -0
- package/dist/integrations/github-activity/auth.js +53 -0
- package/dist/integrations/github-activity/index.js +11 -0
- package/dist/integrations/github-activity/package.json +27 -0
- package/dist/integrations/github-activity/setup.js +103 -0
- package/dist/integrations/github-activity/tools.js +925 -0
- package/dist/integrations/github-apps/auth.js +53 -0
- package/dist/integrations/github-apps/index.js +11 -0
- package/dist/integrations/github-apps/package.json +27 -0
- package/dist/integrations/github-apps/setup.js +103 -0
- package/dist/integrations/github-apps/tools.js +791 -0
- package/dist/integrations/github-billing/auth.js +53 -0
- package/dist/integrations/github-billing/index.js +11 -0
- package/dist/integrations/github-billing/package.json +27 -0
- package/dist/integrations/github-billing/setup.js +103 -0
- package/dist/integrations/github-billing/tools.js +438 -0
- package/dist/integrations/github-checks/auth.js +53 -0
- package/dist/integrations/github-checks/index.js +11 -0
- package/dist/integrations/github-checks/package.json +27 -0
- package/dist/integrations/github-checks/setup.js +103 -0
- package/dist/integrations/github-checks/tools.js +607 -0
- package/dist/integrations/github-code-scanning/auth.js +53 -0
- package/dist/integrations/github-code-scanning/index.js +11 -0
- package/dist/integrations/github-code-scanning/package.json +27 -0
- package/dist/integrations/github-code-scanning/setup.js +103 -0
- package/dist/integrations/github-code-scanning/tools.js +987 -0
- package/dist/integrations/github-dependabot/auth.js +53 -0
- package/dist/integrations/github-dependabot/index.js +11 -0
- package/dist/integrations/github-dependabot/package.json +27 -0
- package/dist/integrations/github-dependabot/setup.js +103 -0
- package/dist/integrations/github-dependabot/tools.js +915 -0
- package/dist/integrations/github-gists/auth.js +53 -0
- package/dist/integrations/github-gists/index.js +11 -0
- package/dist/integrations/github-gists/package.json +27 -0
- package/dist/integrations/github-gists/setup.js +103 -0
- package/dist/integrations/github-gists/tools.js +545 -0
- package/dist/integrations/github-git/auth.js +53 -0
- package/dist/integrations/github-git/index.js +11 -0
- package/dist/integrations/github-git/package.json +27 -0
- package/dist/integrations/github-git/setup.js +103 -0
- package/dist/integrations/github-git/tools.js +513 -0
- package/dist/integrations/github-issues/auth.js +53 -0
- package/dist/integrations/github-issues/index.js +11 -0
- package/dist/integrations/github-issues/package.json +27 -0
- package/dist/integrations/github-issues/setup.js +103 -0
- package/dist/integrations/github-issues/tools.js +2232 -0
- package/dist/integrations/github-orgs/auth.js +53 -0
- package/dist/integrations/github-orgs/index.js +11 -0
- package/dist/integrations/github-orgs/package.json +27 -0
- package/dist/integrations/github-orgs/setup.js +103 -0
- package/dist/integrations/github-orgs/tools.js +3512 -0
- package/dist/integrations/github-packages/auth.js +53 -0
- package/dist/integrations/github-packages/index.js +11 -0
- package/dist/integrations/github-packages/package.json +27 -0
- package/dist/integrations/github-packages/setup.js +103 -0
- package/dist/integrations/github-packages/tools.js +1088 -0
- package/dist/integrations/github-pulls/auth.js +53 -0
- package/dist/integrations/github-pulls/index.js +11 -0
- package/dist/integrations/github-pulls/package.json +27 -0
- package/dist/integrations/github-pulls/setup.js +103 -0
- package/dist/integrations/github-pulls/tools.js +1252 -0
- package/dist/integrations/github-reactions/auth.js +53 -0
- package/dist/integrations/github-reactions/index.js +11 -0
- package/dist/integrations/github-reactions/package.json +27 -0
- package/dist/integrations/github-reactions/setup.js +103 -0
- package/dist/integrations/github-reactions/tools.js +706 -0
- package/dist/integrations/github-repos/auth.js +53 -0
- package/dist/integrations/github-repos/index.js +11 -0
- package/dist/integrations/github-repos/package.json +27 -0
- package/dist/integrations/github-repos/setup.js +103 -0
- package/dist/integrations/github-repos/tools.js +7286 -0
- package/dist/integrations/github-search/auth.js +53 -0
- package/dist/integrations/github-search/index.js +11 -0
- package/dist/integrations/github-search/package.json +27 -0
- package/dist/integrations/github-search/setup.js +103 -0
- package/dist/integrations/github-search/tools.js +370 -0
- package/dist/integrations/github-teams/auth.js +53 -0
- package/dist/integrations/github-teams/index.js +11 -0
- package/dist/integrations/github-teams/package.json +27 -0
- package/dist/integrations/github-teams/setup.js +103 -0
- package/dist/integrations/github-teams/tools.js +633 -0
- package/dist/integrations/github-users/auth.js +53 -0
- package/dist/integrations/github-users/index.js +11 -0
- package/dist/integrations/github-users/package.json +27 -0
- package/dist/integrations/github-users/setup.js +103 -0
- package/dist/integrations/github-users/tools.js +1118 -0
- package/dist/integrations/notion/api.js +108 -0
- package/dist/integrations/notion/auth.js +59 -0
- package/dist/integrations/notion/endpoints.json +630 -0
- package/dist/integrations/notion/index.js +11 -0
- package/dist/integrations/notion/package.json +33 -0
- package/dist/integrations/notion/publish-to-adoptai.js +271 -0
- package/dist/integrations/notion/scripts/generate-endpoints.mjs +306 -0
- package/dist/integrations/notion/setup.js +89 -0
- package/dist/integrations/notion/tools.js +586 -0
- package/dist/integrations/notion/tools.original.js +568 -0
- package/dist/integrations/salesforce/.env +8 -0
- package/dist/integrations/salesforce/.env.example +15 -0
- package/dist/integrations/salesforce/auth.js +311 -0
- package/dist/integrations/salesforce/endpoints.json +1359 -0
- package/dist/integrations/salesforce/env.js +9 -0
- package/dist/integrations/salesforce/index.js +12 -0
- package/dist/integrations/salesforce/package.json +42 -0
- package/dist/integrations/salesforce/publish-smart-specs.js +890 -0
- package/dist/integrations/salesforce/publish-to-adoptai.js +386 -0
- package/dist/integrations/salesforce/scripts/extract-postman.mjs +222 -0
- package/dist/integrations/salesforce/setup.js +112 -0
- package/dist/integrations/salesforce/tools.js +4544 -0
- package/dist/integrations/salesforce/tools.original.js +4487 -0
- package/dist/server/mcp-server.js +50 -0
- package/dist/server/tool-loader.js +47 -0
- package/dist/specs/figma-api.json +13621 -0
- package/dist/specs/split/salesforce-auth.json +3931 -0
- package/dist/specs/split/salesforce-bulk-v1.json +1489 -0
- package/dist/specs/split/salesforce-bulk-v2.json +1951 -0
- package/dist/specs/split/salesforce-composite.json +1246 -0
- package/dist/specs/split/salesforce-connect.json +11639 -0
- package/dist/specs/split/salesforce-einstein-prediction-service.json +576 -0
- package/dist/specs/split/salesforce-event-platform.json +2682 -0
- package/dist/specs/split/salesforce-graphql.json +1754 -0
- package/dist/specs/split/salesforce-industries.json +4115 -0
- package/dist/specs/split/salesforce-metadata.json +555 -0
- package/dist/specs/split/salesforce-rest.json +4798 -0
- package/dist/specs/split/salesforce-soap.json +210 -0
- package/dist/specs/split/salesforce-subscription-management.json +1299 -0
- package/dist/specs/split/salesforce-tooling.json +2026 -0
- package/dist/specs/split/salesforce-ui.json +7426 -0
- package/package.json +47 -0
|
@@ -0,0 +1,4487 @@
|
|
|
1
|
+
import './env.js';
|
|
2
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import axios from 'axios';
|
|
6
|
+
import {
|
|
7
|
+
buildAuthHeaders,
|
|
8
|
+
ensureAccessToken,
|
|
9
|
+
getDefaultUserId,
|
|
10
|
+
getInstanceUrl,
|
|
11
|
+
getToken,
|
|
12
|
+
} from './auth.js';
|
|
13
|
+
|
|
14
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
const manifest = JSON.parse(readFileSync(join(__dirname, 'endpoints.json'), 'utf8'));
|
|
16
|
+
|
|
17
|
+
/** Postman split: Bulk API v2 (Section B — same repo path; optional if file missing). */
|
|
18
|
+
const BULK_V2_POSTMAN = join(__dirname, '../../specs/split/salesforce-bulk-v2.json');
|
|
19
|
+
|
|
20
|
+
/** Postman split: Bulk API v1 + Bulk query jobs (Section B). */
|
|
21
|
+
const BULK_V1_POSTMAN = join(__dirname, '../../specs/split/salesforce-bulk-v1.json');
|
|
22
|
+
|
|
23
|
+
/** Postman split: Metadata SOAP API (Section B). */
|
|
24
|
+
const METADATA_POSTMAN = join(__dirname, '../../specs/split/salesforce-metadata.json');
|
|
25
|
+
|
|
26
|
+
/** Postman split: Connect / Chatter API (Section B). */
|
|
27
|
+
const CONNECT_POSTMAN = join(__dirname, '../../specs/split/salesforce-connect.json');
|
|
28
|
+
|
|
29
|
+
/** Postman split: Tooling API (Section B). */
|
|
30
|
+
const TOOLING_POSTMAN = join(__dirname, '../../specs/split/salesforce-tooling.json');
|
|
31
|
+
|
|
32
|
+
/** Postman split: UI API (Section B). */
|
|
33
|
+
const UI_POSTMAN = join(__dirname, '../../specs/split/salesforce-ui.json');
|
|
34
|
+
|
|
35
|
+
/** Postman split: Einstein Prediction Service / Smart Data Discovery (Section B). */
|
|
36
|
+
const EINSTEIN_PS_POSTMAN = join(__dirname, '../../specs/split/salesforce-einstein-prediction-service.json');
|
|
37
|
+
|
|
38
|
+
/** Postman split: Event Platform (CDC, platform events, streaming; Section B). */
|
|
39
|
+
const EVENT_PLATFORM_POSTMAN = join(__dirname, '../../specs/split/salesforce-event-platform.json');
|
|
40
|
+
|
|
41
|
+
/** Postman split: GraphQL API (Section B). */
|
|
42
|
+
const GRAPHQL_POSTMAN = join(__dirname, '../../specs/split/salesforce-graphql.json');
|
|
43
|
+
|
|
44
|
+
/** Postman split: Subscription Management / commerce (Section B). */
|
|
45
|
+
const SUBSCRIPTION_MGMT_POSTMAN = join(__dirname, '../../specs/split/salesforce-subscription-management.json');
|
|
46
|
+
|
|
47
|
+
/** Postman split: Industries (CPQ, Loyalty, Health, FSC patterns; Section B). */
|
|
48
|
+
const INDUSTRIES_POSTMAN = join(__dirname, '../../specs/split/salesforce-industries.json');
|
|
49
|
+
|
|
50
|
+
/** Dynamic API base — never hardcode instance host (see setup env + OAuth). */
|
|
51
|
+
function getBaseUrl() {
|
|
52
|
+
const inst =
|
|
53
|
+
process.env.SALESFORCE_INSTANCE_URL?.replace(/\/$/, '') || getInstanceUrl()?.replace(/\/$/, '');
|
|
54
|
+
if (!inst) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
'SALESFORCE_INSTANCE_URL is not set. Run setup and ensure your MCP config includes SALESFORCE_INSTANCE_URL, or authenticate again.'
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
const verRaw = process.env.SALESFORCE_API_VERSION || 'v59.0';
|
|
60
|
+
const ver = verRaw.startsWith('v') ? verRaw : `v${verRaw}`;
|
|
61
|
+
return `${inst}/services/data/${ver}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Bulk API 1.0 async root: `{instance}/services/async/{numericVersion}` (no `v` prefix).
|
|
66
|
+
* Matches Salesforce `/services/async/59.0/...` style paths.
|
|
67
|
+
*/
|
|
68
|
+
function getBulkV1AsyncBaseUrl() {
|
|
69
|
+
const inst =
|
|
70
|
+
process.env.SALESFORCE_INSTANCE_URL?.replace(/\/$/, '') || getInstanceUrl()?.replace(/\/$/, '');
|
|
71
|
+
if (!inst) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
'SALESFORCE_INSTANCE_URL is not set. Run setup and ensure your MCP config includes SALESFORCE_INSTANCE_URL, or authenticate again.'
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
const verRaw = process.env.SALESFORCE_API_VERSION || 'v59.0';
|
|
77
|
+
const numVer = String(verRaw).replace(/^v/i, '');
|
|
78
|
+
return `${inst}/services/async/${numVer}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Tooling API root: `{instance}/services/data/{version}/tooling`. */
|
|
82
|
+
function getToolingBaseUrl() {
|
|
83
|
+
const inst =
|
|
84
|
+
process.env.SALESFORCE_INSTANCE_URL?.replace(/\/$/, '') || getInstanceUrl()?.replace(/\/$/, '');
|
|
85
|
+
if (!inst) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
'SALESFORCE_INSTANCE_URL is not set. Run setup and ensure your MCP config includes SALESFORCE_INSTANCE_URL, or authenticate again.'
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
const verRaw = process.env.SALESFORCE_API_VERSION || 'v59.0';
|
|
91
|
+
const ver = verRaw.startsWith('v') ? verRaw : `v${verRaw}`;
|
|
92
|
+
return `${inst}/services/data/${ver}/tooling`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** UI API root: `{instance}/services/data/{version}/ui-api`. */
|
|
96
|
+
function getUiApiBaseUrl() {
|
|
97
|
+
const inst =
|
|
98
|
+
process.env.SALESFORCE_INSTANCE_URL?.replace(/\/$/, '') || getInstanceUrl()?.replace(/\/$/, '');
|
|
99
|
+
if (!inst) {
|
|
100
|
+
throw new Error(
|
|
101
|
+
'SALESFORCE_INSTANCE_URL is not set. Run setup and ensure your MCP config includes SALESFORCE_INSTANCE_URL, or authenticate again.'
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
const verRaw = process.env.SALESFORCE_API_VERSION || 'v59.0';
|
|
105
|
+
const ver = verRaw.startsWith('v') ? verRaw : `v${verRaw}`;
|
|
106
|
+
return `${inst}/services/data/${ver}/ui-api`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** GraphQL API endpoint: `{instance}/services/data/{version}/graphql`. */
|
|
110
|
+
function getGraphqlBaseUrl() {
|
|
111
|
+
const inst =
|
|
112
|
+
process.env.SALESFORCE_INSTANCE_URL?.replace(/\/$/, '') || getInstanceUrl()?.replace(/\/$/, '');
|
|
113
|
+
if (!inst) {
|
|
114
|
+
throw new Error(
|
|
115
|
+
'SALESFORCE_INSTANCE_URL is not set. Run setup and ensure your MCP config includes SALESFORCE_INSTANCE_URL, or authenticate again.'
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
const verRaw = process.env.SALESFORCE_API_VERSION || 'v59.0';
|
|
119
|
+
const ver = verRaw.startsWith('v') ? verRaw : `v${verRaw}`;
|
|
120
|
+
return `${inst}/services/data/${ver}/graphql`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Einstein Vision/Language Platform API root (default `https://api.einstein.ai/v2`).
|
|
125
|
+
* Set `SALESFORCE_EINSTEIN_PLATFORM_URL` to override.
|
|
126
|
+
*/
|
|
127
|
+
function getEinsteinPlatformBaseUrl() {
|
|
128
|
+
const raw = process.env.SALESFORCE_EINSTEIN_PLATFORM_URL?.replace(/\/$/, '') || 'https://api.einstein.ai/v2';
|
|
129
|
+
return raw;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function einsteinPlatformPostMultipart(pathSuffix, buildForm) {
|
|
133
|
+
const envTok = process.env.SALESFORCE_EINSTEIN_ACCESS_TOKEN?.trim() || process.env.EINSTEIN_ACCESS_TOKEN?.trim();
|
|
134
|
+
if (!envTok) await ensureAccessToken();
|
|
135
|
+
const token = envTok || getToken();
|
|
136
|
+
const base = getEinsteinPlatformBaseUrl().replace(/\/$/, '');
|
|
137
|
+
const path = pathSuffix.startsWith('/') ? pathSuffix : `/${pathSuffix}`;
|
|
138
|
+
const url = `${base}${path}`;
|
|
139
|
+
const form = new FormData();
|
|
140
|
+
buildForm(form);
|
|
141
|
+
let res;
|
|
142
|
+
try {
|
|
143
|
+
res = await fetch(url, { method: 'POST', headers: { Authorization: `Bearer ${token}` }, body: form });
|
|
144
|
+
} catch (e) {
|
|
145
|
+
throw new Error(e?.message || 'Einstein Platform request failed');
|
|
146
|
+
}
|
|
147
|
+
const txt = await res.text();
|
|
148
|
+
let data;
|
|
149
|
+
try {
|
|
150
|
+
data = txt ? JSON.parse(txt) : {};
|
|
151
|
+
} catch {
|
|
152
|
+
throw new Error(`Einstein Platform response not JSON (${res.status}): ${txt.slice(0, 500)}`);
|
|
153
|
+
}
|
|
154
|
+
if (!res.ok) {
|
|
155
|
+
handleError({ response: { status: res.status, data, statusText: res.statusText } });
|
|
156
|
+
}
|
|
157
|
+
return data;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function getDataCatalogUrl() {
|
|
161
|
+
const inst =
|
|
162
|
+
process.env.SALESFORCE_INSTANCE_URL?.replace(/\/$/, '') || getInstanceUrl()?.replace(/\/$/, '');
|
|
163
|
+
if (!inst) {
|
|
164
|
+
throw new Error('SALESFORCE_INSTANCE_URL is not set.');
|
|
165
|
+
}
|
|
166
|
+
return `${inst}/services/data`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function getApexBaseUrl() {
|
|
170
|
+
const inst =
|
|
171
|
+
process.env.SALESFORCE_INSTANCE_URL?.replace(/\/$/, '') || getInstanceUrl()?.replace(/\/$/, '');
|
|
172
|
+
if (!inst) throw new Error('SALESFORCE_INSTANCE_URL is not set.');
|
|
173
|
+
return `${inst}/services/apexrest`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function handleError(err) {
|
|
177
|
+
const status = err.response?.status;
|
|
178
|
+
const data = err.response?.data;
|
|
179
|
+
const msg =
|
|
180
|
+
data?.[0]?.message ||
|
|
181
|
+
data?.message ||
|
|
182
|
+
data?.error_description ||
|
|
183
|
+
data?.error ||
|
|
184
|
+
err.message;
|
|
185
|
+
if (status === 401) {
|
|
186
|
+
throw new Error(
|
|
187
|
+
'Token invalid or expired.\nRun: npx adoptai-salesforce-mcp --client cursor'
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
if (status === 403) throw new Error('Insufficient permissions. Check your connected app scopes.');
|
|
191
|
+
if (status === 404) throw new Error('Resource not found. Check your parameters.');
|
|
192
|
+
if (status === 429) throw new Error('Rate limit exceeded. Please wait and try again.');
|
|
193
|
+
throw new Error(msg || 'API request failed');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function applyPathTemplate(path, pathValues) {
|
|
197
|
+
let p = path;
|
|
198
|
+
for (const [k, v] of Object.entries(pathValues)) {
|
|
199
|
+
p = p.replace(`{${k}}`, encodeURIComponent(String(v)));
|
|
200
|
+
}
|
|
201
|
+
if (/\{[^}]+}/.test(p)) {
|
|
202
|
+
throw new Error(`Missing path parameters for: ${p}`);
|
|
203
|
+
}
|
|
204
|
+
return p;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function text(payload) {
|
|
208
|
+
return {
|
|
209
|
+
content: [{ type: 'text', text: JSON.stringify(payload ?? {}, null, 2) }],
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function splitPathAndRest(path, params) {
|
|
214
|
+
const pathKeys = [...path.matchAll(/\{([^}]+)\}/g)].map((m) => m[1]);
|
|
215
|
+
const pathValues = {};
|
|
216
|
+
const rest = { ...params };
|
|
217
|
+
for (const k of pathKeys) {
|
|
218
|
+
if (rest[k] === undefined || rest[k] === null || rest[k] === '') {
|
|
219
|
+
throw new Error(`Missing required path parameter: ${k}`);
|
|
220
|
+
}
|
|
221
|
+
pathValues[k] = rest[k];
|
|
222
|
+
delete rest[k];
|
|
223
|
+
}
|
|
224
|
+
delete rest._rawBody;
|
|
225
|
+
return { pathValues, rest };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function sfRequest(method, path, urlKind, params = {}) {
|
|
229
|
+
await ensureAccessToken();
|
|
230
|
+
const headers = { ...buildAuthHeaders(), 'Content-Type': 'application/json' };
|
|
231
|
+
const rawBody = params._rawBody;
|
|
232
|
+
const { pathValues, rest } = splitPathAndRest(path, params);
|
|
233
|
+
const resolvedPath = applyPathTemplate(path, pathValues);
|
|
234
|
+
|
|
235
|
+
let url;
|
|
236
|
+
if (urlKind === 'apex') {
|
|
237
|
+
url = getApexBaseUrl() + resolvedPath;
|
|
238
|
+
} else if (urlKind === 'data-catalog') {
|
|
239
|
+
url = getDataCatalogUrl() + (resolvedPath === '/' ? '' : resolvedPath);
|
|
240
|
+
} else if (urlKind === 'tooling') {
|
|
241
|
+
url = getToolingBaseUrl() + resolvedPath;
|
|
242
|
+
} else if (urlKind === 'ui-api') {
|
|
243
|
+
url = getUiApiBaseUrl() + resolvedPath;
|
|
244
|
+
} else if (urlKind === 'graphql') {
|
|
245
|
+
url = getGraphqlBaseUrl();
|
|
246
|
+
} else if (urlKind === 'bulk-v1-async') {
|
|
247
|
+
url = getBulkV1AsyncBaseUrl() + resolvedPath;
|
|
248
|
+
} else {
|
|
249
|
+
url = getBaseUrl() + resolvedPath;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const isGetLike = method === 'GET' || method === 'DELETE';
|
|
253
|
+
try {
|
|
254
|
+
const res = await axios({
|
|
255
|
+
method,
|
|
256
|
+
url,
|
|
257
|
+
headers,
|
|
258
|
+
params: isGetLike ? rest : undefined,
|
|
259
|
+
data: !isGetLike ? (rawBody !== undefined ? rawBody : Object.keys(rest).length ? rest : undefined) : undefined,
|
|
260
|
+
timeout: 120000,
|
|
261
|
+
validateStatus: () => true,
|
|
262
|
+
});
|
|
263
|
+
if (res.status < 200 || res.status >= 300) {
|
|
264
|
+
handleError({ response: res, message: res.statusText });
|
|
265
|
+
}
|
|
266
|
+
if (res.status === 204 || res.data === '' || res.data == null) {
|
|
267
|
+
return { success: true, status: res.status };
|
|
268
|
+
}
|
|
269
|
+
return res.data;
|
|
270
|
+
} catch (err) {
|
|
271
|
+
if (err.response) handleError(err);
|
|
272
|
+
throw err;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function escapeSoqlString(s) {
|
|
277
|
+
return String(s).replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function soqlQueryUrl(q) {
|
|
281
|
+
return `${getBaseUrl()}/query/?q=${encodeURIComponent(q)}`;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async function soqlGet(queryString) {
|
|
285
|
+
await ensureAccessToken();
|
|
286
|
+
const headers = { ...buildAuthHeaders(), Accept: 'application/json' };
|
|
287
|
+
try {
|
|
288
|
+
const res = await axios.get(soqlQueryUrl(queryString), { headers, timeout: 120000 });
|
|
289
|
+
return res.data;
|
|
290
|
+
} catch (err) {
|
|
291
|
+
handleError(err);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async function fetchQueryAllPages(initialQuery, maxRecords) {
|
|
296
|
+
await ensureAccessToken();
|
|
297
|
+
const headers = { ...buildAuthHeaders(), Accept: 'application/json' };
|
|
298
|
+
let url = soqlQueryUrl(initialQuery);
|
|
299
|
+
const allRecords = [];
|
|
300
|
+
let done = false;
|
|
301
|
+
let totalFetched = 0;
|
|
302
|
+
|
|
303
|
+
while (!done && totalFetched < maxRecords) {
|
|
304
|
+
let res;
|
|
305
|
+
try {
|
|
306
|
+
res = await axios.get(url, { headers, timeout: 120000 });
|
|
307
|
+
} catch (e) {
|
|
308
|
+
handleError(e);
|
|
309
|
+
}
|
|
310
|
+
const data = res.data;
|
|
311
|
+
const batch = data.records || [];
|
|
312
|
+
for (const row of batch) {
|
|
313
|
+
if (totalFetched >= maxRecords) break;
|
|
314
|
+
allRecords.push(row);
|
|
315
|
+
totalFetched += 1;
|
|
316
|
+
}
|
|
317
|
+
done = data.done === true || !data.nextRecordsUrl;
|
|
318
|
+
if (!done && totalFetched < maxRecords) {
|
|
319
|
+
const next = data.nextRecordsUrl;
|
|
320
|
+
const inst = getInstanceUrl()?.replace(/\/$/, '');
|
|
321
|
+
if (!inst) throw new Error('SALESFORCE_INSTANCE_URL missing for pagination');
|
|
322
|
+
url = next.startsWith('http') ? next : `${inst}${next}`;
|
|
323
|
+
} else {
|
|
324
|
+
done = true;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return {
|
|
329
|
+
totalSize: allRecords.length,
|
|
330
|
+
maxRecords,
|
|
331
|
+
truncated: totalFetched >= maxRecords && !done,
|
|
332
|
+
records: allRecords,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function buildSearchWhere(conditions) {
|
|
337
|
+
const parts = [];
|
|
338
|
+
for (const [field, val] of Object.entries(conditions)) {
|
|
339
|
+
if (val == null || val === '') continue;
|
|
340
|
+
parts.push(`${field} LIKE '%${escapeSoqlString(val)}%'`);
|
|
341
|
+
}
|
|
342
|
+
return parts.length ? parts.join(' AND ') : null;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function pipelinePeriodClause(period) {
|
|
346
|
+
switch (period) {
|
|
347
|
+
case 'this_quarter':
|
|
348
|
+
return 'CloseDate = THIS_QUARTER';
|
|
349
|
+
case 'next_quarter':
|
|
350
|
+
return 'CloseDate = NEXT_QUARTER';
|
|
351
|
+
case 'this_month':
|
|
352
|
+
default:
|
|
353
|
+
return 'CloseDate = THIS_MONTH';
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// ——— Bulk API v2 helpers & Section B extract (salesforce-bulk-v2.json) ———
|
|
358
|
+
|
|
359
|
+
function chunkArray(arr, size) {
|
|
360
|
+
const n = Math.max(1, Number(size) || 200);
|
|
361
|
+
const chunks = [];
|
|
362
|
+
for (let i = 0; i < arr.length; i += n) {
|
|
363
|
+
chunks.push(arr.slice(i, i + n));
|
|
364
|
+
}
|
|
365
|
+
return chunks;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function csvEscapeCell(val) {
|
|
369
|
+
if (val == null) return '';
|
|
370
|
+
const s = String(val);
|
|
371
|
+
if (/[",\n\r]/.test(s)) return `"${s.replace(/"/g, '""')}"`;
|
|
372
|
+
return s;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function objectsToCsv(rows) {
|
|
376
|
+
if (!rows?.length) return '';
|
|
377
|
+
const headers = [...new Set(rows.flatMap((r) => Object.keys(r)))];
|
|
378
|
+
const lines = [headers.map(csvEscapeCell).join(',')];
|
|
379
|
+
for (const row of rows) {
|
|
380
|
+
lines.push(headers.map((h) => csvEscapeCell(row[h])).join(','));
|
|
381
|
+
}
|
|
382
|
+
return lines.join('\n');
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function recordIdsToDeleteCsv(record_ids) {
|
|
386
|
+
if (!record_ids?.length) return '';
|
|
387
|
+
const ids = record_ids.map((id) => String(id).trim()).filter(Boolean);
|
|
388
|
+
return ['Id', ...ids].join('\n');
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async function pollBulkIngestJob(jobId, options = {}) {
|
|
392
|
+
const maxWaitMs = options.maxWaitMs ?? 30 * 60 * 1000;
|
|
393
|
+
const intervalMs = options.intervalMs ?? 2000;
|
|
394
|
+
const base = getBaseUrl();
|
|
395
|
+
await ensureAccessToken();
|
|
396
|
+
const headers = { ...buildAuthHeaders(), Accept: 'application/json' };
|
|
397
|
+
const deadline = Date.now() + maxWaitMs;
|
|
398
|
+
while (Date.now() < deadline) {
|
|
399
|
+
const res = await axios.get(`${base}/jobs/ingest/${encodeURIComponent(jobId)}`, {
|
|
400
|
+
headers,
|
|
401
|
+
timeout: 120000,
|
|
402
|
+
validateStatus: () => true,
|
|
403
|
+
});
|
|
404
|
+
if (res.status < 200 || res.status >= 300) handleError({ response: res });
|
|
405
|
+
const state = res.data.state;
|
|
406
|
+
if (state === 'JobComplete' || state === 'Failed' || state === 'Aborted') {
|
|
407
|
+
return res.data;
|
|
408
|
+
}
|
|
409
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
410
|
+
}
|
|
411
|
+
throw new Error(`Bulk ingest job ${jobId} did not finish within ${maxWaitMs}ms`);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async function runBulkIngestOperation({
|
|
415
|
+
objectType,
|
|
416
|
+
operation,
|
|
417
|
+
externalIdFieldName,
|
|
418
|
+
records,
|
|
419
|
+
recordIds,
|
|
420
|
+
batch_size,
|
|
421
|
+
}) {
|
|
422
|
+
await ensureAccessToken();
|
|
423
|
+
const base = getBaseUrl();
|
|
424
|
+
const auth = buildAuthHeaders();
|
|
425
|
+
const jobBody = {
|
|
426
|
+
object: objectType,
|
|
427
|
+
operation,
|
|
428
|
+
contentType: 'CSV',
|
|
429
|
+
lineEnding: 'LF',
|
|
430
|
+
columnDelimiter: 'COMMA',
|
|
431
|
+
};
|
|
432
|
+
if (externalIdFieldName) {
|
|
433
|
+
jobBody.externalIdFieldName = externalIdFieldName;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const jobRes = await axios.post(`${base}/jobs/ingest`, jobBody, {
|
|
437
|
+
headers: { ...auth, 'Content-Type': 'application/json' },
|
|
438
|
+
timeout: 120000,
|
|
439
|
+
validateStatus: () => true,
|
|
440
|
+
});
|
|
441
|
+
if (jobRes.status < 200 || jobRes.status >= 300) handleError({ response: jobRes });
|
|
442
|
+
|
|
443
|
+
const jobId = jobRes.data.id;
|
|
444
|
+
let payloads = [];
|
|
445
|
+
|
|
446
|
+
if (operation === 'delete') {
|
|
447
|
+
const csv = recordIdsToDeleteCsv(recordIds);
|
|
448
|
+
if (!csv) throw new Error('record_ids must be a non-empty array for delete');
|
|
449
|
+
payloads = [csv];
|
|
450
|
+
} else {
|
|
451
|
+
if (!records?.length) throw new Error('records must be a non-empty array');
|
|
452
|
+
payloads = chunkArray(records, batch_size).map(objectsToCsv);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
for (const csv of payloads) {
|
|
456
|
+
const putRes = await axios.put(`${base}/jobs/ingest/${jobId}/batches`, csv, {
|
|
457
|
+
headers: { ...auth, 'Content-Type': 'text/csv', Accept: 'application/json' },
|
|
458
|
+
timeout: 120000,
|
|
459
|
+
validateStatus: () => true,
|
|
460
|
+
});
|
|
461
|
+
if (putRes.status < 200 || putRes.status >= 300) handleError({ response: putRes });
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const patchRes = await axios.patch(
|
|
465
|
+
`${base}/jobs/ingest/${jobId}`,
|
|
466
|
+
{ state: 'UploadComplete' },
|
|
467
|
+
{
|
|
468
|
+
headers: { ...auth, 'Content-Type': 'application/json' },
|
|
469
|
+
timeout: 120000,
|
|
470
|
+
validateStatus: () => true,
|
|
471
|
+
}
|
|
472
|
+
);
|
|
473
|
+
if (patchRes.status < 200 || patchRes.status >= 300) handleError({ response: patchRes });
|
|
474
|
+
|
|
475
|
+
const job = await pollBulkIngestJob(jobId);
|
|
476
|
+
return { jobId, job };
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function bulkApiMultipliedTools() {
|
|
480
|
+
return [
|
|
481
|
+
{
|
|
482
|
+
name: 'bulk_create_records',
|
|
483
|
+
description:
|
|
484
|
+
'Creates multiple Salesforce records in one batch using Bulk API v2. Handles job creation, data upload and completion polling automatically. Much faster than individual creates for 10+ records.',
|
|
485
|
+
inputSchema: {
|
|
486
|
+
type: 'object',
|
|
487
|
+
properties: {
|
|
488
|
+
object_type: { type: 'string', description: 'SObject API name (e.g. Account, Contact)' },
|
|
489
|
+
records: { type: 'array', description: 'Row objects; columns become CSV headers' },
|
|
490
|
+
batch_size: { type: 'number', description: 'Rows per upload batch (default 200)' },
|
|
491
|
+
},
|
|
492
|
+
required: ['object_type', 'records'],
|
|
493
|
+
},
|
|
494
|
+
handler: async ({ object_type, records, batch_size }) => {
|
|
495
|
+
const result = await runBulkIngestOperation({
|
|
496
|
+
objectType: object_type,
|
|
497
|
+
operation: 'insert',
|
|
498
|
+
records,
|
|
499
|
+
batch_size: batch_size ?? 200,
|
|
500
|
+
});
|
|
501
|
+
return text(result);
|
|
502
|
+
},
|
|
503
|
+
},
|
|
504
|
+
{
|
|
505
|
+
name: 'bulk_update_records',
|
|
506
|
+
description:
|
|
507
|
+
'Updates multiple existing records in one batch. Each record must include an Id. Use for mass updates like reassigning ownership or updating status across many records.',
|
|
508
|
+
inputSchema: {
|
|
509
|
+
type: 'object',
|
|
510
|
+
properties: {
|
|
511
|
+
object_type: { type: 'string' },
|
|
512
|
+
records: {
|
|
513
|
+
type: 'array',
|
|
514
|
+
description: 'Objects including Id plus fields to update',
|
|
515
|
+
},
|
|
516
|
+
batch_size: { type: 'number', description: 'Default 200' },
|
|
517
|
+
},
|
|
518
|
+
required: ['object_type', 'records'],
|
|
519
|
+
},
|
|
520
|
+
handler: async ({ object_type, records, batch_size }) => {
|
|
521
|
+
for (const r of records) {
|
|
522
|
+
if (r.Id == null && r.id == null) {
|
|
523
|
+
throw new Error('Each record must include Id for update');
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
const normalized = records.map((r) => ({ ...r, Id: r.Id ?? r.id }));
|
|
527
|
+
const result = await runBulkIngestOperation({
|
|
528
|
+
objectType: object_type,
|
|
529
|
+
operation: 'update',
|
|
530
|
+
records: normalized,
|
|
531
|
+
batch_size: batch_size ?? 200,
|
|
532
|
+
});
|
|
533
|
+
return text(result);
|
|
534
|
+
},
|
|
535
|
+
},
|
|
536
|
+
{
|
|
537
|
+
name: 'bulk_upsert_records',
|
|
538
|
+
description:
|
|
539
|
+
"Creates or updates records based on an external ID field. Use for data sync when you don't know if records already exist.",
|
|
540
|
+
inputSchema: {
|
|
541
|
+
type: 'object',
|
|
542
|
+
properties: {
|
|
543
|
+
object_type: { type: 'string' },
|
|
544
|
+
external_id_field: { type: 'string', description: 'External ID field API name' },
|
|
545
|
+
records: { type: 'array' },
|
|
546
|
+
batch_size: { type: 'number', description: 'Default 200' },
|
|
547
|
+
},
|
|
548
|
+
required: ['object_type', 'external_id_field', 'records'],
|
|
549
|
+
},
|
|
550
|
+
handler: async ({ object_type, external_id_field, records, batch_size }) => {
|
|
551
|
+
const result = await runBulkIngestOperation({
|
|
552
|
+
objectType: object_type,
|
|
553
|
+
operation: 'upsert',
|
|
554
|
+
externalIdFieldName: external_id_field,
|
|
555
|
+
records,
|
|
556
|
+
batch_size: batch_size ?? 200,
|
|
557
|
+
});
|
|
558
|
+
return text(result);
|
|
559
|
+
},
|
|
560
|
+
},
|
|
561
|
+
{
|
|
562
|
+
name: 'bulk_delete_records',
|
|
563
|
+
description:
|
|
564
|
+
'Permanently deletes multiple records in one batch. Each item needs only an Id. WARNING: irreversible. Use with caution.',
|
|
565
|
+
inputSchema: {
|
|
566
|
+
type: 'object',
|
|
567
|
+
properties: {
|
|
568
|
+
object_type: { type: 'string', description: 'SObject API name' },
|
|
569
|
+
record_ids: {
|
|
570
|
+
type: 'array',
|
|
571
|
+
items: { type: 'string' },
|
|
572
|
+
description: 'Salesforce record Ids to delete',
|
|
573
|
+
},
|
|
574
|
+
},
|
|
575
|
+
required: ['object_type', 'record_ids'],
|
|
576
|
+
},
|
|
577
|
+
handler: async ({ object_type, record_ids }) => {
|
|
578
|
+
const result = await runBulkIngestOperation({
|
|
579
|
+
objectType: object_type,
|
|
580
|
+
operation: 'delete',
|
|
581
|
+
recordIds: record_ids,
|
|
582
|
+
batch_size: 200,
|
|
583
|
+
});
|
|
584
|
+
return text(result);
|
|
585
|
+
},
|
|
586
|
+
},
|
|
587
|
+
{
|
|
588
|
+
name: 'get_bulk_job_status',
|
|
589
|
+
description:
|
|
590
|
+
'Checks the status of a running bulk ingest job. Returns state (InProgress/JobComplete/Failed), records processed and failed count.',
|
|
591
|
+
inputSchema: {
|
|
592
|
+
type: 'object',
|
|
593
|
+
properties: {
|
|
594
|
+
job_id: { type: 'string', description: 'Bulk ingest job Id' },
|
|
595
|
+
},
|
|
596
|
+
required: ['job_id'],
|
|
597
|
+
},
|
|
598
|
+
handler: async ({ job_id }) => {
|
|
599
|
+
await ensureAccessToken();
|
|
600
|
+
const base = getBaseUrl();
|
|
601
|
+
const headers = { ...buildAuthHeaders(), Accept: 'application/json' };
|
|
602
|
+
try {
|
|
603
|
+
const res = await axios.get(`${base}/jobs/ingest/${encodeURIComponent(job_id)}`, {
|
|
604
|
+
headers,
|
|
605
|
+
timeout: 120000,
|
|
606
|
+
validateStatus: () => true,
|
|
607
|
+
});
|
|
608
|
+
if (res.status < 200 || res.status >= 300) handleError({ response: res });
|
|
609
|
+
return text(res.data);
|
|
610
|
+
} catch (e) {
|
|
611
|
+
handleError(e);
|
|
612
|
+
}
|
|
613
|
+
},
|
|
614
|
+
},
|
|
615
|
+
{
|
|
616
|
+
name: 'get_bulk_job_results',
|
|
617
|
+
description:
|
|
618
|
+
'Downloads the results of a completed bulk ingest job including which records succeeded or failed with error messages.',
|
|
619
|
+
inputSchema: {
|
|
620
|
+
type: 'object',
|
|
621
|
+
properties: {
|
|
622
|
+
job_id: { type: 'string' },
|
|
623
|
+
result_type: {
|
|
624
|
+
type: 'string',
|
|
625
|
+
enum: ['successfulResults', 'failedResults', 'unprocessedRecords'],
|
|
626
|
+
description:
|
|
627
|
+
'successfulResults | failedResults | unprocessedRecords (CSV payload)',
|
|
628
|
+
},
|
|
629
|
+
},
|
|
630
|
+
required: ['job_id', 'result_type'],
|
|
631
|
+
},
|
|
632
|
+
handler: async ({ job_id, result_type }) => {
|
|
633
|
+
const map = {
|
|
634
|
+
successfulResults: 'successfulResults',
|
|
635
|
+
failedResults: 'failedResults',
|
|
636
|
+
unprocessedRecords: 'unprocessedrecords',
|
|
637
|
+
};
|
|
638
|
+
const suffix = map[result_type];
|
|
639
|
+
if (!suffix) throw new Error('Invalid result_type');
|
|
640
|
+
await ensureAccessToken();
|
|
641
|
+
const base = getBaseUrl();
|
|
642
|
+
const headers = { ...buildAuthHeaders(), Accept: 'text/csv' };
|
|
643
|
+
const url = `${base}/jobs/ingest/${encodeURIComponent(job_id)}/${suffix}`;
|
|
644
|
+
try {
|
|
645
|
+
const res = await axios.get(url, {
|
|
646
|
+
headers,
|
|
647
|
+
responseType: 'text',
|
|
648
|
+
timeout: 120000,
|
|
649
|
+
validateStatus: () => true,
|
|
650
|
+
});
|
|
651
|
+
if (res.status < 200 || res.status >= 300) handleError({ response: res });
|
|
652
|
+
return text({
|
|
653
|
+
job_id,
|
|
654
|
+
result_type,
|
|
655
|
+
format: 'text/csv',
|
|
656
|
+
body: res.data ?? '',
|
|
657
|
+
});
|
|
658
|
+
} catch (e) {
|
|
659
|
+
handleError(e);
|
|
660
|
+
}
|
|
661
|
+
},
|
|
662
|
+
},
|
|
663
|
+
];
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
const SKIP_BULK_PATH_RE =
|
|
667
|
+
/\/oauth|\/token|\/login|\/logout|\/services\/oauth2|\/webhook|\/Soap\/|\/soap\//i;
|
|
668
|
+
|
|
669
|
+
function bulkVarMap(collection) {
|
|
670
|
+
const m = {};
|
|
671
|
+
for (const v of collection.variable || []) {
|
|
672
|
+
m[v.key] = v.value ?? '';
|
|
673
|
+
}
|
|
674
|
+
return m;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function bulkResolve(str, map) {
|
|
678
|
+
if (str == null) return str;
|
|
679
|
+
let out = String(str);
|
|
680
|
+
for (const [k, v] of Object.entries(map)) {
|
|
681
|
+
out = out.split(`{{${k}}}`).join(v ?? '');
|
|
682
|
+
}
|
|
683
|
+
return out;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function bulkInferType(value) {
|
|
687
|
+
if (value == null) return 'string';
|
|
688
|
+
const v = String(value).toLowerCase().trim();
|
|
689
|
+
if (v === '<integer>' || v === '<number>') return 'number';
|
|
690
|
+
if (v === '<boolean>') return 'boolean';
|
|
691
|
+
if (!Number.isNaN(Number(v)) && v !== '') return 'number';
|
|
692
|
+
if (v === 'true' || v === 'false') return 'boolean';
|
|
693
|
+
return 'string';
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function bulkDesc(req, itemName) {
|
|
697
|
+
const d = req.description;
|
|
698
|
+
if (typeof d === 'string') return d.slice(0, 400);
|
|
699
|
+
if (d && typeof d === 'object' && d.content) return String(d.content).slice(0, 400);
|
|
700
|
+
return itemName;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function bulkVersionedPath(req, varMap) {
|
|
704
|
+
const raw = typeof req.url === 'string' ? req.url : req.url?.raw || '';
|
|
705
|
+
const resolved = bulkResolve(raw, varMap);
|
|
706
|
+
const [pathPart, qs] = resolved.split('?');
|
|
707
|
+
if (SKIP_BULK_PATH_RE.test(pathPart)) return null;
|
|
708
|
+
const noHost = pathPart.replace(/^https?:\/\/[^/?#]+/i, '');
|
|
709
|
+
if (!noHost.startsWith('/services/data/')) return null;
|
|
710
|
+
const after = noHost.slice('/services/data/'.length);
|
|
711
|
+
const m = after.match(/^(v[0-9.]+)\/(.+)$/);
|
|
712
|
+
if (!m) return null;
|
|
713
|
+
let path = `/${m[2]}`;
|
|
714
|
+
path = path.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, '{$1}');
|
|
715
|
+
path = path.replace(/\{\{_jobId\}\}/g, '{jobId}');
|
|
716
|
+
if (qs) path = `${path}?${qs}`;
|
|
717
|
+
return path;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function bulkBuildSchema(req, normalizedPath) {
|
|
721
|
+
const properties = {};
|
|
722
|
+
const required = [];
|
|
723
|
+
|
|
724
|
+
const pathNoQuery = normalizedPath.split('?')[0];
|
|
725
|
+
for (const [, name] of pathNoQuery.matchAll(/\{([^}]+)\}/g)) {
|
|
726
|
+
properties[name] = { type: 'string', description: `Path: ${name}` };
|
|
727
|
+
required.push(name);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
const seen = new Set();
|
|
731
|
+
if (typeof req.url === 'object' && req.url?.query) {
|
|
732
|
+
for (const param of req.url.query) {
|
|
733
|
+
if (!param.key || param.disabled || seen.has(param.key)) continue;
|
|
734
|
+
seen.add(param.key);
|
|
735
|
+
if (properties[param.key]) continue;
|
|
736
|
+
properties[param.key] = {
|
|
737
|
+
type: bulkInferType(param.value),
|
|
738
|
+
description: param.description || `Query: ${param.key}`,
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
if (req.body?.mode === 'raw' && req.body.raw) {
|
|
744
|
+
try {
|
|
745
|
+
const bodyJson = JSON.parse(req.body.raw);
|
|
746
|
+
for (const [key, value] of Object.entries(bodyJson)) {
|
|
747
|
+
if (properties[key]) continue;
|
|
748
|
+
properties[key] = { type: bulkInferType(String(value)), description: `Body: ${key}` };
|
|
749
|
+
}
|
|
750
|
+
} catch {
|
|
751
|
+
/* non-JSON */
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
const isCsvPut =
|
|
756
|
+
req.method?.toUpperCase() === 'PUT' && pathNoQuery.includes('/jobs/ingest/') && pathNoQuery.endsWith('/batches');
|
|
757
|
+
if (isCsvPut) {
|
|
758
|
+
properties.csv_data = {
|
|
759
|
+
type: 'string',
|
|
760
|
+
description: 'CSV file contents (UTF-8) for this batch upload',
|
|
761
|
+
};
|
|
762
|
+
required.push('csv_data');
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
return { type: 'object', properties, required: [...new Set(required)] };
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/** Like bulkBuildSchema plus Postman `formdata` fields (Connect file uploads). */
|
|
769
|
+
function connectBuildSchema(req, normalizedPath) {
|
|
770
|
+
const base = bulkBuildSchema(req, normalizedPath);
|
|
771
|
+
if (req.body?.mode === 'formdata' && req.body.formdata) {
|
|
772
|
+
for (const item of req.body.formdata) {
|
|
773
|
+
if (!item.key || base.properties[item.key]) continue;
|
|
774
|
+
base.properties[item.key] = {
|
|
775
|
+
type: 'string',
|
|
776
|
+
description: `Multipart field “${item.key}” (${item.type || 'text'}). For files, pass base64 or use upload_file_to_record.`,
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
return base;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/** Postman `graphql` body → MCP tool schema (query + optional variables). */
|
|
784
|
+
function graphqlBuildSchema(req) {
|
|
785
|
+
const g = req.body?.mode === 'graphql' ? req.body.graphql : null;
|
|
786
|
+
const exampleQuery = typeof g?.query === 'string' ? g.query : '';
|
|
787
|
+
let exampleVars;
|
|
788
|
+
if (g?.variables != null && String(g.variables).trim() !== '') {
|
|
789
|
+
try {
|
|
790
|
+
exampleVars = typeof g.variables === 'string' ? JSON.parse(g.variables) : g.variables;
|
|
791
|
+
} catch {
|
|
792
|
+
exampleVars = undefined;
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
const properties = {
|
|
796
|
+
query: {
|
|
797
|
+
type: 'string',
|
|
798
|
+
description: exampleQuery
|
|
799
|
+
? 'GraphQL query string (Postman example provided as default).'
|
|
800
|
+
: 'GraphQL query string.',
|
|
801
|
+
...(exampleQuery ? { default: exampleQuery } : {}),
|
|
802
|
+
},
|
|
803
|
+
variables: {
|
|
804
|
+
type: 'object',
|
|
805
|
+
description: 'Optional GraphQL variables object.',
|
|
806
|
+
additionalProperties: true,
|
|
807
|
+
...(exampleVars && typeof exampleVars === 'object' ? { default: exampleVars } : {}),
|
|
808
|
+
},
|
|
809
|
+
};
|
|
810
|
+
return { type: 'object', properties, required: ['query'] };
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function bulkSlug(name) {
|
|
814
|
+
return name
|
|
815
|
+
.replace(/[^a-zA-Z0-9_]+/g, '_')
|
|
816
|
+
.replace(/^_+|_+$/g, '')
|
|
817
|
+
.toLowerCase()
|
|
818
|
+
.slice(0, 55);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
function bulkExtractRequests(items, varMap, acc) {
|
|
822
|
+
for (const item of items || []) {
|
|
823
|
+
if (Array.isArray(item.item)) {
|
|
824
|
+
bulkExtractRequests(item.item, varMap, acc);
|
|
825
|
+
} else if (item.request) {
|
|
826
|
+
const req = item.request;
|
|
827
|
+
const method = String(req.method || 'GET').toUpperCase();
|
|
828
|
+
if (['OPTIONS', 'HEAD', 'TRACE'].includes(method)) continue;
|
|
829
|
+
|
|
830
|
+
const path = bulkVersionedPath(req, varMap);
|
|
831
|
+
if (!path) continue;
|
|
832
|
+
|
|
833
|
+
const pathKey = path.split('?')[0];
|
|
834
|
+
const dedupeKey = `${method}|${pathKey}`;
|
|
835
|
+
const name = `bulk_v2_${bulkSlug(item.name)}`;
|
|
836
|
+
|
|
837
|
+
acc.push({
|
|
838
|
+
dedupeKey,
|
|
839
|
+
name,
|
|
840
|
+
description: bulkDesc(req, item.name),
|
|
841
|
+
method,
|
|
842
|
+
path,
|
|
843
|
+
inputSchema: bulkBuildSchema(req, path.split('?')[0]),
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
function bulkV2ManifestEndpoints() {
|
|
850
|
+
if (!existsSync(BULK_V2_POSTMAN)) return [];
|
|
851
|
+
const col = JSON.parse(readFileSync(BULK_V2_POSTMAN, 'utf8'));
|
|
852
|
+
const varMap = bulkVarMap(col);
|
|
853
|
+
const raw = [];
|
|
854
|
+
bulkExtractRequests(col.item, varMap, raw);
|
|
855
|
+
|
|
856
|
+
const seen = new Set();
|
|
857
|
+
const endpoints = [];
|
|
858
|
+
for (const ep of raw) {
|
|
859
|
+
if (seen.has(ep.dedupeKey)) continue;
|
|
860
|
+
seen.add(ep.dedupeKey);
|
|
861
|
+
const { dedupeKey: _d, ...rest } = ep;
|
|
862
|
+
endpoints.push(rest);
|
|
863
|
+
}
|
|
864
|
+
return endpoints;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
function connectExtractRequests(items, varMap, acc) {
|
|
868
|
+
for (const item of items || []) {
|
|
869
|
+
if (Array.isArray(item.item)) {
|
|
870
|
+
connectExtractRequests(item.item, varMap, acc);
|
|
871
|
+
} else if (item.request) {
|
|
872
|
+
const req = item.request;
|
|
873
|
+
const method = String(req.method || 'GET').toUpperCase();
|
|
874
|
+
if (['OPTIONS', 'HEAD', 'TRACE'].includes(method)) continue;
|
|
875
|
+
|
|
876
|
+
const path = bulkVersionedPath(req, varMap);
|
|
877
|
+
if (!path) continue;
|
|
878
|
+
|
|
879
|
+
const pathKey = path.split('?')[0];
|
|
880
|
+
const dedupeKey = `${method}|${pathKey}`;
|
|
881
|
+
const name = `connect_${bulkSlug(item.name)}`;
|
|
882
|
+
|
|
883
|
+
acc.push({
|
|
884
|
+
dedupeKey,
|
|
885
|
+
name,
|
|
886
|
+
description: bulkDesc(req, item.name),
|
|
887
|
+
method,
|
|
888
|
+
path,
|
|
889
|
+
inputSchema: connectBuildSchema(req, path.split('?')[0]),
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
function connectManifestEndpoints() {
|
|
896
|
+
if (!existsSync(CONNECT_POSTMAN)) return [];
|
|
897
|
+
const col = JSON.parse(readFileSync(CONNECT_POSTMAN, 'utf8'));
|
|
898
|
+
const varMap = bulkVarMap(col);
|
|
899
|
+
const raw = [];
|
|
900
|
+
connectExtractRequests(col.item, varMap, raw);
|
|
901
|
+
|
|
902
|
+
const seen = new Set();
|
|
903
|
+
const endpoints = [];
|
|
904
|
+
for (const ep of raw) {
|
|
905
|
+
if (seen.has(ep.dedupeKey)) continue;
|
|
906
|
+
seen.add(ep.dedupeKey);
|
|
907
|
+
const { dedupeKey: _d, ...rest } = ep;
|
|
908
|
+
endpoints.push(rest);
|
|
909
|
+
}
|
|
910
|
+
return endpoints;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
function connectGeneratedToolsFromPostman() {
|
|
914
|
+
const endpoints = connectManifestEndpoints();
|
|
915
|
+
const toolsOut = [];
|
|
916
|
+
|
|
917
|
+
for (const ep of endpoints) {
|
|
918
|
+
toolsOut.push({
|
|
919
|
+
name: ep.name,
|
|
920
|
+
description: ep.description,
|
|
921
|
+
inputSchema: enrichSchemaForWrite(ep.inputSchema, ep.method),
|
|
922
|
+
handler: async (params) => {
|
|
923
|
+
const [pbase, pq] = ep.path.split('?');
|
|
924
|
+
const merged = { ...params };
|
|
925
|
+
if (pq) {
|
|
926
|
+
for (const [k, v] of new URLSearchParams(pq)) {
|
|
927
|
+
if (merged[k] === undefined) merged[k] = v;
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
const data = await sfRequest(ep.method, pbase, 'versioned', merged);
|
|
931
|
+
return text(data);
|
|
932
|
+
},
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
toolsOut.sort((a, b) => {
|
|
937
|
+
const ea = endpoints.find((e) => e.name === a.name);
|
|
938
|
+
const eb = endpoints.find((e) => e.name === b.name);
|
|
939
|
+
const ra = methodRank(ea?.method);
|
|
940
|
+
const rb = methodRank(eb?.method);
|
|
941
|
+
if (ra !== rb) return ra - rb;
|
|
942
|
+
return a.name.localeCompare(b.name);
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
return toolsOut;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
/** Path under `/tooling` (leading slash), e.g. `/query/?q=...`, from Postman `{_endpoint}/services/data/v{{version}}/tooling/...`. */
|
|
949
|
+
function toolingVersionedPath(req, varMap) {
|
|
950
|
+
const full = bulkVersionedPath(req, varMap);
|
|
951
|
+
if (!full) return null;
|
|
952
|
+
const [pathPart, qs] = full.split('?');
|
|
953
|
+
if (!pathPart.startsWith('/tooling/') && pathPart !== '/tooling') return null;
|
|
954
|
+
const rel = pathPart === '/tooling' ? '' : pathPart.slice('/tooling'.length);
|
|
955
|
+
return qs != null && qs !== '' ? `${rel}?${qs}` : rel;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
function toolingExtractRequests(items, varMap, acc) {
|
|
959
|
+
for (const item of items || []) {
|
|
960
|
+
if (Array.isArray(item.item)) {
|
|
961
|
+
toolingExtractRequests(item.item, varMap, acc);
|
|
962
|
+
} else if (item.request) {
|
|
963
|
+
const req = item.request;
|
|
964
|
+
const method = String(req.method || 'GET').toUpperCase();
|
|
965
|
+
if (['OPTIONS', 'HEAD', 'TRACE'].includes(method)) continue;
|
|
966
|
+
|
|
967
|
+
const path = toolingVersionedPath(req, varMap);
|
|
968
|
+
if (!path) continue;
|
|
969
|
+
|
|
970
|
+
const pathKey = path.split('?')[0];
|
|
971
|
+
const dedupeKey = `${method}|${pathKey}`;
|
|
972
|
+
const name = `tooling_${bulkSlug(item.name)}`;
|
|
973
|
+
|
|
974
|
+
acc.push({
|
|
975
|
+
dedupeKey,
|
|
976
|
+
name,
|
|
977
|
+
description: bulkDesc(req, item.name),
|
|
978
|
+
method,
|
|
979
|
+
path,
|
|
980
|
+
inputSchema: connectBuildSchema(req, path.split('?')[0] || '/'),
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
function toolingManifestEndpoints() {
|
|
987
|
+
if (!existsSync(TOOLING_POSTMAN)) return [];
|
|
988
|
+
const col = JSON.parse(readFileSync(TOOLING_POSTMAN, 'utf8'));
|
|
989
|
+
const varMap = bulkVarMap(col);
|
|
990
|
+
const raw = [];
|
|
991
|
+
toolingExtractRequests(col.item, varMap, raw);
|
|
992
|
+
|
|
993
|
+
const seen = new Set();
|
|
994
|
+
const endpoints = [];
|
|
995
|
+
for (const ep of raw) {
|
|
996
|
+
if (seen.has(ep.dedupeKey)) continue;
|
|
997
|
+
seen.add(ep.dedupeKey);
|
|
998
|
+
const { dedupeKey: _d, ...rest } = ep;
|
|
999
|
+
endpoints.push(rest);
|
|
1000
|
+
}
|
|
1001
|
+
return endpoints;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
function toolingGeneratedToolsFromPostman() {
|
|
1005
|
+
const endpoints = toolingManifestEndpoints();
|
|
1006
|
+
const toolsOut = [];
|
|
1007
|
+
|
|
1008
|
+
for (const ep of endpoints) {
|
|
1009
|
+
toolsOut.push({
|
|
1010
|
+
name: ep.name,
|
|
1011
|
+
description: ep.description,
|
|
1012
|
+
inputSchema: enrichSchemaForWrite(ep.inputSchema, ep.method),
|
|
1013
|
+
handler: async (params) => {
|
|
1014
|
+
const pathWithQ = ep.path.startsWith('/') ? ep.path : `/${ep.path}`;
|
|
1015
|
+
const [pbase, pq] = pathWithQ.split('?');
|
|
1016
|
+
const merged = { ...params };
|
|
1017
|
+
if (pq) {
|
|
1018
|
+
for (const [k, v] of new URLSearchParams(pq)) {
|
|
1019
|
+
if (merged[k] === undefined) merged[k] = v;
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
const data = await sfRequest(ep.method, pbase, 'tooling', merged);
|
|
1023
|
+
return text(data);
|
|
1024
|
+
},
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
toolsOut.sort((a, b) => {
|
|
1029
|
+
const ea = endpoints.find((e) => e.name === a.name);
|
|
1030
|
+
const eb = endpoints.find((e) => e.name === b.name);
|
|
1031
|
+
const ra = methodRank(ea?.method);
|
|
1032
|
+
const rb = methodRank(eb?.method);
|
|
1033
|
+
if (ra !== rb) return ra - rb;
|
|
1034
|
+
return a.name.localeCompare(b.name);
|
|
1035
|
+
});
|
|
1036
|
+
|
|
1037
|
+
return toolsOut;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
/** Path under `/ui-api` from Postman `{_endpoint}/services/data/v{{version}}/ui-api/...`. */
|
|
1041
|
+
function uiVersionedPath(req, varMap) {
|
|
1042
|
+
const full = bulkVersionedPath(req, varMap);
|
|
1043
|
+
if (!full) return null;
|
|
1044
|
+
const [pathPart, qs] = full.split('?');
|
|
1045
|
+
if (!pathPart.startsWith('/ui-api/') && pathPart !== '/ui-api') return null;
|
|
1046
|
+
const rel = pathPart === '/ui-api' ? '' : pathPart.slice('/ui-api'.length);
|
|
1047
|
+
return qs != null && qs !== '' ? `${rel}?${qs}` : rel;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
function uiExtractRequests(items, varMap, acc) {
|
|
1051
|
+
for (const item of items || []) {
|
|
1052
|
+
if (Array.isArray(item.item)) {
|
|
1053
|
+
uiExtractRequests(item.item, varMap, acc);
|
|
1054
|
+
} else if (item.request) {
|
|
1055
|
+
const req = item.request;
|
|
1056
|
+
const method = String(req.method || 'GET').toUpperCase();
|
|
1057
|
+
if (['OPTIONS', 'HEAD', 'TRACE'].includes(method)) continue;
|
|
1058
|
+
|
|
1059
|
+
const path = uiVersionedPath(req, varMap);
|
|
1060
|
+
if (!path) continue;
|
|
1061
|
+
|
|
1062
|
+
const pathKey = path.split('?')[0];
|
|
1063
|
+
const dedupeKey = `${method}|${pathKey}`;
|
|
1064
|
+
const name = `ui_${bulkSlug(item.name)}`;
|
|
1065
|
+
|
|
1066
|
+
acc.push({
|
|
1067
|
+
dedupeKey,
|
|
1068
|
+
name,
|
|
1069
|
+
description: bulkDesc(req, item.name),
|
|
1070
|
+
method,
|
|
1071
|
+
path,
|
|
1072
|
+
inputSchema: connectBuildSchema(req, path.split('?')[0] || '/'),
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
function uiManifestEndpoints() {
|
|
1079
|
+
if (!existsSync(UI_POSTMAN)) return [];
|
|
1080
|
+
const col = JSON.parse(readFileSync(UI_POSTMAN, 'utf8'));
|
|
1081
|
+
const varMap = bulkVarMap(col);
|
|
1082
|
+
const raw = [];
|
|
1083
|
+
uiExtractRequests(col.item, varMap, raw);
|
|
1084
|
+
|
|
1085
|
+
const seen = new Set();
|
|
1086
|
+
const endpoints = [];
|
|
1087
|
+
for (const ep of raw) {
|
|
1088
|
+
if (seen.has(ep.dedupeKey)) continue;
|
|
1089
|
+
seen.add(ep.dedupeKey);
|
|
1090
|
+
const { dedupeKey: _d, ...rest } = ep;
|
|
1091
|
+
endpoints.push(rest);
|
|
1092
|
+
}
|
|
1093
|
+
return endpoints;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
function uiGeneratedToolsFromPostman() {
|
|
1097
|
+
const endpoints = uiManifestEndpoints();
|
|
1098
|
+
const toolsOut = [];
|
|
1099
|
+
|
|
1100
|
+
for (const ep of endpoints) {
|
|
1101
|
+
toolsOut.push({
|
|
1102
|
+
name: ep.name,
|
|
1103
|
+
description: ep.description,
|
|
1104
|
+
inputSchema: enrichSchemaForWrite(ep.inputSchema, ep.method),
|
|
1105
|
+
handler: async (params) => {
|
|
1106
|
+
const pathWithQ = ep.path.startsWith('/') ? ep.path : `/${ep.path}`;
|
|
1107
|
+
const [pbase, pq] = pathWithQ.split('?');
|
|
1108
|
+
const merged = { ...params };
|
|
1109
|
+
if (pq) {
|
|
1110
|
+
for (const [k, v] of new URLSearchParams(pq)) {
|
|
1111
|
+
if (merged[k] === undefined) merged[k] = v;
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
const data = await sfRequest(ep.method, pbase, 'ui-api', merged);
|
|
1115
|
+
return text(data);
|
|
1116
|
+
},
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
toolsOut.sort((a, b) => {
|
|
1121
|
+
const ea = endpoints.find((e) => e.name === a.name);
|
|
1122
|
+
const eb = endpoints.find((e) => e.name === b.name);
|
|
1123
|
+
const ra = methodRank(ea?.method);
|
|
1124
|
+
const rb = methodRank(eb?.method);
|
|
1125
|
+
if (ra !== rb) return ra - rb;
|
|
1126
|
+
return a.name.localeCompare(b.name);
|
|
1127
|
+
});
|
|
1128
|
+
|
|
1129
|
+
return toolsOut;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
/** Smart Data Discovery paths: `/services/data/v…/smartdatadiscovery/…` (same base as `getBaseUrl`). */
|
|
1133
|
+
function einsteinPsVersionedPath(req, varMap) {
|
|
1134
|
+
const full = bulkVersionedPath(req, varMap);
|
|
1135
|
+
if (!full) return null;
|
|
1136
|
+
const pathPart = full.split('?')[0];
|
|
1137
|
+
if (!pathPart.startsWith('/smartdatadiscovery/') && pathPart !== '/smartdatadiscovery') return null;
|
|
1138
|
+
return full;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
function einsteinPsExtractRequests(items, varMap, acc) {
|
|
1142
|
+
for (const item of items || []) {
|
|
1143
|
+
if (Array.isArray(item.item)) {
|
|
1144
|
+
einsteinPsExtractRequests(item.item, varMap, acc);
|
|
1145
|
+
} else if (item.request) {
|
|
1146
|
+
const req = item.request;
|
|
1147
|
+
const method = String(req.method || 'GET').toUpperCase();
|
|
1148
|
+
if (['OPTIONS', 'HEAD', 'TRACE'].includes(method)) continue;
|
|
1149
|
+
|
|
1150
|
+
const path = einsteinPsVersionedPath(req, varMap);
|
|
1151
|
+
if (!path) continue;
|
|
1152
|
+
|
|
1153
|
+
const pathKey = path.split('?')[0];
|
|
1154
|
+
const dedupeKey = `${method}|${pathKey}`;
|
|
1155
|
+
const name = `einstein_ps_${bulkSlug(item.name)}`;
|
|
1156
|
+
|
|
1157
|
+
acc.push({
|
|
1158
|
+
dedupeKey,
|
|
1159
|
+
name,
|
|
1160
|
+
description: bulkDesc(req, item.name),
|
|
1161
|
+
method,
|
|
1162
|
+
path,
|
|
1163
|
+
inputSchema: connectBuildSchema(req, path.split('?')[0] || '/'),
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
function einsteinPsManifestEndpoints() {
|
|
1170
|
+
if (!existsSync(EINSTEIN_PS_POSTMAN)) return [];
|
|
1171
|
+
const col = JSON.parse(readFileSync(EINSTEIN_PS_POSTMAN, 'utf8'));
|
|
1172
|
+
const varMap = bulkVarMap(col);
|
|
1173
|
+
const raw = [];
|
|
1174
|
+
einsteinPsExtractRequests(col.item, varMap, raw);
|
|
1175
|
+
|
|
1176
|
+
const seen = new Set();
|
|
1177
|
+
const endpoints = [];
|
|
1178
|
+
for (const ep of raw) {
|
|
1179
|
+
if (seen.has(ep.dedupeKey)) continue;
|
|
1180
|
+
seen.add(ep.dedupeKey);
|
|
1181
|
+
const { dedupeKey: _d, ...rest } = ep;
|
|
1182
|
+
endpoints.push(rest);
|
|
1183
|
+
}
|
|
1184
|
+
return endpoints;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
function einsteinPsGeneratedToolsFromPostman() {
|
|
1188
|
+
const endpoints = einsteinPsManifestEndpoints();
|
|
1189
|
+
const toolsOut = [];
|
|
1190
|
+
|
|
1191
|
+
for (const ep of endpoints) {
|
|
1192
|
+
toolsOut.push({
|
|
1193
|
+
name: ep.name,
|
|
1194
|
+
description: ep.description,
|
|
1195
|
+
inputSchema: enrichSchemaForWrite(ep.inputSchema, ep.method),
|
|
1196
|
+
handler: async (params) => {
|
|
1197
|
+
const pathWithQ = ep.path.startsWith('/') ? ep.path : `/${ep.path}`;
|
|
1198
|
+
const [pbase, pq] = pathWithQ.split('?');
|
|
1199
|
+
const merged = { ...params };
|
|
1200
|
+
if (pq) {
|
|
1201
|
+
for (const [k, v] of new URLSearchParams(pq)) {
|
|
1202
|
+
if (merged[k] === undefined) merged[k] = v;
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
const data = await sfRequest(ep.method, pbase, 'versioned', merged);
|
|
1206
|
+
return text(data);
|
|
1207
|
+
},
|
|
1208
|
+
});
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
toolsOut.sort((a, b) => {
|
|
1212
|
+
const ea = endpoints.find((e) => e.name === a.name);
|
|
1213
|
+
const eb = endpoints.find((e) => e.name === b.name);
|
|
1214
|
+
const ra = methodRank(ea?.method);
|
|
1215
|
+
const rb = methodRank(eb?.method);
|
|
1216
|
+
if (ra !== rb) return ra - rb;
|
|
1217
|
+
return a.name.localeCompare(b.name);
|
|
1218
|
+
});
|
|
1219
|
+
|
|
1220
|
+
return toolsOut;
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
/** Event Platform: Tooling-relative paths vs standard `/services/data/v…` paths. */
|
|
1224
|
+
function eventPlatformPathAndKind(req, varMap) {
|
|
1225
|
+
const full = bulkVersionedPath(req, varMap);
|
|
1226
|
+
if (!full) return null;
|
|
1227
|
+
const [pathPart, qs] = full.split('?');
|
|
1228
|
+
if (SKIP_BULK_PATH_RE.test(pathPart)) return null;
|
|
1229
|
+
const qstr = qs !== undefined && qs !== '' ? `?${qs}` : '';
|
|
1230
|
+
if (pathPart.startsWith('/tooling/') || pathPart === '/tooling') {
|
|
1231
|
+
const rel = pathPart === '/tooling' ? '' : pathPart.slice('/tooling'.length);
|
|
1232
|
+
return { path: rel + qstr, urlKind: 'tooling' };
|
|
1233
|
+
}
|
|
1234
|
+
return { path: pathPart + qstr, urlKind: 'versioned' };
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
function eventPlatformExtractRequests(items, varMap, acc) {
|
|
1238
|
+
for (const item of items || []) {
|
|
1239
|
+
if (Array.isArray(item.item)) {
|
|
1240
|
+
eventPlatformExtractRequests(item.item, varMap, acc);
|
|
1241
|
+
} else if (item.request) {
|
|
1242
|
+
const req = item.request;
|
|
1243
|
+
const method = String(req.method || 'GET').toUpperCase();
|
|
1244
|
+
if (['OPTIONS', 'HEAD', 'TRACE'].includes(method)) continue;
|
|
1245
|
+
|
|
1246
|
+
const pk = eventPlatformPathAndKind(req, varMap);
|
|
1247
|
+
if (!pk) continue;
|
|
1248
|
+
|
|
1249
|
+
const pathKey = pk.path.split('?')[0];
|
|
1250
|
+
const dedupeKey = `${method}|${pathKey}|${pk.urlKind}`;
|
|
1251
|
+
const name = `evt_pf_${bulkSlug(item.name)}`;
|
|
1252
|
+
|
|
1253
|
+
acc.push({
|
|
1254
|
+
dedupeKey,
|
|
1255
|
+
name,
|
|
1256
|
+
description: bulkDesc(req, item.name),
|
|
1257
|
+
method,
|
|
1258
|
+
path: pk.path,
|
|
1259
|
+
urlKind: pk.urlKind,
|
|
1260
|
+
inputSchema: connectBuildSchema(req, pathKey || '/'),
|
|
1261
|
+
});
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
function eventPlatformManifestEndpoints() {
|
|
1267
|
+
if (!existsSync(EVENT_PLATFORM_POSTMAN)) return [];
|
|
1268
|
+
const col = JSON.parse(readFileSync(EVENT_PLATFORM_POSTMAN, 'utf8'));
|
|
1269
|
+
const varMap = bulkVarMap(col);
|
|
1270
|
+
const raw = [];
|
|
1271
|
+
eventPlatformExtractRequests(col.item, varMap, raw);
|
|
1272
|
+
|
|
1273
|
+
const seen = new Set();
|
|
1274
|
+
const endpoints = [];
|
|
1275
|
+
for (const ep of raw) {
|
|
1276
|
+
if (seen.has(ep.dedupeKey)) continue;
|
|
1277
|
+
seen.add(ep.dedupeKey);
|
|
1278
|
+
const { dedupeKey: _d, ...rest } = ep;
|
|
1279
|
+
endpoints.push(rest);
|
|
1280
|
+
}
|
|
1281
|
+
return endpoints;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
function eventPlatformGeneratedToolsFromPostman() {
|
|
1285
|
+
const endpoints = eventPlatformManifestEndpoints();
|
|
1286
|
+
const toolsOut = [];
|
|
1287
|
+
|
|
1288
|
+
for (const ep of endpoints) {
|
|
1289
|
+
toolsOut.push({
|
|
1290
|
+
name: ep.name,
|
|
1291
|
+
description: ep.description,
|
|
1292
|
+
inputSchema: enrichSchemaForWrite(ep.inputSchema, ep.method),
|
|
1293
|
+
handler: async (params) => {
|
|
1294
|
+
const pathWithQ = ep.path.startsWith('/') ? ep.path : `/${ep.path}`;
|
|
1295
|
+
const [pbase, pq] = pathWithQ.split('?');
|
|
1296
|
+
const merged = { ...params };
|
|
1297
|
+
if (pq) {
|
|
1298
|
+
for (const [k, v] of new URLSearchParams(pq)) {
|
|
1299
|
+
if (merged[k] === undefined) merged[k] = v;
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
const data = await sfRequest(ep.method, pbase, ep.urlKind, merged);
|
|
1303
|
+
return text(data);
|
|
1304
|
+
},
|
|
1305
|
+
});
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
toolsOut.sort((a, b) => {
|
|
1309
|
+
const ea = endpoints.find((e) => e.name === a.name);
|
|
1310
|
+
const eb = endpoints.find((e) => e.name === b.name);
|
|
1311
|
+
const ra = methodRank(ea?.method);
|
|
1312
|
+
const rb = methodRank(eb?.method);
|
|
1313
|
+
if (ra !== rb) return ra - rb;
|
|
1314
|
+
return a.name.localeCompare(b.name);
|
|
1315
|
+
});
|
|
1316
|
+
|
|
1317
|
+
return toolsOut;
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
function graphqlExtractRequests(items, varMap, acc) {
|
|
1321
|
+
for (const item of items || []) {
|
|
1322
|
+
if (Array.isArray(item.item)) {
|
|
1323
|
+
graphqlExtractRequests(item.item, varMap, acc);
|
|
1324
|
+
} else if (item.request) {
|
|
1325
|
+
const req = item.request;
|
|
1326
|
+
const method = String(req.method || 'GET').toUpperCase();
|
|
1327
|
+
if (['OPTIONS', 'HEAD', 'TRACE'].includes(method)) continue;
|
|
1328
|
+
|
|
1329
|
+
const path = bulkVersionedPath(req, varMap);
|
|
1330
|
+
if (!path || path.split('?')[0] !== '/graphql') continue;
|
|
1331
|
+
|
|
1332
|
+
const slug = bulkSlug(item.name);
|
|
1333
|
+
const dedupeKey = `${method}|/graphql|${slug}`;
|
|
1334
|
+
acc.push({
|
|
1335
|
+
dedupeKey,
|
|
1336
|
+
name: `gql_${slug}`,
|
|
1337
|
+
description: bulkDesc(req, item.name) || item.name,
|
|
1338
|
+
method,
|
|
1339
|
+
inputSchema: graphqlBuildSchema(req),
|
|
1340
|
+
});
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
function graphqlManifestEndpoints() {
|
|
1346
|
+
if (!existsSync(GRAPHQL_POSTMAN)) return [];
|
|
1347
|
+
const col = JSON.parse(readFileSync(GRAPHQL_POSTMAN, 'utf8'));
|
|
1348
|
+
const varMap = bulkVarMap(col);
|
|
1349
|
+
const raw = [];
|
|
1350
|
+
graphqlExtractRequests(col.item, varMap, raw);
|
|
1351
|
+
|
|
1352
|
+
const seen = new Set();
|
|
1353
|
+
const endpoints = [];
|
|
1354
|
+
for (const ep of raw) {
|
|
1355
|
+
if (seen.has(ep.dedupeKey)) continue;
|
|
1356
|
+
seen.add(ep.dedupeKey);
|
|
1357
|
+
const { dedupeKey: _d, ...rest } = ep;
|
|
1358
|
+
endpoints.push(rest);
|
|
1359
|
+
}
|
|
1360
|
+
return endpoints;
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
function graphqlGeneratedToolsFromPostman() {
|
|
1364
|
+
const endpoints = graphqlManifestEndpoints();
|
|
1365
|
+
const toolsOut = [];
|
|
1366
|
+
|
|
1367
|
+
for (const ep of endpoints) {
|
|
1368
|
+
toolsOut.push({
|
|
1369
|
+
name: ep.name,
|
|
1370
|
+
description: ep.description,
|
|
1371
|
+
inputSchema: enrichSchemaForWrite(ep.inputSchema, ep.method),
|
|
1372
|
+
handler: async (params) => {
|
|
1373
|
+
const body = { query: params.query };
|
|
1374
|
+
if (
|
|
1375
|
+
params.variables != null &&
|
|
1376
|
+
typeof params.variables === 'object' &&
|
|
1377
|
+
!Array.isArray(params.variables) &&
|
|
1378
|
+
Object.keys(params.variables).length > 0
|
|
1379
|
+
) {
|
|
1380
|
+
body.variables = params.variables;
|
|
1381
|
+
}
|
|
1382
|
+
const data = await sfRequest(ep.method, '/', 'graphql', { _rawBody: body });
|
|
1383
|
+
return text(data);
|
|
1384
|
+
},
|
|
1385
|
+
});
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
toolsOut.sort((a, b) => {
|
|
1389
|
+
const ea = endpoints.find((e) => e.name === a.name);
|
|
1390
|
+
const eb = endpoints.find((e) => e.name === b.name);
|
|
1391
|
+
const ra = methodRank(ea?.method);
|
|
1392
|
+
const rb = methodRank(eb?.method);
|
|
1393
|
+
if (ra !== rb) return ra - rb;
|
|
1394
|
+
return a.name.localeCompare(b.name);
|
|
1395
|
+
});
|
|
1396
|
+
|
|
1397
|
+
return toolsOut;
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
function subMgmtExtractRequests(items, varMap, acc) {
|
|
1401
|
+
for (const item of items || []) {
|
|
1402
|
+
if (Array.isArray(item.item)) {
|
|
1403
|
+
subMgmtExtractRequests(item.item, varMap, acc);
|
|
1404
|
+
} else if (item.request) {
|
|
1405
|
+
const req = item.request;
|
|
1406
|
+
const method = String(req.method || 'GET').toUpperCase();
|
|
1407
|
+
if (['OPTIONS', 'HEAD', 'TRACE'].includes(method)) continue;
|
|
1408
|
+
|
|
1409
|
+
const path = bulkVersionedPath(req, varMap);
|
|
1410
|
+
if (!path) continue;
|
|
1411
|
+
const pathPart = path.split('?')[0];
|
|
1412
|
+
if (SKIP_BULK_PATH_RE.test(pathPart)) continue;
|
|
1413
|
+
|
|
1414
|
+
const slug = bulkSlug(item.name);
|
|
1415
|
+
const dedupeKey = `${method}|${path}|${slug}`;
|
|
1416
|
+
acc.push({
|
|
1417
|
+
dedupeKey,
|
|
1418
|
+
name: `sub_mgmt_${slug}`,
|
|
1419
|
+
description: bulkDesc(req, item.name) || item.name,
|
|
1420
|
+
method,
|
|
1421
|
+
path,
|
|
1422
|
+
inputSchema: connectBuildSchema(req, pathPart || '/'),
|
|
1423
|
+
});
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
function subMgmtManifestEndpoints() {
|
|
1429
|
+
if (!existsSync(SUBSCRIPTION_MGMT_POSTMAN)) return [];
|
|
1430
|
+
const col = JSON.parse(readFileSync(SUBSCRIPTION_MGMT_POSTMAN, 'utf8'));
|
|
1431
|
+
const varMap = bulkVarMap(col);
|
|
1432
|
+
const raw = [];
|
|
1433
|
+
subMgmtExtractRequests(col.item, varMap, raw);
|
|
1434
|
+
|
|
1435
|
+
const seen = new Set();
|
|
1436
|
+
const endpoints = [];
|
|
1437
|
+
for (const ep of raw) {
|
|
1438
|
+
if (seen.has(ep.dedupeKey)) continue;
|
|
1439
|
+
seen.add(ep.dedupeKey);
|
|
1440
|
+
const { dedupeKey: _d, ...rest } = ep;
|
|
1441
|
+
endpoints.push(rest);
|
|
1442
|
+
}
|
|
1443
|
+
return endpoints;
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
function subMgmtGeneratedToolsFromPostman() {
|
|
1447
|
+
const endpoints = subMgmtManifestEndpoints();
|
|
1448
|
+
const toolsOut = [];
|
|
1449
|
+
|
|
1450
|
+
for (const ep of endpoints) {
|
|
1451
|
+
toolsOut.push({
|
|
1452
|
+
name: ep.name,
|
|
1453
|
+
description: ep.description,
|
|
1454
|
+
inputSchema: enrichSchemaForWrite(ep.inputSchema, ep.method),
|
|
1455
|
+
handler: async (params) => {
|
|
1456
|
+
const pathWithQ = ep.path.startsWith('/') ? ep.path : `/${ep.path}`;
|
|
1457
|
+
const [pbase, pq] = pathWithQ.split('?');
|
|
1458
|
+
const merged = { ...params };
|
|
1459
|
+
if (pq) {
|
|
1460
|
+
for (const [k, v] of new URLSearchParams(pq)) {
|
|
1461
|
+
if (merged[k] === undefined) merged[k] = v;
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
const data = await sfRequest(ep.method, pbase, 'versioned', merged);
|
|
1465
|
+
return text(data);
|
|
1466
|
+
},
|
|
1467
|
+
});
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
toolsOut.sort((a, b) => {
|
|
1471
|
+
const ea = endpoints.find((e) => e.name === a.name);
|
|
1472
|
+
const eb = endpoints.find((e) => e.name === b.name);
|
|
1473
|
+
const ra = methodRank(ea?.method);
|
|
1474
|
+
const rb = methodRank(eb?.method);
|
|
1475
|
+
if (ra !== rb) return ra - rb;
|
|
1476
|
+
return a.name.localeCompare(b.name);
|
|
1477
|
+
});
|
|
1478
|
+
|
|
1479
|
+
return toolsOut;
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
/** Apex REST or platform REST path for Industries Postman items. */
|
|
1483
|
+
function industriesPathAndKind(req, varMap) {
|
|
1484
|
+
const raw = typeof req.url === 'string' ? req.url : req.url?.raw || '';
|
|
1485
|
+
const resolved = bulkResolve(raw, varMap);
|
|
1486
|
+
const [pathPart, qs] = resolved.split('?');
|
|
1487
|
+
const noHost = pathPart.replace(/^https?:\/\/[^/?#]+/i, '');
|
|
1488
|
+
|
|
1489
|
+
if (noHost.startsWith('/services/apexrest')) {
|
|
1490
|
+
let after = noHost.slice('/services/apexrest'.length);
|
|
1491
|
+
if (!after.startsWith('/')) after = `/${after}`;
|
|
1492
|
+
const qstr = qs !== undefined && qs !== '' ? `?${qs}` : '';
|
|
1493
|
+
return { path: after + qstr, urlKind: 'apex' };
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
const full = bulkVersionedPath(req, varMap);
|
|
1497
|
+
if (!full) return null;
|
|
1498
|
+
const pp = full.split('?')[0];
|
|
1499
|
+
if (SKIP_BULK_PATH_RE.test(pp)) return null;
|
|
1500
|
+
return { path: full, urlKind: 'versioned' };
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
function industriesUseCasePrefix(trail, itemName, pk) {
|
|
1504
|
+
const blob = [...trail, itemName || ''].join(' ').toLowerCase();
|
|
1505
|
+
const pathLow = (pk.path || '').toLowerCase();
|
|
1506
|
+
const nameLow = (itemName || '').toLowerCase();
|
|
1507
|
+
if (
|
|
1508
|
+
pathLow.includes('sbqq') ||
|
|
1509
|
+
(pathLow.includes('servicerouter') &&
|
|
1510
|
+
/quote|config|contract|product|proposal|save|calculate|validate|read|amend|renew|add.product/i.test(nameLow + blob))
|
|
1511
|
+
) {
|
|
1512
|
+
return 'CPQ / Revenue (configure-price-quote, contracts, product rules) — ';
|
|
1513
|
+
}
|
|
1514
|
+
if (
|
|
1515
|
+
/loyalty|member|voucher|promotion|points|enroll|ledger|journal|benefit|tier|game|redeem|cart|wallet/i.test(
|
|
1516
|
+
blob
|
|
1517
|
+
)
|
|
1518
|
+
) {
|
|
1519
|
+
return 'Loyalty & programs (member lifecycle, offers, wallets) — ';
|
|
1520
|
+
}
|
|
1521
|
+
if (/integration procedure|omnistudio/i.test(blob) || /integration procedure/i.test(nameLow)) {
|
|
1522
|
+
return 'Omnistudio (Integration Procedures, guided flows) — ';
|
|
1523
|
+
}
|
|
1524
|
+
if (pk.urlKind === 'versioned') {
|
|
1525
|
+
return 'Industries REST (platform / data) — ';
|
|
1526
|
+
}
|
|
1527
|
+
return 'Salesforce Industries — ';
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
function industriesExtractRequests(items, varMap, acc, trail = []) {
|
|
1531
|
+
for (const item of items || []) {
|
|
1532
|
+
if (Array.isArray(item.item)) {
|
|
1533
|
+
const label = item.name ? String(item.name) : '';
|
|
1534
|
+
industriesExtractRequests(item.item, varMap, acc, label ? [...trail, label] : trail);
|
|
1535
|
+
} else if (item.request) {
|
|
1536
|
+
const req = item.request;
|
|
1537
|
+
const method = String(req.method || 'GET').toUpperCase();
|
|
1538
|
+
if (['OPTIONS', 'HEAD', 'TRACE'].includes(method)) continue;
|
|
1539
|
+
|
|
1540
|
+
const pk = industriesPathAndKind(req, varMap);
|
|
1541
|
+
if (!pk) continue;
|
|
1542
|
+
const pathPart = pk.path.split('?')[0];
|
|
1543
|
+
if (SKIP_BULK_PATH_RE.test(pathPart)) continue;
|
|
1544
|
+
|
|
1545
|
+
const slug = bulkSlug(item.name);
|
|
1546
|
+
const dedupeKey = `${method}|${pk.path}|${pk.urlKind}|${slug}`;
|
|
1547
|
+
const baseDesc = bulkDesc(req, item.name) || item.name;
|
|
1548
|
+
const breadcrumbs = trail.length ? ` Context: ${trail.join(' › ')}.` : '';
|
|
1549
|
+
const description = (
|
|
1550
|
+
industriesUseCasePrefix(trail, item.name, pk) +
|
|
1551
|
+
baseDesc +
|
|
1552
|
+
breadcrumbs
|
|
1553
|
+
).slice(0, 500);
|
|
1554
|
+
|
|
1555
|
+
acc.push({
|
|
1556
|
+
dedupeKey,
|
|
1557
|
+
name: `industries_${slug}`,
|
|
1558
|
+
description,
|
|
1559
|
+
method,
|
|
1560
|
+
path: pk.path,
|
|
1561
|
+
urlKind: pk.urlKind,
|
|
1562
|
+
inputSchema: connectBuildSchema(req, pathPart || '/'),
|
|
1563
|
+
});
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
function industriesManifestEndpoints() {
|
|
1569
|
+
if (!existsSync(INDUSTRIES_POSTMAN)) return [];
|
|
1570
|
+
const col = JSON.parse(readFileSync(INDUSTRIES_POSTMAN, 'utf8'));
|
|
1571
|
+
const varMap = bulkVarMap(col);
|
|
1572
|
+
const raw = [];
|
|
1573
|
+
industriesExtractRequests(col.item, varMap, raw);
|
|
1574
|
+
|
|
1575
|
+
const seen = new Set();
|
|
1576
|
+
const endpoints = [];
|
|
1577
|
+
for (const ep of raw) {
|
|
1578
|
+
if (seen.has(ep.dedupeKey)) continue;
|
|
1579
|
+
seen.add(ep.dedupeKey);
|
|
1580
|
+
const { dedupeKey: _d, ...rest } = ep;
|
|
1581
|
+
endpoints.push(rest);
|
|
1582
|
+
}
|
|
1583
|
+
return endpoints;
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
function industriesGeneratedToolsFromPostman() {
|
|
1587
|
+
const endpoints = industriesManifestEndpoints();
|
|
1588
|
+
const toolsOut = [];
|
|
1589
|
+
|
|
1590
|
+
for (const ep of endpoints) {
|
|
1591
|
+
toolsOut.push({
|
|
1592
|
+
name: ep.name,
|
|
1593
|
+
description: ep.description,
|
|
1594
|
+
inputSchema: enrichSchemaForWrite(ep.inputSchema, ep.method),
|
|
1595
|
+
handler: async (params) => {
|
|
1596
|
+
const pathWithQ = ep.path.startsWith('/') ? ep.path : `/${ep.path}`;
|
|
1597
|
+
const [pbase, pq] = pathWithQ.split('?');
|
|
1598
|
+
const merged = { ...params };
|
|
1599
|
+
if (pq) {
|
|
1600
|
+
for (const [k, v] of new URLSearchParams(pq)) {
|
|
1601
|
+
if (merged[k] === undefined) merged[k] = v;
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
const data = await sfRequest(ep.method, pbase, ep.urlKind, merged);
|
|
1605
|
+
return text(data);
|
|
1606
|
+
},
|
|
1607
|
+
});
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
toolsOut.sort((a, b) => {
|
|
1611
|
+
const ea = endpoints.find((e) => e.name === a.name);
|
|
1612
|
+
const eb = endpoints.find((e) => e.name === b.name);
|
|
1613
|
+
const ra = methodRank(ea?.method);
|
|
1614
|
+
const rb = methodRank(eb?.method);
|
|
1615
|
+
if (ra !== rb) return ra - rb;
|
|
1616
|
+
return a.name.localeCompare(b.name);
|
|
1617
|
+
});
|
|
1618
|
+
|
|
1619
|
+
return toolsOut;
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
function bulkV2IsCsvResultGet(method, path) {
|
|
1623
|
+
return (
|
|
1624
|
+
method === 'GET' &&
|
|
1625
|
+
/\/jobs\/ingest\/\{jobId\}\/(successfulResults|failedResults|unprocessedrecords)$/.test(
|
|
1626
|
+
path.split('?')[0]
|
|
1627
|
+
)
|
|
1628
|
+
);
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
function bulkV2GeneratedTools() {
|
|
1632
|
+
const endpoints = bulkV2ManifestEndpoints();
|
|
1633
|
+
const toolsOut = [];
|
|
1634
|
+
|
|
1635
|
+
for (const ep of endpoints) {
|
|
1636
|
+
const pathNoQuery = ep.path.split('?')[0];
|
|
1637
|
+
const isCsvPut = ep.method === 'PUT' && pathNoQuery.endsWith('/batches');
|
|
1638
|
+
const isCsvGet = bulkV2IsCsvResultGet(ep.method, pathNoQuery);
|
|
1639
|
+
|
|
1640
|
+
if (isCsvPut) {
|
|
1641
|
+
toolsOut.push({
|
|
1642
|
+
name: ep.name,
|
|
1643
|
+
description: ep.description,
|
|
1644
|
+
inputSchema: enrichSchemaForWrite(ep.inputSchema, ep.method),
|
|
1645
|
+
handler: async (params) => {
|
|
1646
|
+
const basePath = ep.path.split('?')[0];
|
|
1647
|
+
const { pathValues, rest } = splitPathAndRest(basePath, params);
|
|
1648
|
+
const csv = rest.csv_data;
|
|
1649
|
+
if (csv == null) throw new Error('csv_data is required for batch upload');
|
|
1650
|
+
await ensureAccessToken();
|
|
1651
|
+
const url = getBaseUrl() + applyPathTemplate(basePath, pathValues);
|
|
1652
|
+
const headers = { ...buildAuthHeaders(), 'Content-Type': 'text/csv', Accept: 'application/json' };
|
|
1653
|
+
try {
|
|
1654
|
+
const res = await axios.put(url, csv, {
|
|
1655
|
+
headers,
|
|
1656
|
+
timeout: 120000,
|
|
1657
|
+
validateStatus: () => true,
|
|
1658
|
+
});
|
|
1659
|
+
if (res.status < 200 || res.status >= 300) handleError({ response: res });
|
|
1660
|
+
return text(res.data && Object.keys(res.data).length ? res.data : { success: true, status: res.status });
|
|
1661
|
+
} catch (e) {
|
|
1662
|
+
handleError(e);
|
|
1663
|
+
}
|
|
1664
|
+
},
|
|
1665
|
+
});
|
|
1666
|
+
continue;
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
if (isCsvGet) {
|
|
1670
|
+
toolsOut.push({
|
|
1671
|
+
name: ep.name,
|
|
1672
|
+
description: ep.description,
|
|
1673
|
+
inputSchema: enrichSchemaForWrite(ep.inputSchema, ep.method),
|
|
1674
|
+
handler: async (params) => {
|
|
1675
|
+
const queryPath = ep.path.includes('?') ? ep.path.split('?')[1] : '';
|
|
1676
|
+
const staticQuery = queryPath ? Object.fromEntries(new URLSearchParams(queryPath)) : {};
|
|
1677
|
+
const basePath = ep.path.split('?')[0];
|
|
1678
|
+
const { pathValues, rest } = splitPathAndRest(basePath, params);
|
|
1679
|
+
await ensureAccessToken();
|
|
1680
|
+
const url = getBaseUrl() + applyPathTemplate(basePath, pathValues);
|
|
1681
|
+
const headers = { ...buildAuthHeaders(), Accept: 'text/csv' };
|
|
1682
|
+
try {
|
|
1683
|
+
const res = await axios.get(url, {
|
|
1684
|
+
headers,
|
|
1685
|
+
params: { ...staticQuery, ...rest },
|
|
1686
|
+
responseType: 'text',
|
|
1687
|
+
timeout: 120000,
|
|
1688
|
+
validateStatus: () => true,
|
|
1689
|
+
});
|
|
1690
|
+
if (res.status < 200 || res.status >= 300) handleError({ response: res });
|
|
1691
|
+
return text({ format: 'text/csv', body: res.data ?? '' });
|
|
1692
|
+
} catch (e) {
|
|
1693
|
+
handleError(e);
|
|
1694
|
+
}
|
|
1695
|
+
},
|
|
1696
|
+
});
|
|
1697
|
+
continue;
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
toolsOut.push({
|
|
1701
|
+
name: ep.name,
|
|
1702
|
+
description: ep.description,
|
|
1703
|
+
inputSchema: enrichSchemaForWrite(ep.inputSchema, ep.method),
|
|
1704
|
+
handler: async (params) => {
|
|
1705
|
+
const [pbase, pq] = ep.path.split('?');
|
|
1706
|
+
const merged = { ...params };
|
|
1707
|
+
if (pq) {
|
|
1708
|
+
for (const [k, v] of new URLSearchParams(pq)) {
|
|
1709
|
+
if (merged[k] === undefined) merged[k] = v;
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
const data = await sfRequest(ep.method, pbase, 'versioned', merged);
|
|
1713
|
+
return text(data);
|
|
1714
|
+
},
|
|
1715
|
+
});
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
toolsOut.sort((a, b) => {
|
|
1719
|
+
const ea = endpoints.find((e) => e.name === a.name);
|
|
1720
|
+
const eb = endpoints.find((e) => e.name === b.name);
|
|
1721
|
+
const ra = methodRank(ea?.method);
|
|
1722
|
+
const rb = methodRank(eb?.method);
|
|
1723
|
+
if (ra !== rb) return ra - rb;
|
|
1724
|
+
return a.name.localeCompare(b.name);
|
|
1725
|
+
});
|
|
1726
|
+
|
|
1727
|
+
return toolsOut;
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
const BULK_V1_PATH_MARK_JOB = '\uE000\uE001JOB\uE000\uE002';
|
|
1731
|
+
const BULK_V1_PATH_MARK_BATCH = '\uE000\uE001BATCH\uE000\uE002';
|
|
1732
|
+
|
|
1733
|
+
/** Preserve `{{_jobId}}` / `{{_batchId}}` as `{jobId}` / `{batchId}` after resolving `_endpoint` / `version`. */
|
|
1734
|
+
function bulkPathAwareResolve(raw, varMap) {
|
|
1735
|
+
let pre = String(raw);
|
|
1736
|
+
pre = pre.replace(/\{\{_jobId\}\}/g, BULK_V1_PATH_MARK_JOB);
|
|
1737
|
+
pre = pre.replace(/\{\{_batchId\}\}/g, BULK_V1_PATH_MARK_BATCH);
|
|
1738
|
+
let resolved = bulkResolve(pre, varMap);
|
|
1739
|
+
resolved = resolved.split(BULK_V1_PATH_MARK_JOB).join('{jobId}');
|
|
1740
|
+
resolved = resolved.split(BULK_V1_PATH_MARK_BATCH).join('{batchId}');
|
|
1741
|
+
return resolved;
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
/** Paths under Bulk v1 split: REST bulk query jobs (`versioned`) or Async Bulk 1.0 (`bulk-v1-async`). */
|
|
1745
|
+
function bulkV1PathAndKind(req, varMap) {
|
|
1746
|
+
const raw = typeof req.url === 'string' ? req.url : req.url?.raw || '';
|
|
1747
|
+
const resolved = bulkPathAwareResolve(raw, varMap);
|
|
1748
|
+
const [pathPart, qs] = resolved.split('?');
|
|
1749
|
+
if (SKIP_BULK_PATH_RE.test(pathPart)) return null;
|
|
1750
|
+
const noHost = pathPart.replace(/^https?:\/\/[^/?#]+/i, '');
|
|
1751
|
+
|
|
1752
|
+
if (noHost.startsWith('/services/data/')) {
|
|
1753
|
+
const after = noHost.slice('/services/data/'.length);
|
|
1754
|
+
const m = after.match(/^(v[0-9.]+)\/(.+)$/);
|
|
1755
|
+
if (!m) return null;
|
|
1756
|
+
let path = `/${m[2]}`;
|
|
1757
|
+
path = path.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, '{$1}');
|
|
1758
|
+
if (qs) path = `${path}?${qs}`;
|
|
1759
|
+
return { path, urlKind: 'versioned' };
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
if (noHost.startsWith('/services/async/')) {
|
|
1763
|
+
const after = noHost.slice('/services/async/'.length);
|
|
1764
|
+
const m = after.match(/^([0-9.]+)\/(.+)$/);
|
|
1765
|
+
if (!m) return null;
|
|
1766
|
+
let path = `/${m[2]}`;
|
|
1767
|
+
path = path.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, '{$1}');
|
|
1768
|
+
if (qs) path = `${path}?${qs}`;
|
|
1769
|
+
return { path, urlKind: 'bulk-v1-async' };
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
return null;
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
function bulkV1BuildSchema(req, normalizedPath) {
|
|
1776
|
+
const base = bulkBuildSchema(req, normalizedPath);
|
|
1777
|
+
const pathNoQuery = normalizedPath.split('?')[0];
|
|
1778
|
+
const method = String(req.method || 'GET').toUpperCase();
|
|
1779
|
+
const isV1BatchPost = method === 'POST' && /\/job\/\{jobId\}\/batch$/.test(pathNoQuery);
|
|
1780
|
+
const isV1SpecPost = method === 'POST' && /\/job\/\{jobId\}\/spec$/.test(pathNoQuery);
|
|
1781
|
+
if (isV1BatchPost || isV1SpecPost) {
|
|
1782
|
+
base.properties.csv_data = {
|
|
1783
|
+
type: 'string',
|
|
1784
|
+
description: 'Request body as text/csv (UTF-8)',
|
|
1785
|
+
};
|
|
1786
|
+
base.required = [...new Set([...(base.required || []), 'csv_data'])];
|
|
1787
|
+
}
|
|
1788
|
+
return base;
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
function bulkV1ExtractRequests(items, varMap, acc) {
|
|
1792
|
+
for (const item of items || []) {
|
|
1793
|
+
if (Array.isArray(item.item)) {
|
|
1794
|
+
bulkV1ExtractRequests(item.item, varMap, acc);
|
|
1795
|
+
} else if (item.request) {
|
|
1796
|
+
const req = item.request;
|
|
1797
|
+
const method = String(req.method || 'GET').toUpperCase();
|
|
1798
|
+
if (['OPTIONS', 'HEAD', 'TRACE'].includes(method)) continue;
|
|
1799
|
+
|
|
1800
|
+
const parsed = bulkV1PathAndKind(req, varMap);
|
|
1801
|
+
if (!parsed) continue;
|
|
1802
|
+
|
|
1803
|
+
const { path, urlKind } = parsed;
|
|
1804
|
+
const pathKey = path.split('?')[0];
|
|
1805
|
+
const dedupeKey = `${method}|${pathKey}|${urlKind}`;
|
|
1806
|
+
|
|
1807
|
+
acc.push({
|
|
1808
|
+
dedupeKey,
|
|
1809
|
+
name: `bulk_v1_${bulkSlug(item.name)}`,
|
|
1810
|
+
description: bulkDesc(req, item.name),
|
|
1811
|
+
method,
|
|
1812
|
+
path,
|
|
1813
|
+
urlKind,
|
|
1814
|
+
inputSchema: bulkV1BuildSchema(req, path.split('?')[0]),
|
|
1815
|
+
});
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
function bulkV1ManifestEndpoints() {
|
|
1821
|
+
if (!existsSync(BULK_V1_POSTMAN)) return [];
|
|
1822
|
+
const col = JSON.parse(readFileSync(BULK_V1_POSTMAN, 'utf8'));
|
|
1823
|
+
const varMap = bulkVarMap(col);
|
|
1824
|
+
const raw = [];
|
|
1825
|
+
bulkV1ExtractRequests(col.item, varMap, raw);
|
|
1826
|
+
|
|
1827
|
+
const seen = new Set();
|
|
1828
|
+
const endpoints = [];
|
|
1829
|
+
for (const ep of raw) {
|
|
1830
|
+
if (seen.has(ep.dedupeKey)) continue;
|
|
1831
|
+
seen.add(ep.dedupeKey);
|
|
1832
|
+
const { dedupeKey: _d, ...rest } = ep;
|
|
1833
|
+
endpoints.push(rest);
|
|
1834
|
+
}
|
|
1835
|
+
return endpoints;
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
function bulkV1IsCsvQueryResultGet(method, pathNoQuery) {
|
|
1839
|
+
return method === 'GET' && /\/jobs\/query\/\{jobId\}\/results$/.test(pathNoQuery);
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
function bulkV1IsCsvAsyncResultGet(method, pathNoQuery) {
|
|
1843
|
+
return (
|
|
1844
|
+
method === 'GET' &&
|
|
1845
|
+
/\/job\/\{jobId\}\/batch\/\{batchId\}\/result(\/\{batchResultId\})?$/.test(pathNoQuery)
|
|
1846
|
+
);
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
function bulkV1IsCsvPost(method, pathNoQuery) {
|
|
1850
|
+
return (
|
|
1851
|
+
method === 'POST' &&
|
|
1852
|
+
(/\/job\/\{jobId\}\/batch$/.test(pathNoQuery) || /\/job\/\{jobId\}\/spec$/.test(pathNoQuery))
|
|
1853
|
+
);
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
function bulkV1GeneratedTools() {
|
|
1857
|
+
const endpoints = bulkV1ManifestEndpoints();
|
|
1858
|
+
const toolsOut = [];
|
|
1859
|
+
|
|
1860
|
+
for (const ep of endpoints) {
|
|
1861
|
+
const pathNoQuery = ep.path.split('?')[0];
|
|
1862
|
+
const isCsvPost = bulkV1IsCsvPost(ep.method, pathNoQuery);
|
|
1863
|
+
const isCsvGetQuery = bulkV1IsCsvQueryResultGet(ep.method, pathNoQuery);
|
|
1864
|
+
const isCsvGetAsync = bulkV1IsCsvAsyncResultGet(ep.method, pathNoQuery);
|
|
1865
|
+
|
|
1866
|
+
if (isCsvPost) {
|
|
1867
|
+
toolsOut.push({
|
|
1868
|
+
name: ep.name,
|
|
1869
|
+
description: ep.description,
|
|
1870
|
+
inputSchema: enrichSchemaForWrite(ep.inputSchema, ep.method),
|
|
1871
|
+
handler: async (params) => {
|
|
1872
|
+
const [pbase, pq] = ep.path.split('?');
|
|
1873
|
+
const { pathValues, rest } = splitPathAndRest(pbase, params);
|
|
1874
|
+
const csv = rest.csv_data;
|
|
1875
|
+
if (csv == null) throw new Error('csv_data is required');
|
|
1876
|
+
await ensureAccessToken();
|
|
1877
|
+
const url = getBulkV1AsyncBaseUrl() + applyPathTemplate(pbase, pathValues);
|
|
1878
|
+
const staticQ = pq ? Object.fromEntries(new URLSearchParams(pq)) : {};
|
|
1879
|
+
const restQuery = { ...rest };
|
|
1880
|
+
delete restQuery.csv_data;
|
|
1881
|
+
delete restQuery._rawBody;
|
|
1882
|
+
const headers = {
|
|
1883
|
+
...buildAuthHeaders(),
|
|
1884
|
+
'Content-Type': 'text/csv',
|
|
1885
|
+
Accept: 'application/json',
|
|
1886
|
+
};
|
|
1887
|
+
try {
|
|
1888
|
+
const res = await axios.post(url, csv, {
|
|
1889
|
+
headers,
|
|
1890
|
+
params: { ...staticQ, ...restQuery },
|
|
1891
|
+
timeout: 120000,
|
|
1892
|
+
validateStatus: () => true,
|
|
1893
|
+
});
|
|
1894
|
+
if (res.status < 200 || res.status >= 300) handleError({ response: res });
|
|
1895
|
+
const d = res.data;
|
|
1896
|
+
if (d == null || (typeof d === 'object' && !Object.keys(d).length)) {
|
|
1897
|
+
return text({ success: true, status: res.status });
|
|
1898
|
+
}
|
|
1899
|
+
return text(
|
|
1900
|
+
typeof d === 'string' ? { format: 'text', body: d } : d
|
|
1901
|
+
);
|
|
1902
|
+
} catch (e) {
|
|
1903
|
+
handleError(e);
|
|
1904
|
+
}
|
|
1905
|
+
},
|
|
1906
|
+
});
|
|
1907
|
+
continue;
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
if (isCsvGetQuery) {
|
|
1911
|
+
toolsOut.push({
|
|
1912
|
+
name: ep.name,
|
|
1913
|
+
description: ep.description,
|
|
1914
|
+
inputSchema: enrichSchemaForWrite(ep.inputSchema, ep.method),
|
|
1915
|
+
handler: async (params) => {
|
|
1916
|
+
const queryPath = ep.path.includes('?') ? ep.path.split('?')[1] : '';
|
|
1917
|
+
const staticQuery = queryPath ? Object.fromEntries(new URLSearchParams(queryPath)) : {};
|
|
1918
|
+
const basePath = ep.path.split('?')[0];
|
|
1919
|
+
const { pathValues, rest } = splitPathAndRest(basePath, params);
|
|
1920
|
+
await ensureAccessToken();
|
|
1921
|
+
const url = getBaseUrl() + applyPathTemplate(basePath, pathValues);
|
|
1922
|
+
const headers = { ...buildAuthHeaders(), Accept: 'text/csv' };
|
|
1923
|
+
const restQ = { ...rest };
|
|
1924
|
+
delete restQ._rawBody;
|
|
1925
|
+
try {
|
|
1926
|
+
const res = await axios.get(url, {
|
|
1927
|
+
headers,
|
|
1928
|
+
params: { ...staticQuery, ...restQ },
|
|
1929
|
+
responseType: 'text',
|
|
1930
|
+
timeout: 120000,
|
|
1931
|
+
validateStatus: () => true,
|
|
1932
|
+
});
|
|
1933
|
+
if (res.status < 200 || res.status >= 300) handleError({ response: res });
|
|
1934
|
+
return text({ format: 'text/csv', body: res.data ?? '' });
|
|
1935
|
+
} catch (e) {
|
|
1936
|
+
handleError(e);
|
|
1937
|
+
}
|
|
1938
|
+
},
|
|
1939
|
+
});
|
|
1940
|
+
continue;
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
if (isCsvGetAsync) {
|
|
1944
|
+
toolsOut.push({
|
|
1945
|
+
name: ep.name,
|
|
1946
|
+
description: ep.description,
|
|
1947
|
+
inputSchema: enrichSchemaForWrite(ep.inputSchema, ep.method),
|
|
1948
|
+
handler: async (params) => {
|
|
1949
|
+
const basePath = ep.path.split('?')[0];
|
|
1950
|
+
const { pathValues, rest } = splitPathAndRest(basePath, params);
|
|
1951
|
+
await ensureAccessToken();
|
|
1952
|
+
const url = getBulkV1AsyncBaseUrl() + applyPathTemplate(basePath, pathValues);
|
|
1953
|
+
const headers = { ...buildAuthHeaders(), Accept: 'text/csv' };
|
|
1954
|
+
const restQ = { ...rest };
|
|
1955
|
+
delete restQ._rawBody;
|
|
1956
|
+
try {
|
|
1957
|
+
const res = await axios.get(url, {
|
|
1958
|
+
headers,
|
|
1959
|
+
params: restQ,
|
|
1960
|
+
responseType: 'text',
|
|
1961
|
+
timeout: 120000,
|
|
1962
|
+
validateStatus: () => true,
|
|
1963
|
+
});
|
|
1964
|
+
if (res.status < 200 || res.status >= 300) handleError({ response: res });
|
|
1965
|
+
return text({ format: 'text/csv', body: res.data ?? '' });
|
|
1966
|
+
} catch (e) {
|
|
1967
|
+
handleError(e);
|
|
1968
|
+
}
|
|
1969
|
+
},
|
|
1970
|
+
});
|
|
1971
|
+
continue;
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
toolsOut.push({
|
|
1975
|
+
name: ep.name,
|
|
1976
|
+
description: ep.description,
|
|
1977
|
+
inputSchema: enrichSchemaForWrite(ep.inputSchema, ep.method),
|
|
1978
|
+
handler: async (params) => {
|
|
1979
|
+
const [pbase, pq] = ep.path.split('?');
|
|
1980
|
+
const merged = { ...params };
|
|
1981
|
+
if (pq) {
|
|
1982
|
+
for (const [k, v] of new URLSearchParams(pq)) {
|
|
1983
|
+
if (merged[k] === undefined) merged[k] = v;
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
const data = await sfRequest(ep.method, pbase, ep.urlKind, merged);
|
|
1987
|
+
return text(data);
|
|
1988
|
+
},
|
|
1989
|
+
});
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
toolsOut.sort((a, b) => {
|
|
1993
|
+
const ea = endpoints.find((e) => e.name === a.name);
|
|
1994
|
+
const eb = endpoints.find((e) => e.name === b.name);
|
|
1995
|
+
const ra = methodRank(ea?.method);
|
|
1996
|
+
const rb = methodRank(eb?.method);
|
|
1997
|
+
if (ra !== rb) return ra - rb;
|
|
1998
|
+
return a.name.localeCompare(b.name);
|
|
1999
|
+
});
|
|
2000
|
+
|
|
2001
|
+
return toolsOut;
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
/** Pathname e.g. /services/data/v59.0 for Composite sub-request URLs. */
|
|
2005
|
+
function compositeDataPath() {
|
|
2006
|
+
const base = getBaseUrl();
|
|
2007
|
+
try {
|
|
2008
|
+
const pathname = new URL(base).pathname.replace(/\/$/, '');
|
|
2009
|
+
if (!pathname.includes('/services/data/')) {
|
|
2010
|
+
throw new Error('expected /services/data/ in base URL');
|
|
2011
|
+
}
|
|
2012
|
+
return pathname;
|
|
2013
|
+
} catch (e) {
|
|
2014
|
+
throw new Error(`Composite API: invalid instance base URL (${e.message})`);
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
|
|
2018
|
+
function compositeBatchUrlSegment(urlIn) {
|
|
2019
|
+
const u = String(urlIn || '').trim();
|
|
2020
|
+
if (!u) throw new Error('Each batch request needs a non-empty url');
|
|
2021
|
+
if (/^v[0-9.]+\//i.test(u)) return u;
|
|
2022
|
+
if (u.startsWith('/services/data/')) {
|
|
2023
|
+
const m = u.match(/\/services\/data\/(v[0-9.]+)\/(.+)$/i);
|
|
2024
|
+
if (m) return `${m[1]}/${m[2]}`;
|
|
2025
|
+
}
|
|
2026
|
+
const verRaw = process.env.SALESFORCE_API_VERSION || 'v59.0';
|
|
2027
|
+
const v = verRaw.startsWith('v') ? verRaw : `v${verRaw}`;
|
|
2028
|
+
return `${v}/${u.replace(/^\//, '')}`;
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
function compositeSmartTools() {
|
|
2032
|
+
return [
|
|
2033
|
+
{
|
|
2034
|
+
name: 'composite_create_account_with_contact',
|
|
2035
|
+
description:
|
|
2036
|
+
'Creates an Account and a linked Contact in a single atomic API call using Composite. If account creation fails, contact is not created. Use instead of two separate create calls for new customer onboarding.',
|
|
2037
|
+
inputSchema: {
|
|
2038
|
+
type: 'object',
|
|
2039
|
+
properties: {
|
|
2040
|
+
account: {
|
|
2041
|
+
type: 'object',
|
|
2042
|
+
description: 'Account fields (e.g. Name, Industry, BillingCity)',
|
|
2043
|
+
additionalProperties: true,
|
|
2044
|
+
},
|
|
2045
|
+
contact: {
|
|
2046
|
+
type: 'object',
|
|
2047
|
+
description:
|
|
2048
|
+
'Contact fields (e.g. LastName, FirstName, Email); AccountId is set automatically from the new account',
|
|
2049
|
+
additionalProperties: true,
|
|
2050
|
+
},
|
|
2051
|
+
},
|
|
2052
|
+
required: ['account', 'contact'],
|
|
2053
|
+
},
|
|
2054
|
+
handler: async ({ account, contact }) => {
|
|
2055
|
+
const root = compositeDataPath();
|
|
2056
|
+
const payload = {
|
|
2057
|
+
compositeRequest: [
|
|
2058
|
+
{
|
|
2059
|
+
method: 'POST',
|
|
2060
|
+
url: `${root}/sobjects/Account`,
|
|
2061
|
+
referenceId: 'refAccount',
|
|
2062
|
+
body: { ...account },
|
|
2063
|
+
},
|
|
2064
|
+
{
|
|
2065
|
+
method: 'POST',
|
|
2066
|
+
url: `${root}/sobjects/Contact`,
|
|
2067
|
+
referenceId: 'refContact',
|
|
2068
|
+
body: { ...contact, AccountId: '@{refAccount.id}' },
|
|
2069
|
+
},
|
|
2070
|
+
],
|
|
2071
|
+
};
|
|
2072
|
+
await ensureAccessToken();
|
|
2073
|
+
const headers = { ...buildAuthHeaders(), 'Content-Type': 'application/json', Accept: 'application/json' };
|
|
2074
|
+
try {
|
|
2075
|
+
const res = await axios.post(`${getBaseUrl()}/composite/`, payload, {
|
|
2076
|
+
headers,
|
|
2077
|
+
timeout: 120000,
|
|
2078
|
+
validateStatus: () => true,
|
|
2079
|
+
});
|
|
2080
|
+
if (res.status < 200 || res.status >= 300) handleError({ response: res });
|
|
2081
|
+
return text(res.data);
|
|
2082
|
+
} catch (e) {
|
|
2083
|
+
handleError(e);
|
|
2084
|
+
}
|
|
2085
|
+
},
|
|
2086
|
+
},
|
|
2087
|
+
{
|
|
2088
|
+
name: 'composite_create_opportunity_with_contact_role',
|
|
2089
|
+
description:
|
|
2090
|
+
'Creates an Opportunity and assigns a Contact Role in one call. Use when adding a new deal and you already know the key contact.',
|
|
2091
|
+
inputSchema: {
|
|
2092
|
+
type: 'object',
|
|
2093
|
+
properties: {
|
|
2094
|
+
opportunity: {
|
|
2095
|
+
type: 'object',
|
|
2096
|
+
description: 'Opportunity fields (e.g. Name, StageName, CloseDate, AccountId, Amount)',
|
|
2097
|
+
additionalProperties: true,
|
|
2098
|
+
},
|
|
2099
|
+
contact_id: { type: 'string', description: 'Contact Id for the opportunity contact role' },
|
|
2100
|
+
role: {
|
|
2101
|
+
type: 'string',
|
|
2102
|
+
description: 'Role name (e.g. Decision Maker). Default: Primary Contact',
|
|
2103
|
+
},
|
|
2104
|
+
is_primary: { type: 'boolean', description: 'Optional IsPrimary on OpportunityContactRole' },
|
|
2105
|
+
},
|
|
2106
|
+
required: ['opportunity', 'contact_id'],
|
|
2107
|
+
},
|
|
2108
|
+
handler: async ({ opportunity, contact_id, role, is_primary }) => {
|
|
2109
|
+
const root = compositeDataPath();
|
|
2110
|
+
const ocr = {
|
|
2111
|
+
OpportunityId: '@{refOpp.id}',
|
|
2112
|
+
ContactId: contact_id,
|
|
2113
|
+
Role: role || 'Primary Contact',
|
|
2114
|
+
};
|
|
2115
|
+
if (is_primary === true) ocr.IsPrimary = true;
|
|
2116
|
+
|
|
2117
|
+
const payload = {
|
|
2118
|
+
compositeRequest: [
|
|
2119
|
+
{
|
|
2120
|
+
method: 'POST',
|
|
2121
|
+
url: `${root}/sobjects/Opportunity`,
|
|
2122
|
+
referenceId: 'refOpp',
|
|
2123
|
+
body: { ...opportunity },
|
|
2124
|
+
},
|
|
2125
|
+
{
|
|
2126
|
+
method: 'POST',
|
|
2127
|
+
url: `${root}/sobjects/OpportunityContactRole`,
|
|
2128
|
+
referenceId: 'refOcr',
|
|
2129
|
+
body: ocr,
|
|
2130
|
+
},
|
|
2131
|
+
],
|
|
2132
|
+
};
|
|
2133
|
+
await ensureAccessToken();
|
|
2134
|
+
const headers = { ...buildAuthHeaders(), 'Content-Type': 'application/json', Accept: 'application/json' };
|
|
2135
|
+
try {
|
|
2136
|
+
const res = await axios.post(`${getBaseUrl()}/composite/`, payload, {
|
|
2137
|
+
headers,
|
|
2138
|
+
timeout: 120000,
|
|
2139
|
+
validateStatus: () => true,
|
|
2140
|
+
});
|
|
2141
|
+
if (res.status < 200 || res.status >= 300) handleError({ response: res });
|
|
2142
|
+
return text(res.data);
|
|
2143
|
+
} catch (e) {
|
|
2144
|
+
handleError(e);
|
|
2145
|
+
}
|
|
2146
|
+
},
|
|
2147
|
+
},
|
|
2148
|
+
{
|
|
2149
|
+
name: 'batch_get_records',
|
|
2150
|
+
description:
|
|
2151
|
+
'Retrieves multiple records of different types in a single API call using Composite Batch. More efficient than sequential GET requests.',
|
|
2152
|
+
inputSchema: {
|
|
2153
|
+
type: 'object',
|
|
2154
|
+
properties: {
|
|
2155
|
+
requests: {
|
|
2156
|
+
type: 'array',
|
|
2157
|
+
description:
|
|
2158
|
+
'Each entry: { method (default GET), url } — url is the path after /services/data/, e.g. v59.0/sobjects/Account/001xxx or v59.0/query/?q=SELECT+Id+FROM+Contact+LIMIT+1',
|
|
2159
|
+
items: {
|
|
2160
|
+
type: 'object',
|
|
2161
|
+
properties: {
|
|
2162
|
+
method: { type: 'string' },
|
|
2163
|
+
url: { type: 'string' },
|
|
2164
|
+
},
|
|
2165
|
+
required: ['url'],
|
|
2166
|
+
},
|
|
2167
|
+
},
|
|
2168
|
+
halt_on_error: {
|
|
2169
|
+
type: 'boolean',
|
|
2170
|
+
description: 'Maps to haltOnError (default true)',
|
|
2171
|
+
},
|
|
2172
|
+
},
|
|
2173
|
+
required: ['requests'],
|
|
2174
|
+
},
|
|
2175
|
+
handler: async ({ requests, halt_on_error }) => {
|
|
2176
|
+
if (!Array.isArray(requests) || !requests.length) {
|
|
2177
|
+
throw new Error('requests must be a non-empty array');
|
|
2178
|
+
}
|
|
2179
|
+
const batchRequests = requests.map((r) => ({
|
|
2180
|
+
method: (r.method || 'GET').toUpperCase(),
|
|
2181
|
+
url: compositeBatchUrlSegment(r.url),
|
|
2182
|
+
}));
|
|
2183
|
+
const payload = {
|
|
2184
|
+
haltOnError: halt_on_error !== false,
|
|
2185
|
+
batchRequests,
|
|
2186
|
+
};
|
|
2187
|
+
await ensureAccessToken();
|
|
2188
|
+
const headers = { ...buildAuthHeaders(), 'Content-Type': 'application/json', Accept: 'application/json' };
|
|
2189
|
+
try {
|
|
2190
|
+
const res = await axios.post(`${getBaseUrl()}/composite/batch`, payload, {
|
|
2191
|
+
headers,
|
|
2192
|
+
timeout: 120000,
|
|
2193
|
+
validateStatus: () => true,
|
|
2194
|
+
});
|
|
2195
|
+
if (res.status < 200 || res.status >= 300) handleError({ response: res });
|
|
2196
|
+
return text(res.data);
|
|
2197
|
+
} catch (e) {
|
|
2198
|
+
handleError(e);
|
|
2199
|
+
}
|
|
2200
|
+
},
|
|
2201
|
+
},
|
|
2202
|
+
{
|
|
2203
|
+
name: 'mass_transfer_ownership',
|
|
2204
|
+
description:
|
|
2205
|
+
'Transfers ownership of multiple records to a new owner in one composite operation. Faster than individual updates.',
|
|
2206
|
+
inputSchema: {
|
|
2207
|
+
type: 'object',
|
|
2208
|
+
properties: {
|
|
2209
|
+
object_type: { type: 'string', description: 'SObject API name (same type for all ids)' },
|
|
2210
|
+
record_ids: { type: 'array', items: { type: 'string' } },
|
|
2211
|
+
new_owner_id: { type: 'string', description: 'User Id (005...) to assign as OwnerId' },
|
|
2212
|
+
all_or_none: { type: 'boolean', description: 'Default false' },
|
|
2213
|
+
},
|
|
2214
|
+
required: ['object_type', 'record_ids', 'new_owner_id'],
|
|
2215
|
+
},
|
|
2216
|
+
handler: async ({ object_type, record_ids, new_owner_id, all_or_none }) => {
|
|
2217
|
+
if (!record_ids?.length) throw new Error('record_ids must be non-empty');
|
|
2218
|
+
const records = record_ids.map((id) => ({
|
|
2219
|
+
attributes: { type: object_type },
|
|
2220
|
+
id: String(id).trim(),
|
|
2221
|
+
OwnerId: new_owner_id,
|
|
2222
|
+
}));
|
|
2223
|
+
const payload = { allOrNone: all_or_none === true, records };
|
|
2224
|
+
await ensureAccessToken();
|
|
2225
|
+
const headers = { ...buildAuthHeaders(), 'Content-Type': 'application/json', Accept: 'application/json' };
|
|
2226
|
+
try {
|
|
2227
|
+
const res = await axios.patch(`${getBaseUrl()}/composite/sobjects`, payload, {
|
|
2228
|
+
headers,
|
|
2229
|
+
timeout: 120000,
|
|
2230
|
+
validateStatus: () => true,
|
|
2231
|
+
});
|
|
2232
|
+
if (res.status < 200 || res.status >= 300) handleError({ response: res });
|
|
2233
|
+
return text(res.data);
|
|
2234
|
+
} catch (e) {
|
|
2235
|
+
handleError(e);
|
|
2236
|
+
}
|
|
2237
|
+
},
|
|
2238
|
+
},
|
|
2239
|
+
];
|
|
2240
|
+
}
|
|
2241
|
+
|
|
2242
|
+
function getSalesforceApiVersionNumber() {
|
|
2243
|
+
const verRaw = process.env.SALESFORCE_API_VERSION || 'v59.0';
|
|
2244
|
+
return String(verRaw).replace(/^v/i, '');
|
|
2245
|
+
}
|
|
2246
|
+
|
|
2247
|
+
function getSoapMetadataEndpointUrl() {
|
|
2248
|
+
const inst =
|
|
2249
|
+
process.env.SALESFORCE_INSTANCE_URL?.replace(/\/$/, '') || getInstanceUrl()?.replace(/\/$/, '');
|
|
2250
|
+
if (!inst) {
|
|
2251
|
+
throw new Error('SALESFORCE_INSTANCE_URL is not set.');
|
|
2252
|
+
}
|
|
2253
|
+
return `${inst}/services/Soap/m/${getSalesforceApiVersionNumber()}`;
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
function escapeXmlText(s) {
|
|
2257
|
+
return String(s)
|
|
2258
|
+
.replace(/&/g, '&')
|
|
2259
|
+
.replace(/</g, '<')
|
|
2260
|
+
.replace(/>/g, '>')
|
|
2261
|
+
.replace(/"/g, '"');
|
|
2262
|
+
}
|
|
2263
|
+
|
|
2264
|
+
function metadataWalkSoapRequests(items, out) {
|
|
2265
|
+
for (const item of items || []) {
|
|
2266
|
+
if (Array.isArray(item.item)) {
|
|
2267
|
+
metadataWalkSoapRequests(item.item, out);
|
|
2268
|
+
} else if (item.request) {
|
|
2269
|
+
const r = item.request;
|
|
2270
|
+
const hdrs = {};
|
|
2271
|
+
for (const h of r.header || []) {
|
|
2272
|
+
if (h.disabled || !h.key) continue;
|
|
2273
|
+
hdrs[h.key] = h.value || '';
|
|
2274
|
+
}
|
|
2275
|
+
out.push({
|
|
2276
|
+
itemName: item.name,
|
|
2277
|
+
body: r.body?.mode === 'raw' ? r.body.raw || '' : '',
|
|
2278
|
+
headers: hdrs,
|
|
2279
|
+
});
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
function loadMetadataPostmanSoapRequests() {
|
|
2285
|
+
if (!existsSync(METADATA_POSTMAN)) return [];
|
|
2286
|
+
const col = JSON.parse(readFileSync(METADATA_POSTMAN, 'utf8'));
|
|
2287
|
+
const out = [];
|
|
2288
|
+
metadataWalkSoapRequests(col.item, out);
|
|
2289
|
+
return out;
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
async function postMetadataSoapXml(xmlBody, headerTemplate) {
|
|
2293
|
+
await ensureAccessToken();
|
|
2294
|
+
const token = getToken();
|
|
2295
|
+
if (!token) {
|
|
2296
|
+
throw new Error('Not authenticated. Run: npx adoptai-salesforce-mcp --client cursor');
|
|
2297
|
+
}
|
|
2298
|
+
const xml = xmlBody.replace(/\{\{_accessToken\}\}/g, token);
|
|
2299
|
+
const url = getSoapMetadataEndpointUrl();
|
|
2300
|
+
const headers = {
|
|
2301
|
+
'Content-Type': 'text/xml; charset=UTF-8',
|
|
2302
|
+
Accept: 'text/xml',
|
|
2303
|
+
...headerTemplate,
|
|
2304
|
+
};
|
|
2305
|
+
try {
|
|
2306
|
+
const res = await axios.post(url, xml, {
|
|
2307
|
+
headers,
|
|
2308
|
+
timeout: 120000,
|
|
2309
|
+
validateStatus: () => true,
|
|
2310
|
+
});
|
|
2311
|
+
if (res.status < 200 || res.status >= 300) handleError({ response: res });
|
|
2312
|
+
const payload = typeof res.data === 'string' ? { format: 'text/xml', body: res.data } : res.data;
|
|
2313
|
+
return text(payload);
|
|
2314
|
+
} catch (e) {
|
|
2315
|
+
handleError(e);
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
function metadataSoapSlug(itemName) {
|
|
2320
|
+
const base = itemName
|
|
2321
|
+
.replace(/^SOAP\s+/i, '')
|
|
2322
|
+
.replace(/[^a-zA-Z0-9_]+/g, '_')
|
|
2323
|
+
.replace(/^_+|_+$/g, '')
|
|
2324
|
+
.toLowerCase();
|
|
2325
|
+
return `metadata_soap_${base}`;
|
|
2326
|
+
}
|
|
2327
|
+
|
|
2328
|
+
/** Section B: all Metadata API requests in the Postman split (SOAP to /services/Soap/m/). */
|
|
2329
|
+
function metadataSoapToolsFromPostman() {
|
|
2330
|
+
const reqs = loadMetadataPostmanSoapRequests();
|
|
2331
|
+
const tools = [];
|
|
2332
|
+
for (const req of reqs) {
|
|
2333
|
+
const slug = metadataSoapSlug(req.itemName);
|
|
2334
|
+
if (/describe\s*metadata/i.test(req.itemName)) {
|
|
2335
|
+
tools.push({
|
|
2336
|
+
name: slug,
|
|
2337
|
+
description:
|
|
2338
|
+
'Salesforce Metadata SOAP describeMetadata — metadata component types available for this org. From Postman collection salesforce-metadata.',
|
|
2339
|
+
inputSchema: {
|
|
2340
|
+
type: 'object',
|
|
2341
|
+
properties: {
|
|
2342
|
+
as_of_version: {
|
|
2343
|
+
type: 'string',
|
|
2344
|
+
description: 'asOfVersion e.g. 59.0 (defaults to SALESFORCE_API_VERSION without leading v)',
|
|
2345
|
+
},
|
|
2346
|
+
},
|
|
2347
|
+
},
|
|
2348
|
+
handler: async ({ as_of_version }) => {
|
|
2349
|
+
const ver = as_of_version || getSalesforceApiVersionNumber();
|
|
2350
|
+
let xml = req.body.replace(/\{\{version\}\}/g, ver);
|
|
2351
|
+
return postMetadataSoapXml(xml, req.headers);
|
|
2352
|
+
},
|
|
2353
|
+
});
|
|
2354
|
+
} else if (/describe\s*value\s*type/i.test(req.itemName)) {
|
|
2355
|
+
tools.push({
|
|
2356
|
+
name: slug,
|
|
2357
|
+
description:
|
|
2358
|
+
'Salesforce Metadata SOAP describeValueType — details for a metadata type QName. From Postman collection salesforce-metadata.',
|
|
2359
|
+
inputSchema: {
|
|
2360
|
+
type: 'object',
|
|
2361
|
+
properties: {
|
|
2362
|
+
metadata_type: {
|
|
2363
|
+
type: 'string',
|
|
2364
|
+
description: 'Type name e.g. CustomObject, Layout, ApexClass',
|
|
2365
|
+
},
|
|
2366
|
+
},
|
|
2367
|
+
required: ['metadata_type'],
|
|
2368
|
+
},
|
|
2369
|
+
handler: async ({ metadata_type }) => {
|
|
2370
|
+
let xml = req.body.replace(/\{\{version\}\}/g, getSalesforceApiVersionNumber());
|
|
2371
|
+
xml = xml.replace(/INSERT_METADATA_TYPE_NAME/g, metadata_type);
|
|
2372
|
+
return postMetadataSoapXml(xml, req.headers);
|
|
2373
|
+
},
|
|
2374
|
+
});
|
|
2375
|
+
} else if (/list\s*metadata/i.test(req.itemName)) {
|
|
2376
|
+
tools.push({
|
|
2377
|
+
name: slug,
|
|
2378
|
+
description:
|
|
2379
|
+
'Salesforce Metadata SOAP listMetadata — list components of a metadata type. From Postman collection salesforce-metadata.',
|
|
2380
|
+
inputSchema: {
|
|
2381
|
+
type: 'object',
|
|
2382
|
+
properties: {
|
|
2383
|
+
metadata_type: {
|
|
2384
|
+
type: 'string',
|
|
2385
|
+
description: 'e.g. CustomObject, ApexClass, Dashboard',
|
|
2386
|
+
},
|
|
2387
|
+
folder: { type: 'string', description: 'Folder name when the type uses folders' },
|
|
2388
|
+
as_of_version: { type: 'string' },
|
|
2389
|
+
},
|
|
2390
|
+
required: ['metadata_type'],
|
|
2391
|
+
},
|
|
2392
|
+
handler: async ({ metadata_type, folder, as_of_version }) => {
|
|
2393
|
+
const ver = as_of_version || getSalesforceApiVersionNumber();
|
|
2394
|
+
let xml = req.body.replace(/\{\{version\}\}/g, ver);
|
|
2395
|
+
xml = xml.replace(/<type>[^<]*<\/type>/, `<type>${escapeXmlText(metadata_type)}</type>`);
|
|
2396
|
+
const folderXml = folder != null && folder !== '' ? escapeXmlText(folder) : '';
|
|
2397
|
+
xml = xml.replace(/<folder>[^<]*<\/folder>/, `<folder>${folderXml}</folder>`);
|
|
2398
|
+
return postMetadataSoapXml(xml, req.headers);
|
|
2399
|
+
},
|
|
2400
|
+
});
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2403
|
+
return tools;
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2406
|
+
function findDescribeField(describe, fieldApiName) {
|
|
2407
|
+
const want = String(fieldApiName || '').toLowerCase();
|
|
2408
|
+
const fields = describe?.fields || [];
|
|
2409
|
+
return fields.find((f) => (f.name || '').toLowerCase() === want);
|
|
2410
|
+
}
|
|
2411
|
+
|
|
2412
|
+
function extractPicklistEntries(field) {
|
|
2413
|
+
if (!field) return null;
|
|
2414
|
+
const out = [];
|
|
2415
|
+
if (Array.isArray(field.picklistValues)) {
|
|
2416
|
+
for (const p of field.picklistValues) {
|
|
2417
|
+
if (p.active !== false) {
|
|
2418
|
+
out.push({ label: p.label, value: p.value, defaultValue: p.defaultValue });
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
2421
|
+
return out.length ? out : null;
|
|
2422
|
+
}
|
|
2423
|
+
const vs = field.valueSet;
|
|
2424
|
+
if (vs?.valueSetDefinition?.value && Array.isArray(vs.valueSetDefinition.value)) {
|
|
2425
|
+
for (const v of vs.valueSetDefinition.value) {
|
|
2426
|
+
out.push({ label: v.label, value: v.value, default: v.default });
|
|
2427
|
+
}
|
|
2428
|
+
return out.length ? out : null;
|
|
2429
|
+
}
|
|
2430
|
+
if (vs?.controllingField) {
|
|
2431
|
+
return {
|
|
2432
|
+
message: 'Dependent picklist — use describe_sobject and controlling field context',
|
|
2433
|
+
controllingField: vs.controllingField,
|
|
2434
|
+
};
|
|
2435
|
+
}
|
|
2436
|
+
return null;
|
|
2437
|
+
}
|
|
2438
|
+
|
|
2439
|
+
function metadataRestSmartTools() {
|
|
2440
|
+
return [
|
|
2441
|
+
{
|
|
2442
|
+
name: 'list_sobjects',
|
|
2443
|
+
description:
|
|
2444
|
+
'Lists ALL available Salesforce objects (standard and custom). Use to discover what objects exist before querying or describing them.',
|
|
2445
|
+
inputSchema: { type: 'object', properties: {} },
|
|
2446
|
+
handler: async () => {
|
|
2447
|
+
await ensureAccessToken();
|
|
2448
|
+
const headers = { ...buildAuthHeaders(), Accept: 'application/json' };
|
|
2449
|
+
try {
|
|
2450
|
+
const res = await axios.get(`${getBaseUrl()}/sobjects/`, {
|
|
2451
|
+
headers,
|
|
2452
|
+
timeout: 120000,
|
|
2453
|
+
validateStatus: () => true,
|
|
2454
|
+
});
|
|
2455
|
+
if (res.status < 200 || res.status >= 300) handleError({ response: res });
|
|
2456
|
+
return text(res.data);
|
|
2457
|
+
} catch (e) {
|
|
2458
|
+
handleError(e);
|
|
2459
|
+
}
|
|
2460
|
+
},
|
|
2461
|
+
},
|
|
2462
|
+
{
|
|
2463
|
+
name: 'describe_sobject',
|
|
2464
|
+
description:
|
|
2465
|
+
'Returns full metadata for a Salesforce object: all field names, types, picklist values and relationships. Use before building SOQL to know what fields are available.',
|
|
2466
|
+
inputSchema: {
|
|
2467
|
+
type: 'object',
|
|
2468
|
+
properties: {
|
|
2469
|
+
object_type: {
|
|
2470
|
+
type: 'string',
|
|
2471
|
+
description: 'SObject API name e.g. Account, Custom__c',
|
|
2472
|
+
},
|
|
2473
|
+
},
|
|
2474
|
+
required: ['object_type'],
|
|
2475
|
+
},
|
|
2476
|
+
handler: async ({ object_type }) => {
|
|
2477
|
+
await ensureAccessToken();
|
|
2478
|
+
const headers = { ...buildAuthHeaders(), Accept: 'application/json' };
|
|
2479
|
+
try {
|
|
2480
|
+
const res = await axios.get(
|
|
2481
|
+
`${getBaseUrl()}/sobjects/${encodeURIComponent(object_type)}/describe`,
|
|
2482
|
+
{ headers, timeout: 120000, validateStatus: () => true }
|
|
2483
|
+
);
|
|
2484
|
+
if (res.status < 200 || res.status >= 300) handleError({ response: res });
|
|
2485
|
+
return text(res.data);
|
|
2486
|
+
} catch (e) {
|
|
2487
|
+
handleError(e);
|
|
2488
|
+
}
|
|
2489
|
+
},
|
|
2490
|
+
},
|
|
2491
|
+
{
|
|
2492
|
+
name: 'get_picklist_values',
|
|
2493
|
+
description:
|
|
2494
|
+
'Returns all valid picklist values for a specific field on a Salesforce object. Use before creating/updating records to know valid values for Stage, Status, Industry etc.',
|
|
2495
|
+
inputSchema: {
|
|
2496
|
+
type: 'object',
|
|
2497
|
+
properties: {
|
|
2498
|
+
object_type: { type: 'string' },
|
|
2499
|
+
field_name: { type: 'string', description: 'Field API name e.g. StageName, Industry' },
|
|
2500
|
+
},
|
|
2501
|
+
required: ['object_type', 'field_name'],
|
|
2502
|
+
},
|
|
2503
|
+
handler: async ({ object_type, field_name }) => {
|
|
2504
|
+
await ensureAccessToken();
|
|
2505
|
+
const headers = { ...buildAuthHeaders(), Accept: 'application/json' };
|
|
2506
|
+
let describe;
|
|
2507
|
+
try {
|
|
2508
|
+
const res = await axios.get(
|
|
2509
|
+
`${getBaseUrl()}/sobjects/${encodeURIComponent(object_type)}/describe`,
|
|
2510
|
+
{ headers, timeout: 120000, validateStatus: () => true }
|
|
2511
|
+
);
|
|
2512
|
+
if (res.status < 200 || res.status >= 300) handleError({ response: res });
|
|
2513
|
+
describe = res.data;
|
|
2514
|
+
} catch (e) {
|
|
2515
|
+
handleError(e);
|
|
2516
|
+
}
|
|
2517
|
+
const field = findDescribeField(describe, field_name);
|
|
2518
|
+
if (!field) {
|
|
2519
|
+
return text({ error: 'Field not found', object_type, field_name });
|
|
2520
|
+
}
|
|
2521
|
+
const values = extractPicklistEntries(field);
|
|
2522
|
+
return text({
|
|
2523
|
+
object_type,
|
|
2524
|
+
field_name: field.name,
|
|
2525
|
+
label: field.label,
|
|
2526
|
+
type: field.type,
|
|
2527
|
+
values,
|
|
2528
|
+
});
|
|
2529
|
+
},
|
|
2530
|
+
},
|
|
2531
|
+
{
|
|
2532
|
+
name: 'get_org_limits',
|
|
2533
|
+
description:
|
|
2534
|
+
'Returns current API usage and remaining limits for the org. Includes daily API calls used/remaining. Use to monitor integration health.',
|
|
2535
|
+
inputSchema: { type: 'object', properties: {} },
|
|
2536
|
+
handler: async () => {
|
|
2537
|
+
await ensureAccessToken();
|
|
2538
|
+
const headers = { ...buildAuthHeaders(), Accept: 'application/json' };
|
|
2539
|
+
try {
|
|
2540
|
+
const res = await axios.get(`${getBaseUrl()}/limits`, {
|
|
2541
|
+
headers,
|
|
2542
|
+
timeout: 60000,
|
|
2543
|
+
validateStatus: () => true,
|
|
2544
|
+
});
|
|
2545
|
+
if (res.status < 200 || res.status >= 300) handleError({ response: res });
|
|
2546
|
+
return text(res.data);
|
|
2547
|
+
} catch (e) {
|
|
2548
|
+
handleError(e);
|
|
2549
|
+
}
|
|
2550
|
+
},
|
|
2551
|
+
},
|
|
2552
|
+
{
|
|
2553
|
+
name: 'get_record_types',
|
|
2554
|
+
description:
|
|
2555
|
+
'Returns all record types for a Salesforce object. Required before creating records in orgs that use record types.',
|
|
2556
|
+
inputSchema: {
|
|
2557
|
+
type: 'object',
|
|
2558
|
+
properties: {
|
|
2559
|
+
object_type: { type: 'string' },
|
|
2560
|
+
},
|
|
2561
|
+
required: ['object_type'],
|
|
2562
|
+
},
|
|
2563
|
+
handler: async ({ object_type }) => {
|
|
2564
|
+
await ensureAccessToken();
|
|
2565
|
+
const headers = { ...buildAuthHeaders(), Accept: 'application/json' };
|
|
2566
|
+
let describe;
|
|
2567
|
+
try {
|
|
2568
|
+
const res = await axios.get(
|
|
2569
|
+
`${getBaseUrl()}/sobjects/${encodeURIComponent(object_type)}/describe`,
|
|
2570
|
+
{ headers, timeout: 120000, validateStatus: () => true }
|
|
2571
|
+
);
|
|
2572
|
+
if (res.status < 200 || res.status >= 300) handleError({ response: res });
|
|
2573
|
+
describe = res.data;
|
|
2574
|
+
} catch (e) {
|
|
2575
|
+
handleError(e);
|
|
2576
|
+
}
|
|
2577
|
+
const infos = describe?.recordTypeInfos || [];
|
|
2578
|
+
return text({
|
|
2579
|
+
object_type,
|
|
2580
|
+
recordTypeInfos: infos,
|
|
2581
|
+
count: infos.length,
|
|
2582
|
+
});
|
|
2583
|
+
},
|
|
2584
|
+
},
|
|
2585
|
+
];
|
|
2586
|
+
}
|
|
2587
|
+
|
|
2588
|
+
function multipliedTools() {
|
|
2589
|
+
return [
|
|
2590
|
+
{
|
|
2591
|
+
name: 'get_account',
|
|
2592
|
+
description:
|
|
2593
|
+
'Retrieves basic Salesforce account details by ID including name, industry, type, phone, website and billing address. Use when you need account metadata. For related records use get_account_with_contacts or get_account_with_opportunities.',
|
|
2594
|
+
inputSchema: {
|
|
2595
|
+
type: 'object',
|
|
2596
|
+
properties: {
|
|
2597
|
+
account_id: { type: 'string', description: 'Salesforce Account Id' },
|
|
2598
|
+
fields: {
|
|
2599
|
+
type: 'string',
|
|
2600
|
+
description: 'Optional comma-separated API field names (e.g. Name,Industry,BillingCity)',
|
|
2601
|
+
},
|
|
2602
|
+
},
|
|
2603
|
+
required: ['account_id'],
|
|
2604
|
+
},
|
|
2605
|
+
handler: async ({ account_id, fields }) => {
|
|
2606
|
+
const q = fields
|
|
2607
|
+
? `?fields=${encodeURIComponent(fields)}`
|
|
2608
|
+
: '';
|
|
2609
|
+
await ensureAccessToken();
|
|
2610
|
+
const headers = { ...buildAuthHeaders(), Accept: 'application/json' };
|
|
2611
|
+
try {
|
|
2612
|
+
const res = await axios.get(`${getBaseUrl()}/sobjects/Account/${encodeURIComponent(account_id)}${q}`, {
|
|
2613
|
+
headers,
|
|
2614
|
+
timeout: 60000,
|
|
2615
|
+
});
|
|
2616
|
+
return text(res.data);
|
|
2617
|
+
} catch (e) {
|
|
2618
|
+
handleError(e);
|
|
2619
|
+
}
|
|
2620
|
+
},
|
|
2621
|
+
},
|
|
2622
|
+
{
|
|
2623
|
+
name: 'get_account_with_contacts',
|
|
2624
|
+
description:
|
|
2625
|
+
'Retrieves an account AND all its related contacts in one SOQL relationship query. Use when you need both account details and contact list without making two separate API calls.',
|
|
2626
|
+
inputSchema: {
|
|
2627
|
+
type: 'object',
|
|
2628
|
+
properties: {
|
|
2629
|
+
account_id: { type: 'string', description: 'Salesforce Account Id' },
|
|
2630
|
+
},
|
|
2631
|
+
required: ['account_id'],
|
|
2632
|
+
},
|
|
2633
|
+
handler: async ({ account_id }) => {
|
|
2634
|
+
const id = escapeSoqlString(account_id);
|
|
2635
|
+
const q = `SELECT Id,Name,Industry,Type,Phone,Website,BillingStreet,BillingCity,BillingState,BillingPostalCode,BillingCountry,(SELECT Id,FirstName,LastName,Email,Phone FROM Contacts) FROM Account WHERE Id='${id}'`;
|
|
2636
|
+
return text(await soqlGet(q));
|
|
2637
|
+
},
|
|
2638
|
+
},
|
|
2639
|
+
{
|
|
2640
|
+
name: 'get_account_with_opportunities',
|
|
2641
|
+
description:
|
|
2642
|
+
'Retrieves an account AND all associated opportunities with stage, amount and close date. Use for account pipeline review in one call.',
|
|
2643
|
+
inputSchema: {
|
|
2644
|
+
type: 'object',
|
|
2645
|
+
properties: {
|
|
2646
|
+
account_id: { type: 'string' },
|
|
2647
|
+
},
|
|
2648
|
+
required: ['account_id'],
|
|
2649
|
+
},
|
|
2650
|
+
handler: async ({ account_id }) => {
|
|
2651
|
+
const id = escapeSoqlString(account_id);
|
|
2652
|
+
const q = `SELECT Id,Name,Industry,(SELECT Id,Name,StageName,Amount,CloseDate,Probability FROM Opportunities) FROM Account WHERE Id='${id}'`;
|
|
2653
|
+
return text(await soqlGet(q));
|
|
2654
|
+
},
|
|
2655
|
+
},
|
|
2656
|
+
{
|
|
2657
|
+
name: 'get_account_with_cases',
|
|
2658
|
+
description:
|
|
2659
|
+
'Retrieves an account AND all open support cases. Use for customer health checks and service review.',
|
|
2660
|
+
inputSchema: {
|
|
2661
|
+
type: 'object',
|
|
2662
|
+
properties: {
|
|
2663
|
+
account_id: { type: 'string' },
|
|
2664
|
+
},
|
|
2665
|
+
required: ['account_id'],
|
|
2666
|
+
},
|
|
2667
|
+
handler: async ({ account_id }) => {
|
|
2668
|
+
const id = escapeSoqlString(account_id);
|
|
2669
|
+
const q = `SELECT Id,Name,(SELECT Id,CaseNumber,Subject,Status,Priority,CreatedDate FROM Cases WHERE IsClosed=false) FROM Account WHERE Id='${id}'`;
|
|
2670
|
+
return text(await soqlGet(q));
|
|
2671
|
+
},
|
|
2672
|
+
},
|
|
2673
|
+
{
|
|
2674
|
+
name: 'get_account_full_profile',
|
|
2675
|
+
description:
|
|
2676
|
+
'Retrieves a complete 360-degree account profile including contacts, opportunities and cases in a single call. Use when you need everything about an account at once, such as before a customer meeting.',
|
|
2677
|
+
inputSchema: {
|
|
2678
|
+
type: 'object',
|
|
2679
|
+
properties: {
|
|
2680
|
+
account_id: { type: 'string' },
|
|
2681
|
+
},
|
|
2682
|
+
required: ['account_id'],
|
|
2683
|
+
},
|
|
2684
|
+
handler: async ({ account_id }) => {
|
|
2685
|
+
const id = escapeSoqlString(account_id);
|
|
2686
|
+
const q = `SELECT Id,Name,Industry,Type,Phone,Website,BillingCity,BillingCountry,(SELECT Id,FirstName,LastName,Email,Phone FROM Contacts),(SELECT Id,Name,StageName,Amount,CloseDate FROM Opportunities),(SELECT Id,CaseNumber,Subject,Status FROM Cases WHERE IsClosed=false) FROM Account WHERE Id='${id}'`;
|
|
2687
|
+
return text(await soqlGet(q));
|
|
2688
|
+
},
|
|
2689
|
+
},
|
|
2690
|
+
{
|
|
2691
|
+
name: 'update_opportunity',
|
|
2692
|
+
description:
|
|
2693
|
+
'Generic opportunity update — pass any fields to update. Use for multi-field updates. For focused single-field updates use the specific tools like update_opportunity_stage.',
|
|
2694
|
+
inputSchema: {
|
|
2695
|
+
type: 'object',
|
|
2696
|
+
properties: {
|
|
2697
|
+
opportunity_id: { type: 'string' },
|
|
2698
|
+
fields: {
|
|
2699
|
+
type: 'object',
|
|
2700
|
+
description: 'JSON object of Opportunity fields to PATCH (e.g. StageName, Amount, CloseDate)',
|
|
2701
|
+
additionalProperties: true,
|
|
2702
|
+
},
|
|
2703
|
+
},
|
|
2704
|
+
required: ['opportunity_id', 'fields'],
|
|
2705
|
+
},
|
|
2706
|
+
handler: async ({ opportunity_id, fields }) => {
|
|
2707
|
+
await ensureAccessToken();
|
|
2708
|
+
const headers = { ...buildAuthHeaders(), 'Content-Type': 'application/json' };
|
|
2709
|
+
try {
|
|
2710
|
+
const res = await axios.patch(
|
|
2711
|
+
`${getBaseUrl()}/sobjects/Opportunity/${encodeURIComponent(opportunity_id)}`,
|
|
2712
|
+
fields,
|
|
2713
|
+
{ headers, timeout: 60000, validateStatus: () => true }
|
|
2714
|
+
);
|
|
2715
|
+
if (res.status < 200 || res.status >= 300) handleError({ response: res });
|
|
2716
|
+
return text(res.data && Object.keys(res.data).length ? res.data : { success: true });
|
|
2717
|
+
} catch (e) {
|
|
2718
|
+
handleError(e);
|
|
2719
|
+
}
|
|
2720
|
+
},
|
|
2721
|
+
},
|
|
2722
|
+
{
|
|
2723
|
+
name: 'update_opportunity_stage',
|
|
2724
|
+
description:
|
|
2725
|
+
'Updates an opportunity stage (e.g. Prospecting → Proposal → Negotiation). The most common opportunity update in sales workflows. Use instead of update_opportunity when only changing stage.',
|
|
2726
|
+
inputSchema: {
|
|
2727
|
+
type: 'object',
|
|
2728
|
+
properties: {
|
|
2729
|
+
opportunity_id: { type: 'string' },
|
|
2730
|
+
stage: { type: 'string', description: 'StageName value' },
|
|
2731
|
+
close_date: { type: 'string', description: 'Optional CloseDate (YYYY-MM-DD)' },
|
|
2732
|
+
probability: { type: 'number', description: 'Optional Probability percent' },
|
|
2733
|
+
},
|
|
2734
|
+
required: ['opportunity_id', 'stage'],
|
|
2735
|
+
},
|
|
2736
|
+
handler: async ({ opportunity_id, stage, close_date, probability }) => {
|
|
2737
|
+
const body = { StageName: stage };
|
|
2738
|
+
if (close_date != null) body.CloseDate = close_date;
|
|
2739
|
+
if (probability != null) body.Probability = probability;
|
|
2740
|
+
await ensureAccessToken();
|
|
2741
|
+
const headers = { ...buildAuthHeaders(), 'Content-Type': 'application/json' };
|
|
2742
|
+
try {
|
|
2743
|
+
const res = await axios.patch(
|
|
2744
|
+
`${getBaseUrl()}/sobjects/Opportunity/${encodeURIComponent(opportunity_id)}`,
|
|
2745
|
+
body,
|
|
2746
|
+
{ headers, timeout: 60000, validateStatus: () => true }
|
|
2747
|
+
);
|
|
2748
|
+
if (res.status < 200 || res.status >= 300) handleError({ response: res });
|
|
2749
|
+
return text({ success: true });
|
|
2750
|
+
} catch (e) {
|
|
2751
|
+
handleError(e);
|
|
2752
|
+
}
|
|
2753
|
+
},
|
|
2754
|
+
},
|
|
2755
|
+
{
|
|
2756
|
+
name: 'update_opportunity_amount',
|
|
2757
|
+
description:
|
|
2758
|
+
'Updates the monetary value of an opportunity. Use when deal size changes during negotiation.',
|
|
2759
|
+
inputSchema: {
|
|
2760
|
+
type: 'object',
|
|
2761
|
+
properties: {
|
|
2762
|
+
opportunity_id: { type: 'string' },
|
|
2763
|
+
amount: { type: 'number' },
|
|
2764
|
+
},
|
|
2765
|
+
required: ['opportunity_id', 'amount'],
|
|
2766
|
+
},
|
|
2767
|
+
handler: async ({ opportunity_id, amount }) => {
|
|
2768
|
+
await ensureAccessToken();
|
|
2769
|
+
const headers = { ...buildAuthHeaders(), 'Content-Type': 'application/json' };
|
|
2770
|
+
try {
|
|
2771
|
+
const res = await axios.patch(
|
|
2772
|
+
`${getBaseUrl()}/sobjects/Opportunity/${encodeURIComponent(opportunity_id)}`,
|
|
2773
|
+
{ Amount: amount },
|
|
2774
|
+
{ headers, timeout: 60000, validateStatus: () => true }
|
|
2775
|
+
);
|
|
2776
|
+
if (res.status < 200 || res.status >= 300) handleError({ response: res });
|
|
2777
|
+
return text({ success: true });
|
|
2778
|
+
} catch (e) {
|
|
2779
|
+
handleError(e);
|
|
2780
|
+
}
|
|
2781
|
+
},
|
|
2782
|
+
},
|
|
2783
|
+
{
|
|
2784
|
+
name: 'update_opportunity_close_date',
|
|
2785
|
+
description:
|
|
2786
|
+
'Updates the expected close date of an opportunity. Use when a deal slips or accelerates without other field changes.',
|
|
2787
|
+
inputSchema: {
|
|
2788
|
+
type: 'object',
|
|
2789
|
+
properties: {
|
|
2790
|
+
opportunity_id: { type: 'string' },
|
|
2791
|
+
close_date: { type: 'string', description: 'CloseDate (YYYY-MM-DD)' },
|
|
2792
|
+
},
|
|
2793
|
+
required: ['opportunity_id', 'close_date'],
|
|
2794
|
+
},
|
|
2795
|
+
handler: async ({ opportunity_id, close_date }) => {
|
|
2796
|
+
await ensureAccessToken();
|
|
2797
|
+
const headers = { ...buildAuthHeaders(), 'Content-Type': 'application/json' };
|
|
2798
|
+
try {
|
|
2799
|
+
const res = await axios.patch(
|
|
2800
|
+
`${getBaseUrl()}/sobjects/Opportunity/${encodeURIComponent(opportunity_id)}`,
|
|
2801
|
+
{ CloseDate: close_date },
|
|
2802
|
+
{ headers, timeout: 60000, validateStatus: () => true }
|
|
2803
|
+
);
|
|
2804
|
+
if (res.status < 200 || res.status >= 300) handleError({ response: res });
|
|
2805
|
+
return text({ success: true });
|
|
2806
|
+
} catch (e) {
|
|
2807
|
+
handleError(e);
|
|
2808
|
+
}
|
|
2809
|
+
},
|
|
2810
|
+
},
|
|
2811
|
+
{
|
|
2812
|
+
name: 'close_won_opportunity',
|
|
2813
|
+
description:
|
|
2814
|
+
"Marks an opportunity as Closed Won with today's date or a specified close date. Sets probability to 100 automatically. Use when a deal is confirmed won — faster than update_opportunity.",
|
|
2815
|
+
inputSchema: {
|
|
2816
|
+
type: 'object',
|
|
2817
|
+
properties: {
|
|
2818
|
+
opportunity_id: { type: 'string' },
|
|
2819
|
+
close_date: {
|
|
2820
|
+
type: 'string',
|
|
2821
|
+
description: 'Optional YYYY-MM-DD; defaults to today (UTC date)',
|
|
2822
|
+
},
|
|
2823
|
+
},
|
|
2824
|
+
required: ['opportunity_id'],
|
|
2825
|
+
},
|
|
2826
|
+
handler: async ({ opportunity_id, close_date }) => {
|
|
2827
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
2828
|
+
const body = {
|
|
2829
|
+
StageName: 'Closed Won',
|
|
2830
|
+
Probability: 100,
|
|
2831
|
+
CloseDate: close_date || today,
|
|
2832
|
+
};
|
|
2833
|
+
await ensureAccessToken();
|
|
2834
|
+
const headers = { ...buildAuthHeaders(), 'Content-Type': 'application/json' };
|
|
2835
|
+
try {
|
|
2836
|
+
const res = await axios.patch(
|
|
2837
|
+
`${getBaseUrl()}/sobjects/Opportunity/${encodeURIComponent(opportunity_id)}`,
|
|
2838
|
+
body,
|
|
2839
|
+
{ headers, timeout: 60000, validateStatus: () => true }
|
|
2840
|
+
);
|
|
2841
|
+
if (res.status < 200 || res.status >= 300) handleError({ response: res });
|
|
2842
|
+
return text({ success: true });
|
|
2843
|
+
} catch (e) {
|
|
2844
|
+
handleError(e);
|
|
2845
|
+
}
|
|
2846
|
+
},
|
|
2847
|
+
},
|
|
2848
|
+
{
|
|
2849
|
+
name: 'close_lost_opportunity',
|
|
2850
|
+
description:
|
|
2851
|
+
'Marks an opportunity as Closed Lost and optionally records the loss reason. Sets probability to 0 automatically. Use when a deal is definitively lost.',
|
|
2852
|
+
inputSchema: {
|
|
2853
|
+
type: 'object',
|
|
2854
|
+
properties: {
|
|
2855
|
+
opportunity_id: { type: 'string' },
|
|
2856
|
+
loss_reason: { type: 'string', description: 'Optional custom loss reason field if configured' },
|
|
2857
|
+
},
|
|
2858
|
+
required: ['opportunity_id'],
|
|
2859
|
+
},
|
|
2860
|
+
handler: async ({ opportunity_id, loss_reason }) => {
|
|
2861
|
+
const body = { StageName: 'Closed Lost', Probability: 0 };
|
|
2862
|
+
if (loss_reason) {
|
|
2863
|
+
body.Description = loss_reason;
|
|
2864
|
+
}
|
|
2865
|
+
await ensureAccessToken();
|
|
2866
|
+
const headers = { ...buildAuthHeaders(), 'Content-Type': 'application/json' };
|
|
2867
|
+
try {
|
|
2868
|
+
const res = await axios.patch(
|
|
2869
|
+
`${getBaseUrl()}/sobjects/Opportunity/${encodeURIComponent(opportunity_id)}`,
|
|
2870
|
+
body,
|
|
2871
|
+
{ headers, timeout: 60000, validateStatus: () => true }
|
|
2872
|
+
);
|
|
2873
|
+
if (res.status < 200 || res.status >= 300) handleError({ response: res });
|
|
2874
|
+
return text({ success: true });
|
|
2875
|
+
} catch (e) {
|
|
2876
|
+
handleError(e);
|
|
2877
|
+
}
|
|
2878
|
+
},
|
|
2879
|
+
},
|
|
2880
|
+
{
|
|
2881
|
+
name: 'run_soql_query',
|
|
2882
|
+
description:
|
|
2883
|
+
'Executes any raw SOQL query. The most flexible tool for power users who know SOQL. Use when no specific tool covers your need. For common lookups prefer the specific search tools.',
|
|
2884
|
+
inputSchema: {
|
|
2885
|
+
type: 'object',
|
|
2886
|
+
properties: {
|
|
2887
|
+
query: { type: 'string', description: 'Raw SOQL string' },
|
|
2888
|
+
limit: {
|
|
2889
|
+
type: 'number',
|
|
2890
|
+
description:
|
|
2891
|
+
'If the query has no LIMIT clause, append this LIMIT (optional). Leave unset if your query already includes LIMIT.',
|
|
2892
|
+
},
|
|
2893
|
+
},
|
|
2894
|
+
required: ['query'],
|
|
2895
|
+
},
|
|
2896
|
+
handler: async ({ query, limit }) => {
|
|
2897
|
+
let q = query.trim();
|
|
2898
|
+
if (limit != null && !/\blimit\s+\d+/i.test(q)) {
|
|
2899
|
+
q = `${q} LIMIT ${Number(limit)}`;
|
|
2900
|
+
}
|
|
2901
|
+
return text(await soqlGet(q));
|
|
2902
|
+
},
|
|
2903
|
+
},
|
|
2904
|
+
{
|
|
2905
|
+
name: 'run_soql_query_all_pages',
|
|
2906
|
+
description:
|
|
2907
|
+
'Runs a SOQL query and automatically paginates through ALL results. Use when you need complete datasets over 2000 records.',
|
|
2908
|
+
inputSchema: {
|
|
2909
|
+
type: 'object',
|
|
2910
|
+
properties: {
|
|
2911
|
+
query: { type: 'string', description: 'SOQL without automatic LIMIT' },
|
|
2912
|
+
max_records: { type: 'number', description: 'Safety cap (default 10000)' },
|
|
2913
|
+
},
|
|
2914
|
+
required: ['query'],
|
|
2915
|
+
},
|
|
2916
|
+
handler: async ({ query, max_records }) => {
|
|
2917
|
+
const cap = max_records != null ? Number(max_records) : 10000;
|
|
2918
|
+
return text(await fetchQueryAllPages(query.trim(), cap));
|
|
2919
|
+
},
|
|
2920
|
+
},
|
|
2921
|
+
{
|
|
2922
|
+
name: 'search_accounts',
|
|
2923
|
+
description:
|
|
2924
|
+
'Searches accounts by name, industry, type or location without writing SOQL. Use to find account IDs before get_account.',
|
|
2925
|
+
inputSchema: {
|
|
2926
|
+
type: 'object',
|
|
2927
|
+
properties: {
|
|
2928
|
+
name: { type: 'string' },
|
|
2929
|
+
industry: { type: 'string' },
|
|
2930
|
+
type: { type: 'string' },
|
|
2931
|
+
city: { type: 'string', description: 'BillingCity' },
|
|
2932
|
+
limit: { type: 'number', description: 'Default 20' },
|
|
2933
|
+
},
|
|
2934
|
+
required: [],
|
|
2935
|
+
},
|
|
2936
|
+
handler: async (p) => {
|
|
2937
|
+
const lim = p.limit != null ? Number(p.limit) : 20;
|
|
2938
|
+
const where = buildSearchWhere({
|
|
2939
|
+
Name: p.name,
|
|
2940
|
+
Industry: p.industry,
|
|
2941
|
+
Type: p.type,
|
|
2942
|
+
BillingCity: p.city,
|
|
2943
|
+
});
|
|
2944
|
+
if (!where) {
|
|
2945
|
+
throw new Error('Provide at least one of name, industry, type, or city');
|
|
2946
|
+
}
|
|
2947
|
+
const q = `SELECT Id,Name,Industry,Type,BillingCity FROM Account WHERE ${where} LIMIT ${lim}`;
|
|
2948
|
+
return text(await soqlGet(q));
|
|
2949
|
+
},
|
|
2950
|
+
},
|
|
2951
|
+
{
|
|
2952
|
+
name: 'search_contacts',
|
|
2953
|
+
description:
|
|
2954
|
+
'Searches contacts by name, email, phone or account without writing SOQL. Use to find contact IDs before direct reads.',
|
|
2955
|
+
inputSchema: {
|
|
2956
|
+
type: 'object',
|
|
2957
|
+
properties: {
|
|
2958
|
+
name: { type: 'string', description: 'Matches FirstName or LastName' },
|
|
2959
|
+
email: { type: 'string' },
|
|
2960
|
+
phone: { type: 'string' },
|
|
2961
|
+
account_name: { type: 'string', description: 'Account.Name via subquery filter — uses AccountId in LIKE' },
|
|
2962
|
+
title: { type: 'string' },
|
|
2963
|
+
limit: { type: 'number' },
|
|
2964
|
+
},
|
|
2965
|
+
required: [],
|
|
2966
|
+
},
|
|
2967
|
+
handler: async (p) => {
|
|
2968
|
+
const lim = p.limit != null ? Number(p.limit) : 20;
|
|
2969
|
+
const parts = [];
|
|
2970
|
+
if (p.name) {
|
|
2971
|
+
const x = escapeSoqlString(p.name);
|
|
2972
|
+
parts.push(`(FirstName LIKE '%${x}%' OR LastName LIKE '%${x}%')`);
|
|
2973
|
+
}
|
|
2974
|
+
if (p.email) parts.push(`Email LIKE '%${escapeSoqlString(p.email)}%'`);
|
|
2975
|
+
if (p.phone) parts.push(`Phone LIKE '%${escapeSoqlString(p.phone)}%'`);
|
|
2976
|
+
if (p.title) parts.push(`Title LIKE '%${escapeSoqlString(p.title)}%'`);
|
|
2977
|
+
if (p.account_name) {
|
|
2978
|
+
parts.push(
|
|
2979
|
+
`AccountId IN (SELECT Id FROM Account WHERE Name LIKE '%${escapeSoqlString(p.account_name)}%')`
|
|
2980
|
+
);
|
|
2981
|
+
}
|
|
2982
|
+
if (!parts.length) throw new Error('Provide at least one search criterion');
|
|
2983
|
+
const q = `SELECT Id,FirstName,LastName,Email,Phone,Title,AccountId FROM Contact WHERE ${parts.join(' AND ')} LIMIT ${lim}`;
|
|
2984
|
+
return text(await soqlGet(q));
|
|
2985
|
+
},
|
|
2986
|
+
},
|
|
2987
|
+
{
|
|
2988
|
+
name: 'search_leads',
|
|
2989
|
+
description:
|
|
2990
|
+
'Searches leads by name, company, email or status without writing SOQL. Use to find lead IDs before get_lead.',
|
|
2991
|
+
inputSchema: {
|
|
2992
|
+
type: 'object',
|
|
2993
|
+
properties: {
|
|
2994
|
+
name: { type: 'string' },
|
|
2995
|
+
company: { type: 'string' },
|
|
2996
|
+
email: { type: 'string' },
|
|
2997
|
+
status: { type: 'string' },
|
|
2998
|
+
lead_source: { type: 'string' },
|
|
2999
|
+
limit: { type: 'number' },
|
|
3000
|
+
},
|
|
3001
|
+
required: [],
|
|
3002
|
+
},
|
|
3003
|
+
handler: async (p) => {
|
|
3004
|
+
const lim = p.limit != null ? Number(p.limit) : 20;
|
|
3005
|
+
const parts = [];
|
|
3006
|
+
if (p.name) {
|
|
3007
|
+
const x = escapeSoqlString(p.name);
|
|
3008
|
+
parts.push(`(FirstName LIKE '%${x}%' OR LastName LIKE '%${x}%')`);
|
|
3009
|
+
}
|
|
3010
|
+
if (p.company) parts.push(`Company LIKE '%${escapeSoqlString(p.company)}%'`);
|
|
3011
|
+
if (p.email) parts.push(`Email LIKE '%${escapeSoqlString(p.email)}%'`);
|
|
3012
|
+
if (p.status) parts.push(`Status LIKE '%${escapeSoqlString(p.status)}%'`);
|
|
3013
|
+
if (p.lead_source) parts.push(`LeadSource LIKE '%${escapeSoqlString(p.lead_source)}%'`);
|
|
3014
|
+
if (!parts.length) throw new Error('Provide at least one search criterion');
|
|
3015
|
+
const q = `SELECT Id,FirstName,LastName,Company,Email,Status,LeadSource FROM Lead WHERE ${parts.join(' AND ')} LIMIT ${lim}`;
|
|
3016
|
+
return text(await soqlGet(q));
|
|
3017
|
+
},
|
|
3018
|
+
},
|
|
3019
|
+
{
|
|
3020
|
+
name: 'search_opportunities',
|
|
3021
|
+
description:
|
|
3022
|
+
'Searches opportunities by multiple criteria without writing SOQL. Use to find opportunities by stage, value or close date.',
|
|
3023
|
+
inputSchema: {
|
|
3024
|
+
type: 'object',
|
|
3025
|
+
properties: {
|
|
3026
|
+
name: { type: 'string' },
|
|
3027
|
+
account_name: { type: 'string' },
|
|
3028
|
+
stage: { type: 'string', description: 'StageName' },
|
|
3029
|
+
min_amount: { type: 'number' },
|
|
3030
|
+
max_amount: { type: 'number' },
|
|
3031
|
+
closing_before: { type: 'string', description: 'CloseDate < date (YYYY-MM-DD)' },
|
|
3032
|
+
closing_after: { type: 'string', description: 'CloseDate > date' },
|
|
3033
|
+
limit: { type: 'number' },
|
|
3034
|
+
},
|
|
3035
|
+
required: [],
|
|
3036
|
+
},
|
|
3037
|
+
handler: async (p) => {
|
|
3038
|
+
const lim = p.limit != null ? Number(p.limit) : 20;
|
|
3039
|
+
const parts = [];
|
|
3040
|
+
if (p.name) parts.push(`Name LIKE '%${escapeSoqlString(p.name)}%'`);
|
|
3041
|
+
if (p.account_name) {
|
|
3042
|
+
parts.push(
|
|
3043
|
+
`AccountId IN (SELECT Id FROM Account WHERE Name LIKE '%${escapeSoqlString(p.account_name)}%')`
|
|
3044
|
+
);
|
|
3045
|
+
}
|
|
3046
|
+
if (p.stage) parts.push(`StageName LIKE '%${escapeSoqlString(p.stage)}%'`);
|
|
3047
|
+
if (p.min_amount != null) parts.push(`Amount >= ${Number(p.min_amount)}`);
|
|
3048
|
+
if (p.max_amount != null) parts.push(`Amount <= ${Number(p.max_amount)}`);
|
|
3049
|
+
if (p.closing_before) parts.push(`CloseDate < ${escapeSoqlString(p.closing_before)}`);
|
|
3050
|
+
if (p.closing_after) parts.push(`CloseDate > ${escapeSoqlString(p.closing_after)}`);
|
|
3051
|
+
if (!parts.length) throw new Error('Provide at least one search criterion');
|
|
3052
|
+
const q = `SELECT Id,Name,StageName,Amount,CloseDate,AccountId FROM Opportunity WHERE ${parts.join(' AND ')} LIMIT ${lim}`;
|
|
3053
|
+
return text(await soqlGet(q));
|
|
3054
|
+
},
|
|
3055
|
+
},
|
|
3056
|
+
{
|
|
3057
|
+
name: 'get_pipeline_summary',
|
|
3058
|
+
description:
|
|
3059
|
+
'Returns the sales pipeline grouped by stage with deal count and total value per stage. Use for pipeline reviews and forecasting without writing aggregate SOQL.',
|
|
3060
|
+
inputSchema: {
|
|
3061
|
+
type: 'object',
|
|
3062
|
+
properties: {
|
|
3063
|
+
owner_id: { type: 'string', description: 'Optional OwnerId filter' },
|
|
3064
|
+
period: {
|
|
3065
|
+
type: 'string',
|
|
3066
|
+
enum: ['this_month', 'this_quarter', 'next_quarter'],
|
|
3067
|
+
description: 'Filters on CloseDate relative period (open opps)',
|
|
3068
|
+
},
|
|
3069
|
+
},
|
|
3070
|
+
required: [],
|
|
3071
|
+
},
|
|
3072
|
+
handler: async ({ owner_id, period }) => {
|
|
3073
|
+
const per = period || 'this_month';
|
|
3074
|
+
const clauses = ['IsClosed = false', pipelinePeriodClause(per)];
|
|
3075
|
+
if (owner_id) clauses.push(`OwnerId = '${escapeSoqlString(owner_id)}'`);
|
|
3076
|
+
const q = `SELECT StageName, COUNT(Id), SUM(Amount) FROM Opportunity WHERE ${clauses.join(' AND ')} GROUP BY StageName`;
|
|
3077
|
+
return text(await soqlGet(q));
|
|
3078
|
+
},
|
|
3079
|
+
},
|
|
3080
|
+
{
|
|
3081
|
+
name: 'global_search',
|
|
3082
|
+
description:
|
|
3083
|
+
"Searches across ALL Salesforce objects simultaneously using SOSL. Returns matches from Accounts, Contacts, Leads and Opportunities by default. Use when you don't know which object has the data.",
|
|
3084
|
+
inputSchema: {
|
|
3085
|
+
type: 'object',
|
|
3086
|
+
properties: {
|
|
3087
|
+
search_term: { type: 'string' },
|
|
3088
|
+
objects: {
|
|
3089
|
+
type: 'array',
|
|
3090
|
+
items: { type: 'string' },
|
|
3091
|
+
description:
|
|
3092
|
+
'Optional API names to RETURNING (default Account,Contact,Lead,Opportunity)',
|
|
3093
|
+
},
|
|
3094
|
+
limit: { type: 'number', description: 'Per-object row limit (default 10)' },
|
|
3095
|
+
},
|
|
3096
|
+
required: ['search_term'],
|
|
3097
|
+
},
|
|
3098
|
+
handler: async ({ search_term, objects, limit }) => {
|
|
3099
|
+
const lim = limit != null ? Number(limit) : 10;
|
|
3100
|
+
const term = escapeSoqlString(search_term);
|
|
3101
|
+
const objs = Array.isArray(objects) && objects.length
|
|
3102
|
+
? objects
|
|
3103
|
+
: ['Account', 'Contact', 'Lead', 'Opportunity'];
|
|
3104
|
+
const returning = objs.map((o) => `${o}(Id,Name)`).join(',');
|
|
3105
|
+
const sosl = `FIND {${term}} IN ALL FIELDS RETURNING ${returning} LIMIT ${lim}`;
|
|
3106
|
+
await ensureAccessToken();
|
|
3107
|
+
const headers = { ...buildAuthHeaders(), Accept: 'application/json' };
|
|
3108
|
+
const url = `${getBaseUrl()}/search/?q=${encodeURIComponent(sosl)}`;
|
|
3109
|
+
try {
|
|
3110
|
+
const res = await axios.get(url, { headers, timeout: 60000 });
|
|
3111
|
+
return text(res.data);
|
|
3112
|
+
} catch (e) {
|
|
3113
|
+
handleError(e);
|
|
3114
|
+
}
|
|
3115
|
+
},
|
|
3116
|
+
},
|
|
3117
|
+
{
|
|
3118
|
+
name: 'create_task',
|
|
3119
|
+
description: 'Generic task creation with full control over all fields.',
|
|
3120
|
+
inputSchema: {
|
|
3121
|
+
type: 'object',
|
|
3122
|
+
properties: {
|
|
3123
|
+
fields: {
|
|
3124
|
+
type: 'object',
|
|
3125
|
+
description:
|
|
3126
|
+
'Task fields (Subject, ActivityDate, OwnerId, WhoId, WhatId, Status, Priority, Description, etc.)',
|
|
3127
|
+
additionalProperties: true,
|
|
3128
|
+
},
|
|
3129
|
+
},
|
|
3130
|
+
required: ['fields'],
|
|
3131
|
+
},
|
|
3132
|
+
handler: async ({ fields }) => {
|
|
3133
|
+
await ensureAccessToken();
|
|
3134
|
+
const headers = { ...buildAuthHeaders(), 'Content-Type': 'application/json' };
|
|
3135
|
+
try {
|
|
3136
|
+
const res = await axios.post(`${getBaseUrl()}/sobjects/Task`, fields, {
|
|
3137
|
+
headers,
|
|
3138
|
+
timeout: 60000,
|
|
3139
|
+
validateStatus: () => true,
|
|
3140
|
+
});
|
|
3141
|
+
if (res.status < 200 || res.status >= 300) handleError({ response: res });
|
|
3142
|
+
return text(res.data);
|
|
3143
|
+
} catch (e) {
|
|
3144
|
+
handleError(e);
|
|
3145
|
+
}
|
|
3146
|
+
},
|
|
3147
|
+
},
|
|
3148
|
+
{
|
|
3149
|
+
name: 'log_call',
|
|
3150
|
+
description:
|
|
3151
|
+
'Logs a completed phone call as a Task linked to a Contact or Lead. Automatically sets Type=Call and Status=Completed. Use after finishing a sales or support call.',
|
|
3152
|
+
inputSchema: {
|
|
3153
|
+
type: 'object',
|
|
3154
|
+
properties: {
|
|
3155
|
+
subject: { type: 'string' },
|
|
3156
|
+
who_id: { type: 'string', description: 'WhoId (Contact or Lead Id)' },
|
|
3157
|
+
what_id: { type: 'string', description: 'Optional WhatId (Account, Opportunity, etc.)' },
|
|
3158
|
+
description: { type: 'string' },
|
|
3159
|
+
duration_minutes: { type: 'number' },
|
|
3160
|
+
call_disposition: { type: 'string' },
|
|
3161
|
+
call_type: { type: 'string' },
|
|
3162
|
+
},
|
|
3163
|
+
required: ['subject'],
|
|
3164
|
+
},
|
|
3165
|
+
handler: async (p) => {
|
|
3166
|
+
const task = {
|
|
3167
|
+
Subject: p.subject,
|
|
3168
|
+
Type: 'Call',
|
|
3169
|
+
Status: 'Completed',
|
|
3170
|
+
TaskSubtype: 'Call',
|
|
3171
|
+
};
|
|
3172
|
+
if (p.who_id) task.WhoId = p.who_id;
|
|
3173
|
+
if (p.what_id) task.WhatId = p.what_id;
|
|
3174
|
+
if (p.description) task.Description = p.description;
|
|
3175
|
+
if (p.duration_minutes != null) task.CallDurationInSeconds = Number(p.duration_minutes) * 60;
|
|
3176
|
+
if (p.call_disposition) task.CallDisposition = p.call_disposition;
|
|
3177
|
+
if (p.call_type) task.CallType = p.call_type;
|
|
3178
|
+
await ensureAccessToken();
|
|
3179
|
+
const headers = { ...buildAuthHeaders(), 'Content-Type': 'application/json' };
|
|
3180
|
+
try {
|
|
3181
|
+
const res = await axios.post(`${getBaseUrl()}/sobjects/Task`, task, {
|
|
3182
|
+
headers,
|
|
3183
|
+
timeout: 60000,
|
|
3184
|
+
validateStatus: () => true,
|
|
3185
|
+
});
|
|
3186
|
+
if (res.status < 200 || res.status >= 300) handleError({ response: res });
|
|
3187
|
+
return text(res.data);
|
|
3188
|
+
} catch (e) {
|
|
3189
|
+
handleError(e);
|
|
3190
|
+
}
|
|
3191
|
+
},
|
|
3192
|
+
},
|
|
3193
|
+
{
|
|
3194
|
+
name: 'log_email',
|
|
3195
|
+
description:
|
|
3196
|
+
'Logs an outbound email as activity in Salesforce. Creates an EmailMessage record linked to a Contact or Lead. Use after sending an important email to record it in CRM.',
|
|
3197
|
+
inputSchema: {
|
|
3198
|
+
type: 'object',
|
|
3199
|
+
properties: {
|
|
3200
|
+
subject: { type: 'string' },
|
|
3201
|
+
to_address: { type: 'string' },
|
|
3202
|
+
who_id: { type: 'string', description: 'Related Contact or Lead Id' },
|
|
3203
|
+
what_id: { type: 'string', description: 'Optional related record Id' },
|
|
3204
|
+
body: { type: 'string' },
|
|
3205
|
+
sent_date: { type: 'string', description: 'ISO or Salesforce datetime string' },
|
|
3206
|
+
},
|
|
3207
|
+
required: ['subject'],
|
|
3208
|
+
},
|
|
3209
|
+
handler: async (p) => {
|
|
3210
|
+
const msg = {
|
|
3211
|
+
Subject: p.subject,
|
|
3212
|
+
Status: '3',
|
|
3213
|
+
Incoming: false,
|
|
3214
|
+
TextBody: p.body || '',
|
|
3215
|
+
};
|
|
3216
|
+
if (p.to_address) msg.ToAddress = p.to_address;
|
|
3217
|
+
if (p.sent_date) msg.MessageDate = p.sent_date;
|
|
3218
|
+
if (p.what_id) msg.RelatedToId = p.what_id;
|
|
3219
|
+
else if (p.who_id) msg.RelatedToId = p.who_id;
|
|
3220
|
+
await ensureAccessToken();
|
|
3221
|
+
const headers = { ...buildAuthHeaders(), 'Content-Type': 'application/json' };
|
|
3222
|
+
try {
|
|
3223
|
+
const res = await axios.post(`${getBaseUrl()}/sobjects/EmailMessage`, msg, {
|
|
3224
|
+
headers,
|
|
3225
|
+
timeout: 60000,
|
|
3226
|
+
validateStatus: () => true,
|
|
3227
|
+
});
|
|
3228
|
+
if (res.status < 200 || res.status >= 300) handleError({ response: res });
|
|
3229
|
+
return text(res.data);
|
|
3230
|
+
} catch (e) {
|
|
3231
|
+
handleError(e);
|
|
3232
|
+
}
|
|
3233
|
+
},
|
|
3234
|
+
},
|
|
3235
|
+
{
|
|
3236
|
+
name: 'create_followup_task',
|
|
3237
|
+
description:
|
|
3238
|
+
'Creates a follow-up reminder task linked to a Contact, Lead or Opportunity with a due date. The most common activity in sales workflows — use after calls or meetings to set next steps.',
|
|
3239
|
+
inputSchema: {
|
|
3240
|
+
type: 'object',
|
|
3241
|
+
properties: {
|
|
3242
|
+
subject: { type: 'string' },
|
|
3243
|
+
who_id: { type: 'string' },
|
|
3244
|
+
what_id: { type: 'string' },
|
|
3245
|
+
due_date: { type: 'string', description: 'ActivityDate (YYYY-MM-DD)' },
|
|
3246
|
+
priority: { type: 'string' },
|
|
3247
|
+
assigned_to_id: { type: 'string', description: 'OwnerId' },
|
|
3248
|
+
description: { type: 'string' },
|
|
3249
|
+
},
|
|
3250
|
+
required: ['subject', 'due_date'],
|
|
3251
|
+
},
|
|
3252
|
+
handler: async (p) => {
|
|
3253
|
+
const task = {
|
|
3254
|
+
Subject: p.subject,
|
|
3255
|
+
ActivityDate: p.due_date,
|
|
3256
|
+
Status: 'Not Started',
|
|
3257
|
+
};
|
|
3258
|
+
if (p.who_id) task.WhoId = p.who_id;
|
|
3259
|
+
if (p.what_id) task.WhatId = p.what_id;
|
|
3260
|
+
if (p.priority) task.Priority = p.priority;
|
|
3261
|
+
if (p.assigned_to_id) task.OwnerId = p.assigned_to_id;
|
|
3262
|
+
if (p.description) task.Description = p.description;
|
|
3263
|
+
await ensureAccessToken();
|
|
3264
|
+
const headers = { ...buildAuthHeaders(), 'Content-Type': 'application/json' };
|
|
3265
|
+
try {
|
|
3266
|
+
const res = await axios.post(`${getBaseUrl()}/sobjects/Task`, task, {
|
|
3267
|
+
headers,
|
|
3268
|
+
timeout: 60000,
|
|
3269
|
+
validateStatus: () => true,
|
|
3270
|
+
});
|
|
3271
|
+
if (res.status < 200 || res.status >= 300) handleError({ response: res });
|
|
3272
|
+
return text(res.data);
|
|
3273
|
+
} catch (e) {
|
|
3274
|
+
handleError(e);
|
|
3275
|
+
}
|
|
3276
|
+
},
|
|
3277
|
+
},
|
|
3278
|
+
{
|
|
3279
|
+
name: 'get_todays_tasks',
|
|
3280
|
+
description:
|
|
3281
|
+
"Retrieves all open tasks due today for a user. Optionally includes overdue tasks. Use at the start of a workday to review priorities.",
|
|
3282
|
+
inputSchema: {
|
|
3283
|
+
type: 'object',
|
|
3284
|
+
properties: {
|
|
3285
|
+
user_id: {
|
|
3286
|
+
type: 'string',
|
|
3287
|
+
description: 'OwnerId — defaults to connected user from login',
|
|
3288
|
+
},
|
|
3289
|
+
include_overdue: {
|
|
3290
|
+
type: 'boolean',
|
|
3291
|
+
description: 'If true, includes tasks with ActivityDate < TODAY',
|
|
3292
|
+
},
|
|
3293
|
+
},
|
|
3294
|
+
required: [],
|
|
3295
|
+
},
|
|
3296
|
+
handler: async ({ user_id, include_overdue }) => {
|
|
3297
|
+
const uid = user_id || getDefaultUserId();
|
|
3298
|
+
if (!uid) {
|
|
3299
|
+
throw new Error('user_id required (no user id stored from OAuth — pass user_id explicitly)');
|
|
3300
|
+
}
|
|
3301
|
+
const dateClause = include_overdue
|
|
3302
|
+
? '(ActivityDate = TODAY OR ActivityDate < TODAY)'
|
|
3303
|
+
: 'ActivityDate = TODAY';
|
|
3304
|
+
const q = `SELECT Id,Subject,ActivityDate,Status,Priority,WhoId,WhatId FROM Task WHERE OwnerId='${escapeSoqlString(uid)}' AND IsClosed=false AND ${dateClause} ORDER BY ActivityDate ASC`;
|
|
3305
|
+
return text(await soqlGet(q));
|
|
3306
|
+
},
|
|
3307
|
+
},
|
|
3308
|
+
{
|
|
3309
|
+
name: 'get_overdue_tasks',
|
|
3310
|
+
description:
|
|
3311
|
+
'Retrieves all open tasks past their due date. Use to identify missed follow-ups that need immediate attention.',
|
|
3312
|
+
inputSchema: {
|
|
3313
|
+
type: 'object',
|
|
3314
|
+
properties: {
|
|
3315
|
+
user_id: { type: 'string' },
|
|
3316
|
+
limit: { type: 'number', description: 'Default 50' },
|
|
3317
|
+
},
|
|
3318
|
+
required: [],
|
|
3319
|
+
},
|
|
3320
|
+
handler: async ({ user_id, limit }) => {
|
|
3321
|
+
const uid = user_id || getDefaultUserId();
|
|
3322
|
+
if (!uid) {
|
|
3323
|
+
throw new Error('user_id required (no user id stored from OAuth — pass user_id explicitly)');
|
|
3324
|
+
}
|
|
3325
|
+
const lim = limit != null ? Number(limit) : 50;
|
|
3326
|
+
const q = `SELECT Id,Subject,ActivityDate,Status,Priority FROM Task WHERE OwnerId='${escapeSoqlString(uid)}' AND IsClosed=false AND ActivityDate < TODAY ORDER BY ActivityDate ASC LIMIT ${lim}`;
|
|
3327
|
+
return text(await soqlGet(q));
|
|
3328
|
+
},
|
|
3329
|
+
},
|
|
3330
|
+
{
|
|
3331
|
+
name: 'update_lead',
|
|
3332
|
+
description: 'Generic lead update for multiple fields.',
|
|
3333
|
+
inputSchema: {
|
|
3334
|
+
type: 'object',
|
|
3335
|
+
properties: {
|
|
3336
|
+
lead_id: { type: 'string' },
|
|
3337
|
+
fields: { type: 'object', additionalProperties: true },
|
|
3338
|
+
},
|
|
3339
|
+
required: ['lead_id', 'fields'],
|
|
3340
|
+
},
|
|
3341
|
+
handler: async ({ lead_id, fields }) => {
|
|
3342
|
+
await ensureAccessToken();
|
|
3343
|
+
const headers = { ...buildAuthHeaders(), 'Content-Type': 'application/json' };
|
|
3344
|
+
try {
|
|
3345
|
+
const res = await axios.patch(
|
|
3346
|
+
`${getBaseUrl()}/sobjects/Lead/${encodeURIComponent(lead_id)}`,
|
|
3347
|
+
fields,
|
|
3348
|
+
{ headers, timeout: 60000, validateStatus: () => true }
|
|
3349
|
+
);
|
|
3350
|
+
if (res.status < 200 || res.status >= 300) handleError({ response: res });
|
|
3351
|
+
return text({ success: true });
|
|
3352
|
+
} catch (e) {
|
|
3353
|
+
handleError(e);
|
|
3354
|
+
}
|
|
3355
|
+
},
|
|
3356
|
+
},
|
|
3357
|
+
{
|
|
3358
|
+
name: 'update_lead_status',
|
|
3359
|
+
description:
|
|
3360
|
+
'Updates a lead status in the qualification pipeline. Use instead of update_lead when only changing lead status.',
|
|
3361
|
+
inputSchema: {
|
|
3362
|
+
type: 'object',
|
|
3363
|
+
properties: {
|
|
3364
|
+
lead_id: { type: 'string' },
|
|
3365
|
+
status: {
|
|
3366
|
+
type: 'string',
|
|
3367
|
+
description: 'Status picklist value (e.g. New, Working, Nurturing, Qualified, Unqualified)',
|
|
3368
|
+
},
|
|
3369
|
+
},
|
|
3370
|
+
required: ['lead_id', 'status'],
|
|
3371
|
+
},
|
|
3372
|
+
handler: async ({ lead_id, status }) => {
|
|
3373
|
+
await ensureAccessToken();
|
|
3374
|
+
const headers = { ...buildAuthHeaders(), 'Content-Type': 'application/json' };
|
|
3375
|
+
try {
|
|
3376
|
+
const res = await axios.patch(
|
|
3377
|
+
`${getBaseUrl()}/sobjects/Lead/${encodeURIComponent(lead_id)}`,
|
|
3378
|
+
{ Status: status },
|
|
3379
|
+
{ headers, timeout: 60000, validateStatus: () => true }
|
|
3380
|
+
);
|
|
3381
|
+
if (res.status < 200 || res.status >= 300) handleError({ response: res });
|
|
3382
|
+
return text({ success: true });
|
|
3383
|
+
} catch (e) {
|
|
3384
|
+
handleError(e);
|
|
3385
|
+
}
|
|
3386
|
+
},
|
|
3387
|
+
},
|
|
3388
|
+
{
|
|
3389
|
+
name: 'convert_lead',
|
|
3390
|
+
description:
|
|
3391
|
+
'Converts a qualified lead into an Account, Contact and optional Opportunity. This is a critical CRM workflow — use when a lead is sales-qualified and ready to enter the pipeline.',
|
|
3392
|
+
inputSchema: {
|
|
3393
|
+
type: 'object',
|
|
3394
|
+
properties: {
|
|
3395
|
+
lead_id: { type: 'string' },
|
|
3396
|
+
create_opportunity: { type: 'boolean', description: 'Default true' },
|
|
3397
|
+
opportunity_name: { type: 'string' },
|
|
3398
|
+
account_id: { type: 'string' },
|
|
3399
|
+
contact_id: { type: 'string' },
|
|
3400
|
+
converted_status: {
|
|
3401
|
+
type: 'string',
|
|
3402
|
+
description: 'Converted status label (org-specific)',
|
|
3403
|
+
},
|
|
3404
|
+
},
|
|
3405
|
+
required: ['lead_id'],
|
|
3406
|
+
},
|
|
3407
|
+
handler: async (p) => {
|
|
3408
|
+
const createOpp = p.create_opportunity !== false;
|
|
3409
|
+
const inputs = [
|
|
3410
|
+
{
|
|
3411
|
+
leadId: p.lead_id,
|
|
3412
|
+
convertedStatus: p.converted_status || 'Qualified - Converted',
|
|
3413
|
+
doNotCreateOpportunity: !createOpp,
|
|
3414
|
+
opportunityName: createOpp ? p.opportunity_name || null : null,
|
|
3415
|
+
accountId: p.account_id || null,
|
|
3416
|
+
contactId: p.contact_id || null,
|
|
3417
|
+
sendEmailToOwner: false,
|
|
3418
|
+
},
|
|
3419
|
+
];
|
|
3420
|
+
await ensureAccessToken();
|
|
3421
|
+
const headers = { ...buildAuthHeaders(), 'Content-Type': 'application/json' };
|
|
3422
|
+
const url = `${getBaseUrl()}/sobjects/Lead/${encodeURIComponent(p.lead_id)}/actions/standard/convert`;
|
|
3423
|
+
try {
|
|
3424
|
+
const res = await axios.post(url, { inputs }, { headers, timeout: 120000, validateStatus: () => true });
|
|
3425
|
+
if (res.status < 200 || res.status >= 300) handleError({ response: res });
|
|
3426
|
+
return text(res.data);
|
|
3427
|
+
} catch (e) {
|
|
3428
|
+
handleError(e);
|
|
3429
|
+
}
|
|
3430
|
+
},
|
|
3431
|
+
},
|
|
3432
|
+
];
|
|
3433
|
+
}
|
|
3434
|
+
|
|
3435
|
+
function connectSmartTools() {
|
|
3436
|
+
return [
|
|
3437
|
+
{
|
|
3438
|
+
name: 'post_chatter_message',
|
|
3439
|
+
description:
|
|
3440
|
+
'Posts a message to a Chatter feed on a record, group or user profile. Use to share updates or @mention team members directly from the CRM.',
|
|
3441
|
+
inputSchema: {
|
|
3442
|
+
type: 'object',
|
|
3443
|
+
properties: {
|
|
3444
|
+
feed_element_type: {
|
|
3445
|
+
type: 'string',
|
|
3446
|
+
description: 'Usually FeedItem for a standard post.',
|
|
3447
|
+
default: 'FeedItem',
|
|
3448
|
+
},
|
|
3449
|
+
subject_id: {
|
|
3450
|
+
type: 'string',
|
|
3451
|
+
description: 'Record, group or user ID the feed belongs to.',
|
|
3452
|
+
},
|
|
3453
|
+
message_text: {
|
|
3454
|
+
type: 'string',
|
|
3455
|
+
description: 'Plain text body (before @mentions).',
|
|
3456
|
+
},
|
|
3457
|
+
mentions: {
|
|
3458
|
+
type: 'array',
|
|
3459
|
+
items: { type: 'string' },
|
|
3460
|
+
description: 'Salesforce user IDs to @mention (order is appended after message_text).',
|
|
3461
|
+
},
|
|
3462
|
+
},
|
|
3463
|
+
required: ['subject_id', 'feed_element_type'],
|
|
3464
|
+
},
|
|
3465
|
+
handler: async (p) => {
|
|
3466
|
+
const feedElementType = p.feed_element_type || 'FeedItem';
|
|
3467
|
+
const subjectId = p.subject_id;
|
|
3468
|
+
const textPart = p.message_text != null ? String(p.message_text) : '';
|
|
3469
|
+
const mentionIds = Array.isArray(p.mentions) ? p.mentions.filter(Boolean) : [];
|
|
3470
|
+
if (!textPart.trim() && mentionIds.length === 0) {
|
|
3471
|
+
throw new Error('Provide message_text and/or mentions');
|
|
3472
|
+
}
|
|
3473
|
+
const messageSegments = [];
|
|
3474
|
+
if (textPart.length) messageSegments.push({ type: 'Text', text: textPart });
|
|
3475
|
+
for (const id of mentionIds) {
|
|
3476
|
+
messageSegments.push({ type: 'Mention', id: String(id) });
|
|
3477
|
+
}
|
|
3478
|
+
const body = {
|
|
3479
|
+
feedElementType,
|
|
3480
|
+
subjectId,
|
|
3481
|
+
body: { messageSegments },
|
|
3482
|
+
};
|
|
3483
|
+
const data = await sfRequest('POST', '/chatter/feed-elements', 'versioned', body);
|
|
3484
|
+
return text(data);
|
|
3485
|
+
},
|
|
3486
|
+
},
|
|
3487
|
+
{
|
|
3488
|
+
name: 'get_record_feed',
|
|
3489
|
+
description:
|
|
3490
|
+
'Retrieves the Chatter feed for a specific record (Account, Opportunity, Case etc). Returns all posts, comments and file shares on that record.',
|
|
3491
|
+
inputSchema: {
|
|
3492
|
+
type: 'object',
|
|
3493
|
+
properties: {
|
|
3494
|
+
record_id: { type: 'string' },
|
|
3495
|
+
page_size: { type: 'number', description: 'Default 25', default: 25 },
|
|
3496
|
+
},
|
|
3497
|
+
required: ['record_id'],
|
|
3498
|
+
},
|
|
3499
|
+
handler: async ({ record_id, page_size }) => {
|
|
3500
|
+
const ps = page_size != null && Number.isFinite(Number(page_size)) ? Number(page_size) : 25;
|
|
3501
|
+
const data = await sfRequest(
|
|
3502
|
+
'GET',
|
|
3503
|
+
'/chatter/feeds/record/{RECORD_GROUP_ID}/feed-elements',
|
|
3504
|
+
'versioned',
|
|
3505
|
+
{ RECORD_GROUP_ID: record_id, pageSize: ps }
|
|
3506
|
+
);
|
|
3507
|
+
return text(data);
|
|
3508
|
+
},
|
|
3509
|
+
},
|
|
3510
|
+
{
|
|
3511
|
+
name: 'upload_file_to_record',
|
|
3512
|
+
description:
|
|
3513
|
+
'Uploads a file and attaches it to a Salesforce record using the Connect API. Use to attach documents, images or reports to Accounts, Opportunities or Cases.',
|
|
3514
|
+
inputSchema: {
|
|
3515
|
+
type: 'object',
|
|
3516
|
+
properties: {
|
|
3517
|
+
record_id: { type: 'string' },
|
|
3518
|
+
file_name: { type: 'string' },
|
|
3519
|
+
file_content_base64: { type: 'string', description: 'File bytes as base64 (no data: URL prefix).' },
|
|
3520
|
+
description: {
|
|
3521
|
+
type: 'string',
|
|
3522
|
+
description: 'Optional feed text shown with the attachment.',
|
|
3523
|
+
},
|
|
3524
|
+
},
|
|
3525
|
+
required: ['record_id', 'file_name', 'file_content_base64'],
|
|
3526
|
+
},
|
|
3527
|
+
handler: async ({ record_id, file_name, file_content_base64, description }) => {
|
|
3528
|
+
await ensureAccessToken();
|
|
3529
|
+
const headers = buildAuthHeaders();
|
|
3530
|
+
const buf = Buffer.from(String(file_content_base64), 'base64');
|
|
3531
|
+
const form = new FormData();
|
|
3532
|
+
form.append('fileData', new Blob([buf]), file_name);
|
|
3533
|
+
const upUrl = `${getBaseUrl()}/connect/files/users/me`;
|
|
3534
|
+
let upRes;
|
|
3535
|
+
try {
|
|
3536
|
+
upRes = await fetch(upUrl, { method: 'POST', headers, body: form });
|
|
3537
|
+
} catch (e) {
|
|
3538
|
+
throw new Error(e?.message || 'File upload request failed');
|
|
3539
|
+
}
|
|
3540
|
+
const upText = await upRes.text();
|
|
3541
|
+
let fileMeta;
|
|
3542
|
+
try {
|
|
3543
|
+
fileMeta = upText ? JSON.parse(upText) : {};
|
|
3544
|
+
} catch {
|
|
3545
|
+
throw new Error(`Upload response not JSON (${upRes.status}): ${upText.slice(0, 500)}`);
|
|
3546
|
+
}
|
|
3547
|
+
if (!upRes.ok) {
|
|
3548
|
+
handleError({ response: { status: upRes.status, data: fileMeta, statusText: upRes.statusText } });
|
|
3549
|
+
}
|
|
3550
|
+
const fileId = fileMeta.id;
|
|
3551
|
+
if (!fileId) throw new Error('Upload succeeded but no file id in response');
|
|
3552
|
+
const feedBody = {
|
|
3553
|
+
feedElementType: 'FeedItem',
|
|
3554
|
+
subjectId: record_id,
|
|
3555
|
+
capabilities: { files: { items: [{ id: fileId }] } },
|
|
3556
|
+
};
|
|
3557
|
+
if (description != null && String(description).trim()) {
|
|
3558
|
+
feedBody.body = {
|
|
3559
|
+
messageSegments: [{ type: 'Text', text: String(description).trim() }],
|
|
3560
|
+
};
|
|
3561
|
+
}
|
|
3562
|
+
const data = await sfRequest('POST', '/chatter/feed-elements', 'versioned', feedBody);
|
|
3563
|
+
return text({ upload: fileMeta, feedElement: data });
|
|
3564
|
+
},
|
|
3565
|
+
},
|
|
3566
|
+
{
|
|
3567
|
+
name: 'get_user_profile',
|
|
3568
|
+
description:
|
|
3569
|
+
"Retrieves a Salesforce user's profile including name, title, department, email and photo. Use to get user context before assigning records.",
|
|
3570
|
+
inputSchema: {
|
|
3571
|
+
type: 'object',
|
|
3572
|
+
properties: {
|
|
3573
|
+
user_id: {
|
|
3574
|
+
type: 'string',
|
|
3575
|
+
description: 'User id or me for the connected user (default me).',
|
|
3576
|
+
},
|
|
3577
|
+
},
|
|
3578
|
+
required: [],
|
|
3579
|
+
},
|
|
3580
|
+
handler: async ({ user_id }) => {
|
|
3581
|
+
const uid =
|
|
3582
|
+
user_id === undefined || user_id === null || String(user_id).trim() === ''
|
|
3583
|
+
? 'me'
|
|
3584
|
+
: String(user_id).trim();
|
|
3585
|
+
const path =
|
|
3586
|
+
uid === 'me' ? '/chatter/users/me' : `/chatter/users/${encodeURIComponent(uid)}`;
|
|
3587
|
+
const data = await sfRequest('GET', path, 'versioned', {});
|
|
3588
|
+
return text(data);
|
|
3589
|
+
},
|
|
3590
|
+
},
|
|
3591
|
+
{
|
|
3592
|
+
name: 'list_groups',
|
|
3593
|
+
description:
|
|
3594
|
+
'Lists Chatter groups the current user belongs to or searches all groups by name. Use to find group IDs before posting to a group feed.',
|
|
3595
|
+
inputSchema: {
|
|
3596
|
+
type: 'object',
|
|
3597
|
+
properties: {
|
|
3598
|
+
query: { type: 'string', description: 'Optional search string (group name).' },
|
|
3599
|
+
limit: { type: 'number', description: 'Default 25 (pageSize).', default: 25 },
|
|
3600
|
+
},
|
|
3601
|
+
required: [],
|
|
3602
|
+
},
|
|
3603
|
+
handler: async ({ query, limit }) => {
|
|
3604
|
+
const pageSize = limit != null && Number.isFinite(Number(limit)) ? Number(limit) : 25;
|
|
3605
|
+
const params = { pageSize };
|
|
3606
|
+
if (query != null && String(query).trim() !== '') params.q = String(query).trim();
|
|
3607
|
+
const data = await sfRequest('GET', '/chatter/groups', 'versioned', params);
|
|
3608
|
+
return text(data);
|
|
3609
|
+
},
|
|
3610
|
+
},
|
|
3611
|
+
];
|
|
3612
|
+
}
|
|
3613
|
+
|
|
3614
|
+
function toolingSmartTools() {
|
|
3615
|
+
return [
|
|
3616
|
+
{
|
|
3617
|
+
name: 'execute_apex',
|
|
3618
|
+
description:
|
|
3619
|
+
'Executes anonymous Apex code against the Salesforce org. Use for running custom logic, data fixes or testing code snippets without deploying. Returns compilation result, logs pointer and any errors. Uses Tooling executeAnonymous (GET with anonymousBody query parameter).',
|
|
3620
|
+
inputSchema: {
|
|
3621
|
+
type: 'object',
|
|
3622
|
+
properties: {
|
|
3623
|
+
apex_body: {
|
|
3624
|
+
type: 'string',
|
|
3625
|
+
description: 'Apex source to run anonymously (e.g. `System.debug(\\\'x\\\');`).',
|
|
3626
|
+
},
|
|
3627
|
+
},
|
|
3628
|
+
required: ['apex_body'],
|
|
3629
|
+
},
|
|
3630
|
+
handler: async ({ apex_body }) => {
|
|
3631
|
+
const data = await sfRequest('GET', '/executeAnonymous', 'tooling', {
|
|
3632
|
+
anonymousBody: apex_body,
|
|
3633
|
+
});
|
|
3634
|
+
return text(data);
|
|
3635
|
+
},
|
|
3636
|
+
},
|
|
3637
|
+
{
|
|
3638
|
+
name: 'get_apex_logs',
|
|
3639
|
+
description:
|
|
3640
|
+
'Retrieves recent Apex debug logs from the org. Use for debugging failed integrations or custom code execution.',
|
|
3641
|
+
inputSchema: {
|
|
3642
|
+
type: 'object',
|
|
3643
|
+
properties: {
|
|
3644
|
+
limit: { type: 'number', description: 'Default 10', default: 10 },
|
|
3645
|
+
user_id: {
|
|
3646
|
+
type: 'string',
|
|
3647
|
+
description: 'Optional: filter logs to this Salesforce user Id (LogUserId).',
|
|
3648
|
+
},
|
|
3649
|
+
},
|
|
3650
|
+
required: [],
|
|
3651
|
+
},
|
|
3652
|
+
handler: async ({ limit, user_id }) => {
|
|
3653
|
+
const limRaw = limit != null && Number.isFinite(Number(limit)) ? Number(limit) : 10;
|
|
3654
|
+
const lim = Math.max(1, Math.min(2000, Math.trunc(limRaw)));
|
|
3655
|
+
let q =
|
|
3656
|
+
'SELECT Id, Application, DurationMilliseconds, Location, LogLength, LogUserId, Operation, Request, StartTime, Status FROM ApexLog';
|
|
3657
|
+
if (user_id != null && String(user_id).trim() !== '') {
|
|
3658
|
+
q += ` WHERE LogUserId='${escapeSoqlString(String(user_id).trim())}'`;
|
|
3659
|
+
}
|
|
3660
|
+
q += ` ORDER BY StartTime DESC LIMIT ${lim}`;
|
|
3661
|
+
const data = await sfRequest('GET', '/query', 'tooling', { q });
|
|
3662
|
+
return text(data);
|
|
3663
|
+
},
|
|
3664
|
+
},
|
|
3665
|
+
{
|
|
3666
|
+
name: 'run_apex_tests',
|
|
3667
|
+
description:
|
|
3668
|
+
'Triggers Apex test class execution via asynchronous Tooling and returns the API result (includes job id for polling). Use to verify org health before deployments.',
|
|
3669
|
+
inputSchema: {
|
|
3670
|
+
type: 'object',
|
|
3671
|
+
properties: {
|
|
3672
|
+
class_names: {
|
|
3673
|
+
type: 'array',
|
|
3674
|
+
items: { type: 'string' },
|
|
3675
|
+
description: 'Apex test class API names (e.g. MyClassTest).',
|
|
3676
|
+
},
|
|
3677
|
+
},
|
|
3678
|
+
required: ['class_names'],
|
|
3679
|
+
},
|
|
3680
|
+
handler: async ({ class_names }) => {
|
|
3681
|
+
const names = Array.isArray(class_names) ? class_names.map((s) => String(s).trim()).filter(Boolean) : [];
|
|
3682
|
+
if (!names.length) throw new Error('class_names must include at least one class name');
|
|
3683
|
+
const data = await sfRequest('POST', '/runTestsAsynchronous', 'tooling', {
|
|
3684
|
+
classNames: names.join(','),
|
|
3685
|
+
});
|
|
3686
|
+
return text(data);
|
|
3687
|
+
},
|
|
3688
|
+
},
|
|
3689
|
+
{
|
|
3690
|
+
name: 'query_tooling_api',
|
|
3691
|
+
description:
|
|
3692
|
+
'Executes a SOQL query against Tooling API objects like ApexClass, CustomField, ValidationRule etc. Use for org metadata inspection.',
|
|
3693
|
+
inputSchema: {
|
|
3694
|
+
type: 'object',
|
|
3695
|
+
properties: {
|
|
3696
|
+
query: { type: 'string', description: 'Full SOQL string (Tooling entities only).' },
|
|
3697
|
+
},
|
|
3698
|
+
required: ['query'],
|
|
3699
|
+
},
|
|
3700
|
+
handler: async ({ query }) => {
|
|
3701
|
+
const data = await sfRequest('GET', '/query', 'tooling', { q: query });
|
|
3702
|
+
return text(data);
|
|
3703
|
+
},
|
|
3704
|
+
},
|
|
3705
|
+
{
|
|
3706
|
+
name: 'list_apex_classes',
|
|
3707
|
+
description:
|
|
3708
|
+
'Lists Apex classes in the org with name, status and last modified date. Use to discover available Apex classes before executing.',
|
|
3709
|
+
inputSchema: {
|
|
3710
|
+
type: 'object',
|
|
3711
|
+
properties: {
|
|
3712
|
+
name_filter: {
|
|
3713
|
+
type: 'string',
|
|
3714
|
+
description: 'Optional substring match on Name (SOQL LIKE).',
|
|
3715
|
+
},
|
|
3716
|
+
limit: { type: 'number', description: 'Default 50', default: 50 },
|
|
3717
|
+
},
|
|
3718
|
+
required: [],
|
|
3719
|
+
},
|
|
3720
|
+
handler: async ({ name_filter, limit }) => {
|
|
3721
|
+
const limRaw = limit != null && Number.isFinite(Number(limit)) ? Number(limit) : 50;
|
|
3722
|
+
const lim = Math.max(1, Math.min(2000, Math.trunc(limRaw)));
|
|
3723
|
+
let q = 'SELECT Id, Name, Status, LastModifiedDate FROM ApexClass';
|
|
3724
|
+
if (name_filter != null && String(name_filter).trim() !== '') {
|
|
3725
|
+
q += ` WHERE Name LIKE '%${escapeSoqlString(String(name_filter).trim())}%'`;
|
|
3726
|
+
}
|
|
3727
|
+
q += ` ORDER BY LastModifiedDate DESC LIMIT ${lim}`;
|
|
3728
|
+
const data = await sfRequest('GET', '/query', 'tooling', { q });
|
|
3729
|
+
return text(data);
|
|
3730
|
+
},
|
|
3731
|
+
},
|
|
3732
|
+
];
|
|
3733
|
+
}
|
|
3734
|
+
|
|
3735
|
+
function uiApiCommaQuery(value) {
|
|
3736
|
+
if (value == null) return undefined;
|
|
3737
|
+
if (Array.isArray(value)) return value.map((x) => String(x).trim()).filter(Boolean).join(',');
|
|
3738
|
+
return String(value).trim();
|
|
3739
|
+
}
|
|
3740
|
+
|
|
3741
|
+
function uiSmartTools() {
|
|
3742
|
+
return [
|
|
3743
|
+
{
|
|
3744
|
+
name: 'get_record_ui',
|
|
3745
|
+
description:
|
|
3746
|
+
"Retrieves a record's data formatted for UI display including field labels, formatted values and layout sections. Better than raw REST for displaying data to users as it includes human-readable labels.",
|
|
3747
|
+
inputSchema: {
|
|
3748
|
+
type: 'object',
|
|
3749
|
+
properties: {
|
|
3750
|
+
record_id: {
|
|
3751
|
+
type: 'string',
|
|
3752
|
+
description: 'Salesforce record Id (or comma-separated Ids per UI API).',
|
|
3753
|
+
},
|
|
3754
|
+
layout_types: {
|
|
3755
|
+
description:
|
|
3756
|
+
'Optional. Layout types: Compact, Full. Comma-separated string or array of strings.',
|
|
3757
|
+
oneOf: [
|
|
3758
|
+
{ type: 'string' },
|
|
3759
|
+
{ type: 'array', items: { type: 'string' } },
|
|
3760
|
+
],
|
|
3761
|
+
},
|
|
3762
|
+
modes: {
|
|
3763
|
+
description: 'Optional. Create, Edit, View. Comma-separated string or array of strings.',
|
|
3764
|
+
oneOf: [
|
|
3765
|
+
{ type: 'string' },
|
|
3766
|
+
{ type: 'array', items: { type: 'string' } },
|
|
3767
|
+
],
|
|
3768
|
+
},
|
|
3769
|
+
},
|
|
3770
|
+
required: ['record_id'],
|
|
3771
|
+
},
|
|
3772
|
+
handler: async ({ record_id, layout_types, modes }) => {
|
|
3773
|
+
const q = {
|
|
3774
|
+
RECORD_IDS: record_id,
|
|
3775
|
+
};
|
|
3776
|
+
const lt = uiApiCommaQuery(layout_types);
|
|
3777
|
+
const md = uiApiCommaQuery(modes);
|
|
3778
|
+
if (lt) q.layoutTypes = lt;
|
|
3779
|
+
if (md) q.modes = md;
|
|
3780
|
+
const data = await sfRequest('GET', '/record-ui/{RECORD_IDS}', 'ui-api', q);
|
|
3781
|
+
return text(data);
|
|
3782
|
+
},
|
|
3783
|
+
},
|
|
3784
|
+
{
|
|
3785
|
+
name: 'get_object_info',
|
|
3786
|
+
description:
|
|
3787
|
+
'Returns UI-friendly metadata for a Salesforce object including field labels, required fields, and default record type. Use before building forms or displaying records.',
|
|
3788
|
+
inputSchema: {
|
|
3789
|
+
type: 'object',
|
|
3790
|
+
properties: {
|
|
3791
|
+
object_api_name: { type: 'string', description: 'sObject API name (e.g. Account, CustomObject__c).' },
|
|
3792
|
+
},
|
|
3793
|
+
required: ['object_api_name'],
|
|
3794
|
+
},
|
|
3795
|
+
handler: async ({ object_api_name }) => {
|
|
3796
|
+
const data = await sfRequest(
|
|
3797
|
+
'GET',
|
|
3798
|
+
'/object-info/{SOBJECT_API_NAME}',
|
|
3799
|
+
'ui-api',
|
|
3800
|
+
{ SOBJECT_API_NAME: object_api_name }
|
|
3801
|
+
);
|
|
3802
|
+
return text(data);
|
|
3803
|
+
},
|
|
3804
|
+
},
|
|
3805
|
+
{
|
|
3806
|
+
name: 'get_picklist_values_ui',
|
|
3807
|
+
description:
|
|
3808
|
+
'Returns all picklist field values for an object in UI-friendly format with labels and valid values per record type. Use before creating or editing records.',
|
|
3809
|
+
inputSchema: {
|
|
3810
|
+
type: 'object',
|
|
3811
|
+
properties: {
|
|
3812
|
+
object_api_name: { type: 'string' },
|
|
3813
|
+
record_type_id: {
|
|
3814
|
+
type: 'string',
|
|
3815
|
+
description: 'Optional; defaults to defaultRecordTypeId from object-info when omitted.',
|
|
3816
|
+
},
|
|
3817
|
+
},
|
|
3818
|
+
required: ['object_api_name'],
|
|
3819
|
+
},
|
|
3820
|
+
handler: async ({ object_api_name, record_type_id }) => {
|
|
3821
|
+
let rtid =
|
|
3822
|
+
record_type_id != null && String(record_type_id).trim() !== ''
|
|
3823
|
+
? String(record_type_id).trim()
|
|
3824
|
+
: null;
|
|
3825
|
+
if (!rtid) {
|
|
3826
|
+
const info = await sfRequest(
|
|
3827
|
+
'GET',
|
|
3828
|
+
'/object-info/{SOBJECT_API_NAME}',
|
|
3829
|
+
'ui-api',
|
|
3830
|
+
{ SOBJECT_API_NAME: object_api_name }
|
|
3831
|
+
);
|
|
3832
|
+
rtid = info?.defaultRecordTypeId || null;
|
|
3833
|
+
if (!rtid && Array.isArray(info?.recordTypeInfos) && info.recordTypeInfos.length) {
|
|
3834
|
+
rtid = info.recordTypeInfos[0].recordTypeId || info.recordTypeInfos[0].id;
|
|
3835
|
+
}
|
|
3836
|
+
}
|
|
3837
|
+
if (!rtid) {
|
|
3838
|
+
throw new Error(
|
|
3839
|
+
'record_type_id is required for this org/object (could not resolve defaultRecordTypeId).'
|
|
3840
|
+
);
|
|
3841
|
+
}
|
|
3842
|
+
const data = await sfRequest(
|
|
3843
|
+
'GET',
|
|
3844
|
+
'/object-info/{SOBJECT_API_NAME}/picklist-values/{RECORD_TYPE_ID}',
|
|
3845
|
+
'ui-api',
|
|
3846
|
+
{ SOBJECT_API_NAME: object_api_name, RECORD_TYPE_ID: rtid }
|
|
3847
|
+
);
|
|
3848
|
+
return text(data);
|
|
3849
|
+
},
|
|
3850
|
+
},
|
|
3851
|
+
{
|
|
3852
|
+
name: 'get_layout',
|
|
3853
|
+
description:
|
|
3854
|
+
'Returns the page layout for a Salesforce object showing which fields appear in which sections and their order. Use to understand record structure.',
|
|
3855
|
+
inputSchema: {
|
|
3856
|
+
type: 'object',
|
|
3857
|
+
properties: {
|
|
3858
|
+
object_api_name: { type: 'string' },
|
|
3859
|
+
layout_type: {
|
|
3860
|
+
type: 'string',
|
|
3861
|
+
description: 'Full or Compact (maps to layoutTypes query param).',
|
|
3862
|
+
enum: ['Full', 'Compact'],
|
|
3863
|
+
},
|
|
3864
|
+
record_type_id: { type: 'string', description: 'Record type Id for the layout.' },
|
|
3865
|
+
},
|
|
3866
|
+
required: ['object_api_name', 'layout_type', 'record_type_id'],
|
|
3867
|
+
},
|
|
3868
|
+
handler: async ({ object_api_name, layout_type, record_type_id }) => {
|
|
3869
|
+
const data = await sfRequest('GET', '/layout/{SOBJECT_API_NAME}', 'ui-api', {
|
|
3870
|
+
SOBJECT_API_NAME: object_api_name,
|
|
3871
|
+
layoutTypes: String(layout_type).trim(),
|
|
3872
|
+
recordTypeId: String(record_type_id).trim(),
|
|
3873
|
+
modes: 'View',
|
|
3874
|
+
});
|
|
3875
|
+
return text(data);
|
|
3876
|
+
},
|
|
3877
|
+
},
|
|
3878
|
+
];
|
|
3879
|
+
}
|
|
3880
|
+
|
|
3881
|
+
function einsteinSmartTools() {
|
|
3882
|
+
return [
|
|
3883
|
+
{
|
|
3884
|
+
name: 'analyze_sentiment',
|
|
3885
|
+
description:
|
|
3886
|
+
'Analyzes the sentiment (positive/negative/neutral) of text using Einstein Language. Use to automatically classify customer emails, case descriptions or survey responses. Calls the Einstein Platform `language/sentiment` endpoint (multipart). Use a Language JWT in SALESFORCE_EINSTEIN_ACCESS_TOKEN or EINSTEIN_ACCESS_TOKEN; if unset, the Salesforce session token is sent (may not be accepted by api.einstein.ai). Default model_id is CommunitySentiment when omitted.',
|
|
3887
|
+
inputSchema: {
|
|
3888
|
+
type: 'object',
|
|
3889
|
+
properties: {
|
|
3890
|
+
text: {
|
|
3891
|
+
description: 'Text to analyze, or an array of strings for batch calls.',
|
|
3892
|
+
oneOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }],
|
|
3893
|
+
},
|
|
3894
|
+
model_id: {
|
|
3895
|
+
type: 'string',
|
|
3896
|
+
description: 'Einstein Language model id. Omit to use CommunitySentiment.',
|
|
3897
|
+
},
|
|
3898
|
+
},
|
|
3899
|
+
required: ['text'],
|
|
3900
|
+
},
|
|
3901
|
+
handler: async (p) => {
|
|
3902
|
+
const modelId =
|
|
3903
|
+
p.model_id != null && String(p.model_id).trim() !== '' ? String(p.model_id).trim() : 'CommunitySentiment';
|
|
3904
|
+
const raw = p.text;
|
|
3905
|
+
const texts = Array.isArray(raw) ? raw.map((t) => String(t)) : [String(raw)];
|
|
3906
|
+
if (!texts.length || texts.every((t) => !t.trim())) throw new Error('text must be non-empty');
|
|
3907
|
+
const results = [];
|
|
3908
|
+
for (const doc of texts) {
|
|
3909
|
+
const row = await einsteinPlatformPostMultipart('/language/sentiment', (f) => {
|
|
3910
|
+
f.append('modelId', modelId);
|
|
3911
|
+
f.append('document', doc);
|
|
3912
|
+
});
|
|
3913
|
+
results.push(row);
|
|
3914
|
+
}
|
|
3915
|
+
return text(results.length === 1 ? results[0] : { results });
|
|
3916
|
+
},
|
|
3917
|
+
},
|
|
3918
|
+
{
|
|
3919
|
+
name: 'classify_intent',
|
|
3920
|
+
description:
|
|
3921
|
+
'Classifies the intent of a text input using a trained Einstein Language model. Use to automatically route incoming messages, emails or cases based on customer intent. Requires Einstein Platform auth (see analyze_sentiment).',
|
|
3922
|
+
inputSchema: {
|
|
3923
|
+
type: 'object',
|
|
3924
|
+
properties: {
|
|
3925
|
+
text: { type: 'string' },
|
|
3926
|
+
model_id: { type: 'string', description: 'Your trained intent model id.' },
|
|
3927
|
+
},
|
|
3928
|
+
required: ['text', 'model_id'],
|
|
3929
|
+
},
|
|
3930
|
+
handler: async ({ text, model_id }) => {
|
|
3931
|
+
const mid = String(model_id).trim();
|
|
3932
|
+
if (!mid) throw new Error('model_id is required');
|
|
3933
|
+
const data = await einsteinPlatformPostMultipart('/language/intent', (f) => {
|
|
3934
|
+
f.append('modelId', mid);
|
|
3935
|
+
f.append('document', String(text));
|
|
3936
|
+
});
|
|
3937
|
+
return text(data);
|
|
3938
|
+
},
|
|
3939
|
+
},
|
|
3940
|
+
{
|
|
3941
|
+
name: 'predict_image',
|
|
3942
|
+
description:
|
|
3943
|
+
'Classifies an image using a trained Einstein Vision model. Returns prediction labels and confidence scores. Pass image_url or image_base64 (raw base64, no data: prefix). Requires Einstein Platform auth (see analyze_sentiment).',
|
|
3944
|
+
inputSchema: {
|
|
3945
|
+
type: 'object',
|
|
3946
|
+
properties: {
|
|
3947
|
+
image_url: { type: 'string', description: 'Public URL of the image.' },
|
|
3948
|
+
image_base64: { type: 'string', description: 'Image bytes as base64.' },
|
|
3949
|
+
model_id: { type: 'string', description: 'Einstein Vision model id.' },
|
|
3950
|
+
},
|
|
3951
|
+
required: ['model_id'],
|
|
3952
|
+
},
|
|
3953
|
+
handler: async ({ image_url, image_base64, model_id }) => {
|
|
3954
|
+
const mid = String(model_id).trim();
|
|
3955
|
+
if (!mid) throw new Error('model_id is required');
|
|
3956
|
+
const url = image_url != null && String(image_url).trim() !== '' ? String(image_url).trim() : null;
|
|
3957
|
+
const b64 = image_base64 != null && String(image_base64).trim() !== '' ? String(image_base64).trim() : null;
|
|
3958
|
+
if (!url && !b64) throw new Error('Provide image_url or image_base64');
|
|
3959
|
+
const data = await einsteinPlatformPostMultipart('/vision/predict', (f) => {
|
|
3960
|
+
f.append('modelId', mid);
|
|
3961
|
+
if (url) f.append('sampleLocation', url);
|
|
3962
|
+
else f.append('sampleBase64Content', b64);
|
|
3963
|
+
});
|
|
3964
|
+
return text(data);
|
|
3965
|
+
},
|
|
3966
|
+
},
|
|
3967
|
+
];
|
|
3968
|
+
}
|
|
3969
|
+
|
|
3970
|
+
function toPlatformEventApiName(event_name) {
|
|
3971
|
+
const s = String(event_name ?? '').trim();
|
|
3972
|
+
if (!s) throw new Error('event_name is required');
|
|
3973
|
+
if (s.endsWith('__e')) return s;
|
|
3974
|
+
return `${s}__e`;
|
|
3975
|
+
}
|
|
3976
|
+
|
|
3977
|
+
function eventPlatformSmartTools() {
|
|
3978
|
+
return [
|
|
3979
|
+
{
|
|
3980
|
+
name: 'publish_platform_event',
|
|
3981
|
+
description:
|
|
3982
|
+
'Publishes a Platform Event to the Salesforce event bus. Use to trigger real-time integrations, notifications or downstream automations that subscribe to this event.',
|
|
3983
|
+
inputSchema: {
|
|
3984
|
+
type: 'object',
|
|
3985
|
+
properties: {
|
|
3986
|
+
event_name: {
|
|
3987
|
+
type: 'string',
|
|
3988
|
+
description: 'Platform event API name (with or without __e suffix).',
|
|
3989
|
+
},
|
|
3990
|
+
event_data: {
|
|
3991
|
+
type: 'object',
|
|
3992
|
+
additionalProperties: true,
|
|
3993
|
+
description: 'Field API names → values for the event payload.',
|
|
3994
|
+
},
|
|
3995
|
+
},
|
|
3996
|
+
required: ['event_name'],
|
|
3997
|
+
},
|
|
3998
|
+
handler: async ({ event_name, event_data }) => {
|
|
3999
|
+
const apiName = toPlatformEventApiName(event_name);
|
|
4000
|
+
const body = event_data != null && typeof event_data === 'object' ? event_data : {};
|
|
4001
|
+
const data = await sfRequest('POST', '/sobjects/{EventApiName}/', 'versioned', {
|
|
4002
|
+
EventApiName: apiName,
|
|
4003
|
+
_rawBody: body,
|
|
4004
|
+
});
|
|
4005
|
+
return text(data);
|
|
4006
|
+
},
|
|
4007
|
+
},
|
|
4008
|
+
{
|
|
4009
|
+
name: 'list_platform_events',
|
|
4010
|
+
description:
|
|
4011
|
+
'Lists Platform Event types in the org (EntityDefinition where API name ends with __e). Use to discover available events before publishing.',
|
|
4012
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
4013
|
+
handler: async () => {
|
|
4014
|
+
const q =
|
|
4015
|
+
"SELECT QualifiedApiName, Label, DeveloperName, DurableId FROM EntityDefinition WHERE QualifiedApiName LIKE '%__e' ORDER BY QualifiedApiName LIMIT 500";
|
|
4016
|
+
const data = await sfRequest('GET', '/query', 'tooling', { q });
|
|
4017
|
+
return text(data);
|
|
4018
|
+
},
|
|
4019
|
+
},
|
|
4020
|
+
{
|
|
4021
|
+
name: 'create_streaming_topic',
|
|
4022
|
+
description:
|
|
4023
|
+
'Creates a PushTopic for streaming real-time record changes via the Streaming API. Use to set up real-time data sync integrations.',
|
|
4024
|
+
inputSchema: {
|
|
4025
|
+
type: 'object',
|
|
4026
|
+
properties: {
|
|
4027
|
+
name: { type: 'string', description: 'Unique PushTopic name.' },
|
|
4028
|
+
query: { type: 'string', description: 'SOQL query (Id, fields must be valid for streaming).' },
|
|
4029
|
+
notify_for_fields: {
|
|
4030
|
+
type: 'string',
|
|
4031
|
+
description: 'PushTopic NotifyForFields: Referenced (default), Select, Where, or All.',
|
|
4032
|
+
enum: ['Referenced', 'Select', 'Where', 'All'],
|
|
4033
|
+
},
|
|
4034
|
+
},
|
|
4035
|
+
required: ['name', 'query'],
|
|
4036
|
+
},
|
|
4037
|
+
handler: async ({ name, query, notify_for_fields }) => {
|
|
4038
|
+
const verRaw = process.env.SALESFORCE_API_VERSION || 'v59.0';
|
|
4039
|
+
const apiNum = Number.parseFloat(String(verRaw).replace(/^v/i, '')) || 59;
|
|
4040
|
+
const notify = notify_for_fields != null && String(notify_for_fields).trim() !== ''
|
|
4041
|
+
? String(notify_for_fields).trim()
|
|
4042
|
+
: 'Referenced';
|
|
4043
|
+
const data = await sfRequest('POST', '/sobjects/PushTopic', 'versioned', {
|
|
4044
|
+
_rawBody: {
|
|
4045
|
+
Name: String(name).trim(),
|
|
4046
|
+
Query: String(query),
|
|
4047
|
+
NotifyForFields: notify,
|
|
4048
|
+
ApiVersion: apiNum,
|
|
4049
|
+
},
|
|
4050
|
+
});
|
|
4051
|
+
return text(data);
|
|
4052
|
+
},
|
|
4053
|
+
},
|
|
4054
|
+
];
|
|
4055
|
+
}
|
|
4056
|
+
|
|
4057
|
+
const GQL_ACCOUNTS_WITH_CONTACTS = `query accountsWithContacts {
|
|
4058
|
+
uiapi {
|
|
4059
|
+
query {
|
|
4060
|
+
Account {
|
|
4061
|
+
edges {
|
|
4062
|
+
node {
|
|
4063
|
+
Id
|
|
4064
|
+
Name { value }
|
|
4065
|
+
Contacts {
|
|
4066
|
+
edges {
|
|
4067
|
+
node {
|
|
4068
|
+
Id
|
|
4069
|
+
Name { value }
|
|
4070
|
+
Email { value }
|
|
4071
|
+
}
|
|
4072
|
+
}
|
|
4073
|
+
}
|
|
4074
|
+
}
|
|
4075
|
+
}
|
|
4076
|
+
}
|
|
4077
|
+
}
|
|
4078
|
+
}
|
|
4079
|
+
}`;
|
|
4080
|
+
|
|
4081
|
+
function graphqlSmartTools() {
|
|
4082
|
+
return [
|
|
4083
|
+
{
|
|
4084
|
+
name: 'graphql_query',
|
|
4085
|
+
description:
|
|
4086
|
+
'Executes a GraphQL query against Salesforce data. More efficient than REST for fetching related records in one request. Use when you need nested related data without multiple API calls.',
|
|
4087
|
+
inputSchema: {
|
|
4088
|
+
type: 'object',
|
|
4089
|
+
properties: {
|
|
4090
|
+
query: { type: 'string', description: 'GraphQL query string.' },
|
|
4091
|
+
variables: {
|
|
4092
|
+
type: 'object',
|
|
4093
|
+
description: 'Optional GraphQL variables object.',
|
|
4094
|
+
additionalProperties: true,
|
|
4095
|
+
},
|
|
4096
|
+
},
|
|
4097
|
+
required: ['query'],
|
|
4098
|
+
},
|
|
4099
|
+
handler: async ({ query, variables }) => {
|
|
4100
|
+
const body = { query };
|
|
4101
|
+
if (
|
|
4102
|
+
variables != null &&
|
|
4103
|
+
typeof variables === 'object' &&
|
|
4104
|
+
!Array.isArray(variables) &&
|
|
4105
|
+
Object.keys(variables).length > 0
|
|
4106
|
+
) {
|
|
4107
|
+
body.variables = variables;
|
|
4108
|
+
}
|
|
4109
|
+
const data = await sfRequest('POST', '/', 'graphql', { _rawBody: body });
|
|
4110
|
+
return text(data);
|
|
4111
|
+
},
|
|
4112
|
+
},
|
|
4113
|
+
{
|
|
4114
|
+
name: 'graphql_query_accounts_with_contacts',
|
|
4115
|
+
description:
|
|
4116
|
+
'Fetches accounts with their related contacts using GraphQL in a single request. Returns only the fields you need. More efficient than SOQL relationship queries for nested data.',
|
|
4117
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
4118
|
+
handler: async () => {
|
|
4119
|
+
const data = await sfRequest('POST', '/', 'graphql', {
|
|
4120
|
+
_rawBody: { query: GQL_ACCOUNTS_WITH_CONTACTS },
|
|
4121
|
+
});
|
|
4122
|
+
return text(data);
|
|
4123
|
+
},
|
|
4124
|
+
},
|
|
4125
|
+
];
|
|
4126
|
+
}
|
|
4127
|
+
|
|
4128
|
+
async function resolvePricebook2Id(pricebook_id) {
|
|
4129
|
+
if (pricebook_id != null && String(pricebook_id).trim() !== '') return String(pricebook_id).trim();
|
|
4130
|
+
const q =
|
|
4131
|
+
"SELECT Id FROM Pricebook2 WHERE Name = 'Standard Price Book' AND IsActive = true LIMIT 1";
|
|
4132
|
+
const d = await sfRequest('GET', '/query/', 'versioned', { q });
|
|
4133
|
+
return d.records?.[0]?.Id || null;
|
|
4134
|
+
}
|
|
4135
|
+
|
|
4136
|
+
function subscriptionMgmtSmartTools() {
|
|
4137
|
+
return [
|
|
4138
|
+
{
|
|
4139
|
+
name: 'get_product_catalog',
|
|
4140
|
+
description:
|
|
4141
|
+
'Lists products in a Salesforce price book with names, codes and prices. Use before adding products to opportunities or orders. Defaults to the Standard Price Book when pricebook_id is omitted.',
|
|
4142
|
+
inputSchema: {
|
|
4143
|
+
type: 'object',
|
|
4144
|
+
properties: {
|
|
4145
|
+
pricebook_id: { type: 'string', description: 'Optional Pricebook2 Id (defaults to Standard Price Book).' },
|
|
4146
|
+
active_only: {
|
|
4147
|
+
type: 'boolean',
|
|
4148
|
+
description: 'If true (default), only active PricebookEntry and Product2 rows.',
|
|
4149
|
+
default: true,
|
|
4150
|
+
},
|
|
4151
|
+
},
|
|
4152
|
+
required: [],
|
|
4153
|
+
},
|
|
4154
|
+
handler: async ({ pricebook_id, active_only }) => {
|
|
4155
|
+
const pbId = await resolvePricebook2Id(pricebook_id);
|
|
4156
|
+
if (!pbId) throw new Error('Could not resolve price book (set pricebook_id or ensure Standard Price Book exists).');
|
|
4157
|
+
const active = active_only !== false;
|
|
4158
|
+
let q = `SELECT Id, Name, Product2Id, Product2.Name, Product2.ProductCode, Product2.IsActive, UnitPrice, IsActive, UseStandardPrice FROM PricebookEntry WHERE Pricebook2Id='${escapeSoqlString(pbId)}'`;
|
|
4159
|
+
if (active) q += ' AND IsActive = true AND Product2.IsActive = true';
|
|
4160
|
+
q += ' ORDER BY Product2.Name LIMIT 2000';
|
|
4161
|
+
const data = await sfRequest('GET', '/query/', 'versioned', { q });
|
|
4162
|
+
return text({ pricebook_id: pbId, ...data });
|
|
4163
|
+
},
|
|
4164
|
+
},
|
|
4165
|
+
{
|
|
4166
|
+
name: 'create_order_from_opportunity',
|
|
4167
|
+
description:
|
|
4168
|
+
'Creates an Order from an Opportunity and copies its opportunity line items to order products in one flow. Use when converting a won deal to an order. Requires Product2/pricebook alignment on line items; add BillToContactId etc. via org automation if your Order rules require it.',
|
|
4169
|
+
inputSchema: {
|
|
4170
|
+
type: 'object',
|
|
4171
|
+
properties: {
|
|
4172
|
+
opportunity_id: { type: 'string' },
|
|
4173
|
+
pricebook_id: { type: 'string', description: "Used if the opportunity has no price book; else opportunity's price book is used." },
|
|
4174
|
+
effective_date: {
|
|
4175
|
+
type: 'string',
|
|
4176
|
+
description: 'Order effective date (YYYY-MM-DD).',
|
|
4177
|
+
},
|
|
4178
|
+
},
|
|
4179
|
+
required: ['opportunity_id', 'pricebook_id', 'effective_date'],
|
|
4180
|
+
},
|
|
4181
|
+
handler: async ({ opportunity_id, pricebook_id, effective_date }) => {
|
|
4182
|
+
const oid = escapeSoqlString(opportunity_id);
|
|
4183
|
+
const oppRes = await sfRequest('GET', '/query/', 'versioned', {
|
|
4184
|
+
q: `SELECT Id, AccountId, Pricebook2Id, CurrencyIsoCode FROM Opportunity WHERE Id='${oid}' LIMIT 1`,
|
|
4185
|
+
});
|
|
4186
|
+
const opp = oppRes.records?.[0];
|
|
4187
|
+
if (!opp) throw new Error('Opportunity not found');
|
|
4188
|
+
const pbFromParam = pricebook_id != null && String(pricebook_id).trim() !== '' ? String(pricebook_id).trim() : null;
|
|
4189
|
+
const pbId = pbFromParam || opp.Pricebook2Id;
|
|
4190
|
+
if (!pbId) throw new Error('No price book on opportunity and pricebook_id not provided');
|
|
4191
|
+
const eff = String(effective_date).trim().slice(0, 10);
|
|
4192
|
+
|
|
4193
|
+
const oliRes = await sfRequest('GET', '/query/', 'versioned', {
|
|
4194
|
+
q: `SELECT Id, Product2Id, Quantity, UnitPrice, PricebookEntryId, Description FROM OpportunityLineItem WHERE OpportunityId='${oid}'`,
|
|
4195
|
+
});
|
|
4196
|
+
const lines = oliRes.records || [];
|
|
4197
|
+
if (!lines.length) throw new Error('No opportunity line items to convert');
|
|
4198
|
+
|
|
4199
|
+
const orderBody = {
|
|
4200
|
+
AccountId: opp.AccountId,
|
|
4201
|
+
OpportunityId: opp.Id,
|
|
4202
|
+
Pricebook2Id: pbId,
|
|
4203
|
+
EffectiveDate: eff,
|
|
4204
|
+
Status: 'Draft',
|
|
4205
|
+
};
|
|
4206
|
+
if (opp.CurrencyIsoCode) orderBody.CurrencyIsoCode = opp.CurrencyIsoCode;
|
|
4207
|
+
|
|
4208
|
+
const order = await sfRequest('POST', '/sobjects/Order', 'versioned', { _rawBody: orderBody });
|
|
4209
|
+
const orderId = order.id;
|
|
4210
|
+
if (!orderId) throw new Error('Order create failed (check required Order fields for your org)');
|
|
4211
|
+
|
|
4212
|
+
const createdItems = [];
|
|
4213
|
+
for (const line of lines) {
|
|
4214
|
+
if (!line.PricebookEntryId) {
|
|
4215
|
+
throw new Error(`Opportunity line ${line.Id} has no PricebookEntryId; refresh lines with the order price book`);
|
|
4216
|
+
}
|
|
4217
|
+
const oiBody = {
|
|
4218
|
+
OrderId: orderId,
|
|
4219
|
+
PricebookEntryId: line.PricebookEntryId,
|
|
4220
|
+
Quantity: line.Quantity,
|
|
4221
|
+
UnitPrice: line.UnitPrice,
|
|
4222
|
+
};
|
|
4223
|
+
if (line.Description) oiBody.Description = line.Description;
|
|
4224
|
+
const oi = await sfRequest('POST', '/sobjects/OrderItem', 'versioned', { _rawBody: oiBody });
|
|
4225
|
+
createdItems.push(oi);
|
|
4226
|
+
}
|
|
4227
|
+
|
|
4228
|
+
return text({ order, orderItems: createdItems });
|
|
4229
|
+
},
|
|
4230
|
+
},
|
|
4231
|
+
{
|
|
4232
|
+
name: 'get_subscription_status',
|
|
4233
|
+
description:
|
|
4234
|
+
'Retrieves recent orders for an account with nested order line items (product, quantity, dates). Use for renewal-style views where subscriptions are represented on Orders (Subscription Management / Revenue Cloud patterns vary by org).',
|
|
4235
|
+
inputSchema: {
|
|
4236
|
+
type: 'object',
|
|
4237
|
+
properties: {
|
|
4238
|
+
account_id: { type: 'string', description: 'Salesforce Account Id.' },
|
|
4239
|
+
},
|
|
4240
|
+
required: ['account_id'],
|
|
4241
|
+
},
|
|
4242
|
+
handler: async ({ account_id }) => {
|
|
4243
|
+
const aid = escapeSoqlString(account_id);
|
|
4244
|
+
const q = `SELECT Id, OrderNumber, Status, EffectiveDate, TotalAmount,
|
|
4245
|
+
(SELECT Id, Product2Id, Product2.Name, Quantity, UnitPrice, ListPrice, ServiceDate FROM OrderItems ORDER BY CreatedDate)
|
|
4246
|
+
FROM Order WHERE AccountId='${aid}' ORDER BY EffectiveDate DESC LIMIT 200`;
|
|
4247
|
+
const data = await sfRequest('GET', '/query/', 'versioned', { q });
|
|
4248
|
+
return text(data);
|
|
4249
|
+
},
|
|
4250
|
+
},
|
|
4251
|
+
];
|
|
4252
|
+
}
|
|
4253
|
+
|
|
4254
|
+
function enrichSchemaForWrite(schema, method) {
|
|
4255
|
+
const s = structuredClone(schema);
|
|
4256
|
+
s.properties = s.properties || {};
|
|
4257
|
+
if (['POST', 'PUT', 'PATCH'].includes(method)) {
|
|
4258
|
+
s.properties._rawBody = {
|
|
4259
|
+
type: 'object',
|
|
4260
|
+
description:
|
|
4261
|
+
'Optional: full JSON body. When set, replaces inferred body fields from other arguments.',
|
|
4262
|
+
additionalProperties: true,
|
|
4263
|
+
};
|
|
4264
|
+
}
|
|
4265
|
+
return s;
|
|
4266
|
+
}
|
|
4267
|
+
|
|
4268
|
+
function shouldSkipManifestEndpoint(ep) {
|
|
4269
|
+
if (ep.method === 'GET' && ep.path === '/query/') return true;
|
|
4270
|
+
if (ep.method === 'GET' && ep.path === '/search') return true;
|
|
4271
|
+
return false;
|
|
4272
|
+
}
|
|
4273
|
+
|
|
4274
|
+
function methodRank(m) {
|
|
4275
|
+
return (
|
|
4276
|
+
{ GET: 0, POST: 1, PATCH: 2, PUT: 3, DELETE: 4 }[m] ?? 99
|
|
4277
|
+
);
|
|
4278
|
+
}
|
|
4279
|
+
|
|
4280
|
+
function generatedToolsFromManifest() {
|
|
4281
|
+
const out = [];
|
|
4282
|
+
for (const ep of manifest.endpoints) {
|
|
4283
|
+
if (shouldSkipManifestEndpoint(ep)) continue;
|
|
4284
|
+
out.push({
|
|
4285
|
+
name: ep.name,
|
|
4286
|
+
description: ep.description,
|
|
4287
|
+
inputSchema: enrichSchemaForWrite(ep.inputSchema, ep.method),
|
|
4288
|
+
handler: async (params) => {
|
|
4289
|
+
const data = await sfRequest(ep.method, ep.path, ep.urlKind, params);
|
|
4290
|
+
return text(data);
|
|
4291
|
+
},
|
|
4292
|
+
});
|
|
4293
|
+
}
|
|
4294
|
+
out.sort((a, b) => {
|
|
4295
|
+
const ea = manifest.endpoints.find((e) => e.name === a.name);
|
|
4296
|
+
const eb = manifest.endpoints.find((e) => e.name === b.name);
|
|
4297
|
+
const ra = methodRank(ea?.method);
|
|
4298
|
+
const rb = methodRank(eb?.method);
|
|
4299
|
+
if (ra !== rb) return ra - rb;
|
|
4300
|
+
return a.name.localeCompare(b.name);
|
|
4301
|
+
});
|
|
4302
|
+
return out;
|
|
4303
|
+
}
|
|
4304
|
+
|
|
4305
|
+
const mult = multipliedTools();
|
|
4306
|
+
const gen = generatedToolsFromManifest();
|
|
4307
|
+
const bulkWrappers = bulkApiMultipliedTools();
|
|
4308
|
+
const bulkGen = bulkV2GeneratedTools();
|
|
4309
|
+
const bulkV1Gen = bulkV1GeneratedTools();
|
|
4310
|
+
const compositeTools = compositeSmartTools();
|
|
4311
|
+
const metadataSmart = metadataRestSmartTools();
|
|
4312
|
+
const metadataSoap = metadataSoapToolsFromPostman();
|
|
4313
|
+
const connectGen = connectGeneratedToolsFromPostman();
|
|
4314
|
+
const connectSmart = connectSmartTools();
|
|
4315
|
+
const toolingGen = toolingGeneratedToolsFromPostman();
|
|
4316
|
+
const toolingSmart = toolingSmartTools();
|
|
4317
|
+
const uiGen = uiGeneratedToolsFromPostman();
|
|
4318
|
+
const uiSmart = uiSmartTools();
|
|
4319
|
+
const einsteinPsGen = einsteinPsGeneratedToolsFromPostman();
|
|
4320
|
+
const einsteinSmart = einsteinSmartTools();
|
|
4321
|
+
const evtPfGen = eventPlatformGeneratedToolsFromPostman();
|
|
4322
|
+
const evtPfSmart = eventPlatformSmartTools();
|
|
4323
|
+
const gqlGen = graphqlGeneratedToolsFromPostman();
|
|
4324
|
+
const gqlSmart = graphqlSmartTools();
|
|
4325
|
+
const subMgmtGen = subMgmtGeneratedToolsFromPostman();
|
|
4326
|
+
const subMgmtSmart = subscriptionMgmtSmartTools();
|
|
4327
|
+
const industriesGen = industriesGeneratedToolsFromPostman();
|
|
4328
|
+
|
|
4329
|
+
const seen = new Set(mult.map((t) => t.name));
|
|
4330
|
+
for (const t of gen) {
|
|
4331
|
+
if (seen.has(t.name)) {
|
|
4332
|
+
t.name = `${t.name}_rest`;
|
|
4333
|
+
}
|
|
4334
|
+
seen.add(t.name);
|
|
4335
|
+
}
|
|
4336
|
+
for (const t of bulkWrappers) {
|
|
4337
|
+
if (seen.has(t.name)) {
|
|
4338
|
+
t.name = `${t.name}_wrap`;
|
|
4339
|
+
}
|
|
4340
|
+
seen.add(t.name);
|
|
4341
|
+
}
|
|
4342
|
+
for (const t of bulkGen) {
|
|
4343
|
+
if (seen.has(t.name)) {
|
|
4344
|
+
t.name = `${t.name}_bulk`;
|
|
4345
|
+
}
|
|
4346
|
+
seen.add(t.name);
|
|
4347
|
+
}
|
|
4348
|
+
for (const t of bulkV1Gen) {
|
|
4349
|
+
if (seen.has(t.name)) {
|
|
4350
|
+
t.name = `${t.name}_bv1`;
|
|
4351
|
+
}
|
|
4352
|
+
seen.add(t.name);
|
|
4353
|
+
}
|
|
4354
|
+
for (const t of compositeTools) {
|
|
4355
|
+
if (seen.has(t.name)) {
|
|
4356
|
+
t.name = `${t.name}_comp`;
|
|
4357
|
+
}
|
|
4358
|
+
seen.add(t.name);
|
|
4359
|
+
}
|
|
4360
|
+
for (const t of metadataSmart) {
|
|
4361
|
+
if (seen.has(t.name)) {
|
|
4362
|
+
t.name = `${t.name}_meta`;
|
|
4363
|
+
}
|
|
4364
|
+
seen.add(t.name);
|
|
4365
|
+
}
|
|
4366
|
+
for (const t of metadataSoap) {
|
|
4367
|
+
if (seen.has(t.name)) {
|
|
4368
|
+
t.name = `${t.name}_soap`;
|
|
4369
|
+
}
|
|
4370
|
+
seen.add(t.name);
|
|
4371
|
+
}
|
|
4372
|
+
for (const t of connectGen) {
|
|
4373
|
+
if (seen.has(t.name)) {
|
|
4374
|
+
t.name = `${t.name}_conn`;
|
|
4375
|
+
}
|
|
4376
|
+
seen.add(t.name);
|
|
4377
|
+
}
|
|
4378
|
+
for (const t of connectSmart) {
|
|
4379
|
+
if (seen.has(t.name)) {
|
|
4380
|
+
t.name = `${t.name}_conn`;
|
|
4381
|
+
}
|
|
4382
|
+
seen.add(t.name);
|
|
4383
|
+
}
|
|
4384
|
+
for (const t of toolingGen) {
|
|
4385
|
+
if (seen.has(t.name)) {
|
|
4386
|
+
t.name = `${t.name}_tl`;
|
|
4387
|
+
}
|
|
4388
|
+
seen.add(t.name);
|
|
4389
|
+
}
|
|
4390
|
+
for (const t of toolingSmart) {
|
|
4391
|
+
if (seen.has(t.name)) {
|
|
4392
|
+
t.name = `${t.name}_tl`;
|
|
4393
|
+
}
|
|
4394
|
+
seen.add(t.name);
|
|
4395
|
+
}
|
|
4396
|
+
for (const t of uiGen) {
|
|
4397
|
+
if (seen.has(t.name)) {
|
|
4398
|
+
t.name = `${t.name}_uia`;
|
|
4399
|
+
}
|
|
4400
|
+
seen.add(t.name);
|
|
4401
|
+
}
|
|
4402
|
+
for (const t of uiSmart) {
|
|
4403
|
+
if (seen.has(t.name)) {
|
|
4404
|
+
t.name = `${t.name}_uia`;
|
|
4405
|
+
}
|
|
4406
|
+
seen.add(t.name);
|
|
4407
|
+
}
|
|
4408
|
+
for (const t of einsteinPsGen) {
|
|
4409
|
+
if (seen.has(t.name)) {
|
|
4410
|
+
t.name = `${t.name}_eps`;
|
|
4411
|
+
}
|
|
4412
|
+
seen.add(t.name);
|
|
4413
|
+
}
|
|
4414
|
+
for (const t of einsteinSmart) {
|
|
4415
|
+
if (seen.has(t.name)) {
|
|
4416
|
+
t.name = `${t.name}_eps`;
|
|
4417
|
+
}
|
|
4418
|
+
seen.add(t.name);
|
|
4419
|
+
}
|
|
4420
|
+
for (const t of evtPfGen) {
|
|
4421
|
+
if (seen.has(t.name)) {
|
|
4422
|
+
t.name = `${t.name}_evp`;
|
|
4423
|
+
}
|
|
4424
|
+
seen.add(t.name);
|
|
4425
|
+
}
|
|
4426
|
+
for (const t of evtPfSmart) {
|
|
4427
|
+
if (seen.has(t.name)) {
|
|
4428
|
+
t.name = `${t.name}_evp`;
|
|
4429
|
+
}
|
|
4430
|
+
seen.add(t.name);
|
|
4431
|
+
}
|
|
4432
|
+
for (const t of gqlGen) {
|
|
4433
|
+
if (seen.has(t.name)) {
|
|
4434
|
+
t.name = `${t.name}_gql`;
|
|
4435
|
+
}
|
|
4436
|
+
seen.add(t.name);
|
|
4437
|
+
}
|
|
4438
|
+
for (const t of gqlSmart) {
|
|
4439
|
+
if (seen.has(t.name)) {
|
|
4440
|
+
t.name = `${t.name}_gql`;
|
|
4441
|
+
}
|
|
4442
|
+
seen.add(t.name);
|
|
4443
|
+
}
|
|
4444
|
+
for (const t of subMgmtGen) {
|
|
4445
|
+
if (seen.has(t.name)) {
|
|
4446
|
+
t.name = `${t.name}_subm`;
|
|
4447
|
+
}
|
|
4448
|
+
seen.add(t.name);
|
|
4449
|
+
}
|
|
4450
|
+
for (const t of subMgmtSmart) {
|
|
4451
|
+
if (seen.has(t.name)) {
|
|
4452
|
+
t.name = `${t.name}_subm`;
|
|
4453
|
+
}
|
|
4454
|
+
seen.add(t.name);
|
|
4455
|
+
}
|
|
4456
|
+
for (const t of industriesGen) {
|
|
4457
|
+
if (seen.has(t.name)) {
|
|
4458
|
+
t.name = `${t.name}_ind`;
|
|
4459
|
+
}
|
|
4460
|
+
seen.add(t.name);
|
|
4461
|
+
}
|
|
4462
|
+
|
|
4463
|
+
export const tools = [
|
|
4464
|
+
...mult,
|
|
4465
|
+
...gen,
|
|
4466
|
+
...bulkWrappers,
|
|
4467
|
+
...bulkGen,
|
|
4468
|
+
...bulkV1Gen,
|
|
4469
|
+
...compositeTools,
|
|
4470
|
+
...metadataSmart,
|
|
4471
|
+
...metadataSoap,
|
|
4472
|
+
...connectGen,
|
|
4473
|
+
...connectSmart,
|
|
4474
|
+
...toolingGen,
|
|
4475
|
+
...toolingSmart,
|
|
4476
|
+
...uiGen,
|
|
4477
|
+
...uiSmart,
|
|
4478
|
+
...einsteinPsGen,
|
|
4479
|
+
...einsteinSmart,
|
|
4480
|
+
...evtPfGen,
|
|
4481
|
+
...evtPfSmart,
|
|
4482
|
+
...gqlGen,
|
|
4483
|
+
...gqlSmart,
|
|
4484
|
+
...subMgmtGen,
|
|
4485
|
+
...subMgmtSmart,
|
|
4486
|
+
...industriesGen,
|
|
4487
|
+
];
|