@yashwant.dharmdas/elementor-mcp 3.13.0 → 3.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +650 -62
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1046,8 +1046,291 @@ function createMcpServer(sites) {
1046
1046
  // ═══════════════════════════════════════════════════════════════════════════
1047
1047
  // ── COMPOUND BUILDERS — scaffold multi-step ops into one tool ─────────────
1048
1048
  // ═══════════════════════════════════════════════════════════════════════════
1049
+ // ── Helper: typography settings builder for child widgets ─────────────────
1050
+ // Maps clean params (typography_global, font_size, color, etc.) → Elementor's
1051
+ // typography_* keys + __globals__ entries. Used by create-section children.
1052
+ const buildTypography = (opts, prefix = "typography") => {
1053
+ const settings = {};
1054
+ const globals = {};
1055
+ if (!opts)
1056
+ return { settings, globals };
1057
+ // Global typography token wins over everything else
1058
+ if (opts.typography_global) {
1059
+ globals[`${prefix}_typography`] = `globals/typography?id=${opts.typography_global}`;
1060
+ return { settings, globals };
1061
+ }
1062
+ // Otherwise build custom typography
1063
+ let touched = false;
1064
+ const fontKey = `${prefix}_font_family`;
1065
+ const sizeKey = `${prefix}_font_size`;
1066
+ if (opts.font_family) {
1067
+ settings[fontKey] = opts.font_family;
1068
+ touched = true;
1069
+ }
1070
+ const fs1 = parseSize(opts.font_size);
1071
+ if (fs1) {
1072
+ settings[sizeKey] = fs1;
1073
+ touched = true;
1074
+ }
1075
+ const fs2 = parseSize(opts.font_size_tablet);
1076
+ if (fs2) {
1077
+ settings[`${sizeKey}_tablet`] = fs2;
1078
+ touched = true;
1079
+ }
1080
+ const fs3 = parseSize(opts.font_size_mobile);
1081
+ if (fs3) {
1082
+ settings[`${sizeKey}_mobile`] = fs3;
1083
+ touched = true;
1084
+ }
1085
+ if (opts.font_weight) {
1086
+ settings[`${prefix}_font_weight`] = String(opts.font_weight);
1087
+ touched = true;
1088
+ }
1089
+ if (opts.letter_spacing !== undefined) {
1090
+ settings[`${prefix}_letter_spacing`] = { unit: "px", size: opts.letter_spacing };
1091
+ touched = true;
1092
+ }
1093
+ const lh = parseSize(opts.line_height);
1094
+ if (lh) {
1095
+ settings[`${prefix}_line_height`] = lh;
1096
+ touched = true;
1097
+ }
1098
+ if (opts.text_transform) {
1099
+ settings[`${prefix}_text_transform`] = opts.text_transform;
1100
+ touched = true;
1101
+ }
1102
+ if (opts.text_decoration) {
1103
+ settings[`${prefix}_text_decoration`] = opts.text_decoration;
1104
+ touched = true;
1105
+ }
1106
+ if (touched)
1107
+ settings[`${prefix}_typography`] = "custom";
1108
+ return { settings, globals };
1109
+ };
1110
+ // ── Helper: build container settings from clean params ────────────────────
1111
+ const buildContainerSettings = (opts) => {
1112
+ const s = {};
1113
+ const g = {};
1114
+ if (opts.content_width)
1115
+ s.content_width = opts.content_width;
1116
+ if (opts.direction)
1117
+ s.flex_direction = opts.direction;
1118
+ if (opts.direction_mobile)
1119
+ s.flex_direction_mobile = opts.direction_mobile;
1120
+ if (opts.justify)
1121
+ s.flex_justify_content = opts.justify;
1122
+ if (opts.align)
1123
+ s.flex_align_items = opts.align;
1124
+ if (opts.wrap)
1125
+ s.flex_wrap = opts.wrap;
1126
+ const bw = parseSize(opts.boxed_width);
1127
+ if (bw)
1128
+ s.boxed_width = bw;
1129
+ const p1 = parseSpacing(opts.padding);
1130
+ if (p1)
1131
+ s.padding = p1;
1132
+ const p2 = parseSpacing(opts.padding_tablet);
1133
+ if (p2)
1134
+ s.padding_tablet = p2;
1135
+ const p3 = parseSpacing(opts.padding_mobile);
1136
+ if (p3)
1137
+ s.padding_mobile = p3;
1138
+ const g1 = parseGap(opts.gap);
1139
+ if (g1)
1140
+ s.flex_gap = g1;
1141
+ const g2 = parseGap(opts.gap_tablet);
1142
+ if (g2)
1143
+ s.flex_gap_tablet = g2;
1144
+ const g3 = parseGap(opts.gap_mobile);
1145
+ if (g3)
1146
+ s.flex_gap_mobile = g3;
1147
+ const mh1 = parseSize(opts.min_height);
1148
+ if (mh1)
1149
+ s.min_height = mh1;
1150
+ const mh2 = parseSize(opts.min_height_tablet);
1151
+ if (mh2)
1152
+ s.min_height_tablet = mh2;
1153
+ const mh3 = parseSize(opts.min_height_mobile);
1154
+ if (mh3)
1155
+ s.min_height_mobile = mh3;
1156
+ const w1 = parseSize(opts.width);
1157
+ if (w1)
1158
+ s.width = w1;
1159
+ const w2 = parseSize(opts.width_tablet);
1160
+ if (w2)
1161
+ s.width_tablet = w2;
1162
+ const w3 = parseSize(opts.width_mobile);
1163
+ if (w3)
1164
+ s.width_mobile = w3;
1165
+ if (opts.background_color) {
1166
+ s.background_background = "classic";
1167
+ const c = resolveColor(opts.background_color);
1168
+ if (c.value)
1169
+ s.background_color = c.value;
1170
+ if (c.global_id)
1171
+ g.background_color = c.global_id;
1172
+ }
1173
+ if (opts.background_image) {
1174
+ s.background_background = "classic";
1175
+ s.background_image = { url: opts.background_image, id: opts.background_image_id ?? "" };
1176
+ s.background_size = "cover";
1177
+ s.background_position = "center center";
1178
+ }
1179
+ if (opts.overlay_color || opts.overlay_opacity !== undefined) {
1180
+ s.background_overlay_background = "classic";
1181
+ if (opts.overlay_color) {
1182
+ const oc = resolveColor(opts.overlay_color);
1183
+ if (oc.value)
1184
+ s.background_overlay_color = oc.value;
1185
+ if (oc.global_id)
1186
+ g.background_overlay_color = oc.global_id;
1187
+ }
1188
+ if (opts.overlay_opacity !== undefined) {
1189
+ s.background_overlay_opacity = { unit: "px", size: opts.overlay_opacity };
1190
+ }
1191
+ }
1192
+ if (Object.keys(g).length)
1193
+ s.__globals__ = { ...(s.__globals__ || {}), ...g };
1194
+ return s;
1195
+ };
1196
+ // ── Helper: recursive child-element builder ───────────────────────────────
1197
+ // Accepts a ChildDef object and returns an Elementor element JSON node.
1198
+ // Supported `type` values: container, heading, text, button, image, spacer.
1199
+ // Containers can nest further children. Unknown types throw.
1200
+ const buildChild = (child) => {
1201
+ if (!child || typeof child !== "object") {
1202
+ throw new Error("create-section child must be an object with a 'type' field.");
1203
+ }
1204
+ const t = String(child.type || "").toLowerCase();
1205
+ // ── container ───────────────────────────────────────────────────────
1206
+ if (t === "container") {
1207
+ const s = buildContainerSettings(child);
1208
+ const node = {
1209
+ elType: "container",
1210
+ settings: s,
1211
+ elements: [],
1212
+ isInner: true,
1213
+ };
1214
+ if (Array.isArray(child.children)) {
1215
+ node.elements = child.children.map((c) => buildChild(c));
1216
+ }
1217
+ return node;
1218
+ }
1219
+ // ── heading ─────────────────────────────────────────────────────────
1220
+ if (t === "heading") {
1221
+ const s = { title: String(child.title ?? "") };
1222
+ if (child.header_size)
1223
+ s.header_size = child.header_size;
1224
+ if (child.align)
1225
+ s.align = child.align;
1226
+ const typo = buildTypography(child);
1227
+ Object.assign(s, typo.settings);
1228
+ if (Object.keys(typo.globals).length)
1229
+ s.__globals__ = { ...(s.__globals__ || {}), ...typo.globals };
1230
+ if (child.color) {
1231
+ const c = resolveColor(child.color);
1232
+ if (c.value)
1233
+ s.title_color = c.value;
1234
+ if (c.global_id)
1235
+ s.__globals__ = { ...(s.__globals__ || {}), title_color: c.global_id };
1236
+ }
1237
+ return { elType: "widget", widgetType: "heading", settings: s, elements: [] };
1238
+ }
1239
+ // ── text-editor (paragraph / rich text) ──────────────────────────────
1240
+ if (t === "text" || t === "text-editor") {
1241
+ const s = { editor: String(child.content ?? "") };
1242
+ const typo = buildTypography(child);
1243
+ Object.assign(s, typo.settings);
1244
+ if (Object.keys(typo.globals).length)
1245
+ s.__globals__ = { ...(s.__globals__ || {}), ...typo.globals };
1246
+ if (child.color) {
1247
+ const c = resolveColor(child.color);
1248
+ if (c.value)
1249
+ s.text_color = c.value;
1250
+ if (c.global_id)
1251
+ s.__globals__ = { ...(s.__globals__ || {}), text_color: c.global_id };
1252
+ }
1253
+ if (child.align)
1254
+ s.align = child.align;
1255
+ return { elType: "widget", widgetType: "text-editor", settings: s, elements: [] };
1256
+ }
1257
+ // ── button ───────────────────────────────────────────────────────────
1258
+ if (t === "button") {
1259
+ const s = { text: String(child.text ?? "") };
1260
+ if (child.link)
1261
+ s.link = { url: child.link };
1262
+ if (child.align)
1263
+ s.align = child.align;
1264
+ const typo = buildTypography(child);
1265
+ Object.assign(s, typo.settings);
1266
+ if (Object.keys(typo.globals).length)
1267
+ s.__globals__ = { ...(s.__globals__ || {}), ...typo.globals };
1268
+ if (child.text_color) {
1269
+ const c = resolveColor(child.text_color);
1270
+ if (c.value)
1271
+ s.button_text_color = c.value;
1272
+ if (c.global_id)
1273
+ s.__globals__ = { ...(s.__globals__ || {}), button_text_color: c.global_id };
1274
+ }
1275
+ if (child.background_color) {
1276
+ const c = resolveColor(child.background_color);
1277
+ if (c.value)
1278
+ s.background_color = c.value;
1279
+ if (c.global_id)
1280
+ s.__globals__ = { ...(s.__globals__ || {}), background_color: c.global_id };
1281
+ }
1282
+ if (child.border) {
1283
+ s.border_border = child.border.style || "solid";
1284
+ if (child.border.color) {
1285
+ const c = resolveColor(child.border.color);
1286
+ if (c.value)
1287
+ s.border_color = c.value;
1288
+ if (c.global_id)
1289
+ s.__globals__ = { ...(s.__globals__ || {}), border_color: c.global_id };
1290
+ }
1291
+ if (child.border.width !== undefined) {
1292
+ const w = Number(child.border.width);
1293
+ s.border_width = { unit: "px", top: String(w), right: String(w), bottom: String(w), left: String(w), isLinked: true };
1294
+ }
1295
+ }
1296
+ if (child.border_radius !== undefined) {
1297
+ const r = Number(child.border_radius);
1298
+ s.border_radius = { unit: "px", top: String(r), right: String(r), bottom: String(r), left: String(r), isLinked: true };
1299
+ }
1300
+ const p1 = parseSpacing(child.padding);
1301
+ if (p1)
1302
+ s.padding = p1;
1303
+ return { elType: "widget", widgetType: "button", settings: s, elements: [] };
1304
+ }
1305
+ // ── image ────────────────────────────────────────────────────────────
1306
+ if (t === "image") {
1307
+ const s = {
1308
+ image: { url: String(child.url ?? ""), id: child.attachment_id ?? "" },
1309
+ image_size: child.image_size || "full",
1310
+ };
1311
+ if (child.alt)
1312
+ s.alt = child.alt;
1313
+ if (child.link)
1314
+ s.link = { url: child.link };
1315
+ if (child.align)
1316
+ s.align = child.align;
1317
+ const w = parseSize(child.width);
1318
+ if (w)
1319
+ s.width = w;
1320
+ return { elType: "widget", widgetType: "image", settings: s, elements: [] };
1321
+ }
1322
+ // ── spacer ───────────────────────────────────────────────────────────
1323
+ if (t === "spacer") {
1324
+ const s = {};
1325
+ const h = parseSize(child.height);
1326
+ if (h)
1327
+ s.space = h;
1328
+ return { elType: "widget", widgetType: "spacer", settings: s, elements: [] };
1329
+ }
1330
+ throw new Error(`Unknown child type '${child.type}'. Supported: container, heading, text, button, image, spacer.`);
1331
+ };
1049
1332
  // ── 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.", {
1333
+ server.tool("create-section", "Scaffold a section container optionally with its FULL subtree of nested containers + widgets — in ONE call. Pass top-level settings (padding, bg, etc.) AND an optional 'children' array of typed widget defs. This replaces 50+ round-trips (insert-element + N×set-X) with a single call. Supported child types: 'container' (nests further), 'heading', 'text' (text-editor), 'button', 'image', 'spacer'. Each child accepts typography_global (preferred — uses kit globals) OR custom typography props (font_size, font_weight, etc.). Colors accept either hex ('#FFF') or kit-global slug ('primary').", {
1051
1334
  page_id: z.string().describe("WordPress Page ID"),
1052
1335
  parent_id: z.string().optional().describe("Parent container ID. Omit for top-level section."),
1053
1336
  position: z.enum(["before", "after", "first_child", "last_child"]).optional().describe("Default 'last_child' (append to parent or page end)"),
@@ -1065,78 +1348,38 @@ function createMcpServer(sites) {
1065
1348
  min_height: z.string().optional().describe("Min-height — '420px', '80vh'"),
1066
1349
  min_height_tablet: z.string().optional(),
1067
1350
  min_height_mobile: z.string().optional(),
1351
+ justify: z.enum(["flex-start", "center", "flex-end", "space-between", "space-around", "space-evenly"]).optional().describe("Justify content along the flex main axis"),
1352
+ align: z.enum(["flex-start", "center", "flex-end", "stretch", "baseline"]).optional().describe("Align items along the flex cross axis"),
1353
+ wrap: z.enum(["wrap", "nowrap"]).optional().describe("Flex wrap. Default 'nowrap'."),
1068
1354
  background_color: z.string().optional().describe("Background color — hex or global slug"),
1069
1355
  background_image: z.string().optional().describe("Background image URL"),
1356
+ background_image_id: z.number().optional().describe("WP attachment ID for the bg image (enables srcset). Use the id returned by list-media-library."),
1070
1357
  overlay_color: z.string().optional().describe("Overlay color — hex or global slug"),
1071
1358
  overlay_opacity: z.number().optional().describe("Overlay opacity 0–1"),
1359
+ children: z.array(z.any()).optional().describe("Optional array of child elements to build inside this section in the SAME call. Each child: { type: 'container'|'heading'|'text'|'button'|'image'|'spacer', ...settings }. Containers can nest further via their own 'children' array. Prefer typography_global ('primary-text', 'body') over hardcoded typography. Prefer kit-global color slugs ('primary', 'accent') over hex. This is the fastest way to build a section — ~50x fewer round-trips than the per-widget tools."),
1072
1360
  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 }) => {
1361
+ }, async (args) => {
1074
1362
  try {
1363
+ const { page_id, parent_id, position, reference_id, children, site } = args;
1075
1364
  const { wpUrl, authHeader } = resolveSite(sites, site);
1076
- const settings = {
1077
- content_width: content_width || "boxed",
1078
- flex_direction: direction || "column",
1365
+ // Default content_width "boxed" / direction "column" for top-level sections
1366
+ const containerOpts = {
1367
+ ...args,
1368
+ content_width: args.content_width || "boxed",
1369
+ direction: args.direction || "column",
1079
1370
  };
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 };
1371
+ const settings = buildContainerSettings(containerOpts);
1372
+ // Build any inline children recursively
1373
+ let elements = [];
1374
+ if (Array.isArray(children) && children.length) {
1375
+ try {
1376
+ elements = children.map((c) => buildChild(c));
1134
1377
  }
1135
- if (overlay_opacity !== undefined) {
1136
- settings.background_overlay_opacity = { unit: "px", size: overlay_opacity };
1378
+ catch (err) {
1379
+ return { content: [{ type: "text", text: `Error building children: ${err.message}` }], isError: true };
1137
1380
  }
1138
1381
  }
1139
- const newElement = { elType: "container", settings, elements: [] };
1382
+ const newElement = { elType: "container", settings, elements };
1140
1383
  const body = { element: newElement, position: position || "last_child" };
1141
1384
  if (parent_id)
1142
1385
  body.parent_id = parent_id;
@@ -1270,6 +1513,305 @@ function createMcpServer(sites) {
1270
1513
  return { content: [{ type: "text", text: `Error searching templates: ${error.response?.data?.message || error.message}` }], isError: true };
1271
1514
  }
1272
1515
  });
1516
+ // ═══════════════════════════════════════════════════════════════════════════
1517
+ // ── v3.14.0 — Figma → Elementor pipeline tools ────────────────────────────
1518
+ // ═══════════════════════════════════════════════════════════════════════════
1519
+ // ── 18. upload-image-to-media-library ─────────────────────────────────────
1520
+ 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.", {
1521
+ 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."),
1522
+ url: z.string().optional().describe("Remote URL to fetch and upload (e.g. external CDN URL). WordPress downloads it server-side."),
1523
+ 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."),
1524
+ mime_type: z.string().optional().describe("MIME type (e.g. 'image/png', 'image/jpeg', 'image/svg+xml'). Auto-detected from filename if omitted."),
1525
+ filename: z.string().describe("Desired filename in media library, e.g. 'hero-clearstone.png'. Must include extension."),
1526
+ title: z.string().optional().describe("Attachment title (defaults to filename without extension)"),
1527
+ alt_text: z.string().optional().describe("Alt text for accessibility"),
1528
+ site: siteParam,
1529
+ }, async ({ file_path, url, base64_data, mime_type, filename, title, alt_text, site }) => {
1530
+ try {
1531
+ const { wpUrl, authHeader } = resolveSite(sites, site);
1532
+ let body = { filename };
1533
+ if (title)
1534
+ body.title = title;
1535
+ if (alt_text)
1536
+ body.alt_text = alt_text;
1537
+ if (mime_type)
1538
+ body.mimeType = mime_type;
1539
+ if (file_path) {
1540
+ if (!fs.existsSync(file_path)) {
1541
+ return { content: [{ type: "text", text: `Error: File not found at path: ${file_path}` }], isError: true };
1542
+ }
1543
+ // Read file from disk and convert to base64 — happens in MCP server memory, NOT Claude context
1544
+ const buf = fs.readFileSync(file_path);
1545
+ body.data = buf.toString("base64");
1546
+ // Auto-detect mime from extension if not provided
1547
+ if (!body.mimeType) {
1548
+ const ext = path.extname(file_path).toLowerCase();
1549
+ const mimes = {
1550
+ ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg",
1551
+ ".gif": "image/gif", ".webp": "image/webp", ".svg": "image/svg+xml",
1552
+ ".pdf": "application/pdf",
1553
+ };
1554
+ body.mimeType = mimes[ext] || "application/octet-stream";
1555
+ }
1556
+ }
1557
+ else if (url) {
1558
+ body.url = url;
1559
+ }
1560
+ else if (base64_data) {
1561
+ body.data = base64_data;
1562
+ }
1563
+ else {
1564
+ return { content: [{ type: "text", text: "Provide one of: file_path, url, or base64_data." }], isError: true };
1565
+ }
1566
+ const r = await axios.post(`${wpUrl}/wp-json/erc/v1/media/upload`, body, {
1567
+ headers: { Authorization: authHeader, "Content-Type": "application/json" },
1568
+ maxBodyLength: 100 * 1024 * 1024, // allow up to 100MB
1569
+ maxContentLength: 100 * 1024 * 1024,
1570
+ });
1571
+ return { content: [{ type: "text", text: JSON.stringify(r.data, null, 2) }] };
1572
+ }
1573
+ catch (error) {
1574
+ return { content: [{ type: "text", text: `Error uploading to media library: ${error.response?.data?.message || error.message}` }], isError: true };
1575
+ }
1576
+ });
1577
+ // ── 19. set-element-shadow ────────────────────────────────────────────────
1578
+ 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.", {
1579
+ page_id: z.string().describe("WordPress Page ID"),
1580
+ element_id: z.string().describe("Elementor element ID"),
1581
+ shadow_type: z.enum(["none", "outline", "inset"]).optional().describe("'outline' = drop shadow (default), 'inset' = inner shadow, 'none' = remove shadow"),
1582
+ color: z.string().optional().describe("Shadow color hex with alpha — e.g. '#00000040' for 25% black. Or use global slug."),
1583
+ horizontal: z.number().optional().describe("X offset in px (positive = right)"),
1584
+ vertical: z.number().optional().describe("Y offset in px (positive = down)"),
1585
+ blur: z.number().optional().describe("Blur radius in px (Figma calls this 'radius')"),
1586
+ spread: z.number().optional().describe("Spread radius in px (Figma drop-shadow has 'spread' as the 4th value)"),
1587
+ site: siteParam,
1588
+ }, async ({ page_id, element_id, shadow_type, color, horizontal, vertical, blur, spread, site }) => {
1589
+ try {
1590
+ const { wpUrl, authHeader } = resolveSite(sites, site);
1591
+ const s = {};
1592
+ if (shadow_type === "none") {
1593
+ s.box_shadow_box_shadow_type = "";
1594
+ }
1595
+ else if (shadow_type) {
1596
+ s.box_shadow_box_shadow_type = shadow_type;
1597
+ }
1598
+ if (color) {
1599
+ const c = resolveColor(color);
1600
+ if (c.value)
1601
+ s.box_shadow_box_shadow = { ...(s.box_shadow_box_shadow || {}), color: c.value };
1602
+ if (c.global_id)
1603
+ s.__globals__ = { ...(s.__globals__ || {}), box_shadow_box_shadow: c.global_id };
1604
+ }
1605
+ // Elementor stores box_shadow as a single object with horizontal/vertical/blur/spread/color
1606
+ const shadowObj = s.box_shadow_box_shadow || {};
1607
+ if (horizontal !== undefined)
1608
+ shadowObj.horizontal = horizontal;
1609
+ if (vertical !== undefined)
1610
+ shadowObj.vertical = vertical;
1611
+ if (blur !== undefined)
1612
+ shadowObj.blur = blur;
1613
+ if (spread !== undefined)
1614
+ shadowObj.spread = spread;
1615
+ if (Object.keys(shadowObj).length > 0)
1616
+ s.box_shadow_box_shadow = shadowObj;
1617
+ const result = await applyElementSettings(wpUrl, authHeader, page_id, element_id, s);
1618
+ return { content: [{ type: "text", text: JSON.stringify({ applied: s, result }, null, 2) }] };
1619
+ }
1620
+ catch (error) {
1621
+ return { content: [{ type: "text", text: `Error setting shadow: ${error.response?.data?.message || error.message}` }], isError: true };
1622
+ }
1623
+ });
1624
+ // ── 20. set-element-typography ────────────────────────────────────────────
1625
+ 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.", {
1626
+ page_id: z.string().describe("WordPress Page ID"),
1627
+ element_id: z.string().describe("Elementor widget ID (heading, text-editor, button, etc.)"),
1628
+ global_typography: z.string().optional().describe("Kit typography token slug — references a global (e.g. 'primary', 'heading'). Preferred over individual properties."),
1629
+ font_family: z.string().optional().describe("Font family name, e.g. 'Playfair Display', 'Inter'"),
1630
+ font_size: z.string().optional().describe("Desktop font size — '60', '60px', '2.5rem', '60vh'"),
1631
+ font_size_tablet: z.string().optional(),
1632
+ font_size_mobile: z.string().optional(),
1633
+ font_weight: z.string().optional().describe("'400', '500', '600', '700', 'normal', 'bold'"),
1634
+ text_transform: z.enum(["none", "uppercase", "lowercase", "capitalize"]).optional(),
1635
+ text_decoration: z.enum(["none", "underline", "overline", "line-through"]).optional(),
1636
+ font_style: z.enum(["normal", "italic", "oblique"]).optional(),
1637
+ line_height: z.string().optional().describe("Line height — '1.5' (unitless), '24px', '1.5em'"),
1638
+ letter_spacing: z.string().optional().describe("Letter spacing — '1' (px default), '2px', '0.05em'"),
1639
+ color: z.string().optional().describe("Text color hex (e.g. '#000000') OR global slug (e.g. 'primary')"),
1640
+ align: z.enum(["left", "center", "right", "justify"]).optional().describe("Text alignment (only applies to widgets with align setting)"),
1641
+ align_mobile: z.enum(["left", "center", "right", "justify"]).optional(),
1642
+ site: siteParam,
1643
+ }, 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 }) => {
1644
+ try {
1645
+ const { wpUrl, authHeader } = resolveSite(sites, site);
1646
+ const s = {};
1647
+ // Typography is composite — Elementor uses typography_typography="custom" to enable per-element settings
1648
+ const needsCustomTypography = font_family || font_size || font_weight || text_transform || text_decoration || font_style || line_height || letter_spacing;
1649
+ if (needsCustomTypography)
1650
+ s.typography_typography = "custom";
1651
+ if (global_typography) {
1652
+ s.__globals__ = { ...(s.__globals__ || {}), typography_typography: `globals/typography?id=${global_typography}` };
1653
+ }
1654
+ if (font_family)
1655
+ s.typography_font_family = font_family;
1656
+ if (font_weight)
1657
+ s.typography_font_weight = font_weight;
1658
+ if (text_transform)
1659
+ s.typography_text_transform = text_transform;
1660
+ if (text_decoration)
1661
+ s.typography_text_decoration = text_decoration;
1662
+ if (font_style)
1663
+ s.typography_font_style = font_style;
1664
+ const fs1 = parseSize(font_size);
1665
+ if (fs1)
1666
+ s.typography_font_size = fs1;
1667
+ const fs2 = parseSize(font_size_tablet);
1668
+ if (fs2)
1669
+ s.typography_font_size_tablet = fs2;
1670
+ const fs3 = parseSize(font_size_mobile);
1671
+ if (fs3)
1672
+ s.typography_font_size_mobile = fs3;
1673
+ if (line_height) {
1674
+ // Unitless (e.g. "1.5") → use 'em'-like ratio; with unit → use that unit
1675
+ const lh = parseSize(line_height);
1676
+ if (lh)
1677
+ s.typography_line_height = lh;
1678
+ else if (/^\d+(\.\d+)?$/.test(String(line_height).trim())) {
1679
+ s.typography_line_height = { unit: "em", size: parseFloat(String(line_height)) };
1680
+ }
1681
+ }
1682
+ const ls = parseSize(letter_spacing);
1683
+ if (ls)
1684
+ s.typography_letter_spacing = ls;
1685
+ if (color) {
1686
+ const c = resolveColor(color);
1687
+ // Common color keys across widgets: title_color (heading), color (text-editor), button_text_color (button)
1688
+ // We set ALL three so it works regardless of widget type — Elementor ignores keys that don't apply
1689
+ if (c.value) {
1690
+ s.title_color = c.value;
1691
+ s.color = c.value;
1692
+ s.button_text_color = c.value;
1693
+ }
1694
+ if (c.global_id) {
1695
+ s.__globals__ = {
1696
+ ...(s.__globals__ || {}),
1697
+ title_color: c.global_id,
1698
+ color: c.global_id,
1699
+ button_text_color: c.global_id,
1700
+ };
1701
+ }
1702
+ }
1703
+ if (align)
1704
+ s.align = align;
1705
+ if (align_mobile)
1706
+ s.align_mobile = align_mobile;
1707
+ const result = await applyElementSettings(wpUrl, authHeader, page_id, element_id, s);
1708
+ return { content: [{ type: "text", text: JSON.stringify({ applied: s, result }, null, 2) }] };
1709
+ }
1710
+ catch (error) {
1711
+ return { content: [{ type: "text", text: `Error setting typography: ${error.response?.data?.message || error.message}` }], isError: true };
1712
+ }
1713
+ });
1714
+ // ── 21. set-text-content ──────────────────────────────────────────────────
1715
+ 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.", {
1716
+ page_id: z.string().describe("WordPress Page ID"),
1717
+ element_id: z.string().describe("Elementor widget ID"),
1718
+ text: z.string().describe("New text content. Can include basic HTML for text-editor widget."),
1719
+ 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."),
1720
+ site: siteParam,
1721
+ }, async ({ page_id, element_id, text, widget_type, site }) => {
1722
+ try {
1723
+ const { wpUrl, authHeader } = resolveSite(sites, site);
1724
+ const s = {};
1725
+ const t = widget_type ?? "auto";
1726
+ if (t === "heading")
1727
+ s.title = text;
1728
+ else if (t === "text-editor")
1729
+ s.editor = text;
1730
+ else if (t === "button")
1731
+ s.text = text;
1732
+ else if (t === "icon-box") {
1733
+ s.title_text = text;
1734
+ }
1735
+ else {
1736
+ // auto — set all common keys; Elementor ignores those that don't apply to the widget
1737
+ s.title = text;
1738
+ s.editor = text;
1739
+ s.text = text;
1740
+ s.title_text = text;
1741
+ }
1742
+ const result = await applyElementSettings(wpUrl, authHeader, page_id, element_id, s);
1743
+ return { content: [{ type: "text", text: JSON.stringify({ applied: s, result }, null, 2) }] };
1744
+ }
1745
+ catch (error) {
1746
+ return { content: [{ type: "text", text: `Error setting text content: ${error.response?.data?.message || error.message}` }], isError: true };
1747
+ }
1748
+ });
1749
+ // ── 22. set-image-widget-src ──────────────────────────────────────────────
1750
+ 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.", {
1751
+ page_id: z.string().describe("WordPress Page ID"),
1752
+ element_id: z.string().describe("Elementor image widget ID"),
1753
+ url: z.string().describe("Image URL (use the URL returned from upload-image-to-media-library)"),
1754
+ attachment_id: z.number().optional().describe("WordPress attachment ID (from upload-image-to-media-library response). Enables responsive srcset."),
1755
+ alt_text: z.string().optional().describe("Alt text for accessibility (also sets caption on some widgets)"),
1756
+ site: siteParam,
1757
+ }, async ({ page_id, element_id, url, attachment_id, alt_text, site }) => {
1758
+ try {
1759
+ const { wpUrl, authHeader } = resolveSite(sites, site);
1760
+ const s = {
1761
+ image: { url, id: attachment_id ?? "" },
1762
+ };
1763
+ if (alt_text) {
1764
+ s.image_alt = alt_text;
1765
+ s.caption = alt_text; // some widgets use 'caption' key
1766
+ }
1767
+ const result = await applyElementSettings(wpUrl, authHeader, page_id, element_id, s);
1768
+ return { content: [{ type: "text", text: JSON.stringify({ applied: s, result }, null, 2) }] };
1769
+ }
1770
+ catch (error) {
1771
+ return { content: [{ type: "text", text: `Error setting image src: ${error.response?.data?.message || error.message}` }], isError: true };
1772
+ }
1773
+ });
1774
+ // ── 23. set-element-opacity ───────────────────────────────────────────────
1775
+ 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.", {
1776
+ page_id: z.string().describe("WordPress Page ID"),
1777
+ element_id: z.string().describe("Elementor element ID"),
1778
+ opacity: z.number().describe("Opacity value — 0–1 (Figma style, e.g. 0.5) or 0–100 (percent, e.g. 50). Auto-detected."),
1779
+ site: siteParam,
1780
+ }, async ({ page_id, element_id, opacity, site }) => {
1781
+ try {
1782
+ const { wpUrl, authHeader } = resolveSite(sites, site);
1783
+ // Normalize to 0–1 range
1784
+ const normalized = opacity > 1 ? opacity / 100 : opacity;
1785
+ const s = {
1786
+ _element_opacity: { unit: "px", size: normalized },
1787
+ opacity: { unit: "px", size: normalized }, // alt key some widgets use
1788
+ };
1789
+ const result = await applyElementSettings(wpUrl, authHeader, page_id, element_id, s);
1790
+ return { content: [{ type: "text", text: JSON.stringify({ applied: s, result }, null, 2) }] };
1791
+ }
1792
+ catch (error) {
1793
+ return { content: [{ type: "text", text: `Error setting opacity: ${error.response?.data?.message || error.message}` }], isError: true };
1794
+ }
1795
+ });
1796
+ // ── 24. set-element-rotation ──────────────────────────────────────────────
1797
+ 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.", {
1798
+ page_id: z.string().describe("WordPress Page ID"),
1799
+ element_id: z.string().describe("Elementor element ID"),
1800
+ rotation: z.number().describe("Rotation in degrees (e.g. 45, -90, 180). 0 = no rotation."),
1801
+ site: siteParam,
1802
+ }, async ({ page_id, element_id, rotation, site }) => {
1803
+ try {
1804
+ const { wpUrl, authHeader } = resolveSite(sites, site);
1805
+ const s = {
1806
+ _element_rotate: { unit: "deg", size: rotation },
1807
+ };
1808
+ const result = await applyElementSettings(wpUrl, authHeader, page_id, element_id, s);
1809
+ return { content: [{ type: "text", text: JSON.stringify({ applied: s, result }, null, 2) }] };
1810
+ }
1811
+ catch (error) {
1812
+ return { content: [{ type: "text", text: `Error setting rotation: ${error.response?.data?.message || error.message}` }], isError: true };
1813
+ }
1814
+ });
1273
1815
  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
