@yashwant.dharmdas/elementor-mcp 3.15.0 → 3.16.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 +201 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1812,7 +1812,7 @@ function createMcpServer(sites) {
|
|
|
1812
1812
|
return { content: [{ type: "text", text: `Error setting rotation: ${error.response?.data?.message || error.message}` }], isError: true };
|
|
1813
1813
|
}
|
|
1814
1814
|
});
|
|
1815
|
-
server.tool("update-data", "
|
|
1815
|
+
server.tool("update-data", "⚠️ HEAVY HAMMER — use ONLY for first-time page scaffold. Replaces the ENTIRE Elementor element tree of a page.\n\nWHEN TO USE: First creation of a brand-new page from scratch (one POST).\n\nWHEN NOT TO USE: Any edit to an existing page. Use surgical tools instead — they're faster, safer, and don't require you to reconstruct the whole tree:\n • Text change → set-text-content\n • Button text + link → set-button\n • Image swap → set-image-widget-src\n • Container layout (flex, justify, align) → set-container-layout\n • Container background → set-container-background\n • Typography → set-element-typography\n • Spacing / padding / margin → set-element-spacing\n • Border / shadow / radius → set-element-border / set-element-shadow\n • Add a widget → add-widget-to-page / insert-element / clone-element\n • Move/reorder element → move-element\n • Delete an element → delete-element\n • Swap widget type → change-widget-type\n • Restructure a container's children → restructure-container\n • Same change on many elements → bulk-set-property\n • Anything else on settings → merge-element-settings\n\nSIZE LIMIT: There is no MCP-enforced size limit. The PHP backend writes via Elementor's $document->save() and accepts whatever your WordPress server's post_max_size allows (typically 64MB+). Do NOT fall back to Python scripts to 'work around a size limit' — there isn't one. If your payload is huge, you're using the wrong tool.\n\nBy default refuses if the new data is dramatically smaller than existing (protects against accidental wipeouts). Use force_replace=true to override.", {
|
|
1816
1816
|
page_id: z.string().describe("WordPress Page ID"),
|
|
1817
1817
|
elements_json: z.string().describe("Full elements array as stringified JSON"),
|
|
1818
1818
|
force_replace: z.boolean().optional().describe("Set true to allow replacing with dramatically fewer top-level elements"),
|
|
@@ -3805,6 +3805,206 @@ function createMcpServer(sites) {
|
|
|
3805
3805
|
return { content: [{ type: "text", text: `Error listing media library: ${error.response?.data?.message || error.message}` }], isError: true };
|
|
3806
3806
|
}
|
|
3807
3807
|
});
|
|
3808
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
3809
|
+
// ── v3.16.0 — Coverage gap closers ────────────────────────────────────────
|
|
3810
|
+
// These five tools fill the gaps that previously forced fallback to
|
|
3811
|
+
// update-data (the heavy hammer). After v3.16.0, update-data should ONLY
|
|
3812
|
+
// be used for the very first scaffold of a new page. Every subsequent
|
|
3813
|
+
// change uses one of the surgical tools below or an earlier typed setter.
|
|
3814
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
3815
|
+
// ── delete-element ────────────────────────────────────────────────────────
|
|
3816
|
+
server.tool("delete-element", "Remove an Elementor element from a page by ID. Surgical, server-side, no full-page download. Works for any element (widget, container, section). For top-level sections you must pass force=true to confirm — protects against accidental wipeouts. After deleting, the parent container's children array is automatically resaved. Use this INSTEAD of update-data when you want to remove just one element.", {
|
|
3817
|
+
page_id: z.string().describe("WordPress Page ID"),
|
|
3818
|
+
element_id: z.string().describe("Elementor Element ID to remove"),
|
|
3819
|
+
force: z.boolean().optional().describe("Required true to delete a top-level section (safety guard). Children/widgets inside a container delete without force."),
|
|
3820
|
+
site: siteParam,
|
|
3821
|
+
}, async ({ page_id, element_id, force, site }) => {
|
|
3822
|
+
try {
|
|
3823
|
+
const { wpUrl, authHeader } = resolveSite(sites, site);
|
|
3824
|
+
const r = await axios.delete(`${wpUrl}/wp-json/erc/v1/pages/${page_id}/elements/${element_id}`, {
|
|
3825
|
+
headers: { Authorization: authHeader },
|
|
3826
|
+
params: { force_delete: force === true ? "true" : "false" },
|
|
3827
|
+
});
|
|
3828
|
+
return { content: [{ type: "text", text: JSON.stringify(r.data, null, 2) }] };
|
|
3829
|
+
}
|
|
3830
|
+
catch (error) {
|
|
3831
|
+
return { content: [{ type: "text", text: `Error deleting element: ${error.response?.data?.message || error.message}` }], isError: true };
|
|
3832
|
+
}
|
|
3833
|
+
});
|
|
3834
|
+
// ── change-widget-type ────────────────────────────────────────────────────
|
|
3835
|
+
server.tool("change-widget-type", "Swap a widget's type (e.g. heading → button, text-editor → heading) while keeping its position in the page. Old settings are cleared (they belong to the OLD widget type and wouldn't render under the new one); pass new_settings to seed the new widget. Use this INSTEAD of delete + insert when you want to keep the same element ID and avoid disturbing surrounding flex order. NEVER use update-data for type swaps.", {
|
|
3836
|
+
page_id: z.string().describe("WordPress Page ID"),
|
|
3837
|
+
element_id: z.string().describe("Existing widget ID to convert"),
|
|
3838
|
+
new_widget: z.enum(["heading", "text-editor", "button", "image", "icon", "icon-box", "spacer", "divider", "video", "html", "shortcode"]).describe("Target widgetType"),
|
|
3839
|
+
new_settings: z.record(z.any()).optional().describe("Initial settings for the new widget (e.g. { title: 'Click me', link: { url: '/contact' } }). Empty by default."),
|
|
3840
|
+
site: siteParam,
|
|
3841
|
+
}, async ({ page_id, element_id, new_widget, new_settings, site }) => {
|
|
3842
|
+
try {
|
|
3843
|
+
const { wpUrl, authHeader } = resolveSite(sites, site);
|
|
3844
|
+
// GET current element
|
|
3845
|
+
const getRes = await axios.get(`${wpUrl}/wp-json/erc/v1/pages/${page_id}/elements/${element_id}`, { headers: { Authorization: authHeader } });
|
|
3846
|
+
const current = getRes.data;
|
|
3847
|
+
if (current.elType !== "widget") {
|
|
3848
|
+
return { content: [{ type: "text", text: `Element ${element_id} is elType="${current.elType}", not "widget". change-widget-type only works on widgets. For containers, use restructure-container.` }], isError: true };
|
|
3849
|
+
}
|
|
3850
|
+
// POST with new widgetType + fresh settings (top-level array_merge in PHP will replace)
|
|
3851
|
+
const body = {
|
|
3852
|
+
id: element_id,
|
|
3853
|
+
elType: "widget",
|
|
3854
|
+
widgetType: new_widget,
|
|
3855
|
+
settings: new_settings ?? {},
|
|
3856
|
+
elements: [], // widgets don't have children
|
|
3857
|
+
};
|
|
3858
|
+
const postRes = await axios.post(`${wpUrl}/wp-json/erc/v1/pages/${page_id}/elements/${element_id}`, body, { headers: { Authorization: authHeader, "Content-Type": "application/json" } });
|
|
3859
|
+
return { content: [{ type: "text", text: JSON.stringify({ from: current.widgetType, to: new_widget, ...postRes.data }, null, 2) }] };
|
|
3860
|
+
}
|
|
3861
|
+
catch (error) {
|
|
3862
|
+
return { content: [{ type: "text", text: `Error changing widget type: ${error.response?.data?.message || error.message}` }], isError: true };
|
|
3863
|
+
}
|
|
3864
|
+
});
|
|
3865
|
+
// ── restructure-container ─────────────────────────────────────────────────
|
|
3866
|
+
server.tool("restructure-container", "Replace a container's entire children array — the surgical way to split one column into two, add a column, reorder children, or rebuild a container's internal layout WITHOUT touching anything outside that container. Children are described in the same typed format as create-section's children param (container / heading / text / button / image / spacer). The container's own settings (flex direction, padding, background) are preserved. Use this INSTEAD of update-data for ANY structural change inside a single container.", {
|
|
3867
|
+
page_id: z.string().describe("WordPress Page ID"),
|
|
3868
|
+
container_id: z.string().describe("Container Element ID whose children you want to replace"),
|
|
3869
|
+
children: z.array(z.any()).describe("New children array in typed format. Same schema as create-section's `children` field. Example: [{ type:'container', flex_direction:'column', children:[ {type:'heading', text:'Hi'} ] }]"),
|
|
3870
|
+
site: siteParam,
|
|
3871
|
+
}, async ({ page_id, container_id, children, site }) => {
|
|
3872
|
+
try {
|
|
3873
|
+
const { wpUrl, authHeader } = resolveSite(sites, site);
|
|
3874
|
+
// GET current container
|
|
3875
|
+
const getRes = await axios.get(`${wpUrl}/wp-json/erc/v1/pages/${page_id}/elements/${container_id}`, { headers: { Authorization: authHeader } });
|
|
3876
|
+
const current = getRes.data;
|
|
3877
|
+
if (current.elType !== "container") {
|
|
3878
|
+
return { content: [{ type: "text", text: `Element ${container_id} is elType="${current.elType}", not "container". restructure-container only works on containers.` }], isError: true };
|
|
3879
|
+
}
|
|
3880
|
+
// Build the new children array via the shared buildChild helper
|
|
3881
|
+
const builtChildren = children.map((c) => buildChild(c));
|
|
3882
|
+
// POST back the container with its own id/elType/settings preserved + new children
|
|
3883
|
+
const body = {
|
|
3884
|
+
id: container_id,
|
|
3885
|
+
elType: "container",
|
|
3886
|
+
settings: current.settings || {},
|
|
3887
|
+
elements: builtChildren,
|
|
3888
|
+
};
|
|
3889
|
+
const postRes = await axios.post(`${wpUrl}/wp-json/erc/v1/pages/${page_id}/elements/${container_id}`, body, { headers: { Authorization: authHeader, "Content-Type": "application/json" } });
|
|
3890
|
+
return { content: [{ type: "text", text: JSON.stringify({ container_id, new_child_count: builtChildren.length, ...postRes.data }, null, 2) }] };
|
|
3891
|
+
}
|
|
3892
|
+
catch (error) {
|
|
3893
|
+
return { content: [{ type: "text", text: `Error restructuring container: ${error.response?.data?.message || error.message}` }], isError: true };
|
|
3894
|
+
}
|
|
3895
|
+
});
|
|
3896
|
+
// ── bulk-set-property ─────────────────────────────────────────────────────
|
|
3897
|
+
server.tool("bulk-set-property", "Apply one settings patch to every element on a page matching a filter — e.g. 'set color = primary global on all h2 headings', 'add padding 24px to every button', 'set background = light-bg on every container with css_class=\"card\"'. Runs the filter in-memory on the page tree, then issues a parallel batch of merge-element-settings calls. Use this INSTEAD of looping with update-data when you need the same change in many places.", {
|
|
3898
|
+
page_id: z.string().describe("WordPress Page ID"),
|
|
3899
|
+
match_elType: z.enum(["widget", "container", "section", "column"]).optional().describe("Filter by elType (e.g. 'container' to target only containers)"),
|
|
3900
|
+
match_widget_type: z.string().optional().describe("Filter by widgetType (e.g. 'heading', 'button', 'image')"),
|
|
3901
|
+
match_css_class: z.string().optional().describe("Filter by CSS class substring (matches if class list contains this token)"),
|
|
3902
|
+
match_text_value: z.string().optional().describe("Filter by current text contents (case-insensitive substring match against title/editor/text settings)"),
|
|
3903
|
+
settings: z.record(z.any()).describe("Settings patch to deep-merge into every matched element (same shape as merge-element-settings)"),
|
|
3904
|
+
dry_run: z.boolean().optional().describe("Return matched IDs without applying (default false)"),
|
|
3905
|
+
site: siteParam,
|
|
3906
|
+
}, async ({ page_id, match_elType, match_widget_type, match_css_class, match_text_value, settings, dry_run, site }) => {
|
|
3907
|
+
try {
|
|
3908
|
+
const { wpUrl, authHeader } = resolveSite(sites, site);
|
|
3909
|
+
if (!match_elType && !match_widget_type && !match_css_class && !match_text_value) {
|
|
3910
|
+
return { content: [{ type: "text", text: "Refusing to operate without at least one match_* filter — that would touch every element on the page. Add at least one filter parameter." }], isError: true };
|
|
3911
|
+
}
|
|
3912
|
+
// Pull the full page tree once
|
|
3913
|
+
const pageRes = await axios.get(`${wpUrl}/wp-json/erc/v1/pages/${page_id}/data`, { headers: { Authorization: authHeader } });
|
|
3914
|
+
const elements = pageRes.data?.elements ?? pageRes.data ?? [];
|
|
3915
|
+
const matched = [];
|
|
3916
|
+
const txtNeedle = match_text_value?.toLowerCase();
|
|
3917
|
+
const walk = (els) => {
|
|
3918
|
+
for (const el of els) {
|
|
3919
|
+
const elT = el.elType;
|
|
3920
|
+
const wT = el.widgetType;
|
|
3921
|
+
const cls = (el?.settings?._css_classes ?? "");
|
|
3922
|
+
const textVals = [el?.settings?.title, el?.settings?.editor, el?.settings?.text]
|
|
3923
|
+
.filter((v) => typeof v === "string")
|
|
3924
|
+
.map((v) => v.toLowerCase());
|
|
3925
|
+
let ok = true;
|
|
3926
|
+
if (match_elType && elT !== match_elType)
|
|
3927
|
+
ok = false;
|
|
3928
|
+
if (match_widget_type && wT !== match_widget_type)
|
|
3929
|
+
ok = false;
|
|
3930
|
+
if (match_css_class && !String(cls).split(/\s+/).includes(match_css_class))
|
|
3931
|
+
ok = false;
|
|
3932
|
+
if (txtNeedle && !textVals.some((v) => v.includes(txtNeedle)))
|
|
3933
|
+
ok = false;
|
|
3934
|
+
if (ok && el.id) {
|
|
3935
|
+
matched.push({ id: el.id, elType: elT, widgetType: wT });
|
|
3936
|
+
}
|
|
3937
|
+
if (Array.isArray(el.elements) && el.elements.length > 0)
|
|
3938
|
+
walk(el.elements);
|
|
3939
|
+
}
|
|
3940
|
+
};
|
|
3941
|
+
walk(elements);
|
|
3942
|
+
if (dry_run) {
|
|
3943
|
+
return { content: [{ type: "text", text: JSON.stringify({ dry_run: true, matched_count: matched.length, matched }, null, 2) }] };
|
|
3944
|
+
}
|
|
3945
|
+
// Apply in parallel — each one is its own GET+merge+POST, but they're independent
|
|
3946
|
+
const results = await Promise.allSettled(matched.map((m) => applyElementSettings(wpUrl, authHeader, page_id, m.id, settings)));
|
|
3947
|
+
const okCount = results.filter((r) => r.status === "fulfilled").length;
|
|
3948
|
+
const failures = results
|
|
3949
|
+
.map((r, i) => r.status === "rejected" ? { id: matched[i].id, error: String(r.reason) } : null)
|
|
3950
|
+
.filter(Boolean);
|
|
3951
|
+
return { content: [{ type: "text", text: JSON.stringify({ matched_count: matched.length, applied: okCount, failed: failures.length, failures }, null, 2) }] };
|
|
3952
|
+
}
|
|
3953
|
+
catch (error) {
|
|
3954
|
+
return { content: [{ type: "text", text: `Error in bulk-set-property: ${error.response?.data?.message || error.message}` }], isError: true };
|
|
3955
|
+
}
|
|
3956
|
+
});
|
|
3957
|
+
// ── set-button ────────────────────────────────────────────────────────────
|
|
3958
|
+
server.tool("set-button", "One-call setter for a button widget — sets text + link + (optional) icon + (optional) size/style in one shot. Replaces the 'set-text-content then set-element-link' two-step. Use this for ANY button content change. Idempotent: omit a field to leave it unchanged.", {
|
|
3959
|
+
page_id: z.string().describe("WordPress Page ID"),
|
|
3960
|
+
element_id: z.string().describe("Button widget ID"),
|
|
3961
|
+
text: z.string().optional().describe("Button label text"),
|
|
3962
|
+
url: z.string().optional().describe("Button link URL"),
|
|
3963
|
+
open_in_new_tab: z.boolean().optional().describe("Native Elementor 'Open in new window' toggle"),
|
|
3964
|
+
nofollow: z.boolean().optional().describe("Native Elementor 'Add nofollow' toggle"),
|
|
3965
|
+
icon: z.string().optional().describe("Elementor icon library value, e.g. 'fas fa-arrow-right'. Omit to leave icon alone."),
|
|
3966
|
+
icon_align: z.enum(["left", "right"]).optional().describe("Icon position relative to text"),
|
|
3967
|
+
button_size: z.enum(["xs", "sm", "md", "lg", "xl"]).optional().describe("Elementor preset size"),
|
|
3968
|
+
button_type: z.enum(["default", "info", "success", "warning", "danger"]).optional().describe("Elementor color preset"),
|
|
3969
|
+
site: siteParam,
|
|
3970
|
+
}, async ({ page_id, element_id, text, url, open_in_new_tab, nofollow, icon, icon_align, button_size, button_type, site }) => {
|
|
3971
|
+
try {
|
|
3972
|
+
const { wpUrl, authHeader } = resolveSite(sites, site);
|
|
3973
|
+
// 1) Settings patch — text/icon/size/type via applyElementSettings
|
|
3974
|
+
const s = {};
|
|
3975
|
+
if (text !== undefined)
|
|
3976
|
+
s.text = text;
|
|
3977
|
+
if (icon !== undefined)
|
|
3978
|
+
s.selected_icon = { value: icon, library: "fa-solid" };
|
|
3979
|
+
if (icon_align !== undefined)
|
|
3980
|
+
s.icon_align = icon_align;
|
|
3981
|
+
if (button_size !== undefined)
|
|
3982
|
+
s.size = button_size;
|
|
3983
|
+
if (button_type !== undefined)
|
|
3984
|
+
s.button_type = button_type;
|
|
3985
|
+
let settingsResult = null;
|
|
3986
|
+
if (Object.keys(s).length > 0) {
|
|
3987
|
+
settingsResult = await applyElementSettings(wpUrl, authHeader, page_id, element_id, s);
|
|
3988
|
+
}
|
|
3989
|
+
// 2) Link — via the dedicated /link endpoint (handles is_external, nofollow natively)
|
|
3990
|
+
let linkResult = null;
|
|
3991
|
+
if (url !== undefined || open_in_new_tab !== undefined || nofollow !== undefined) {
|
|
3992
|
+
const body = {};
|
|
3993
|
+
if (url !== undefined)
|
|
3994
|
+
body.url = url;
|
|
3995
|
+
if (open_in_new_tab !== undefined)
|
|
3996
|
+
body.open_in_new_tab = open_in_new_tab;
|
|
3997
|
+
if (nofollow !== undefined)
|
|
3998
|
+
body.nofollow = nofollow;
|
|
3999
|
+
const r = await axios.post(`${wpUrl}/wp-json/erc/v1/pages/${page_id}/elements/${element_id}/link`, body, { headers: { Authorization: authHeader, "Content-Type": "application/json" } });
|
|
4000
|
+
linkResult = r.data;
|
|
4001
|
+
}
|
|
4002
|
+
return { content: [{ type: "text", text: JSON.stringify({ settings: settingsResult, link: linkResult }, null, 2) }] };
|
|
4003
|
+
}
|
|
4004
|
+
catch (error) {
|
|
4005
|
+
return { content: [{ type: "text", text: `Error setting button: ${error.response?.data?.message || error.message}` }], isError: true };
|
|
4006
|
+
}
|
|
4007
|
+
});
|
|
3808
4008
|
// ── validate-page-structure ───────────────────────────────────────────────
|
|
3809
4009
|
server.tool("validate-page-structure", "Post-build sanity check on a page. Returns a report of structural / standards violations: header widgets in page body (should be a template), images with external URLs missing WP attachment IDs, hardcoded fonts/colors that should use kit globals, legacy _element_width on flex children, missing image alt text, suspicious min-heights. Call this AFTER building a page from Figma. If errors are found, fix them with the typed tools and re-run. If only warnings remain, surface them to the human for review.", {
|
|
3810
4010
|
page_id: z.string().describe("WordPress Page ID to validate"),
|
package/package.json
CHANGED