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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import JSZip from "jszip";
|
|
2
2
|
import { API_VERSION, x, fetchWithTimeout, sanitizeError } from "./salesforce.js";
|
|
3
3
|
// ─── Zip-based deploy helpers ─────────────────────────────────────────────────
|
|
4
|
-
function buildPackageXml(types, apiVersion) {
|
|
4
|
+
export function buildPackageXml(types, apiVersion) {
|
|
5
5
|
const typesXml = types.map(t => `
|
|
6
6
|
<types>
|
|
7
7
|
${t.members.map(m => `<members>${m}</members>`).join("\n ")}
|
|
@@ -59,8 +59,9 @@ export async function buildStaticResourceZip(resourceName, content, contentType,
|
|
|
59
59
|
const zip = new JSZip();
|
|
60
60
|
const packageXml = buildPackageXml([{ name: "StaticResource", members: [resourceName] }], apiVersion);
|
|
61
61
|
zip.file("package.xml", packageXml);
|
|
62
|
-
|
|
63
|
-
zip.file(`staticresources/${resourceName}.resource
|
|
62
|
+
// Content must be in the .resource file; do NOT embed in the meta XML
|
|
63
|
+
zip.file(`staticresources/${resourceName}.resource`, content);
|
|
64
|
+
zip.file(`staticresources/${resourceName}.resource-meta.xml`, `<?xml version="1.0" encoding="UTF-8"?>\n<StaticResource xmlns="http://soap.sforce.com/2006/04/metadata">\n <cacheControl>${contentType === "text/plain" ? "Public" : "Private"}</cacheControl>\n <contentType>${contentType}</contentType>\n</StaticResource>`);
|
|
64
65
|
const buffer = await zip.generateAsync({ type: "nodebuffer" });
|
|
65
66
|
return buffer.toString("base64");
|
|
66
67
|
}
|
|
@@ -89,15 +90,16 @@ export async function buildGenAiActionZip(params, apiVersion) {
|
|
|
89
90
|
const isApex = params.invocationTargetType === "ApexClass";
|
|
90
91
|
const invocationTarget = isApex ? (params.apexClassName ?? "") : (params.flowApiName ?? "");
|
|
91
92
|
const invocationTargetType = isApex ? "apexClass" : "flow";
|
|
93
|
+
// WSDL alphabetical order: description, invocationTarget, invocationTargetType, isConfirmationRequired, mappingAttributes, masterLabel
|
|
92
94
|
const functionXml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
93
95
|
<GenAiFunction xmlns="http://soap.sforce.com/2006/04/metadata">
|
|
94
|
-
<description>${escapeXml(params.description)}</description>
|
|
96
|
+
<description>${escapeXml(params.description ?? "")}</description>
|
|
95
97
|
<invocationTarget>${escapeXml(invocationTarget)}</invocationTarget>
|
|
96
98
|
<invocationTargetType>${invocationTargetType}</invocationTargetType>
|
|
97
99
|
<isConfirmationRequired>false</isConfirmationRequired>
|
|
98
|
-
<masterLabel>${escapeXml(params.label)}</masterLabel>
|
|
99
100
|
${inputsXml}
|
|
100
101
|
${outputsXml}
|
|
102
|
+
<masterLabel>${escapeXml(params.label)}</masterLabel>
|
|
101
103
|
</GenAiFunction>`;
|
|
102
104
|
zip.file(`genAiFunctions/${functionName}/${functionName}.genAiFunction-meta.xml`, functionXml);
|
|
103
105
|
// Build input schema JSON
|
|
@@ -219,7 +221,7 @@ export async function deployZip(auth, base64Zip, options = {}) {
|
|
|
219
221
|
${options.testLevel && options.testLevel !== "RunAllTestsInOrg" ? `<met:testLevel>${options.testLevel}</met:testLevel>` : ""}
|
|
220
222
|
${testsXml}
|
|
221
223
|
<met:rollbackOnError>${options.rollbackOnError !== false}</met:rollbackOnError>
|
|
222
|
-
<met:singlePackage
|
|
224
|
+
<met:singlePackage>${options.singlePackage !== false}</met:singlePackage>
|
|
223
225
|
</met:DeployOptions>
|
|
224
226
|
</met:deploy>
|
|
225
227
|
</soapenv:Body>
|
|
@@ -294,18 +296,15 @@ export async function checkDeployStatus(auth, deployId) {
|
|
|
294
296
|
return { done, success, status, message };
|
|
295
297
|
}
|
|
296
298
|
// ─── Retrieve metadata ────────────────────────────────────────────────────────
|
|
297
|
-
|
|
299
|
+
function buildRetrieveEnvelope(auth, components) {
|
|
298
300
|
const typeMap = new Map();
|
|
299
301
|
for (const c of components) {
|
|
300
302
|
const existing = typeMap.get(c.type) ?? [];
|
|
301
303
|
existing.push(c.name);
|
|
302
304
|
typeMap.set(c.type, existing);
|
|
303
305
|
}
|
|
304
|
-
const typesXml = [...typeMap.entries()].map(([name, members]) =>
|
|
305
|
-
|
|
306
|
-
${members.map(m => `<met:types><met:members>${m}</met:members><met:name>${name}</met:name></met:types>`).join("\n")}
|
|
307
|
-
</met:unpackaged>`).join("\n");
|
|
308
|
-
const envelope = `<?xml version="1.0" encoding="utf-8"?>
|
|
306
|
+
const typesXml = [...typeMap.entries()].map(([name, members]) => `<types>${members.map(m => `<members>${m}</members>`).join("")}<name>${name}</name></types>`).join("");
|
|
307
|
+
return `<?xml version="1.0" encoding="utf-8"?>
|
|
309
308
|
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:met="http://soap.sforce.com/2006/04/metadata">
|
|
310
309
|
<soapenv:Header>
|
|
311
310
|
<met:CallOptions><met:client>salesforce-metadata-mcp</met:client></met:CallOptions>
|
|
@@ -315,12 +314,18 @@ export async function retrieveMetadata(auth, components) {
|
|
|
315
314
|
<met:retrieve>
|
|
316
315
|
<met:retrieveRequest>
|
|
317
316
|
<met:apiVersion>${API_VERSION}</met:apiVersion>
|
|
318
|
-
|
|
317
|
+
<met:unpackaged>${typesXml}</met:unpackaged>
|
|
319
318
|
</met:retrieveRequest>
|
|
320
319
|
</met:retrieve>
|
|
321
320
|
</soapenv:Body>
|
|
322
321
|
</soapenv:Envelope>`;
|
|
322
|
+
}
|
|
323
|
+
export async function retrieveMetadata(auth, components) {
|
|
324
|
+
if (!components || !Array.isArray(components) || components.length === 0) {
|
|
325
|
+
return { success: false, message: "No components specified for retrieval. Provide components array, e.g. [{type:'CustomObject',name:'MyObj__c'}]." };
|
|
326
|
+
}
|
|
323
327
|
try {
|
|
328
|
+
const envelope = buildRetrieveEnvelope(auth, components);
|
|
324
329
|
const response = await fetchWithTimeout(`${auth.instanceUrl}/services/Soap/m/${API_VERSION}`, { method: "POST", headers: { "Content-Type": "text/xml; charset=utf-8", SOAPAction: '"retrieve"' }, body: envelope }, 60_000);
|
|
325
330
|
if (!response.ok)
|
|
326
331
|
throw new Error(`Retrieve SOAP request failed with HTTP ${response.status}.`);
|
|
@@ -339,4 +344,55 @@ export async function retrieveMetadata(auth, components) {
|
|
|
339
344
|
return { success: false, message: sanitizeError(err instanceof Error ? err.message : String(err)) };
|
|
340
345
|
}
|
|
341
346
|
}
|
|
347
|
+
// ─── Poll retrieve status and return zip ─────────────────────────────────────
|
|
348
|
+
export async function pollRetrieveAndGetZip(auth, retrieveId, maxWaitMs = 3 * 60 * 1000) {
|
|
349
|
+
const start = Date.now();
|
|
350
|
+
while (Date.now() - start < maxWaitMs) {
|
|
351
|
+
await new Promise(r => setTimeout(r, 5_000));
|
|
352
|
+
const envelope = `<?xml version="1.0" encoding="utf-8"?>
|
|
353
|
+
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:met="http://soap.sforce.com/2006/04/metadata">
|
|
354
|
+
<soapenv:Header>
|
|
355
|
+
<met:CallOptions><met:client>salesforce-metadata-mcp</met:client></met:CallOptions>
|
|
356
|
+
<met:SessionHeader><met:sessionId>${auth.accessToken}</met:sessionId></met:SessionHeader>
|
|
357
|
+
</soapenv:Header>
|
|
358
|
+
<soapenv:Body>
|
|
359
|
+
<met:checkRetrieveStatus>
|
|
360
|
+
<met:asyncProcessId>${retrieveId}</met:asyncProcessId>
|
|
361
|
+
<met:includeZip>true</met:includeZip>
|
|
362
|
+
</met:checkRetrieveStatus>
|
|
363
|
+
</soapenv:Body>
|
|
364
|
+
</soapenv:Envelope>`;
|
|
365
|
+
const resp = await fetchWithTimeout(`${auth.instanceUrl}/services/Soap/m/${API_VERSION}`, { method: "POST", headers: { "Content-Type": "text/xml; charset=utf-8", SOAPAction: '"checkRetrieveStatus"' }, body: envelope }, 60_000);
|
|
366
|
+
if (!resp.ok) throw new Error(`checkRetrieveStatus HTTP ${resp.status}`);
|
|
367
|
+
const xml = await resp.text();
|
|
368
|
+
const doneMatch = xml.match(/<done>(true|false)<\/done>/i);
|
|
369
|
+
if (doneMatch?.[1] !== "true") continue;
|
|
370
|
+
const successMatch = xml.match(/<success>(true|false)<\/success>/i);
|
|
371
|
+
if (successMatch?.[1] !== "true") {
|
|
372
|
+
const errMsg = xml.match(/<errorMessage[^>]*>([\s\S]*?)<\/errorMessage>/i);
|
|
373
|
+
throw new Error(errMsg ? errMsg[1].trim() : "Retrieve failed");
|
|
374
|
+
}
|
|
375
|
+
const zipMatch = xml.match(/<zipFile[^>]*>([\s\S]*?)<\/zipFile>/i);
|
|
376
|
+
if (!zipMatch) throw new Error("Retrieve completed but no zip in response");
|
|
377
|
+
return zipMatch[1].replace(/\s/g, "");
|
|
378
|
+
}
|
|
379
|
+
throw new Error(`Retrieve timed out after ${maxWaitMs / 1000}s`);
|
|
380
|
+
}
|
|
381
|
+
// ─── Retrieve then deploy (for validate/deploy of existing org components) ───
|
|
382
|
+
export async function retrieveAndDeploy(auth, components, deployOptions, waitMinutes = 10) {
|
|
383
|
+
const envelope = buildRetrieveEnvelope(auth, components);
|
|
384
|
+
const response = await fetchWithTimeout(`${auth.instanceUrl}/services/Soap/m/${API_VERSION}`, { method: "POST", headers: { "Content-Type": "text/xml; charset=utf-8", SOAPAction: '"retrieve"' }, body: envelope }, 60_000);
|
|
385
|
+
if (!response.ok) throw new Error(`Retrieve SOAP request failed with HTTP ${response.status}.`);
|
|
386
|
+
const xml = await response.text();
|
|
387
|
+
const idMatch = xml.match(/<id[^>]*>([A-Za-z0-9]{15,18})<\/id>/);
|
|
388
|
+
if (!idMatch) {
|
|
389
|
+
const fault = xml.match(/<faultstring[^>]*>([\s\S]*?)<\/faultstring>/i);
|
|
390
|
+
throw new Error(fault ? fault[1].trim() : "Retrieve failed to start");
|
|
391
|
+
}
|
|
392
|
+
const retrieveId = idMatch[1];
|
|
393
|
+
const base64Zip = await pollRetrieveAndGetZip(auth, retrieveId, waitMinutes * 30 * 1000);
|
|
394
|
+
// Retrieved zips use unpackaged/ folder structure, so singlePackage must be false
|
|
395
|
+
const deployId = await deployZip(auth, base64Zip, { ...deployOptions, singlePackage: false });
|
|
396
|
+
return await pollDeployStatus(auth, deployId, waitMinutes * 60 * 1000);
|
|
397
|
+
}
|
|
342
398
|
//# sourceMappingURL=deployment.js.map
|