bricks-builder-mcp 3.10.0 → 3.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +8 -0
  2. package/package.json +1 -1
  3. package/server.js +313 -0
package/README.md CHANGED
@@ -117,6 +117,14 @@ Claude Code détectera automatiquement le skill au prochain démarrage.
117
117
  - **v3.10 — Santé des médias** : `naturalWidth > 0` sur les `<img>` (lazy-load cassé), `readyState ≥ 2` sur les `<video>`, alt présents
118
118
  - Chaque check porte une `severity` (`critical` / `warning` / `info`) et un `hint` actionnable. Désactivable par catégorie via `checks: { sibling_coherence: false }`.
119
119
 
120
+ **⭐ Audit page entière (v3.11+)**
121
+ - `audit_page` — fullpage screenshot **avec annotations dessinées** + report consolidé. Complète `verify_element` (qui est ciblé sur 1 élément).
122
+ - Scanne TOUS les éléments Bricks d'une page d'un coup
123
+ - Dessine des cadres colorés (rouge=critical, orange=warning, jaune=info) sur les zones problématiques directement sur le screenshot
124
+ - Multi-viewport en 1 call (par défaut `["desktop", "mobile_portrait"]`)
125
+ - Idéal pour : état initial avant refonte, démo client, audit après une grosse vague de modifs
126
+ - Checks intégrés : containers vides, images cassées + alt manquants, text-align mixé, débordement horizontal global de la page
127
+
120
128
  **⭐ Upload optimisé (v3.8+)**
121
129
  - `upload_local_file`, `upload_local_files_batch` — l'IA donne juste le path local, le MCP server lit le fichier, conversion WebP automatique (-80 à -95% de poids), renommage SEO via le `title`
122
130
  - `upload_media`, `upload_media_batch` (accepte URL HTTP/HTTPS **ou** data URI)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "bricks-builder-mcp",
4
- "version": "3.10.0",
4
+ "version": "3.11.1",
5
5
  "description": "Serveur MCP pour piloter Bricks Builder (WordPress) depuis Claude — édition de pages, gestion d'éléments, réordonnancement des sections, vérification visuelle (verify_element), upload optimisé WebP. Communauté Discord : https://discord.gg/rX22zHRzH",
6
6
  "homepage": "https://discord.gg/rX22zHRzH",
7
7
  "main": "server.js",
package/server.js CHANGED
@@ -1142,6 +1142,35 @@ mcpServer.setRequestHandler(ListToolsRequestSchema, async () => {
1142
1142
  },
1143
1143
  },
1144
1144
 
1145
+ // ===== v3.11 — AUDIT_PAGE (fullpage screenshot + annotations + report consolidé) =====
1146
+ {
1147
+ name: "audit_page",
1148
+ description: "⭐ AUDIT GLOBAL d'une page Bricks en 1 call. Lance un browser headless, charge la page, scanne TOUS les éléments Bricks visibles, dessine des cadres colorés sur le screenshot fullpage là où il y a un problème (rouge=critical, orange=warning, jaune=info), et retourne un report consolidé avec severityCounts. Idéal pour un coup d'œil rapide 'qu'est-ce qui cloche sur cette page ?' avant ou après une refonte. Complète verify_element (qui est ciblé sur 1 élément).",
1149
+ inputSchema: {
1150
+ type: "object",
1151
+ properties: {
1152
+ pageId: { type: "number", description: "L'ID de la page WP à auditer" },
1153
+ viewports: {
1154
+ type: "array",
1155
+ items: { type: "string", enum: ["desktop", "tablet", "mobile_landscape", "mobile_portrait"] },
1156
+ description: "Viewports à tester (défaut: ['desktop', 'mobile_portrait']). Renvoie un screenshot annoté + un report par viewport.",
1157
+ },
1158
+ checks: {
1159
+ type: "object",
1160
+ description: "Activer/désactiver les catégories de checks. Toutes activées par défaut.",
1161
+ properties: {
1162
+ empty_containers: { type: "boolean", description: "Containers Bricks ≥ 50×50px sans contenu (défaut: true)" },
1163
+ media_health: { type: "boolean", description: "Images cassées (naturalWidth=0) + alt manquant (défaut: true)" },
1164
+ sibling_coherence: { type: "boolean", description: "text-align mixé entre frères Bricks (défaut: true)" },
1165
+ page_overflow: { type: "boolean", description: "Débordement horizontal global de la page (défaut: true)" },
1166
+ },
1167
+ },
1168
+ maxAnnotations: { type: "number", description: "Limite d'annotations dessinées sur le screenshot (défaut: 30, pour éviter d'écraser visuellement la page)" },
1169
+ },
1170
+ required: ["pageId"],
1171
+ },
1172
+ },
1173
+
1145
1174
  // ===== v3.6.0 — FEEDBACK SYSTEM (missing MCP features) =====
