@noleemits/vision-builder-control-mcp 4.119.0 → 4.120.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 +146 -18
  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.119.0';
114
+ const VERSION = '4.120.0';
115
115
  const MIN_PLUGIN_VERSION = '4.13.0'; // Minimum WP plugin version required by this MCP server
116
116
 
117
117
  // v4.110.0: shared styling philosophy appended to every build/style tool so the
@@ -3164,15 +3164,18 @@ function getToolDefinitions() {
3164
3164
  },
3165
3165
  {
3166
3166
  name: 'add_dynamic_field',
3167
- description: 'Add a dynamic-data widget to a loop-item / single / archive template — a STANDARD widget (heading/text-editor/image) carrying the correct Elementor __dynamic__ tag. Use this INSTEAD of the theme-post-* widgets (theme-post-title/-excerpt/-featured-image), which are unreliable when authored via REST (they render placeholders). Types: title→heading+post-title, excerpt→text-editor+post-excerpt, content→text-editor+post-content, featured_image→image+post-featured-image, acf→heading+acf (pass field = the ACF field key/name). The widget renders the current post\'s data inside loop/single context.',
3167
+ description: 'Add a dynamic-data widget to a loop-item / single / archive template — a STANDARD widget (heading/text-editor/image) carrying the correct Elementor __dynamic__ tag. Use this INSTEAD of the theme-post-* widgets (theme-post-title/-excerpt/-featured-image), which are unreliable when authored via REST (they render placeholders). Types: title→heading+post-title, excerpt→text-editor+post-excerpt, content→text-editor+post-content, featured_image→image+post-featured-image, acf→typed ACF tag (acf-text/acf-number/acf-image/acf-url). For type=acf set `field` (the ACF field NAME) and optionally `field_type` (default text) and `source`: "post" (default — current/queried post) or "options" (an ACF OPTIONS page → renders the same site-wide value on EVERY page). Requires the field group to be attached to an options page (see update_acf_field_group).',
3168
3168
  inputSchema: {
3169
3169
  type: 'object',
3170
3170
  properties: {
3171
3171
  page_id: { type: 'number', description: 'Template (or page) ID to add into' },
3172
3172
  parent_id: { type: 'string', description: 'Elementor ID of the parent container' },
3173
- type: { type: 'string', enum: ['title', 'excerpt', 'content', 'featured_image', 'acf'], description: 'Which post field to bind.' },
3174
- field: { type: 'string', description: 'For type=acf: the ACF field key or name to bind.' },
3175
- tag: { type: 'string', description: 'Override the dynamic tag name (advanced e.g. a custom registered tag).' },
3173
+ type: { type: 'string', enum: ['title', 'excerpt', 'content', 'featured_image', 'acf'], description: 'Which field to bind.' },
3174
+ field: { type: 'string', description: 'For type=acf: the ACF field NAME to bind (e.g. "stat_success").' },
3175
+ field_type:{ type: 'string', enum: ['text', 'number', 'url', 'image'], description: 'For type=acf: ACF field type picks the native tag (acf-text/acf-number/acf-url/acf-image) and host widget. Default "text".' },
3176
+ source: { type: 'string', enum: ['post', 'options'], description: 'For type=acf: "post" (default) resolves against the current/queried post; "options" resolves against the ACF options page → same value site-wide.' },
3177
+ field_key: { type: 'string', description: 'For type=acf + source=post: optional exact ACF field key (e.g. "field_abc123") for a precise binding (key encoded as "<field_key>:<field>"). Ignored when source=options.' },
3178
+ tag: { type: 'string', description: 'Override the dynamic tag name for non-acf types (advanced — e.g. a custom registered tag).' },
3176
3179
  settings: { type: 'object', description: 'Extra widget settings to merge (e.g. typography, header_size, link). additionalProperties.', additionalProperties: true },
3177
3180
  position: { type: 'number', description: 'Optional 0-based insert index. Omit to append.' },
3178
3181
  force: { type: 'boolean', description: 'Override edit locks.' }
@@ -3665,22 +3668,22 @@ function getToolDefinitions() {
3665
3668
  },
3666
3669
  {
3667
3670
  name: 'get_acf_fields',
3668
- description: 'Get all ACF field values for a specific post. Returns field names, labels, types, and current values. Supports all ACF field types including repeater and group fields.',
3671
+ description: 'Get all ACF field values for a specific post — OR for an ACF options page. Returns field names, labels, types, and current values. Supports all ACF field types including repeater and group fields. For a site-wide ACF options page, pass post_id="options" (or a custom options post_id). Use list_acf_option_pages to discover registered options pages.',
3669
3672
  inputSchema: {
3670
3673
  type: 'object',
3671
3674
  properties: {
3672
- post_id: { type: 'number', description: 'WordPress post ID to get ACF fields for.' }
3675
+ post_id: { type: ['number', 'string'], description: 'WordPress post ID (number) to read, OR the string "options" for the ACF options page (or a custom options post_id).' }
3673
3676
  },
3674
3677
  required: ['post_id']
3675
3678
  }
3676
3679
  },
3677
3680
  {
3678
3681
  name: 'set_acf_fields',
3679
- description: 'Set/update ACF field values on a post. Accepts a JSON object of field_name: value pairs. Always dry_run=true first to preview.',
3682
+ description: 'Set/update ACF field values on a post — OR on an ACF options page (site-wide). Accepts a JSON object of field_name: value pairs. Always dry_run=true first to preview. For a site-wide options page, pass post_id="options".',
3680
3683
  inputSchema: {
3681
3684
  type: 'object',
3682
3685
  properties: {
3683
- post_id: { type: 'number', description: 'WordPress post ID to update.' },
3686
+ post_id: { type: ['number', 'string'], description: 'WordPress post ID (number) to update, OR the string "options" for the ACF options page (or a custom options post_id).' },
3684
3687
  fields: { type: 'object', description: 'Object of field_name: value pairs. Example: {"hero_title": "New Title", "show_sidebar": true}' },
3685
3688
  dry_run: { type: 'boolean', description: 'Preview changes without saving (default: true). Set false to apply.' }
3686
3689
  },
@@ -3749,6 +3752,40 @@ function getToolDefinitions() {
3749
3752
  required: ['group_key']
3750
3753
  }
3751
3754
  },
3755
+ {
3756
+ name: 'list_acf_option_pages',
3757
+ description: 'List registered ACF options pages (ACF Pro). Returns each page title, menu_slug, and the post_id string to pass to get_acf_fields/set_acf_fields (default "options"). Use this to confirm a site-wide settings page exists before reading/writing options data. If none exist, create one with register_acf_option_page.',
3758
+ inputSchema: { type: 'object', properties: {} }
3759
+ },
3760
+ {
3761
+ name: 'register_acf_option_page',
3762
+ description: 'Register (and persist) an ACF options page so site-wide fields can be stored once and read on every page (ACF Pro). Idempotent by menu_slug. The page is persisted in a WP option and re-registered on every load via acf/init — so it survives. After creating, attach field groups to it with update_acf_field_group (options_page=<menu_slug>), then read/write values with post_id "options".',
3763
+ inputSchema: {
3764
+ type: 'object',
3765
+ properties: {
3766
+ page_title: { type: 'string', description: 'Display title, e.g. "Site Settings".' },
3767
+ menu_slug: { type: 'string', description: 'Optional admin menu slug. Defaults to a slug of page_title.' },
3768
+ parent_slug: { type: 'string', description: 'Optional parent menu slug to nest under (e.g. "options-general.php" or another options page slug).' }
3769
+ },
3770
+ required: ['page_title']
3771
+ }
3772
+ },
3773
+ {
3774
+ name: 'update_acf_field_group',
3775
+ description: 'Patch an EXISTING ACF field group\'s metadata (title / active / location) without deleting+recreating it — so field keys that Elementor dynamic tags reference are preserved. Use to attach an orphaned group to an options page (options_page=<menu_slug>) or to post types. Pass a full ACF `location` array for advanced rules, or use post_types[]/options_page convenience builders.',
3776
+ inputSchema: {
3777
+ type: 'object',
3778
+ properties: {
3779
+ group_key: { type: 'string', description: 'ACF field group key (e.g. "group_6a2315c28164f"). Use list_acf_field_groups to discover.' },
3780
+ title: { type: 'string', description: 'Optional new title.' },
3781
+ active: { type: 'boolean', description: 'Optional active flag.' },
3782
+ options_page: { type: 'string', description: 'Attach the group to this options-page menu_slug (builds an options_page location rule).' },
3783
+ post_types: { type: 'array', items: { type: 'string' }, description: 'Attach the group to these post types (builds post_type location rules).' },
3784
+ location: { type: 'array', description: 'Advanced: a full ACF location rule array (overrides post_types/options_page). Shape: [[{param,operator,value}]].' }
3785
+ },
3786
+ required: ['group_key']
3787
+ }
3788
+ },
3752
3789
  // ── Audits ──
3753
3790
  {
3754
3791
  name: 'audit_orphan_pages',
@@ -6035,17 +6072,60 @@ async function handleToolCall(name, args) {
6035
6072
  excerpt: { widgetType: 'text-editor', control: 'editor', tag: 'post-excerpt', base: { editor: '' } },
6036
6073
  content: { widgetType: 'text-editor', control: 'editor', tag: 'post-content', base: { editor: '' } },
6037
6074
  featured_image: { widgetType: 'image', control: 'image', tag: 'post-featured-image', base: { image: { url: '', id: '' } } },
6038
- acf: { widgetType: 'heading', control: 'title', tag: 'acf', base: { title: '', header_size: 'h2' } },
6039
6075
  };
6076
+
6077
+ // ── ACF: native Elementor Pro ACF dynamic tag ──────────────────────────
6078
+ // Elementor's ACF tags are typed: acf-text / acf-number / acf-image / etc.
6079
+ // (NOT a generic "acf" tag). The value resolves from settings.key, which
6080
+ // Elementor splits on ":" — `Dynamic_Value_Provider::get_value()`:
6081
+ // key = "options:<field_name>" → get_field_object(<field_name>, 'options') [site-wide]
6082
+ // key = "<field_key>:<field_name>" → resolves against the current/queried post
6083
+ // key = "<field_name>" (bare) → resolves by name against the queried post
6084
+ // So options-context binding is simply the "options:" prefix.
6085
+ if (args.type === 'acf') {
6086
+ if (!args.field) return ok('Failed: type=acf requires a `field` (the ACF field name).');
6087
+ const ACF_TAGS = {
6088
+ text: { tag: 'acf-text', widgetType: 'heading', control: 'title', base: { title: '', header_size: 'h2' } },
6089
+ number: { tag: 'acf-number', widgetType: 'heading', control: 'title', base: { title: '', header_size: 'h2' } },
6090
+ url: { tag: 'acf-url', widgetType: 'text-editor', control: 'editor', base: { editor: '' } },
6091
+ image: { tag: 'acf-image', widgetType: 'image', control: 'image', base: { image: { url: '', id: '' } } },
6092
+ };
6093
+ const ft = (args.field_type || 'text').toLowerCase();
6094
+ const a = ACF_TAGS[ft];
6095
+ if (!a) return ok(`Failed: unknown field_type "${ft}". Use one of: ${Object.keys(ACF_TAGS).join(', ')}.`);
6096
+
6097
+ const source = (args.source || 'post').toLowerCase();
6098
+ let key;
6099
+ if (source === 'options') key = `options:${args.field}`;
6100
+ else if (args.field_key) key = `${args.field_key}:${args.field}`;
6101
+ else key = args.field; // bare name → resolves against the queried post
6102
+
6103
+ const tagId = Array.from({ length: 7 }, () => 'abcdef0123456789'[Math.floor(Math.random() * 16)]).join('');
6104
+ const enc = encodeURIComponent(JSON.stringify({ key }));
6105
+ const dynamicValue = `[elementor-tag id="${tagId}" name="${a.tag}" settings="${enc}"]`;
6106
+
6107
+ const settings = Object.assign({}, a.base, args.settings || {}, {
6108
+ __dynamic__: Object.assign({}, (args.settings && args.settings.__dynamic__) || {}, { [a.control]: dynamicValue }),
6109
+ });
6110
+ const element = { elType: 'widget', widgetType: a.widgetType, settings };
6111
+
6112
+ const body = { parent_id: args.parent_id, element };
6113
+ if (args.position !== undefined) body.position = args.position;
6114
+ if (args.force) body.force = true;
6115
+ const r = await apiCall(`/pages/${args.page_id}/add-element`, 'POST', body);
6116
+ if (r.code || r.error) return ok(`Failed: ${r.message || r.error || 'Unknown error'}`);
6117
+ const ctx = source === 'options'
6118
+ ? 'It resolves against the ACF OPTIONS context, so it renders the same site-wide value on EVERY page.'
6119
+ : 'It renders the current/queried post\'s ACF value (loop/single context).';
6120
+ return ok(`Dynamic ACF field added!\nField: ${args.field} (${ft}) | source: ${source}\nTag: ${a.tag} → ${a.widgetType} widget | key: "${key}"\nElement ID: ${r.element_id} (parent ${r.parent_id})\n${ctx}`);
6121
+ }
6122
+
6040
6123
  const m = MAP[args.type];
6041
- if (!m) return ok(`Failed: unknown type "${args.type}". Use one of: ${Object.keys(MAP).join(', ')}.`);
6042
- if (args.type === 'acf' && !args.field) return ok('Failed: type=acf requires a `field` (the ACF field key/name).');
6124
+ if (!m) return ok(`Failed: unknown type "${args.type}". Use one of: ${Object.keys(MAP).join(', ')}, acf.`);
6043
6125
 
6044
6126
  // Elementor dynamic-tag shortcode: [elementor-tag id="<7-char>" name="<tag>" settings="<urlencoded-json>"]
6045
6127
  const tagId = Array.from({ length: 7 }, () => 'abcdef0123456789'[Math.floor(Math.random() * 16)]).join('');
6046
- const tagSettings = {};
6047
- if (args.type === 'acf') tagSettings.key = args.field;
6048
- const enc = encodeURIComponent(JSON.stringify(tagSettings));
6128
+ const enc = encodeURIComponent(JSON.stringify({}));
6049
6129
  const dynamicValue = `[elementor-tag id="${tagId}" name="${args.tag || m.tag}" settings="${enc}"]`;
6050
6130
 
6051
6131
  const settings = Object.assign({}, m.base, args.settings || {}, {
@@ -6058,7 +6138,7 @@ async function handleToolCall(name, args) {
6058
6138
  if (args.force) body.force = true;
6059
6139
  const r = await apiCall(`/pages/${args.page_id}/add-element`, 'POST', body);
6060
6140
  if (r.code || r.error) return ok(`Failed: ${r.message || r.error || 'Unknown error'}`);
6061
- return ok(`Dynamic field added!\nType: ${args.type} → ${m.widgetType} widget bound to "${args.tag || m.tag}"${args.field ? ` (field: ${args.field})` : ''}\nElement ID: ${r.element_id} (parent ${r.parent_id})\nIt renders the current post's data in loop/single context.`);
6141
+ return ok(`Dynamic field added!\nType: ${args.type} → ${m.widgetType} widget bound to "${args.tag || m.tag}"\nElement ID: ${r.element_id} (parent ${r.parent_id})\nIt renders the current post's data in loop/single context.`);
6062
6142
  }
6063
6143
 
6064
6144
  case 'get_element': {
@@ -7130,6 +7210,8 @@ async function handleToolCall(name, args) {
7130
7210
  r.field_groups.forEach(g => {
7131
7211
  out += `\n[${g.key}] ${g.title} ${g.active ? '' : '(INACTIVE)'}\n`;
7132
7212
  if (g.post_types?.length) out += ` Post types: ${g.post_types.join(', ')}\n`;
7213
+ if (g.option_pages?.length) out += ` Options pages: ${g.option_pages.join(', ')}\n`;
7214
+ if (!g.post_types?.length && !g.option_pages?.length) out += ` Location: (none — orphaned)\n`;
7133
7215
  if (g.fields?.length) {
7134
7216
  g.fields.forEach(f => {
7135
7217
  out += ` • ${f.name} (${f.type}) — "${f.label}"${f.required ? ' *required' : ''}\n`;
@@ -7146,9 +7228,9 @@ async function handleToolCall(name, args) {
7146
7228
  }
7147
7229
 
7148
7230
  case 'get_acf_fields': {
7149
- const r = await apiCall(`/acf-fields?post_id=${args.post_id}`);
7231
+ const r = await apiCall(`/acf-fields?post_id=${encodeURIComponent(args.post_id)}`);
7150
7232
  if (r.code || r.error) return ok(`Failed: ${r.message || r.error}`);
7151
- let out = `=== ACF FIELDS: ${r.post_title} (ID:${r.post_id}) ===\n`;
7233
+ let out = `=== ACF FIELDS: ${r.post_title} (${r.is_options ? 'options' : 'ID:' + r.post_id}) ===\n`;
7152
7234
  if (!r.fields?.length) {
7153
7235
  out += 'No ACF fields found for this post.\n';
7154
7236
  } else {
@@ -7246,6 +7328,52 @@ async function handleToolCall(name, args) {
7246
7328
  return ok(msg);
7247
7329
  }
7248
7330
 
7331
+ case 'list_acf_option_pages': {
7332
+ const r = await apiCall('/acf-option-pages');
7333
+ if (r.code || r.error) return ok(`Failed: ${r.message || r.error}`);
7334
+ let out = `=== ACF OPTIONS PAGES (${r.count}) ===\n`;
7335
+ if (!r.option_pages?.length) {
7336
+ out += 'No ACF options pages registered.\n';
7337
+ } else {
7338
+ r.option_pages.forEach(p => {
7339
+ out += `\n• ${p.page_title}\n menu_slug: ${p.menu_slug}\n post_id: ${p.post_id}\n`;
7340
+ if (p.parent_slug) out += ` parent: ${p.parent_slug}\n`;
7341
+ });
7342
+ }
7343
+ if (r.note) out += `\n${r.note}\n`;
7344
+ return ok(out);
7345
+ }
7346
+
7347
+ case 'register_acf_option_page': {
7348
+ const body = { page_title: args.page_title };
7349
+ if (args.menu_slug) body.menu_slug = args.menu_slug;
7350
+ if (args.parent_slug) body.parent_slug = args.parent_slug;
7351
+ const r = await apiCall('/acf-option-pages', 'POST', body);
7352
+ if (r.code || r.error) return ok(`Failed: ${r.message || r.error}`);
7353
+ let out = r.created ? `=== ACF OPTIONS PAGE REGISTERED ===\n` : `=== ACF OPTIONS PAGE (already existed) ===\n`;
7354
+ out += `Title: ${r.page_title}\nmenu_slug: ${r.menu_slug}\npost_id: ${r.post_id}\n`;
7355
+ if (r.note) out += `\n${r.note}\n`;
7356
+ return ok(out);
7357
+ }
7358
+
7359
+ case 'update_acf_field_group': {
7360
+ const body = { group_key: args.group_key };
7361
+ if (args.title !== undefined) body.title = args.title;
7362
+ if (args.active !== undefined) body.active = args.active;
7363
+ if (args.options_page !== undefined) body.options_page = args.options_page;
7364
+ if (args.post_types !== undefined) body.post_types = args.post_types;
7365
+ if (args.location !== undefined) body.location = args.location;
7366
+ const r = await apiCall('/acf-field-group', 'PATCH', body);
7367
+ if (r.code || r.error) return ok(`Failed: ${r.message || r.error}`);
7368
+ let out = `=== ACF FIELD GROUP UPDATED ===\n`;
7369
+ out += `[${r.key}] ${r.title}${r.active ? '' : ' (INACTIVE)'}\n`;
7370
+ out += `Changed: ${(r.changed || []).join(', ')}\n`;
7371
+ if (r.post_types?.length) out += `Post types: ${r.post_types.join(', ')}\n`;
7372
+ if (r.option_pages?.length) out += `Options pages: ${r.option_pages.join(', ')}\n`;
7373
+ if (r.note) out += `\n${r.note}\n`;
7374
+ return ok(out);
7375
+ }
7376
+
7249
7377
  case 'audit_orphan_pages': {
7250
7378
  const r = await apiCall('/audit-orphan-pages');
7251
7379
  let msg = `=== ORPHAN PAGES ===\nTotal pages: ${r.total_pages} | Linked: ${r.linked_pages} | Orphans: ${r.orphan_count}\n`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noleemits/vision-builder-control-mcp",
3
- "version": "4.119.0",
3
+ "version": "4.120.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",