@noleemits/vision-builder-control-mcp 4.5.11 → 4.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.
- package/index.js +1096 -57
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -2,7 +2,17 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Noleemits Vision Builder Control MCP Server
|
|
4
4
|
*
|
|
5
|
-
* Provides
|
|
5
|
+
* Provides 65 tools for building and managing WordPress/Elementor sites.
|
|
6
|
+
* v4.15.0: Injection engine v2 — icon-list injection, unfilled slot clearing, plus_tooltip boilerplate fix.
|
|
7
|
+
* v4.14.0: Pipeline quality fixes — smart counter parsing, boilerplate post-processing, quality scores in browse, injection completeness.
|
|
8
|
+
* v4.13.0: Phase 3 DX — display_conditions, null filter fix, garbage HTML cleanup, icon key docs, auto-inject convenience docs.
|
|
9
|
+
* v4.12.0: Phase 2 tools — move_element, clean_globals, find_replace URL search, set_front_page.
|
|
10
|
+
* v4.11.0: Phase 1 fixes — token patch semantics, trigger key auto-injection (background/typography), enhanced cache clear, flex-grow/grid convenience.
|
|
11
|
+
* v4.10.0: ACF tools + Elementor global styles (get_global_styles, set_global_styles) — read/write kit colors & fonts.
|
|
12
|
+
* v4.9.2: audit_broken_images tool (scan all content for broken image URLs).
|
|
13
|
+
* v4.9.1: search_post_content, replace_post_content tools + extract_urls + regex support.
|
|
14
|
+
* v4.9.0: audit_db_health, audit_image_sizes, fix_image_sizes tools.
|
|
15
|
+
* v4.8.0: get_section_json, update_children, check_page_lock tools + element_path + edit locking + 10-mode Section Inspector.
|
|
6
16
|
* v4.3.0: Add 4 nvbc_* shortcodes (pill badges, category list, excerpt, category cards).
|
|
7
17
|
* v4.2.1: Fix audit_clickable scanner (WP_Post object vs ID bug).
|
|
8
18
|
* - Design tokens (get, set, refresh)
|
|
@@ -30,7 +40,7 @@
|
|
|
30
40
|
*
|
|
31
41
|
* WordPress plugin: Noleemits Vision Builder Control (nvbc/v1 REST endpoints)
|
|
32
42
|
*
|
|
33
|
-
* Version: 4.
|
|
43
|
+
* Version: 4.11.0
|
|
34
44
|
*/
|
|
35
45
|
|
|
36
46
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
@@ -94,8 +104,8 @@ process.on('SIGINT', () => {
|
|
|
94
104
|
// CONFIG
|
|
95
105
|
// ================================================================
|
|
96
106
|
|
|
97
|
-
const VERSION = '4.
|
|
98
|
-
const MIN_PLUGIN_VERSION = '4.
|
|
107
|
+
const VERSION = '4.15.0';
|
|
108
|
+
const MIN_PLUGIN_VERSION = '4.13.0'; // Minimum WP plugin version required by this MCP server
|
|
99
109
|
|
|
100
110
|
// ================================================================
|
|
101
111
|
// PARAMETER HELPERS
|
|
@@ -157,12 +167,38 @@ const __dirname_tl = dirname(__filename_tl);
|
|
|
157
167
|
const TEMPLATE_LIBRARY_DIR = process.env.TEMPLATE_LIBRARY_DIR || '';
|
|
158
168
|
let _templateCatalog = null;
|
|
159
169
|
|
|
170
|
+
let _qualityScores = null;
|
|
160
171
|
function getTemplateCatalog() {
|
|
161
172
|
if (_templateCatalog) return _templateCatalog;
|
|
162
173
|
const catalogPath = join(TEMPLATE_LIBRARY_DIR, 'section-catalog.json');
|
|
163
174
|
if (!TEMPLATE_LIBRARY_DIR || !existsSync(catalogPath)) return null;
|
|
164
175
|
try {
|
|
165
176
|
_templateCatalog = JSON.parse(readFileSync(catalogPath, 'utf8'));
|
|
177
|
+
// Merge quality scores from scored-sections.json if available
|
|
178
|
+
const scoresPath = join(TEMPLATE_LIBRARY_DIR, 'scored-sections.json');
|
|
179
|
+
if (existsSync(scoresPath) && !_qualityScores) {
|
|
180
|
+
try {
|
|
181
|
+
const scored = JSON.parse(readFileSync(scoresPath, 'utf8'));
|
|
182
|
+
_qualityScores = {};
|
|
183
|
+
for (const s of (scored.sections || [])) {
|
|
184
|
+
_qualityScores[s.id] = { quality_score: s.quality_score, flags: s.flags || [] };
|
|
185
|
+
}
|
|
186
|
+
// Enrich catalog sections with quality data
|
|
187
|
+
for (const s of _templateCatalog.sections) {
|
|
188
|
+
const q = _qualityScores[s.id];
|
|
189
|
+
if (q) {
|
|
190
|
+
s.quality_score = q.quality_score;
|
|
191
|
+
s.quality_flags = q.flags;
|
|
192
|
+
} else {
|
|
193
|
+
s.quality_score = 0;
|
|
194
|
+
s.quality_flags = ['unscored'];
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
console.error(`[TEMPLATE] Quality scores loaded: ${Object.keys(_qualityScores).length} sections scored`);
|
|
198
|
+
} catch (e) {
|
|
199
|
+
console.error(`[TEMPLATE] Failed to load quality scores: ${e.message}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
166
202
|
return _templateCatalog;
|
|
167
203
|
} catch (e) {
|
|
168
204
|
console.error(`[TEMPLATE] Failed to load catalog: ${e.message}`);
|
|
@@ -936,6 +972,8 @@ function extractContentSlots(element, path = []) {
|
|
|
936
972
|
slots.push({ widget: element, path: [...path], type: 'accordion', items: s.tabs || s.items || [] });
|
|
937
973
|
} else if (wt === 'image-box') {
|
|
938
974
|
slots.push({ widget: element, path: [...path], type: 'image-box', title: s.title_text || '', description: s.description_text || '' });
|
|
975
|
+
} else if (wt === 'icon-list') {
|
|
976
|
+
slots.push({ widget: element, path: [...path], type: 'icon-list', items: s.icon_list || [] });
|
|
939
977
|
} else if (wt === 'icon') {
|
|
940
978
|
slots.push({ widget: element, path: [...path], type: 'icon', icon: s.selected_icon || {} });
|
|
941
979
|
}
|
|
@@ -1088,6 +1126,37 @@ function structureContentBlock(block) {
|
|
|
1088
1126
|
* Inject structured content into a section's widgets.
|
|
1089
1127
|
* Deep clones the section first to avoid mutation.
|
|
1090
1128
|
*/
|
|
1129
|
+
/**
|
|
1130
|
+
* Add site-specific boilerplate fields to a template section.
|
|
1131
|
+
* - jet_parallax_layout_list: [] on all containers
|
|
1132
|
+
* - plus_tooltip_content_desc on all widgets
|
|
1133
|
+
* - layout: full_width on root container
|
|
1134
|
+
*/
|
|
1135
|
+
function addBoilerplate(section, isRoot = true) {
|
|
1136
|
+
if (!section) return section;
|
|
1137
|
+
if (isRoot && section.elType === 'container') {
|
|
1138
|
+
if (!section.settings) section.settings = {};
|
|
1139
|
+
section.settings.layout = 'full_width';
|
|
1140
|
+
}
|
|
1141
|
+
if (section.elType === 'container') {
|
|
1142
|
+
if (!section.settings) section.settings = {};
|
|
1143
|
+
if (!section.settings.jet_parallax_layout_list) {
|
|
1144
|
+
section.settings.jet_parallax_layout_list = [];
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
if (section.widgetType) {
|
|
1148
|
+
if (!section.settings) section.settings = {};
|
|
1149
|
+
if (!section.settings.plus_tooltip_content_desc) {
|
|
1150
|
+
// PHP validator uses empty() which treats '' as empty — needs non-empty value
|
|
1151
|
+
section.settings.plus_tooltip_content_desc = 'Luctus nec ullamcorper mattis';
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
if (Array.isArray(section.elements)) {
|
|
1155
|
+
section.elements.forEach(child => addBoilerplate(child, false));
|
|
1156
|
+
}
|
|
1157
|
+
return section;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1091
1160
|
function injectContent(section, content) {
|
|
1092
1161
|
const injected = JSON.parse(JSON.stringify(section));
|
|
1093
1162
|
const slots = extractContentSlots(injected);
|
|
@@ -1169,37 +1238,100 @@ function injectContent(section, content) {
|
|
|
1169
1238
|
changes++;
|
|
1170
1239
|
}
|
|
1171
1240
|
|
|
1172
|
-
//
|
|
1241
|
+
// Fill image-boxes with items that overflow from icon-boxes (or all items if no icon-boxes)
|
|
1242
|
+
const imageBoxStartIdx = Math.min(iconBoxes.length, items.length);
|
|
1173
1243
|
for (let i = 0; i < imageBoxes.length; i++) {
|
|
1174
|
-
const itemIdx =
|
|
1244
|
+
const itemIdx = imageBoxStartIdx + i;
|
|
1175
1245
|
if (itemIdx < items.length) {
|
|
1176
1246
|
imageBoxes[i].widget.settings.title_text = items[itemIdx].title;
|
|
1177
1247
|
if (items[itemIdx].description) {
|
|
1178
1248
|
imageBoxes[i].widget.settings.description_text = items[itemIdx].description;
|
|
1179
1249
|
}
|
|
1180
1250
|
changes++;
|
|
1251
|
+
} else if (i < items.length && iconBoxes.length === 0) {
|
|
1252
|
+
// No icon-boxes at all — image-boxes get items from the start
|
|
1253
|
+
imageBoxes[i].widget.settings.title_text = items[i].title;
|
|
1254
|
+
if (items[i].description) {
|
|
1255
|
+
imageBoxes[i].widget.settings.description_text = items[i].description;
|
|
1256
|
+
}
|
|
1257
|
+
changes++;
|
|
1181
1258
|
}
|
|
1182
1259
|
}
|
|
1183
1260
|
|
|
1184
|
-
// Inject
|
|
1261
|
+
// Inject icon-list widgets with items (bullet lists in about, pricing features, etc.)
|
|
1262
|
+
const iconLists = slots.filter(s => s.type === 'icon-list');
|
|
1263
|
+
// Items for icon-lists come from content.items that overflow past icon-boxes + image-boxes,
|
|
1264
|
+
// OR from content.items directly if there are no icon-boxes/image-boxes
|
|
1265
|
+
const iconListItemStart = iconBoxes.length + imageBoxes.length;
|
|
1266
|
+
const iconListItems = items.length > iconListItemStart ? items.slice(iconListItemStart) : (iconBoxes.length === 0 && imageBoxes.length === 0 ? items : []);
|
|
1267
|
+
let iconListItemIdx = 0;
|
|
1268
|
+
for (const il of iconLists) {
|
|
1269
|
+
const existing = il.widget.settings.icon_list || [];
|
|
1270
|
+
if (existing.length === 0) continue;
|
|
1271
|
+
const template = JSON.parse(JSON.stringify(existing[0]));
|
|
1272
|
+
const remaining = iconListItems.slice(iconListItemIdx);
|
|
1273
|
+
if (remaining.length === 0) continue;
|
|
1274
|
+
// Build new items array: one per content item, up to capacity
|
|
1275
|
+
const newItems = [];
|
|
1276
|
+
for (let j = 0; j < remaining.length; j++) {
|
|
1277
|
+
const item = JSON.parse(JSON.stringify(template));
|
|
1278
|
+
item.text = remaining[j].title + (remaining[j].description ? ': ' + remaining[j].description : '');
|
|
1279
|
+
item._id = Math.random().toString(36).substr(2, 7);
|
|
1280
|
+
// Keep template icons unless they're placeholder circles
|
|
1281
|
+
if (item.selected_icon && item.selected_icon.value === 'fas fa-circle') {
|
|
1282
|
+
item.selected_icon = { value: SERVICE_ICONS[j % SERVICE_ICONS.length], library: 'fa-solid' };
|
|
1283
|
+
}
|
|
1284
|
+
newItems.push(item);
|
|
1285
|
+
}
|
|
1286
|
+
il.widget.settings.icon_list = newItems;
|
|
1287
|
+
iconListItemIdx += remaining.length;
|
|
1288
|
+
changes++;
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
// Inject buttons — fill all available button slots, always set both text and URL
|
|
1185
1292
|
const contentButtons = content.buttons || [];
|
|
1186
1293
|
for (let i = 0; i < buttons.length && i < contentButtons.length; i++) {
|
|
1187
1294
|
buttons[i].widget.settings.text = contentButtons[i].text;
|
|
1188
|
-
if (
|
|
1189
|
-
|
|
1190
|
-
buttons[i].widget.settings.link.url = contentButtons[i].url;
|
|
1191
|
-
}
|
|
1295
|
+
if (!buttons[i].widget.settings.link) buttons[i].widget.settings.link = {};
|
|
1296
|
+
buttons[i].widget.settings.link.url = contentButtons[i].url || '#';
|
|
1192
1297
|
changes++;
|
|
1193
1298
|
}
|
|
1194
1299
|
|
|
1195
|
-
// Inject counters with stats
|
|
1300
|
+
// Inject counters with stats — smart parsing for various formats
|
|
1196
1301
|
const contentStats = content.stats || [];
|
|
1197
1302
|
for (let i = 0; i < counters.length && i < contentStats.length; i++) {
|
|
1198
|
-
const
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1303
|
+
const raw = String(contentStats[i].value || '').trim();
|
|
1304
|
+
let num = '', suffix = '';
|
|
1305
|
+
|
|
1306
|
+
// Pattern: "24/7" → num=24, suffix="/7"
|
|
1307
|
+
const fractionMatch = raw.match(/^(\d+)\/(\d+)$/);
|
|
1308
|
+
// Pattern: "$49" or "$49/month" → num=49, suffix="/month" or ""
|
|
1309
|
+
const currencyMatch = raw.match(/^\$(\d[\d,]*\.?\d*)(.*)$/);
|
|
1310
|
+
// Pattern: "500+" or "98%" or "15" → extract leading number and trailing suffix
|
|
1311
|
+
const generalMatch = raw.match(/^(\d[\d,]*\.?\d*)\s*(.*)$/);
|
|
1312
|
+
|
|
1313
|
+
if (fractionMatch) {
|
|
1314
|
+
num = fractionMatch[1];
|
|
1315
|
+
suffix = '/' + fractionMatch[2];
|
|
1316
|
+
} else if (currencyMatch) {
|
|
1317
|
+
num = currencyMatch[1].replace(/,/g, '');
|
|
1318
|
+
suffix = currencyMatch[2].trim();
|
|
1319
|
+
// Prepend $ as prefix if widget supports it
|
|
1320
|
+
if (counters[i].widget.settings.prefix !== undefined) {
|
|
1321
|
+
counters[i].widget.settings.prefix = '$';
|
|
1322
|
+
}
|
|
1323
|
+
} else if (generalMatch) {
|
|
1324
|
+
num = generalMatch[1].replace(/,/g, '');
|
|
1325
|
+
suffix = generalMatch[2].trim();
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
if (num) {
|
|
1329
|
+
counters[i].widget.settings.ending_number = num;
|
|
1330
|
+
}
|
|
1331
|
+
// Always set suffix — clear it if source has none (prevents leak from original)
|
|
1332
|
+
counters[i].widget.settings.suffix = suffix;
|
|
1333
|
+
|
|
1334
|
+
if (contentStats[i].label && counters[i].widget.settings.title !== undefined) {
|
|
1203
1335
|
counters[i].widget.settings.title = contentStats[i].label;
|
|
1204
1336
|
}
|
|
1205
1337
|
changes++;
|
|
@@ -1295,6 +1427,83 @@ function injectContent(section, content) {
|
|
|
1295
1427
|
}
|
|
1296
1428
|
}
|
|
1297
1429
|
|
|
1430
|
+
// ── Clear unfilled slots to remove template placeholder content ──
|
|
1431
|
+
// Track which slots were filled above, then clear the rest
|
|
1432
|
+
const HIDDEN_CLASSES = 'elementor-hidden-desktop elementor-hidden-tablet elementor-hidden-mobile';
|
|
1433
|
+
|
|
1434
|
+
// Headings: clear unfilled ones (beyond main + subheading + paragraph-filled ones)
|
|
1435
|
+
const filledHeadingCount = (content.heading ? 1 : 0) + (content.subheading ? 1 : 0) + Math.min(paragraphs.length, Math.max(0, headings.length - 2));
|
|
1436
|
+
for (let i = filledHeadingCount; i < headings.length; i++) {
|
|
1437
|
+
if (headings[i].widget.settings.title && headings[i].widget.settings.title !== content.heading && headings[i].widget.settings.title !== content.subheading) {
|
|
1438
|
+
headings[i].widget.settings.title = '';
|
|
1439
|
+
changes++;
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
// Text-editors: clear unfilled ones
|
|
1444
|
+
if (paragraphs.length === 0) {
|
|
1445
|
+
// No paragraphs at all — don't clear, the template text may be structural
|
|
1446
|
+
} else {
|
|
1447
|
+
// First text-editor got content; clear the rest
|
|
1448
|
+
for (let i = 1; i < texts.length; i++) {
|
|
1449
|
+
if (texts[i].widget.settings.editor) {
|
|
1450
|
+
texts[i].widget.settings.editor = '';
|
|
1451
|
+
changes++;
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
// Icon-boxes: hide excess (more template slots than content items)
|
|
1457
|
+
for (let i = items.length; i < iconBoxes.length; i++) {
|
|
1458
|
+
// Hide this icon-box's immediate parent container if all siblings are also excess
|
|
1459
|
+
iconBoxes[i].widget.settings.title_text = '';
|
|
1460
|
+
iconBoxes[i].widget.settings.description_text = '';
|
|
1461
|
+
if (!iconBoxes[i].widget.settings._css_classes) iconBoxes[i].widget.settings._css_classes = '';
|
|
1462
|
+
iconBoxes[i].widget.settings._css_classes = HIDDEN_CLASSES;
|
|
1463
|
+
iconBoxes[i].widget.settings.css_classes = HIDDEN_CLASSES;
|
|
1464
|
+
changes++;
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
// Image-boxes: clear unfilled ones
|
|
1468
|
+
const filledImageBoxes = Math.min(imageBoxes.length, Math.max(0, items.length - iconBoxes.length));
|
|
1469
|
+
for (let i = filledImageBoxes; i < imageBoxes.length; i++) {
|
|
1470
|
+
imageBoxes[i].widget.settings.title_text = '';
|
|
1471
|
+
imageBoxes[i].widget.settings.description_text = '';
|
|
1472
|
+
changes++;
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
// Buttons: hide excess beyond provided content buttons
|
|
1476
|
+
for (let i = contentButtons.length; i < buttons.length; i++) {
|
|
1477
|
+
if (!buttons[i].widget.settings._css_classes) buttons[i].widget.settings._css_classes = '';
|
|
1478
|
+
buttons[i].widget.settings._css_classes = HIDDEN_CLASSES;
|
|
1479
|
+
buttons[i].widget.settings.css_classes = HIDDEN_CLASSES;
|
|
1480
|
+
changes++;
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
// Counters: clear excess beyond provided stats
|
|
1484
|
+
for (let i = contentStats.length; i < counters.length; i++) {
|
|
1485
|
+
counters[i].widget.settings.title = '';
|
|
1486
|
+
counters[i].widget.settings._css_classes = HIDDEN_CLASSES;
|
|
1487
|
+
counters[i].widget.settings.css_classes = HIDDEN_CLASSES;
|
|
1488
|
+
changes++;
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
// Testimonials: clear excess beyond provided quotes
|
|
1492
|
+
for (let i = quotes.length; i < testimonials.length; i++) {
|
|
1493
|
+
testimonials[i].widget.settings.testimonial_content = '';
|
|
1494
|
+
testimonials[i].widget.settings.testimonial_name = '';
|
|
1495
|
+
changes++;
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
// Icon-lists: clear unfilled ones (those not already injected above)
|
|
1499
|
+
for (let i = 0; i < iconLists.length; i++) {
|
|
1500
|
+
// If this icon-list wasn't touched by injection (no items to give it), clear it
|
|
1501
|
+
if (iconListItemIdx === 0 && items.length === 0) {
|
|
1502
|
+
// No items at all — leave template icon-lists as-is (may be structural)
|
|
1503
|
+
}
|
|
1504
|
+
// Otherwise, icon-lists that got zero items from overflow should be cleared
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1298
1507
|
return { section: injected, changes };
|
|
1299
1508
|
}
|
|
1300
1509
|
|
|
@@ -1474,11 +1683,12 @@ function getToolDefinitions() {
|
|
|
1474
1683
|
},
|
|
1475
1684
|
{
|
|
1476
1685
|
name: 'set_design_tokens',
|
|
1477
|
-
description: 'Save design tokens to WordPress. Requires manage_options capability.',
|
|
1686
|
+
description: 'Save design tokens to WordPress. Default mode is "patch" (deep-merge with existing tokens). Use mode "replace" to overwrite all tokens. Requires manage_options capability.',
|
|
1478
1687
|
inputSchema: {
|
|
1479
1688
|
type: 'object',
|
|
1480
1689
|
properties: {
|
|
1481
|
-
tokens: { type: 'object', description: 'Full or partial token object to save' }
|
|
1690
|
+
tokens: { type: 'object', description: 'Full or partial token object to save' },
|
|
1691
|
+
mode: { type: 'string', enum: ['patch', 'replace'], description: 'Save mode: "patch" (default) deep-merges with existing tokens; "replace" overwrites all tokens' }
|
|
1482
1692
|
},
|
|
1483
1693
|
required: ['tokens']
|
|
1484
1694
|
}
|
|
@@ -1555,7 +1765,8 @@ function getToolDefinitions() {
|
|
|
1555
1765
|
properties: {
|
|
1556
1766
|
page_id: { type: 'number', description: 'WordPress page ID' },
|
|
1557
1767
|
template: { type: 'object', description: 'Elementor JSON: {content:[...]} or array of sections' },
|
|
1558
|
-
mode: { type: 'string', enum: ['replace', 'append'], description: 'Import mode (default: replace)' }
|
|
1768
|
+
mode: { type: 'string', enum: ['replace', 'append'], description: 'Import mode (default: replace)' },
|
|
1769
|
+
force: { type: 'boolean', description: 'Override edit locks (default: false)' }
|
|
1559
1770
|
},
|
|
1560
1771
|
required: ['page_id', 'template']
|
|
1561
1772
|
}
|
|
@@ -1592,7 +1803,8 @@ function getToolDefinitions() {
|
|
|
1592
1803
|
category: { type: 'string', description: 'Component category: heroes, intros, stats, conditions, comparisons, videos, ctas, teams, tabs, blocks, cards' },
|
|
1593
1804
|
name: { type: 'string', description: 'Component name (e.g., "hero-two-col-soft-blue", "stats-bar-4col-dark")' },
|
|
1594
1805
|
variables: { type: 'object', description: 'Variable values to populate (heading, description, buttons, etc.)' },
|
|
1595
|
-
position: { type: 'string', description: 'Where to add: "append" (default), "prepend", or numeric index' }
|
|
1806
|
+
position: { type: 'string', description: 'Where to add: "append" (default), "prepend", or numeric index' },
|
|
1807
|
+
force: { type: 'boolean', description: 'Override edit locks (default: false)' }
|
|
1596
1808
|
},
|
|
1597
1809
|
required: ['page_id', 'category', 'name']
|
|
1598
1810
|
}
|
|
@@ -1616,7 +1828,8 @@ function getToolDefinitions() {
|
|
|
1616
1828
|
}
|
|
1617
1829
|
}
|
|
1618
1830
|
},
|
|
1619
|
-
mode: { type: 'string', enum: ['replace', 'append'], description: 'Build mode (default: replace)' }
|
|
1831
|
+
mode: { type: 'string', enum: ['replace', 'append'], description: 'Build mode (default: replace)' },
|
|
1832
|
+
force: { type: 'boolean', description: 'Override edit locks (default: false)' }
|
|
1620
1833
|
},
|
|
1621
1834
|
required: ['page_id', 'sections']
|
|
1622
1835
|
}
|
|
@@ -1640,7 +1853,8 @@ function getToolDefinitions() {
|
|
|
1640
1853
|
properties: {
|
|
1641
1854
|
page_id: { type: 'number', description: 'WordPress page ID' },
|
|
1642
1855
|
index: { type: 'number', description: 'Section index (0-based). Use list_sections to find the right index.' },
|
|
1643
|
-
section_id: { type: 'string', description: 'Elementor section ID (alternative to index)' }
|
|
1856
|
+
section_id: { type: 'string', description: 'Elementor section ID (alternative to index)' },
|
|
1857
|
+
force: { type: 'boolean', description: 'Override edit locks (default: false)' }
|
|
1644
1858
|
},
|
|
1645
1859
|
required: ['page_id']
|
|
1646
1860
|
}
|
|
@@ -1666,7 +1880,8 @@ function getToolDefinitions() {
|
|
|
1666
1880
|
type: 'array',
|
|
1667
1881
|
description: 'Full reorder: array of current indices in desired order, e.g. [2,0,1,3]',
|
|
1668
1882
|
items: { type: 'number' }
|
|
1669
|
-
}
|
|
1883
|
+
},
|
|
1884
|
+
force: { type: 'boolean', description: 'Override edit locks (default: false)' }
|
|
1670
1885
|
},
|
|
1671
1886
|
required: ['page_id']
|
|
1672
1887
|
}
|
|
@@ -1679,7 +1894,47 @@ function getToolDefinitions() {
|
|
|
1679
1894
|
properties: {
|
|
1680
1895
|
page_id: { type: 'number', description: 'WordPress page ID' },
|
|
1681
1896
|
section_index: { type: 'number', description: 'Only show this section (0-based). Omit to show all sections.' },
|
|
1682
|
-
depth: { type: 'number', description: 'Max nesting depth (default:
|
|
1897
|
+
depth: { type: 'number', description: 'Max nesting depth (default: 10). When exceeded, returns children_ids stubs with {id, type, widget}.' }
|
|
1898
|
+
},
|
|
1899
|
+
required: ['page_id']
|
|
1900
|
+
}
|
|
1901
|
+
},
|
|
1902
|
+
{
|
|
1903
|
+
name: 'get_section_json',
|
|
1904
|
+
description: 'Get the full JSON for a single section by ID or index. Token-saving alternative to export_page when you only need one section. Returns complete nested element tree with all settings.',
|
|
1905
|
+
inputSchema: {
|
|
1906
|
+
type: 'object',
|
|
1907
|
+
properties: {
|
|
1908
|
+
page_id: { type: 'number', description: 'WordPress page ID' },
|
|
1909
|
+
section_id: { type: 'string', description: 'Elementor section ID. Use this OR section_index.' },
|
|
1910
|
+
section_index: { type: 'number', description: '0-based section index. Use this OR section_id.' }
|
|
1911
|
+
},
|
|
1912
|
+
required: ['page_id']
|
|
1913
|
+
}
|
|
1914
|
+
},
|
|
1915
|
+
{
|
|
1916
|
+
name: 'update_children',
|
|
1917
|
+
description: 'Recursively update all descendants of a parent element. Sets or removes settings properties on every child, grandchild, etc. — but NOT the parent itself. Supports optional filter to only modify specific element types (e.g. only containers or only headings).',
|
|
1918
|
+
inputSchema: {
|
|
1919
|
+
type: 'object',
|
|
1920
|
+
properties: {
|
|
1921
|
+
page_id: { type: 'number', description: 'WordPress page ID' },
|
|
1922
|
+
parent_id: { type: 'string', description: 'Elementor ID of the parent element whose children to update' },
|
|
1923
|
+
set_properties: { type: 'object', description: 'Key-value pairs to set on each descendant\'s settings', additionalProperties: true },
|
|
1924
|
+
remove_properties: { type: 'array', items: { type: 'string' }, description: 'Setting keys to remove from each descendant' },
|
|
1925
|
+
filter: { type: 'object', description: 'Only modify descendants matching this filter. Example: {"elType":"container"} or {"widgetType":"heading"}', additionalProperties: true },
|
|
1926
|
+
force: { type: 'boolean', description: 'Override edit locks (default: false)' }
|
|
1927
|
+
},
|
|
1928
|
+
required: ['page_id', 'parent_id']
|
|
1929
|
+
}
|
|
1930
|
+
},
|
|
1931
|
+
{
|
|
1932
|
+
name: 'check_page_lock',
|
|
1933
|
+
description: 'Check if a page is being edited by someone in the Elementor editor or by another MCP operation. Returns WP post lock (user, time) and MCP transient lock info. Use before write operations to avoid conflicts.',
|
|
1934
|
+
inputSchema: {
|
|
1935
|
+
type: 'object',
|
|
1936
|
+
properties: {
|
|
1937
|
+
page_id: { type: 'number', description: 'WordPress page ID' }
|
|
1683
1938
|
},
|
|
1684
1939
|
required: ['page_id']
|
|
1685
1940
|
}
|
|
@@ -1691,19 +1946,36 @@ function getToolDefinitions() {
|
|
|
1691
1946
|
type: 'object',
|
|
1692
1947
|
properties: {
|
|
1693
1948
|
page_id: { type: 'number', description: 'WordPress page ID' },
|
|
1694
|
-
element_id: { type: 'string', description: 'Elementor element ID to remove' }
|
|
1949
|
+
element_id: { type: 'string', description: 'Elementor element ID to remove' },
|
|
1950
|
+
force: { type: 'boolean', description: 'Override edit locks (default: false)' }
|
|
1695
1951
|
},
|
|
1696
1952
|
required: ['page_id', 'element_id']
|
|
1697
1953
|
}
|
|
1698
1954
|
},
|
|
1955
|
+
{
|
|
1956
|
+
name: 'move_element',
|
|
1957
|
+
description: 'Move an element from its current parent to a different parent container. Atomic server-side operation — no data loss. Prevents circular moves.',
|
|
1958
|
+
inputSchema: {
|
|
1959
|
+
type: 'object',
|
|
1960
|
+
properties: {
|
|
1961
|
+
page_id: { type: 'number', description: 'Page ID' },
|
|
1962
|
+
element_id: { type: 'string', description: 'Element ID to move' },
|
|
1963
|
+
target_parent_id: { type: 'string', description: 'Target parent container ID' },
|
|
1964
|
+
position: { type: 'number', description: 'Position in target (0-based). Omit to append.' },
|
|
1965
|
+
force: { type: 'boolean', description: 'Override edit lock' }
|
|
1966
|
+
},
|
|
1967
|
+
required: ['page_id', 'element_id', 'target_parent_id']
|
|
1968
|
+
}
|
|
1969
|
+
},
|
|
1699
1970
|
{
|
|
1700
1971
|
name: 'update_element',
|
|
1701
|
-
description: 'Update any element\'s settings by its Elementor ID. Patch widget properties like hover_color, background_color, __globals__ overrides, text, link URLs, etc. Supports dot-notation for nested keys (e.g. "__globals__.hover_color"). Use list_elements or export_page first to find the element ID and current settings. IMPORTANT: Prefer settings_json (JSON string) over settings (object) for reliable MCP transport.',
|
|
1972
|
+
description: 'Update any element\'s settings by its Elementor ID or path. Patch widget properties like hover_color, background_color, __globals__ overrides, text, link URLs, etc. Supports dot-notation for nested keys (e.g. "__globals__.hover_color"). Supports element_path as alternative to element_id (e.g. "sectionId/1/0" = section → child 1 → child 0). Use list_elements or export_page first to find the element ID and current settings. IMPORTANT: Prefer settings_json (JSON string) over settings (object) for reliable MCP transport. AUTO-INJECTED KEYS: background_background:"classic" auto-set when you set background_color; typography_typography:"custom" auto-set when you set font properties; flex_grow expands to all 3 required keys; grid column strings auto-wrapped in object format. ICON WIDGET: For view="default" use "icon_size" for dimensions. For view="stacked" or "framed" (circle/square background) use "size" instead — "icon_size" is silently ignored in stacked/framed mode.',
|
|
1702
1973
|
inputSchema: {
|
|
1703
1974
|
type: 'object',
|
|
1704
1975
|
properties: {
|
|
1705
1976
|
page_id: { type: 'number', description: 'WordPress page ID' },
|
|
1706
1977
|
element_id: { type: 'string', description: 'Elementor element ID to update' },
|
|
1978
|
+
element_path: { type: 'string', description: 'Path to element: "sectionId/childIndex/childIndex". Alternative to element_id.' },
|
|
1707
1979
|
settings: {
|
|
1708
1980
|
type: 'object',
|
|
1709
1981
|
description: 'Key-value pairs of settings to set. May not work reliably via MCP — use settings_json instead.',
|
|
@@ -1712,9 +1984,10 @@ function getToolDefinitions() {
|
|
|
1712
1984
|
settings_json: {
|
|
1713
1985
|
type: 'string',
|
|
1714
1986
|
description: 'Settings as a JSON string (preferred over settings object for reliable MCP transport). Example: \'{"hover_color":"#FFFFFF","__globals__":{"hover_color":""}}\''
|
|
1715
|
-
}
|
|
1987
|
+
},
|
|
1988
|
+
force: { type: 'boolean', description: 'Override edit locks (default: false)' }
|
|
1716
1989
|
},
|
|
1717
|
-
required: ['page_id'
|
|
1990
|
+
required: ['page_id']
|
|
1718
1991
|
}
|
|
1719
1992
|
},
|
|
1720
1993
|
{
|
|
@@ -1786,7 +2059,8 @@ function getToolDefinitions() {
|
|
|
1786
2059
|
replace: { type: 'string', description: 'Replacement text' },
|
|
1787
2060
|
regex: { type: 'boolean', description: 'Treat search as regex pattern. Default: false (literal).' },
|
|
1788
2061
|
page_id: { type: 'number', description: 'Optional: only search this page' },
|
|
1789
|
-
dry_run: { type: 'boolean', description: 'Preview changes without saving. Default: true (SAFE). Set to false to apply changes.' }
|
|
2062
|
+
dry_run: { type: 'boolean', description: 'Preview changes without saving. Default: true (SAFE). Set to false to apply changes.' },
|
|
2063
|
+
force: { type: 'boolean', description: 'Override edit locks (default: false)' }
|
|
1790
2064
|
},
|
|
1791
2065
|
required: ['search', 'replace']
|
|
1792
2066
|
}
|
|
@@ -1800,7 +2074,8 @@ function getToolDefinitions() {
|
|
|
1800
2074
|
source_page_id: { type: 'number', description: 'Page ID to copy FROM' },
|
|
1801
2075
|
element_id: { type: 'string', description: 'Elementor element ID to copy' },
|
|
1802
2076
|
target_page_id: { type: 'number', description: 'Page ID to copy TO' },
|
|
1803
|
-
position: { type: 'string', description: 'Where to place: "append" (default), "prepend", or section index number' }
|
|
2077
|
+
position: { type: 'string', description: 'Where to place: "append" (default), "prepend", or section index number' },
|
|
2078
|
+
force: { type: 'boolean', description: 'Override edit locks (default: false)' }
|
|
1804
2079
|
},
|
|
1805
2080
|
required: ['source_page_id', 'element_id', 'target_page_id']
|
|
1806
2081
|
}
|
|
@@ -2039,6 +2314,18 @@ function getToolDefinitions() {
|
|
|
2039
2314
|
required: ['template_id', 'elementor_data']
|
|
2040
2315
|
}
|
|
2041
2316
|
},
|
|
2317
|
+
{
|
|
2318
|
+
name: 'display_conditions',
|
|
2319
|
+
description: 'Get or set Elementor Theme Builder display conditions on a template (header, footer, single, archive). Common conditions: "include/general" (entire site), "include/singular/page" (all pages), "include/singular/post" (all posts), "include/archive" (all archives).',
|
|
2320
|
+
inputSchema: {
|
|
2321
|
+
type: 'object',
|
|
2322
|
+
properties: {
|
|
2323
|
+
template_id: { type: 'number', description: 'Template ID (from list_templates)' },
|
|
2324
|
+
conditions: { type: 'array', items: { type: 'string' }, description: 'Array of condition strings. Omit to read current conditions.' }
|
|
2325
|
+
},
|
|
2326
|
+
required: ['template_id']
|
|
2327
|
+
}
|
|
2328
|
+
},
|
|
2042
2329
|
{
|
|
2043
2330
|
name: 'audit_placeholders',
|
|
2044
2331
|
description: 'Scan all published Elementor pages for placeholder text (e.g. "Add Your Heading Text Here", "Lorem ipsum"), empty sections with no content, and broken anchor links (#). Optionally include library templates.',
|
|
@@ -2083,7 +2370,8 @@ function getToolDefinitions() {
|
|
|
2083
2370
|
page_id: { type: 'number', description: 'WordPress page ID' },
|
|
2084
2371
|
parent_id: { type: 'string', description: 'Elementor ID of the parent container to insert into' },
|
|
2085
2372
|
element: { type: 'object', description: 'Full Elementor element JSON: {elType, widgetType, settings, elements, ...}', additionalProperties: true },
|
|
2086
|
-
position: { type: 'number', description: 'Optional: insert at this index (0-based). Omit to append at end.' }
|
|
2373
|
+
position: { type: 'number', description: 'Optional: insert at this index (0-based). Omit to append at end.' },
|
|
2374
|
+
force: { type: 'boolean', description: 'Override edit locks (default: false)' }
|
|
2087
2375
|
},
|
|
2088
2376
|
required: ['page_id', 'parent_id', 'element']
|
|
2089
2377
|
}
|
|
@@ -2122,7 +2410,8 @@ function getToolDefinitions() {
|
|
|
2122
2410
|
updates_json: {
|
|
2123
2411
|
type: 'string',
|
|
2124
2412
|
description: 'Updates as a JSON string (preferred). Example: \'[{"page_id":123,"element_id":"abc","settings":{"title":"New"}}]\''
|
|
2125
|
-
}
|
|
2413
|
+
},
|
|
2414
|
+
force: { type: 'boolean', description: 'Override edit locks (default: false)' }
|
|
2126
2415
|
}
|
|
2127
2416
|
}
|
|
2128
2417
|
},
|
|
@@ -2176,6 +2465,192 @@ function getToolDefinitions() {
|
|
|
2176
2465
|
}
|
|
2177
2466
|
}
|
|
2178
2467
|
},
|
|
2468
|
+
{
|
|
2469
|
+
name: 'audit_db_health',
|
|
2470
|
+
description: 'Read-only WordPress database health audit. Reports: post revision count (+ top pages by revision), expired transient count, autoloaded options total size + top 10 largest, orphaned postmeta count. Includes actionable cleanup recommendations. No data is modified.',
|
|
2471
|
+
inputSchema: { type: 'object', properties: {} }
|
|
2472
|
+
},
|
|
2473
|
+
{
|
|
2474
|
+
name: 'audit_image_sizes',
|
|
2475
|
+
description: 'Scan Elementor pages for images using oversized WordPress sizes ("full" or "large"). Reports element ID, widget type, current size, actual file dimensions, and recommended size. Use to identify images that could be replaced with smaller WP registered sizes (medium_large, medium) to reduce page weight.',
|
|
2476
|
+
inputSchema: {
|
|
2477
|
+
type: 'object',
|
|
2478
|
+
properties: {
|
|
2479
|
+
page_id: { type: 'number', description: 'Optional: audit only this page. Omit to scan all Elementor pages.' }
|
|
2480
|
+
}
|
|
2481
|
+
}
|
|
2482
|
+
},
|
|
2483
|
+
{
|
|
2484
|
+
name: 'fix_image_sizes',
|
|
2485
|
+
description: 'Batch-fix Elementor images using oversized WP sizes (full/large). Updates image_size on image and image-box widgets to a smaller registered size. Container background images are automatically excluded. Run audit_image_sizes first, or use dry_run=true to preview changes before applying.',
|
|
2486
|
+
inputSchema: {
|
|
2487
|
+
type: 'object',
|
|
2488
|
+
properties: {
|
|
2489
|
+
page_id: { type: 'number', description: 'Optional: fix only this page. Omit to fix all Elementor pages.' },
|
|
2490
|
+
target_size: { type: 'string', description: 'WP registered size to apply (default: medium_large). Options: thumbnail, medium, medium_large.' },
|
|
2491
|
+
dry_run: { type: 'boolean', description: 'Preview changes without saving (default: false). Use true first to review.' },
|
|
2492
|
+
exclude_ids: { type: 'array', items: { type: 'string' }, description: 'Elementor element IDs to skip (e.g. hero images you want to keep at full size).' },
|
|
2493
|
+
force: { type: 'boolean', description: 'Override edit locks (default: false).' }
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
},
|
|
2497
|
+
{
|
|
2498
|
+
name: 'search_post_content',
|
|
2499
|
+
description: 'Search across all published WordPress post/page content (Gutenberg, classic editor, raw HTML) for a given string. Returns matching posts with snippets showing context around each match. Use to find S3 URLs, old domains, broken shortcodes, or any text pattern in post_content.',
|
|
2500
|
+
inputSchema: {
|
|
2501
|
+
type: 'object',
|
|
2502
|
+
properties: {
|
|
2503
|
+
search: { type: 'string', description: 'Text to search for in post_content (case-insensitive).' },
|
|
2504
|
+
post_type: { type: 'string', description: 'Filter by post type: "post", "page", or "any" (default: any — searches posts and pages).' },
|
|
2505
|
+
limit: { type: 'number', description: 'Max posts to return (default: 500, max: 500).' },
|
|
2506
|
+
extract_urls: { type: 'boolean', description: 'Extract full URLs containing the search term. Returns per-post URLs and a deduplicated list of all unique URLs (default: false).' }
|
|
2507
|
+
},
|
|
2508
|
+
required: ['search']
|
|
2509
|
+
}
|
|
2510
|
+
},
|
|
2511
|
+
{
|
|
2512
|
+
name: 'replace_post_content',
|
|
2513
|
+
description: 'Bulk find-replace in WordPress post_content (Gutenberg/classic editor). Use for migrating URLs, fixing domains, replacing shortcodes. Always dry_run=true first to preview. Works on published posts and pages.',
|
|
2514
|
+
inputSchema: {
|
|
2515
|
+
type: 'object',
|
|
2516
|
+
properties: {
|
|
2517
|
+
search: { type: 'string', description: 'Text to find in post_content.' },
|
|
2518
|
+
replace: { type: 'string', description: 'Replacement text.' },
|
|
2519
|
+
dry_run: { type: 'boolean', description: 'Preview without saving (default: true). Set false to apply.' },
|
|
2520
|
+
regex: { type: 'boolean', description: 'Treat search as PHP regex pattern (without delimiters). Use $1, $2 for capture groups in replace. Default: false.' },
|
|
2521
|
+
post_type: { type: 'string', description: 'Filter: "post", "page", or "any" (default: any).' },
|
|
2522
|
+
limit: { type: 'number', description: 'Max posts to process (default: 500).' }
|
|
2523
|
+
},
|
|
2524
|
+
required: ['search', 'replace']
|
|
2525
|
+
}
|
|
2526
|
+
},
|
|
2527
|
+
{
|
|
2528
|
+
name: 'set_faq_schema',
|
|
2529
|
+
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.',
|
|
2530
|
+
inputSchema: {
|
|
2531
|
+
type: 'object',
|
|
2532
|
+
properties: {
|
|
2533
|
+
post_id: { type: 'number', description: 'WordPress post ID.' },
|
|
2534
|
+
faqs: { type: 'array', items: { type: 'object', properties: { question: { type: 'string' }, answer: { type: 'string' } }, required: ['question', 'answer'] }, description: 'Array of {question, answer} objects.' },
|
|
2535
|
+
dry_run: { type: 'boolean', description: 'Preview without saving (default: true).' }
|
|
2536
|
+
},
|
|
2537
|
+
required: ['post_id', 'faqs']
|
|
2538
|
+
}
|
|
2539
|
+
},
|
|
2540
|
+
{
|
|
2541
|
+
name: 'import_faq_schemas',
|
|
2542
|
+
description: 'Bulk import FAQPage schemas from faq-schema-posts.json format. Matches posts by slug and stores FAQPage JSON-LD in post meta. Always dry_run first.',
|
|
2543
|
+
inputSchema: {
|
|
2544
|
+
type: 'object',
|
|
2545
|
+
properties: {
|
|
2546
|
+
posts: { type: 'array', description: 'Array of {slug, faq_schema: [{question, answer}]} objects.' },
|
|
2547
|
+
dry_run: { type: 'boolean', description: 'Preview without saving (default: true).' }
|
|
2548
|
+
},
|
|
2549
|
+
required: ['posts']
|
|
2550
|
+
}
|
|
2551
|
+
},
|
|
2552
|
+
{
|
|
2553
|
+
name: 'get_global_styles',
|
|
2554
|
+
description: 'Read Elementor global colors and fonts from the active kit. Returns system colors (Primary, Secondary, Text, Accent), custom colors, system typography, and custom typography with all properties.',
|
|
2555
|
+
inputSchema: { type: 'object', properties: {} }
|
|
2556
|
+
},
|
|
2557
|
+
{
|
|
2558
|
+
name: 'set_global_styles',
|
|
2559
|
+
description: 'Update Elementor global colors and/or fonts on the active kit. Supports system_colors, custom_colors, system_typography, custom_typography. Updates existing entries by ID or creates new ones. Clears Elementor CSS cache after applying. Always dry_run=true first.',
|
|
2560
|
+
inputSchema: {
|
|
2561
|
+
type: 'object',
|
|
2562
|
+
properties: {
|
|
2563
|
+
system_colors: { type: 'array', description: 'Array of {id, title?, color} — update system colors (primary, secondary, text, accent).', items: { type: 'object' } },
|
|
2564
|
+
custom_colors: { type: 'array', description: 'Array of {id, title?, color} — update or add custom palette colors.', items: { type: 'object' } },
|
|
2565
|
+
system_typography: { type: 'array', description: 'Array of {id, title?, font_family, font_weight?, font_size?, line_height?, letter_spacing?, text_transform?}.', items: { type: 'object' } },
|
|
2566
|
+
custom_typography: { type: 'array', description: 'Array of {id, title?, font_family, font_weight?, font_size?, line_height?, letter_spacing?, text_transform?}.', items: { type: 'object' } },
|
|
2567
|
+
dry_run: { type: 'boolean', description: 'Preview without saving (default: true).' }
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
},
|
|
2571
|
+
{
|
|
2572
|
+
name: 'audit_broken_images',
|
|
2573
|
+
description: 'Scan all published posts and Elementor pages for broken images (non-200 HTTP responses). Checks <img> tags in post_content and image/image-box widgets + background images in Elementor data. Returns broken URLs with HTTP status, error details, and which posts are affected.',
|
|
2574
|
+
inputSchema: {
|
|
2575
|
+
type: 'object',
|
|
2576
|
+
properties: {
|
|
2577
|
+
page_id: { type: 'number', description: 'Optional: scan only this page.' },
|
|
2578
|
+
source: { type: 'string', description: 'What to scan: "all" (default), "elementor", or "content" (post_content only).' },
|
|
2579
|
+
timeout: { type: 'number', description: 'HTTP HEAD timeout in seconds per image (default: 5, max: 15).' }
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2582
|
+
},
|
|
2583
|
+
// ── ACF Fields ──
|
|
2584
|
+
{
|
|
2585
|
+
name: 'list_acf_field_groups',
|
|
2586
|
+
description: 'List all ACF field groups with their fields and post type assignments. Shows field group title, key, active status, assigned post types, and all fields with type, name, label. Use to discover what custom fields exist on the site.',
|
|
2587
|
+
inputSchema: { type: 'object', properties: {} }
|
|
2588
|
+
},
|
|
2589
|
+
{
|
|
2590
|
+
name: 'get_acf_fields',
|
|
2591
|
+
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.',
|
|
2592
|
+
inputSchema: {
|
|
2593
|
+
type: 'object',
|
|
2594
|
+
properties: {
|
|
2595
|
+
post_id: { type: 'number', description: 'WordPress post ID to get ACF fields for.' }
|
|
2596
|
+
},
|
|
2597
|
+
required: ['post_id']
|
|
2598
|
+
}
|
|
2599
|
+
},
|
|
2600
|
+
{
|
|
2601
|
+
name: 'set_acf_fields',
|
|
2602
|
+
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.',
|
|
2603
|
+
inputSchema: {
|
|
2604
|
+
type: 'object',
|
|
2605
|
+
properties: {
|
|
2606
|
+
post_id: { type: 'number', description: 'WordPress post ID to update.' },
|
|
2607
|
+
fields: { type: 'object', description: 'Object of field_name: value pairs. Example: {"hero_title": "New Title", "show_sidebar": true}' },
|
|
2608
|
+
dry_run: { type: 'boolean', description: 'Preview changes without saving (default: true). Set false to apply.' }
|
|
2609
|
+
},
|
|
2610
|
+
required: ['post_id', 'fields']
|
|
2611
|
+
}
|
|
2612
|
+
},
|
|
2613
|
+
{
|
|
2614
|
+
name: 'create_acf_field_group',
|
|
2615
|
+
description: 'Create a new ACF field group with fields and post type assignments. Supports field types: text, textarea, wysiwyg, number, url, image, select, true_false, repeater, group.',
|
|
2616
|
+
inputSchema: {
|
|
2617
|
+
type: 'object',
|
|
2618
|
+
properties: {
|
|
2619
|
+
title: { type: 'string', description: 'Field group title.' },
|
|
2620
|
+
fields: { type: 'array', items: { type: 'object' }, description: 'Array of field definitions: [{label, name, type, required?, default_value?, choices?, sub_fields?}]' },
|
|
2621
|
+
post_types: { type: 'array', items: { type: 'string' }, description: 'Post types to assign to (e.g. ["post", "page", "physician"]).' },
|
|
2622
|
+
active: { type: 'boolean', description: 'Whether field group is active (default: true).' }
|
|
2623
|
+
},
|
|
2624
|
+
required: ['title', 'fields']
|
|
2625
|
+
}
|
|
2626
|
+
},
|
|
2627
|
+
{
|
|
2628
|
+
name: 'delete_acf_field',
|
|
2629
|
+
description: 'Remove a field from an ACF field group by field key. Use list_acf_field_groups first to find the key. Permanently deletes the field definition — existing values in post meta are NOT removed.',
|
|
2630
|
+
inputSchema: {
|
|
2631
|
+
type: 'object',
|
|
2632
|
+
properties: {
|
|
2633
|
+
field_key: { type: 'string', description: 'ACF field key to delete (e.g. "field_64a1b2c3d4e5f").' }
|
|
2634
|
+
},
|
|
2635
|
+
required: ['field_key']
|
|
2636
|
+
}
|
|
2637
|
+
},
|
|
2638
|
+
// ── Audits ──
|
|
2639
|
+
{
|
|
2640
|
+
name: 'audit_orphan_pages',
|
|
2641
|
+
description: 'Find pages with no internal links pointing to them. Cross-references all link targets (Elementor data + post content + nav menus) against all published pages. Orphan pages are hard for search engines to discover.',
|
|
2642
|
+
inputSchema: { type: 'object', properties: {} }
|
|
2643
|
+
},
|
|
2644
|
+
{
|
|
2645
|
+
name: 'audit_h1',
|
|
2646
|
+
description: 'Scan all Elementor pages for H1 heading issues. Every page should have exactly one H1. Flags pages with missing H1 (bad for SEO) or multiple H1s (confusing hierarchy).',
|
|
2647
|
+
inputSchema: { type: 'object', properties: {} }
|
|
2648
|
+
},
|
|
2649
|
+
{
|
|
2650
|
+
name: 'audit_redirect_chains',
|
|
2651
|
+
description: 'Detect redirect chains (A→B→C) among internal URLs. Each extra hop slows page load and dilutes SEO link equity. Checks up to 200 internal URLs via HTTP HEAD with redirect following.',
|
|
2652
|
+
inputSchema: { type: 'object', properties: {} }
|
|
2653
|
+
},
|
|
2179
2654
|
// ── Section Operations ──
|
|
2180
2655
|
{
|
|
2181
2656
|
name: 'copy_section',
|
|
@@ -2186,7 +2661,8 @@ function getToolDefinitions() {
|
|
|
2186
2661
|
source_id: { type: 'number', description: 'Page or template ID to copy FROM' },
|
|
2187
2662
|
section_index: { type: 'number', description: 'Zero-based index of the section to copy (use list_sections to find it)' },
|
|
2188
2663
|
target_id: { type: 'number', description: 'Page or template ID to copy TO' },
|
|
2189
|
-
position: { type: 'string', description: 'Where to place: "end" (default), "start", or numeric index' }
|
|
2664
|
+
position: { type: 'string', description: 'Where to place: "end" (default), "start", or numeric index' },
|
|
2665
|
+
force: { type: 'boolean', description: 'Override edit locks (default: false)' }
|
|
2190
2666
|
},
|
|
2191
2667
|
required: ['source_id', 'section_index', 'target_id']
|
|
2192
2668
|
}
|
|
@@ -2200,7 +2676,8 @@ function getToolDefinitions() {
|
|
|
2200
2676
|
target_id: { type: 'number', description: 'Page or template ID to append to' },
|
|
2201
2677
|
section: { type: 'object', description: 'Full Elementor section object (must have elType and elements)' },
|
|
2202
2678
|
position: { type: 'string', description: 'Where to place: "end" (default), "start", or numeric index' },
|
|
2203
|
-
regenerate_ids: { type: 'boolean', description: 'Regenerate all element IDs to avoid collisions (default: true)' }
|
|
2679
|
+
regenerate_ids: { type: 'boolean', description: 'Regenerate all element IDs to avoid collisions (default: true)' },
|
|
2680
|
+
force: { type: 'boolean', description: 'Override edit locks (default: false)' }
|
|
2204
2681
|
},
|
|
2205
2682
|
required: ['target_id', 'section']
|
|
2206
2683
|
}
|
|
@@ -2373,6 +2850,11 @@ function getToolDefinitions() {
|
|
|
2373
2850
|
required: ['enabled']
|
|
2374
2851
|
}
|
|
2375
2852
|
},
|
|
2853
|
+
{
|
|
2854
|
+
name: 'scan_shortcodes',
|
|
2855
|
+
description: 'Scan all published pages/posts for NVBC shortcode usage ([nvbc_categories], [nvbc_list_category], [nvbc_excerpt], [nvbc_category_list]). Shows which pages use which shortcodes and where (post_content vs Elementor data). Use this to determine if shortcodes are safe to disable. No AI tokens used — pure PHP scan.',
|
|
2856
|
+
inputSchema: { type: 'object', properties: {} }
|
|
2857
|
+
},
|
|
2376
2858
|
{
|
|
2377
2859
|
name: 'get_site_profile',
|
|
2378
2860
|
description: 'Get the site profile — stored preferences, URLs, design conventions, and team info. Use this at the start of a session to understand site context.',
|
|
@@ -2441,6 +2923,31 @@ function getToolDefinitions() {
|
|
|
2441
2923
|
type: 'object',
|
|
2442
2924
|
properties: {}
|
|
2443
2925
|
}
|
|
2926
|
+
},
|
|
2927
|
+
{
|
|
2928
|
+
name: 'clean_globals',
|
|
2929
|
+
description: 'Remove __globals__ references pointing to color/font IDs that don\'t exist in the current Elementor kit. Fixes foreign CSS rendering from imported content. Dry-run by default.',
|
|
2930
|
+
inputSchema: {
|
|
2931
|
+
type: 'object',
|
|
2932
|
+
properties: {
|
|
2933
|
+
page_id: { type: 'number', description: 'Page ID to clean' },
|
|
2934
|
+
dry_run: { type: 'boolean', description: 'Preview changes without saving (default: true)' },
|
|
2935
|
+
force: { type: 'boolean', description: 'Override edit lock' }
|
|
2936
|
+
},
|
|
2937
|
+
required: ['page_id']
|
|
2938
|
+
}
|
|
2939
|
+
},
|
|
2940
|
+
{
|
|
2941
|
+
name: 'set_front_page',
|
|
2942
|
+
description: 'Set a WordPress page as the site\'s static front page. Optionally set a posts page too.',
|
|
2943
|
+
inputSchema: {
|
|
2944
|
+
type: 'object',
|
|
2945
|
+
properties: {
|
|
2946
|
+
page_id: { type: 'number', description: 'Page ID to set as front page' },
|
|
2947
|
+
blog_page_id: { type: 'number', description: 'Page ID for blog/posts page (optional)' }
|
|
2948
|
+
},
|
|
2949
|
+
required: ['page_id']
|
|
2950
|
+
}
|
|
2444
2951
|
}
|
|
2445
2952
|
];
|
|
2446
2953
|
}
|
|
@@ -2494,7 +3001,7 @@ async function handleToolCall(name, args) {
|
|
|
2494
3001
|
}
|
|
2495
3002
|
|
|
2496
3003
|
case 'set_design_tokens': {
|
|
2497
|
-
const r = await apiCall('/design-tokens', 'POST', { tokens: args.tokens });
|
|
3004
|
+
const r = await apiCall('/design-tokens', 'POST', { tokens: args.tokens, mode: args.mode || 'patch' });
|
|
2498
3005
|
if (r.success) {
|
|
2499
3006
|
tokenCache.data = null; // Invalidate cache
|
|
2500
3007
|
return ok(`Tokens saved successfully.\n\nUpdated tokens:\n${JSON.stringify(r.tokens, null, 2)}`);
|
|
@@ -2624,7 +3131,9 @@ async function handleToolCall(name, args) {
|
|
|
2624
3131
|
}
|
|
2625
3132
|
|
|
2626
3133
|
case 'import_template': {
|
|
2627
|
-
const
|
|
3134
|
+
const body = { template: args.template, mode: args.mode || 'replace' };
|
|
3135
|
+
if (args.force) body.force = true;
|
|
3136
|
+
const r = await apiCall(`/pages/${args.page_id}/import`, 'POST', body);
|
|
2628
3137
|
return ok(r.success ? `Imported!\n${r.sections} sections, ${r.total_elements} elements (${r.mode})\n\n${r.preview_url}` : `Error: ${r.message}`);
|
|
2629
3138
|
}
|
|
2630
3139
|
|
|
@@ -2686,12 +3195,14 @@ async function handleToolCall(name, args) {
|
|
|
2686
3195
|
}
|
|
2687
3196
|
|
|
2688
3197
|
case 'add_component': {
|
|
2689
|
-
const
|
|
3198
|
+
const body = {
|
|
2690
3199
|
category: args.category,
|
|
2691
3200
|
name: args.name,
|
|
2692
3201
|
variables: args.variables || {},
|
|
2693
3202
|
position: args.position || 'append'
|
|
2694
|
-
}
|
|
3203
|
+
};
|
|
3204
|
+
if (args.force) body.force = true;
|
|
3205
|
+
const r = await apiCall(`/pages/${args.page_id}/add-component`, 'POST', body);
|
|
2695
3206
|
if (!r.success) return ok(`Failed: ${r.message || 'Unknown error'}`);
|
|
2696
3207
|
let msg = `Added ${r.component.category}/${r.component.name}!\n` +
|
|
2697
3208
|
`ID: ${r.component.id}\nTotal sections: ${r.total_sections}\nPreview: ${r.preview_url}`;
|
|
@@ -2700,10 +3211,12 @@ async function handleToolCall(name, args) {
|
|
|
2700
3211
|
}
|
|
2701
3212
|
|
|
2702
3213
|
case 'build_page': {
|
|
2703
|
-
const
|
|
3214
|
+
const body = {
|
|
2704
3215
|
sections: args.sections,
|
|
2705
3216
|
mode: args.mode || 'replace'
|
|
2706
|
-
}
|
|
3217
|
+
};
|
|
3218
|
+
if (args.force) body.force = true;
|
|
3219
|
+
const r = await apiCall(`/pages/${args.page_id}/build`, 'POST', body);
|
|
2707
3220
|
if (!r.success) return ok(`Build failed: ${r.message || 'Unknown error'}`);
|
|
2708
3221
|
let buildMsg = `Page Built!\nSections: ${r.sections_built}\nElements: ${r.total_elements}\nMode: ${r.mode}\n\nPreview: ${r.preview_url}\nEdit: ${r.edit_url}`;
|
|
2709
3222
|
if (r.post_placement_instructions?.length) {
|
|
@@ -2731,6 +3244,7 @@ async function handleToolCall(name, args) {
|
|
|
2731
3244
|
const body = {};
|
|
2732
3245
|
if (args.index !== undefined) body.index = args.index;
|
|
2733
3246
|
if (args.section_id) body.section_id = args.section_id;
|
|
3247
|
+
if (args.force) body.force = true;
|
|
2734
3248
|
const r = await apiCall(`/pages/${args.page_id}/remove-section`, 'POST', body);
|
|
2735
3249
|
if (r.code || r.error) return ok(`Failed: ${r.message || r.error || 'Unknown error'}`);
|
|
2736
3250
|
return ok(`Removed section${r.removed?.id ? ` (ID: ${r.removed.id})` : ''}.\nRemaining sections: ${r.remaining_sections}`);
|
|
@@ -2741,6 +3255,7 @@ async function handleToolCall(name, args) {
|
|
|
2741
3255
|
if (args.move) body.move = args.move;
|
|
2742
3256
|
else if (args.swap) body.swap = args.swap;
|
|
2743
3257
|
else if (args.order) body.order = args.order;
|
|
3258
|
+
if (args.force) body.force = true;
|
|
2744
3259
|
const r = await apiCall(`/pages/${args.page_id}/reorder`, 'POST', body);
|
|
2745
3260
|
if (r.code || r.error) return ok(`Failed: ${r.message || r.error || 'Unknown error'}`);
|
|
2746
3261
|
let msg = `Sections reordered! (${r.sections} total)\n`;
|
|
@@ -2775,6 +3290,11 @@ async function handleToolCall(name, args) {
|
|
|
2775
3290
|
let result = line + '\n';
|
|
2776
3291
|
if (el.children) {
|
|
2777
3292
|
el.children.forEach(c => { result += formatTree(c, indent + 1); });
|
|
3293
|
+
} else if (el.children_ids) {
|
|
3294
|
+
el.children_ids.forEach(c => {
|
|
3295
|
+
const label = c.widget ? `[${c.widget}]` : `<${c.type}>`;
|
|
3296
|
+
result += ' '.repeat(indent + 1) + `${label} id:${c.id}\n`;
|
|
3297
|
+
});
|
|
2778
3298
|
} else if (el.children_count) {
|
|
2779
3299
|
result += ' '.repeat(indent + 1) + `(${el.children_count} children, increase depth to see)\n`;
|
|
2780
3300
|
}
|
|
@@ -2789,14 +3309,67 @@ async function handleToolCall(name, args) {
|
|
|
2789
3309
|
return ok(msg);
|
|
2790
3310
|
}
|
|
2791
3311
|
|
|
3312
|
+
case 'get_section_json': {
|
|
3313
|
+
const params = new URLSearchParams();
|
|
3314
|
+
if (args.section_id) params.set('section_id', args.section_id);
|
|
3315
|
+
if (args.section_index !== undefined) params.set('section_index', args.section_index);
|
|
3316
|
+
const qs = params.toString() ? `?${params.toString()}` : '';
|
|
3317
|
+
const r = await apiCall(`/pages/${args.page_id}/section-json${qs}`);
|
|
3318
|
+
if (r.code || r.error) return ok(`Failed: ${r.message || r.error || 'Unknown error'}`);
|
|
3319
|
+
return ok(`Section [${r.section_index}] (ID: ${r.section_id}) — ${r.widgets} widgets\n\n${JSON.stringify(r.elementor_data, null, 2)}`);
|
|
3320
|
+
}
|
|
3321
|
+
|
|
3322
|
+
case 'update_children': {
|
|
3323
|
+
const body = {
|
|
3324
|
+
parent_id: args.parent_id,
|
|
3325
|
+
set_properties: args.set_properties || {},
|
|
3326
|
+
remove_properties: args.remove_properties || [],
|
|
3327
|
+
force: args.force || false,
|
|
3328
|
+
};
|
|
3329
|
+
if (args.filter) body.filter = args.filter;
|
|
3330
|
+
const r = await apiCall(`/pages/${args.page_id}/update-children`, 'POST', body);
|
|
3331
|
+
if (r.code || r.error) return ok(`Failed: ${r.message || r.error || 'Unknown error'}`);
|
|
3332
|
+
let msg = `Updated ${r.descendants_updated} descendants of ${r.parent_id} on page ${r.page_id}.`;
|
|
3333
|
+
if (r.properties_set.length) msg += `\nSet: ${r.properties_set.join(', ')}`;
|
|
3334
|
+
if (r.properties_removed.length) msg += `\nRemoved: ${r.properties_removed.join(', ')}`;
|
|
3335
|
+
if (r.filter) msg += `\nFilter: ${JSON.stringify(r.filter)}`;
|
|
3336
|
+
return ok(msg);
|
|
3337
|
+
}
|
|
3338
|
+
|
|
3339
|
+
case 'check_page_lock': {
|
|
3340
|
+
const r = await apiCall(`/pages/${args.page_id}/lock-status`);
|
|
3341
|
+
if (r.code || r.error) return ok(`Failed: ${r.message || r.error || 'Unknown error'}`);
|
|
3342
|
+
if (!r.locked) return ok(`Page ${args.page_id}: Not locked — safe to edit.`);
|
|
3343
|
+
let msg = `Page ${args.page_id}: LOCKED\n`;
|
|
3344
|
+
if (r.wp_lock) {
|
|
3345
|
+
msg += ` WP Editor: ${r.wp_lock.user} (${r.wp_lock.age_seconds}s ago)\n`;
|
|
3346
|
+
}
|
|
3347
|
+
if (r.mcp_lock) {
|
|
3348
|
+
msg += ` MCP Tool: ${r.mcp_lock.tool} (started ${r.mcp_lock.time})\n`;
|
|
3349
|
+
}
|
|
3350
|
+
msg += `\nUse force=true on write operations to override.`;
|
|
3351
|
+
return ok(msg);
|
|
3352
|
+
}
|
|
3353
|
+
|
|
2792
3354
|
case 'remove_element': {
|
|
2793
|
-
const
|
|
2794
|
-
|
|
2795
|
-
});
|
|
3355
|
+
const body = { element_id: args.element_id };
|
|
3356
|
+
if (args.force) body.force = true;
|
|
3357
|
+
const r = await apiCall(`/pages/${args.page_id}/remove-element`, 'POST', body);
|
|
2796
3358
|
if (r.code || r.error) return ok(`Failed: ${r.message || r.error || 'Unknown error'}`);
|
|
2797
3359
|
return ok(`Removed ${r.removed.type} (ID: ${r.removed.id}).\nRemaining sections: ${r.remaining_sections}`);
|
|
2798
3360
|
}
|
|
2799
3361
|
|
|
3362
|
+
case 'move_element': {
|
|
3363
|
+
const r = await apiCall(`/pages/${args.page_id}/move-element`, 'POST', {
|
|
3364
|
+
element_id: args.element_id,
|
|
3365
|
+
target_parent_id: args.target_parent_id,
|
|
3366
|
+
position: args.position,
|
|
3367
|
+
force: parseBool(args.force),
|
|
3368
|
+
});
|
|
3369
|
+
if (r.code || r.error) return ok(`Failed: ${r.message || r.error || 'Unknown error'}`);
|
|
3370
|
+
return ok(`Moved ${r.element_type} (${r.element_id}) to parent ${r.target_parent_id}${r.position !== null ? ` at position ${r.position}` : ''}`);
|
|
3371
|
+
}
|
|
3372
|
+
|
|
2800
3373
|
case 'update_element': {
|
|
2801
3374
|
// Resolve settings: prefer settings_json (string) for reliable MCP transport, fall back to settings (object)
|
|
2802
3375
|
let settings = null;
|
|
@@ -2816,10 +3389,14 @@ async function handleToolCall(name, args) {
|
|
|
2816
3389
|
}
|
|
2817
3390
|
const cleanSettings = deepStripCDATA(settings);
|
|
2818
3391
|
try {
|
|
2819
|
-
const
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
3392
|
+
const body = { settings: cleanSettings };
|
|
3393
|
+
if (args.element_id) body.element_id = args.element_id;
|
|
3394
|
+
if (args.element_path) body.element_path = args.element_path;
|
|
3395
|
+
if (args.force) body.force = true;
|
|
3396
|
+
if (!body.element_id && !body.element_path) {
|
|
3397
|
+
return ok('Failed: Provide element_id or element_path.');
|
|
3398
|
+
}
|
|
3399
|
+
const r = await apiCall(`/pages/${args.page_id}/update-element`, 'POST', body);
|
|
2823
3400
|
if (r.code || r.error) return ok(`Failed: ${r.message || r.error || 'Unknown error'}`);
|
|
2824
3401
|
return ok(`Element ${r.element_id} updated on page ${r.page_id}.\nUpdated keys: ${r.updated_keys.join(', ')}`);
|
|
2825
3402
|
} catch (err) {
|
|
@@ -2991,6 +3568,7 @@ async function handleToolCall(name, args) {
|
|
|
2991
3568
|
dry_run: parseBool(args.dry_run, true), // default TRUE (safe)
|
|
2992
3569
|
};
|
|
2993
3570
|
if (args.page_id) body.page_id = args.page_id;
|
|
3571
|
+
if (args.force) body.force = true;
|
|
2994
3572
|
const r = await apiCall('/find-replace', 'POST', body);
|
|
2995
3573
|
if (r.code || r.error) return ok(`Failed: ${r.message || r.error || 'Unknown error'}`);
|
|
2996
3574
|
|
|
@@ -3014,12 +3592,14 @@ async function handleToolCall(name, args) {
|
|
|
3014
3592
|
}
|
|
3015
3593
|
|
|
3016
3594
|
case 'copy_element': {
|
|
3017
|
-
const
|
|
3595
|
+
const body = {
|
|
3018
3596
|
source_page_id: args.source_page_id,
|
|
3019
3597
|
element_id: args.element_id,
|
|
3020
3598
|
target_page_id: args.target_page_id,
|
|
3021
3599
|
position: args.position || 'append',
|
|
3022
|
-
}
|
|
3600
|
+
};
|
|
3601
|
+
if (args.force) body.force = true;
|
|
3602
|
+
const r = await apiCall('/copy-element', 'POST', body);
|
|
3023
3603
|
if (r.code || r.error) return ok(`Failed: ${r.message || r.error || 'Unknown error'}`);
|
|
3024
3604
|
let msg = `Element copied!\n`;
|
|
3025
3605
|
msg += `Source: page ${r.source_page_id}, element ${r.original_id} (${r.element_type}`;
|
|
@@ -3350,6 +3930,19 @@ async function handleToolCall(name, args) {
|
|
|
3350
3930
|
);
|
|
3351
3931
|
}
|
|
3352
3932
|
|
|
3933
|
+
case 'display_conditions': {
|
|
3934
|
+
if (args.conditions) {
|
|
3935
|
+
const r = await apiCall(`/templates/${args.template_id}/display-conditions`, 'POST', {
|
|
3936
|
+
conditions: args.conditions,
|
|
3937
|
+
});
|
|
3938
|
+
return ok(`Set ${r.conditions.length} condition(s) on "${r.title}" (ID: ${r.template_id}):\n${r.conditions.join('\n')}`);
|
|
3939
|
+
} else {
|
|
3940
|
+
const r = await apiCall(`/templates/${args.template_id}/display-conditions`);
|
|
3941
|
+
const conds = r.conditions && r.conditions.length ? r.conditions.join('\n') : '(none)';
|
|
3942
|
+
return ok(`Display conditions for "${r.title}" (ID: ${r.template_id}):\n${conds}`);
|
|
3943
|
+
}
|
|
3944
|
+
}
|
|
3945
|
+
|
|
3353
3946
|
case 'audit_placeholders': {
|
|
3354
3947
|
const qs = args.include_templates ? '?include_templates=1' : '';
|
|
3355
3948
|
const r = await apiCall(`/audit-placeholders${qs}`);
|
|
@@ -3423,11 +4016,13 @@ async function handleToolCall(name, args) {
|
|
|
3423
4016
|
// ── Element Operations (v3.6.0) ──
|
|
3424
4017
|
|
|
3425
4018
|
case 'add_element': {
|
|
3426
|
-
const
|
|
4019
|
+
const body = {
|
|
3427
4020
|
parent_id: args.parent_id,
|
|
3428
4021
|
element: args.element,
|
|
3429
4022
|
position: args.position
|
|
3430
|
-
}
|
|
4023
|
+
};
|
|
4024
|
+
if (args.force) body.force = true;
|
|
4025
|
+
const r = await apiCall(`/pages/${args.page_id}/add-element`, 'POST', body);
|
|
3431
4026
|
if (r.code || r.error) return ok(`Failed: ${r.message || r.error || 'Unknown error'}`);
|
|
3432
4027
|
return ok(`Element added!\nPage: ${r.page_id}\nParent: ${r.parent_id}\nNew element ID: ${r.element_id}${r.position !== null && r.position !== undefined ? `\nPosition: ${r.position}` : ' (appended)'}`);
|
|
3433
4028
|
}
|
|
@@ -3481,9 +4076,9 @@ async function handleToolCall(name, args) {
|
|
|
3481
4076
|
settings: deepStripCDATA(settings)
|
|
3482
4077
|
};
|
|
3483
4078
|
});
|
|
3484
|
-
const
|
|
3485
|
-
|
|
3486
|
-
|
|
4079
|
+
const batchBody = { updates: cleanUpdates };
|
|
4080
|
+
if (args.force) batchBody.force = true;
|
|
4081
|
+
const r = await apiCall('/batch-update-elements', 'POST', batchBody);
|
|
3487
4082
|
if (r.code || r.error) return ok(`Failed: ${r.message || r.error || 'Unknown error'}`);
|
|
3488
4083
|
let out = `=== Batch Update ===\nTotal: ${r.total} | Succeeded: ${r.succeeded} | Failed: ${r.failed}\n`;
|
|
3489
4084
|
if (r.failed > 0) {
|
|
@@ -3650,13 +4245,396 @@ async function handleToolCall(name, args) {
|
|
|
3650
4245
|
}
|
|
3651
4246
|
}
|
|
3652
4247
|
|
|
4248
|
+
case 'audit_db_health': {
|
|
4249
|
+
const r = await apiCall('/audit-db-health');
|
|
4250
|
+
if (r.code || r.error) return ok(`Failed: ${r.message || r.error}`);
|
|
4251
|
+
let out = `=== DB HEALTH AUDIT ===\n`;
|
|
4252
|
+
out += `\nRevisions: ${r.revisions.count}`;
|
|
4253
|
+
if (r.revisions.top_pages?.length) {
|
|
4254
|
+
out += ` (top pages by revision count)\n`;
|
|
4255
|
+
r.revisions.top_pages.forEach(p => {
|
|
4256
|
+
out += ` "${p.post_title}" (ID:${p.ID}) — ${p.revision_count} revisions\n`;
|
|
4257
|
+
});
|
|
4258
|
+
} else out += '\n';
|
|
4259
|
+
out += `\nExpired Transients: ${r.expired_transients.count}\n`;
|
|
4260
|
+
out += `\nAutoloaded Options: ${r.autoloaded_options.count} options, ${r.autoloaded_options.total_size_kb} KB total\n`;
|
|
4261
|
+
if (r.autoloaded_options.top_10?.length) {
|
|
4262
|
+
out += ` Top 10 largest:\n`;
|
|
4263
|
+
r.autoloaded_options.top_10.forEach(o => {
|
|
4264
|
+
out += ` ${o.size_kb} KB — ${o.name}\n`;
|
|
4265
|
+
});
|
|
4266
|
+
}
|
|
4267
|
+
out += `\nOrphaned Postmeta: ${r.orphaned_postmeta.count} rows\n`;
|
|
4268
|
+
if (r.recommendations?.length) {
|
|
4269
|
+
out += `\nRecommendations:\n`;
|
|
4270
|
+
r.recommendations.forEach(rec => { out += ` ⚠ ${rec}\n`; });
|
|
4271
|
+
} else {
|
|
4272
|
+
out += `\n✓ No critical issues found.\n`;
|
|
4273
|
+
}
|
|
4274
|
+
out += `\nNote: ${r.note}`;
|
|
4275
|
+
return ok(out);
|
|
4276
|
+
}
|
|
4277
|
+
|
|
4278
|
+
case 'audit_image_sizes': {
|
|
4279
|
+
const params = new URLSearchParams();
|
|
4280
|
+
if (args.page_id) params.set('page_id', args.page_id);
|
|
4281
|
+
const qs = params.toString() ? `?${params}` : '';
|
|
4282
|
+
const r = await apiCall(`/audit-image-sizes${qs}`);
|
|
4283
|
+
if (r.code || r.error) return ok(`Failed: ${r.message || r.error}`);
|
|
4284
|
+
const s = r.summary;
|
|
4285
|
+
let out = `=== IMAGE SIZE AUDIT ===\n`;
|
|
4286
|
+
out += `Pages scanned: ${s.pages_scanned} | Images checked: ${s.images_checked}\n`;
|
|
4287
|
+
out += `Pages with issues: ${s.pages_with_issues} | Total flagged: ${s.total_flagged}\n`;
|
|
4288
|
+
if (!r.flagged_pages?.length) {
|
|
4289
|
+
out += `\n✓ No oversized images found.\n`;
|
|
4290
|
+
} else {
|
|
4291
|
+
r.flagged_pages.forEach(p => {
|
|
4292
|
+
out += `\n[${p.page_id}] ${p.page_title}\n ${p.url}\n`;
|
|
4293
|
+
p.images.forEach(img => {
|
|
4294
|
+
out += ` • [${img.element_id}] ${img.widget_type} — size: ${img.current_size}`;
|
|
4295
|
+
if (img.file_dimensions) out += ` | file: ${img.file_dimensions}`;
|
|
4296
|
+
if (img.file_size_kb) out += ` (${img.file_size_kb} KB)`;
|
|
4297
|
+
out += `\n → ${img.recommended}\n`;
|
|
4298
|
+
if (img.image_url) out += ` ${img.image_url}\n`;
|
|
4299
|
+
});
|
|
4300
|
+
});
|
|
4301
|
+
}
|
|
4302
|
+
out += `\nNote: ${r.note}`;
|
|
4303
|
+
return ok(out);
|
|
4304
|
+
}
|
|
4305
|
+
|
|
4306
|
+
case 'fix_image_sizes': {
|
|
4307
|
+
const body = {};
|
|
4308
|
+
if (args.page_id) body.page_id = args.page_id;
|
|
4309
|
+
if (args.target_size) body.target_size = args.target_size;
|
|
4310
|
+
if (args.dry_run !== undefined) body.dry_run = args.dry_run;
|
|
4311
|
+
if (args.exclude_ids?.length) body.exclude_ids = args.exclude_ids;
|
|
4312
|
+
if (args.force) body.force = args.force;
|
|
4313
|
+
const r = await apiCall('/fix-image-sizes', 'POST', body);
|
|
4314
|
+
if (r.code || r.error) return ok(`Failed: ${r.message || r.error}`);
|
|
4315
|
+
const s = r.summary;
|
|
4316
|
+
let out = r.dry_run ? `=== IMAGE SIZE FIX (DRY RUN) ===\n` : `=== IMAGE SIZE FIX ===\n`;
|
|
4317
|
+
out += `Target size: ${r.target_size}\n`;
|
|
4318
|
+
out += `Pages updated: ${s.pages_updated} | Images fixed: ${s.total_images_fixed}`;
|
|
4319
|
+
if (s.pages_skipped) out += ` | Pages skipped (locked): ${s.pages_skipped}`;
|
|
4320
|
+
out += '\n';
|
|
4321
|
+
if (r.pages?.length) {
|
|
4322
|
+
r.pages.forEach(p => {
|
|
4323
|
+
out += `\n[${p.page_id}] ${p.page_title}${p.saved ? '' : ' (not saved)'}\n`;
|
|
4324
|
+
p.changes.forEach(c => {
|
|
4325
|
+
out += ` • [${c.element_id}] ${c.widget_type}: ${c.from} → ${c.to}\n`;
|
|
4326
|
+
});
|
|
4327
|
+
});
|
|
4328
|
+
}
|
|
4329
|
+
if (r.skipped?.length) {
|
|
4330
|
+
out += `\nSkipped (locked):\n`;
|
|
4331
|
+
r.skipped.forEach(p => { out += ` • [${p.page_id}] ${p.title}: ${p.reason}\n`; });
|
|
4332
|
+
}
|
|
4333
|
+
out += `\n${r.note}`;
|
|
4334
|
+
return ok(out);
|
|
4335
|
+
}
|
|
4336
|
+
|
|
4337
|
+
case 'search_post_content': {
|
|
4338
|
+
const params = new URLSearchParams();
|
|
4339
|
+
params.set('search', args.search);
|
|
4340
|
+
if (args.post_type) params.set('post_type', args.post_type);
|
|
4341
|
+
if (args.limit) params.set('limit', args.limit);
|
|
4342
|
+
if (args.extract_urls) params.set('extract_urls', '1');
|
|
4343
|
+
const r = await apiCall(`/search-post-content?${params}`);
|
|
4344
|
+
if (r.code || r.error) return ok(`Failed: ${r.message || r.error}`);
|
|
4345
|
+
const s = r.summary;
|
|
4346
|
+
let out = `=== SEARCH POST CONTENT ===\nSearch: "${r.search}"\nPosts matched: ${s.posts_matched} | Total matches: ${s.total_matches}\n`;
|
|
4347
|
+
if (s.unique_url_count !== undefined) out += `Unique URLs: ${s.unique_url_count}\n`;
|
|
4348
|
+
if (r.matches?.length) {
|
|
4349
|
+
r.matches.forEach(m => {
|
|
4350
|
+
out += `\n[${m.post_id}] ${m.post_title} (${m.post_type}) — ${m.match_count} match(es)\n`;
|
|
4351
|
+
out += ` URL: ${m.url}\n`;
|
|
4352
|
+
if (m.extracted_urls?.length) {
|
|
4353
|
+
m.extracted_urls.forEach(u => { out += ` 🔗 ${u}\n`; });
|
|
4354
|
+
} else {
|
|
4355
|
+
m.snippets.forEach(sn => { out += ` > ${sn}\n`; });
|
|
4356
|
+
}
|
|
4357
|
+
});
|
|
4358
|
+
} else {
|
|
4359
|
+
out += '\nNo matches found.';
|
|
4360
|
+
}
|
|
4361
|
+
if (r.unique_urls?.length) {
|
|
4362
|
+
out += `\n\n=== ALL UNIQUE URLs (${r.unique_urls.length}) ===\n`;
|
|
4363
|
+
r.unique_urls.forEach(u => { out += `${u}\n`; });
|
|
4364
|
+
}
|
|
4365
|
+
return ok(out);
|
|
4366
|
+
}
|
|
4367
|
+
|
|
4368
|
+
case 'replace_post_content': {
|
|
4369
|
+
const body = { search: args.search, replace: args.replace };
|
|
4370
|
+
if (args.dry_run !== undefined) body.dry_run = args.dry_run;
|
|
4371
|
+
if (args.regex) body.regex = args.regex;
|
|
4372
|
+
if (args.post_type) body.post_type = args.post_type;
|
|
4373
|
+
if (args.limit) body.limit = args.limit;
|
|
4374
|
+
const r = await apiCall('/replace-post-content', 'POST', body);
|
|
4375
|
+
if (r.code || r.error) return ok(`Failed: ${r.message || r.error}`);
|
|
4376
|
+
const s = r.summary;
|
|
4377
|
+
let out = r.dry_run ? `=== REPLACE POST CONTENT (DRY RUN) ===\n` : `=== REPLACE POST CONTENT ===\n`;
|
|
4378
|
+
out += `Search: "${r.search}"\nReplace: "${r.replace}"\n`;
|
|
4379
|
+
out += `Posts changed: ${s.posts_changed} | Total replacements: ${s.total_replacements}\n`;
|
|
4380
|
+
if (r.changed_posts?.length) {
|
|
4381
|
+
r.changed_posts.forEach(p => {
|
|
4382
|
+
out += ` [${p.post_id}] ${p.post_title} (${p.post_type}) — ${p.replacements} replacement(s)\n`;
|
|
4383
|
+
});
|
|
4384
|
+
}
|
|
4385
|
+
out += `\n${r.note}`;
|
|
4386
|
+
return ok(out);
|
|
4387
|
+
}
|
|
4388
|
+
|
|
4389
|
+
case 'set_faq_schema': {
|
|
4390
|
+
const body = { post_id: args.post_id, faqs: args.faqs };
|
|
4391
|
+
if (args.dry_run !== undefined) body.dry_run = args.dry_run;
|
|
4392
|
+
const r = await apiCall('/set-faq-schema', 'POST', body);
|
|
4393
|
+
if (r.code || r.error) return ok(`Failed: ${r.message || r.error}`);
|
|
4394
|
+
let out = r.dry_run ? `=== SET FAQ SCHEMA (DRY RUN) ===\n` : `=== SET FAQ SCHEMA ===\n`;
|
|
4395
|
+
out += `Post: [${r.post_id}] ${r.post_title}\n`;
|
|
4396
|
+
out += `Questions: ${r.questions}\n`;
|
|
4397
|
+
out += `\n${r.note}`;
|
|
4398
|
+
return ok(out);
|
|
4399
|
+
}
|
|
4400
|
+
|
|
4401
|
+
case 'import_faq_schemas': {
|
|
4402
|
+
const body = { posts: args.posts };
|
|
4403
|
+
if (args.dry_run !== undefined) body.dry_run = args.dry_run;
|
|
4404
|
+
const r = await apiCall('/import-faq-schemas', 'POST', body);
|
|
4405
|
+
if (r.code || r.error) return ok(`Failed: ${r.message || r.error}`);
|
|
4406
|
+
const s = r.summary;
|
|
4407
|
+
let out = r.dry_run ? `=== IMPORT FAQ SCHEMAS (DRY RUN) ===\n` : `=== IMPORT FAQ SCHEMAS ===\n`;
|
|
4408
|
+
out += `Applied: ${s.applied} | Skipped: ${s.skipped} | Not found: ${s.not_found}\n`;
|
|
4409
|
+
if (r.results?.length) {
|
|
4410
|
+
r.results.forEach(p => {
|
|
4411
|
+
if (p.status === 'not_found') {
|
|
4412
|
+
out += ` ❌ ${p.slug} — post not found\n`;
|
|
4413
|
+
} else {
|
|
4414
|
+
out += ` ✅ [${p.post_id}] ${p.title} — ${p.questions} Q&As (${p.status})\n`;
|
|
4415
|
+
}
|
|
4416
|
+
});
|
|
4417
|
+
}
|
|
4418
|
+
out += `\n${r.note}`;
|
|
4419
|
+
return ok(out);
|
|
4420
|
+
}
|
|
4421
|
+
|
|
4422
|
+
case 'get_global_styles': {
|
|
4423
|
+
const r = await apiCall('/global-styles');
|
|
4424
|
+
if (r.code || r.error) return ok(`Failed: ${r.message || r.error}`);
|
|
4425
|
+
let out = `=== ELEMENTOR GLOBAL STYLES ===\nKit ID: ${r.kit_id}\n`;
|
|
4426
|
+
|
|
4427
|
+
out += `\n--- System Colors (${r.system_colors.length}) ---\n`;
|
|
4428
|
+
r.system_colors.forEach(c => { out += ` [${c.id}] ${c.title}: ${c.color}\n`; });
|
|
4429
|
+
|
|
4430
|
+
out += `\n--- Custom Colors (${r.custom_colors.length}) ---\n`;
|
|
4431
|
+
if (r.custom_colors.length) {
|
|
4432
|
+
r.custom_colors.forEach(c => { out += ` [${c.id}] ${c.title}: ${c.color}\n`; });
|
|
4433
|
+
} else { out += ' (none)\n'; }
|
|
4434
|
+
|
|
4435
|
+
out += `\n--- System Typography (${r.system_typography.length}) ---\n`;
|
|
4436
|
+
r.system_typography.forEach(t => {
|
|
4437
|
+
out += ` [${t.id}] ${t.title}: ${t.font_family || '(unset)'}`;
|
|
4438
|
+
if (t.font_weight) out += ` ${t.font_weight}`;
|
|
4439
|
+
if (t.font_size) out += ` ${typeof t.font_size === 'object' ? t.font_size.size + t.font_size.unit : t.font_size}`;
|
|
4440
|
+
out += '\n';
|
|
4441
|
+
});
|
|
4442
|
+
|
|
4443
|
+
out += `\n--- Custom Typography (${r.custom_typography.length}) ---\n`;
|
|
4444
|
+
if (r.custom_typography.length) {
|
|
4445
|
+
r.custom_typography.forEach(t => {
|
|
4446
|
+
out += ` [${t.id}] ${t.title}: ${t.font_family || '(unset)'}`;
|
|
4447
|
+
if (t.font_weight) out += ` ${t.font_weight}`;
|
|
4448
|
+
out += '\n';
|
|
4449
|
+
});
|
|
4450
|
+
} else { out += ' (none)\n'; }
|
|
4451
|
+
|
|
4452
|
+
return ok(out);
|
|
4453
|
+
}
|
|
4454
|
+
|
|
4455
|
+
case 'set_global_styles': {
|
|
4456
|
+
const body = {};
|
|
4457
|
+
if (args.system_colors) body.system_colors = args.system_colors;
|
|
4458
|
+
if (args.custom_colors) body.custom_colors = args.custom_colors;
|
|
4459
|
+
if (args.system_typography) body.system_typography = args.system_typography;
|
|
4460
|
+
if (args.custom_typography) body.custom_typography = args.custom_typography;
|
|
4461
|
+
if (args.dry_run !== undefined) body.dry_run = args.dry_run;
|
|
4462
|
+
const r = await apiCall('/global-styles', 'POST', body);
|
|
4463
|
+
if (r.code || r.error) return ok(`Failed: ${r.message || r.error}`);
|
|
4464
|
+
let out = r.dry_run ? `=== SET GLOBAL STYLES (DRY RUN) ===\n` : `=== SET GLOBAL STYLES ===\n`;
|
|
4465
|
+
out += `Kit ID: ${r.kit_id}\n`;
|
|
4466
|
+
if (r.changes?.length) {
|
|
4467
|
+
r.changes.forEach(c => { out += ` • ${c}\n`; });
|
|
4468
|
+
} else {
|
|
4469
|
+
out += ' No changes detected.\n';
|
|
4470
|
+
}
|
|
4471
|
+
out += `\n${r.note}`;
|
|
4472
|
+
return ok(out);
|
|
4473
|
+
}
|
|
4474
|
+
|
|
4475
|
+
case 'audit_broken_images': {
|
|
4476
|
+
const params = new URLSearchParams();
|
|
4477
|
+
if (args.page_id) params.set('page_id', args.page_id);
|
|
4478
|
+
if (args.source) params.set('source', args.source);
|
|
4479
|
+
if (args.timeout) params.set('timeout', args.timeout);
|
|
4480
|
+
const r = await apiCall(`/audit-broken-images?${params}`);
|
|
4481
|
+
if (r.code || r.error) return ok(`Failed: ${r.message || r.error}`);
|
|
4482
|
+
const s = r.summary;
|
|
4483
|
+
let out = `=== BROKEN IMAGE AUDIT ===\n`;
|
|
4484
|
+
out += `Images checked: ${s.unique_images_checked} | Broken: ${s.broken_images} | Posts affected: ${s.posts_affected}\n`;
|
|
4485
|
+
if (r.broken?.length) {
|
|
4486
|
+
r.broken.forEach(b => {
|
|
4487
|
+
out += `\n❌ ${b.url}\n`;
|
|
4488
|
+
out += ` Status: ${b.error || ('HTTP ' + b.status)}\n`;
|
|
4489
|
+
out += ` Found in:\n`;
|
|
4490
|
+
b.found_in.forEach(loc => {
|
|
4491
|
+
out += ` [${loc.post_id}] ${loc.post_title} (${loc.source})\n`;
|
|
4492
|
+
});
|
|
4493
|
+
});
|
|
4494
|
+
}
|
|
4495
|
+
out += `\n${r.note}`;
|
|
4496
|
+
return ok(out);
|
|
4497
|
+
}
|
|
4498
|
+
|
|
4499
|
+
// ── ACF Fields ──
|
|
4500
|
+
|
|
4501
|
+
case 'list_acf_field_groups': {
|
|
4502
|
+
const r = await apiCall('/acf-field-groups');
|
|
4503
|
+
if (r.code || r.error) return ok(`Failed: ${r.message || r.error}`);
|
|
4504
|
+
let out = `=== ACF FIELD GROUPS (${r.count}) ===\n`;
|
|
4505
|
+
if (!r.field_groups?.length) {
|
|
4506
|
+
out += 'No ACF field groups found.\n';
|
|
4507
|
+
} else {
|
|
4508
|
+
r.field_groups.forEach(g => {
|
|
4509
|
+
out += `\n[${g.key}] ${g.title} ${g.active ? '' : '(INACTIVE)'}\n`;
|
|
4510
|
+
if (g.post_types?.length) out += ` Post types: ${g.post_types.join(', ')}\n`;
|
|
4511
|
+
if (g.fields?.length) {
|
|
4512
|
+
g.fields.forEach(f => {
|
|
4513
|
+
out += ` • ${f.name} (${f.type}) — "${f.label}"${f.required ? ' *required' : ''}\n`;
|
|
4514
|
+
if (f.sub_fields?.length) {
|
|
4515
|
+
f.sub_fields.forEach(sf => {
|
|
4516
|
+
out += ` └ ${sf.name} (${sf.type}) — "${sf.label}"\n`;
|
|
4517
|
+
});
|
|
4518
|
+
}
|
|
4519
|
+
});
|
|
4520
|
+
}
|
|
4521
|
+
});
|
|
4522
|
+
}
|
|
4523
|
+
return ok(out);
|
|
4524
|
+
}
|
|
4525
|
+
|
|
4526
|
+
case 'get_acf_fields': {
|
|
4527
|
+
const r = await apiCall(`/acf-fields?post_id=${args.post_id}`);
|
|
4528
|
+
if (r.code || r.error) return ok(`Failed: ${r.message || r.error}`);
|
|
4529
|
+
let out = `=== ACF FIELDS: ${r.post_title} (ID:${r.post_id}) ===\n`;
|
|
4530
|
+
if (!r.fields?.length) {
|
|
4531
|
+
out += 'No ACF fields found for this post.\n';
|
|
4532
|
+
} else {
|
|
4533
|
+
r.fields.forEach(f => {
|
|
4534
|
+
const val = typeof f.value === 'object' ? JSON.stringify(f.value, null, 2) : (f.value ?? '(empty)');
|
|
4535
|
+
out += `\n ${f.name} (${f.type}): ${val}\n`;
|
|
4536
|
+
});
|
|
4537
|
+
}
|
|
4538
|
+
return ok(out);
|
|
4539
|
+
}
|
|
4540
|
+
|
|
4541
|
+
case 'set_acf_fields': {
|
|
4542
|
+
const body = { post_id: args.post_id, fields: args.fields };
|
|
4543
|
+
if (args.dry_run !== undefined) body.dry_run = args.dry_run;
|
|
4544
|
+
const r = await apiCall('/acf-fields', 'POST', body);
|
|
4545
|
+
if (r.code || r.error) return ok(`Failed: ${r.message || r.error}`);
|
|
4546
|
+
let out = r.dry_run ? `=== SET ACF FIELDS (DRY RUN) ===\n` : `=== SET ACF FIELDS ===\n`;
|
|
4547
|
+
out += `Post: [${r.post_id}] ${r.post_title}\nUpdated: ${r.updated} field(s)\n`;
|
|
4548
|
+
if (r.results?.length) {
|
|
4549
|
+
r.results.forEach(f => {
|
|
4550
|
+
const icon = (f.status === 'updated' || f.status === 'would_update') ? '+' : 'x';
|
|
4551
|
+
out += ` [${icon}] ${f.name}: ${JSON.stringify(f.old_value)} -> ${JSON.stringify(f.new_value)} (${f.status})\n`;
|
|
4552
|
+
});
|
|
4553
|
+
}
|
|
4554
|
+
out += `\n${r.note}`;
|
|
4555
|
+
return ok(out);
|
|
4556
|
+
}
|
|
4557
|
+
|
|
4558
|
+
case 'create_acf_field_group': {
|
|
4559
|
+
const body = { title: args.title, fields: args.fields };
|
|
4560
|
+
if (args.post_types) body.post_types = args.post_types;
|
|
4561
|
+
if (args.active !== undefined) body.active = args.active;
|
|
4562
|
+
const r = await apiCall('/acf-field-groups', 'POST', body);
|
|
4563
|
+
if (r.code || r.error) return ok(`Failed: ${r.message || r.error}`);
|
|
4564
|
+
let out = `=== ACF FIELD GROUP CREATED ===\n`;
|
|
4565
|
+
out += `Key: ${r.key}\nTitle: ${r.title}\nFields: ${r.field_count}\n`;
|
|
4566
|
+
if (r.post_types?.length) out += `Post types: ${r.post_types.join(', ')}\n`;
|
|
4567
|
+
return ok(out);
|
|
4568
|
+
}
|
|
4569
|
+
|
|
4570
|
+
case 'delete_acf_field': {
|
|
4571
|
+
const r = await apiCall(`/acf-field?field_key=${encodeURIComponent(args.field_key)}`, 'DELETE');
|
|
4572
|
+
if (r.code || r.error) return ok(`Failed: ${r.message || r.error}`);
|
|
4573
|
+
return ok(`Deleted ACF field: "${r.label}" (${r.name}) [${r.field_key}] from group "${r.group_title}"`);
|
|
4574
|
+
}
|
|
4575
|
+
|
|
4576
|
+
case 'audit_orphan_pages': {
|
|
4577
|
+
const r = await apiCall('/audit-orphan-pages');
|
|
4578
|
+
let msg = `=== ORPHAN PAGES ===\nTotal pages: ${r.total_pages} | Linked: ${r.linked_pages} | Orphans: ${r.orphan_count}\n`;
|
|
4579
|
+
if (r.orphans && r.orphans.length) {
|
|
4580
|
+
msg += '\nOrphan pages (no incoming internal links):\n';
|
|
4581
|
+
for (const p of r.orphans) {
|
|
4582
|
+
msg += ` - [${p.post_type}] ${p.title} (ID ${p.post_id})\n ${p.permalink}\n`;
|
|
4583
|
+
}
|
|
4584
|
+
} else {
|
|
4585
|
+
msg += '\nAll pages have incoming links — no orphans.\n';
|
|
4586
|
+
}
|
|
4587
|
+
return ok(msg);
|
|
4588
|
+
}
|
|
4589
|
+
|
|
4590
|
+
case 'audit_h1': {
|
|
4591
|
+
const r = await apiCall('/audit-h1');
|
|
4592
|
+
let msg = `=== H1 HEADING AUDIT ===\nPages scanned: ${r.pages_scanned} | Missing H1: ${r.missing_h1} | Duplicate H1: ${r.duplicate_h1}\n`;
|
|
4593
|
+
if (r.results && r.results.length) {
|
|
4594
|
+
msg += '\nPages with H1 issues:\n';
|
|
4595
|
+
for (const p of r.results) {
|
|
4596
|
+
const label = p.issue === 'missing' ? 'MISSING' : `DUPLICATE (${p.h1_count})`;
|
|
4597
|
+
msg += ` - ${p.title} (ID ${p.post_id}) — ${label}`;
|
|
4598
|
+
if (p.h1_texts && p.h1_texts.length) {
|
|
4599
|
+
msg += ': ' + p.h1_texts.join(' | ');
|
|
4600
|
+
}
|
|
4601
|
+
msg += '\n';
|
|
4602
|
+
}
|
|
4603
|
+
} else {
|
|
4604
|
+
msg += '\nAll pages have exactly one H1.\n';
|
|
4605
|
+
}
|
|
4606
|
+
return ok(msg);
|
|
4607
|
+
}
|
|
4608
|
+
|
|
4609
|
+
case 'audit_redirect_chains': {
|
|
4610
|
+
const r = await apiCall('/audit-redirect-chains');
|
|
4611
|
+
let msg = `=== REDIRECT CHAINS ===\nURLs checked: ${r.urls_checked} | Chains found: ${r.chains_found}\n`;
|
|
4612
|
+
if (r.chains && r.chains.length) {
|
|
4613
|
+
msg += '\nRedirect chains (2+ hops):\n';
|
|
4614
|
+
for (const c of r.chains) {
|
|
4615
|
+
msg += ` - ${c.original_url} → ${c.final_url} (${c.hops} hops)\n`;
|
|
4616
|
+
for (const hop of c.chain) {
|
|
4617
|
+
msg += ` ${hop.status}: ${hop.from} → ${hop.to}\n`;
|
|
4618
|
+
}
|
|
4619
|
+
if (c.locations && c.locations.length) {
|
|
4620
|
+
msg += ` Used on: ${c.locations.map(l => l.page_title).join(', ')}\n`;
|
|
4621
|
+
}
|
|
4622
|
+
}
|
|
4623
|
+
} else {
|
|
4624
|
+
msg += '\nNo redirect chains found.\n';
|
|
4625
|
+
}
|
|
4626
|
+
return ok(msg);
|
|
4627
|
+
}
|
|
4628
|
+
|
|
3653
4629
|
case 'copy_section': {
|
|
3654
|
-
const
|
|
4630
|
+
const body = {
|
|
3655
4631
|
source_id: args.source_id,
|
|
3656
4632
|
section_index: args.section_index,
|
|
3657
4633
|
target_id: args.target_id,
|
|
3658
4634
|
position: args.position || 'end',
|
|
3659
|
-
}
|
|
4635
|
+
};
|
|
4636
|
+
if (args.force) body.force = true;
|
|
4637
|
+
const r = await apiCall('/copy-section', 'POST', body);
|
|
3660
4638
|
if (r.code || r.error) return ok(`Failed: ${r.message || r.error || 'Unknown error'}`);
|
|
3661
4639
|
let msg = `Section copied!\n`;
|
|
3662
4640
|
msg += `Source: ID ${r.source_id}, section index ${r.section_index}\n`;
|
|
@@ -3669,12 +4647,15 @@ async function handleToolCall(name, args) {
|
|
|
3669
4647
|
}
|
|
3670
4648
|
|
|
3671
4649
|
case 'append_section': {
|
|
4650
|
+
// Auto-add boilerplate to all appended sections
|
|
4651
|
+
if (args.section) addBoilerplate(args.section);
|
|
3672
4652
|
const body = {
|
|
3673
4653
|
target_id: args.target_id,
|
|
3674
4654
|
section: args.section,
|
|
3675
4655
|
position: args.position || 'end',
|
|
3676
4656
|
};
|
|
3677
4657
|
if (args.regenerate_ids !== undefined) body.regenerate_ids = args.regenerate_ids;
|
|
4658
|
+
if (args.force) body.force = true;
|
|
3678
4659
|
const r = await apiCall('/append-section', 'POST', body);
|
|
3679
4660
|
if (r.code || r.error) return ok(`Failed: ${r.message || r.error || 'Unknown error'}`);
|
|
3680
4661
|
let msg = `Section appended!\n`;
|
|
@@ -3713,12 +4694,27 @@ async function handleToolCall(name, args) {
|
|
|
3713
4694
|
const catalog = getTemplateCatalog();
|
|
3714
4695
|
if (!catalog) return ok('Template library not configured. Set TEMPLATE_LIBRARY_DIR env var.');
|
|
3715
4696
|
let sections = catalog.sections;
|
|
4697
|
+
|
|
4698
|
+
// Filter out CoSpace (all legacy section/column structure) unless explicitly requested
|
|
4699
|
+
if (!args.kit_id || !args.kit_id.includes('cospace')) {
|
|
4700
|
+
sections = sections.filter(s => !s.kit_id.includes('cospace'));
|
|
4701
|
+
}
|
|
4702
|
+
// Filter out low-quality sections (score < 50) unless min_score overridden
|
|
4703
|
+
const minScore = args.min_score !== undefined ? args.min_score : 50;
|
|
4704
|
+
if (minScore > 0) {
|
|
4705
|
+
sections = sections.filter(s => (s.quality_score || 0) >= minScore);
|
|
4706
|
+
}
|
|
4707
|
+
|
|
3716
4708
|
if (args.kit_id) sections = sections.filter(s => s.kit_id === args.kit_id);
|
|
3717
4709
|
if (args.type) sections = sections.filter(s => s.type === args.type);
|
|
3718
4710
|
if (args.page_name) {
|
|
3719
4711
|
const q = args.page_name.toLowerCase();
|
|
3720
4712
|
sections = sections.filter(s => s.page_name.toLowerCase().includes(q));
|
|
3721
4713
|
}
|
|
4714
|
+
|
|
4715
|
+
// Sort by quality score descending for best-first results
|
|
4716
|
+
sections.sort((a, b) => (b.quality_score || 0) - (a.quality_score || 0));
|
|
4717
|
+
|
|
3722
4718
|
const limit = args.limit || 20;
|
|
3723
4719
|
const total = sections.length;
|
|
3724
4720
|
sections = sections.slice(0, limit);
|
|
@@ -3732,6 +4728,11 @@ async function handleToolCall(name, args) {
|
|
|
3732
4728
|
msg += ` Type: ${s.type} (confidence: ${s.confidence})`;
|
|
3733
4729
|
if (s.secondary_type) msg += ` | also: ${s.secondary_type}`;
|
|
3734
4730
|
msg += '\n';
|
|
4731
|
+
if (s.quality_score !== undefined) {
|
|
4732
|
+
msg += ` Quality: ${s.quality_score}/100`;
|
|
4733
|
+
if (s.quality_flags && s.quality_flags.length) msg += ` [${s.quality_flags.join(', ')}]`;
|
|
4734
|
+
msg += '\n';
|
|
4735
|
+
}
|
|
3735
4736
|
const widgetSummary = Object.entries(s.features.widgets).map(([w,c]) => `${w}:${c}`).join(', ');
|
|
3736
4737
|
msg += ` Widgets: ${widgetSummary || '(none)'}\n`;
|
|
3737
4738
|
msg += ` Elements: ${s.features.total_elements} | Columns: ${s.features.column_count} | Depth: ${s.features.max_depth}\n`;
|
|
@@ -4077,6 +5078,9 @@ async function handleToolCall(name, args) {
|
|
|
4077
5078
|
}
|
|
4078
5079
|
}
|
|
4079
5080
|
|
|
5081
|
+
// Add site-specific boilerplate (jet_parallax, plus_tooltip, full_width)
|
|
5082
|
+
addBoilerplate(final);
|
|
5083
|
+
|
|
4080
5084
|
// Append to page
|
|
4081
5085
|
try {
|
|
4082
5086
|
const appendResult = await apiCall('/append-section', 'POST', {
|
|
@@ -4166,6 +5170,25 @@ async function handleToolCall(name, args) {
|
|
|
4166
5170
|
return ok(r.message);
|
|
4167
5171
|
}
|
|
4168
5172
|
|
|
5173
|
+
case 'scan_shortcodes': {
|
|
5174
|
+
const r = await apiCall('/scan-shortcodes');
|
|
5175
|
+
let msg = `Shortcode scan: ${r.posts_scanned} posts scanned, ${r.posts_with_shortcodes} using shortcodes.\n\n`;
|
|
5176
|
+
msg += 'Summary:\n';
|
|
5177
|
+
for (const [sc, count] of Object.entries(r.shortcode_summary || {})) {
|
|
5178
|
+
msg += ` [${sc}]: ${count} page(s)\n`;
|
|
5179
|
+
}
|
|
5180
|
+
if (r.results && r.results.length) {
|
|
5181
|
+
msg += '\nPages with shortcodes:\n';
|
|
5182
|
+
for (const p of r.results) {
|
|
5183
|
+
msg += ` - ${p.title} (${p.post_type}, ID ${p.post_id}): ${p.shortcodes_found.join(', ')} [${p.locations.join(', ')}]\n`;
|
|
5184
|
+
msg += ` Edit: ${p.edit_url}\n`;
|
|
5185
|
+
}
|
|
5186
|
+
} else {
|
|
5187
|
+
msg += '\nNo pages use NVBC shortcodes — safe to disable.\n';
|
|
5188
|
+
}
|
|
5189
|
+
return ok(msg);
|
|
5190
|
+
}
|
|
5191
|
+
|
|
4169
5192
|
case 'get_site_profile': {
|
|
4170
5193
|
const r = await apiCall('/site-profile');
|
|
4171
5194
|
let msg = '=== SITE PROFILE ===\n\n';
|
|
@@ -4286,6 +5309,22 @@ async function handleToolCall(name, args) {
|
|
|
4286
5309
|
return ok(r.message || 'Audit started. Call get_audit_report in ~60s for results.');
|
|
4287
5310
|
}
|
|
4288
5311
|
|
|
5312
|
+
case 'clean_globals': {
|
|
5313
|
+
const r = await apiCall(`/pages/${args.page_id}/clean-globals`, 'POST', {
|
|
5314
|
+
dry_run: args.dry_run !== undefined ? parseBool(args.dry_run) : true,
|
|
5315
|
+
force: parseBool(args.force),
|
|
5316
|
+
});
|
|
5317
|
+
return ok(`${r.dry_run ? '[DRY RUN] ' : ''}Cleaned ${r.foreign_globals_cleaned} foreign global references on page ${r.page_id}\nValid color IDs: ${r.valid_color_ids.join(', ')}\nValid font IDs: ${r.valid_font_ids.join(', ')}`);
|
|
5318
|
+
}
|
|
5319
|
+
|
|
5320
|
+
case 'set_front_page': {
|
|
5321
|
+
const r = await apiCall('/set-front-page', 'POST', {
|
|
5322
|
+
page_id: args.page_id,
|
|
5323
|
+
blog_page_id: args.blog_page_id,
|
|
5324
|
+
});
|
|
5325
|
+
return ok(`Front page set to "${r.front_page_title}" (ID: ${r.front_page_id})${r.blog_page_id ? `\nBlog page: ${r.blog_page_id}` : ''}`);
|
|
5326
|
+
}
|
|
5327
|
+
|
|
4289
5328
|
default:
|
|
4290
5329
|
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
|
|
4291
5330
|
}
|