@yashwant.dharmdas/elementor-mcp 3.2.8 → 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.
Files changed (2) hide show
  1. package/dist/index.js +412 -0
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1370,6 +1370,418 @@ 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
+ });
1373
1785
  return server;
1374
1786
  }
1375
1787
  // ─── Entry Point ──────────────────────────────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yashwant.dharmdas/elementor-mcp",
3
- "version": "3.2.8",
3
+ "version": "3.3.0",
4
4
  "description": "MCP server for controlling Elementor via Claude — supports multiple WordPress sites",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",