salesforce-metadata-mcp 2.5.2 → 2.5.3
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/CHANGELOG.md +48 -0
- package/README.md +18 -1
- package/dist/index.js +4 -4
- package/dist/schemas/index.js +406 -191
- package/dist/services/deployment.js +69 -13
- package/dist/services/salesforce.js +1262 -478
- package/dist/services/tooling.js +46 -21
- package/dist/tools/deployment.js +9 -13
- package/dist/tools/metadata.js +1 -0
- package/dist/tools/ui.js +57 -16
- package/package.json +1 -1
package/dist/services/tooling.js
CHANGED
|
@@ -122,11 +122,24 @@ export async function addComponentsToChangeSet(auth, changeSetName, components)
|
|
|
122
122
|
}
|
|
123
123
|
export async function createScheduledJob(auth, className, jobName, cronExpression) {
|
|
124
124
|
try {
|
|
125
|
-
const apexCode = `System.schedule('${jobName.replace(/'/g, "\\'")}', '${cronExpression.replace(/'/g, "\\'")}', new ${className}());`;
|
|
126
125
|
const client = createClient(auth);
|
|
126
|
+
// Check if the schedulable class exists; create a minimal stub if not
|
|
127
|
+
const classCheck = await client.get(`/tooling/query?q=${encodeURIComponent(`SELECT Id FROM ApexClass WHERE Name='${className.replace(/'/g, "\\'")}'`)}`);
|
|
128
|
+
if (!classCheck.data.records?.length) {
|
|
129
|
+
const classBody = `public class ${className} implements Schedulable {\n public void execute(SchedulableContext sc) {\n // scheduled job placeholder\n }\n}`;
|
|
130
|
+
const classResp = await client.post("/tooling/sobjects/ApexClass", { Name: className, Body: classBody, ApiVersion: parseFloat("66.0") });
|
|
131
|
+
if (!classResp.data?.id && !classResp.data?.Id) {
|
|
132
|
+
return { success: false, message: `Class '${className}' not found and could not be created.` };
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const apexCode = `System.schedule('${jobName.replace(/'/g, "\\'")}', '${cronExpression.replace(/'/g, "\\'")}', new ${className}());`;
|
|
127
136
|
const resp = await client.get(`/tooling/executeAnonymous?anonymousBody=${encodeURIComponent(apexCode)}`);
|
|
128
137
|
if (!resp.data.compiled || !resp.data.success) {
|
|
129
|
-
|
|
138
|
+
const errMsg = resp.data.exceptionMessage ?? resp.data.compileProblem ?? "Failed to schedule job";
|
|
139
|
+
if (errMsg.toLowerCase().includes("already scheduled")) {
|
|
140
|
+
return { success: true, fullName: jobName, created: false, message: `Scheduled job '${jobName}' already exists.` };
|
|
141
|
+
}
|
|
142
|
+
return { success: false, message: errMsg };
|
|
130
143
|
}
|
|
131
144
|
return {
|
|
132
145
|
success: true, fullName: jobName, created: true,
|
|
@@ -141,33 +154,45 @@ export async function createScheduledJob(auth, className, jobName, cronExpressio
|
|
|
141
154
|
export async function createApexEmailService(auth, params) {
|
|
142
155
|
try {
|
|
143
156
|
const client = createClient(auth);
|
|
157
|
+
// Look up the Apex class ID; create a stub class if it doesn't exist (ApexClassId is required)
|
|
158
|
+
const className = params.apexClassName ?? params.className;
|
|
159
|
+
let apexClassId = null;
|
|
160
|
+
if (className) {
|
|
161
|
+
const classResp = await client.get(`/tooling/query?q=${encodeURIComponent(`SELECT Id FROM ApexClass WHERE Name='${className.replace(/'/g, "\\'")}'`)}`).catch(() => null);
|
|
162
|
+
apexClassId = classResp?.data?.records?.[0]?.Id ?? null;
|
|
163
|
+
if (!apexClassId) {
|
|
164
|
+
// Create a minimal stub class implementing InboundEmailHandler
|
|
165
|
+
const classBody = `global class ${className} implements Messaging.InboundEmailHandler {\n global Messaging.InboundEmailResult handleInboundEmail(Messaging.InboundEmail email, Messaging.InboundEnvelope envelope) {\n return new Messaging.InboundEmailResult();\n }\n}`;
|
|
166
|
+
const createResp = await client.post("/tooling/sobjects/ApexClass", { Name: className, Body: classBody, ApiVersion: parseFloat("66.0") }).catch(() => null);
|
|
167
|
+
apexClassId = createResp?.data?.id ?? createResp?.data?.Id ?? null;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// EmailServicesFunction SObject fields (no FunctionExceptionAction — that field doesn't exist)
|
|
144
171
|
const body = {
|
|
145
|
-
ApexClassId: null,
|
|
146
|
-
AttachmentOption: params.attachmentOption,
|
|
147
|
-
AuthenticationFailureAction: params.authenticationFailureAction,
|
|
148
|
-
FunctionExceptionAction: params.functionExceptionAction,
|
|
149
|
-
FunctionInactiveAction: params.functionInactiveAction,
|
|
150
|
-
IsActive: params.isActive,
|
|
151
|
-
IsAuthenticationRequired: params.isAuthenticationRequired,
|
|
152
|
-
IsErrorRoutingEnabled: params.isErrorRoutingEnabled,
|
|
153
|
-
OverLimitAction: params.overLimitAction,
|
|
154
|
-
ErrorRoutingAddress: params.errorRoutingAddress ?? null,
|
|
155
172
|
FunctionName: params.functionName,
|
|
173
|
+
...(apexClassId ? { ApexClassId: apexClassId } : {}),
|
|
174
|
+
AttachmentOption: params.attachmentOption ?? "None",
|
|
175
|
+
AuthenticationFailureAction: params.authenticationFailureAction ?? "UseDefaultResponse",
|
|
176
|
+
FunctionInactiveAction: params.functionInactiveAction ?? "UseDefaultResponse",
|
|
177
|
+
IsActive: params.isActive !== false,
|
|
178
|
+
IsAuthenticationRequired: params.isAuthenticationRequired ?? false,
|
|
179
|
+
IsErrorRoutingEnabled: params.isErrorRoutingEnabled ?? false,
|
|
180
|
+
OverLimitAction: params.overLimitAction ?? "UseDefaultResponse",
|
|
181
|
+
...(params.errorRoutingAddress ? { ErrorRoutingAddress: params.errorRoutingAddress } : {}),
|
|
156
182
|
};
|
|
157
|
-
|
|
158
|
-
const
|
|
159
|
-
if (classResp.data.records.length) {
|
|
160
|
-
body["ApexClassId"] = classResp.data.records[0].Id;
|
|
161
|
-
}
|
|
162
|
-
const resp = await client.post("/tooling/sobjects/ApexEmailNotification", body);
|
|
163
|
-
const id = resp.data?.Id ?? resp.data?.id;
|
|
183
|
+
const resp = await client.post("/sobjects/EmailServicesFunction", body);
|
|
184
|
+
const id = resp.data?.id ?? resp.data?.Id;
|
|
164
185
|
return {
|
|
165
186
|
success: true, fullName: params.functionName, created: true,
|
|
166
|
-
message: `Apex Email Service '${params.functionName}' created${id ? ` with ID: ${id}` : ""}.`
|
|
187
|
+
message: `Apex Email Service '${params.functionName}' created${id ? ` with ID: ${id}` : ""}. Add an address via Setup → Email Services.`
|
|
167
188
|
};
|
|
168
189
|
}
|
|
169
190
|
catch (err) {
|
|
170
|
-
|
|
191
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
192
|
+
if (msg.includes("DUPLICATE_VALUE") || msg.toLowerCase().includes("duplicates an existing")) {
|
|
193
|
+
return { success: true, fullName: params.functionName, created: false, message: `Apex Email Service '${params.functionName}' already exists.` };
|
|
194
|
+
}
|
|
195
|
+
return { success: false, message: msg };
|
|
171
196
|
}
|
|
172
197
|
}
|
|
173
198
|
//# sourceMappingURL=tooling.js.map
|
package/dist/tools/deployment.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { CreateOutboundChangeSetSchema, AddToChangeSetSchema, DeployMetadataSchema, CheckDeployStatusSchema, RetrieveMetadataSchema, ValidateDeploymentSchema, ListMetadataSchema, CompareOrgsSchema, RollbackDeploymentSchema, CreateScratchOrgSchema, CreateSandboxSchema, RefreshSandboxSchema, ExportPackageXmlSchema, } from "../schemas/index.js";
|
|
2
2
|
import { getAuth, listMetadataForTool, compareOrgs, rollbackDeployment, createScratchOrg, createSandbox, refreshSandbox, exportPackageXml, } from "../services/salesforce.js";
|
|
3
|
-
import { buildGenericDeployZip, deployZip, pollDeployStatus, checkDeployStatus, retrieveMetadata, } from "../services/deployment.js";
|
|
3
|
+
import { buildGenericDeployZip, deployZip, pollDeployStatus, checkDeployStatus, retrieveMetadata, retrieveAndDeploy, } from "../services/deployment.js";
|
|
4
4
|
import { createOutboundChangeSet, addComponentsToChangeSet, } from "../services/tooling.js";
|
|
5
5
|
import { resultContent } from "./utils.js";
|
|
6
6
|
export function registerDeploymentTools(server) {
|
|
@@ -33,24 +33,22 @@ export function registerDeploymentTools(server) {
|
|
|
33
33
|
});
|
|
34
34
|
server.registerTool("sf_deploy_metadata", {
|
|
35
35
|
title: "Deploy Metadata",
|
|
36
|
-
description: `Deploys a set of metadata components directly to the org using the Metadata API SOAP deploy operation.
|
|
36
|
+
description: `Deploys a set of metadata components directly to the org using the Metadata API SOAP deploy operation. Retrieves the current metadata from the org, then deploys it. Supports validate-only (checkOnly:true) for pre-deployment validation without making changes. Specify runTests to execute test classes during deployment (required for production). Set testLevel to RunSpecifiedTests, RunLocalTests, or RunAllTestsInOrg. Polls until complete or timeout.`,
|
|
37
37
|
inputSchema: DeployMetadataSchema,
|
|
38
38
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
39
39
|
}, async (params) => {
|
|
40
40
|
const auth = await getAuth();
|
|
41
41
|
try {
|
|
42
|
-
const
|
|
43
|
-
const deployId = await deployZip(auth, base64Zip, {
|
|
42
|
+
const result = await retrieveAndDeploy(auth, params.components, {
|
|
44
43
|
checkOnly: params.checkOnly,
|
|
45
44
|
runTests: params.runTests,
|
|
46
45
|
rollbackOnError: params.rollbackOnError,
|
|
47
46
|
testLevel: params.testLevel,
|
|
48
|
-
});
|
|
49
|
-
const result = await pollDeployStatus(auth, deployId, params.waitMinutes * 60 * 1000);
|
|
47
|
+
}, params.waitMinutes);
|
|
50
48
|
return resultContent({
|
|
51
49
|
success: result.success,
|
|
52
50
|
message: `${params.checkOnly ? "[VALIDATE ONLY] " : ""}${result.message}`,
|
|
53
|
-
...(result.success ? { fullName:
|
|
51
|
+
...(result.success ? { fullName: result.message, created: !params.checkOnly } : {})
|
|
54
52
|
});
|
|
55
53
|
}
|
|
56
54
|
catch (err) {
|
|
@@ -88,24 +86,22 @@ export function registerDeploymentTools(server) {
|
|
|
88
86
|
});
|
|
89
87
|
server.registerTool("sf_validate_deployment", {
|
|
90
88
|
title: "Validate Deployment (Check-Only)",
|
|
91
|
-
description: `Runs a check-only deployment that validates metadata components against the org without making any changes.
|
|
89
|
+
description: `Runs a check-only deployment that validates metadata components against the org without making any changes. Retrieves the current metadata from the org, then validates it. Use before deploying to production to catch errors and test failures without risk. Specify testLevel to control which tests run during validation.`,
|
|
92
90
|
inputSchema: ValidateDeploymentSchema,
|
|
93
91
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
94
92
|
}, async (params) => {
|
|
95
93
|
const auth = await getAuth();
|
|
96
94
|
try {
|
|
97
|
-
const
|
|
98
|
-
const deployId = await deployZip(auth, base64Zip, {
|
|
95
|
+
const result = await retrieveAndDeploy(auth, params.components, {
|
|
99
96
|
checkOnly: true,
|
|
100
97
|
runTests: params.runTests,
|
|
101
98
|
rollbackOnError: true,
|
|
102
99
|
testLevel: params.testLevel,
|
|
103
|
-
});
|
|
104
|
-
const result = await pollDeployStatus(auth, deployId, (params.waitMinutes ?? 10) * 60 * 1000);
|
|
100
|
+
}, params.waitMinutes ?? 10);
|
|
105
101
|
return resultContent({
|
|
106
102
|
success: result.success,
|
|
107
103
|
message: `[VALIDATE ONLY] ${result.message}`,
|
|
108
|
-
...(result.success ? { fullName:
|
|
104
|
+
...(result.success ? { fullName: result.message, created: false } : {})
|
|
109
105
|
});
|
|
110
106
|
} catch (err) {
|
|
111
107
|
return resultContent({ success: false, message: err instanceof Error ? err.message : String(err) });
|
package/dist/tools/metadata.js
CHANGED
|
@@ -98,6 +98,7 @@ export function registerMetadataTools(server) {
|
|
|
98
98
|
elements: params.elements,
|
|
99
99
|
variables: params.variables,
|
|
100
100
|
status: params.status,
|
|
101
|
+
runInMode: params.runInMode,
|
|
101
102
|
submitForApprovalProcessName: params.submitForApprovalProcessName,
|
|
102
103
|
});
|
|
103
104
|
return resultContent(result);
|
package/dist/tools/ui.js
CHANGED
|
@@ -150,6 +150,7 @@ export function registerUiTools(server) {
|
|
|
150
150
|
label: params.label,
|
|
151
151
|
description: params.description,
|
|
152
152
|
baseObject: params.baseObject,
|
|
153
|
+
columns: params.columns,
|
|
153
154
|
category: params.category,
|
|
154
155
|
deployed: params.deployed,
|
|
155
156
|
relationships: params.relationships,
|
|
@@ -163,31 +164,71 @@ export function registerUiTools(server) {
|
|
|
163
164
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
164
165
|
}, async (params) => {
|
|
165
166
|
const auth = await getAuth();
|
|
166
|
-
|
|
167
|
-
const
|
|
167
|
+
const { upsertMetadata, createClient } = await import("../services/salesforce.js");
|
|
168
|
+
const x2 = (s) => String(s ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
169
|
+
// Helper: resolve report name to full folder/name path via Tooling API
|
|
170
|
+
const resolveReportName = async (name) => {
|
|
171
|
+
if (name.includes("/")) return name;
|
|
172
|
+
try {
|
|
173
|
+
const client = createClient(auth);
|
|
174
|
+
const resp = await client.get(`/tooling/query?q=${encodeURIComponent(`SELECT DeveloperName, Folder.DeveloperName FROM Report WHERE DeveloperName = '${name}'`)}`);
|
|
175
|
+
const rec = resp.data?.records?.[0];
|
|
176
|
+
const folder = rec?.Folder?.DeveloperName ?? rec?.FolderName;
|
|
177
|
+
if (folder) return `${folder}/${name}`;
|
|
178
|
+
} catch {}
|
|
179
|
+
// Fallback: use unfiled$public which is the default folder for createReport
|
|
180
|
+
return `unfiled$public/${name}`;
|
|
181
|
+
};
|
|
182
|
+
// Dashboard must be in a folder; auto-create DashboardFolder when none specified
|
|
183
|
+
const rawName = params.fullName ?? params.dashboardName;
|
|
184
|
+
const folderLabel = params.folderName ?? "Dashboards";
|
|
185
|
+
if (!rawName.includes("/") && !params.folderName) {
|
|
186
|
+
// Ensure the default dashboard folder exists
|
|
187
|
+
await upsertMetadata(auth, `<met:metadata xsi:type="met:DashboardFolder" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
|
188
|
+
<met:accessType>Shared</met:accessType>
|
|
189
|
+
<met:fullName>${x2(folderLabel)}</met:fullName>
|
|
190
|
+
<met:name>${x2(folderLabel)}</met:name>
|
|
191
|
+
</met:metadata>`);
|
|
192
|
+
}
|
|
193
|
+
const fullName = rawName.includes("/") ? rawName : `${params.folderName ?? folderLabel}/${rawName}`;
|
|
194
|
+
const x = (s) => String(s ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
195
|
+
const chartTypes = new Set(["Bar", "Column", "Line", "Donut", "Funnel", "Pie", "Scatter"]);
|
|
196
|
+
const resolvedComponents = await Promise.all((params.components ?? []).map(async (c) => {
|
|
197
|
+
const ctype = c.componentType ?? c.type ?? "Bar";
|
|
198
|
+
const rawReport = c.reportName ?? c.reportApiName ?? "";
|
|
199
|
+
const report = rawReport ? await resolveReportName(rawReport) : "";
|
|
200
|
+
const header = c.header ?? c.title ?? "";
|
|
201
|
+
const sortBy = c.sortBy ?? "RowLabelAscending";
|
|
202
|
+
const needsSortBy = chartTypes.has(ctype);
|
|
203
|
+
// WSDL element order for DashboardComponent: componentType, header, report, sortBy
|
|
204
|
+
return `
|
|
168
205
|
<met:components>
|
|
169
|
-
<met:
|
|
170
|
-
${
|
|
171
|
-
${
|
|
172
|
-
${
|
|
173
|
-
${
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
206
|
+
<met:componentType>${x(ctype)}</met:componentType>
|
|
207
|
+
${header ? `<met:header>${x(header)}</met:header>` : ""}
|
|
208
|
+
${report ? `<met:report>${x(report)}</met:report>` : ""}
|
|
209
|
+
${needsSortBy ? `<met:chartAxisRange>Auto</met:chartAxisRange>` : ""}
|
|
210
|
+
${needsSortBy ? `<met:sortBy>${x(sortBy)}</met:sortBy>` : ""}
|
|
211
|
+
</met:components>`;
|
|
212
|
+
}));
|
|
213
|
+
const componentsXml = resolvedComponents.join("\n");
|
|
214
|
+
// WSDL element order for Dashboard: backgroundEndColor, backgroundFadeDirection, backgroundStartColor,
|
|
215
|
+
// dashboardType, description, fullName, leftSection, middleSection, rightSection, runningUser, textColor, title, titleColor, titleSize
|
|
177
216
|
const xml = `<met:metadata xsi:type="met:Dashboard" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
|
178
|
-
<met:fullName>${params.fullName}</met:fullName>
|
|
179
|
-
<met:title>${params.title}</met:title>
|
|
180
|
-
${params.description ? `<met:description>${params.description}</met:description>` : ""}
|
|
181
|
-
${params.runningUser ? `<met:runningUser>${params.runningUser}</met:runningUser>` : ""}
|
|
182
217
|
<met:backgroundEndColor>#FFFFFF</met:backgroundEndColor>
|
|
183
218
|
<met:backgroundFadeDirection>Diagonal</met:backgroundFadeDirection>
|
|
184
219
|
<met:backgroundStartColor>#FFFFFF</met:backgroundStartColor>
|
|
220
|
+
<met:dashboardType>SpecifiedUser</met:dashboardType>
|
|
221
|
+
${params.description ? `<met:description>${x(params.description)}</met:description>` : ""}
|
|
222
|
+
<met:fullName>${x(fullName)}</met:fullName>
|
|
223
|
+
<met:leftSection><met:columnSize>Medium</met:columnSize>${componentsXml}</met:leftSection>
|
|
224
|
+
<met:middleSection><met:columnSize>Medium</met:columnSize></met:middleSection>
|
|
225
|
+
<met:rightSection><met:columnSize>Medium</met:columnSize></met:rightSection>
|
|
226
|
+
${params.runningUser ? `<met:runningUser>${x(params.runningUser)}</met:runningUser>` : ""}
|
|
185
227
|
<met:textColor>#000000</met:textColor>
|
|
228
|
+
<met:title>${x(params.title)}</met:title>
|
|
186
229
|
<met:titleColor>#000000</met:titleColor>
|
|
187
230
|
<met:titleSize>12</met:titleSize>
|
|
188
|
-
${componentsXml}
|
|
189
231
|
</met:metadata>`;
|
|
190
|
-
const { upsertMetadata } = await import("../services/salesforce.js");
|
|
191
232
|
const result = await upsertMetadata(auth, xml);
|
|
192
233
|
return resultContent(result);
|
|
193
234
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "salesforce-metadata-mcp",
|
|
3
|
-
"version": "2.5.
|
|
3
|
+
"version": "2.5.3",
|
|
4
4
|
"description": "The most comprehensive Salesforce MCP server: 200+ tools covering custom objects, flows, Apex, LWC, page layouts, permissions, profiles, users, reports, dashboards, Experience Cloud, OmniStudio, OmniChannel, Agentforce, Einstein AI, audit trail, territories, sandboxes, CDC, REST resources, document generation, and more — everything a Salesforce admin, advanced admin and developer needs.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"private": false,
|