@noleemits/vision-builder-control-mcp 4.39.0 → 4.42.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 +168 -4
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Noleemits Vision Builder Control MCP Server
|
|
4
4
|
*
|
|
5
|
-
* Provides
|
|
5
|
+
* Provides 69 tools for building and managing WordPress/Elementor sites.
|
|
6
|
+
* v4.42.0: audit_text + replace_text — unified search/replace across Elementor + post columns with literal/regex/fuzzy modes. Fuzzy mode tolerates NBSP, curly quotes/dashes, ideographic punctuation — fixes the "copy-pasted needle won't match" bug.
|
|
6
7
|
* v4.35.0: PixelVault image integration — pixelvault_status, find_images, generate_image, get_batch_status, insert_image tools; proxy REST endpoints at /nvbc/v1/pixelvault/*.
|
|
7
8
|
* v4.34.0: Design tokens auto-inject as CSS custom properties (--nvbc-color-*, --nvbc-fs-*, --nvbc-space-*) site-wide via wp_head; kit global CSS tools (get_kit_global_css, set_kit_global_css); get_kit_settings formatter handles gap-style objects.
|
|
8
9
|
* v4.33.0: Elementor kit layout settings (get_kit_settings, set_kit_settings) — read/write container_width, viewports, gutter.
|
|
@@ -107,7 +108,7 @@ process.on('SIGINT', () => {
|
|
|
107
108
|
// CONFIG
|
|
108
109
|
// ================================================================
|
|
109
110
|
|
|
110
|
-
const VERSION = '4.
|
|
111
|
+
const VERSION = '4.42.0';
|
|
111
112
|
const MIN_PLUGIN_VERSION = '4.13.0'; // Minimum WP plugin version required by this MCP server
|
|
112
113
|
|
|
113
114
|
// ================================================================
|
|
@@ -2632,9 +2633,20 @@ function getToolDefinitions() {
|
|
|
2632
2633
|
required: ['name']
|
|
2633
2634
|
}
|
|
2634
2635
|
},
|
|
2636
|
+
{
|
|
2637
|
+
name: 'list_menu_items',
|
|
2638
|
+
description: 'List all items in a nav menu, in order. Returns each item\'s id, title, url, type, object (page/post/category/etc.), object_id, parent (parent item ID, 0 for top-level), order, target, classes (CSS class array), and description. Use before set_menu_items so you can patch one entry without losing the others.',
|
|
2639
|
+
inputSchema: {
|
|
2640
|
+
type: 'object',
|
|
2641
|
+
properties: {
|
|
2642
|
+
menu_id: { type: 'number', description: 'Menu ID from list_nav_menus' }
|
|
2643
|
+
},
|
|
2644
|
+
required: ['menu_id']
|
|
2645
|
+
}
|
|
2646
|
+
},
|
|
2635
2647
|
{
|
|
2636
2648
|
name: 'set_menu_items',
|
|
2637
|
-
description: 'Bulk-replace all items in a nav menu. Each item: {title, url (for type=custom), type ("custom"|"post_type"|"taxonomy"), object ("page"|"post"|"category"), object_id, parent_index (for nesting — 0-based index of parent in this same array), target ("_blank")}. Items are inserted in order; nested items must come AFTER their parent in the array.',
|
|
2649
|
+
description: 'Bulk-replace all items in a nav menu. Each item: {title, url (for type=custom), type ("custom"|"post_type"|"taxonomy"), object ("page"|"post"|"category"), object_id, parent_index (for nesting — 0-based index of parent in this same array), target ("_blank"), classes (CSS classes — string or array, applied as <li class="...">), description (free text shown by some menu widgets)}. Items are inserted in order; nested items must come AFTER their parent in the array.',
|
|
2638
2650
|
inputSchema: {
|
|
2639
2651
|
type: 'object',
|
|
2640
2652
|
properties: {
|
|
@@ -2651,7 +2663,9 @@ function getToolDefinitions() {
|
|
|
2651
2663
|
object: { type: 'string', description: 'e.g. "page", "post", "category" — required when type !== "custom"' },
|
|
2652
2664
|
object_id: { type: 'number', description: 'WP object ID — required when type !== "custom"' },
|
|
2653
2665
|
parent_index: { type: 'number', description: 'Index in this array of the parent item (for nesting). Omit for top-level.' },
|
|
2654
|
-
target: { type: 'string', enum: ['_blank', ''], description: 'Optional. "_blank" opens in new tab.' }
|
|
2666
|
+
target: { type: 'string', enum: ['_blank', ''], description: 'Optional. "_blank" opens in new tab.' },
|
|
2667
|
+
classes: { description: 'CSS classes for the <li>. Pass a string ("mega-menu primary") or an array (["mega-menu", "primary"]). Useful for targeting specific items in CSS without relying on URL substrings.' },
|
|
2668
|
+
description: { type: 'string', description: 'Optional description text. Some menu walkers render it; otherwise stored as data-description.' }
|
|
2655
2669
|
}
|
|
2656
2670
|
}
|
|
2657
2671
|
}
|
|
@@ -3054,6 +3068,42 @@ function getToolDefinitions() {
|
|
|
3054
3068
|
required: ['search', 'replace']
|
|
3055
3069
|
}
|
|
3056
3070
|
},
|
|
3071
|
+
{
|
|
3072
|
+
name: 'audit_text',
|
|
3073
|
+
description: 'Unified text search across Elementor (full settings tree, every string value), post_content, post_title, and post_excerpt. The fuzzy mode tolerates unicode whitespace and punctuation variants — use it when a literal copy-pasted string mysteriously fails to match (NBSP between words, curly quotes/dashes/periods inserted by builders, etc.). Read-only.',
|
|
3074
|
+
inputSchema: {
|
|
3075
|
+
type: 'object',
|
|
3076
|
+
properties: {
|
|
3077
|
+
search: { type: 'string', description: 'Text or pattern to find.' },
|
|
3078
|
+
mode: { type: 'string', enum: ['literal', 'regex', 'fuzzy'], description: '"literal" (exact bytes), "regex" (PHP regex without delimiters, u flag forced), or "fuzzy" (tolerates NBSP, curly quotes/dashes/periods, ideographic whitespace). Default: literal.' },
|
|
3079
|
+
case_insensitive: { type: 'boolean', description: 'Ignore case. Default: false.' },
|
|
3080
|
+
stores: { type: 'array', items: { type: 'string', enum: ['elementor', 'post_content', 'post_title', 'post_excerpt'] }, description: 'Which stores to scan. Default: all four. Pass [\"elementor\"] to limit scope.' },
|
|
3081
|
+
post_type: { type: 'string', description: '"post", "page", "any", or a specific CPT slug. Default: any.' },
|
|
3082
|
+
include_drafts: { type: 'boolean', description: 'Include draft/private/pending posts. Default: true.' },
|
|
3083
|
+
limit: { type: 'number', description: 'Max posts to scan per store (default 1000, max 5000).' }
|
|
3084
|
+
},
|
|
3085
|
+
required: ['search']
|
|
3086
|
+
}
|
|
3087
|
+
},
|
|
3088
|
+
{
|
|
3089
|
+
name: 'replace_text',
|
|
3090
|
+
description: 'Unified bulk find/replace across Elementor (entire settings tree), post_content, post_title, and post_excerpt. Same matching modes as audit_text — fuzzy mode is the fix for the "copy-pasted needle won\'t match" class of bug. ALWAYS dry_run=true first. Never changes post_modified date.',
|
|
3091
|
+
inputSchema: {
|
|
3092
|
+
type: 'object',
|
|
3093
|
+
properties: {
|
|
3094
|
+
search: { type: 'string', description: 'Text or pattern to find.' },
|
|
3095
|
+
replace: { type: 'string', description: 'Replacement text. For regex/fuzzy modes, $1 etc. work as backreferences in regex mode only.' },
|
|
3096
|
+
mode: { type: 'string', enum: ['literal', 'regex', 'fuzzy'], description: '"literal", "regex", or "fuzzy". Default: literal.' },
|
|
3097
|
+
case_insensitive: { type: 'boolean', description: 'Ignore case. Default: false.' },
|
|
3098
|
+
stores: { type: 'array', items: { type: 'string', enum: ['elementor', 'post_content', 'post_title', 'post_excerpt'] }, description: 'Stores to operate on. Default: all four.' },
|
|
3099
|
+
post_type: { type: 'string', description: '"post", "page", "any", CPT slug. Default: any.' },
|
|
3100
|
+
include_drafts: { type: 'boolean', description: 'Include draft/private/pending. Default: true.' },
|
|
3101
|
+
limit: { type: 'number', description: 'Max posts per store. Default 1000, max 5000.' },
|
|
3102
|
+
dry_run: { type: 'boolean', description: 'Preview without saving. Default: true (SAFE).' }
|
|
3103
|
+
},
|
|
3104
|
+
required: ['search', 'replace']
|
|
3105
|
+
}
|
|
3106
|
+
},
|
|
3057
3107
|
{
|
|
3058
3108
|
name: 'set_faq_schema',
|
|
3059
3109
|
description: 'Set FAQPage JSON-LD schema on a single post. Stores in post meta and auto-injects into page head. Use for adding FAQ rich snippets to posts with Q&A content.',
|
|
@@ -4729,6 +4779,25 @@ async function handleToolCall(name, args) {
|
|
|
4729
4779
|
: `Menu already existed: "${r.name}" (ID ${r.id})`);
|
|
4730
4780
|
}
|
|
4731
4781
|
|
|
4782
|
+
case 'list_menu_items': {
|
|
4783
|
+
const r = await apiCall(`/menus/${args.menu_id}/items`);
|
|
4784
|
+
if (!r.items || !r.items.length) return ok(`Menu ${args.menu_id} has no items.`);
|
|
4785
|
+
const byParent = {};
|
|
4786
|
+
r.items.forEach(it => { (byParent[it.parent || 0] ||= []).push(it); });
|
|
4787
|
+
const lines = [];
|
|
4788
|
+
const renderItem = (it, depth) => {
|
|
4789
|
+
const indent = ' '.repeat(depth);
|
|
4790
|
+
const target = it.type === 'custom'
|
|
4791
|
+
? it.url
|
|
4792
|
+
: `${it.type}/${it.object}#${it.object_id}`;
|
|
4793
|
+
const cls = Array.isArray(it.classes) && it.classes.length ? ` .${it.classes.join('.')}` : '';
|
|
4794
|
+
lines.push(`${indent}[${it.id}] ${it.title} → ${target}${cls}`);
|
|
4795
|
+
(byParent[it.id] || []).forEach(c => renderItem(c, depth + 1));
|
|
4796
|
+
};
|
|
4797
|
+
(byParent[0] || []).forEach(it => renderItem(it, 0));
|
|
4798
|
+
return ok(`Menu ${r.menu_id} items (${r.items.length}):\n${lines.join('\n')}`);
|
|
4799
|
+
}
|
|
4800
|
+
|
|
4732
4801
|
case 'set_menu_items': {
|
|
4733
4802
|
const r = await apiCall(`/menus/${args.menu_id}/items`, 'POST', { items: args.items });
|
|
4734
4803
|
const errLines = (r.errors || []).map(e => ` • #${e.index}: ${e.message}`);
|
|
@@ -5293,6 +5362,101 @@ async function handleToolCall(name, args) {
|
|
|
5293
5362
|
return ok(out);
|
|
5294
5363
|
}
|
|
5295
5364
|
|
|
5365
|
+
case 'audit_text': {
|
|
5366
|
+
const body = { search: stripCDATA(args.search) };
|
|
5367
|
+
if (args.mode) body.mode = args.mode;
|
|
5368
|
+
if (args.case_insensitive) body.case_insensitive = parseBool(args.case_insensitive, false);
|
|
5369
|
+
if (args.stores) body.stores = args.stores;
|
|
5370
|
+
if (args.post_type) body.post_type = args.post_type;
|
|
5371
|
+
if (args.include_drafts !== undefined) body.include_drafts = parseBool(args.include_drafts, true);
|
|
5372
|
+
if (args.limit) body.limit = args.limit;
|
|
5373
|
+
const r = await apiCall('/audit-text', 'POST', body);
|
|
5374
|
+
if (r.code || r.error) return ok(`Failed: ${r.message || r.error || 'Unknown error'}`);
|
|
5375
|
+
const s = r.summary || { total_matches: 0, posts_affected: 0, by_store: {} };
|
|
5376
|
+
let out = `=== AUDIT TEXT (${r.mode || 'literal'}${r.case_insensitive ? ', ci' : ''}) ===\n`;
|
|
5377
|
+
out += `Search: "${r.search}"\n`;
|
|
5378
|
+
out += `Stores: ${(r.stores || []).join(', ')}\n`;
|
|
5379
|
+
out += `Total matches: ${s.total_matches} | Posts affected: ${s.posts_affected}\n`;
|
|
5380
|
+
if (s.by_store && Object.keys(s.by_store).length) {
|
|
5381
|
+
out += `By store: ${Object.entries(s.by_store).map(([k,v]) => `${k}=${v}`).join(', ')}\n`;
|
|
5382
|
+
}
|
|
5383
|
+
if (!r.matches?.length) {
|
|
5384
|
+
out += '\nNo matches found.';
|
|
5385
|
+
if (r.mode !== 'fuzzy') out += '\n💡 Try mode="fuzzy" to tolerate NBSP / curly punctuation.';
|
|
5386
|
+
return ok(out);
|
|
5387
|
+
}
|
|
5388
|
+
// Group by post for readability
|
|
5389
|
+
const groups = {};
|
|
5390
|
+
r.matches.forEach(m => {
|
|
5391
|
+
const key = `${m.post_id}|${m.post_title || ''}`;
|
|
5392
|
+
(groups[key] = groups[key] || []).push(m);
|
|
5393
|
+
});
|
|
5394
|
+
Object.entries(groups).slice(0, 50).forEach(([key, ms]) => {
|
|
5395
|
+
const [pid, title] = key.split('|');
|
|
5396
|
+
out += `\n--- [${pid}] ${title} — ${ms.length} match(es) ---\n`;
|
|
5397
|
+
ms.slice(0, 10).forEach(m => {
|
|
5398
|
+
if (m.store === 'elementor') {
|
|
5399
|
+
out += ` [elementor.${m.widget || '?'}] ${m.element_id}.${m.field || ''}\n`;
|
|
5400
|
+
} else {
|
|
5401
|
+
out += ` [${m.store}] ${m.field || ''}\n`;
|
|
5402
|
+
}
|
|
5403
|
+
out += ` raw: "${(m.raw_match || '').slice(0, 80).replace(/\n/g, ' ')}"\n`;
|
|
5404
|
+
out += ` ctx: ${(m.context || '').slice(0, 160)}\n`;
|
|
5405
|
+
});
|
|
5406
|
+
if (ms.length > 10) out += ` … and ${ms.length - 10} more\n`;
|
|
5407
|
+
});
|
|
5408
|
+
if (Object.keys(groups).length > 50) out += `\n…and ${Object.keys(groups).length - 50} more posts (truncated)`;
|
|
5409
|
+
return ok(out);
|
|
5410
|
+
}
|
|
5411
|
+
|
|
5412
|
+
case 'replace_text': {
|
|
5413
|
+
const body = {
|
|
5414
|
+
search: stripCDATA(args.search),
|
|
5415
|
+
replace: stripCDATA(args.replace),
|
|
5416
|
+
dry_run: parseBool(args.dry_run, true),
|
|
5417
|
+
};
|
|
5418
|
+
if (args.mode) body.mode = args.mode;
|
|
5419
|
+
if (args.case_insensitive) body.case_insensitive = parseBool(args.case_insensitive, false);
|
|
5420
|
+
if (args.stores) body.stores = args.stores;
|
|
5421
|
+
if (args.post_type) body.post_type = args.post_type;
|
|
5422
|
+
if (args.include_drafts !== undefined) body.include_drafts = parseBool(args.include_drafts, true);
|
|
5423
|
+
if (args.limit) body.limit = args.limit;
|
|
5424
|
+
const r = await apiCall('/replace-text', 'POST', body);
|
|
5425
|
+
if (r.code || r.error) return ok(`Failed: ${r.message || r.error || 'Unknown error'}`);
|
|
5426
|
+
const s = r.summary || { total_matches: 0, posts_affected: 0, by_store: {} };
|
|
5427
|
+
const tag = r.dry_run ? 'DRY RUN' : 'APPLIED';
|
|
5428
|
+
let out = `=== REPLACE TEXT (${tag} · ${r.mode || 'literal'}${r.case_insensitive ? ', ci' : ''}) ===\n`;
|
|
5429
|
+
out += `Search: "${r.search}"\nReplace: "${r.replace}"\n`;
|
|
5430
|
+
out += `Stores: ${(r.stores || []).join(', ')}\n`;
|
|
5431
|
+
out += `Total matches: ${s.total_matches} | Posts affected: ${s.posts_affected}\n`;
|
|
5432
|
+
if (s.by_store && Object.keys(s.by_store).length) {
|
|
5433
|
+
out += `By store: ${Object.entries(s.by_store).map(([k,v]) => `${k}=${v}`).join(', ')}\n`;
|
|
5434
|
+
}
|
|
5435
|
+
if (!r.matches?.length) {
|
|
5436
|
+
out += '\nNo matches found.';
|
|
5437
|
+
if (r.mode !== 'fuzzy') out += '\n💡 Try mode="fuzzy" to tolerate NBSP / curly punctuation.';
|
|
5438
|
+
return ok(out);
|
|
5439
|
+
}
|
|
5440
|
+
const groups = {};
|
|
5441
|
+
r.matches.forEach(m => {
|
|
5442
|
+
const key = `${m.post_id}|${m.post_title || ''}`;
|
|
5443
|
+
(groups[key] = groups[key] || []).push(m);
|
|
5444
|
+
});
|
|
5445
|
+
Object.entries(groups).slice(0, 50).forEach(([key, ms]) => {
|
|
5446
|
+
const [pid, title] = key.split('|');
|
|
5447
|
+
out += `\n--- [${pid}] ${title} — ${ms.length} change(s) ---\n`;
|
|
5448
|
+
ms.slice(0, 8).forEach(m => {
|
|
5449
|
+
const loc = m.store === 'elementor'
|
|
5450
|
+
? `elementor.${m.widget || '?'} ${m.element_id}.${m.field || ''}`
|
|
5451
|
+
: `${m.store}`;
|
|
5452
|
+
out += ` [${loc}] "${(m.raw_match || '').slice(0, 60).replace(/\n/g, ' ')}"\n`;
|
|
5453
|
+
});
|
|
5454
|
+
if (ms.length > 8) out += ` … and ${ms.length - 8} more\n`;
|
|
5455
|
+
});
|
|
5456
|
+
if (r.dry_run) out += `\n(Dry run — no changes saved. Set dry_run=false to apply.)`;
|
|
5457
|
+
return ok(out);
|
|
5458
|
+
}
|
|
5459
|
+
|
|
5296
5460
|
case 'set_faq_schema': {
|
|
5297
5461
|
const body = { post_id: args.post_id, faqs: args.faqs };
|
|
5298
5462
|
if (args.dry_run !== undefined) body.dry_run = args.dry_run;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noleemits/vision-builder-control-mcp",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.42.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",
|