@yashwant.dharmdas/elementor-mcp 3.2.7 → 3.3.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/dist/index.js +534 -44
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1092,15 +1092,16 @@ function createMcpServer(sites) {
|
|
|
1092
1092
|
await page.goto(pageUrl, { waitUntil: "networkidle2", timeout: 45_000 });
|
|
1093
1093
|
// ── Auto-scroll with multi-pass stabilization ────────────────────
|
|
1094
1094
|
// Many Elementor pages lazy-load content on scroll (images, Swiper
|
|
1095
|
-
// carousels, reveal animations).
|
|
1096
|
-
//
|
|
1097
|
-
// total page height stops increasing
|
|
1095
|
+
// carousels, reveal animations). We scroll slowly so IntersectionObservers
|
|
1096
|
+
// have time to fire, then wait at the bottom for content to settle, and
|
|
1097
|
+
// repeat passes until the total page height stops increasing.
|
|
1098
1098
|
if (auto_scroll !== false) {
|
|
1099
1099
|
await page.evaluate(async () => {
|
|
1100
1100
|
const getHeight = () => Math.max(document.body.scrollHeight, document.documentElement.scrollHeight, document.body.offsetHeight, document.documentElement.offsetHeight);
|
|
1101
1101
|
const scrollOnce = () => new Promise((resolve) => {
|
|
1102
1102
|
let y = 0;
|
|
1103
|
-
const step =
|
|
1103
|
+
const step = 180; // smaller step
|
|
1104
|
+
const delay = 110; // slower interval — gives observers time to fire
|
|
1104
1105
|
const timer = setInterval(() => {
|
|
1105
1106
|
const h = getHeight();
|
|
1106
1107
|
window.scrollBy(0, step);
|
|
@@ -1109,89 +1110,166 @@ function createMcpServer(sites) {
|
|
|
1109
1110
|
clearInterval(timer);
|
|
1110
1111
|
resolve();
|
|
1111
1112
|
}
|
|
1112
|
-
},
|
|
1113
|
+
}, delay);
|
|
1113
1114
|
});
|
|
1114
1115
|
// Repeat until height stabilizes (max 4 passes)
|
|
1115
1116
|
let lastHeight = 0;
|
|
1116
1117
|
for (let i = 0; i < 4; i++) {
|
|
1117
1118
|
window.scrollTo(0, 0);
|
|
1118
|
-
await new Promise(r => setTimeout(r,
|
|
1119
|
+
await new Promise(r => setTimeout(r, 200));
|
|
1119
1120
|
await scrollOnce();
|
|
1120
|
-
//
|
|
1121
|
-
await new Promise(r => setTimeout(r,
|
|
1121
|
+
// Long wait at the bottom — bg images / Swipers need time
|
|
1122
|
+
await new Promise(r => setTimeout(r, 1500));
|
|
1122
1123
|
const h = getHeight();
|
|
1123
|
-
if (h === lastHeight)
|
|
1124
|
+
if (h === lastHeight && i > 0)
|
|
1124
1125
|
break;
|
|
1125
1126
|
lastHeight = h;
|
|
1126
1127
|
}
|
|
1128
|
+
// One more slow scroll pass at half speed to nail any stragglers
|
|
1129
|
+
await new Promise((resolve) => {
|
|
1130
|
+
window.scrollTo(0, 0);
|
|
1131
|
+
let y = 0;
|
|
1132
|
+
const step = 120;
|
|
1133
|
+
const timer = setInterval(() => {
|
|
1134
|
+
const h = getHeight();
|
|
1135
|
+
window.scrollBy(0, step);
|
|
1136
|
+
y += step;
|
|
1137
|
+
if (y >= h) {
|
|
1138
|
+
clearInterval(timer);
|
|
1139
|
+
resolve();
|
|
1140
|
+
}
|
|
1141
|
+
}, 130);
|
|
1142
|
+
});
|
|
1143
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
1127
1144
|
// Back to top for the capture
|
|
1128
1145
|
window.scrollTo(0, 0);
|
|
1129
1146
|
});
|
|
1130
1147
|
}
|
|
1131
|
-
// ── Force-load all
|
|
1148
|
+
// ── Force-load all images, Elementor backgrounds, and reveal sections ─
|
|
1132
1149
|
await page.evaluate(async () => {
|
|
1133
|
-
// 1.
|
|
1150
|
+
// 1. Inject CSS that disables animations AND forces opacity:1
|
|
1151
|
+
// on Elementor's hidden reveal states. This MUST be first so
|
|
1152
|
+
// subsequent layout / render passes don't re-hide content.
|
|
1153
|
+
const style = document.createElement("style");
|
|
1154
|
+
style.id = "__mcp_screenshot_overrides__";
|
|
1155
|
+
style.textContent = `
|
|
1156
|
+
*, *::before, *::after {
|
|
1157
|
+
animation-duration: 0s !important;
|
|
1158
|
+
animation-delay: 0s !important;
|
|
1159
|
+
transition-duration: 0s !important;
|
|
1160
|
+
transition-delay: 0s !important;
|
|
1161
|
+
}
|
|
1162
|
+
.elementor-invisible,
|
|
1163
|
+
.elementor-element[data-settings*="animation"] {
|
|
1164
|
+
opacity: 1 !important;
|
|
1165
|
+
visibility: visible !important;
|
|
1166
|
+
transform: none !important;
|
|
1167
|
+
}
|
|
1168
|
+
/* Defeat any lazy-load opacity tricks */
|
|
1169
|
+
.elementor-element { opacity: 1 !important; }
|
|
1170
|
+
`;
|
|
1171
|
+
document.head.appendChild(style);
|
|
1172
|
+
// 2. Strip loading="lazy" on images
|
|
1134
1173
|
document.querySelectorAll('img[loading="lazy"]').forEach((img) => {
|
|
1135
1174
|
img.setAttribute("loading", "eager");
|
|
1136
1175
|
});
|
|
1137
|
-
//
|
|
1176
|
+
// 3. Resolve data-src → src for any deferred-load libraries
|
|
1138
1177
|
document.querySelectorAll("img[data-src]").forEach((img) => {
|
|
1139
1178
|
const ds = img.getAttribute("data-src");
|
|
1140
1179
|
if (ds && !img.getAttribute("src"))
|
|
1141
1180
|
img.setAttribute("src", ds);
|
|
1142
1181
|
});
|
|
1143
|
-
//
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1182
|
+
// 4. Resolve <source srcset> inside <picture> (just in case)
|
|
1183
|
+
document.querySelectorAll("source[data-srcset]").forEach((src) => {
|
|
1184
|
+
const ds = src.getAttribute("data-srcset");
|
|
1185
|
+
if (ds && !src.getAttribute("srcset"))
|
|
1186
|
+
src.setAttribute("srcset", ds);
|
|
1187
|
+
});
|
|
1188
|
+
// 5. Force-load Elementor's lazy background images.
|
|
1189
|
+
// Elementor uses MULTIPLE patterns over the years:
|
|
1190
|
+
// - class .elementor-element-bg-lazyload + CSS var --e-bg-lazyload
|
|
1191
|
+
// - data-bg / data-background-image
|
|
1192
|
+
// - data-settings JSON containing background_image
|
|
1193
|
+
// We try all of them.
|
|
1194
|
+
document.querySelectorAll(".elementor-element-bg-lazyload, [data-bg], [data-background-image], [data-settings]").forEach((el) => {
|
|
1195
|
+
let url = el.getAttribute("data-bg") ||
|
|
1196
|
+
el.getAttribute("data-background-image");
|
|
1197
|
+
// Try the CSS variable
|
|
1198
|
+
if (!url) {
|
|
1199
|
+
const v = getComputedStyle(el).getPropertyValue("--e-bg-lazyload").trim();
|
|
1200
|
+
if (v && v !== "none") {
|
|
1201
|
+
url = v.replace(/^url\(["']?|["']?\)$/g, "");
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
// Try parsing data-settings JSON
|
|
1205
|
+
if (!url) {
|
|
1206
|
+
try {
|
|
1207
|
+
const ds = el.getAttribute("data-settings");
|
|
1208
|
+
if (ds) {
|
|
1209
|
+
const obj = JSON.parse(ds);
|
|
1210
|
+
url = obj?.background_image?.url
|
|
1211
|
+
|| obj?._background_image?.url
|
|
1212
|
+
|| obj?.background_overlay_image?.url
|
|
1213
|
+
|| null;
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
catch { /* ignore */ }
|
|
1217
|
+
}
|
|
1218
|
+
if (url && url !== "none") {
|
|
1219
|
+
// Set inline so it overrides any external lazy CSS
|
|
1220
|
+
el.style.setProperty("background-image", `url("${url}")`, "important");
|
|
1221
|
+
el.style.setProperty("--e-bg-lazyload", "none");
|
|
1153
1222
|
}
|
|
1154
1223
|
el.classList.remove("elementor-element-bg-lazyload");
|
|
1155
1224
|
});
|
|
1156
|
-
//
|
|
1157
|
-
// hidden (opacity:0 / .elementor-invisible) waiting for scroll trigger
|
|
1225
|
+
// 6. Force-show Elementor reveal-animation elements
|
|
1158
1226
|
document.querySelectorAll(".elementor-invisible").forEach((el) => {
|
|
1159
1227
|
el.classList.remove("elementor-invisible");
|
|
1160
1228
|
el.style.opacity = "1";
|
|
1161
1229
|
el.style.visibility = "visible";
|
|
1162
1230
|
el.style.transform = "none";
|
|
1163
1231
|
});
|
|
1164
|
-
//
|
|
1165
|
-
//
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1232
|
+
// 7. Re-trigger Elementor frontend lazy-load if available.
|
|
1233
|
+
// Elementor exposes the global `elementorFrontend`; calling its
|
|
1234
|
+
// elementsHandler reinitialises lazy modules.
|
|
1235
|
+
try {
|
|
1236
|
+
const ef = window.elementorFrontend;
|
|
1237
|
+
if (ef?.elementsHandler?.runReadyTrigger) {
|
|
1238
|
+
document.querySelectorAll(".elementor-element").forEach((el) => {
|
|
1239
|
+
try {
|
|
1240
|
+
ef.elementsHandler.runReadyTrigger(el);
|
|
1241
|
+
}
|
|
1242
|
+
catch { /* ignore */ }
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
catch { /* ignore */ }
|
|
1247
|
+
// 8. Dispatch scroll/resize to wake any remaining IntersectionObservers
|
|
1179
1248
|
window.dispatchEvent(new Event("scroll"));
|
|
1180
1249
|
window.dispatchEvent(new Event("resize"));
|
|
1181
|
-
|
|
1250
|
+
document.dispatchEvent(new Event("DOMContentLoaded"));
|
|
1251
|
+
// 9. Wait for ALL <img> tags (now eager) to finish loading + decode
|
|
1182
1252
|
const images = Array.from(document.querySelectorAll("img"));
|
|
1183
1253
|
await Promise.all(images.map((img) => {
|
|
1184
|
-
if (img.complete && img.naturalWidth > 0)
|
|
1185
|
-
return Promise.resolve();
|
|
1254
|
+
if (img.complete && img.naturalWidth > 0) {
|
|
1255
|
+
return img.decode?.().catch(() => { }) ?? Promise.resolve();
|
|
1256
|
+
}
|
|
1186
1257
|
return new Promise((resolve) => {
|
|
1187
1258
|
img.addEventListener("load", () => resolve(), { once: true });
|
|
1188
1259
|
img.addEventListener("error", () => resolve(), { once: true });
|
|
1189
1260
|
setTimeout(() => resolve(), 5000);
|
|
1190
1261
|
});
|
|
1191
1262
|
}));
|
|
1263
|
+
// 10. Wait for fonts to be fully ready (avoids FOUT in screenshot)
|
|
1264
|
+
if (document.fonts?.ready) {
|
|
1265
|
+
try {
|
|
1266
|
+
await document.fonts.ready;
|
|
1267
|
+
}
|
|
1268
|
+
catch { /* ignore */ }
|
|
1269
|
+
}
|
|
1192
1270
|
});
|
|
1193
|
-
// Extra settle time
|
|
1194
|
-
await new Promise(resolve => setTimeout(resolve, wait ??
|
|
1271
|
+
// Extra settle time — bumped to 3500ms default to handle slow CDNs / videos
|
|
1272
|
+
await new Promise(resolve => setTimeout(resolve, wait ?? 3500));
|
|
1195
1273
|
const fmt = format ?? "jpeg";
|
|
1196
1274
|
const baseOptions = { type: fmt, fullPage: !max_height && (full_page ?? true) };
|
|
1197
1275
|
if (max_height)
|
|
@@ -1292,6 +1370,418 @@ function createMcpServer(sites) {
|
|
|
1292
1370
|
};
|
|
1293
1371
|
}
|
|
1294
1372
|
});
|
|
1373
|
+
// ── Group 10: Breakpoints ────────────────────────────────────────────────────
|
|
1374
|
+
server.tool("set-header-breakpoint", "Set the exact pixel width at which the desktop nav collapses into a hamburger menu globally across the site. Writes to Elementor's global breakpoints in the active kit.", {
|
|
1375
|
+
breakpoint_px: z.number().describe("Pixel width at which mobile hamburger menu activates (e.g. 1300)"),
|
|
1376
|
+
site: siteParam,
|
|
1377
|
+
}, async ({ breakpoint_px, site }) => {
|
|
1378
|
+
try {
|
|
1379
|
+
const { wpUrl, authHeader } = resolveSite(sites, site);
|
|
1380
|
+
const r = await axios.post(`${wpUrl}/wp-json/erc/v1/site/header-breakpoint`, { breakpoint_px }, { headers: { Authorization: authHeader, "Content-Type": "application/json" } });
|
|
1381
|
+
return { content: [{ type: "text", text: JSON.stringify(r.data, null, 2) }] };
|
|
1382
|
+
}
|
|
1383
|
+
catch (error) {
|
|
1384
|
+
return { content: [{ type: "text", text: `Error setting header breakpoint: ${error.response?.data?.message || error.message}` }], isError: true };
|
|
1385
|
+
}
|
|
1386
|
+
});
|
|
1387
|
+
server.tool("set-responsive-breakpoints", "Add or update custom global breakpoints in Elementor Pro's responsive settings. Lets you target non-standard viewport widths (e.g. 1280–1366px laptop range) natively without CSS workarounds.", {
|
|
1388
|
+
breakpoints: z.string().describe('JSON array of breakpoint objects. Each object: { "name": string, "min_width": number, "max_width": number }. Example: [{"name":"laptop","min_width":1280,"max_width":1366}]'),
|
|
1389
|
+
site: siteParam,
|
|
1390
|
+
}, async ({ breakpoints, site }) => {
|
|
1391
|
+
let parsed;
|
|
1392
|
+
try {
|
|
1393
|
+
parsed = JSON.parse(breakpoints);
|
|
1394
|
+
}
|
|
1395
|
+
catch {
|
|
1396
|
+
return { content: [{ type: "text", text: "Invalid JSON in breakpoints parameter." }], isError: true };
|
|
1397
|
+
}
|
|
1398
|
+
try {
|
|
1399
|
+
const { wpUrl, authHeader } = resolveSite(sites, site);
|
|
1400
|
+
const r = await axios.post(`${wpUrl}/wp-json/erc/v1/site/responsive-breakpoints`, { breakpoints: parsed }, { headers: { Authorization: authHeader, "Content-Type": "application/json" } });
|
|
1401
|
+
return { content: [{ type: "text", text: JSON.stringify(r.data, null, 2) }] };
|
|
1402
|
+
}
|
|
1403
|
+
catch (error) {
|
|
1404
|
+
return { content: [{ type: "text", text: `Error setting responsive breakpoints: ${error.response?.data?.message || error.message}` }], isError: true };
|
|
1405
|
+
}
|
|
1406
|
+
});
|
|
1407
|
+
// ── Group 11: Form Settings ───────────────────────────────────────────────────
|
|
1408
|
+
server.tool("update-form-settings", "Update 'Actions After Submit' settings on a specific Elementor Pro form widget — redirect URL, success message, email recipient, and email subject. No WP admin access needed.", {
|
|
1409
|
+
page_id: z.string().describe("WordPress Page ID containing the form"),
|
|
1410
|
+
form_widget_id: z.string().describe("Elementor element ID of the form widget"),
|
|
1411
|
+
redirect_url: z.string().optional().describe("URL to redirect to after successful submission"),
|
|
1412
|
+
success_message: z.string().optional().describe("Text message shown on successful submission"),
|
|
1413
|
+
email_to: z.string().optional().describe("Email address for form notification emails"),
|
|
1414
|
+
email_subject: z.string().optional().describe("Subject line for notification emails"),
|
|
1415
|
+
site: siteParam,
|
|
1416
|
+
}, async ({ page_id, form_widget_id, redirect_url, success_message, email_to, email_subject, site }) => {
|
|
1417
|
+
try {
|
|
1418
|
+
const { wpUrl, authHeader } = resolveSite(sites, site);
|
|
1419
|
+
// Fetch the current element
|
|
1420
|
+
const getRes = await axios.get(`${wpUrl}/wp-json/erc/v1/pages/${page_id}/elements/${form_widget_id}`, { headers: { Authorization: authHeader } });
|
|
1421
|
+
const el = getRes.data;
|
|
1422
|
+
const settings = el.settings || {};
|
|
1423
|
+
// Merge form action settings
|
|
1424
|
+
const emailSettings = settings.email || {};
|
|
1425
|
+
const redirectSettings = settings.redirect || {};
|
|
1426
|
+
if (email_to)
|
|
1427
|
+
emailSettings.to = email_to;
|
|
1428
|
+
if (email_subject)
|
|
1429
|
+
emailSettings.subject = email_subject;
|
|
1430
|
+
if (redirect_url)
|
|
1431
|
+
redirectSettings.url = redirect_url;
|
|
1432
|
+
const updatedSettings = { ...settings };
|
|
1433
|
+
if (email_to || email_subject)
|
|
1434
|
+
updatedSettings.email = emailSettings;
|
|
1435
|
+
if (redirect_url)
|
|
1436
|
+
updatedSettings.redirect = redirectSettings;
|
|
1437
|
+
if (success_message !== undefined)
|
|
1438
|
+
updatedSettings.success_message = success_message;
|
|
1439
|
+
const merged = { ...el, settings: updatedSettings };
|
|
1440
|
+
const postRes = await axios.post(`${wpUrl}/wp-json/erc/v1/pages/${page_id}/elements/${form_widget_id}`, merged, { headers: { Authorization: authHeader, "Content-Type": "application/json" } });
|
|
1441
|
+
return { content: [{ type: "text", text: JSON.stringify(postRes.data, null, 2) }] };
|
|
1442
|
+
}
|
|
1443
|
+
catch (error) {
|
|
1444
|
+
return { content: [{ type: "text", text: `Error updating form settings: ${error.response?.data?.message || error.message}` }], isError: true };
|
|
1445
|
+
}
|
|
1446
|
+
});
|
|
1447
|
+
// ── Group 12: SEO Meta ────────────────────────────────────────────────────────
|
|
1448
|
+
server.tool("update-seo-meta", "Set SEO meta title, meta description, OG title, OG description, and OG image for a page. Works with Yoast SEO and RankMath without needing WP admin access.", {
|
|
1449
|
+
page_id: z.string().describe("WordPress Page ID"),
|
|
1450
|
+
meta_title: z.string().optional().describe("SEO meta title (shown in browser tab and search results)"),
|
|
1451
|
+
meta_description: z.string().optional().describe("SEO meta description (max 160 characters recommended)"),
|
|
1452
|
+
og_title: z.string().optional().describe("Open Graph title for social sharing"),
|
|
1453
|
+
og_description: z.string().optional().describe("Open Graph description for social sharing"),
|
|
1454
|
+
og_image_url: z.string().optional().describe("Full URL of the Open Graph share image"),
|
|
1455
|
+
site: siteParam,
|
|
1456
|
+
}, async ({ page_id, meta_title, meta_description, og_title, og_description, og_image_url, site }) => {
|
|
1457
|
+
try {
|
|
1458
|
+
const { wpUrl, authHeader } = resolveSite(sites, site);
|
|
1459
|
+
const body = {};
|
|
1460
|
+
if (meta_title)
|
|
1461
|
+
body.meta_title = meta_title;
|
|
1462
|
+
if (meta_description)
|
|
1463
|
+
body.meta_description = meta_description;
|
|
1464
|
+
if (og_title)
|
|
1465
|
+
body.og_title = og_title;
|
|
1466
|
+
if (og_description)
|
|
1467
|
+
body.og_description = og_description;
|
|
1468
|
+
if (og_image_url)
|
|
1469
|
+
body.og_image_url = og_image_url;
|
|
1470
|
+
const r = await axios.post(`${wpUrl}/wp-json/erc/v1/pages/${page_id}/seo-meta`, body, { headers: { Authorization: authHeader, "Content-Type": "application/json" } });
|
|
1471
|
+
return { content: [{ type: "text", text: JSON.stringify(r.data, null, 2) }] };
|
|
1472
|
+
}
|
|
1473
|
+
catch (error) {
|
|
1474
|
+
return { content: [{ type: "text", text: `Error updating SEO meta: ${error.response?.data?.message || error.message}` }], isError: true };
|
|
1475
|
+
}
|
|
1476
|
+
});
|
|
1477
|
+
// ── Group 13: Page Slug ───────────────────────────────────────────────────────
|
|
1478
|
+
server.tool("update-page-slug", "Update a WordPress page's slug/permalink and optionally create a 301 redirect from the old URL to the new one — both in a single operation.", {
|
|
1479
|
+
page_id: z.string().describe("WordPress Page ID"),
|
|
1480
|
+
new_slug: z.string().describe("New URL slug (e.g. 'services/cryotherapy-in-verona-nj'). Do not include leading/trailing slashes."),
|
|
1481
|
+
create_redirect: z.boolean().optional().describe("Automatically create a 301 redirect from the old URL to the new one (default: true)"),
|
|
1482
|
+
site: siteParam,
|
|
1483
|
+
}, async ({ page_id, new_slug, create_redirect, site }) => {
|
|
1484
|
+
try {
|
|
1485
|
+
const { wpUrl, authHeader } = resolveSite(sites, site);
|
|
1486
|
+
const r = await axios.post(`${wpUrl}/wp-json/erc/v1/pages/${page_id}/slug`, { new_slug, create_redirect: create_redirect ?? true }, { headers: { Authorization: authHeader, "Content-Type": "application/json" } });
|
|
1487
|
+
return { content: [{ type: "text", text: JSON.stringify(r.data, null, 2) }] };
|
|
1488
|
+
}
|
|
1489
|
+
catch (error) {
|
|
1490
|
+
return { content: [{ type: "text", text: `Error updating page slug: ${error.response?.data?.message || error.message}` }], isError: true };
|
|
1491
|
+
}
|
|
1492
|
+
});
|
|
1493
|
+
// ── Group 14: Find & Replace Across Pages ────────────────────────────────────
|
|
1494
|
+
server.tool("find-and-replace-across-pages", "Search all Elementor page data site-wide for a text string and replace it everywhere it appears — in one operation. Can target all pages or a specific list of page IDs. Use for bulk fixes: phone numbers, addresses, hours, placeholder text.", {
|
|
1495
|
+
find: z.string().describe("Exact text string to find"),
|
|
1496
|
+
replace: z.string().describe("Text to replace it with (use empty string to remove)"),
|
|
1497
|
+
scope: z.enum(["all_pages", "page_ids"]).describe("'all_pages' to scan every page, or 'page_ids' to target specific pages"),
|
|
1498
|
+
page_ids: z.array(z.number()).optional().describe("Required when scope is 'page_ids'. Array of WordPress page IDs to target."),
|
|
1499
|
+
site: siteParam,
|
|
1500
|
+
}, async ({ find, replace, scope, page_ids, site }) => {
|
|
1501
|
+
try {
|
|
1502
|
+
const { wpUrl, authHeader } = resolveSite(sites, site);
|
|
1503
|
+
const body = { find, replace, scope };
|
|
1504
|
+
if (scope === "page_ids") {
|
|
1505
|
+
if (!page_ids || page_ids.length === 0) {
|
|
1506
|
+
return { content: [{ type: "text", text: "page_ids is required when scope is 'page_ids'." }], isError: true };
|
|
1507
|
+
}
|
|
1508
|
+
body.page_ids = page_ids;
|
|
1509
|
+
}
|
|
1510
|
+
const r = await axios.post(`${wpUrl}/wp-json/erc/v1/site/find-replace`, body, { headers: { Authorization: authHeader, "Content-Type": "application/json" } });
|
|
1511
|
+
return { content: [{ type: "text", text: JSON.stringify(r.data, null, 2) }] };
|
|
1512
|
+
}
|
|
1513
|
+
catch (error) {
|
|
1514
|
+
return { content: [{ type: "text", text: `Error running find-and-replace: ${error.response?.data?.message || error.message}` }], isError: true };
|
|
1515
|
+
}
|
|
1516
|
+
});
|
|
1517
|
+
// ── Group 15: Element Visibility ─────────────────────────────────────────────
|
|
1518
|
+
server.tool("set-element-visibility-per-device", "Show or hide a specific Elementor element on desktop, tablet, and/or mobile — without needing to know the widget-type-specific internal setting key.", {
|
|
1519
|
+
page_id: z.string().describe("WordPress Page ID"),
|
|
1520
|
+
element_id: z.string().describe("Elementor element ID"),
|
|
1521
|
+
show_on: z.array(z.enum(["desktop", "tablet", "mobile"])).describe("Devices on which this element should be VISIBLE. Devices not listed will have the element hidden."),
|
|
1522
|
+
site: siteParam,
|
|
1523
|
+
}, async ({ page_id, element_id, show_on, site }) => {
|
|
1524
|
+
try {
|
|
1525
|
+
const { wpUrl, authHeader } = resolveSite(sites, site);
|
|
1526
|
+
const getRes = await axios.get(`${wpUrl}/wp-json/erc/v1/pages/${page_id}/elements/${element_id}`, { headers: { Authorization: authHeader } });
|
|
1527
|
+
const el = getRes.data;
|
|
1528
|
+
const visibilitySettings = {
|
|
1529
|
+
hide_desktop: show_on.includes("desktop") ? "" : "hidden",
|
|
1530
|
+
hide_tablet: show_on.includes("tablet") ? "" : "hidden",
|
|
1531
|
+
hide_mobile: show_on.includes("mobile") ? "" : "hidden",
|
|
1532
|
+
};
|
|
1533
|
+
const merged = { ...el, settings: { ...(el.settings || {}), ...visibilitySettings } };
|
|
1534
|
+
const postRes = await axios.post(`${wpUrl}/wp-json/erc/v1/pages/${page_id}/elements/${element_id}`, merged, { headers: { Authorization: authHeader, "Content-Type": "application/json" } });
|
|
1535
|
+
return { content: [{ type: "text", text: JSON.stringify({ visibility: visibilitySettings, result: postRes.data }, null, 2) }] };
|
|
1536
|
+
}
|
|
1537
|
+
catch (error) {
|
|
1538
|
+
return { content: [{ type: "text", text: `Error setting element visibility: ${error.response?.data?.message || error.message}` }], isError: true };
|
|
1539
|
+
}
|
|
1540
|
+
});
|
|
1541
|
+
// ── Group 16: Sync Spacing Across Pages ──────────────────────────────────────
|
|
1542
|
+
server.tool("sync-spacing-across-pages", "Copy spacing settings (padding, margin, gap) from a reference element on one page and apply the same values to matching elements across multiple target pages — all in one operation.", {
|
|
1543
|
+
source_page_id: z.string().describe("Page ID to copy spacing FROM"),
|
|
1544
|
+
source_element_id: z.string().describe("Element ID on the source page to copy spacing FROM"),
|
|
1545
|
+
target_page_ids: z.array(z.number()).describe("Array of page IDs to apply spacing TO"),
|
|
1546
|
+
target_element_selector: z.string().describe("Widget type or CSS class to match on target pages (e.g. 'section', 'heading', or a CSS class name)"),
|
|
1547
|
+
settings_to_sync: z.array(z.string()).optional().describe("Specific spacing keys to copy. Defaults to: padding, padding_tablet, padding_mobile, margin, margin_tablet, margin_mobile, gap"),
|
|
1548
|
+
site: siteParam,
|
|
1549
|
+
}, async ({ source_page_id, source_element_id, target_page_ids, target_element_selector, settings_to_sync, site }) => {
|
|
1550
|
+
try {
|
|
1551
|
+
const { wpUrl, authHeader } = resolveSite(sites, site);
|
|
1552
|
+
const keysToSync = settings_to_sync ?? ["padding", "padding_tablet", "padding_mobile", "margin", "margin_tablet", "margin_mobile", "gap"];
|
|
1553
|
+
// 1. Fetch source element settings
|
|
1554
|
+
const srcRes = await axios.get(`${wpUrl}/wp-json/erc/v1/pages/${source_page_id}/elements/${source_element_id}`, { headers: { Authorization: authHeader } });
|
|
1555
|
+
const srcSettings = srcRes.data.settings || {};
|
|
1556
|
+
const spacingToCopy = Object.fromEntries(keysToSync.map(k => [k, srcSettings[k]]).filter(([, v]) => v !== undefined));
|
|
1557
|
+
if (Object.keys(spacingToCopy).length === 0) {
|
|
1558
|
+
return { content: [{ type: "text", text: `No matching spacing keys found on source element. Checked: ${keysToSync.join(", ")}` }], isError: true };
|
|
1559
|
+
}
|
|
1560
|
+
// 2. Apply to each target page
|
|
1561
|
+
const results = [];
|
|
1562
|
+
for (const targetPageId of target_page_ids) {
|
|
1563
|
+
try {
|
|
1564
|
+
const pageRes = await axios.get(`${wpUrl}/wp-json/erc/v1/pages/${targetPageId}/data`, { headers: { Authorization: authHeader } });
|
|
1565
|
+
const allEls = flattenElements(pageRes.data);
|
|
1566
|
+
const targets = allEls.filter((el) => el.widgetType === target_element_selector ||
|
|
1567
|
+
el.elType === target_element_selector ||
|
|
1568
|
+
(el.settings?._css_classes || "").includes(target_element_selector));
|
|
1569
|
+
let updated = 0;
|
|
1570
|
+
for (const tgt of targets) {
|
|
1571
|
+
const merged = { ...tgt, settings: { ...(tgt.settings || {}), ...spacingToCopy } };
|
|
1572
|
+
await axios.post(`${wpUrl}/wp-json/erc/v1/pages/${targetPageId}/elements/${tgt.id}`, merged, { headers: { Authorization: authHeader, "Content-Type": "application/json" } });
|
|
1573
|
+
updated++;
|
|
1574
|
+
}
|
|
1575
|
+
results.push({ page_id: targetPageId, elements_updated: updated });
|
|
1576
|
+
}
|
|
1577
|
+
catch (err) {
|
|
1578
|
+
results.push({ page_id: targetPageId, error: err.response?.data?.message || err.message });
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
return { content: [{ type: "text", text: JSON.stringify({ spacing_copied: spacingToCopy, pages: results }, null, 2) }] };
|
|
1582
|
+
}
|
|
1583
|
+
catch (error) {
|
|
1584
|
+
return { content: [{ type: "text", text: `Error syncing spacing: ${error.response?.data?.message || error.message}` }], isError: true };
|
|
1585
|
+
}
|
|
1586
|
+
});
|
|
1587
|
+
// ── Group 17: Popup Settings ──────────────────────────────────────────────────
|
|
1588
|
+
server.tool("update-popup-settings", "Update popup-level settings on an Elementor Pro popup template — dimensions, screen position, entrance animation, overlay toggle, and close button delay. Does not modify popup content elements.", {
|
|
1589
|
+
popup_id: z.string().describe("Elementor popup template ID"),
|
|
1590
|
+
width: z.string().optional().describe('Width as JSON: {"unit":"px","size":400} or {"unit":"%","size":80}'),
|
|
1591
|
+
height: z.string().optional().describe('Height as JSON: {"unit":"vh","size":100} or {"unit":"px","size":600}'),
|
|
1592
|
+
position: z.string().optional().describe("Screen position: top-left, top-center, top-right, center-left, center-center, center-right, bottom-left, bottom-center, bottom-right"),
|
|
1593
|
+
entrance_animation: z.string().optional().describe("CSS animation name: fadeIn, slideInRight, slideInLeft, slideInUp, slideInDown, zoomIn, bounceIn, etc."),
|
|
1594
|
+
show_overlay: z.boolean().optional().describe("Show background overlay behind the popup"),
|
|
1595
|
+
close_button_delay: z.number().optional().describe("Seconds before the close (×) button appears (0 = immediately)"),
|
|
1596
|
+
site: siteParam,
|
|
1597
|
+
}, async ({ popup_id, width, height, position, entrance_animation, show_overlay, close_button_delay, site }) => {
|
|
1598
|
+
try {
|
|
1599
|
+
const { wpUrl, authHeader } = resolveSite(sites, site);
|
|
1600
|
+
const settings = {};
|
|
1601
|
+
if (width !== undefined) {
|
|
1602
|
+
try {
|
|
1603
|
+
settings.width = JSON.parse(width);
|
|
1604
|
+
}
|
|
1605
|
+
catch {
|
|
1606
|
+
settings.width = width;
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
if (height !== undefined) {
|
|
1610
|
+
try {
|
|
1611
|
+
settings.height = JSON.parse(height);
|
|
1612
|
+
}
|
|
1613
|
+
catch {
|
|
1614
|
+
settings.height = height;
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
if (position !== undefined)
|
|
1618
|
+
settings.position = position;
|
|
1619
|
+
if (entrance_animation !== undefined)
|
|
1620
|
+
settings.entrance_animation = entrance_animation;
|
|
1621
|
+
if (show_overlay !== undefined)
|
|
1622
|
+
settings.overlay_prevent_close = !show_overlay;
|
|
1623
|
+
if (close_button_delay !== undefined)
|
|
1624
|
+
settings.close_button_delay = close_button_delay;
|
|
1625
|
+
const r = await axios.patch(`${wpUrl}/wp-json/erc/v1/templates/${popup_id}/popup-settings`, settings, { headers: { Authorization: authHeader, "Content-Type": "application/json" } });
|
|
1626
|
+
return { content: [{ type: "text", text: JSON.stringify(r.data, null, 2) }] };
|
|
1627
|
+
}
|
|
1628
|
+
catch (error) {
|
|
1629
|
+
return { content: [{ type: "text", text: `Error updating popup settings: ${error.response?.data?.message || error.message}` }], isError: true };
|
|
1630
|
+
}
|
|
1631
|
+
});
|
|
1632
|
+
// ── Group 18: Add Widget To Page ─────────────────────────────────────────────
|
|
1633
|
+
server.tool("add-widget-to-page", "Copy a specific widget from a source page or template and insert it into a target page at a defined position. Handles JSON extraction and insertion automatically.", {
|
|
1634
|
+
source_page_id: z.string().describe("Page or template ID to copy the widget FROM"),
|
|
1635
|
+
source_element_id: z.string().describe("Element ID of the widget to copy"),
|
|
1636
|
+
target_page_id: z.string().describe("Page ID to insert the widget INTO"),
|
|
1637
|
+
position: z.enum(["top", "bottom"]).optional().describe("Where to insert: 'top' = before the first section, 'bottom' = after the last section (default: bottom)"),
|
|
1638
|
+
after_element_id: z.string().optional().describe("Insert after this specific element ID on the target page (overrides position if provided)"),
|
|
1639
|
+
site: siteParam,
|
|
1640
|
+
}, async ({ source_page_id, source_element_id, target_page_id, position, after_element_id, site }) => {
|
|
1641
|
+
try {
|
|
1642
|
+
const { wpUrl, authHeader } = resolveSite(sites, site);
|
|
1643
|
+
// 1. Fetch the source widget
|
|
1644
|
+
const srcRes = await axios.get(`${wpUrl}/wp-json/erc/v1/pages/${source_page_id}/elements/${source_element_id}`, { headers: { Authorization: authHeader } });
|
|
1645
|
+
const widgetToCopy = srcRes.data;
|
|
1646
|
+
// Give the copy a new unique ID suffix so it doesn't clash
|
|
1647
|
+
const newId = `${source_element_id}_copy_${Date.now().toString(36)}`;
|
|
1648
|
+
const widgetCopy = { ...widgetToCopy, id: newId };
|
|
1649
|
+
// 2. Fetch target page data
|
|
1650
|
+
const tgtRes = await axios.get(`${wpUrl}/wp-json/erc/v1/pages/${target_page_id}/data`, { headers: { Authorization: authHeader } });
|
|
1651
|
+
let elements = tgtRes.data;
|
|
1652
|
+
// 3. Insert at the correct position
|
|
1653
|
+
if (after_element_id) {
|
|
1654
|
+
// Find the element and insert after it in the top-level array
|
|
1655
|
+
const idx = elements.findIndex((el) => el.id === after_element_id);
|
|
1656
|
+
if (idx === -1) {
|
|
1657
|
+
return { content: [{ type: "text", text: `Element with id "${after_element_id}" not found at top level of target page.` }], isError: true };
|
|
1658
|
+
}
|
|
1659
|
+
elements = [...elements.slice(0, idx + 1), widgetCopy, ...elements.slice(idx + 1)];
|
|
1660
|
+
}
|
|
1661
|
+
else if (position === "top") {
|
|
1662
|
+
elements = [widgetCopy, ...elements];
|
|
1663
|
+
}
|
|
1664
|
+
else {
|
|
1665
|
+
// bottom (default)
|
|
1666
|
+
elements = [...elements, widgetCopy];
|
|
1667
|
+
}
|
|
1668
|
+
// 4. Save updated target page
|
|
1669
|
+
const putRes = await axios.put(`${wpUrl}/wp-json/erc/v1/pages/${target_page_id}/data`, elements, { headers: { Authorization: authHeader, "Content-Type": "application/json" } });
|
|
1670
|
+
return { content: [{ type: "text", text: JSON.stringify({ new_element_id: newId, result: putRes.data }, null, 2) }] };
|
|
1671
|
+
}
|
|
1672
|
+
catch (error) {
|
|
1673
|
+
return { content: [{ type: "text", text: `Error adding widget to page: ${error.response?.data?.message || error.message}` }], isError: true };
|
|
1674
|
+
}
|
|
1675
|
+
});
|
|
1676
|
+
// ── Group 19: Disable Read More Sitewide ─────────────────────────────────────
|
|
1677
|
+
server.tool("disable-read-more-sitewide", "Find all Elementor Posts widgets across the site (or specific pages) and disable the Read More button using the native Elementor Posts widget setting — not CSS. Standard SEO practice applied in one operation.", {
|
|
1678
|
+
scope: z.enum(["all_pages", "page_ids"]).describe("'all_pages' to process every page, or 'page_ids' to target specific pages"),
|
|
1679
|
+
page_ids: z.array(z.number()).optional().describe("Required when scope is 'page_ids'. Array of WordPress page IDs."),
|
|
1680
|
+
site: siteParam,
|
|
1681
|
+
}, async ({ scope, page_ids, site }) => {
|
|
1682
|
+
try {
|
|
1683
|
+
const { wpUrl, authHeader } = resolveSite(sites, site);
|
|
1684
|
+
// 1. Build list of pages to process
|
|
1685
|
+
let pageList = [];
|
|
1686
|
+
if (scope === "all_pages") {
|
|
1687
|
+
const listRes = await axios.get(`${wpUrl}/wp-json/erc/v1/pages`, { headers: { Authorization: authHeader } });
|
|
1688
|
+
pageList = listRes.data.map((p) => Number(p.id || p.ID));
|
|
1689
|
+
}
|
|
1690
|
+
else {
|
|
1691
|
+
if (!page_ids || page_ids.length === 0) {
|
|
1692
|
+
return { content: [{ type: "text", text: "page_ids is required when scope is 'page_ids'." }], isError: true };
|
|
1693
|
+
}
|
|
1694
|
+
pageList = page_ids;
|
|
1695
|
+
}
|
|
1696
|
+
const results = [];
|
|
1697
|
+
for (const pid of pageList) {
|
|
1698
|
+
try {
|
|
1699
|
+
const dataRes = await axios.get(`${wpUrl}/wp-json/erc/v1/pages/${pid}/data`, { headers: { Authorization: authHeader } });
|
|
1700
|
+
const allEls = flattenElements(dataRes.data);
|
|
1701
|
+
const postWidgets = allEls.filter((el) => el.widgetType === "posts" || el.widgetType === "archive-posts");
|
|
1702
|
+
if (postWidgets.length === 0) {
|
|
1703
|
+
results.push({ page_id: pid, widgets_updated: 0 });
|
|
1704
|
+
continue;
|
|
1705
|
+
}
|
|
1706
|
+
for (const widget of postWidgets) {
|
|
1707
|
+
const merged = {
|
|
1708
|
+
...widget,
|
|
1709
|
+
settings: { ...(widget.settings || {}), show_read_more: "no" },
|
|
1710
|
+
};
|
|
1711
|
+
await axios.post(`${wpUrl}/wp-json/erc/v1/pages/${pid}/elements/${widget.id}`, merged, { headers: { Authorization: authHeader, "Content-Type": "application/json" } });
|
|
1712
|
+
}
|
|
1713
|
+
results.push({ page_id: pid, widgets_updated: postWidgets.length });
|
|
1714
|
+
}
|
|
1715
|
+
catch (err) {
|
|
1716
|
+
results.push({ page_id: pid, error: err.response?.data?.message || err.message });
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
const total = results.reduce((acc, r) => acc + (r.widgets_updated || 0), 0);
|
|
1720
|
+
return { content: [{ type: "text", text: JSON.stringify({ total_widgets_updated: total, pages: results }, null, 2) }] };
|
|
1721
|
+
}
|
|
1722
|
+
catch (error) {
|
|
1723
|
+
return { content: [{ type: "text", text: `Error disabling Read More: ${error.response?.data?.message || error.message}` }], isError: true };
|
|
1724
|
+
}
|
|
1725
|
+
});
|
|
1726
|
+
// ── Group 20: Update Video URL ────────────────────────────────────────────────
|
|
1727
|
+
server.tool("update-video-url", "Update the video source URL on a specific Elementor video widget. Validates the URL format, updates the widget setting, and clears Elementor cache so the new video loads immediately.", {
|
|
1728
|
+
page_id: z.string().describe("WordPress Page ID containing the video widget"),
|
|
1729
|
+
widget_id: z.string().describe("Elementor element ID of the video widget"),
|
|
1730
|
+
video_url: z.string().describe("New video URL (e.g. Bunny CDN embed URL, YouTube, Vimeo, or direct MP4 URL)"),
|
|
1731
|
+
video_type: z.enum(["youtube", "vimeo", "hosted", "bunny_cdn"]).optional().describe("Video source type. Use 'hosted' for direct MP4 or 'bunny_cdn' for Bunny CDN embed (default: auto-detect from URL)"),
|
|
1732
|
+
site: siteParam,
|
|
1733
|
+
}, async ({ page_id, widget_id, video_url, video_type, site }) => {
|
|
1734
|
+
try {
|
|
1735
|
+
const { wpUrl, authHeader } = resolveSite(sites, site);
|
|
1736
|
+
// Auto-detect type if not provided
|
|
1737
|
+
let resolvedType = video_type;
|
|
1738
|
+
if (!resolvedType) {
|
|
1739
|
+
if (video_url.includes("youtube.com") || video_url.includes("youtu.be")) {
|
|
1740
|
+
resolvedType = "youtube";
|
|
1741
|
+
}
|
|
1742
|
+
else if (video_url.includes("vimeo.com")) {
|
|
1743
|
+
resolvedType = "vimeo";
|
|
1744
|
+
}
|
|
1745
|
+
else if (video_url.includes("mediadelivery.net") || video_url.includes("bunnycdn") || video_url.includes("iframe.mediadelivery")) {
|
|
1746
|
+
resolvedType = "bunny_cdn";
|
|
1747
|
+
}
|
|
1748
|
+
else {
|
|
1749
|
+
resolvedType = "hosted";
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
// Fetch current element
|
|
1753
|
+
const getRes = await axios.get(`${wpUrl}/wp-json/erc/v1/pages/${page_id}/elements/${widget_id}`, { headers: { Authorization: authHeader } });
|
|
1754
|
+
const el = getRes.data;
|
|
1755
|
+
// Build updated settings based on video type
|
|
1756
|
+
const videoSettings = {
|
|
1757
|
+
video_type: resolvedType === "bunny_cdn" ? "hosted" : resolvedType,
|
|
1758
|
+
};
|
|
1759
|
+
if (resolvedType === "youtube") {
|
|
1760
|
+
videoSettings.youtube_url = video_url;
|
|
1761
|
+
}
|
|
1762
|
+
else if (resolvedType === "vimeo") {
|
|
1763
|
+
videoSettings.vimeo_url = video_url;
|
|
1764
|
+
}
|
|
1765
|
+
else {
|
|
1766
|
+
// hosted / bunny_cdn — use external_url for Bunny CDN iframes, hosted_url for direct MP4
|
|
1767
|
+
if (resolvedType === "bunny_cdn") {
|
|
1768
|
+
videoSettings.video_type = "hosted";
|
|
1769
|
+
videoSettings.hosted_url = { url: video_url };
|
|
1770
|
+
}
|
|
1771
|
+
else {
|
|
1772
|
+
videoSettings.hosted_url = { url: video_url };
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
const merged = { ...el, settings: { ...(el.settings || {}), ...videoSettings } };
|
|
1776
|
+
const postRes = await axios.post(`${wpUrl}/wp-json/erc/v1/pages/${page_id}/elements/${widget_id}`, merged, { headers: { Authorization: authHeader, "Content-Type": "application/json" } });
|
|
1777
|
+
// Clear cache so new video is served immediately
|
|
1778
|
+
await axios.post(`${wpUrl}/wp-json/erc/v1/site/clear-cache`, {}, { headers: { Authorization: authHeader }, params: { post_id: page_id } });
|
|
1779
|
+
return { content: [{ type: "text", text: JSON.stringify({ video_type: resolvedType, video_url, result: postRes.data }, null, 2) }] };
|
|
1780
|
+
}
|
|
1781
|
+
catch (error) {
|
|
1782
|
+
return { content: [{ type: "text", text: `Error updating video URL: ${error.response?.data?.message || error.message}` }], isError: true };
|
|
1783
|
+
}
|
|
1784
|
+
});
|
|
1295
1785
|
return server;
|
|
1296
1786
|
}
|
|
1297
1787
|
// ─── Entry Point ──────────────────────────────────────────────────────────────
|
package/package.json
CHANGED