@yashwant.dharmdas/elementor-mcp 3.12.0 → 3.14.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 +1233 -15
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -163,6 +163,115 @@ function createMcpServer(sites) {
|
|
|
163
163
|
const putRes = await axios.put(`${wpUrl}/wp-json/erc/v1/pages/${page_id}/data`, transformed, { headers: { Authorization: authHeader, "Content-Type": "application/json" } });
|
|
164
164
|
return putRes.data;
|
|
165
165
|
};
|
|
166
|
+
// ── Shared helper: merge settings into a specific Elementor element ───────
|
|
167
|
+
// Used by all the typed wrapper tools (set-container-background, set-element-spacing, etc.)
|
|
168
|
+
// GET element → merge new settings keys → POST full element back.
|
|
169
|
+
const applyElementSettings = async (wpUrl, authHeader, page_id, element_id, newSettings) => {
|
|
170
|
+
// Strip undefined values so they don't clobber existing settings
|
|
171
|
+
const clean = {};
|
|
172
|
+
for (const [k, v] of Object.entries(newSettings)) {
|
|
173
|
+
if (v !== undefined && v !== null)
|
|
174
|
+
clean[k] = v;
|
|
175
|
+
}
|
|
176
|
+
if (Object.keys(clean).length === 0) {
|
|
177
|
+
return { success: true, message: "No settings to apply (all params undefined)", element_id };
|
|
178
|
+
}
|
|
179
|
+
const getRes = await axios.get(`${wpUrl}/wp-json/erc/v1/pages/${page_id}/elements/${element_id}`, { headers: { Authorization: authHeader } });
|
|
180
|
+
const merged = {
|
|
181
|
+
...getRes.data,
|
|
182
|
+
settings: { ...(getRes.data.settings || {}), ...clean },
|
|
183
|
+
};
|
|
184
|
+
const postRes = await axios.post(`${wpUrl}/wp-json/erc/v1/pages/${page_id}/elements/${element_id}`, merged, { headers: { Authorization: authHeader, "Content-Type": "application/json" } });
|
|
185
|
+
return postRes.data;
|
|
186
|
+
};
|
|
187
|
+
// ── Spacing parser: "120 48 120 48" or "5%" → Elementor structured object ─
|
|
188
|
+
// Accepts: single value ("5%"), two values ("100 0"), four values ("120 48 120 48").
|
|
189
|
+
// Returns Elementor's { unit, top, right, bottom, left, isLinked } shape.
|
|
190
|
+
const parseSpacing = (input) => {
|
|
191
|
+
if (input === undefined || input === null || input === "")
|
|
192
|
+
return undefined;
|
|
193
|
+
const trimmed = String(input).trim();
|
|
194
|
+
if (!trimmed)
|
|
195
|
+
return undefined;
|
|
196
|
+
// Detect unit suffix
|
|
197
|
+
let unit = "px";
|
|
198
|
+
let valuesStr = trimmed;
|
|
199
|
+
const unitMatch = trimmed.match(/(%|px|em|rem|vh|vw)$/i);
|
|
200
|
+
if (unitMatch) {
|
|
201
|
+
unit = unitMatch[1].toLowerCase();
|
|
202
|
+
valuesStr = trimmed.slice(0, -unitMatch[1].length).trim();
|
|
203
|
+
}
|
|
204
|
+
const values = valuesStr.split(/[\s,]+/).filter(Boolean);
|
|
205
|
+
if (values.length === 0)
|
|
206
|
+
return undefined;
|
|
207
|
+
let top, right, bottom, left;
|
|
208
|
+
let isLinked = false;
|
|
209
|
+
if (values.length === 1) {
|
|
210
|
+
top = right = bottom = left = values[0];
|
|
211
|
+
isLinked = true;
|
|
212
|
+
}
|
|
213
|
+
else if (values.length === 2) {
|
|
214
|
+
top = bottom = values[0];
|
|
215
|
+
right = left = values[1];
|
|
216
|
+
}
|
|
217
|
+
else if (values.length === 3) {
|
|
218
|
+
top = values[0];
|
|
219
|
+
right = left = values[1];
|
|
220
|
+
bottom = values[2];
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
[top, right, bottom, left] = values;
|
|
224
|
+
}
|
|
225
|
+
return { unit, top, right, bottom, left, isLinked };
|
|
226
|
+
};
|
|
227
|
+
// ── Size parser: "420px" / "5vh" / "100%" → { unit, size } ────────────────
|
|
228
|
+
const parseSize = (input) => {
|
|
229
|
+
if (input === undefined || input === null || input === "")
|
|
230
|
+
return undefined;
|
|
231
|
+
const str = String(input).trim();
|
|
232
|
+
if (!str)
|
|
233
|
+
return undefined;
|
|
234
|
+
const m = str.match(/^(-?\d+(?:\.\d+)?)\s*(%|px|em|rem|vh|vw|deg|fr)?$/i);
|
|
235
|
+
if (!m)
|
|
236
|
+
return undefined;
|
|
237
|
+
return { unit: (m[2] || "px").toLowerCase(), size: parseFloat(m[1]) };
|
|
238
|
+
};
|
|
239
|
+
// ── Gap parser: "64" / "64 32" / "20px" → { unit, column, row, isLinked } ─
|
|
240
|
+
const parseGap = (input) => {
|
|
241
|
+
if (input === undefined || input === null || input === "")
|
|
242
|
+
return undefined;
|
|
243
|
+
const str = String(input).trim();
|
|
244
|
+
if (!str)
|
|
245
|
+
return undefined;
|
|
246
|
+
let unit = "px";
|
|
247
|
+
let valStr = str;
|
|
248
|
+
const m = str.match(/(%|px|em|rem|vh|vw)$/i);
|
|
249
|
+
if (m) {
|
|
250
|
+
unit = m[1].toLowerCase();
|
|
251
|
+
valStr = str.slice(0, -m[1].length).trim();
|
|
252
|
+
}
|
|
253
|
+
const parts = valStr.split(/[\s,]+/).filter(Boolean);
|
|
254
|
+
if (parts.length === 0)
|
|
255
|
+
return undefined;
|
|
256
|
+
const col = parts[0];
|
|
257
|
+
const row = parts[1] ?? parts[0];
|
|
258
|
+
return { unit, column: col, row, isLinked: parts.length === 1 };
|
|
259
|
+
};
|
|
260
|
+
// ── Globals resolver: "primary" → {__globals__: {<key>: "globals/colors?id=primary"}} ─
|
|
261
|
+
// Or accepts raw hex like "#FFFFFF" → returns just the value
|
|
262
|
+
const resolveColor = (value) => {
|
|
263
|
+
if (!value)
|
|
264
|
+
return {};
|
|
265
|
+
const v = String(value).trim();
|
|
266
|
+
if (!v)
|
|
267
|
+
return {};
|
|
268
|
+
// If it looks like a hex/rgb/keyword, return as plain value
|
|
269
|
+
if (v.startsWith("#") || v.startsWith("rgb") || v.startsWith("hsl") || v.startsWith("var(")) {
|
|
270
|
+
return { value: v };
|
|
271
|
+
}
|
|
272
|
+
// Otherwise treat as a global color slug
|
|
273
|
+
return { global_id: `globals/colors?id=${v}` };
|
|
274
|
+
};
|
|
166
275
|
// ── Meta: list-sites ──────────────────────────────────────────────────────
|
|
167
276
|
server.tool("list-sites", "List all configured WordPress sites available to this MCP server.", {}, async () => {
|
|
168
277
|
if (sites.length === 0) {
|
|
@@ -202,16 +311,19 @@ function createMcpServer(sites) {
|
|
|
202
311
|
return { content: [{ type: "text", text: `Error fetching page data: ${error.response?.data?.message || error.message}` }], isError: true };
|
|
203
312
|
}
|
|
204
313
|
});
|
|
205
|
-
server.tool("find-elements", "Search the Elementor element tree of a page SERVER-SIDE and return ONLY matching elements — never downloads the full JSON.
|
|
314
|
+
server.tool("find-elements", "Search the Elementor element tree of a page SERVER-SIDE and return ONLY matching elements — never downloads the full JSON. All filters AND together. Example uses: find buttons with no URL (widget_type='button' + has_empty='link.url'); find elements with a specific custom class (css_class='Mbutton'); find every page that embeds a specific template (template_id='3287'); audit all containers with background images (has_background_image=true).", {
|
|
206
315
|
page_id: z.string().describe("WordPress Page ID"),
|
|
207
316
|
el_type: z.string().optional().describe("Filter by elType: 'widget' or 'container'"),
|
|
208
|
-
widget_type: z.string().optional().describe("Filter by widgetType e.g. 'button', 'heading', 'image', 'text-editor'"),
|
|
317
|
+
widget_type: z.string().optional().describe("Filter by widgetType e.g. 'button', 'heading', 'image', 'text-editor', 'template'"),
|
|
209
318
|
contains: z.string().optional().describe("Substring that must appear anywhere in the element's settings JSON"),
|
|
210
319
|
has_empty: z.string().optional().describe("Dot-notation setting path that must be empty/missing — e.g. 'link.url' finds buttons with no URL"),
|
|
320
|
+
css_class: z.string().optional().describe("Custom CSS class that must be in _css_classes — finds elements with a specific class, e.g. 'Mbutton', 'blogcontent'"),
|
|
321
|
+
template_id: z.string().optional().describe("Find template widgets that embed a specific template ID — useful for finding/replacing hero/CF/footer template references sitewide"),
|
|
322
|
+
has_background_image: z.boolean().optional().describe("Set true to match only containers with background_image.url set — useful for hero image audits"),
|
|
211
323
|
return_keys: z.array(z.string()).optional().describe("Limit which settings keys are returned — e.g. ['text','link'] to keep response small"),
|
|
212
324
|
include_path: z.boolean().optional().describe("Include ancestor element IDs in each result"),
|
|
213
325
|
site: siteParam,
|
|
214
|
-
}, async ({ page_id, el_type, widget_type, contains, has_empty, return_keys, include_path, site }) => {
|
|
326
|
+
}, async ({ page_id, el_type, widget_type, contains, has_empty, css_class, template_id, has_background_image, return_keys, include_path, site }) => {
|
|
215
327
|
try {
|
|
216
328
|
const { wpUrl, authHeader } = resolveSite(sites, site);
|
|
217
329
|
const body = {};
|
|
@@ -223,6 +335,12 @@ function createMcpServer(sites) {
|
|
|
223
335
|
body.contains = contains;
|
|
224
336
|
if (has_empty)
|
|
225
337
|
body.has_empty = has_empty;
|
|
338
|
+
if (css_class)
|
|
339
|
+
body.css_class = css_class;
|
|
340
|
+
if (template_id)
|
|
341
|
+
body.template_id = template_id;
|
|
342
|
+
if (has_background_image)
|
|
343
|
+
body.has_background_image = has_background_image;
|
|
226
344
|
if (return_keys)
|
|
227
345
|
body.return_keys = return_keys;
|
|
228
346
|
if (include_path)
|
|
@@ -353,10 +471,10 @@ function createMcpServer(sites) {
|
|
|
353
471
|
return { content: [{ type: "text", text: `Error moving element: ${error.response?.data?.message || error.message}` }], isError: true };
|
|
354
472
|
}
|
|
355
473
|
});
|
|
356
|
-
server.tool("merge-element-settings", "Deep-merge settings into a specific Elementor element without replacing the full element. Use for
|
|
474
|
+
server.tool("merge-element-settings", "Deep-merge settings into a specific Elementor element without replacing the full element. ALWAYS prefer a dedicated typed tool when one exists (set-container-background, set-element-spacing, set-container-layout, set-element-width, set-element-min-height, set-element-border, set-element-position, set-element-sticky, set-element-motion-effects, set-element-flex-order, set-element-css-class, set-posts-widget-filter, set-element-link, hide-page-title). Use this fallback only for niche keys with no dedicated tool. For global tokens use the __globals__ object — e.g. {\"__globals__\":{\"text_color\":\"globals/colors?id=primary\",\"typography_typography\":\"globals/typography?id=heading\"}} sets a heading's color + typography from the kit.", {
|
|
357
475
|
page_id: z.string().describe("WordPress Page ID"),
|
|
358
476
|
element_id: z.string().describe("Elementor Element ID"),
|
|
359
|
-
settings: z.string().describe("JSON object of settings to merge (stringified)"),
|
|
477
|
+
settings: z.string().describe("JSON object of settings to merge (stringified). Use __globals__ key to reference kit tokens, e.g. '{\"__globals__\":{\"text_color\":\"globals/colors?id=primary\"}}'"),
|
|
360
478
|
site: siteParam,
|
|
361
479
|
}, async ({ page_id, element_id, settings, site }) => {
|
|
362
480
|
let parsedSettings;
|
|
@@ -368,15 +486,1089 @@ function createMcpServer(sites) {
|
|
|
368
486
|
}
|
|
369
487
|
try {
|
|
370
488
|
const { wpUrl, authHeader } = resolveSite(sites, site);
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
const postRes = await axios.post(`${wpUrl}/wp-json/erc/v1/pages/${page_id}/elements/${element_id}`, merged, { headers: { Authorization: authHeader } });
|
|
374
|
-
return { content: [{ type: "text", text: JSON.stringify(postRes.data, null, 2) }] };
|
|
489
|
+
const result = await applyElementSettings(wpUrl, authHeader, page_id, element_id, parsedSettings);
|
|
490
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
375
491
|
}
|
|
376
492
|
catch (error) {
|
|
377
493
|
return { content: [{ type: "text", text: `Error merging settings: ${error.response?.data?.message || error.message}` }], isError: true };
|
|
378
494
|
}
|
|
379
495
|
});
|
|
496
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
497
|
+
// ── v3.13.0 — TYPED SETTING WRAPPERS — built for Elementor web-design skill ─
|
|
498
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
499
|
+
// All wrappers call applyElementSettings() under the hood. They translate
|
|
500
|
+
// clean parameter names into Elementor's quirky setting keys so the AI
|
|
501
|
+
// doesn't have to memorize them.
|
|
502
|
+
// ── 1. set-container-background ───────────────────────────────────────────
|
|
503
|
+
server.tool("set-container-background", "Set background of any container element — color, image, gradient — including overlay. Handles all background_* settings + responsive variants in one call. Use color='primary' to reference a kit global, or color='#FFFFFF' for hex. For gradient, pass background_type='gradient' + color + color_b. For an image with overlay, pass image_url + overlay_color + overlay_opacity. Used on every hero, CF section, team section, decorative background.", {
|
|
504
|
+
page_id: z.string().describe("WordPress Page ID"),
|
|
505
|
+
element_id: z.string().describe("Elementor container/widget ID"),
|
|
506
|
+
background_type: z.enum(["classic", "gradient", "video", "slideshow", "none"]).optional().describe("'classic' = solid/image, 'gradient' = two-color gradient, 'none' = clear background"),
|
|
507
|
+
color: z.string().optional().describe("Background color hex (e.g. '#FFFFFF') OR global slug (e.g. 'primary'). Use 'primary'/'secondary'/'accent' for kit globals."),
|
|
508
|
+
color_b: z.string().optional().describe("Gradient end color (only when background_type='gradient'). Hex or global slug."),
|
|
509
|
+
gradient_angle: z.number().optional().describe("Gradient angle in degrees (0–360). Default 180 (top to bottom)."),
|
|
510
|
+
gradient_type: z.enum(["linear", "radial"]).optional().describe("Gradient direction type. Default 'linear'."),
|
|
511
|
+
image_url: z.string().optional().describe("Background image URL (full https URL)"),
|
|
512
|
+
image_url_tablet: z.string().optional().describe("Tablet background image URL"),
|
|
513
|
+
image_url_mobile: z.string().optional().describe("Mobile background image URL"),
|
|
514
|
+
position: z.string().optional().describe("Background position e.g. 'center center', 'top left', 'bottom right'"),
|
|
515
|
+
position_tablet: z.string().optional(),
|
|
516
|
+
position_mobile: z.string().optional(),
|
|
517
|
+
size: z.enum(["cover", "contain", "auto"]).optional().describe("Background size — 'cover' (most common), 'contain', or 'auto'"),
|
|
518
|
+
repeat: z.enum(["no-repeat", "repeat", "repeat-x", "repeat-y"]).optional(),
|
|
519
|
+
overlay_color: z.string().optional().describe("Overlay color hex or global slug — adds a tinted layer over the background image"),
|
|
520
|
+
overlay_image_url: z.string().optional().describe("Overlay image URL — used for decorative SVG watermarks on top of background"),
|
|
521
|
+
overlay_opacity: z.number().min(0).max(1).optional().describe("Overlay opacity 0–1 (e.g. 0.4 for 40% dark overlay)"),
|
|
522
|
+
overlay_blend_mode: z.enum(["normal", "multiply", "screen", "overlay", "darken", "lighten", "color-dodge", "color-burn", "hard-light", "soft-light", "difference", "exclusion", "hue", "saturation", "color", "luminosity"]).optional().describe("CSS blend mode — 'multiply' is common for tinted images"),
|
|
523
|
+
site: siteParam,
|
|
524
|
+
}, async ({ page_id, element_id, background_type, color, color_b, gradient_angle, gradient_type, image_url, image_url_tablet, image_url_mobile, position, position_tablet, position_mobile, size, repeat, overlay_color, overlay_image_url, overlay_opacity, overlay_blend_mode, site }) => {
|
|
525
|
+
try {
|
|
526
|
+
const { wpUrl, authHeader } = resolveSite(sites, site);
|
|
527
|
+
const s = {};
|
|
528
|
+
if (background_type)
|
|
529
|
+
s.background_background = background_type === "none" ? "" : background_type;
|
|
530
|
+
if (color) {
|
|
531
|
+
const c = resolveColor(color);
|
|
532
|
+
if (c.value)
|
|
533
|
+
s.background_color = c.value;
|
|
534
|
+
if (c.global_id)
|
|
535
|
+
s.__globals__ = { ...(s.__globals__ || {}), background_color: c.global_id };
|
|
536
|
+
}
|
|
537
|
+
if (color_b) {
|
|
538
|
+
const c2 = resolveColor(color_b);
|
|
539
|
+
if (c2.value)
|
|
540
|
+
s.background_color_b = c2.value;
|
|
541
|
+
if (c2.global_id)
|
|
542
|
+
s.__globals__ = { ...(s.__globals__ || {}), background_color_b: c2.global_id };
|
|
543
|
+
}
|
|
544
|
+
if (gradient_angle !== undefined)
|
|
545
|
+
s.background_gradient_angle = { unit: "deg", size: gradient_angle };
|
|
546
|
+
if (gradient_type)
|
|
547
|
+
s.background_gradient_type = gradient_type;
|
|
548
|
+
if (image_url)
|
|
549
|
+
s.background_image = { url: image_url, id: "" };
|
|
550
|
+
if (image_url_tablet)
|
|
551
|
+
s.background_image_tablet = { url: image_url_tablet, id: "" };
|
|
552
|
+
if (image_url_mobile)
|
|
553
|
+
s.background_image_mobile = { url: image_url_mobile, id: "" };
|
|
554
|
+
if (position)
|
|
555
|
+
s.background_position = position;
|
|
556
|
+
if (position_tablet)
|
|
557
|
+
s.background_position_tablet = position_tablet;
|
|
558
|
+
if (position_mobile)
|
|
559
|
+
s.background_position_mobile = position_mobile;
|
|
560
|
+
if (size)
|
|
561
|
+
s.background_size = size;
|
|
562
|
+
if (repeat)
|
|
563
|
+
s.background_repeat = repeat;
|
|
564
|
+
if (overlay_color || overlay_image_url || overlay_opacity !== undefined || overlay_blend_mode) {
|
|
565
|
+
s.background_overlay_background = "classic";
|
|
566
|
+
}
|
|
567
|
+
if (overlay_color) {
|
|
568
|
+
const c = resolveColor(overlay_color);
|
|
569
|
+
if (c.value)
|
|
570
|
+
s.background_overlay_color = c.value;
|
|
571
|
+
if (c.global_id)
|
|
572
|
+
s.__globals__ = { ...(s.__globals__ || {}), background_overlay_color: c.global_id };
|
|
573
|
+
}
|
|
574
|
+
if (overlay_image_url)
|
|
575
|
+
s.background_overlay_image = { url: overlay_image_url, id: "" };
|
|
576
|
+
if (overlay_opacity !== undefined)
|
|
577
|
+
s.background_overlay_opacity = { unit: "px", size: overlay_opacity };
|
|
578
|
+
if (overlay_blend_mode)
|
|
579
|
+
s.overlay_blend_mode = overlay_blend_mode;
|
|
580
|
+
const result = await applyElementSettings(wpUrl, authHeader, page_id, element_id, s);
|
|
581
|
+
return { content: [{ type: "text", text: JSON.stringify({ applied: s, result }, null, 2) }] };
|
|
582
|
+
}
|
|
583
|
+
catch (error) {
|
|
584
|
+
return { content: [{ type: "text", text: `Error setting background: ${error.response?.data?.message || error.message}` }], isError: true };
|
|
585
|
+
}
|
|
586
|
+
});
|
|
587
|
+
// ── 2. set-element-spacing ────────────────────────────────────────────────
|
|
588
|
+
server.tool("set-element-spacing", "Set padding and/or margin on any element across desktop/tablet/mobile. Accepts compact strings: '120 48 120 48' = top right bottom left in px, '5%' = all sides 5%, '100 0' = vertical horizontal. Supports negative values for margin: '-153 0 0 0' for negative top margin (header overlap technique). The #1 most-used setting after background.", {
|
|
589
|
+
page_id: z.string().describe("WordPress Page ID"),
|
|
590
|
+
element_id: z.string().describe("Elementor element ID"),
|
|
591
|
+
padding: z.string().optional().describe("Desktop padding. '120 48 120 48' (top right bottom left px), '5%' (all sides %), '100 0' (vertical horizontal)"),
|
|
592
|
+
padding_tablet: z.string().optional().describe("Tablet padding (same format as padding)"),
|
|
593
|
+
padding_mobile: z.string().optional().describe("Mobile padding (same format as padding)"),
|
|
594
|
+
margin: z.string().optional().describe("Desktop margin. Same format. Supports negatives: '-153 0 0 0' for negative top margin."),
|
|
595
|
+
margin_tablet: z.string().optional().describe("Tablet margin"),
|
|
596
|
+
margin_mobile: z.string().optional().describe("Mobile margin"),
|
|
597
|
+
site: siteParam,
|
|
598
|
+
}, async ({ page_id, element_id, padding, padding_tablet, padding_mobile, margin, margin_tablet, margin_mobile, site }) => {
|
|
599
|
+
try {
|
|
600
|
+
const { wpUrl, authHeader } = resolveSite(sites, site);
|
|
601
|
+
const s = {};
|
|
602
|
+
const p1 = parseSpacing(padding);
|
|
603
|
+
if (p1)
|
|
604
|
+
s.padding = p1;
|
|
605
|
+
const p2 = parseSpacing(padding_tablet);
|
|
606
|
+
if (p2)
|
|
607
|
+
s.padding_tablet = p2;
|
|
608
|
+
const p3 = parseSpacing(padding_mobile);
|
|
609
|
+
if (p3)
|
|
610
|
+
s.padding_mobile = p3;
|
|
611
|
+
const m1 = parseSpacing(margin);
|
|
612
|
+
if (m1)
|
|
613
|
+
s.margin = m1;
|
|
614
|
+
const m2 = parseSpacing(margin_tablet);
|
|
615
|
+
if (m2)
|
|
616
|
+
s.margin_tablet = m2;
|
|
617
|
+
const m3 = parseSpacing(margin_mobile);
|
|
618
|
+
if (m3)
|
|
619
|
+
s.margin_mobile = m3;
|
|
620
|
+
const result = await applyElementSettings(wpUrl, authHeader, page_id, element_id, s);
|
|
621
|
+
return { content: [{ type: "text", text: JSON.stringify({ applied: s, result }, null, 2) }] };
|
|
622
|
+
}
|
|
623
|
+
catch (error) {
|
|
624
|
+
return { content: [{ type: "text", text: `Error setting spacing: ${error.response?.data?.message || error.message}` }], isError: true };
|
|
625
|
+
}
|
|
626
|
+
});
|
|
627
|
+
// ── 3. set-container-layout ───────────────────────────────────────────────
|
|
628
|
+
server.tool("set-container-layout", "Set all flexbox layout properties on a container — direction, justify, align, wrap, gap, content_width — across desktop/tablet/mobile. THE most common container operation after background+spacing. For mobile stacking pass direction_mobile='column'. For boxed sites pass content_width='boxed' + boxed_width='1280'.", {
|
|
629
|
+
page_id: z.string().describe("WordPress Page ID"),
|
|
630
|
+
element_id: z.string().describe("Elementor container ID"),
|
|
631
|
+
direction: z.enum(["row", "column", "row-reverse", "column-reverse"]).optional().describe("Desktop flex direction"),
|
|
632
|
+
direction_tablet: z.enum(["row", "column", "row-reverse", "column-reverse"]).optional(),
|
|
633
|
+
direction_mobile: z.enum(["row", "column", "row-reverse", "column-reverse"]).optional().describe("Mobile direction — almost always 'column' for stacking"),
|
|
634
|
+
justify: z.enum(["flex-start", "center", "flex-end", "space-between", "space-around", "space-evenly"]).optional().describe("Desktop justify-content"),
|
|
635
|
+
justify_tablet: z.enum(["flex-start", "center", "flex-end", "space-between", "space-around", "space-evenly"]).optional(),
|
|
636
|
+
justify_mobile: z.enum(["flex-start", "center", "flex-end", "space-between", "space-around", "space-evenly"]).optional(),
|
|
637
|
+
align: z.enum(["flex-start", "center", "flex-end", "stretch"]).optional().describe("Desktop align-items"),
|
|
638
|
+
align_tablet: z.enum(["flex-start", "center", "flex-end", "stretch"]).optional(),
|
|
639
|
+
align_mobile: z.enum(["flex-start", "center", "flex-end", "stretch"]).optional(),
|
|
640
|
+
wrap: z.enum(["nowrap", "wrap"]).optional().describe("Desktop flex-wrap"),
|
|
641
|
+
wrap_tablet: z.enum(["nowrap", "wrap"]).optional(),
|
|
642
|
+
wrap_mobile: z.enum(["nowrap", "wrap"]).optional(),
|
|
643
|
+
gap: z.string().optional().describe("Desktop gap — '64' (px both axes), '64 32' (column row in px), '5%'"),
|
|
644
|
+
gap_tablet: z.string().optional(),
|
|
645
|
+
gap_mobile: z.string().optional(),
|
|
646
|
+
content_width: z.enum(["boxed", "full"]).optional().describe("'boxed' = constrain inner content to boxed_width; 'full' = stretch full viewport"),
|
|
647
|
+
boxed_width: z.string().optional().describe("Max content width when content_width='boxed'. E.g. '1280' (px), '1440px', '90%'"),
|
|
648
|
+
site: siteParam,
|
|
649
|
+
}, async ({ page_id, element_id, direction, direction_tablet, direction_mobile, justify, justify_tablet, justify_mobile, align, align_tablet, align_mobile, wrap, wrap_tablet, wrap_mobile, gap, gap_tablet, gap_mobile, content_width, boxed_width, site }) => {
|
|
650
|
+
try {
|
|
651
|
+
const { wpUrl, authHeader } = resolveSite(sites, site);
|
|
652
|
+
const s = {};
|
|
653
|
+
if (direction)
|
|
654
|
+
s.flex_direction = direction;
|
|
655
|
+
if (direction_tablet)
|
|
656
|
+
s.flex_direction_tablet = direction_tablet;
|
|
657
|
+
if (direction_mobile)
|
|
658
|
+
s.flex_direction_mobile = direction_mobile;
|
|
659
|
+
if (justify)
|
|
660
|
+
s.flex_justify_content = justify;
|
|
661
|
+
if (justify_tablet)
|
|
662
|
+
s.flex_justify_content_tablet = justify_tablet;
|
|
663
|
+
if (justify_mobile)
|
|
664
|
+
s.flex_justify_content_mobile = justify_mobile;
|
|
665
|
+
if (align)
|
|
666
|
+
s.flex_align_items = align;
|
|
667
|
+
if (align_tablet)
|
|
668
|
+
s.flex_align_items_tablet = align_tablet;
|
|
669
|
+
if (align_mobile)
|
|
670
|
+
s.flex_align_items_mobile = align_mobile;
|
|
671
|
+
if (wrap)
|
|
672
|
+
s.flex_wrap = wrap;
|
|
673
|
+
if (wrap_tablet)
|
|
674
|
+
s.flex_wrap_tablet = wrap_tablet;
|
|
675
|
+
if (wrap_mobile)
|
|
676
|
+
s.flex_wrap_mobile = wrap_mobile;
|
|
677
|
+
const g1 = parseGap(gap);
|
|
678
|
+
if (g1)
|
|
679
|
+
s.flex_gap = g1;
|
|
680
|
+
const g2 = parseGap(gap_tablet);
|
|
681
|
+
if (g2)
|
|
682
|
+
s.flex_gap_tablet = g2;
|
|
683
|
+
const g3 = parseGap(gap_mobile);
|
|
684
|
+
if (g3)
|
|
685
|
+
s.flex_gap_mobile = g3;
|
|
686
|
+
if (content_width)
|
|
687
|
+
s.content_width = content_width;
|
|
688
|
+
const bw = parseSize(boxed_width);
|
|
689
|
+
if (bw)
|
|
690
|
+
s.boxed_width = bw;
|
|
691
|
+
const result = await applyElementSettings(wpUrl, authHeader, page_id, element_id, s);
|
|
692
|
+
return { content: [{ type: "text", text: JSON.stringify({ applied: s, result }, null, 2) }] };
|
|
693
|
+
}
|
|
694
|
+
catch (error) {
|
|
695
|
+
return { content: [{ type: "text", text: `Error setting layout: ${error.response?.data?.message || error.message}` }], isError: true };
|
|
696
|
+
}
|
|
697
|
+
});
|
|
698
|
+
// ── 4. set-element-width ──────────────────────────────────────────────────
|
|
699
|
+
server.tool("set-element-width", "Set width of a column/container element across breakpoints. For child containers in a row, pass desktop='50%', mobile='100%' for two-column-stacking. Critical for every column layout.", {
|
|
700
|
+
page_id: z.string().describe("WordPress Page ID"),
|
|
701
|
+
element_id: z.string().describe("Elementor element ID"),
|
|
702
|
+
desktop: z.string().optional().describe("Desktop width — '50%', '300px', '50' (px default), '1fr' for grid"),
|
|
703
|
+
tablet: z.string().optional().describe("Tablet width"),
|
|
704
|
+
mobile: z.string().optional().describe("Mobile width — typically '100%' for stack-to-full-width"),
|
|
705
|
+
site: siteParam,
|
|
706
|
+
}, async ({ page_id, element_id, desktop, tablet, mobile, site }) => {
|
|
707
|
+
try {
|
|
708
|
+
const { wpUrl, authHeader } = resolveSite(sites, site);
|
|
709
|
+
const s = {};
|
|
710
|
+
const w1 = parseSize(desktop);
|
|
711
|
+
const w2 = parseSize(tablet);
|
|
712
|
+
const w3 = parseSize(mobile);
|
|
713
|
+
if (w1) {
|
|
714
|
+
s._element_width = "custom";
|
|
715
|
+
s._element_custom_width = w1;
|
|
716
|
+
s.width = w1;
|
|
717
|
+
}
|
|
718
|
+
if (w2) {
|
|
719
|
+
s._element_width_tablet = "custom";
|
|
720
|
+
s._element_custom_width_tablet = w2;
|
|
721
|
+
s.width_tablet = w2;
|
|
722
|
+
}
|
|
723
|
+
if (w3) {
|
|
724
|
+
s._element_width_mobile = "custom";
|
|
725
|
+
s._element_custom_width_mobile = w3;
|
|
726
|
+
s.width_mobile = w3;
|
|
727
|
+
}
|
|
728
|
+
const result = await applyElementSettings(wpUrl, authHeader, page_id, element_id, s);
|
|
729
|
+
return { content: [{ type: "text", text: JSON.stringify({ applied: s, result }, null, 2) }] };
|
|
730
|
+
}
|
|
731
|
+
catch (error) {
|
|
732
|
+
return { content: [{ type: "text", text: `Error setting width: ${error.response?.data?.message || error.message}` }], isError: true };
|
|
733
|
+
}
|
|
734
|
+
});
|
|
735
|
+
// ── 5. set-element-min-height ─────────────────────────────────────────────
|
|
736
|
+
server.tool("set-element-min-height", "Set minimum height of a container across breakpoints. Used on every hero section, CF image column, full-height containers. Supports px and vh — heroes often use '420px' desktop / '80vh' for viewport-relative.", {
|
|
737
|
+
page_id: z.string().describe("WordPress Page ID"),
|
|
738
|
+
element_id: z.string().describe("Elementor container ID"),
|
|
739
|
+
desktop: z.string().optional().describe("Desktop min-height — '420px', '80vh', '600'"),
|
|
740
|
+
tablet: z.string().optional(),
|
|
741
|
+
mobile: z.string().optional(),
|
|
742
|
+
site: siteParam,
|
|
743
|
+
}, async ({ page_id, element_id, desktop, tablet, mobile, site }) => {
|
|
744
|
+
try {
|
|
745
|
+
const { wpUrl, authHeader } = resolveSite(sites, site);
|
|
746
|
+
const s = {};
|
|
747
|
+
const v1 = parseSize(desktop);
|
|
748
|
+
if (v1)
|
|
749
|
+
s.min_height = v1;
|
|
750
|
+
const v2 = parseSize(tablet);
|
|
751
|
+
if (v2)
|
|
752
|
+
s.min_height_tablet = v2;
|
|
753
|
+
const v3 = parseSize(mobile);
|
|
754
|
+
if (v3)
|
|
755
|
+
s.min_height_mobile = v3;
|
|
756
|
+
const result = await applyElementSettings(wpUrl, authHeader, page_id, element_id, s);
|
|
757
|
+
return { content: [{ type: "text", text: JSON.stringify({ applied: s, result }, null, 2) }] };
|
|
758
|
+
}
|
|
759
|
+
catch (error) {
|
|
760
|
+
return { content: [{ type: "text", text: `Error setting min-height: ${error.response?.data?.message || error.message}` }], isError: true };
|
|
761
|
+
}
|
|
762
|
+
});
|
|
763
|
+
// ── 6. set-element-border ─────────────────────────────────────────────────
|
|
764
|
+
server.tool("set-element-border", "Set border properties (type, width, color, radius) on any element across breakpoints. Some footer/card patterns show different borders per breakpoint — pass per-device width if needed. Use color='primary' for kit globals.", {
|
|
765
|
+
page_id: z.string().describe("WordPress Page ID"),
|
|
766
|
+
element_id: z.string().describe("Elementor element ID"),
|
|
767
|
+
border_type: z.enum(["none", "solid", "dashed", "dotted", "double"]).optional().describe("'solid' is most common. 'none' clears all borders."),
|
|
768
|
+
width: z.string().optional().describe("Desktop border width — '1' (all sides 1px), '0 1 0 0' (right-only)"),
|
|
769
|
+
width_tablet: z.string().optional(),
|
|
770
|
+
width_mobile: z.string().optional(),
|
|
771
|
+
color: z.string().optional().describe("Border color hex or global slug (e.g. 'primary')"),
|
|
772
|
+
radius: z.string().optional().describe("Desktop border radius — '0' (square corners — standard), '8' (rounded), '10 10 0 0' (top-only round)"),
|
|
773
|
+
radius_tablet: z.string().optional(),
|
|
774
|
+
radius_mobile: z.string().optional(),
|
|
775
|
+
site: siteParam,
|
|
776
|
+
}, async ({ page_id, element_id, border_type, width, width_tablet, width_mobile, color, radius, radius_tablet, radius_mobile, site }) => {
|
|
777
|
+
try {
|
|
778
|
+
const { wpUrl, authHeader } = resolveSite(sites, site);
|
|
779
|
+
const s = {};
|
|
780
|
+
if (border_type)
|
|
781
|
+
s.border_border = border_type === "none" ? "" : border_type;
|
|
782
|
+
const w1 = parseSpacing(width);
|
|
783
|
+
if (w1)
|
|
784
|
+
s.border_width = w1;
|
|
785
|
+
const w2 = parseSpacing(width_tablet);
|
|
786
|
+
if (w2)
|
|
787
|
+
s.border_width_tablet = w2;
|
|
788
|
+
const w3 = parseSpacing(width_mobile);
|
|
789
|
+
if (w3)
|
|
790
|
+
s.border_width_mobile = w3;
|
|
791
|
+
if (color) {
|
|
792
|
+
const c = resolveColor(color);
|
|
793
|
+
if (c.value)
|
|
794
|
+
s.border_color = c.value;
|
|
795
|
+
if (c.global_id)
|
|
796
|
+
s.__globals__ = { ...(s.__globals__ || {}), border_color: c.global_id };
|
|
797
|
+
}
|
|
798
|
+
const r1 = parseSpacing(radius);
|
|
799
|
+
if (r1)
|
|
800
|
+
s.border_radius = r1;
|
|
801
|
+
const r2 = parseSpacing(radius_tablet);
|
|
802
|
+
if (r2)
|
|
803
|
+
s.border_radius_tablet = r2;
|
|
804
|
+
const r3 = parseSpacing(radius_mobile);
|
|
805
|
+
if (r3)
|
|
806
|
+
s.border_radius_mobile = r3;
|
|
807
|
+
const result = await applyElementSettings(wpUrl, authHeader, page_id, element_id, s);
|
|
808
|
+
return { content: [{ type: "text", text: JSON.stringify({ applied: s, result }, null, 2) }] };
|
|
809
|
+
}
|
|
810
|
+
catch (error) {
|
|
811
|
+
return { content: [{ type: "text", text: `Error setting border: ${error.response?.data?.message || error.message}` }], isError: true };
|
|
812
|
+
}
|
|
813
|
+
});
|
|
814
|
+
// ── 7. set-element-position ───────────────────────────────────────────────
|
|
815
|
+
server.tool("set-element-position", "Set CSS position and offset values on an element. Used for absolute-positioned decorative border frames, overlap techniques, and fixed elements. Pass position='absolute' + offset_x/_y for offset positioning. Pass position='' (empty) to reset to default flow positioning.", {
|
|
816
|
+
page_id: z.string().describe("WordPress Page ID"),
|
|
817
|
+
element_id: z.string().describe("Elementor element ID"),
|
|
818
|
+
position: z.enum(["", "absolute", "relative", "fixed"]).optional().describe("'absolute' takes element out of flow; 'relative' allows offset without breaking flow; '' resets"),
|
|
819
|
+
offset_x: z.string().optional().describe("Horizontal offset desktop — '20px', '-2%' (negative for left)"),
|
|
820
|
+
offset_x_tablet: z.string().optional(),
|
|
821
|
+
offset_x_mobile: z.string().optional(),
|
|
822
|
+
offset_y: z.string().optional().describe("Vertical offset desktop — '20px', '-2%' (negative for up)"),
|
|
823
|
+
offset_y_tablet: z.string().optional(),
|
|
824
|
+
offset_y_mobile: z.string().optional(),
|
|
825
|
+
site: siteParam,
|
|
826
|
+
}, async ({ page_id, element_id, position, offset_x, offset_x_tablet, offset_x_mobile, offset_y, offset_y_tablet, offset_y_mobile, site }) => {
|
|
827
|
+
try {
|
|
828
|
+
const { wpUrl, authHeader } = resolveSite(sites, site);
|
|
829
|
+
const s = {};
|
|
830
|
+
if (position !== undefined)
|
|
831
|
+
s.position = position;
|
|
832
|
+
const ox1 = parseSize(offset_x);
|
|
833
|
+
if (ox1)
|
|
834
|
+
s._offset_x = ox1;
|
|
835
|
+
const ox2 = parseSize(offset_x_tablet);
|
|
836
|
+
if (ox2)
|
|
837
|
+
s._offset_x_tablet = ox2;
|
|
838
|
+
const ox3 = parseSize(offset_x_mobile);
|
|
839
|
+
if (ox3)
|
|
840
|
+
s._offset_x_mobile = ox3;
|
|
841
|
+
const oy1 = parseSize(offset_y);
|
|
842
|
+
if (oy1)
|
|
843
|
+
s._offset_y = oy1;
|
|
844
|
+
const oy2 = parseSize(offset_y_tablet);
|
|
845
|
+
if (oy2)
|
|
846
|
+
s._offset_y_tablet = oy2;
|
|
847
|
+
const oy3 = parseSize(offset_y_mobile);
|
|
848
|
+
if (oy3)
|
|
849
|
+
s._offset_y_mobile = oy3;
|
|
850
|
+
const result = await applyElementSettings(wpUrl, authHeader, page_id, element_id, s);
|
|
851
|
+
return { content: [{ type: "text", text: JSON.stringify({ applied: s, result }, null, 2) }] };
|
|
852
|
+
}
|
|
853
|
+
catch (error) {
|
|
854
|
+
return { content: [{ type: "text", text: `Error setting position: ${error.response?.data?.message || error.message}` }], isError: true };
|
|
855
|
+
}
|
|
856
|
+
});
|
|
857
|
+
// ── 8. set-element-sticky ─────────────────────────────────────────────────
|
|
858
|
+
server.tool("set-element-sticky", "Configure sticky scroll behavior on a container/widget. Used on every header (sticky='top') and some image columns. Pass sticky_on=['desktop','tablet','mobile'] to control where it applies. sticky_offset = scroll distance before sticking; sticky_effects_offset = trigger for opacity/animation effects.", {
|
|
859
|
+
page_id: z.string().describe("WordPress Page ID"),
|
|
860
|
+
element_id: z.string().describe("Elementor element ID"),
|
|
861
|
+
sticky: z.enum(["", "top", "bottom"]).optional().describe("'top' = sticks at top of viewport on scroll (typical header); '' = disabled"),
|
|
862
|
+
sticky_on: z.array(z.enum(["desktop", "tablet", "mobile"])).optional().describe("Which breakpoints sticky applies on. Default Elementor: all three."),
|
|
863
|
+
sticky_offset: z.number().optional().describe("Scroll offset in px before stickiness engages (default 0)"),
|
|
864
|
+
sticky_effects_offset: z.number().optional().describe("Scroll px before scroll-triggered effects kick in"),
|
|
865
|
+
site: siteParam,
|
|
866
|
+
}, async ({ page_id, element_id, sticky, sticky_on, sticky_offset, sticky_effects_offset, site }) => {
|
|
867
|
+
try {
|
|
868
|
+
const { wpUrl, authHeader } = resolveSite(sites, site);
|
|
869
|
+
const s = {};
|
|
870
|
+
if (sticky !== undefined)
|
|
871
|
+
s.sticky = sticky;
|
|
872
|
+
if (sticky_on)
|
|
873
|
+
s.sticky_on = sticky_on;
|
|
874
|
+
if (sticky_offset !== undefined)
|
|
875
|
+
s.sticky_offset = sticky_offset;
|
|
876
|
+
if (sticky_effects_offset !== undefined)
|
|
877
|
+
s.sticky_effects_offset = sticky_effects_offset;
|
|
878
|
+
const result = await applyElementSettings(wpUrl, authHeader, page_id, element_id, s);
|
|
879
|
+
return { content: [{ type: "text", text: JSON.stringify({ applied: s, result }, null, 2) }] };
|
|
880
|
+
}
|
|
881
|
+
catch (error) {
|
|
882
|
+
return { content: [{ type: "text", text: `Error setting sticky: ${error.response?.data?.message || error.message}` }], isError: true };
|
|
883
|
+
}
|
|
884
|
+
});
|
|
885
|
+
// ── 9. set-element-motion-effects (merged entrance animation + motion fx) ─
|
|
886
|
+
server.tool("set-element-motion-effects", "Set scroll/entrance motion effects — entrance animation OR background motion-fx (scroll opacity, scroll translate). Used on hero sections, sticky headers, and scroll-triggered card reveals. For entrance: pass entrance_animation + duration. For scroll fade-out on sticky headers: pass scroll_opacity_enabled=true + scroll_opacity_to=0.4.", {
|
|
887
|
+
page_id: z.string().describe("WordPress Page ID"),
|
|
888
|
+
element_id: z.string().describe("Elementor element ID"),
|
|
889
|
+
entrance_animation: z.string().optional().describe("Entrance animation: 'fadeIn', 'fadeInLeft', 'fadeInRight', 'fadeInUp', 'fadeInDown', 'zoomIn', 'slideInUp', 'none'"),
|
|
890
|
+
animation_duration: z.enum(["slow", "normal", "fast"]).optional().describe("Default: 'normal'"),
|
|
891
|
+
animation_delay: z.number().optional().describe("Delay in milliseconds before entrance plays"),
|
|
892
|
+
scroll_opacity_enabled: z.boolean().optional().describe("Enable scroll-triggered opacity effect (sticky header fade on scroll)"),
|
|
893
|
+
scroll_opacity_from: z.number().min(0).max(1).optional().describe("Opacity start value (default 1)"),
|
|
894
|
+
scroll_opacity_to: z.number().min(0).max(1).optional().describe("Opacity end value (e.g. 0.4 for partial fade)"),
|
|
895
|
+
scroll_translate_enabled: z.boolean().optional().describe("Enable parallax translateY effect"),
|
|
896
|
+
scroll_translate_speed: z.number().optional().describe("Translate speed value (Elementor default 4)"),
|
|
897
|
+
motion_fx_range: z.enum(["", "viewport", "page"]).optional().describe("Range where motion-fx applies"),
|
|
898
|
+
site: siteParam,
|
|
899
|
+
}, async ({ page_id, element_id, entrance_animation, animation_duration, animation_delay, scroll_opacity_enabled, scroll_opacity_from, scroll_opacity_to, scroll_translate_enabled, scroll_translate_speed, motion_fx_range, site }) => {
|
|
900
|
+
try {
|
|
901
|
+
const { wpUrl, authHeader } = resolveSite(sites, site);
|
|
902
|
+
const s = {};
|
|
903
|
+
if (entrance_animation)
|
|
904
|
+
s.animation = entrance_animation;
|
|
905
|
+
if (animation_duration)
|
|
906
|
+
s.animation_duration = animation_duration;
|
|
907
|
+
if (animation_delay !== undefined)
|
|
908
|
+
s._animation_delay = animation_delay;
|
|
909
|
+
if (scroll_opacity_enabled !== undefined) {
|
|
910
|
+
s.motion_fx_motion_fx_scrolling = "yes";
|
|
911
|
+
s.motion_fx_opacity_effect = scroll_opacity_enabled ? "yes" : "";
|
|
912
|
+
}
|
|
913
|
+
if (scroll_opacity_from !== undefined || scroll_opacity_to !== undefined) {
|
|
914
|
+
s.motion_fx_opacity_range = { unit: "%", sizes: { from: scroll_opacity_from ?? 1, to: scroll_opacity_to ?? 0.4 } };
|
|
915
|
+
}
|
|
916
|
+
if (scroll_translate_enabled !== undefined) {
|
|
917
|
+
s.motion_fx_motion_fx_scrolling = "yes";
|
|
918
|
+
s.motion_fx_translateY_effect = scroll_translate_enabled ? "yes" : "";
|
|
919
|
+
}
|
|
920
|
+
if (scroll_translate_speed !== undefined) {
|
|
921
|
+
s.motion_fx_translateY_speed = { unit: "px", size: scroll_translate_speed };
|
|
922
|
+
}
|
|
923
|
+
if (motion_fx_range !== undefined)
|
|
924
|
+
s.motion_fx_range = motion_fx_range;
|
|
925
|
+
const result = await applyElementSettings(wpUrl, authHeader, page_id, element_id, s);
|
|
926
|
+
return { content: [{ type: "text", text: JSON.stringify({ applied: s, result }, null, 2) }] };
|
|
927
|
+
}
|
|
928
|
+
catch (error) {
|
|
929
|
+
return { content: [{ type: "text", text: `Error setting motion effects: ${error.response?.data?.message || error.message}` }], isError: true };
|
|
930
|
+
}
|
|
931
|
+
});
|
|
932
|
+
// ── 10. set-element-flex-order ────────────────────────────────────────────
|
|
933
|
+
server.tool("set-element-flex-order", "Set CSS order per breakpoint — used to reorder visual layout (e.g. image goes above text on mobile). Pass mobile='start' to pull this element to the top in the mobile stack. THE fix for 'image should appear above text on mobile but to the right on desktop'.", {
|
|
934
|
+
page_id: z.string().describe("WordPress Page ID"),
|
|
935
|
+
element_id: z.string().describe("Elementor element ID"),
|
|
936
|
+
desktop: z.enum(["default", "start", "end", "custom"]).optional(),
|
|
937
|
+
desktop_custom: z.number().optional().describe("Custom order integer (when desktop='custom')"),
|
|
938
|
+
tablet: z.enum(["default", "start", "end", "custom"]).optional(),
|
|
939
|
+
tablet_custom: z.number().optional(),
|
|
940
|
+
mobile: z.enum(["default", "start", "end", "custom"]).optional().describe("'start' = pull this element to the top of mobile stack (most common use)"),
|
|
941
|
+
mobile_custom: z.number().optional(),
|
|
942
|
+
site: siteParam,
|
|
943
|
+
}, async ({ page_id, element_id, desktop, desktop_custom, tablet, tablet_custom, mobile, mobile_custom, site }) => {
|
|
944
|
+
try {
|
|
945
|
+
const { wpUrl, authHeader } = resolveSite(sites, site);
|
|
946
|
+
const s = {};
|
|
947
|
+
if (desktop)
|
|
948
|
+
s._flex_order = desktop;
|
|
949
|
+
if (desktop_custom !== undefined)
|
|
950
|
+
s._flex_order_custom = desktop_custom;
|
|
951
|
+
if (tablet)
|
|
952
|
+
s._flex_order_tablet = tablet;
|
|
953
|
+
if (tablet_custom !== undefined)
|
|
954
|
+
s._flex_order_custom_tablet = tablet_custom;
|
|
955
|
+
if (mobile)
|
|
956
|
+
s._flex_order_mobile = mobile;
|
|
957
|
+
if (mobile_custom !== undefined)
|
|
958
|
+
s._flex_order_custom_mobile = mobile_custom;
|
|
959
|
+
const result = await applyElementSettings(wpUrl, authHeader, page_id, element_id, s);
|
|
960
|
+
return { content: [{ type: "text", text: JSON.stringify({ applied: s, result }, null, 2) }] };
|
|
961
|
+
}
|
|
962
|
+
catch (error) {
|
|
963
|
+
return { content: [{ type: "text", text: `Error setting flex order: ${error.response?.data?.message || error.message}` }], isError: true };
|
|
964
|
+
}
|
|
965
|
+
});
|
|
966
|
+
// ── 11. set-element-css-class ─────────────────────────────────────────────
|
|
967
|
+
server.tool("set-element-css-class", "Set custom CSS class(es) and/or element-scoped custom CSS. Used across all sites — e.g. 'Mbutton' class on header CTA buttons, 'blogcontent' on post content, 'sticky-wrapper' on header. Pass space-separated classes. Pass css_code to add inline CSS scoped to this element.", {
|
|
968
|
+
page_id: z.string().describe("WordPress Page ID"),
|
|
969
|
+
element_id: z.string().describe("Elementor element ID"),
|
|
970
|
+
css_classes: z.string().optional().describe("Space-separated CSS classes, e.g. 'Mbutton brand-cta'"),
|
|
971
|
+
css_code: z.string().optional().describe("Custom CSS scoped to this element. Use 'selector' as the placeholder for this element's selector."),
|
|
972
|
+
site: siteParam,
|
|
973
|
+
}, async ({ page_id, element_id, css_classes, css_code, site }) => {
|
|
974
|
+
try {
|
|
975
|
+
const { wpUrl, authHeader } = resolveSite(sites, site);
|
|
976
|
+
const s = {};
|
|
977
|
+
if (css_classes !== undefined)
|
|
978
|
+
s._css_classes = css_classes;
|
|
979
|
+
if (css_code !== undefined)
|
|
980
|
+
s.custom_css = css_code;
|
|
981
|
+
const result = await applyElementSettings(wpUrl, authHeader, page_id, element_id, s);
|
|
982
|
+
return { content: [{ type: "text", text: JSON.stringify({ applied: s, result }, null, 2) }] };
|
|
983
|
+
}
|
|
984
|
+
catch (error) {
|
|
985
|
+
return { content: [{ type: "text", text: `Error setting CSS class: ${error.response?.data?.message || error.message}` }], isError: true };
|
|
986
|
+
}
|
|
987
|
+
});
|
|
988
|
+
// ── 12. set-posts-widget-filter ───────────────────────────────────────────
|
|
989
|
+
server.tool("set-posts-widget-filter", "Configure query/filter settings on a Posts widget — post type, term IDs, ordering, skin, columns. Replaces ~15 verbose merge-element-settings keys with named params. Posts widget is dominant for blog/services listings.", {
|
|
990
|
+
page_id: z.string().describe("WordPress Page ID"),
|
|
991
|
+
element_id: z.string().describe("Elementor Posts widget ID"),
|
|
992
|
+
post_type: z.string().optional().describe("'post', 'page', 'astra-portfolio', 'by_id', or any custom post type"),
|
|
993
|
+
include_term_ids: z.array(z.number()).optional().describe("Include posts from these taxonomy term IDs"),
|
|
994
|
+
exclude_term_ids: z.array(z.number()).optional(),
|
|
995
|
+
include_ids: z.array(z.number()).optional().describe("Specific post IDs to include (used with post_type='by_id')"),
|
|
996
|
+
exclude_ids: z.array(z.number()).optional(),
|
|
997
|
+
posts_per_page: z.number().optional().describe("How many posts per page (e.g. 12)"),
|
|
998
|
+
order: z.enum(["ASC", "DESC"]).optional(),
|
|
999
|
+
orderby: z.enum(["date", "modified", "title", "menu_order", "rand", "comment_count"]).optional(),
|
|
1000
|
+
skin: z.enum(["classic", "cards", "full_content"]).optional(),
|
|
1001
|
+
columns: z.number().optional().describe("Desktop column count"),
|
|
1002
|
+
columns_tablet: z.number().optional(),
|
|
1003
|
+
columns_mobile: z.number().optional(),
|
|
1004
|
+
show_excerpt: z.boolean().optional(),
|
|
1005
|
+
show_read_more: z.boolean().optional(),
|
|
1006
|
+
site: siteParam,
|
|
1007
|
+
}, async ({ page_id, element_id, post_type, include_term_ids, exclude_term_ids, include_ids, exclude_ids, posts_per_page, order, orderby, skin, columns, columns_tablet, columns_mobile, show_excerpt, show_read_more, site }) => {
|
|
1008
|
+
try {
|
|
1009
|
+
const { wpUrl, authHeader } = resolveSite(sites, site);
|
|
1010
|
+
const s = {};
|
|
1011
|
+
if (post_type)
|
|
1012
|
+
s.posts_post_type = post_type;
|
|
1013
|
+
if (include_term_ids)
|
|
1014
|
+
s.posts_include_term_ids = include_term_ids;
|
|
1015
|
+
if (exclude_term_ids)
|
|
1016
|
+
s.posts_exclude_term_ids = exclude_term_ids;
|
|
1017
|
+
if (include_ids)
|
|
1018
|
+
s.posts_include_ids = include_ids;
|
|
1019
|
+
if (exclude_ids)
|
|
1020
|
+
s.posts_exclude_ids = exclude_ids;
|
|
1021
|
+
if (posts_per_page !== undefined)
|
|
1022
|
+
s.posts_per_page = posts_per_page;
|
|
1023
|
+
if (order)
|
|
1024
|
+
s.order = order;
|
|
1025
|
+
if (orderby)
|
|
1026
|
+
s.orderby = orderby;
|
|
1027
|
+
if (skin)
|
|
1028
|
+
s._skin = skin;
|
|
1029
|
+
if (columns !== undefined)
|
|
1030
|
+
s.columns = { unit: "px", size: columns };
|
|
1031
|
+
if (columns_tablet !== undefined)
|
|
1032
|
+
s.columns_tablet = { unit: "px", size: columns_tablet };
|
|
1033
|
+
if (columns_mobile !== undefined)
|
|
1034
|
+
s.columns_mobile = { unit: "px", size: columns_mobile };
|
|
1035
|
+
if (show_excerpt !== undefined)
|
|
1036
|
+
s.show_excerpt = show_excerpt ? "yes" : "";
|
|
1037
|
+
if (show_read_more !== undefined)
|
|
1038
|
+
s.show_read_more = show_read_more ? "yes" : "";
|
|
1039
|
+
const result = await applyElementSettings(wpUrl, authHeader, page_id, element_id, s);
|
|
1040
|
+
return { content: [{ type: "text", text: JSON.stringify({ applied: s, result }, null, 2) }] };
|
|
1041
|
+
}
|
|
1042
|
+
catch (error) {
|
|
1043
|
+
return { content: [{ type: "text", text: `Error setting posts filter: ${error.response?.data?.message || error.message}` }], isError: true };
|
|
1044
|
+
}
|
|
1045
|
+
});
|
|
1046
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1047
|
+
// ── COMPOUND BUILDERS — scaffold multi-step ops into one tool ─────────────
|
|
1048
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1049
|
+
// ── 13. create-section ────────────────────────────────────────────────────
|
|
1050
|
+
server.tool("create-section", "Scaffold a new section container with all common defaults — padding, flex direction, gap, background, min-height — in ONE call. Replaces a typical insert-element → set-container-background → set-element-spacing → set-container-layout sequence (4 calls) with 1. Returns the new container's element ID so you can immediately add child widgets/columns.", {
|
|
1051
|
+
page_id: z.string().describe("WordPress Page ID"),
|
|
1052
|
+
parent_id: z.string().optional().describe("Parent container ID. Omit for top-level section."),
|
|
1053
|
+
position: z.enum(["before", "after", "first_child", "last_child"]).optional().describe("Default 'last_child' (append to parent or page end)"),
|
|
1054
|
+
reference_id: z.string().optional().describe("Required when position='before'/'after' — the sibling element ID to anchor to"),
|
|
1055
|
+
direction: z.enum(["row", "column"]).optional().describe("Flex direction — default 'column' for section, 'row' for inner row containers"),
|
|
1056
|
+
direction_mobile: z.enum(["row", "column"]).optional().describe("Default 'column' for mobile stacking"),
|
|
1057
|
+
content_width: z.enum(["boxed", "full"]).optional().describe("'boxed' = constrained to boxed_width; 'full' = stretch to viewport"),
|
|
1058
|
+
boxed_width: z.string().optional().describe("Max content width when boxed — '1280', '1440px'"),
|
|
1059
|
+
padding: z.string().optional().describe("Desktop padding — '120 48 120 48', '5%'"),
|
|
1060
|
+
padding_tablet: z.string().optional(),
|
|
1061
|
+
padding_mobile: z.string().optional(),
|
|
1062
|
+
gap: z.string().optional().describe("Flex gap — '64', '64 32'"),
|
|
1063
|
+
gap_tablet: z.string().optional(),
|
|
1064
|
+
gap_mobile: z.string().optional(),
|
|
1065
|
+
min_height: z.string().optional().describe("Min-height — '420px', '80vh'"),
|
|
1066
|
+
min_height_tablet: z.string().optional(),
|
|
1067
|
+
min_height_mobile: z.string().optional(),
|
|
1068
|
+
background_color: z.string().optional().describe("Background color — hex or global slug"),
|
|
1069
|
+
background_image: z.string().optional().describe("Background image URL"),
|
|
1070
|
+
overlay_color: z.string().optional().describe("Overlay color — hex or global slug"),
|
|
1071
|
+
overlay_opacity: z.number().optional().describe("Overlay opacity 0–1"),
|
|
1072
|
+
site: siteParam,
|
|
1073
|
+
}, async ({ page_id, parent_id, position, reference_id, direction, direction_mobile, content_width, boxed_width, padding, padding_tablet, padding_mobile, gap, gap_tablet, gap_mobile, min_height, min_height_tablet, min_height_mobile, background_color, background_image, overlay_color, overlay_opacity, site }) => {
|
|
1074
|
+
try {
|
|
1075
|
+
const { wpUrl, authHeader } = resolveSite(sites, site);
|
|
1076
|
+
const settings = {
|
|
1077
|
+
content_width: content_width || "boxed",
|
|
1078
|
+
flex_direction: direction || "column",
|
|
1079
|
+
};
|
|
1080
|
+
if (direction_mobile)
|
|
1081
|
+
settings.flex_direction_mobile = direction_mobile;
|
|
1082
|
+
const bw = parseSize(boxed_width);
|
|
1083
|
+
if (bw)
|
|
1084
|
+
settings.boxed_width = bw;
|
|
1085
|
+
const p1 = parseSpacing(padding);
|
|
1086
|
+
if (p1)
|
|
1087
|
+
settings.padding = p1;
|
|
1088
|
+
const p2 = parseSpacing(padding_tablet);
|
|
1089
|
+
if (p2)
|
|
1090
|
+
settings.padding_tablet = p2;
|
|
1091
|
+
const p3 = parseSpacing(padding_mobile);
|
|
1092
|
+
if (p3)
|
|
1093
|
+
settings.padding_mobile = p3;
|
|
1094
|
+
const g1 = parseGap(gap);
|
|
1095
|
+
if (g1)
|
|
1096
|
+
settings.flex_gap = g1;
|
|
1097
|
+
const g2 = parseGap(gap_tablet);
|
|
1098
|
+
if (g2)
|
|
1099
|
+
settings.flex_gap_tablet = g2;
|
|
1100
|
+
const g3 = parseGap(gap_mobile);
|
|
1101
|
+
if (g3)
|
|
1102
|
+
settings.flex_gap_mobile = g3;
|
|
1103
|
+
const mh1 = parseSize(min_height);
|
|
1104
|
+
if (mh1)
|
|
1105
|
+
settings.min_height = mh1;
|
|
1106
|
+
const mh2 = parseSize(min_height_tablet);
|
|
1107
|
+
if (mh2)
|
|
1108
|
+
settings.min_height_tablet = mh2;
|
|
1109
|
+
const mh3 = parseSize(min_height_mobile);
|
|
1110
|
+
if (mh3)
|
|
1111
|
+
settings.min_height_mobile = mh3;
|
|
1112
|
+
if (background_color) {
|
|
1113
|
+
settings.background_background = "classic";
|
|
1114
|
+
const c = resolveColor(background_color);
|
|
1115
|
+
if (c.value)
|
|
1116
|
+
settings.background_color = c.value;
|
|
1117
|
+
if (c.global_id)
|
|
1118
|
+
settings.__globals__ = { ...(settings.__globals__ || {}), background_color: c.global_id };
|
|
1119
|
+
}
|
|
1120
|
+
if (background_image) {
|
|
1121
|
+
settings.background_background = "classic";
|
|
1122
|
+
settings.background_image = { url: background_image, id: "" };
|
|
1123
|
+
settings.background_size = "cover";
|
|
1124
|
+
settings.background_position = "center center";
|
|
1125
|
+
}
|
|
1126
|
+
if (overlay_color || overlay_opacity !== undefined) {
|
|
1127
|
+
settings.background_overlay_background = "classic";
|
|
1128
|
+
if (overlay_color) {
|
|
1129
|
+
const oc = resolveColor(overlay_color);
|
|
1130
|
+
if (oc.value)
|
|
1131
|
+
settings.background_overlay_color = oc.value;
|
|
1132
|
+
if (oc.global_id)
|
|
1133
|
+
settings.__globals__ = { ...(settings.__globals__ || {}), background_overlay_color: oc.global_id };
|
|
1134
|
+
}
|
|
1135
|
+
if (overlay_opacity !== undefined) {
|
|
1136
|
+
settings.background_overlay_opacity = { unit: "px", size: overlay_opacity };
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
const newElement = { elType: "container", settings, elements: [] };
|
|
1140
|
+
const body = { element: newElement, position: position || "last_child" };
|
|
1141
|
+
if (parent_id)
|
|
1142
|
+
body.parent_id = parent_id;
|
|
1143
|
+
if (reference_id)
|
|
1144
|
+
body.reference_id = reference_id;
|
|
1145
|
+
const r = await axios.post(`${wpUrl}/wp-json/erc/v1/pages/${page_id}/insert-element`, body, { headers: { Authorization: authHeader, "Content-Type": "application/json" } });
|
|
1146
|
+
return { content: [{ type: "text", text: JSON.stringify(r.data, null, 2) }] };
|
|
1147
|
+
}
|
|
1148
|
+
catch (error) {
|
|
1149
|
+
return { content: [{ type: "text", text: `Error creating section: ${error.response?.data?.message || error.message}` }], isError: true };
|
|
1150
|
+
}
|
|
1151
|
+
});
|
|
1152
|
+
// ── 14. embed-template-in-page ────────────────────────────────────────────
|
|
1153
|
+
server.tool("embed-template-in-page", "Insert a saved Elementor template (header/footer/hero/CF section) into a page as a template widget — wrapped in a full-width container. This is how every page should embed its hero, CF, and footer templates. Pass either template_id (numeric) OR template_title (calls get-template-by-title internally). Default position is last_child — use first_child for hero/header at top.", {
|
|
1154
|
+
page_id: z.string().describe("WordPress Page ID where the template should be embedded"),
|
|
1155
|
+
template_id: z.string().optional().describe("Numeric template ID (use this if you have it)"),
|
|
1156
|
+
template_title: z.string().optional().describe("Template title (substring match) — used if template_id is not provided"),
|
|
1157
|
+
template_type: z.string().optional().describe("When using template_title, optionally filter by type (header/footer/popup/section/page) for a more precise match"),
|
|
1158
|
+
position: z.enum(["before", "after", "first_child", "last_child"]).optional().describe("Default 'last_child'. Use 'first_child' for hero/title template at top."),
|
|
1159
|
+
reference_id: z.string().optional().describe("Required when position='before'/'after' — sibling element to anchor to"),
|
|
1160
|
+
parent_id: z.string().optional().describe("Parent container — omit for top-level"),
|
|
1161
|
+
site: siteParam,
|
|
1162
|
+
}, async ({ page_id, template_id, template_title, template_type, position, reference_id, parent_id, site }) => {
|
|
1163
|
+
try {
|
|
1164
|
+
const { wpUrl, authHeader } = resolveSite(sites, site);
|
|
1165
|
+
let resolvedId = template_id;
|
|
1166
|
+
if (!resolvedId && template_title) {
|
|
1167
|
+
const params = { title: template_title };
|
|
1168
|
+
if (template_type)
|
|
1169
|
+
params.type = template_type;
|
|
1170
|
+
const s = await axios.get(`${wpUrl}/wp-json/erc/v1/templates/search`, {
|
|
1171
|
+
headers: { Authorization: authHeader },
|
|
1172
|
+
params,
|
|
1173
|
+
});
|
|
1174
|
+
if (!s.data?.results?.length) {
|
|
1175
|
+
return { content: [{ type: "text", text: `No template found matching title='${template_title}'${template_type ? ` type='${template_type}'` : ""}` }], isError: true };
|
|
1176
|
+
}
|
|
1177
|
+
resolvedId = String(s.data.results[0].id);
|
|
1178
|
+
}
|
|
1179
|
+
if (!resolvedId) {
|
|
1180
|
+
return { content: [{ type: "text", text: "Provide either template_id or template_title." }], isError: true };
|
|
1181
|
+
}
|
|
1182
|
+
const newElement = {
|
|
1183
|
+
elType: "container",
|
|
1184
|
+
settings: {
|
|
1185
|
+
content_width: "full",
|
|
1186
|
+
padding: { unit: "px", top: "0", right: "0", bottom: "0", left: "0", isLinked: true },
|
|
1187
|
+
},
|
|
1188
|
+
elements: [{
|
|
1189
|
+
elType: "widget",
|
|
1190
|
+
widgetType: "template",
|
|
1191
|
+
settings: { template_id: resolvedId },
|
|
1192
|
+
}],
|
|
1193
|
+
};
|
|
1194
|
+
const body = { element: newElement, position: position || "last_child" };
|
|
1195
|
+
if (parent_id)
|
|
1196
|
+
body.parent_id = parent_id;
|
|
1197
|
+
if (reference_id)
|
|
1198
|
+
body.reference_id = reference_id;
|
|
1199
|
+
const r = await axios.post(`${wpUrl}/wp-json/erc/v1/pages/${page_id}/insert-element`, body, { headers: { Authorization: authHeader, "Content-Type": "application/json" } });
|
|
1200
|
+
return { content: [{ type: "text", text: JSON.stringify({ embedded_template_id: resolvedId, ...r.data }, null, 2) }] };
|
|
1201
|
+
}
|
|
1202
|
+
catch (error) {
|
|
1203
|
+
return { content: [{ type: "text", text: `Error embedding template: ${error.response?.data?.message || error.message}` }], isError: true };
|
|
1204
|
+
}
|
|
1205
|
+
});
|
|
1206
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1207
|
+
// ── KIT TOKENS + TEMPLATE SEARCH ──────────────────────────────────────────
|
|
1208
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1209
|
+
// ── 15. set-global-kit-colors ─────────────────────────────────────────────
|
|
1210
|
+
server.tool("set-global-kit-colors", "Add or update individual global color tokens in the Elementor kit. Every new site build starts here. Pass an array of {id, title, value} objects. Existing colors with the same id are updated; new ones appended. Standard token IDs: 'primary', 'secondary', 'accent', 'text', 'white_color'.", {
|
|
1211
|
+
colors: z.array(z.object({
|
|
1212
|
+
id: z.string().describe("Token slug (e.g. 'primary', 'secondary', 'accent', 'text')"),
|
|
1213
|
+
title: z.string().describe("Human-readable name shown in the kit panel"),
|
|
1214
|
+
value: z.string().describe("Hex color value, e.g. '#0066cc'"),
|
|
1215
|
+
})).min(1).describe("Array of color tokens to upsert"),
|
|
1216
|
+
site: siteParam,
|
|
1217
|
+
}, async ({ colors, site }) => {
|
|
1218
|
+
try {
|
|
1219
|
+
const { wpUrl, authHeader } = resolveSite(sites, site);
|
|
1220
|
+
const r = await axios.post(`${wpUrl}/wp-json/erc/v1/kit/colors`, { colors }, { headers: { Authorization: authHeader, "Content-Type": "application/json" } });
|
|
1221
|
+
return { content: [{ type: "text", text: JSON.stringify(r.data, null, 2) }] };
|
|
1222
|
+
}
|
|
1223
|
+
catch (error) {
|
|
1224
|
+
return { content: [{ type: "text", text: `Error setting kit colors: ${error.response?.data?.message || error.message}` }], isError: true };
|
|
1225
|
+
}
|
|
1226
|
+
});
|
|
1227
|
+
// ── 16. set-global-kit-typography ─────────────────────────────────────────
|
|
1228
|
+
server.tool("set-global-kit-typography", "Add or update individual global typography tokens in the Elementor kit. Every new site needs 3-5 typography tokens (primary heading, secondary heading, body, accent) before widgets can reference globals correctly.", {
|
|
1229
|
+
typography: z.array(z.object({
|
|
1230
|
+
id: z.string().describe("Token slug (e.g. 'primary', 'secondary', 'text', 'accent')"),
|
|
1231
|
+
title: z.string().describe("Human-readable name"),
|
|
1232
|
+
font_family: z.string().optional().describe("Font family name (must be loaded by theme), e.g. 'Playfair Display', 'Inter'"),
|
|
1233
|
+
font_size: z.union([z.number(), z.object({ unit: z.string(), size: z.number() })]).optional().describe("Number = px (e.g. 60). Or object {unit:'px',size:60}"),
|
|
1234
|
+
font_weight: z.string().optional().describe("'400', '500', '600', '700', 'normal', 'bold'"),
|
|
1235
|
+
text_transform: z.enum(["none", "uppercase", "lowercase", "capitalize"]).optional(),
|
|
1236
|
+
letter_spacing: z.union([z.number(), z.object({ unit: z.string(), size: z.number() })]).optional(),
|
|
1237
|
+
line_height: z.union([z.number(), z.object({ unit: z.string(), size: z.number() })]).optional(),
|
|
1238
|
+
font_style: z.enum(["normal", "italic", "oblique"]).optional(),
|
|
1239
|
+
text_decoration: z.enum(["none", "underline", "line-through", "overline"]).optional(),
|
|
1240
|
+
})).min(1).describe("Array of typography tokens to upsert"),
|
|
1241
|
+
site: siteParam,
|
|
1242
|
+
}, async ({ typography, site }) => {
|
|
1243
|
+
try {
|
|
1244
|
+
const { wpUrl, authHeader } = resolveSite(sites, site);
|
|
1245
|
+
const r = await axios.post(`${wpUrl}/wp-json/erc/v1/kit/typography`, { typography }, { headers: { Authorization: authHeader, "Content-Type": "application/json" } });
|
|
1246
|
+
return { content: [{ type: "text", text: JSON.stringify(r.data, null, 2) }] };
|
|
1247
|
+
}
|
|
1248
|
+
catch (error) {
|
|
1249
|
+
return { content: [{ type: "text", text: `Error setting kit typography: ${error.response?.data?.message || error.message}` }], isError: true };
|
|
1250
|
+
}
|
|
1251
|
+
});
|
|
1252
|
+
// ── 17. get-template-by-title ─────────────────────────────────────────────
|
|
1253
|
+
server.tool("get-template-by-title", "Find Elementor templates by title (case-insensitive substring match). When building a new page you usually know template names ('Header', 'Footer', 'CF Section') but not their numeric IDs. Eliminates the list-templates → manual scan step.", {
|
|
1254
|
+
title: z.string().describe("Title to search for (substring, case-insensitive)"),
|
|
1255
|
+
type: z.string().optional().describe("Optional filter by template type — 'header', 'footer', 'popup', 'section', 'page', 'archive', 'single'"),
|
|
1256
|
+
site: siteParam,
|
|
1257
|
+
}, async ({ title, type, site }) => {
|
|
1258
|
+
try {
|
|
1259
|
+
const { wpUrl, authHeader } = resolveSite(sites, site);
|
|
1260
|
+
const params = { title };
|
|
1261
|
+
if (type)
|
|
1262
|
+
params.type = type;
|
|
1263
|
+
const r = await axios.get(`${wpUrl}/wp-json/erc/v1/templates/search`, {
|
|
1264
|
+
headers: { Authorization: authHeader },
|
|
1265
|
+
params,
|
|
1266
|
+
});
|
|
1267
|
+
return { content: [{ type: "text", text: JSON.stringify(r.data, null, 2) }] };
|
|
1268
|
+
}
|
|
1269
|
+
catch (error) {
|
|
1270
|
+
return { content: [{ type: "text", text: `Error searching templates: ${error.response?.data?.message || error.message}` }], isError: true };
|
|
1271
|
+
}
|
|
1272
|
+
});
|
|
1273
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1274
|
+
// ── v3.14.0 — Figma → Elementor pipeline tools ────────────────────────────
|
|
1275
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1276
|
+
// ── 18. upload-image-to-media-library ─────────────────────────────────────
|
|
1277
|
+
server.tool("upload-image-to-media-library", "Upload an image to the WordPress media library and return its permanent URL. Three input modes: 1) file_path (local file on this machine — MCP server reads it from disk, NEVER passes bytes through Claude's context — efficient for Figma exports), 2) url (remote URL — WP fetches it directly), 3) base64_data (inline base64 — use sparingly, expensive in tokens). After upload, use the returned URL with set-container-background, set-image-widget-src, etc.", {
|
|
1278
|
+
file_path: z.string().optional().describe("Absolute local file path (e.g. D:\\figma-exports\\hero.png). Most efficient — file goes disk → MCP server → WP without passing through Claude's context."),
|
|
1279
|
+
url: z.string().optional().describe("Remote URL to fetch and upload (e.g. external CDN URL). WordPress downloads it server-side."),
|
|
1280
|
+
base64_data: z.string().optional().describe("Base64-encoded image bytes (NO data: prefix). Use only when you already have base64 in context — passing it costs tokens proportional to image size."),
|
|
1281
|
+
mime_type: z.string().optional().describe("MIME type (e.g. 'image/png', 'image/jpeg', 'image/svg+xml'). Auto-detected from filename if omitted."),
|
|
1282
|
+
filename: z.string().describe("Desired filename in media library, e.g. 'hero-clearstone.png'. Must include extension."),
|
|
1283
|
+
title: z.string().optional().describe("Attachment title (defaults to filename without extension)"),
|
|
1284
|
+
alt_text: z.string().optional().describe("Alt text for accessibility"),
|
|
1285
|
+
site: siteParam,
|
|
1286
|
+
}, async ({ file_path, url, base64_data, mime_type, filename, title, alt_text, site }) => {
|
|
1287
|
+
try {
|
|
1288
|
+
const { wpUrl, authHeader } = resolveSite(sites, site);
|
|
1289
|
+
let body = { filename };
|
|
1290
|
+
if (title)
|
|
1291
|
+
body.title = title;
|
|
1292
|
+
if (alt_text)
|
|
1293
|
+
body.alt_text = alt_text;
|
|
1294
|
+
if (mime_type)
|
|
1295
|
+
body.mimeType = mime_type;
|
|
1296
|
+
if (file_path) {
|
|
1297
|
+
if (!fs.existsSync(file_path)) {
|
|
1298
|
+
return { content: [{ type: "text", text: `Error: File not found at path: ${file_path}` }], isError: true };
|
|
1299
|
+
}
|
|
1300
|
+
// Read file from disk and convert to base64 — happens in MCP server memory, NOT Claude context
|
|
1301
|
+
const buf = fs.readFileSync(file_path);
|
|
1302
|
+
body.data = buf.toString("base64");
|
|
1303
|
+
// Auto-detect mime from extension if not provided
|
|
1304
|
+
if (!body.mimeType) {
|
|
1305
|
+
const ext = path.extname(file_path).toLowerCase();
|
|
1306
|
+
const mimes = {
|
|
1307
|
+
".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg",
|
|
1308
|
+
".gif": "image/gif", ".webp": "image/webp", ".svg": "image/svg+xml",
|
|
1309
|
+
".pdf": "application/pdf",
|
|
1310
|
+
};
|
|
1311
|
+
body.mimeType = mimes[ext] || "application/octet-stream";
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
else if (url) {
|
|
1315
|
+
body.url = url;
|
|
1316
|
+
}
|
|
1317
|
+
else if (base64_data) {
|
|
1318
|
+
body.data = base64_data;
|
|
1319
|
+
}
|
|
1320
|
+
else {
|
|
1321
|
+
return { content: [{ type: "text", text: "Provide one of: file_path, url, or base64_data." }], isError: true };
|
|
1322
|
+
}
|
|
1323
|
+
const r = await axios.post(`${wpUrl}/wp-json/erc/v1/media/upload`, body, {
|
|
1324
|
+
headers: { Authorization: authHeader, "Content-Type": "application/json" },
|
|
1325
|
+
maxBodyLength: 100 * 1024 * 1024, // allow up to 100MB
|
|
1326
|
+
maxContentLength: 100 * 1024 * 1024,
|
|
1327
|
+
});
|
|
1328
|
+
return { content: [{ type: "text", text: JSON.stringify(r.data, null, 2) }] };
|
|
1329
|
+
}
|
|
1330
|
+
catch (error) {
|
|
1331
|
+
return { content: [{ type: "text", text: `Error uploading to media library: ${error.response?.data?.message || error.message}` }], isError: true };
|
|
1332
|
+
}
|
|
1333
|
+
});
|
|
1334
|
+
// ── 19. set-element-shadow ────────────────────────────────────────────────
|
|
1335
|
+
server.tool("set-element-shadow", "Set box-shadow on any element — drop shadow or inset shadow. Figma's drop-shadow effects translate directly to this. Maps to Elementor's box_shadow_* settings. Set shadow_type='none' to remove. For multiple stacked shadows (common in Figma), call this once per shadow with merge: just pass the dominant one — Elementor's UI supports one box shadow per element.", {
|
|
1336
|
+
page_id: z.string().describe("WordPress Page ID"),
|
|
1337
|
+
element_id: z.string().describe("Elementor element ID"),
|
|
1338
|
+
shadow_type: z.enum(["none", "outline", "inset"]).optional().describe("'outline' = drop shadow (default), 'inset' = inner shadow, 'none' = remove shadow"),
|
|
1339
|
+
color: z.string().optional().describe("Shadow color hex with alpha — e.g. '#00000040' for 25% black. Or use global slug."),
|
|
1340
|
+
horizontal: z.number().optional().describe("X offset in px (positive = right)"),
|
|
1341
|
+
vertical: z.number().optional().describe("Y offset in px (positive = down)"),
|
|
1342
|
+
blur: z.number().optional().describe("Blur radius in px (Figma calls this 'radius')"),
|
|
1343
|
+
spread: z.number().optional().describe("Spread radius in px (Figma drop-shadow has 'spread' as the 4th value)"),
|
|
1344
|
+
site: siteParam,
|
|
1345
|
+
}, async ({ page_id, element_id, shadow_type, color, horizontal, vertical, blur, spread, site }) => {
|
|
1346
|
+
try {
|
|
1347
|
+
const { wpUrl, authHeader } = resolveSite(sites, site);
|
|
1348
|
+
const s = {};
|
|
1349
|
+
if (shadow_type === "none") {
|
|
1350
|
+
s.box_shadow_box_shadow_type = "";
|
|
1351
|
+
}
|
|
1352
|
+
else if (shadow_type) {
|
|
1353
|
+
s.box_shadow_box_shadow_type = shadow_type;
|
|
1354
|
+
}
|
|
1355
|
+
if (color) {
|
|
1356
|
+
const c = resolveColor(color);
|
|
1357
|
+
if (c.value)
|
|
1358
|
+
s.box_shadow_box_shadow = { ...(s.box_shadow_box_shadow || {}), color: c.value };
|
|
1359
|
+
if (c.global_id)
|
|
1360
|
+
s.__globals__ = { ...(s.__globals__ || {}), box_shadow_box_shadow: c.global_id };
|
|
1361
|
+
}
|
|
1362
|
+
// Elementor stores box_shadow as a single object with horizontal/vertical/blur/spread/color
|
|
1363
|
+
const shadowObj = s.box_shadow_box_shadow || {};
|
|
1364
|
+
if (horizontal !== undefined)
|
|
1365
|
+
shadowObj.horizontal = horizontal;
|
|
1366
|
+
if (vertical !== undefined)
|
|
1367
|
+
shadowObj.vertical = vertical;
|
|
1368
|
+
if (blur !== undefined)
|
|
1369
|
+
shadowObj.blur = blur;
|
|
1370
|
+
if (spread !== undefined)
|
|
1371
|
+
shadowObj.spread = spread;
|
|
1372
|
+
if (Object.keys(shadowObj).length > 0)
|
|
1373
|
+
s.box_shadow_box_shadow = shadowObj;
|
|
1374
|
+
const result = await applyElementSettings(wpUrl, authHeader, page_id, element_id, s);
|
|
1375
|
+
return { content: [{ type: "text", text: JSON.stringify({ applied: s, result }, null, 2) }] };
|
|
1376
|
+
}
|
|
1377
|
+
catch (error) {
|
|
1378
|
+
return { content: [{ type: "text", text: `Error setting shadow: ${error.response?.data?.message || error.message}` }], isError: true };
|
|
1379
|
+
}
|
|
1380
|
+
});
|
|
1381
|
+
// ── 20. set-element-typography ────────────────────────────────────────────
|
|
1382
|
+
server.tool("set-element-typography", "Set typography on a heading/text/button widget — font family, size, weight, line height, letter spacing, text-transform, color. Critical for Figma → Elementor: Figma has explicit font weights (400/500/700) that must translate to Elementor's typography_font_weight. Use global_typography='heading' to reference a kit typography token (preferred). Use color='primary' to reference a kit color global.", {
|
|
1383
|
+
page_id: z.string().describe("WordPress Page ID"),
|
|
1384
|
+
element_id: z.string().describe("Elementor widget ID (heading, text-editor, button, etc.)"),
|
|
1385
|
+
global_typography: z.string().optional().describe("Kit typography token slug — references a global (e.g. 'primary', 'heading'). Preferred over individual properties."),
|
|
1386
|
+
font_family: z.string().optional().describe("Font family name, e.g. 'Playfair Display', 'Inter'"),
|
|
1387
|
+
font_size: z.string().optional().describe("Desktop font size — '60', '60px', '2.5rem', '60vh'"),
|
|
1388
|
+
font_size_tablet: z.string().optional(),
|
|
1389
|
+
font_size_mobile: z.string().optional(),
|
|
1390
|
+
font_weight: z.string().optional().describe("'400', '500', '600', '700', 'normal', 'bold'"),
|
|
1391
|
+
text_transform: z.enum(["none", "uppercase", "lowercase", "capitalize"]).optional(),
|
|
1392
|
+
text_decoration: z.enum(["none", "underline", "overline", "line-through"]).optional(),
|
|
1393
|
+
font_style: z.enum(["normal", "italic", "oblique"]).optional(),
|
|
1394
|
+
line_height: z.string().optional().describe("Line height — '1.5' (unitless), '24px', '1.5em'"),
|
|
1395
|
+
letter_spacing: z.string().optional().describe("Letter spacing — '1' (px default), '2px', '0.05em'"),
|
|
1396
|
+
color: z.string().optional().describe("Text color hex (e.g. '#000000') OR global slug (e.g. 'primary')"),
|
|
1397
|
+
align: z.enum(["left", "center", "right", "justify"]).optional().describe("Text alignment (only applies to widgets with align setting)"),
|
|
1398
|
+
align_mobile: z.enum(["left", "center", "right", "justify"]).optional(),
|
|
1399
|
+
site: siteParam,
|
|
1400
|
+
}, async ({ page_id, element_id, global_typography, font_family, font_size, font_size_tablet, font_size_mobile, font_weight, text_transform, text_decoration, font_style, line_height, letter_spacing, color, align, align_mobile, site }) => {
|
|
1401
|
+
try {
|
|
1402
|
+
const { wpUrl, authHeader } = resolveSite(sites, site);
|
|
1403
|
+
const s = {};
|
|
1404
|
+
// Typography is composite — Elementor uses typography_typography="custom" to enable per-element settings
|
|
1405
|
+
const needsCustomTypography = font_family || font_size || font_weight || text_transform || text_decoration || font_style || line_height || letter_spacing;
|
|
1406
|
+
if (needsCustomTypography)
|
|
1407
|
+
s.typography_typography = "custom";
|
|
1408
|
+
if (global_typography) {
|
|
1409
|
+
s.__globals__ = { ...(s.__globals__ || {}), typography_typography: `globals/typography?id=${global_typography}` };
|
|
1410
|
+
}
|
|
1411
|
+
if (font_family)
|
|
1412
|
+
s.typography_font_family = font_family;
|
|
1413
|
+
if (font_weight)
|
|
1414
|
+
s.typography_font_weight = font_weight;
|
|
1415
|
+
if (text_transform)
|
|
1416
|
+
s.typography_text_transform = text_transform;
|
|
1417
|
+
if (text_decoration)
|
|
1418
|
+
s.typography_text_decoration = text_decoration;
|
|
1419
|
+
if (font_style)
|
|
1420
|
+
s.typography_font_style = font_style;
|
|
1421
|
+
const fs1 = parseSize(font_size);
|
|
1422
|
+
if (fs1)
|
|
1423
|
+
s.typography_font_size = fs1;
|
|
1424
|
+
const fs2 = parseSize(font_size_tablet);
|
|
1425
|
+
if (fs2)
|
|
1426
|
+
s.typography_font_size_tablet = fs2;
|
|
1427
|
+
const fs3 = parseSize(font_size_mobile);
|
|
1428
|
+
if (fs3)
|
|
1429
|
+
s.typography_font_size_mobile = fs3;
|
|
1430
|
+
if (line_height) {
|
|
1431
|
+
// Unitless (e.g. "1.5") → use 'em'-like ratio; with unit → use that unit
|
|
1432
|
+
const lh = parseSize(line_height);
|
|
1433
|
+
if (lh)
|
|
1434
|
+
s.typography_line_height = lh;
|
|
1435
|
+
else if (/^\d+(\.\d+)?$/.test(String(line_height).trim())) {
|
|
1436
|
+
s.typography_line_height = { unit: "em", size: parseFloat(String(line_height)) };
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
const ls = parseSize(letter_spacing);
|
|
1440
|
+
if (ls)
|
|
1441
|
+
s.typography_letter_spacing = ls;
|
|
1442
|
+
if (color) {
|
|
1443
|
+
const c = resolveColor(color);
|
|
1444
|
+
// Common color keys across widgets: title_color (heading), color (text-editor), button_text_color (button)
|
|
1445
|
+
// We set ALL three so it works regardless of widget type — Elementor ignores keys that don't apply
|
|
1446
|
+
if (c.value) {
|
|
1447
|
+
s.title_color = c.value;
|
|
1448
|
+
s.color = c.value;
|
|
1449
|
+
s.button_text_color = c.value;
|
|
1450
|
+
}
|
|
1451
|
+
if (c.global_id) {
|
|
1452
|
+
s.__globals__ = {
|
|
1453
|
+
...(s.__globals__ || {}),
|
|
1454
|
+
title_color: c.global_id,
|
|
1455
|
+
color: c.global_id,
|
|
1456
|
+
button_text_color: c.global_id,
|
|
1457
|
+
};
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
if (align)
|
|
1461
|
+
s.align = align;
|
|
1462
|
+
if (align_mobile)
|
|
1463
|
+
s.align_mobile = align_mobile;
|
|
1464
|
+
const result = await applyElementSettings(wpUrl, authHeader, page_id, element_id, s);
|
|
1465
|
+
return { content: [{ type: "text", text: JSON.stringify({ applied: s, result }, null, 2) }] };
|
|
1466
|
+
}
|
|
1467
|
+
catch (error) {
|
|
1468
|
+
return { content: [{ type: "text", text: `Error setting typography: ${error.response?.data?.message || error.message}` }], isError: true };
|
|
1469
|
+
}
|
|
1470
|
+
});
|
|
1471
|
+
// ── 21. set-text-content ──────────────────────────────────────────────────
|
|
1472
|
+
server.tool("set-text-content", "Write text content directly to a heading, text-editor, or button widget. Auto-detects the right setting key (title for heading, editor for text-editor, text for button). Use this instead of merge-element-settings for content updates — clearer intent and less prone to wrong-key errors.", {
|
|
1473
|
+
page_id: z.string().describe("WordPress Page ID"),
|
|
1474
|
+
element_id: z.string().describe("Elementor widget ID"),
|
|
1475
|
+
text: z.string().describe("New text content. Can include basic HTML for text-editor widget."),
|
|
1476
|
+
widget_type: z.enum(["auto", "heading", "text-editor", "button", "icon-box"]).optional().describe("Default 'auto' — picks the right key by trying common ones. Specify for clarity if you know the widget type."),
|
|
1477
|
+
site: siteParam,
|
|
1478
|
+
}, async ({ page_id, element_id, text, widget_type, site }) => {
|
|
1479
|
+
try {
|
|
1480
|
+
const { wpUrl, authHeader } = resolveSite(sites, site);
|
|
1481
|
+
const s = {};
|
|
1482
|
+
const t = widget_type ?? "auto";
|
|
1483
|
+
if (t === "heading")
|
|
1484
|
+
s.title = text;
|
|
1485
|
+
else if (t === "text-editor")
|
|
1486
|
+
s.editor = text;
|
|
1487
|
+
else if (t === "button")
|
|
1488
|
+
s.text = text;
|
|
1489
|
+
else if (t === "icon-box") {
|
|
1490
|
+
s.title_text = text;
|
|
1491
|
+
}
|
|
1492
|
+
else {
|
|
1493
|
+
// auto — set all common keys; Elementor ignores those that don't apply to the widget
|
|
1494
|
+
s.title = text;
|
|
1495
|
+
s.editor = text;
|
|
1496
|
+
s.text = text;
|
|
1497
|
+
s.title_text = text;
|
|
1498
|
+
}
|
|
1499
|
+
const result = await applyElementSettings(wpUrl, authHeader, page_id, element_id, s);
|
|
1500
|
+
return { content: [{ type: "text", text: JSON.stringify({ applied: s, result }, null, 2) }] };
|
|
1501
|
+
}
|
|
1502
|
+
catch (error) {
|
|
1503
|
+
return { content: [{ type: "text", text: `Error setting text content: ${error.response?.data?.message || error.message}` }], isError: true };
|
|
1504
|
+
}
|
|
1505
|
+
});
|
|
1506
|
+
// ── 22. set-image-widget-src ──────────────────────────────────────────────
|
|
1507
|
+
server.tool("set-image-widget-src", "Set the source URL of an Elementor image widget. Pair with upload-image-to-media-library: 1) upload the Figma export, 2) pass the returned URL here. Sets the image, optionally with attachment_id (recommended for WP media library images so srcset/responsive sizes work). Also supports alt text and caption.", {
|
|
1508
|
+
page_id: z.string().describe("WordPress Page ID"),
|
|
1509
|
+
element_id: z.string().describe("Elementor image widget ID"),
|
|
1510
|
+
url: z.string().describe("Image URL (use the URL returned from upload-image-to-media-library)"),
|
|
1511
|
+
attachment_id: z.number().optional().describe("WordPress attachment ID (from upload-image-to-media-library response). Enables responsive srcset."),
|
|
1512
|
+
alt_text: z.string().optional().describe("Alt text for accessibility (also sets caption on some widgets)"),
|
|
1513
|
+
site: siteParam,
|
|
1514
|
+
}, async ({ page_id, element_id, url, attachment_id, alt_text, site }) => {
|
|
1515
|
+
try {
|
|
1516
|
+
const { wpUrl, authHeader } = resolveSite(sites, site);
|
|
1517
|
+
const s = {
|
|
1518
|
+
image: { url, id: attachment_id ?? "" },
|
|
1519
|
+
};
|
|
1520
|
+
if (alt_text) {
|
|
1521
|
+
s.image_alt = alt_text;
|
|
1522
|
+
s.caption = alt_text; // some widgets use 'caption' key
|
|
1523
|
+
}
|
|
1524
|
+
const result = await applyElementSettings(wpUrl, authHeader, page_id, element_id, s);
|
|
1525
|
+
return { content: [{ type: "text", text: JSON.stringify({ applied: s, result }, null, 2) }] };
|
|
1526
|
+
}
|
|
1527
|
+
catch (error) {
|
|
1528
|
+
return { content: [{ type: "text", text: `Error setting image src: ${error.response?.data?.message || error.message}` }], isError: true };
|
|
1529
|
+
}
|
|
1530
|
+
});
|
|
1531
|
+
// ── 23. set-element-opacity ───────────────────────────────────────────────
|
|
1532
|
+
server.tool("set-element-opacity", "Set the opacity of any Elementor element. Figma nodes with opacity < 1 translate directly to this. Accepts 0–1 (Figma format) or 0–100 (CSS percentage) — auto-detects.", {
|
|
1533
|
+
page_id: z.string().describe("WordPress Page ID"),
|
|
1534
|
+
element_id: z.string().describe("Elementor element ID"),
|
|
1535
|
+
opacity: z.number().describe("Opacity value — 0–1 (Figma style, e.g. 0.5) or 0–100 (percent, e.g. 50). Auto-detected."),
|
|
1536
|
+
site: siteParam,
|
|
1537
|
+
}, async ({ page_id, element_id, opacity, site }) => {
|
|
1538
|
+
try {
|
|
1539
|
+
const { wpUrl, authHeader } = resolveSite(sites, site);
|
|
1540
|
+
// Normalize to 0–1 range
|
|
1541
|
+
const normalized = opacity > 1 ? opacity / 100 : opacity;
|
|
1542
|
+
const s = {
|
|
1543
|
+
_element_opacity: { unit: "px", size: normalized },
|
|
1544
|
+
opacity: { unit: "px", size: normalized }, // alt key some widgets use
|
|
1545
|
+
};
|
|
1546
|
+
const result = await applyElementSettings(wpUrl, authHeader, page_id, element_id, s);
|
|
1547
|
+
return { content: [{ type: "text", text: JSON.stringify({ applied: s, result }, null, 2) }] };
|
|
1548
|
+
}
|
|
1549
|
+
catch (error) {
|
|
1550
|
+
return { content: [{ type: "text", text: `Error setting opacity: ${error.response?.data?.message || error.message}` }], isError: true };
|
|
1551
|
+
}
|
|
1552
|
+
});
|
|
1553
|
+
// ── 24. set-element-rotation ──────────────────────────────────────────────
|
|
1554
|
+
server.tool("set-element-rotation", "Rotate any Elementor element by N degrees. Figma's node.rotation translates directly. Positive = clockwise. Maps to Elementor's _element_rotation setting.", {
|
|
1555
|
+
page_id: z.string().describe("WordPress Page ID"),
|
|
1556
|
+
element_id: z.string().describe("Elementor element ID"),
|
|
1557
|
+
rotation: z.number().describe("Rotation in degrees (e.g. 45, -90, 180). 0 = no rotation."),
|
|
1558
|
+
site: siteParam,
|
|
1559
|
+
}, async ({ page_id, element_id, rotation, site }) => {
|
|
1560
|
+
try {
|
|
1561
|
+
const { wpUrl, authHeader } = resolveSite(sites, site);
|
|
1562
|
+
const s = {
|
|
1563
|
+
_element_rotate: { unit: "deg", size: rotation },
|
|
1564
|
+
};
|
|
1565
|
+
const result = await applyElementSettings(wpUrl, authHeader, page_id, element_id, s);
|
|
1566
|
+
return { content: [{ type: "text", text: JSON.stringify({ applied: s, result }, null, 2) }] };
|
|
1567
|
+
}
|
|
1568
|
+
catch (error) {
|
|
1569
|
+
return { content: [{ type: "text", text: `Error setting rotation: ${error.response?.data?.message || error.message}` }], isError: true };
|
|
1570
|
+
}
|
|
1571
|
+
});
|
|
380
1572
|
server.tool("update-data", "Replace the entire Elementor element tree for a page. By default refuses if the new data is dramatically smaller than existing. Use force_replace=true to override.", {
|
|
381
1573
|
page_id: z.string().describe("WordPress Page ID"),
|
|
382
1574
|
elements_json: z.string().describe("Full elements array as stringified JSON"),
|
|
@@ -603,13 +1795,17 @@ function createMcpServer(sites) {
|
|
|
603
1795
|
return { content: [{ type: "text", text: `Error fetching template: ${error.response?.data?.message || error.message}` }], isError: true };
|
|
604
1796
|
}
|
|
605
1797
|
});
|
|
606
|
-
server.tool("create-template", "Create a new Elementor template of any type (page, section, popup, header, footer).", {
|
|
1798
|
+
server.tool("create-template", "Create a new Elementor template of any type (page, section, popup, header, footer, archive, single). For theme-builder templates (header/footer/popup), pass display_condition to auto-register where it shows — 'entire_site' is the most common. For popups, pass popup_settings to configure trigger/width/height/etc. Returns the template ID and edit URL.", {
|
|
607
1799
|
title: z.string().describe("Template title"),
|
|
608
|
-
type: z.string().describe("Template type: page, section, popup, header, footer"),
|
|
1800
|
+
type: z.string().describe("Template type: page, section, popup, header, footer, archive, single, single-page, single-post"),
|
|
609
1801
|
status: z.string().optional().describe("Post status: publish (default) or draft"),
|
|
610
1802
|
elements: z.string().optional().describe("Initial elements JSON (stringified array)"),
|
|
1803
|
+
display_condition: z.enum(["entire_site", "specific_post_type", "specific_page", "none"]).optional().describe("For header/footer/popup/archive/single: where to display. 'entire_site' = show on all pages. 'specific_post_type' = pair with display_post_type. 'specific_page' = pair with display_page_id."),
|
|
1804
|
+
display_post_type: z.string().optional().describe("Used when display_condition='specific_post_type' — e.g. 'page', 'post', 'astra-portfolio'"),
|
|
1805
|
+
display_page_id: z.string().optional().describe("Used when display_condition='specific_page' — numeric WP page ID"),
|
|
1806
|
+
popup_settings: z.string().optional().describe("For type='popup': JSON object with popup config (stringified). Common keys: triggers, conditions, advanced_rules, width, height, position. E.g. '{\"triggers\":{\"page_load\":\"yes\",\"page_load_delay\":3}}'"),
|
|
611
1807
|
site: siteParam,
|
|
612
|
-
}, async ({ title, type, status, elements, site }) => {
|
|
1808
|
+
}, async ({ title, type, status, elements, display_condition, display_post_type, display_page_id, popup_settings, site }) => {
|
|
613
1809
|
try {
|
|
614
1810
|
const { wpUrl, authHeader } = resolveSite(sites, site);
|
|
615
1811
|
const body = { title, type, status: status || "publish" };
|
|
@@ -619,6 +1815,20 @@ function createMcpServer(sites) {
|
|
|
619
1815
|
}
|
|
620
1816
|
catch { }
|
|
621
1817
|
}
|
|
1818
|
+
if (display_condition && display_condition !== "none")
|
|
1819
|
+
body.display_condition = display_condition;
|
|
1820
|
+
if (display_post_type)
|
|
1821
|
+
body.display_post_type = display_post_type;
|
|
1822
|
+
if (display_page_id)
|
|
1823
|
+
body.display_page_id = display_page_id;
|
|
1824
|
+
if (popup_settings) {
|
|
1825
|
+
try {
|
|
1826
|
+
body.popup_settings = JSON.parse(popup_settings);
|
|
1827
|
+
}
|
|
1828
|
+
catch {
|
|
1829
|
+
return { content: [{ type: "text", text: "Invalid JSON in popup_settings." }], isError: true };
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
622
1832
|
const r = await axios.post(`${wpUrl}/wp-json/erc/v1/templates`, body, { headers: { Authorization: authHeader } });
|
|
623
1833
|
return { content: [{ type: "text", text: JSON.stringify(r.data, null, 2) }] };
|
|
624
1834
|
}
|
|
@@ -1283,18 +2493,26 @@ function createMcpServer(sites) {
|
|
|
1283
2493
|
}
|
|
1284
2494
|
});
|
|
1285
2495
|
// ── Group 9: Screenshot ──────────────────────────────────────────────────────
|
|
1286
|
-
server.tool("screenshot-page", "Take a screenshot of a published WordPress page using system Chrome. Works with any post type (page, post, custom post types like astra-portfolio). Pass either page_id OR a direct url. Saves the file to disk and returns both a preview image and the saved file path.
|
|
2496
|
+
server.tool("screenshot-page", "Take a screenshot of a published WordPress page using system Chrome. Works with any post type (page, post, custom post types like astra-portfolio). Pass either page_id OR a direct url. Use viewport='tablet' or 'mobile' to verify responsive behavior — every responsive build should be screenshotted at all 3 breakpoints. Saves the file to disk and returns both a preview image and the saved file path.", {
|
|
1287
2497
|
page_id: z.string().optional().describe("WordPress post/page ID (works for any post type). Either page_id OR url must be provided."),
|
|
1288
2498
|
url: z.string().optional().describe("Direct URL to screenshot (e.g. https://site.com/services/xyz/). Use this for custom post types or external URLs. Takes precedence over page_id."),
|
|
2499
|
+
viewport: z.enum(["desktop", "tablet", "mobile"]).optional().describe("Preset viewport: desktop=1440px, tablet=768px, mobile=375px. Overrides width if both are passed. Critical for verifying responsive output at each breakpoint."),
|
|
1289
2500
|
full_page: z.boolean().optional().describe("Capture full scrollable page (default: true)"),
|
|
1290
|
-
width: z.number().optional().describe("Viewport width in pixels (default: 1440)"),
|
|
2501
|
+
width: z.number().optional().describe("Viewport width in pixels (default: 1440). Ignored if viewport preset is passed."),
|
|
1291
2502
|
format: z.enum(["jpeg", "png"]).optional().describe("Image format — jpeg is much smaller and recommended (default: jpeg)"),
|
|
1292
2503
|
quality: z.number().min(1).max(100).optional().describe("JPEG quality 1–100 (default: 82). Lower = smaller file size."),
|
|
1293
2504
|
max_height: z.number().optional().describe("Cap the captured height in pixels (e.g. 4000). Useful for very long pages to stay under size limits."),
|
|
1294
2505
|
wait: z.number().optional().describe("Extra milliseconds to wait after page load + auto-scroll before capturing (default: 2000). Increase for heavy animations/videos."),
|
|
1295
2506
|
auto_scroll: z.boolean().optional().describe("Scroll through the page to trigger lazy-loaded images and content (default: true). Disable only for pages with infinite scroll."),
|
|
1296
2507
|
site: siteParam,
|
|
1297
|
-
}, async ({ page_id, url, full_page, width, format, quality, max_height, wait, auto_scroll, site }) => {
|
|
2508
|
+
}, async ({ page_id, url, viewport, full_page, width, format, quality, max_height, wait, auto_scroll, site }) => {
|
|
2509
|
+
// viewport preset overrides explicit width
|
|
2510
|
+
if (viewport === "desktop")
|
|
2511
|
+
width = 1440;
|
|
2512
|
+
else if (viewport === "tablet")
|
|
2513
|
+
width = 768;
|
|
2514
|
+
else if (viewport === "mobile")
|
|
2515
|
+
width = 375;
|
|
1298
2516
|
try {
|
|
1299
2517
|
if (!page_id && !url) {
|
|
1300
2518
|
throw new Error("Provide either page_id or url.");
|