@yashwant.dharmdas/elementor-mcp 3.13.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 +299 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1270,6 +1270,305 @@ function createMcpServer(sites) {
|
|
|
1270
1270
|
return { content: [{ type: "text", text: `Error searching templates: ${error.response?.data?.message || error.message}` }], isError: true };
|
|
1271
1271
|
}
|
|
1272
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
|
+
});
|
|
1273
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.", {
|
|
1274
1573
|
page_id: z.string().describe("WordPress Page ID"),
|
|
1275
1574
|
elements_json: z.string().describe("Full elements array as stringified JSON"),
|
package/package.json
CHANGED