1146
1175
  {
1147
1176
  name: "report_missing_feature",
@@ -2173,6 +2202,290 @@ mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
2173
2202
  return { content: responseContent };
2174
2203
  }
2175
2204
 
2205
+ // ===== v3.11 — AUDIT_PAGE (fullpage + annotations + report) =====
2206
+ case "audit_page": {
2207
+ const pageId = args.pageId;
2208
+ const viewportList = (Array.isArray(args.viewports) && args.viewports.length > 0)
2209
+ ? args.viewports
2210
+ : ["desktop", "mobile_portrait"];
2211
+ const checksConfig = Object.assign({
2212
+ empty_containers: true,
2213
+ media_health: true,
2214
+ sibling_coherence: true,
2215
+ page_overflow: true,
2216
+ }, args.checks || {});
2217
+ const maxAnnotations = typeof args.maxAnnotations === 'number' ? args.maxAnnotations : 30;
2218
+ const viewportSizes = {
2219
+ desktop: { width: 1920, height: 1080 },
2220
+ tablet: { width: 991, height: 1200 },
2221
+ mobile_landscape: { width: 767, height: 600 },
2222
+ mobile_portrait: { width: 478, height: 800 },
2223
+ };
2224
+
2225
+ // 1) Récupérer l'URL de la page via /list-pages (pas de plugin update nécessaire)
2226
+ const allPages = await callWordPressAPI("/list-pages", "GET");
2227
+ const pageMeta = (Array.isArray(allPages) ? allPages : []).find(p => p.id === pageId);
2228
+ if (!pageMeta) {
2229
+ return { content: [{ type: "text", text: JSON.stringify({ success: false, error: `Page ${pageId} introuvable dans list_bricks_pages`, hint: "Vérifie l'ID via list_bricks_pages." }, null, 2) }] };
2230
+ }
2231
+ const pageUrl = pageMeta.url;
2232
+
2233
+ // 2) Boucler sur chaque viewport
2234
+ const perViewport = [];
2235
+
2236
+ for (const viewport of viewportList) {
2237
+ let page;
2238
+ try {
2239
+ page = await getNewPage(viewport);
2240
+ } catch (browserErr) {
2241
+ perViewport.push({
2242
+ viewport,
2243
+ success: false,
2244
+ error: browserErr.message,
2245
+ hint: "Installation Chromium requise pour audit_page. Lance : npx playwright install chromium",
2246
+ });
2247
+ continue;
2248
+ }
2249
+
2250
+ try {
2251
+ await page.goto(pageUrl, { waitUntil: 'domcontentloaded', timeout: 30000 });
2252
+ // Wait : fonts + lazy load (networkidle pour les images lazy)
2253
+ try {
2254
+ await page.waitForLoadState('networkidle', { timeout: 8000 });
2255
+ } catch {} // pas grave si certaines requêtes restent en cours
2256
+ await page.waitForTimeout(1500);
2257
+ // Scroll bottom puis top pour déclencher les lazy-load
2258
+ await page.evaluate(() => {
2259
+ return new Promise(resolve => {
2260
+ const totalHeight = document.documentElement.scrollHeight;
2261
+ let scrolled = 0;
2262
+ const step = 400;
2263
+ const timer = setInterval(() => {
2264
+ window.scrollBy(0, step);
2265
+ scrolled += step;
2266
+ if (scrolled >= totalHeight) {
2267
+ clearInterval(timer);
2268
+ window.scrollTo(0, 0);
2269
+ setTimeout(resolve, 800);
2270
+ }
2271
+ }, 100);
2272
+ });
2273
+ });
2274
+
2275
+ // 3) Collecte d'issues globales sur toute la page
2276
+ const auditResult = await page.evaluate((cfg) => {
2277
+ const issues = [];
2278
+ const allBrxe = Array.from(document.querySelectorAll('[id^="brxe-"], [class*="brxe-"]'));
2279
+
2280
+ // Helper : bbox en coordonnées absolues (page entière, pas viewport)
2281
+ const absBbox = (rect) => ({
2282
+ x: Math.round(rect.left + window.scrollX),
2283
+ y: Math.round(rect.top + window.scrollY),
2284
+ w: Math.round(rect.width),
2285
+ h: Math.round(rect.height),
2286
+ });
2287
+
2288
+ // === Empty containers ===
2289
+ if (cfg.empty_containers) {
2290
+ allBrxe.forEach(b => {
2291
+ const rect = b.getBoundingClientRect();
2292
+ if (rect.width < 50 || rect.height < 50) return;
2293
+ const area = rect.width * rect.height;
2294
+ if (area < 2500) return;
2295
+ // Ignorer les éléments en dehors du viewport rendu (hauteur fullpage)
2296
+ const hasText = (b.textContent || '').trim().length > 0;
2297
+ const hasMedia = b.querySelector('img, picture, svg, video, iframe') !== null;
2298
+ const hasInteractive = b.querySelector('a, button, input, select, textarea') !== null;
2299
+ const bcs = getComputedStyle(b);
2300
+ const hasBgImg = bcs.backgroundImage && bcs.backgroundImage !== 'none';
2301
+ if (!hasText && !hasMedia && !hasInteractive && !hasBgImg) {
2302
+ issues.push({
2303
+ type: 'empty_container',
2304
+ severity: 'warning',
2305
+ element: b.id || (Array.from(b.classList).find(c => c.startsWith('brxe-')) || '?'),
2306
+ label: `Container vide ${Math.round(rect.width)}×${Math.round(rect.height)}px`,
2307
+ hint: "Bloc visible sans contenu — souvent un wrapper écrasé par align-items: stretch. Fixer aspect-ratio ou ajouter du contenu.",
2308
+ bbox: absBbox(rect),
2309
+ });
2310
+ }
2311
+ });
2312
+ }
2313
+
2314
+ // === Media health ===
2315
+ if (cfg.media_health) {
2316
+ const imgs = Array.from(document.querySelectorAll('img'));
2317
+ imgs.forEach(img => {
2318
+ const rect = img.getBoundingClientRect();
2319
+ if (rect.width < 1 || rect.height < 1) return;
2320
+ if (!img.naturalWidth) {
2321
+ issues.push({
2322
+ type: 'broken_image',
2323
+ severity: 'critical',
2324
+ element: (img.src || '').split('/').pop() || '(no src)',
2325
+ label: `Image non chargée : ${(img.src || '').split('/').pop() || '(no src)'}`,
2326
+ hint: "naturalWidth = 0 — lazy-load non déclenché ou 404. Vérifie l'URL src et la stratégie de chargement.",
2327
+ bbox: absBbox(rect),
2328
+ });
2329
+ } else if (!img.alt || !img.alt.trim()) {
2330
+ issues.push({
2331
+ type: 'no_alt',
2332
+ severity: 'warning',
2333
+ element: (img.src || '').split('/').pop() || '(no src)',
2334
+ label: `alt manquant sur image`,
2335
+ hint: "Renseigne alt à l'upload (upload_local_file({alt})) — accessibilité + SEO.",
2336
+ bbox: absBbox(rect),
2337
+ });
2338
+ }
2339
+ });
2340
+ }
2341
+
2342
+ // === Sibling coherence ===
2343
+ if (cfg.sibling_coherence) {
2344
+ allBrxe.forEach(parent => {
2345
+ const realChildren = Array.from(parent.children).filter(c =>
2346
+ (c.id && c.id.startsWith('brxe-')) ||
2347
+ Array.from(c.classList).some(cls => cls.startsWith('brxe-'))
2348
+ );
2349
+ if (realChildren.length < 2) return;
2350
+ const aligns = [...new Set(realChildren.map(c => getComputedStyle(c).textAlign))];
2351
+ if (aligns.length > 1) {
2352
+ const rect = parent.getBoundingClientRect();
2353
+ issues.push({
2354
+ type: 'mixed_text_align',
2355
+ severity: 'warning',
2356
+ element: parent.id || (Array.from(parent.classList).find(c => c.startsWith('brxe-')) || '?'),
2357
+ label: `text-align mixé entre frères (${aligns.join(', ')})`,
2358
+ hint: "Frères directs avec text-align différents — souvent un bug visuel. Aligner ou justifier le choix.",
2359
+ bbox: absBbox(rect),
2360
+ });
2361
+ }
2362
+ });
2363
+ }
2364
+
2365
+ // === Page overflow X ===
2366
+ if (cfg.page_overflow) {
2367
+ const doc = document.documentElement;
2368
+ if (doc.scrollWidth > doc.clientWidth + 1) {
2369
+ issues.push({
2370
+ type: 'page_overflow_x',
2371
+ severity: 'critical',
2372
+ element: 'document',
2373
+ label: `Débordement horizontal de la page (${doc.scrollWidth}px > ${doc.clientWidth}px)`,
2374
+ hint: "Un élément dépasse en largeur. Cherche les _widthMax manquants ou un white-space:nowrap.",
2375
+ bbox: null,
2376
+ });
2377
+ }
2378
+ }
2379
+
2380
+ return {
2381
+ issues,
2382
+ pageDimensions: {
2383
+ width: document.documentElement.clientWidth,
2384
+ height: document.documentElement.scrollHeight,
2385
+ },
2386
+ totalBrxe: allBrxe.length,
2387
+ };
2388
+ }, checksConfig);
2389
+
2390
+ // 4) Limiter les annotations (les plus sévères en premier)
2391
+ const severityRank = { critical: 0, warning: 1, info: 2 };
2392
+ const annotatable = auditResult.issues
2393
+ .filter(i => i.bbox)
2394
+ .sort((a, b) => (severityRank[a.severity] ?? 9) - (severityRank[b.severity] ?? 9))
2395
+ .slice(0, maxAnnotations);
2396
+
2397
+ // 5) Injecter les overlays dans le DOM puis screenshot fullpage
2398
+ await page.evaluate((annotations) => {
2399
+ const colors = { critical: '#ef4444', warning: '#f59e0b', info: '#facc15' };
2400
+ const labelBg = { critical: 'rgba(239, 68, 68, 0.9)', warning: 'rgba(245, 158, 11, 0.9)', info: 'rgba(250, 204, 21, 0.9)' };
2401
+ annotations.forEach((ann, idx) => {
2402
+ const overlay = document.createElement('div');
2403
+ overlay.style.cssText = `
2404
+ position: absolute;
2405
+ left: ${ann.bbox.x}px;
2406
+ top: ${ann.bbox.y}px;
2407
+ width: ${ann.bbox.w}px;
2408
+ height: ${ann.bbox.h}px;
2409
+ border: 3px solid ${colors[ann.severity] || '#888'};
2410
+ pointer-events: none;
2411
+ z-index: 2147483646;
2412
+ box-sizing: border-box;
2413
+ `;
2414
+ document.body.appendChild(overlay);
2415
+
2416
+ // Petit label en haut à gauche du cadre avec le numéro
2417
+ const label = document.createElement('div');
2418
+ label.textContent = String(idx + 1);
2419
+ label.style.cssText = `
2420
+ position: absolute;
2421
+ left: ${ann.bbox.x}px;
2422
+ top: ${Math.max(0, ann.bbox.y - 24)}px;
2423
+ background: ${labelBg[ann.severity] || 'rgba(136,136,136,0.9)'};
2424
+ color: white;
2425
+ font-family: system-ui, sans-serif;
2426
+ font-size: 14px;
2427
+ font-weight: 700;
2428
+ padding: 2px 8px;
2429
+ border-radius: 3px;
2430
+ pointer-events: none;
2431
+ z-index: 2147483647;
2432
+ `;
2433
+ document.body.appendChild(label);
2434
+ });
2435
+ }, annotatable);
2436
+
2437
+ // Fullpage screenshot
2438
+ const buf = await page.screenshot({ type: 'jpeg', quality: 80, fullPage: true });
2439
+ const screenshotBase64 = buf.toString('base64');
2440
+
2441
+ // 6) Aggrégation
2442
+ const counts = { critical: 0, warning: 0, info: 0 };
2443
+ auditResult.issues.forEach(i => {
2444
+ counts[i.severity] = (counts[i.severity] || 0) + 1;
2445
+ });
2446
+
2447
+ perViewport.push({
2448
+ viewport,
2449
+ success: true,
2450
+ pageDimensions: auditResult.pageDimensions,
2451
+ totalBrxeElements: auditResult.totalBrxe,
2452
+ severityCounts: counts,
2453
+ totalIssues: auditResult.issues.length,
2454
+ annotated: annotatable.length,
2455
+ // On numérote les issues annotées pour matcher avec les cadres sur le screenshot
2456
+ issues: auditResult.issues.map((iss, idx) => {
2457
+ const annIdx = annotatable.indexOf(iss);
2458
+ return annIdx >= 0 ? { ...iss, annotationNumber: annIdx + 1 } : iss;
2459
+ }),
2460
+ screenshotBase64,
2461
+ });
2462
+ } finally {
2463
+ if (page && !page.isClosed()) {
2464
+ await page.close().catch(() => {});
2465
+ }
2466
+ }
2467
+ }
2468
+
2469
+ // 7) Réponse MCP : 1 JSON global + 1 image par viewport
2470
+ const responseContent = [];
2471
+ const summary = {
2472
+ pageId,
2473
+ pageUrl,
2474
+ pageTitle: pageMeta.title,
2475
+ viewports: perViewport.map(vr => {
2476
+ const { screenshotBase64: _drop, ...rest } = vr;
2477
+ return rest;
2478
+ }),
2479
+ };
2480
+ responseContent.push({ type: "text", text: JSON.stringify(summary, null, 2) });
2481
+ for (const vr of perViewport) {
2482
+ if (vr.screenshotBase64) {
2483
+ responseContent.push({ type: "image", data: vr.screenshotBase64, mimeType: "image/jpeg" });
2484
+ }
2485
+ }
2486
+ return { content: responseContent };
2487
+ }
2488
+
2176
2489
  // ===== v3.6.0 — FEEDBACK SYSTEM =====
2177
2490
  case "report_missing_feature":
2178
2491
  result = await callWordPressAPI("/report-missing-feature", "POST", {