Penpot MCP: gotchas, template script, workflow documenté
This commit is contained in:
@@ -0,0 +1,275 @@
|
||||
/**
|
||||
* Création d'une affiche "SOLDES -50% Chaussures" dans Penpot
|
||||
*/
|
||||
import http from 'http';
|
||||
|
||||
const MCP_HOST = '192.168.1.150';
|
||||
const MCP_PORT = 9002;
|
||||
|
||||
let sessionId = null;
|
||||
let msgId = 1;
|
||||
|
||||
function postMCP(body) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const payload = JSON.stringify(body);
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': Buffer.byteLength(payload),
|
||||
'Accept': 'application/json, text/event-stream',
|
||||
};
|
||||
if (sessionId) headers['mcp-session-id'] = sessionId;
|
||||
|
||||
const req = http.request({
|
||||
hostname: MCP_HOST, port: MCP_PORT, path: '/mcp', method: 'POST', headers
|
||||
}, res => {
|
||||
if (res.headers['mcp-session-id']) sessionId = res.headers['mcp-session-id'];
|
||||
let data = '';
|
||||
res.setEncoding('utf8');
|
||||
res.on('data', c => data += c);
|
||||
res.on('end', () => {
|
||||
const ct = res.headers['content-type'] || '';
|
||||
if (ct.includes('text/event-stream')) {
|
||||
let result = null, error = null;
|
||||
for (const line of data.split('\n')) {
|
||||
if (line.startsWith('data:')) {
|
||||
const raw = line.slice(5).trim();
|
||||
if (!raw || raw === '[DONE]') continue;
|
||||
try {
|
||||
const j = JSON.parse(raw);
|
||||
if (j.result !== undefined) result = j.result;
|
||||
if (j.error) error = j.error;
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
if (error) reject(new Error(JSON.stringify(error)));
|
||||
else resolve(result);
|
||||
} else {
|
||||
try {
|
||||
const j = JSON.parse(data);
|
||||
if (j.error) reject(new Error(JSON.stringify(j.error)));
|
||||
else resolve(j.result ?? j);
|
||||
} catch { resolve(data || null); }
|
||||
}
|
||||
});
|
||||
});
|
||||
req.on('error', reject);
|
||||
req.write(payload);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
async function init() {
|
||||
await postMCP({
|
||||
jsonrpc: '2.0', id: msgId++, method: 'initialize',
|
||||
params: { protocolVersion: '2025-03-26', capabilities: {}, clientInfo: { name: 'nox', version: '1.0' } }
|
||||
});
|
||||
await postMCP({ jsonrpc: '2.0', method: 'notifications/initialized', params: {} }).catch(() => {});
|
||||
console.log('✅ Session:', sessionId);
|
||||
}
|
||||
|
||||
async function callTool(name, args = {}) {
|
||||
const result = await postMCP({ jsonrpc: '2.0', id: msgId++, method: 'tools/call', params: { name, arguments: args } });
|
||||
if (!result) return null;
|
||||
if (result.content && result.content[0]) {
|
||||
const text = result.content[0].text;
|
||||
try { return JSON.parse(text); } catch { return text; }
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// ─── MAIN ───────────────────────────────────────────────────────────────────
|
||||
|
||||
await init();
|
||||
|
||||
// 1. Profil pour récupérer defaultTeamId et defaultProjectId
|
||||
const profile = await callTool('get_profile', {});
|
||||
const teamId = profile?.defaultTeamId;
|
||||
const projectId = profile?.defaultProjectId;
|
||||
console.log('TeamId:', teamId);
|
||||
console.log('ProjectId:', projectId);
|
||||
|
||||
if (!projectId || !teamId) {
|
||||
console.error('❌ Pas de profil/projet', JSON.stringify(profile));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 2. Créer le fichier
|
||||
console.log('\n📄 Création du fichier...');
|
||||
const file = await callTool('create_file', { project_id: projectId, name: 'Affiche SOLDES -50%' });
|
||||
console.log('File:', JSON.stringify(file).slice(0, 300));
|
||||
|
||||
const fileId = file?.id || file?.file_id;
|
||||
if (!fileId) { console.error('❌ Pas de fileId', file); process.exit(1); }
|
||||
console.log('FileId:', fileId);
|
||||
|
||||
// 3. Récupérer la première page
|
||||
const pages = await callTool('list_pages', { file_id: fileId });
|
||||
console.log('Pages:', JSON.stringify(pages).slice(0, 200));
|
||||
let pageId = null;
|
||||
if (Array.isArray(pages)) pageId = pages[0]?.id;
|
||||
else if (pages?.pages) pageId = pages.pages[0]?.id;
|
||||
console.log('PageId:', pageId);
|
||||
|
||||
// ─── DESIGN ─────────────────────────────────────────────────────────────────
|
||||
// Format affiche : 600 x 900px
|
||||
// Palette : fond rouge foncé, texte blanc/jaune, accent orange
|
||||
|
||||
// 4. Frame principale (l'affiche)
|
||||
console.log('\n🎨 Création du frame principal...');
|
||||
const mainFrame = await callTool('create_frame', {
|
||||
file_id: fileId,
|
||||
page_id: pageId,
|
||||
name: 'Affiche SOLDES',
|
||||
x: 50,
|
||||
y: 50,
|
||||
width: 600,
|
||||
height: 900,
|
||||
fill_color: '#C0392B',
|
||||
});
|
||||
console.log('MainFrame:', JSON.stringify(mainFrame).slice(0, 200));
|
||||
const frameId = mainFrame?.id;
|
||||
|
||||
// 5. Bande décorative haut (jaune)
|
||||
await callTool('create_rectangle', {
|
||||
file_id: fileId, page_id: pageId,
|
||||
parent_id: frameId,
|
||||
name: 'Bande haut',
|
||||
x: 0, y: 0, width: 600, height: 12,
|
||||
fill_color: '#F1C40F',
|
||||
});
|
||||
|
||||
// 6. Bande décorative bas (jaune)
|
||||
await callTool('create_rectangle', {
|
||||
file_id: fileId, page_id: pageId,
|
||||
parent_id: frameId,
|
||||
name: 'Bande bas',
|
||||
x: 0, y: 888, width: 600, height: 12,
|
||||
fill_color: '#F1C40F',
|
||||
});
|
||||
|
||||
// 7. Bloc accroche en haut
|
||||
await callTool('create_rectangle', {
|
||||
file_id: fileId, page_id: pageId,
|
||||
parent_id: frameId,
|
||||
name: 'Bloc top',
|
||||
x: 0, y: 12, width: 600, height: 80,
|
||||
fill_color: '#922B21',
|
||||
});
|
||||
|
||||
// 8. Texte "MEGA SOLDES" (haut)
|
||||
await callTool('create_text', {
|
||||
file_id: fileId, page_id: pageId,
|
||||
parent_id: frameId,
|
||||
name: 'Label MEGA',
|
||||
content: 'MEGA SOLDES',
|
||||
x: 0, y: 25,
|
||||
width: 600, height: 60,
|
||||
font_size: 32,
|
||||
font_weight: '700',
|
||||
fill_color: '#F1C40F',
|
||||
align: 'center',
|
||||
});
|
||||
|
||||
// 9. Gros texte "-50%"
|
||||
await callTool('create_text', {
|
||||
file_id: fileId, page_id: pageId,
|
||||
parent_id: frameId,
|
||||
name: '-50%',
|
||||
content: '-50%',
|
||||
x: 0, y: 120,
|
||||
width: 600, height: 280,
|
||||
font_size: 200,
|
||||
font_weight: '900',
|
||||
fill_color: '#FFFFFF',
|
||||
align: 'center',
|
||||
});
|
||||
|
||||
// 10. Texte "SUR TOUTES NOS CHAUSSURES"
|
||||
await callTool('create_text', {
|
||||
file_id: fileId, page_id: pageId,
|
||||
parent_id: frameId,
|
||||
name: 'Sous-titre',
|
||||
content: 'SUR TOUTES NOS CHAUSSURES',
|
||||
x: 0, y: 415,
|
||||
width: 600, height: 60,
|
||||
font_size: 26,
|
||||
font_weight: '600',
|
||||
fill_color: '#F1C40F',
|
||||
align: 'center',
|
||||
});
|
||||
|
||||
// 11. Séparateur
|
||||
await callTool('create_rectangle', {
|
||||
file_id: fileId, page_id: pageId,
|
||||
parent_id: frameId,
|
||||
name: 'Separateur',
|
||||
x: 80, y: 490, width: 440, height: 4,
|
||||
fill_color: '#F1C40F',
|
||||
});
|
||||
|
||||
// 12. Bloc central jaune pour "OFFRE LIMITÉE"
|
||||
await callTool('create_rectangle', {
|
||||
file_id: fileId, page_id: pageId,
|
||||
parent_id: frameId,
|
||||
name: 'Badge offre',
|
||||
x: 130, y: 520, width: 340, height: 70,
|
||||
fill_color: '#F1C40F',
|
||||
corner_radius: 8,
|
||||
});
|
||||
|
||||
// 13. Texte "OFFRE LIMITÉE"
|
||||
await callTool('create_text', {
|
||||
file_id: fileId, page_id: pageId,
|
||||
parent_id: frameId,
|
||||
name: 'Offre limitee',
|
||||
content: 'OFFRE LIMITÉE',
|
||||
x: 130, y: 534,
|
||||
width: 340, height: 44,
|
||||
font_size: 24,
|
||||
font_weight: '800',
|
||||
fill_color: '#C0392B',
|
||||
align: 'center',
|
||||
});
|
||||
|
||||
// 14. Texte "du 1er au 31 janvier"
|
||||
await callTool('create_text', {
|
||||
file_id: fileId, page_id: pageId,
|
||||
parent_id: frameId,
|
||||
name: 'Dates',
|
||||
content: 'du 1er au 31 mars 2026',
|
||||
x: 0, y: 620,
|
||||
width: 600, height: 40,
|
||||
font_size: 20,
|
||||
font_weight: '400',
|
||||
fill_color: '#FADBD8',
|
||||
align: 'center',
|
||||
});
|
||||
|
||||
// 15. Grande icône chaussure (emoji via texte)
|
||||
await callTool('create_text', {
|
||||
file_id: fileId, page_id: pageId,
|
||||
parent_id: frameId,
|
||||
name: 'Icone chaussure',
|
||||
content: '👟',
|
||||
x: 0, y: 660,
|
||||
width: 600, height: 160,
|
||||
font_size: 130,
|
||||
align: 'center',
|
||||
});
|
||||
|
||||
// 16. Texte bas "boutique-example.fr"
|
||||
await callTool('create_text', {
|
||||
file_id: fileId, page_id: pageId,
|
||||
parent_id: frameId,
|
||||
name: 'Site web',
|
||||
content: 'www.votre-boutique.fr',
|
||||
x: 0, y: 845,
|
||||
width: 600, height: 35,
|
||||
font_size: 16,
|
||||
font_weight: '400',
|
||||
fill_color: '#F8C471',
|
||||
align: 'center',
|
||||
});
|
||||
|
||||
console.log('\n✅ Affiche créée dans Penpot !');
|
||||
console.log(`🔗 Ouvrir : http://192.168.1.150:9001 → projet → fichier "Affiche SOLDES -50%"`);
|
||||
Reference in New Issue
Block a user