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.
@@ -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
- return { success: false, message: resp.data.exceptionMessage ?? "Failed to schedule job" };
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
- // First get the Apex class Id
158
- const classResp = await client.get(`/tooling/query?q=${encodeURIComponent(`SELECT Id FROM ApexClass WHERE Name='${params.apexClassName}'`)}`);
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
- return { success: false, message: err instanceof Error ? err.message : String(err) };
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
@@ -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. Builds a package.xml and deployment zip in memory. 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.`,
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 base64Zip = await buildGenericDeployZip(params.components, "66.0");
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: deployId, created: true } : {})
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. Equivalent to sf_deploy_metadata with checkOnly:true. Use before deploying to production to catch errors and test failures without risk. Specify testLevel to control which tests run during validation.`,
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 base64Zip = await buildGenericDeployZip(params.components, "66.0");
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: deployId, created: false } : {})
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) });
@@ -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
- // Build dashboard XML inline
167
- const componentsXml = (params.components ?? []).map((c, i) => `
167
+ const { upsertMetadata, createClient } = await import("../services/salesforce.js");
168
+ const x2 = (s) => String(s ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
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:type>${c.type}</met:type>
170
- ${c.reportApiName ? `<met:report>${c.reportApiName}</met:report>` : ""}
171
- ${c.header ? `<met:header>${c.header}</met:header>` : ""}
172
- ${c.footer ? `<met:footer>${c.footer}</met:footer>` : ""}
173
- ${c.chartType ? `<met:chartAxisRange>Auto</met:chartAxisRange>` : ""}
174
- <met:columnSpan>${c.columnSpan}</met:columnSpan>
175
- <met:rowSpan>${c.rowSpan}</met:rowSpan>
176
- </met:components>`).join("\n");
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.2",
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,