1816
  page_id: z.string().describe("WordPress Page ID"),
1275
1817
  elements_json: z.string().describe("Full elements array as stringified JSON"),
@@ -3233,6 +3775,52 @@ function createMcpServer(sites) {
3233
3775
  return { content: [{ type: "text", text: `Error updating video overlay: ${error.response?.data?.message || error.message}` }], isError: true };
3234
3776
  }
3235
3777
  });
3778
+ // ═══════════════════════════════════════════════════════════════════════════
3779
+ // ── v3.15.0 — Media library browse + page-structure validation ────────────
3780
+ // Replaces the "export images from Figma → upload" workflow with a
3781
+ // "developer uploads clean source images, AI matches by name" workflow.
3782
+ // ═══════════════════════════════════════════════════════════════════════════
3783
+ // ── list-media-library ────────────────────────────────────────────────────
3784
+ server.tool("list-media-library", "List images already uploaded to the WordPress media library. THIS IS THE PREFERRED way to source images for a Figma-→-Elementor build. Workflow: 1) Developer/designer uploads clean source images to WP media (bulk upload via WP admin → Media → Add New), 2) Call this tool to get the inventory, 3) Match each Figma layer name to a media item by 'normalised_slug' (kebab-cased filename) or fuzzy substring, 4) Pass the returned attachment_id + url to set-image-widget-src / set-container-background. NEVER export images from Figma — exports bake in overlays, text, and effects.", {
3785
+ search: z.string().optional().describe("Substring to filter by — matches title, filename, normalised_slug. e.g. 'hero' returns 'hero-bg.jpg', 'home-hero.webp'."),
3786
+ mime: z.string().optional().describe("MIME prefix filter (default 'image'). Use '' for all types, 'image/png' to narrow further."),
3787
+ per_page: z.number().optional().describe("Items per page (default 100, max 500)"),
3788
+ page: z.number().optional().describe("Page number for pagination (default 1)"),
3789
+ site: siteParam,
3790
+ }, async ({ search, mime, per_page, page, site }) => {
3791
+ try {
3792
+ const { wpUrl, authHeader } = resolveSite(sites, site);
3793
+ const r = await axios.get(`${wpUrl}/wp-json/erc/v1/media/list`, {
3794
+ headers: { Authorization: authHeader },
3795
+ params: {
3796
+ search: search ?? "",
3797
+ mime: mime ?? "image",
3798
+ per_page: per_page ?? 100,
3799
+ page: page ?? 1,
3800
+ },
3801
+ });
3802
+ return { content: [{ type: "text", text: JSON.stringify(r.data, null, 2) }] };
3803
+ }
3804
+ catch (error) {
3805
+ return { content: [{ type: "text", text: `Error listing media library: ${error.response?.data?.message || error.message}` }], isError: true };
3806
+ }
3807
+ });
3808
+ // ── validate-page-structure ───────────────────────────────────────────────
3809
+ server.tool("validate-page-structure", "Post-build sanity check on a page. Returns a report of structural / standards violations: header widgets in page body (should be a template), images with external URLs missing WP attachment IDs, hardcoded fonts/colors that should use kit globals, legacy _element_width on flex children, missing image alt text, suspicious min-heights. Call this AFTER building a page from Figma. If errors are found, fix them with the typed tools and re-run. If only warnings remain, surface them to the human for review.", {
3810
+ page_id: z.string().describe("WordPress Page ID to validate"),
3811
+ site: siteParam,
3812
+ }, async ({ page_id, site }) => {
3813
+ try {
3814
+ const { wpUrl, authHeader } = resolveSite(sites, site);
3815
+ const r = await axios.get(`${wpUrl}/wp-json/erc/v1/pages/${page_id}/validate-structure`, {
3816
+ headers: { Authorization: authHeader },
3817
+ });
3818
+ return { content: [{ type: "text", text: JSON.stringify(r.data, null, 2) }] };
3819
+ }
3820
+ catch (error) {
3821
+ return { content: [{ type: "text", text: `Error validating page structure: ${error.response?.data?.message || error.message}` }], isError: true };
3822
+ }
3823
+ });
3236
3824
  return server;
3237
3825
  }
3238
3826
  // ─── Entry Point ──────────────────────────────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yashwant.dharmdas/elementor-mcp",
3
- "version": "3.13.0",
3
+ "version": "3.15.0",
4
4
  "description": "MCP server for controlling Elementor via Claude — supports multiple WordPress sites",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",