@yashwant.dharmdas/elementor-mcp 3.14.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 +351 -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;
@@ -3532,6 +3775,52 @@ function createMcpServer(sites) {
3532
3775
  return { content: [{ type: "text", text: `Error updating video overlay: ${error.response?.data?.message || error.message}` }], isError: true };
3533
3776
  }
3534
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
+ });
3535
3824
  return server;
3536
3825
  }
3537
3826
  // ─── Entry Point ──────────────────────────────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yashwant.dharmdas/elementor-mcp",
3
- "version": "3.14.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",