@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.
- package/index.js +146 -18
- 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.
|
|
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→
|
|
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
|
|
3174
|
-
field: { type: 'string', description: 'For type=acf: the ACF field
|
|
3175
|
-
|
|
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
|
|
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
|
|
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}"
|
|
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} (
|
|
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.
|
|
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",
|