@yashwant.dharmdas/elementor-mcp 3.2.8 → 3.4.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 +568 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1370,6 +1370,574 @@ function createMcpServer(sites) {
|
|
|
1370
1370
|
};
|
|
1371
1371
|
}
|
|
1372
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
|
+
});
|
|
1785
|
+
// ── Group 21: WordPress Menu ─────────────────────────────────────────────────
|
|
1786
|
+
server.tool("update-wordpress-menu", "Read and update a WordPress navigation menu — reorder items, add items, remove items, or disable links. The Elementor Nav Menu widget order is controlled here, not in Elementor.", {
|
|
1787
|
+
menu_name: z.string().describe("WordPress menu name (e.g. 'Primary Menu', 'Footer Menu') or numeric menu ID"),
|
|
1788
|
+
action: z.enum(["get", "reorder", "disable_link", "enable_link", "remove_item", "add_item"]).describe("Action to perform: 'get' = read current menu, 'reorder' = set item order, 'disable_link' = make item non-clickable (#), 'enable_link' = restore item URL, 'remove_item' = delete a menu item, 'add_item' = append a new item"),
|
|
1789
|
+
item_order: z.array(z.string()).optional().describe("Required for 'reorder'. Array of item titles in desired display order."),
|
|
1790
|
+
item_title: z.string().optional().describe("Required for disable_link / enable_link / remove_item. The menu item title to target."),
|
|
1791
|
+
item_url: z.string().optional().describe("Required for add_item. The URL of the new menu item."),
|
|
1792
|
+
item_parent: z.string().optional().describe("For add_item — title of the parent item (for dropdown sub-items). Omit for top-level."),
|
|
1793
|
+
site: siteParam,
|
|
1794
|
+
}, async ({ menu_name, action, item_order, item_title, item_url, item_parent, site }) => {
|
|
1795
|
+
try {
|
|
1796
|
+
const { wpUrl, authHeader } = resolveSite(sites, site);
|
|
1797
|
+
const body = { menu_name, action };
|
|
1798
|
+
if (item_order)
|
|
1799
|
+
body.item_order = item_order;
|
|
1800
|
+
if (item_title)
|
|
1801
|
+
body.item_title = item_title;
|
|
1802
|
+
if (item_url)
|
|
1803
|
+
body.item_url = item_url;
|
|
1804
|
+
if (item_parent)
|
|
1805
|
+
body.item_parent = item_parent;
|
|
1806
|
+
const r = await axios.post(`${wpUrl}/wp-json/erc/v1/site/menu`, body, { headers: { Authorization: authHeader, "Content-Type": "application/json" } });
|
|
1807
|
+
return { content: [{ type: "text", text: JSON.stringify(r.data, null, 2) }] };
|
|
1808
|
+
}
|
|
1809
|
+
catch (error) {
|
|
1810
|
+
return { content: [{ type: "text", text: `Error updating WordPress menu: ${error.response?.data?.message || error.message}` }], isError: true };
|
|
1811
|
+
}
|
|
1812
|
+
});
|
|
1813
|
+
// ── Group 22: Rank Math SEO ───────────────────────────────────────────────────
|
|
1814
|
+
server.tool("update-rank-math-seo", "Update Rank Math SEO settings for a page — meta title, meta description, OG title, OG description, and social share image. Writes directly to Rank Math post meta. Use the hero section image as the og_image_url as per standard procedure.", {
|
|
1815
|
+
page_id: z.string().describe("WordPress Page ID"),
|
|
1816
|
+
meta_title: z.string().optional().describe("SEO meta title shown in search results and browser tab"),
|
|
1817
|
+
meta_description: z.string().optional().describe("SEO meta description (max 160 characters recommended)"),
|
|
1818
|
+
og_title: z.string().optional().describe("Open Graph / social share title"),
|
|
1819
|
+
og_description: z.string().optional().describe("Open Graph / social share description"),
|
|
1820
|
+
og_image_url: z.string().optional().describe("Full URL of the social share image (use hero section image as standard)"),
|
|
1821
|
+
og_image_id: z.number().optional().describe("WordPress media attachment ID of the OG image (preferred over URL if available)"),
|
|
1822
|
+
site: siteParam,
|
|
1823
|
+
}, async ({ page_id, meta_title, meta_description, og_title, og_description, og_image_url, og_image_id, site }) => {
|
|
1824
|
+
try {
|
|
1825
|
+
const { wpUrl, authHeader } = resolveSite(sites, site);
|
|
1826
|
+
// Rank Math stores its data in specific post meta keys:
|
|
1827
|
+
// rank_math_title, rank_math_description, rank_math_og_title,
|
|
1828
|
+
// rank_math_og_description, rank_math_facebook_image,
|
|
1829
|
+
// rank_math_facebook_image_id
|
|
1830
|
+
const meta = {};
|
|
1831
|
+
if (meta_title)
|
|
1832
|
+
meta.rank_math_title = meta_title;
|
|
1833
|
+
if (meta_description)
|
|
1834
|
+
meta.rank_math_description = meta_description;
|
|
1835
|
+
if (og_title)
|
|
1836
|
+
meta.rank_math_og_title = og_title;
|
|
1837
|
+
if (og_description)
|
|
1838
|
+
meta.rank_math_og_description = og_description;
|
|
1839
|
+
if (og_image_url)
|
|
1840
|
+
meta.rank_math_facebook_image = og_image_url;
|
|
1841
|
+
if (og_image_id)
|
|
1842
|
+
meta.rank_math_facebook_image_id = og_image_id;
|
|
1843
|
+
const r = await axios.post(`${wpUrl}/wp-json/erc/v1/pages/${page_id}/post-meta`, { meta }, { headers: { Authorization: authHeader, "Content-Type": "application/json" } });
|
|
1844
|
+
return { content: [{ type: "text", text: JSON.stringify(r.data, null, 2) }] };
|
|
1845
|
+
}
|
|
1846
|
+
catch (error) {
|
|
1847
|
+
return { content: [{ type: "text", text: `Error updating Rank Math SEO: ${error.response?.data?.message || error.message}` }], isError: true };
|
|
1848
|
+
}
|
|
1849
|
+
});
|
|
1850
|
+
// ── Group 23: Element Order Per Device ───────────────────────────────────────
|
|
1851
|
+
server.tool("set-element-order-per-device", "Set the CSS display order of an Elementor element per breakpoint — desktop, tablet, and/or mobile. Changes visual stacking order without altering the DOM. Use to move images above text on mobile (order 1) while keeping them below on desktop (order 2).", {
|
|
1852
|
+
page_id: z.string().describe("WordPress Page ID"),
|
|
1853
|
+
element_id: z.string().describe("Elementor element ID"),
|
|
1854
|
+
order_desktop: z.number().optional().describe("CSS order value for desktop (e.g. 1 = first, 2 = second)"),
|
|
1855
|
+
order_tablet: z.number().optional().describe("CSS order value for tablet"),
|
|
1856
|
+
order_mobile: z.number().optional().describe("CSS order value for mobile"),
|
|
1857
|
+
site: siteParam,
|
|
1858
|
+
}, async ({ page_id, element_id, order_desktop, order_tablet, order_mobile, site }) => {
|
|
1859
|
+
try {
|
|
1860
|
+
const { wpUrl, authHeader } = resolveSite(sites, site);
|
|
1861
|
+
const getRes = await axios.get(`${wpUrl}/wp-json/erc/v1/pages/${page_id}/elements/${element_id}`, { headers: { Authorization: authHeader } });
|
|
1862
|
+
const el = getRes.data;
|
|
1863
|
+
const orderSettings = {};
|
|
1864
|
+
if (order_desktop !== undefined)
|
|
1865
|
+
orderSettings._element_order = order_desktop;
|
|
1866
|
+
if (order_tablet !== undefined)
|
|
1867
|
+
orderSettings._element_order_tablet = order_tablet;
|
|
1868
|
+
if (order_mobile !== undefined)
|
|
1869
|
+
orderSettings._element_order_mobile = order_mobile;
|
|
1870
|
+
const merged = { ...el, settings: { ...(el.settings || {}), ...orderSettings } };
|
|
1871
|
+
const postRes = await axios.post(`${wpUrl}/wp-json/erc/v1/pages/${page_id}/elements/${element_id}`, merged, { headers: { Authorization: authHeader, "Content-Type": "application/json" } });
|
|
1872
|
+
return { content: [{ type: "text", text: JSON.stringify({ order_applied: orderSettings, result: postRes.data }, null, 2) }] };
|
|
1873
|
+
}
|
|
1874
|
+
catch (error) {
|
|
1875
|
+
return { content: [{ type: "text", text: `Error setting element order: ${error.response?.data?.message || error.message}` }], isError: true };
|
|
1876
|
+
}
|
|
1877
|
+
});
|
|
1878
|
+
// ── Group 24: Element Z-Index ─────────────────────────────────────────────────
|
|
1879
|
+
server.tool("set-element-z-index", "Set the Z-index value for a specific Elementor element. Use to fix images showing over the header, overlapping sections, or elements bleeding through other elements. Can set an absolute value or relative to another element (above/below).", {
|
|
1880
|
+
page_id: z.string().describe("WordPress Page ID"),
|
|
1881
|
+
element_id: z.string().describe("Elementor element ID to update"),
|
|
1882
|
+
z_index: z.number().optional().describe("Absolute Z-index value (e.g. 1, 10, 100). Use this OR reference_element_id — not both."),
|
|
1883
|
+
reference_element_id: z.string().optional().describe("Element ID to compare against. Use with 'position' to set z-index relative to another element."),
|
|
1884
|
+
position: z.enum(["above", "below"]).optional().describe("'above' = z-index of reference + 1, 'below' = z-index of reference - 1"),
|
|
1885
|
+
site: siteParam,
|
|
1886
|
+
}, async ({ page_id, element_id, z_index, reference_element_id, position, site }) => {
|
|
1887
|
+
try {
|
|
1888
|
+
const { wpUrl, authHeader } = resolveSite(sites, site);
|
|
1889
|
+
let finalZIndex = z_index;
|
|
1890
|
+
// If relative mode: fetch reference element's current z-index
|
|
1891
|
+
if (reference_element_id && position) {
|
|
1892
|
+
const refRes = await axios.get(`${wpUrl}/wp-json/erc/v1/pages/${page_id}/elements/${reference_element_id}`, { headers: { Authorization: authHeader } });
|
|
1893
|
+
const refZ = parseInt(refRes.data.settings?._z_index || "0", 10);
|
|
1894
|
+
finalZIndex = position === "above" ? refZ + 1 : Math.max(0, refZ - 1);
|
|
1895
|
+
}
|
|
1896
|
+
if (finalZIndex === undefined) {
|
|
1897
|
+
return { content: [{ type: "text", text: "Provide either z_index or reference_element_id + position." }], isError: true };
|
|
1898
|
+
}
|
|
1899
|
+
const getRes = await axios.get(`${wpUrl}/wp-json/erc/v1/pages/${page_id}/elements/${element_id}`, { headers: { Authorization: authHeader } });
|
|
1900
|
+
const el = getRes.data;
|
|
1901
|
+
const merged = { ...el, settings: { ...(el.settings || {}), _z_index: finalZIndex } };
|
|
1902
|
+
const postRes = await axios.post(`${wpUrl}/wp-json/erc/v1/pages/${page_id}/elements/${element_id}`, merged, { headers: { Authorization: authHeader, "Content-Type": "application/json" } });
|
|
1903
|
+
return { content: [{ type: "text", text: JSON.stringify({ z_index_set: finalZIndex, result: postRes.data }, null, 2) }] };
|
|
1904
|
+
}
|
|
1905
|
+
catch (error) {
|
|
1906
|
+
return { content: [{ type: "text", text: `Error setting Z-index: ${error.response?.data?.message || error.message}` }], isError: true };
|
|
1907
|
+
}
|
|
1908
|
+
});
|
|
1909
|
+
// ── Group 25: Video Overlay ───────────────────────────────────────────────────
|
|
1910
|
+
server.tool("update-video-overlay", "Set the image overlay on an Elementor video widget so the video appears to load immediately on page load. The overlay image (typically the video thumbnail or hero image) is shown until the user clicks play. This is the correct fix for 'video not loading immediately' QA issues — not changing the video URL.", {
|
|
1911
|
+
page_id: z.string().describe("WordPress Page ID containing the video widget"),
|
|
1912
|
+
widget_id: z.string().describe("Elementor element ID of the video widget"),
|
|
1913
|
+
overlay_image_url: z.string().describe("Full URL of the overlay/thumbnail image (use the video thumbnail or hero section image)"),
|
|
1914
|
+
overlay_image_id: z.number().optional().describe("WordPress media attachment ID of the overlay image (preferred over URL if available)"),
|
|
1915
|
+
show_play_icon: z.boolean().optional().describe("Show a play button icon over the overlay image (default: true)"),
|
|
1916
|
+
site: siteParam,
|
|
1917
|
+
}, async ({ page_id, widget_id, overlay_image_url, overlay_image_id, show_play_icon, site }) => {
|
|
1918
|
+
try {
|
|
1919
|
+
const { wpUrl, authHeader } = resolveSite(sites, site);
|
|
1920
|
+
const getRes = await axios.get(`${wpUrl}/wp-json/erc/v1/pages/${page_id}/elements/${widget_id}`, { headers: { Authorization: authHeader } });
|
|
1921
|
+
const el = getRes.data;
|
|
1922
|
+
const overlaySettings = {
|
|
1923
|
+
show_image_overlay: "yes",
|
|
1924
|
+
image_overlay: {
|
|
1925
|
+
url: overlay_image_url,
|
|
1926
|
+
id: overlay_image_id ?? 0,
|
|
1927
|
+
},
|
|
1928
|
+
show_play_icon: (show_play_icon ?? true) ? "yes" : "no",
|
|
1929
|
+
lightbox: "no", // Ensure video plays inline, not in a lightbox
|
|
1930
|
+
};
|
|
1931
|
+
const merged = { ...el, settings: { ...(el.settings || {}), ...overlaySettings } };
|
|
1932
|
+
const postRes = await axios.post(`${wpUrl}/wp-json/erc/v1/pages/${page_id}/elements/${widget_id}`, merged, { headers: { Authorization: authHeader, "Content-Type": "application/json" } });
|
|
1933
|
+
// Clear page cache so the updated overlay loads immediately
|
|
1934
|
+
await axios.post(`${wpUrl}/wp-json/erc/v1/site/clear-cache`, {}, { headers: { Authorization: authHeader }, params: { post_id: page_id } });
|
|
1935
|
+
return { content: [{ type: "text", text: JSON.stringify({ overlay_image_url, result: postRes.data }, null, 2) }] };
|
|
1936
|
+
}
|
|
1937
|
+
catch (error) {
|
|
1938
|
+
return { content: [{ type: "text", text: `Error updating video overlay: ${error.response?.data?.message || error.message}` }], isError: true };
|
|
1939
|
+
}
|
|
1940
|
+
});
|
|
1373
1941
|
return server;
|
|
1374
1942
|
}
|
|
1375
1943
|
// ─── Entry Point ──────────────────────────────────────────────────────────────
|
package/package.json
CHANGED