@noleemits/vision-builder-control-mcp 4.90.0 → 4.96.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/index.js +138 -10
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -111,7 +111,7 @@ process.on('SIGINT', () => {
111
111
  // CONFIG
112
112
  // ================================================================
113
113
 
114
- const VERSION = '4.90.0';
114
+ const VERSION = '4.96.0';
115
115
  const MIN_PLUGIN_VERSION = '4.13.0'; // Minimum WP plugin version required by this MCP server
116
116
 
117
117
  // ================================================================
@@ -2213,7 +2213,7 @@ function getToolDefinitions() {
2213
2213
  },
2214
2214
  {
2215
2215
  name: 'set_container_layout',
2216
- description: 'Set a container\'s layout (flex/grid) from a clean intent — prefer this over raw update_element when you want WYSIWYG-editable native Elementor layout instead of custom_css workarounds. Translates {mode, columns, gap, responsive} into the full Elementor schema (container_type, grid_columns_grid object, gap object shape, _tablet/_mobile variants, etc.). Modes: "grid" (uses Elementor native CSS Grid — best for card layouts), "flex-row" (horizontal flex), "flex-column" (vertical stack). Defaults: gap=24px, responsive=auto (tablet halves columns, mobile = 1 col). Pass responsive: false to skip responsive variants. Only operates on containers (rejected with invalid_target if you target a widget). The resulting layout shows up in Elementor\'s Layout panel as if a human set it, so non-technical editors can modify it through the UI. v4.44.0+. v4.86.0: containers now default to content_width:"full" (no boxed `.e-con-inner` wrapper) so flex-row children lay out side-by-side and `.row > .e-con` CSS works pass content_width:"boxed" to opt back into a centered max-width container.',
2216
+ description: 'Set a container\'s layout (flex/grid) from a clean intent — prefer this over raw update_element when you want WYSIWYG-editable native Elementor layout instead of custom_css workarounds. Translates {mode, columns, gap, responsive} into the full Elementor schema (container_type, grid_columns_grid object, gap object shape, _tablet/_mobile variants, etc.). Modes: "grid" (uses Elementor native CSS Grid — best for card layouts), "flex-row" (horizontal flex), "flex-column" (vertical stack). Defaults: gap=24px, responsive=auto (tablet halves columns, mobile = 1 col). Pass responsive: false to skip responsive variants. Only operates on containers (rejected with invalid_target if you target a widget). The resulting layout shows up in Elementor\'s Layout panel as if a human set it, so non-technical editors can modify it through the UI. v4.44.0+. WIDTH SEMANTICS (v4.86.0+): containers default to content_width:"full" no boxed `.e-con-inner` wrapper, flex-row children lay out side-by-side, and `.your-class > .e-con` CSS hits direct children. Pass content_width:"boxed" for a centered max-width container; combine with boxed_width to set the cap (Elementor\'s default cap is 1140px when unset). Common gotcha: a boxed container with a child carrying _element_custom_width in px greater than the boxed cap will overflow at every viewport — pre-flight with validate_section, which now flags this (child_width_exceeds_parent, flex_row_fixed_widths_overflow). For atomic v4 e-flexbox, width is set via a different settings shape — use add_element preset:"cols-N" for ready-made full-width column rows.',
2217
2217
  inputSchema: {
2218
2218
  type: 'object',
2219
2219
  properties: {
@@ -3663,7 +3663,7 @@ function getToolDefinitions() {
3663
3663
  },
3664
3664
  {
3665
3665
  name: 'append_section',
3666
- description: 'Append a section to an existing page or template. Pass full section JSON OR use the preset param for instant gap-safe column layouts. PRESET SHORTHAND: pass preset="cols-3" (or cols-2, cols-4, sidebar-left, sidebar-right, split, stack) and the scaffold JSON is generated for you — no manual flex-grow JSON needed. The section param becomes optional when using preset; any settings in section are merged into the preset root (use it to set background_color, css_classes, _title, etc.). engine param selects v3 (classic Container) or v4 (atomic e-flexbox); auto-detected if omitted.',
3666
+ description: 'Append a section to an existing page or template. Pass full section JSON OR use the preset param for instant gap-safe column layouts. PRESET SHORTHAND: pass preset="cols-3" (or cols-2, cols-4, sidebar-left, sidebar-right, split, stack) and the scaffold JSON is generated for you — no manual flex-grow JSON needed. The section param becomes optional when using preset; any settings in section are merged into the preset root (use it to set background_color, css_classes, _title, etc.). engine param selects v3 (classic Container) or v4 (atomic e-flexbox); auto-detected if omitted. WIDTH SEMANTICS: section root containers default to content_width="full" (no boxed `.e-con-inner` wrapper) — flex-row children lay out side-by-side; pass content_width="boxed" + numeric boxed_width for a centered max-width section. When a section has content_width="boxed", any child with _element_custom_width in px greater than boxed_width will overflow horizontally — run validate_section before submitting to catch this (rules child_width_exceeds_parent + flex_row_fixed_widths_overflow, v4.95.0+).',
3667
3667
  inputSchema: {
3668
3668
  type: 'object',
3669
3669
  properties: {
@@ -3681,7 +3681,7 @@ function getToolDefinitions() {
3681
3681
  },
3682
3682
  {
3683
3683
  name: 'validate_section',
3684
- description: 'Lint a section JSON for known bug patterns BEFORE submitting it via append_section / build_page / import_template. Detects: (1) class_based_selectors_only — [data-id^="prefix"] selectors that match nested elements; (2) grid_target_e_con_inner — grid CSS on outer container instead of `.your-grid > .e-con-inner`; (3) button_styling_needs_scoped_css — button widgets without scoped CSS override; (4) numeric_dimensional_fields — clamp()/var() in padding/margin/font_size; (5) no_id_prefix_collision — child element ids that start with the parent\'s id. Returns warnings array with rule name, message, and offending element/selector. Run this whenever you generate complex section JSON, especially with custom CSS.',
3684
+ description: 'Lint a section JSON for known bug patterns BEFORE submitting it via append_section / build_page / import_template. Detects: (1) class_based_selectors_only — [data-id^="prefix"] selectors that match nested elements; (2) grid_target_e_con_inner — grid CSS on outer container instead of `.your-grid > .e-con-inner`; (3) button_styling_needs_scoped_css — button widgets without scoped CSS override; (4) numeric_dimensional_fields — clamp()/var() in padding/margin/font_size; (5) no_id_prefix_collision — child element ids that start with the parent\'s id; (6) v4.95.0+ child_width_exceeds_parent — a single child\'s px _element_custom_width is wider than the parent\'s boxed content_width (would overflow at every viewport); (7) v4.95.0+ flex_row_fixed_widths_overflow — on flex_direction=row + flex_wrap=nowrap, the sum of children\'s fixed px widths exceeds the parent\'s content_width (would overflow horizontally) — emitted as an ERROR. Returns warnings + errors arrays with rule name, message, and offending element/selector. Run this whenever you generate complex section JSON, especially with custom CSS or boxed containers carrying fixed-width children. v4.95.0 width rules are v3-only (atomic e-flexbox uses a different settings shape).',
3685
3685
  inputSchema: {
3686
3686
  type: 'object',
3687
3687
  properties: {
@@ -3960,12 +3960,14 @@ function getToolDefinitions() {
3960
3960
  // ── Class Registry ──
3961
3961
  {
3962
3962
  name: 'list_classes',
3963
- description: 'List all registered CSS classes in the NVBC class registry. Returns classes with name, description, category, and which element types they apply to. Use to discover available classes before applying them via update_element. Optionally filter by category or element type.',
3963
+ description: 'List registered CSS classes in the NVBC class registry. Returns classes with name, description, category, applies_to, scope, and status. Use to discover available classes before applying them via update_element. By default DRAFT site classes are hidden — pass include_drafts:true to see them. Pass page_id to filter site classes to those that are global OR scoped to that page (built-ins always pass). v4.92.0+ adds scope/status filtering; pre-v4.92 site classes are treated as scope=global + status=published.',
3964
3964
  inputSchema: {
3965
3965
  type: 'object',
3966
3966
  properties: {
3967
- category: { type: 'string', enum: ['hero', 'cards', 'layout', 'grid', 'spacing', 'typography', 'decorative', 'utility'], description: 'Filter to a specific category.' },
3968
- applies_to: { type: 'string', enum: ['container', 'widget', 'heading', 'button', 'image', 'any'], description: 'Filter to classes that apply to this element type.' }
3967
+ category: { type: 'string', enum: ['hero', 'cards', 'layout', 'grid', 'spacing', 'typography', 'decorative', 'utility'], description: 'Filter to a specific category.' },
3968
+ applies_to: { type: 'string', enum: ['container', 'widget', 'heading', 'button', 'image', 'any'], description: 'Filter to classes that apply to this element type.' },
3969
+ page_id: { type: 'number', description: 'WordPress page ID. When set, site classes with scope=page are filtered to those including this page. Omit to see all (global + every page-scoped class).' },
3970
+ include_drafts: { type: 'boolean', description: 'Include site classes with status=draft. Default false.' }
3969
3971
  }
3970
3972
  }
3971
3973
  },
@@ -4009,7 +4011,7 @@ function getToolDefinitions() {
4009
4011
  },
4010
4012
  {
4011
4013
  name: 'upsert_class',
4012
- description: 'Define or update a site CSS class — stores its metadata AND CSS body in the NVBC class registry. NVBC renders the CSS page-scoped (only on pages that use the class); it is NOT written to the Elementor Kit. The class appears in list_classes with source=[site] and in the editor Browse picker. Optional apply_to: attach the class to element IDs on specific pages in the same call. Call again with the same name to update (css is replaced). v4.76.0+.',
4014
+ description: 'Define or update a site CSS class — stores its metadata AND CSS body in the NVBC class registry. NVBC renders the CSS page-scoped (only on pages that use the class); it is NOT written to the Elementor Kit. The class appears in list_classes with source=[site] and in the editor Browse picker. Optional apply_to: attach the class to element IDs on specific pages in the same call. Call again with the same name to update (css is replaced). v4.76.0+. v4.92.0 adds scope (global vs page) and status (published vs draft): a page-scoped class only appears in the editor picker on its target pages; a draft class is hidden from the picker entirely until you flip status to published.',
4013
4015
  inputSchema: {
4014
4016
  type: 'object',
4015
4017
  properties: {
@@ -4018,9 +4020,12 @@ function getToolDefinitions() {
4018
4020
  description: { type: 'string', description: 'Human-readable description shown in list_classes output.' },
4019
4021
  category: { type: 'string', enum: ['hero', 'cards', 'layout', 'grid', 'spacing', 'typography', 'decorative', 'utility'], description: 'Category for list_classes grouping. Default: "utility".' },
4020
4022
  applies_to: { type: 'array', items: { type: 'string' }, description: 'Element types this class applies to. Default: ["any"].' },
4023
+ scope: { type: 'string', enum: ['global', 'page'], description: 'v4.92.0+. "global" (default) = appears in the editor picker on every page. "page" = only appears in the picker on the pages listed in scope_page_ids (or derived from apply_to). Render is unaffected: if the class is on an element, its CSS still emits — scope only filters editor-picker visibility.' },
4024
+ scope_page_ids: { type: 'array', items: { type: 'number' }, description: 'v4.92.0+. Required when scope=page (or supply apply_to with page_id entries and the server will derive these). WordPress page IDs the class belongs to.' },
4025
+ status: { type: 'string', enum: ['published', 'draft'], description: 'v4.92.0+. "published" (default) = appears in list_classes + editor picker. "draft" = hidden from both until promoted. Useful while iterating on a class.' },
4021
4026
  apply_to: {
4022
4027
  type: 'array',
4023
- description: 'Optional: attach this class to these elements in the same call.',
4028
+ description: 'Optional: attach this class to these elements in the same call. When scope=page and scope_page_ids is omitted, the page_ids here are used as scope_page_ids.',
4024
4029
  items: {
4025
4030
  type: 'object',
4026
4031
  properties: {
@@ -4101,6 +4106,42 @@ function getToolDefinitions() {
4101
4106
  }
4102
4107
  }
4103
4108
  },
4109
+ {
4110
+ name: 'list_icons',
4111
+ description: 'List bundled Phosphor icons. NVBC ships a curated ~130-icon set of Phosphor regular weight as static SVG assets. Output groups icons by category (arrows, status, navigation, business, medical, legal, communication, files, media, people, misc) and includes a flat sorted name array. Filter with q (substring search, case-insensitive) or category. Pair with sideload_icon to convert a name into a WP attachment id for e-svg / icon-box. Icons sideloaded once survive plugin deactivation (they become normal media-library attachments). v4.96.0+.',
4112
+ inputSchema: {
4113
+ type: 'object',
4114
+ properties: {
4115
+ library: { type: 'string', enum: ['phosphor'], description: 'Icon library. Only "phosphor" is bundled. Default phosphor.' },
4116
+ weight: { type: 'string', enum: ['regular'], description: 'Icon weight. Only "regular" is bundled. Default regular.' },
4117
+ q: { type: 'string', description: 'Substring search (case-insensitive) — matches icon names containing the query.' },
4118
+ category: { type: 'string', enum: ['arrows', 'status', 'navigation', 'business', 'medical', 'legal', 'communication', 'files', 'media', 'people', 'misc'], description: 'Filter to a single category.' }
4119
+ }
4120
+ }
4121
+ },
4122
+ {
4123
+ name: 'sideload_icon',
4124
+ description: 'Sideload a bundled Phosphor SVG into the WP media library and return its attachment id. On first reference the SVG is uploaded; subsequent calls hit the plugin-level cache and return instantly. Use the returned attachment_id with e-svg widgets, icon-box selected_icon (custom svg path), or anywhere an attachment id is accepted. Sideloaded icons become normal WP attachments — they persist if NVBC is deactivated. Pass shorthand:"phosphor:caduceus" for brevity, or library/weight/name explicitly. v4.96.0+.',
4125
+ inputSchema: {
4126
+ type: 'object',
4127
+ properties: {
4128
+ shorthand: { type: 'string', description: 'Convenience: "name", "library:name", or "library:weight:name". Defaults library=phosphor, weight=regular. e.g. "caduceus", "phosphor:check", "phosphor:regular:gavel".' },
4129
+ library: { type: 'string', enum: ['phosphor'], description: 'Library when not using shorthand. Default phosphor.' },
4130
+ weight: { type: 'string', enum: ['regular'], description: 'Weight when not using shorthand. Default regular.' },
4131
+ name: { type: 'string', description: 'Icon name (no extension) when not using shorthand. e.g. "arrow-right".' }
4132
+ }
4133
+ }
4134
+ },
4135
+ {
4136
+ name: 'install_font_size_presets',
4137
+ description: 'Register the 7 built-in fluid font-size classes (nvbc-font-xxsm…xxl) as REAL Elementor 4 Global Classes so they appear in the Atomic Editor\'s class picker and apply on selection. Each class\'s font-size is var(--nvbc-font-size-X) (the design-token scale), so editing the token re-sizes every instance — no re-install. Idempotent (upsert by label). On Elementor 3 these classes already ship as built-in registry classes (page-scoped) and need no install. Defaults to a DRY RUN — pass dry_run:false to write. Returns label→g-id so you can place a size via add_element with settings.classes.value:["g-id"]. v4.91.0+.',
4138
+ inputSchema: {
4139
+ type: 'object',
4140
+ properties: {
4141
+ dry_run: { type: 'boolean', description: 'Report only, write nothing. Default TRUE — pass false to apply.' }
4142
+ }
4143
+ }
4144
+ },
4104
4145
  ];
4105
4146
  }
4106
4147
 
@@ -7637,10 +7678,19 @@ async function handleToolCall(name, args) {
7637
7678
  const params = new URLSearchParams();
7638
7679
  if (args.category) params.set('category', args.category);
7639
7680
  if (args.applies_to) params.set('applies_to', args.applies_to);
7681
+ if (args.page_id) params.set('page_id', String(args.page_id));
7682
+ if (args.include_drafts) params.set('include_status', 'published,draft');
7640
7683
 
7641
7684
  const r = await apiCall(`/class-registry?${params}`);
7642
7685
 
7643
7686
  let msg = `=== CSS CLASS REGISTRY (${r.total} classes) ===\n`;
7687
+ if (r.filters) {
7688
+ const f = r.filters;
7689
+ const bits = [];
7690
+ if (f.page_id) bits.push(`page_id=${f.page_id}`);
7691
+ if (Array.isArray(f.include_status)) bits.push(`status=${f.include_status.join('+')}`);
7692
+ if (bits.length) msg += `Filters: ${bits.join(' · ')}\n`;
7693
+ }
7644
7694
  msg += `Categories: ${r.categories.join(', ')}\n\n`;
7645
7695
 
7646
7696
  const byCategory = {};
@@ -7654,7 +7704,17 @@ async function handleToolCall(name, args) {
7654
7704
  for (const c of items) {
7655
7705
  const tags = c.applies_to.join(', ');
7656
7706
  const src = c.source === 'plugin' ? '[plugin]' : '[site]';
7657
- msg += ` .${c.name} ${src}\n`;
7707
+ // v4.92.0 — surface scope/status when set on site classes.
7708
+ const flags = [];
7709
+ if (c.source !== 'plugin') {
7710
+ if (c.scope === 'page') {
7711
+ const ids = Array.isArray(c.scope_page_ids) ? c.scope_page_ids.join(',') : '';
7712
+ flags.push(`scope=page${ids ? `[${ids}]` : ''}`);
7713
+ }
7714
+ if (c.status === 'draft') flags.push('DRAFT');
7715
+ }
7716
+ const flagStr = flags.length ? ` (${flags.join(' · ')})` : '';
7717
+ msg += ` .${c.name} ${src}${flagStr}\n`;
7658
7718
  msg += ` ${c.description}\n`;
7659
7719
  msg += ` Applies to: ${tags}\n`;
7660
7720
  if (c.source === 'site' && c.css) {
@@ -7677,11 +7737,28 @@ async function handleToolCall(name, args) {
7677
7737
  applies_to: args.applies_to || ['any'],
7678
7738
  };
7679
7739
  if (args.css) regBody.css = args.css;
7740
+ // v4.92.0 — scope + status + scope_page_ids.
7741
+ if (args.scope) regBody.scope = args.scope;
7742
+ if (args.status) regBody.status = args.status;
7743
+ if (Array.isArray(args.scope_page_ids) && args.scope_page_ids.length) {
7744
+ regBody.scope_page_ids = args.scope_page_ids;
7745
+ }
7746
+ // When scope=page and no explicit ids, forward apply_to so the server derives them.
7747
+ if (args.scope === 'page' && !regBody.scope_page_ids && Array.isArray(args.apply_to)) {
7748
+ regBody.apply_to = args.apply_to;
7749
+ }
7680
7750
 
7681
7751
  const r = await apiCall('/class-registry/custom', 'POST', regBody);
7682
7752
  if (r.error || r.code) return ok(`Failed to save class definition: ${r.message || r.error}`);
7683
7753
 
7684
7754
  let msg = `Class .${args.name} saved to registry.\n`;
7755
+ if (r.class?.scope === 'page') {
7756
+ const ids = (r.class.scope_page_ids || []).join(', ');
7757
+ msg += `Scope: page-only (${ids}). The class is hidden from the editor picker on other pages.\n`;
7758
+ }
7759
+ if (r.class?.status === 'draft') {
7760
+ msg += `Status: DRAFT. The class is hidden from list_classes + the editor picker until you flip it to published.\n`;
7761
+ }
7685
7762
  if (args.css) {
7686
7763
  msg += `CSS stored on the class — renders page-scoped (only on pages that use .${args.name}).\n`;
7687
7764
  }
@@ -7769,6 +7846,41 @@ async function handleToolCall(name, args) {
7769
7846
  return ok(msg);
7770
7847
  }
7771
7848
 
7849
+ case 'list_icons': {
7850
+ const params = new URLSearchParams();
7851
+ if (args.library) params.set('library', args.library);
7852
+ if (args.weight) params.set('weight', args.weight);
7853
+ if (args.q) params.set('q', args.q);
7854
+ if (args.category) params.set('category', args.category);
7855
+ const r = await apiCall(`/icons?${params}`);
7856
+ if (r.error || r.code) return ok(`Failed: ${r.message || r.error}`);
7857
+
7858
+ let msg = `=== ICONS (${r.library} ${r.weight}, ${r.total} match${r.total === 1 ? '' : 'es'}) ===\n`;
7859
+ for (const [cat, names] of Object.entries(r.categories || {})) {
7860
+ msg += `\n── ${cat.toUpperCase()} (${names.length}) ──\n `;
7861
+ msg += names.join(', ') + '\n';
7862
+ }
7863
+ msg += `\nUse: sideload_icon({shorthand: "${r.library}:NAME"}) → attachment_id for e-svg / icon-box.\n`;
7864
+ return ok(msg);
7865
+ }
7866
+
7867
+ case 'sideload_icon': {
7868
+ const body = {};
7869
+ if (args.shorthand) body.shorthand = args.shorthand;
7870
+ if (args.library) body.library = args.library;
7871
+ if (args.weight) body.weight = args.weight;
7872
+ if (args.name) body.name = args.name;
7873
+ const r = await apiCall('/icons/sideload', 'POST', body);
7874
+ if (r.error || r.code) return ok(`Sideload failed: ${r.message || r.error || r.code}`);
7875
+ const tag = r.cached ? '(cache hit)' : '(uploaded)';
7876
+ let msg = `${r.library}:${r.weight}:${r.name} → attachment_id ${r.attachment_id} ${tag}\n`;
7877
+ msg += `URL: ${r.url}\n`;
7878
+ msg += `Use this attachment_id in:\n`;
7879
+ msg += ` • e-svg widget: settings.image.value.src.value.id\n`;
7880
+ msg += ` • icon widgets: selected_icon.library:"svg", selected_icon.value.id\n`;
7881
+ return ok(msg);
7882
+ }
7883
+
7772
7884
  case 'install_pill_presets': {
7773
7885
  const body = {};
7774
7886
  if (args.engine) body.engine = args.engine;
@@ -7798,6 +7910,22 @@ async function handleToolCall(name, args) {
7798
7910
  return ok(msg);
7799
7911
  }
7800
7912
 
7913
+ case 'install_font_size_presets': {
7914
+ const body = { dry_run: (args.dry_run === false) ? false : true };
7915
+ const r = await apiCall('/install-font-size-presets', 'POST', body);
7916
+ if (r.error || r.code) return ok(`Install failed: ${r.message || r.error || r.code}`);
7917
+ let msg = `${r.dry_run ? 'DRY RUN — nothing written.' : 'Installed.'} Elementor4=${r.elementor4 ? 'yes' : 'no'}\n`;
7918
+ msg += `Sizes: ${(r.sizes || []).join(', ')}\n`;
7919
+ if (r.note) msg += `${r.note}\n`;
7920
+ for (const c of (r.changes || [])) msg += ` ${c}\n`;
7921
+ if (r.class_ids && Object.keys(r.class_ids).length) {
7922
+ const ids = Object.entries(r.class_ids).map(([l, id]) => `${l} → ${id || '(new)'}`);
7923
+ msg += ` ids: ${ids.join(', ')}\n`;
7924
+ }
7925
+ if (r.dry_run) msg += `\nRun again with dry_run:false to apply.\n`;
7926
+ return ok(msg);
7927
+ }
7928
+
7801
7929
  case 'repair_page_classes': {
7802
7930
  const r = await apiCall(`/pages/${args.page_id}/repair-element-classes`, 'POST', {
7803
7931
  dry_run: args.dry_run ?? false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noleemits/vision-builder-control-mcp",
3
- "version": "4.90.0",
3
+ "version": "4.96.0",
4
4
  "description": "Vision Builder Control MCP server - design token-driven page builder tools for WordPress/Elementor websites",
5
5
  "type": "module",
6
6
  "main": "index.js",