bricks-builder-mcp 3.5.0 → 3.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +5 -3
  2. package/server.js +442 -5
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "bricks-builder-mcp",
4
- "version": "3.5.0",
5
- "description": "Serveur MCP pour piloter Bricks Builder (WordPress) depuis Claude — édition de pages, gestion d'éléments, réordonnancement des sections.",
4
+ "version": "3.6.0",
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 + technique (verify_element).",
6
6
  "main": "server.js",
7
7
  "bin": {
8
8
  "bricks-builder-mcp": "./server.js"
@@ -13,6 +13,7 @@
13
13
  ],
14
14
  "scripts": {
15
15
  "start": "node server.js",
16
+ "postinstall": "node -e \"console.log('\\n[bricks-builder-mcp] Pour activer verify_element, lance UNE FOIS :\\n npx playwright install chromium\\n')\"",
16
17
  "test": "echo \"No tests yet\" && exit 0"
17
18
  },
18
19
  "keywords": [
@@ -28,6 +29,7 @@
28
29
  "node": ">=18"
29
30
  },
30
31
  "dependencies": {
31
- "@modelcontextprotocol/sdk": "^1.23.0"
32
+ "@modelcontextprotocol/sdk": "^1.23.0",
33
+ "playwright-core": "^1.50.0"
32
34
  }
33
35
  }
package/server.js CHANGED
@@ -10,6 +10,112 @@ import fs from 'fs';
10
10
  import { fileURLToPath } from 'url';
11
11
  import { dirname, join } from 'path';
12
12
 
13
+ // ===== v3.6.0 — Playwright (lazy import pour verify_element) =====
14
+ // Chargé à la demande pour ne pas crasher si chromium pas installé.
15
+ let _chromium = null;
16
+ let _browser = null;
17
+ let _browserContext = null;
18
+
19
+ async function getChromium() {
20
+ if (_chromium) return _chromium;
21
+ try {
22
+ const mod = await import('playwright-core');
23
+ _chromium = mod.chromium;
24
+ return _chromium;
25
+ } catch (err) {
26
+ throw new Error(
27
+ "playwright-core introuvable. Réinstalle le package : npm i -g bricks-builder-mcp"
28
+ );
29
+ }
30
+ }
31
+
32
+ async function getBrowser() {
33
+ if (_browser && _browser.isConnected()) return _browser;
34
+ const chromium = await getChromium();
35
+ try {
36
+ _browser = await chromium.launch({ headless: true });
37
+ logToFile('[VERIFY] Browser Chromium lancé');
38
+ return _browser;
39
+ } catch (err) {
40
+ // Chromium pas installé
41
+ throw new Error(
42
+ "Chromium n'est pas installé pour Playwright.\n" +
43
+ "Lance UNE FOIS dans un terminal : npx playwright install chromium\n" +
44
+ "Détail : " + err.message
45
+ );
46
+ }
47
+ }
48
+
49
+ async function getNewPage(viewport) {
50
+ const browser = await getBrowser();
51
+ const sizes = {
52
+ desktop: { width: 1920, height: 1080 },
53
+ tablet: { width: 991, height: 1200 },
54
+ mobile_landscape: { width: 767, height: 600 },
55
+ mobile_portrait: { width: 478, height: 800 },
56
+ };
57
+ const size = sizes[viewport] || sizes.desktop;
58
+ if (!_browserContext) {
59
+ _browserContext = await browser.newContext({
60
+ viewport: size,
61
+ // Ignore SSL errors pour les sites pré-prod (cohérent avec INSECURE_SSL)
62
+ ignoreHTTPSErrors: INSECURE_SSL,
63
+ });
64
+ } else {
65
+ // Adapter le viewport si différent
66
+ }
67
+ const page = await _browserContext.newPage();
68
+ await page.setViewportSize(size);
69
+ return page;
70
+ }
71
+
72
+ // Cleanup à exit
73
+ process.on('exit', () => {
74
+ if (_browser) {
75
+ try { _browser.close(); } catch {}
76
+ }
77
+ });
78
+ process.on('SIGINT', () => {
79
+ if (_browser) {
80
+ try { _browser.close(); } catch {}
81
+ }
82
+ process.exit(0);
83
+ });
84
+
85
+ // Normaliser les valeurs CSS pour comparaison (rgba/spaces/units)
86
+ function normaliseCssValue(val) {
87
+ if (val == null) return '';
88
+ let s = String(val).trim().toLowerCase();
89
+ // Supprime espaces internes (rgba(0, 0, 0) → rgba(0,0,0))
90
+ s = s.replace(/\s+/g, '');
91
+ // Normalise "0px" et "0" et "0%"
92
+ if (s === '0' || s === '0px' || s === '0%') return '0';
93
+ // Si valeur sans unité fournie (ex "32") et getComputedStyle renvoie "32px"
94
+ if (/^\d+(\.\d+)?$/.test(s)) s += 'px';
95
+ return s;
96
+ }
97
+
98
+ function generateHint(prop, expected, got) {
99
+ if (prop === 'gap' || prop === 'column-gap' || prop === 'row-gap') {
100
+ if (got === '0' || got === '0px' || got === 'normal') {
101
+ return "Ajouter les 3 propriétés : _gap + _columnGap + _rowGap (cf bricks-2.3-formats.md)";
102
+ }
103
+ }
104
+ if (prop === 'flex-direction' && got === 'column' && expected === 'row') {
105
+ return "Ajouter _direction: 'row' explicitement (défaut Bricks = column)";
106
+ }
107
+ if (prop.startsWith('border-') && prop.endsWith('-radius')) {
108
+ return "Utiliser _border.radius (imbriqué) — PAS _borderRadius flat";
109
+ }
110
+ if (prop === 'font-family' && got.includes('Times') || got.includes('serif')) {
111
+ return "Police pas chargée — set_custom_code({customScriptsHeader: '<link Google Fonts>'})";
112
+ }
113
+ if (prop === 'background-color' && got === 'rgba(0,0,0,0)') {
114
+ return "Background transparent — utiliser {color: {raw: 'rgba(...)'}} pour rgba (PAS hex)";
115
+ }
116
+ return null;
117
+ }
118
+
13
119
  // Configuration du fichier de log
