bricks-builder-mcp 3.6.4 → 3.7.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.
- package/README.md +2 -0
- package/package.json +3 -2
- package/server.js +196 -10
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Serveur **Model Context Protocol (MCP)** qui permet à Claude (Desktop, Cowork, Claude Code) de piloter **Bricks Builder** sur un site WordPress : lister les pages, lire/modifier le JSON Bricks, ajouter ou réorganiser des sections, etc.
|
|
4
4
|
|
|
5
|
+
💬 **Communauté Discord** : [https://discord.gg/rX22zHRzH](https://discord.gg/rX22zHRzH) — viens discuter Bricks + IA, partager tes builds et signaler des bugs.
|
|
6
|
+
|
|
5
7
|
## Architecture
|
|
6
8
|
|
|
7
9
|
```
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "bricks-builder-mcp",
|
|
4
|
-
"version": "3.
|
|
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
|
|
4
|
+
"version": "3.7.1",
|
|
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
|
+
"homepage": "https://discord.gg/rX22zHRzH",
|
|
6
7
|
"main": "server.js",
|
|
7
8
|
"bin": {
|
|
8
9
|
"bricks-builder-mcp": "./server.js"
|
package/server.js
CHANGED
|
@@ -76,6 +76,49 @@ process.on('exit', () => {
|
|
|
76
76
|
try { _browser.close(); } catch {}
|
|
77
77
|
}
|
|
78
78
|
});
|
|
79
|
+
|
|
80
|
+
// ===== v3.7.0 — Helpers upload local =====
|
|
81
|
+
// Le MCP server tourne sur la machine user (lancé par Claude Desktop via stdio),
|
|
82
|
+
// donc il a accès direct au filesystem user. Il peut lire les fichiers et les
|
|
83
|
+
// encoder en base64 LOCALEMENT pour les envoyer au plugin sans jamais que les
|
|
84
|
+
// bytes passent dans le contexte de l'AI.
|
|
85
|
+
|
|
86
|
+
const MIME_BY_EXT = {
|
|
87
|
+
png: 'image/png',
|
|
88
|
+
jpg: 'image/jpeg',
|
|
89
|
+
jpeg: 'image/jpeg',
|
|
90
|
+
gif: 'image/gif',
|
|
91
|
+
webp: 'image/webp',
|
|
92
|
+
svg: 'image/svg+xml',
|
|
93
|
+
avif: 'image/avif',
|
|
94
|
+
mp4: 'video/mp4',
|
|
95
|
+
webm: 'video/webm',
|
|
96
|
+
mov: 'video/quicktime',
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
function readLocalFileAsDataUri(localPath) {
|
|
100
|
+
if (!fs.existsSync(localPath)) {
|
|
101
|
+
throw new Error(`Fichier introuvable : ${localPath}`);
|
|
102
|
+
}
|
|
103
|
+
const stat = fs.statSync(localPath);
|
|
104
|
+
if (!stat.isFile()) {
|
|
105
|
+
throw new Error(`Pas un fichier : ${localPath}`);
|
|
106
|
+
}
|
|
107
|
+
const ext = (localPath.split('.').pop() || '').toLowerCase();
|
|
108
|
+
const mime = MIME_BY_EXT[ext];
|
|
109
|
+
if (!mime) {
|
|
110
|
+
throw new Error(`Extension non supportée : ${ext} (formats: ${Object.keys(MIME_BY_EXT).join(', ')})`);
|
|
111
|
+
}
|
|
112
|
+
const bytes = fs.readFileSync(localPath);
|
|
113
|
+
const b64 = bytes.toString('base64');
|
|
114
|
+
return {
|
|
115
|
+
dataUri: `data:${mime};base64,${b64}`,
|
|
116
|
+
size: stat.size,
|
|
117
|
+
mime,
|
|
118
|
+
ext,
|
|
119
|
+
basename: localPath.split('/').pop(),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
79
122
|
process.on('SIGINT', () => {
|
|
80
123
|
if (_browser) {
|
|
81
124
|
try { _browser.close(); } catch {}
|
|
@@ -109,8 +152,8 @@ function normaliseColor(s) {
|
|
|
109
152
|
return s;
|
|
110
153
|
}
|
|
111
154
|
|
|
112
|
-
// Normaliser les valeurs CSS pour comparaison (rgba/spaces/units/hex)
|
|
113
|
-
function normaliseCssValue(val) {
|
|
155
|
+
// Normaliser les valeurs CSS pour comparaison (rgba/spaces/units/hex/vh)
|
|
156
|
+
function normaliseCssValue(val, viewportContext) {
|
|
114
157
|
if (val == null) return '';
|
|
115
158
|
let s = String(val).trim().toLowerCase();
|
|
116
159
|
s = s.replace(/\s+/g, '');
|
|
@@ -126,6 +169,23 @@ function normaliseCssValue(val) {
|
|
|
126
169
|
return normaliseColor(s);
|
|
127
170
|
}
|
|
128
171
|
|
|
172
|
+
// vh / vw → px en fonction du viewport actuel (getComputedStyle renvoie en px)
|
|
173
|
+
if (viewportContext) {
|
|
174
|
+
const vhMatch = s.match(/^(\d+(?:\.\d+)?)vh$/);
|
|
175
|
+
if (vhMatch && viewportContext.height) {
|
|
176
|
+
return Math.round(parseFloat(vhMatch[1]) * viewportContext.height / 100) + 'px';
|
|
177
|
+
}
|
|
178
|
+
const vwMatch = s.match(/^(\d+(?:\.\d+)?)vw$/);
|
|
179
|
+
if (vwMatch && viewportContext.width) {
|
|
180
|
+
return Math.round(parseFloat(vwMatch[1]) * viewportContext.width / 100) + 'px';
|
|
181
|
+
}
|
|
182
|
+
// svh, dvh, lvh : on assimile à vh pour l'instant (approximatif mais OK pour la plupart des cas)
|
|
183
|
+
const svhMatch = s.match(/^(\d+(?:\.\d+)?)(?:svh|dvh|lvh)$/);
|
|
184
|
+
if (svhMatch && viewportContext.height) {
|
|
185
|
+
return Math.round(parseFloat(svhMatch[1]) * viewportContext.height / 100) + 'px';
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
129
189
|
// Normalise "0px" et "0" et "0%"
|
|
130
190
|
if (s === '0' || s === '0px' || s === '0%') return '0';
|
|
131
191
|
// Si valeur sans unité fournie (ex "32") et getComputedStyle renvoie "32px"
|
|
@@ -997,6 +1057,51 @@ mcpServer.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
997
1057
|
},
|
|
998
1058
|
},
|
|
999
1059
|
|
|
1060
|
+
// ===== v3.7.0 — UPLOAD FROM LOCAL FILESYSTEM =====
|
|
1061
|
+
// Lit le fichier directement sur le disque (le MCP server tourne en local),
|
|
1062
|
+
// encode en base64 LOCALEMENT, et l'envoie au plugin via data URI.
|
|
1063
|
+
// L'AI ne voit JAMAIS les bytes — elle donne juste le path.
|
|
1064
|
+
{
|
|
1065
|
+
name: "upload_local_file",
|
|
1066
|
+
description: "⭐ UPLOAD OPTIMAL pour des fichiers locaux. L'AI donne juste le path, le MCP server lit le fichier en local et l'envoie au plugin. Aucun b64 ne transite par le contexte AI. Param `optimize: true` (recommandé) convertit en WebP qualité 80, redim à 2000px max, et renomme avec extension .webp. Idéal pour SEO + perf web. Retourne {success, id, url, filename, optimization: {originalSize, optimizedSize, savings}}.",
|
|
1067
|
+
inputSchema: {
|
|
1068
|
+
type: "object",
|
|
1069
|
+
properties: {
|
|
1070
|
+
localPath: { type: "string", description: "Chemin absolu du fichier local (ex: /Users/.../jt-assets/logo.png)" },
|
|
1071
|
+
title: { type: "string", description: "Titre WP (sert aussi de nom de fichier slugifié — utilisé pour SEO)" },
|
|
1072
|
+
alt: { type: "string", description: "Texte alternatif (OBLIGATOIRE pour SEO + accessibilité)" },
|
|
1073
|
+
caption: { type: "string", description: "Légende optionnelle" },
|
|
1074
|
+
optimize: { type: "boolean", description: "Convertit en WebP qualité 80, redim à 2000px max. Défaut: true (recommandé)." },
|
|
1075
|
+
},
|
|
1076
|
+
required: ["localPath"],
|
|
1077
|
+
},
|
|
1078
|
+
},
|
|
1079
|
+
{
|
|
1080
|
+
name: "upload_local_files_batch",
|
|
1081
|
+
description: "Upload plusieurs fichiers locaux en 1 appel. Même principe que upload_local_file mais en lot. Continue même si certains échouent. Recommandé pour intégrer un dossier d'assets (logos, photos, etc.) en 1 commande.",
|
|
1082
|
+
inputSchema: {
|
|
1083
|
+
type: "object",
|
|
1084
|
+
properties: {
|
|
1085
|
+
items: {
|
|
1086
|
+
type: "array",
|
|
1087
|
+
description: "Liste des fichiers à uploader",
|
|
1088
|
+
items: {
|
|
1089
|
+
type: "object",
|
|
1090
|
+
properties: {
|
|
1091
|
+
localPath: { type: "string", description: "Chemin absolu du fichier local" },
|
|
1092
|
+
title: { type: "string", description: "Titre WP (slugifié pour le nom de fichier)" },
|
|
1093
|
+
alt: { type: "string", description: "Alt SEO" },
|
|
1094
|
+
caption: { type: "string", description: "Légende" },
|
|
1095
|
+
},
|
|
1096
|
+
required: ["localPath"],
|
|
1097
|
+
},
|
|
1098
|
+
},
|
|
1099
|
+
optimize: { type: "boolean", description: "Convertit tous en WebP. Défaut: true." },
|
|
1100
|
+
},
|
|
1101
|
+
required: ["items"],
|
|
1102
|
+
},
|
|
1103
|
+
},
|
|
1104
|
+
|
|
1000
1105
|
// ===== v3.6.0 — UPLOAD MEDIA BATCH =====
|
|
1001
1106
|
{
|
|
1002
1107
|
name: "upload_media_batch",
|
|
@@ -1436,6 +1541,13 @@ mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1436
1541
|
const pageId = args.pageId;
|
|
1437
1542
|
const elementId = args.elementId;
|
|
1438
1543
|
const viewport = args.viewport || "desktop";
|
|
1544
|
+
const viewportSizes = {
|
|
1545
|
+
desktop: { width: 1920, height: 1080 },
|
|
1546
|
+
tablet: { width: 991, height: 1200 },
|
|
1547
|
+
mobile_landscape: { width: 767, height: 600 },
|
|
1548
|
+
mobile_portrait: { width: 478, height: 800 },
|
|
1549
|
+
};
|
|
1550
|
+
const viewportContext = viewportSizes[viewport] || viewportSizes.desktop;
|
|
1439
1551
|
|
|
1440
1552
|
// 1) Récupérer infos plugin (URL, sélecteur, expected styles)
|
|
1441
1553
|
const info = await callWordPressAPI("/verify-element-info", "POST", { pageId, elementId });
|
|
@@ -1581,12 +1693,12 @@ mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1581
1693
|
});
|
|
1582
1694
|
}
|
|
1583
1695
|
|
|
1584
|
-
// Compare chaque expected
|
|
1696
|
+
// Compare chaque expected (viewport-aware pour vh/vw)
|
|
1585
1697
|
const expected = info.expected || {};
|
|
1586
1698
|
for (const [prop, expectedVal] of Object.entries(expected)) {
|
|
1587
1699
|
const got = computed[prop];
|
|
1588
1700
|
if (got === undefined) continue;
|
|
1589
|
-
const ok = normaliseCssValue(got) === normaliseCssValue(expectedVal);
|
|
1701
|
+
const ok = normaliseCssValue(got, viewportContext) === normaliseCssValue(expectedVal, viewportContext);
|
|
1590
1702
|
const check = { ok, label: `${prop} = ${expectedVal}` };
|
|
1591
1703
|
if (!ok) {
|
|
1592
1704
|
check.expected = expectedVal;
|
|
@@ -1597,16 +1709,34 @@ mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1597
1709
|
checks.push(check);
|
|
1598
1710
|
}
|
|
1599
1711
|
|
|
1600
|
-
// Console errors
|
|
1601
|
-
|
|
1712
|
+
// Console errors : séparer les vrais bugs JS des erreurs réseau (429/timeout)
|
|
1713
|
+
// qui dépendent du serveur, pas du code de l'élément.
|
|
1714
|
+
const realErrors = consoleErrors.filter(e =>
|
|
1715
|
+
!e.includes('429') &&
|
|
1716
|
+
!e.includes('net::ERR_') &&
|
|
1717
|
+
!e.includes('Failed to load resource')
|
|
1718
|
+
);
|
|
1719
|
+
const networkErrors = consoleErrors.filter(e =>
|
|
1720
|
+
e.includes('429') || e.includes('net::ERR_') || e.includes('Failed to load resource')
|
|
1721
|
+
);
|
|
1722
|
+
|
|
1723
|
+
if (realErrors.length > 0) {
|
|
1602
1724
|
checks.push({
|
|
1603
1725
|
ok: false,
|
|
1604
|
-
label: `${
|
|
1605
|
-
got:
|
|
1606
|
-
hint: "
|
|
1726
|
+
label: `${realErrors.length} erreur(s) JS dans la console`,
|
|
1727
|
+
got: realErrors.slice(0, 3),
|
|
1728
|
+
hint: "Erreur JavaScript détectée — pas lié au serveur",
|
|
1607
1729
|
});
|
|
1608
1730
|
} else {
|
|
1609
|
-
checks.push({ ok: true, label: "Aucune erreur console" });
|
|
1731
|
+
checks.push({ ok: true, label: "Aucune erreur JS console" });
|
|
1732
|
+
}
|
|
1733
|
+
// Erreurs réseau : info seulement, pas un échec
|
|
1734
|
+
if (networkErrors.length > 0) {
|
|
1735
|
+
checks.push({
|
|
1736
|
+
ok: true,
|
|
1737
|
+
label: `${networkErrors.length} erreur(s) réseau (429/load) — non bloquant`,
|
|
1738
|
+
got: networkErrors.slice(0, 2),
|
|
1739
|
+
});
|
|
1610
1740
|
}
|
|
1611
1741
|
|
|
1612
1742
|
// Overflow horizontal
|
|
@@ -1691,9 +1821,65 @@ mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1691
1821
|
case "upload_media_batch":
|
|
1692
1822
|
result = await callWordPressAPI("/upload-media-batch", "POST", {
|
|
1693
1823
|
items: args.items,
|
|
1824
|
+
optimize: args.optimize,
|
|
1694
1825
|
});
|
|
1695
1826
|
break;
|
|
1696
1827
|
|
|
1828
|
+
// ===== v3.7.0 — UPLOAD FROM LOCAL FILESYSTEM =====
|
|
1829
|
+
case "upload_local_file": {
|
|
1830
|
+
try {
|
|
1831
|
+
const fileData = readLocalFileAsDataUri(args.localPath);
|
|
1832
|
+
logToFile(`[upload_local_file] ${args.localPath} (${fileData.size} bytes ${fileData.mime})`);
|
|
1833
|
+
// Le b64 est généré ici, dans le MCP server. Il part vers le plugin
|
|
1834
|
+
// mais ne transite JAMAIS par le contexte AI.
|
|
1835
|
+
result = await callWordPressAPI("/upload-media", "POST", {
|
|
1836
|
+
sourceUrl: fileData.dataUri,
|
|
1837
|
+
title: args.title || fileData.basename.replace(/\.[^.]+$/, ''),
|
|
1838
|
+
alt: args.alt,
|
|
1839
|
+
caption: args.caption,
|
|
1840
|
+
optimize: args.optimize !== false, // défaut true
|
|
1841
|
+
});
|
|
1842
|
+
} catch (err) {
|
|
1843
|
+
result = { success: false, error: err.message, localPath: args.localPath };
|
|
1844
|
+
}
|
|
1845
|
+
break;
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
case "upload_local_files_batch": {
|
|
1849
|
+
const batchItems = [];
|
|
1850
|
+
const readErrors = [];
|
|
1851
|
+
for (const item of (args.items || [])) {
|
|
1852
|
+
try {
|
|
1853
|
+
const fileData = readLocalFileAsDataUri(item.localPath);
|
|
1854
|
+
batchItems.push({
|
|
1855
|
+
sourceUrl: fileData.dataUri,
|
|
1856
|
+
title: item.title || fileData.basename.replace(/\.[^.]+$/, ''),
|
|
1857
|
+
alt: item.alt,
|
|
1858
|
+
caption: item.caption,
|
|
1859
|
+
});
|
|
1860
|
+
} catch (err) {
|
|
1861
|
+
readErrors.push({ localPath: item.localPath, error: err.message });
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
if (batchItems.length === 0) {
|
|
1865
|
+
result = {
|
|
1866
|
+
success: false,
|
|
1867
|
+
error: "Aucun fichier lisible",
|
|
1868
|
+
readErrors,
|
|
1869
|
+
};
|
|
1870
|
+
break;
|
|
1871
|
+
}
|
|
1872
|
+
result = await callWordPressAPI("/upload-media-batch", "POST", {
|
|
1873
|
+
items: batchItems,
|
|
1874
|
+
optimize: args.optimize !== false, // défaut true
|
|
1875
|
+
});
|
|
1876
|
+
// Joindre les erreurs de lecture locales au résultat
|
|
1877
|
+
if (readErrors.length > 0) {
|
|
1878
|
+
result.localReadErrors = readErrors;
|
|
1879
|
+
}
|
|
1880
|
+
break;
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1697
1883
|
default:
|
|
1698
1884
|
console.error(`[LOG] Tool inconnu: ${name}`);
|
|
1699
1885
|
result = { error: `Tool ${name} not found` };
|