14
120
  const __filename = fileURLToPath(import.meta.url);
15
121
  const __dirname = dirname(__filename);
@@ -250,7 +356,7 @@ mcpServer.setRequestHandler(ListToolsRequestSchema, async () => {
250
356
 
251
357
  {
252
358
  name: "update_element",
253
- description: "Modifie UN SEUL élément sans recharger/renvoyer toute la page. Ultra économe en tokens. Utilise pour changer une couleur, un texte, etc.",
359
+ description: "Modifie UN SEUL élément sans recharger/renvoyer toute la page. Ultra économe en tokens. Utilise pour changer une couleur, un texte, etc. Permet aussi de renommer l'élément dans la structure Bricks via le paramètre `label`.",
254
360
  inputSchema: {
255
361
  type: "object",
256
362
  properties: {
@@ -264,10 +370,14 @@ mcpServer.setRequestHandler(ListToolsRequestSchema, async () => {
264
370
  },
265
371
  newSettings: {
266
372
  type: "object",
267
- description: "Les nouveaux settings (fusionnés avec les anciens)",
373
+ description: "Les nouveaux settings (fusionnés avec les anciens). Optionnel si label est fourni seul.",
374
+ },
375
+ label: {
376
+ type: "string",
377
+ description: "Nom affiché dans la structure du builder Bricks (ex: 'Hero Section', 'Header Pill', 'Logo', 'CTA Buttons'). Modifie l'attribut `label` au niveau racine de l'élément. Optionnel.",
268
378
  },
269
379
  },
270
- required: ["pageId", "elementId", "newSettings"],
380
+ required: ["pageId", "elementId"],
271
381
  },
272
382
  },
273
383
 
@@ -787,6 +897,93 @@ mcpServer.setRequestHandler(ListToolsRequestSchema, async () => {
787
897
  description: "Liste les components Bricks (templates avec type=component, Bricks 2.x).",
788
898
  inputSchema: { type: "object", properties: {} },
789
899
  },
900
+
901
+ // ===== v3.6.0 — VERIFY ELEMENT (vérification visuelle + technique) =====
902
+ {
903
+ name: "verify_element",
904
+ description: "⭐ APRÈS CHAQUE batch_add OU update_element SIGNIFICATIF, utilise cet outil. Lance un browser headless, navigue sur la page, scroll vers l'élément, prend un screenshot crop et compare les computed styles avec les settings attendus. Retourne {screenshot (visible par Claude), report: {score, checks}, computed}. Tu vois ce que tu as fait. Force l'évolution petit-à-petit-vérifier-petit-à-petit.",
905
+ inputSchema: {
906
+ type: "object",
907
+ properties: {
908
+ pageId: { type: "number", description: "L'ID de la page" },
909
+ elementId: { type: "string", description: "L'ID de l'élément à vérifier (ex: 'section_abc')" },
910
+ viewport: {
911
+ type: "string",
912
+ enum: ["desktop", "tablet", "mobile_landscape", "mobile_portrait"],
913
+ description: "Taille d'écran à tester (défaut: desktop)",
914
+ },
915
+ },
916
+ required: ["pageId", "elementId"],
917
+ },
918
+ },
919
+
920
+ // ===== v3.6.0 — FEEDBACK SYSTEM (missing MCP features) =====
921
+ {
922
+ name: "report_missing_feature",
923
+ description: "À UTILISER UNIQUEMENT quand Bricks supporte une feature nativement (vérifié via doc officielle) MAIS le MCP ne l'expose pas correctement (outil manquant / buggy / setting ignoré). PAS pour les limites Bricks elles-mêmes — dans ce cas code une alternative libre (CSS/JS via set_page_custom_code). Le gestionnaire du MCP lit ces feedbacks pour combler les trous.",
924
+ inputSchema: {
925
+ type: "object",
926
+ properties: {
927
+ title: { type: "string", description: "Titre court du manque (ex: 'Pas d outil pour Interactions Bricks')" },
928
+ bricksFeature: { type: "string", description: "Nom officiel Bricks de la feature (ex: 'Interactions API')" },
929
+ bricksDocUrl: { type: "string", description: "Lien vers la doc Bricks qui prouve que la feature est native" },
930
+ whatItShouldDo: { type: "string", description: "Ce que l'outil MCP devrait faire" },
931
+ whatITried: { type: "string", description: "Ce que tu as tenté avec les outils actuels et le résultat" },
932
+ proposedTool: { type: "string", description: "Nom d'outil suggéré (ex: 'set_element_interactions')" },
933
+ bricksVersion: { type: "string", description: "Version Bricks du site (ex: '2.3.2')" },
934
+ context: { type: "string", description: "Contexte du chat (URL page, ce que tu construisais)" },
935
+ },
936
+ required: ["title", "bricksFeature"],
937
+ },
938
+ },
939
+ {
940
+ name: "list_missing_features",
941
+ description: "Liste les feedbacks remontés par d'autres chats. Pour le mainteneur du MCP qui veut savoir quoi prioriser.",
942
+ inputSchema: {
943
+ type: "object",
944
+ properties: {
945
+ status: { type: "string", enum: ["open", "resolved"], description: "Filtrer par statut (défaut: tous)" },
946
+ },
947
+ },
948
+ },
949
+ {
950
+ name: "resolve_missing_feature",
951
+ description: "Marque un feedback comme résolu (nouvel outil ajouté ou doc enrichie).",
952
+ inputSchema: {
953
+ type: "object",
954
+ properties: {
955
+ id: { type: "string", description: "ID du feedback à résoudre" },
956
+ resolutionNote: { type: "string", description: "Comment c'est résolu (nouvel outil, doc, version)" },
957
+ },
958
+ required: ["id"],
959
+ },
960
+ },
961
+
962
+ // ===== v3.6.0 — UPLOAD MEDIA BATCH =====
963
+ {
964
+ name: "upload_media_batch",
965
+ description: "Upload plusieurs images en 1 appel (vs upload_media qui en fait 1 à la fois). Continue même si certaines échouent. Retourne {uploaded, failed, successes: [{id, url, sourceUrl, filename}], failures: [{sourceUrl, error}]}.",
966
+ inputSchema: {
967
+ type: "object",
968
+ properties: {
969
+ items: {
970
+ type: "array",
971
+ description: "Liste des images à uploader",
972
+ items: {
973
+ type: "object",
974
+ properties: {
975
+ sourceUrl: { type: "string", description: "URL source de l'image" },
976
+ title: { type: "string", description: "Titre WP (sert aussi de nom de fichier slugifié)" },
977
+ alt: { type: "string", description: "Texte alternatif (SEO + accessibilité)" },
978
+ caption: { type: "string", description: "Légende optionnelle" },
979
+ },
980
+ required: ["sourceUrl"],
981
+ },
982
+ },
983
+ },
984
+ required: ["items"],
985
+ },
986
+ },
790
987
  ],
791
988
  };
792
989
  });
@@ -865,11 +1062,12 @@ mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
865
1062
 
866
1063
  case "update_element":
867
1064
  console.error(`[LOG] Exécution: update_element`);
868
- console.error(`[LOG] Modification de l'élément ${args.elementId}`);
1065
+ console.error(`[LOG] Modification de l'élément ${args.elementId}${args.label ? ` (label: "${args.label}")` : ''}`);
869
1066
  result = await callWordPressAPI("/update-element", "POST", {
870
1067
  pageId: args.pageId,
871
1068
  elementId: args.elementId,
872
- newSettings: args.newSettings,
1069
+ newSettings: args.newSettings || {},
1070
+ label: args.label,
873
1071
  });
874
1072
  break;
875
1073
 
@@ -1195,6 +1393,245 @@ mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
1195
1393
  result = await callWordPressAPI("/list-components", "GET");
1196
1394
  break;
1197
1395
 
1396
+ // ===== v3.6.0 — VERIFY ELEMENT =====
1397
+ case "verify_element": {
1398
+ const pageId = args.pageId;
1399
+ const elementId = args.elementId;
1400
+ const viewport = args.viewport || "desktop";
1401
+
1402
+ // 1) Récupérer infos plugin (URL, sélecteur, expected styles)
1403
+ const info = await callWordPressAPI("/verify-element-info", "POST", { pageId, elementId });
1404
+
1405
+ // 2) Lancer browser headless
1406
+ let page;
1407
+ try {
1408
+ page = await getNewPage(viewport);
1409
+ } catch (browserErr) {
1410
+ // Erreur Playwright (chromium pas installé etc) — retourner sans crasher
1411
+ result = {
1412
+ success: false,
1413
+ error: browserErr.message,
1414
+ url: info.url,
1415
+ selector: info.selector,
1416
+ hint: "Installation Chromium requise pour verify_element. À défaut, utilise screenshot-website-fast en MCP externe.",
1417
+ };
1418
+ break;
1419
+ }
1420
+
1421
+ // Collecte console errors
1422
+ const consoleErrors = [];
1423
+ page.on('console', msg => {
1424
+ if (msg.type() === 'error') consoleErrors.push(msg.text());
1425
+ });
1426
+ page.on('pageerror', err => consoleErrors.push('PageError: ' + err.message));
1427
+
1428
+ let screenshotBase64 = null;
1429
+ let report = { score: '0/0', checks: [] };
1430
+ let computed = null;
1431
+ let loadedFonts = [];
1432
+
1433
+ try {
1434
+ await page.goto(info.url, { waitUntil: 'domcontentloaded', timeout: 30000 });
1435
+ // Attendre 2s pour Font Awesome / Google Fonts (règle d'or skill)
1436
+ await page.waitForTimeout(2000);
1437
+
1438
+ const element = await page.$(info.selector);
1439
+ if (!element) {
1440
+ result = {
1441
+ success: false,
1442
+ error: `Élément ${info.selector} introuvable dans le DOM (caché par CSS, parent invisible, ou non rendu côté front)`,
1443
+ url: info.url,
1444
+ selector: info.selector,
1445
+ hint: "Vérifie que la page est publiée et que l'élément n'a pas _hidden ou _display: none.",
1446
+ };
1447
+ await page.close();
1448
+ break;
1449
+ }
1450
+
1451
+ await element.scrollIntoViewIfNeeded();
1452
+ await page.waitForTimeout(400);
1453
+
1454
+ // Computed style + dimensions
1455
+ computed = await element.evaluate(el => {
1456
+ const cs = getComputedStyle(el);
1457
+ const rect = el.getBoundingClientRect();
1458
+ return {
1459
+ display: cs.display,
1460
+ 'flex-direction': cs.flexDirection,
1461
+ 'justify-content': cs.justifyContent,
1462
+ 'align-items': cs.alignItems,
1463
+ gap: cs.gap,
1464
+ 'column-gap': cs.columnGap,
1465
+ 'row-gap': cs.rowGap,
1466
+ width: Math.round(rect.width) + 'px',
1467
+ height: Math.round(rect.height) + 'px',
1468
+ 'max-width': cs.maxWidth,
1469
+ 'padding-top': cs.paddingTop,
1470
+ 'padding-right': cs.paddingRight,
1471
+ 'padding-bottom': cs.paddingBottom,
1472
+ 'padding-left': cs.paddingLeft,
1473
+ 'margin-top': cs.marginTop,
1474
+ 'margin-right': cs.marginRight,
1475
+ 'margin-bottom': cs.marginBottom,
1476
+ 'margin-left': cs.marginLeft,
1477
+ 'background-color': cs.backgroundColor,
1478
+ 'font-size': cs.fontSize,
1479
+ 'font-family': cs.fontFamily,
1480
+ 'font-weight': cs.fontWeight,
1481
+ 'line-height': cs.lineHeight,
1482
+ color: cs.color,
1483
+ 'text-align': cs.textAlign,
1484
+ 'border-top-left-radius': cs.borderTopLeftRadius,
1485
+ 'border-top-right-radius': cs.borderTopRightRadius,
1486
+ 'border-bottom-right-radius': cs.borderBottomRightRadius,
1487
+ 'border-bottom-left-radius': cs.borderBottomLeftRadius,
1488
+ visibility: cs.visibility,
1489
+ opacity: cs.opacity,
1490
+ childrenInDom: el.children.length,
1491
+ isVisible: rect.width > 0 && rect.height > 0 && cs.visibility !== 'hidden' && cs.opacity !== '0',
1492
+ hasOverflowX: el.scrollWidth > el.clientWidth,
1493
+ };
1494
+ });
1495
+
1496
+ // Fonts loaded
1497
+ loadedFonts = await page.evaluate(() => {
1498
+ const set = new Set();
1499
+ document.fonts.forEach(f => { if (f.status === 'loaded') set.add(f.family); });
1500
+ return Array.from(set);
1501
+ });
1502
+
1503
+ // Build checks
1504
+ const checks = [];
1505
+ checks.push({ ok: true, label: `Élément trouvé dans le DOM (${info.selector})` });
1506
+ checks.push({
1507
+ ok: computed.isVisible,
1508
+ label: `Élément visible (${computed.width} × ${computed.height})`,
1509
+ ...(computed.isVisible ? {} : { hint: "Largeur ou hauteur à 0 — vérifie les enfants ou le padding du parent" })
1510
+ });
1511
+
1512
+ // childrenCount expected vs réel
1513
+ if (typeof info.childrenCount === 'number') {
1514
+ const ok = info.childrenCount === computed.childrenInDom;
1515
+ checks.push({
1516
+ ok,
1517
+ label: `${info.childrenCount} enfant(s) attendu(s) → ${computed.childrenInDom} dans le DOM`,
1518
+ ...(ok ? {} : { expected: info.childrenCount, got: computed.childrenInDom })
1519
+ });
1520
+ }
1521
+
1522
+ // Compare chaque expected
1523
+ const expected = info.expected || {};
1524
+ for (const [prop, expectedVal] of Object.entries(expected)) {
1525
+ const got = computed[prop];
1526
+ if (got === undefined) continue;
1527
+ const ok = normaliseCssValue(got) === normaliseCssValue(expectedVal);
1528
+ const check = { ok, label: `${prop} = ${expectedVal}` };
1529
+ if (!ok) {
1530
+ check.expected = expectedVal;
1531
+ check.got = got;
1532
+ const hint = generateHint(prop, expectedVal, got);
1533
+ if (hint) check.hint = hint;
1534
+ }
1535
+ checks.push(check);
1536
+ }
1537
+
1538
+ // Console errors
1539
+ if (consoleErrors.length > 0) {
1540
+ checks.push({
1541
+ ok: false,
1542
+ label: `${consoleErrors.length} erreur(s) console`,
1543
+ got: consoleErrors.slice(0, 3),
1544
+ hint: "Vérifie les ressources 404 (font, image, script)",
1545
+ });
1546
+ } else {
1547
+ checks.push({ ok: true, label: "Aucune erreur console" });
1548
+ }
1549
+
1550
+ // Overflow horizontal
1551
+ if (computed.hasOverflowX) {
1552
+ checks.push({
1553
+ ok: false,
1554
+ label: "Débordement horizontal détecté",
1555
+ hint: "Un enfant dépasse la largeur du conteneur — vérifie les _widthMax et white-space",
1556
+ });
1557
+ }
1558
+
1559
+ // Screenshot crop (sur l'élément)
1560
+ try {
1561
+ const buf = await element.screenshot({ type: 'png' });
1562
+ screenshotBase64 = buf.toString('base64');
1563
+ } catch (e) {
1564
+ // Si element invisible/hors viewport, fallback fullpage
1565
+ const buf = await page.screenshot({ type: 'png', fullPage: false });
1566
+ screenshotBase64 = buf.toString('base64');
1567
+ }
1568
+
1569
+ const okCount = checks.filter(c => c.ok).length;
1570
+ report = { score: `${okCount}/${checks.length}`, checks };
1571
+
1572
+ result = {
1573
+ success: true,
1574
+ url: info.url,
1575
+ urlWithAnchor: info.urlWithAnchor,
1576
+ selector: info.selector,
1577
+ name: info.name,
1578
+ label: info.label,
1579
+ viewport,
1580
+ report,
1581
+ computed,
1582
+ loadedFonts,
1583
+ };
1584
+ } finally {
1585
+ if (page && !page.isClosed()) {
1586
+ await page.close().catch(() => {});
1587
+ }
1588
+ }
1589
+
1590
+ // Réponse spéciale : ajouter l'image au content MCP pour que Claude la voie
1591
+ const responseContent = [
1592
+ { type: "text", text: JSON.stringify(result, null, 2) },
1593
+ ];
1594
+ if (screenshotBase64) {
1595
+ responseContent.push({
1596
+ type: "image",
1597
+ data: screenshotBase64,
1598
+ mimeType: "image/png",
1599
+ });
1600
+ }
1601
+ return { content: responseContent };
1602
+ }
1603
+
1604
+ // ===== v3.6.0 — FEEDBACK SYSTEM =====
1605
+ case "report_missing_feature":
1606
+ result = await callWordPressAPI("/report-missing-feature", "POST", {
1607
+ title: args.title,
1608
+ bricksFeature: args.bricksFeature,
1609
+ bricksDocUrl: args.bricksDocUrl,
1610
+ whatItShouldDo: args.whatItShouldDo,
1611
+ whatITried: args.whatITried,
1612
+ proposedTool: args.proposedTool,
1613
+ bricksVersion: args.bricksVersion,
1614
+ context: args.context,
1615
+ });
1616
+ break;
1617
+
1618
+ case "list_missing_features":
1619
+ result = await callWordPressAPI("/list-missing-features" + (args.status ? `?status=${encodeURIComponent(args.status)}` : ""), "GET");
1620
+ break;
1621
+
1622
+ case "resolve_missing_feature":
1623
+ result = await callWordPressAPI("/resolve-missing-feature", "POST", {
1624
+ id: args.id,
1625
+ resolutionNote: args.resolutionNote,
1626
+ });
1627
+ break;
1628
+
1629
+ case "upload_media_batch":
1630
+ result = await callWordPressAPI("/upload-media-batch", "POST", {
1631
+ items: args.items,
1632
+ });
1633
+ break;
1634
+
1198
1635
  default:
1199
1636
  console.error(`[LOG] Tool inconnu: ${name}`);
1200
1637
  result = { error: `Tool ${name} not found` };