story-theme-mcp 1.3.0 → 1.5.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/README.md CHANGED
@@ -59,6 +59,7 @@ Le MCP ne contient pas le thème : au démarrage, il demande à l'espace membre
59
59
  | Construire | `create_project`, `update_brief`, `apply_recipe`, `localize_template`, `list_texts`, `edit_template`, `write_file`, `update_theme_settings`, `read_file`, `exclude_template` |
60
60
  | Contrôler | `validate_json`, `review_project`, `undo`, `revert_file` |
61
61
  | Livrer | `export_theme` (zip à importer + check-list), `import_theme` (auditer un thème téléchargé d'une boutique) |
62
+ | Vos boutiques | `list_shops`, `shops_overview`, `shop_graphql` (Admin GraphQL sur une, plusieurs ou toutes les boutiques), `push_to_shop` (écrire le projet dans un thème non publié, sans zip) — via l'appli Shopify Story Apps, sans aucune clé Shopify |
62
63
 
63
64
  Prompts prêts à l'emploi : `creer-boutique`, `refaire-une-page`, `auditer-un-theme`. Les guides sont aussi exposés en ressources (`storytheme://guides/<sujet>`).
64
65
 
@@ -67,7 +68,8 @@ Prompts prêts à l'emploi : `creer-boutique`, `refaire-une-page`, `auditer-un-t
67
68
  - **Aucun fichier invalide n'est enregistré.** Chaque modèle, groupe de sections et `settings_data.json` est validé contre les vrais schémas du thème : types de sections et de blocs, blocs privés, blocs statiques et leurs identifiants, réglages (identifiant, type, options, bornes et pas des curseurs), texte riche, liens, images, palettes, polices de la bibliothèque Shopify, limites Shopify (25 sections, 50 blocs, 8 niveaux). Les erreurs proposent la bonne orthographe (« vouliez-vous dire banner ? »).
68
69
  - **Plus strict que Shopify Theme Check** : sur un même fichier piégé, Theme Check ne voit que le bloc interdit ; le MCP refuse aussi le réglage inconnu et la valeur hors limites.
69
70
  - **Revue design** : textes d'exemple ou en anglais laissés sur une boutique française, faux avis et faux chiffres des presets, bouton sans lien, plusieurs `<h1>`, contrastes insuffisants (WCAG AA, boutons compris), polices dépréciées par Shopify (Futura → Jost…), compte à rebours qui redémarre et stock simulé (pratiques commerciales trompeuses), pop-ups multiples, rythme des palettes.
70
- - **Jamais le code du thème**, jamais une boutique en ligne : seuls `templates/*.json`, `sections/<groupe>.json` et `config/settings_data.json` sont modifiables, dans un dossier de projet.
71
+ - **Jamais le code du thème** : seuls `templates/*.json`, `sections/<groupe>.json` et `config/settings_data.json` sont modifiables, dans un dossier de projet. Le Liquid, le CSS et le JS du thème ne sont jamais touchés, ni en local ni à distance.
72
+ - **Écrire dans une boutique reste encadré** : `push_to_shop` n'envoie que les fichiers du projet, refuse une boutique qui ne tourne pas sur un Story Thème de la même version majeure, refuse le thème publié sans confirmation explicite, et ne supprime jamais rien à distance. Les accès passent par l'appli Story Apps (OAuth par boutique, révocable par le marchand) et sont journalisés.
71
73
  - **Historique** : chaque écriture est précédée d'un instantané ; `undo` revient en arrière autant de fois que nécessaire.
72
74
  - **Export bloqué** tant qu'une erreur bloquante reste ; signalé « pas prêt à publier » tant qu'un point 🔴 reste.
73
75
 
@@ -97,7 +99,7 @@ Prompts prêts à l'emploi : `creer-boutique`, `refaire-une-page`, `auditer-un-t
97
99
 
98
100
  ## Limites
99
101
 
100
- Pas de connexion à une boutique : le MCP fabrique un thème à importer, il ne publie rien et ne crée ni produits, ni collections, ni menus, ni images, ni réductions (la check-list les liste). Il ne remplace pas une vérification visuelle dans l'éditeur Shopify.
102
+ Le MCP ne publie jamais un thème : il écrit dans un thème non publié, le marchand vérifie et publie lui-même. Sans l'appli Story Apps installée sur la boutique, il fabrique seulement un zip à importer. Il ne crée ni produits, ni collections, ni menus, ni images, ni réductions par lui-même : `shop_graphql` le permet quand une boutique est connectée, sinon la check-list les liste. Et il ne remplace pas une vérification visuelle dans l'éditeur Shopify.
101
103
 
102
104
  ## Développement (New Story)
103
105
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "story-theme-mcp",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
4
4
  "description": "Serveur MCP du Story Thème : votre assistant IA connaît le thème Shopify Story par cœur et construit des boutiques propres et professionnelles.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -23,7 +23,6 @@
23
23
  "dependencies": {
24
24
  "@modelcontextprotocol/sdk": "^1.30.0",
25
25
  "fflate": "^0.8.3",
26
- "story-theme-mcp": "^1.2.1",
27
26
  "zod": "^4.6.5"
28
27
  },
29
28
  "keywords": [
@@ -16,16 +16,16 @@ const clone = (value) => JSON.parse(JSON.stringify(value));
16
16
  // A style picks sensible defaults when the merchant has not said more: brand colour, neutrals,
17
17
  // font pairing and home page recipe.
18
18
  export const STYLES = {
19
- essentiel: { label: 'polyvalent', color: '#2F4B3A', neutral: 'tinted', fonts: 'clean-sans', home: 'accueil-essentiel' },
20
- premium: { label: 'premium, luxe', color: '#2B2622', neutral: 'warm', fonts: 'editorial', home: 'accueil-marque-premium' },
21
- mode: { label: 'mode, prêt-à-porter', color: '#1A1A1A', neutral: 'pure', fonts: 'fashion-sans', home: 'accueil-marque-premium' },
22
- beaute: { label: 'beauté, soin, bien-être', color: '#A45C68', neutral: 'warm', fonts: 'soft-modern', home: 'accueil-beaute' },
23
- nature: { label: 'naturel, artisanal, épicerie', color: '#3F5A3C', neutral: 'warm', fonts: 'natural', home: 'accueil-artisan' },
24
- maison: { label: 'maison, déco, grand catalogue', color: '#7A5C44', neutral: 'warm', fonts: 'minimal', home: 'accueil-catalogue' },
25
- tech: { label: 'tech, accessoires', color: '#2F5BEA', neutral: 'cool', fonts: 'tech', home: 'accueil-produit-phare' },
26
- sport: { label: 'sport, énergie', color: '#D8432B', neutral: 'pure', fonts: 'bold-sport', home: 'accueil-essentiel' },
27
- enfant: { label: 'enfants, animaux, ludique', color: '#E08A2E', neutral: 'warm', fonts: 'friendly', home: 'accueil-essentiel' },
28
- 'produit-unique': { label: 'marque d’un produit phare (DTC)', color: '#1F3A5F', neutral: 'cool', fonts: 'contemporary', home: 'accueil-produit-phare' },
19
+ essentiel: { label: 'polyvalent', color: '#2F4B3A', neutral: 'tinted', fonts: 'clean-sans', home: 'accueil-essentiel', homes: ['accueil-essentiel', 'accueil-produit-phare', 'accueil-catalogue'] },
20
+ premium: { label: 'premium, luxe', color: '#2B2622', neutral: 'warm', fonts: 'editorial', home: 'accueil-marque-premium', homes: ['accueil-marque-premium', 'accueil-essentiel', 'accueil-produit-phare'] },
21
+ mode: { label: 'mode, prêt-à-porter', color: '#1A1A1A', neutral: 'pure', fonts: 'fashion-sans', home: 'accueil-marque-premium', homes: ['accueil-marque-premium', 'accueil-catalogue', 'accueil-essentiel'] },
22
+ beaute: { label: 'beauté, soin, bien-être', color: '#A45C68', neutral: 'warm', fonts: 'soft-modern', home: 'accueil-beaute', homes: ['accueil-beaute', 'accueil-produit-phare', 'accueil-essentiel'] },
23
+ nature: { label: 'naturel, artisanal, épicerie', color: '#3F5A3C', neutral: 'warm', fonts: 'natural', home: 'accueil-artisan', homes: ['accueil-artisan', 'accueil-essentiel', 'accueil-catalogue'] },
24
+ maison: { label: 'maison, déco, grand catalogue', color: '#7A5C44', neutral: 'warm', fonts: 'minimal', home: 'accueil-catalogue', homes: ['accueil-catalogue', 'accueil-marque-premium', 'accueil-essentiel'] },
25
+ tech: { label: 'tech, accessoires', color: '#2F5BEA', neutral: 'cool', fonts: 'tech', home: 'accueil-produit-phare', homes: ['accueil-produit-phare', 'accueil-essentiel', 'accueil-catalogue'] },
26
+ sport: { label: 'sport, énergie', color: '#D8432B', neutral: 'pure', fonts: 'bold-sport', home: 'accueil-essentiel', homes: ['accueil-essentiel', 'accueil-produit-phare', 'accueil-beaute'] },
27
+ enfant: { label: 'enfants, animaux, ludique', color: '#E08A2E', neutral: 'warm', fonts: 'friendly', home: 'accueil-essentiel', homes: ['accueil-essentiel', 'accueil-catalogue', 'accueil-artisan'] },
28
+ 'produit-unique': { label: 'marque d’un produit phare (DTC)', color: '#1F3A5F', neutral: 'cool', fonts: 'contemporary', home: 'accueil-produit-phare', homes: ['accueil-produit-phare', 'accueil-beaute', 'accueil-essentiel'] },
29
29
  };
30
30
 
31
31
  const KEYWORDS = {
@@ -411,6 +411,81 @@ export function enrichir(steps, { direction = 'editorial', graine = 0, hasImages
411
411
  return sortie;
412
412
  }
413
413
 
414
+ // ---- variété des structures
415
+ //
416
+ // Le reproche était juste : deux boutiques du même style sortaient avec le même enchaînement de
417
+ // sections, parce que la recette imposait l'ordre et que l'enrichissement tombait toujours au même
418
+ // endroit (juste avant la FAQ). On garde la tête (le héros, le bandeau qui le suit) et la queue
419
+ // (FAQ, newsletter) — leur place est justifiée — et on rebat le MILIEU avec la graine de la marque.
420
+ //
421
+ // Rebattre ne veut pas dire mélanger au hasard : une page doit rester vendeuse. Les sections sont
422
+ // donc réparties en deux familles (commerce et récit) et réassemblées selon l'un des quatre rythmes
423
+ // ci-dessous. Deux sections de même type ne se retrouvent jamais collées.
424
+
425
+ const COMMERCE = new Set(['product-list', 'collection-list', 'featured-product', 'bundle-builder', 'image-hotspots', 'hotspots']);
426
+
427
+ // Chaque rythme est une intention de page, pas un tirage : C = commerce, R = récit.
428
+ const RYTHMES = [
429
+ 'CRRCRRC', // le produit d'abord, la marque autour
430
+ 'RCRRCRC', // une respiration de marque, puis le produit
431
+ 'CCRRCRR', // vitrine groupée en haut
432
+ 'CRCRRRC', // alternance serrée
433
+ ];
434
+
435
+ const alea = (graine) => {
436
+ let x = (graine % 2147483647) || 1;
437
+ return () => (x = (x * 48271) % 2147483647) / 2147483647;
438
+ };
439
+
440
+ const melanger = (liste, tirage) => {
441
+ const out = [...liste];
442
+ for (let i = out.length - 1; i > 0; i--) {
443
+ const j = Math.floor(tirage() * (i + 1));
444
+ [out[i], out[j]] = [out[j], out[i]];
445
+ }
446
+ return out;
447
+ };
448
+
449
+ /** La queue d'une page : ce qui se lit en dernier et ne se déplace pas. */
450
+ const FIN = new Set(['faq', 'newsletter', 'contact-form', 'map', 'blog-posts']);
451
+
452
+ /**
453
+ * Rebat le milieu de l'accueil pour que deux marques du même style n'aient pas la même page.
454
+ * @param {object[]} steps sections déjà adaptées et enrichies
455
+ * @param {{graine: number}} options
456
+ */
457
+ export function varier(steps, { graine = 0 } = {}) {
458
+ // La tête : le héros, plus le bandeau défilant ou la barre de réassurance qui le suit.
459
+ let debut = 1;
460
+ while (debut < steps.length && ['marquee', 'marquee-text', 'trust-badges', 'announcement'].includes(steps[debut]?.type)) debut += 1;
461
+ let fin = steps.length;
462
+ while (fin > debut && FIN.has(steps[fin - 1]?.type)) fin -= 1;
463
+
464
+ const milieu = steps.slice(debut, fin);
465
+ // En dessous de quatre sections mobiles, permuter ne produit pas de variété, seulement du bruit.
466
+ if (milieu.length < 4) return steps;
467
+
468
+ const tirage = alea(graine + 7);
469
+ const commerce = melanger(milieu.filter((etape) => COMMERCE.has(etape.type)), tirage);
470
+ const recit = melanger(milieu.filter((etape) => !COMMERCE.has(etape.type)), tirage);
471
+ const rythme = RYTHMES[graine % RYTHMES.length];
472
+
473
+ const sortie = [];
474
+ for (let i = 0; sortie.length < milieu.length; i++) {
475
+ const veutCommerce = rythme[i % rythme.length] === 'C';
476
+ let premier = veutCommerce ? commerce : recit;
477
+ let second = veutCommerce ? recit : commerce;
478
+ // Deux sections du même type à la suite : on prend dans l'autre famille, sinon on décale.
479
+ const dernier = sortie.at(-1)?.type;
480
+ if (premier.length && premier[0].type === dernier && second.length) [premier, second] = [second, premier];
481
+ const choisi = (premier.length ? premier : second).shift();
482
+ if (!choisi) break;
483
+ sortie.push(choisi);
484
+ }
485
+
486
+ return [...steps.slice(0, debut), ...sortie, ...steps.slice(fin)];
487
+ }
488
+
414
489
  export function adaptSteps(steps, { templateType, hasImages, hasLarge = hasImages, proofs, collections }) {
415
490
  const out = [];
416
491
  const planned = steps.map((step) => `${step.type}|${step.preset}`);
@@ -753,7 +828,10 @@ export function buildStore({ catalog, store }, input) {
753
828
 
754
829
  // Pages
755
830
  const recipes = [];
756
- const home = findRecipe(input.home) ? input.home : preset.home;
831
+ // Trois accueils par style, et la graine de la marque tranche : deux boutiques « beauté » ne
832
+ // partent plus de la même recette. input.home reste prioritaire si l'assistant en impose une.
833
+ const famille = (preset.homes || [preset.home]).filter((id) => findRecipe(id));
834
+ const home = findRecipe(input.home) ? input.home : famille[graineDe(input.variation || input.name) % famille.length] || preset.home;
757
835
  recipes.push([home, 'templates/index.json']);
758
836
  const avecOffres = input.offers !== false;
759
837
  recipes.push([avecOffres ? 'produit-offres' : 'produit-complet', 'templates/product.json']);
@@ -777,9 +855,10 @@ export function buildStore({ catalog, store }, input) {
777
855
  const adaptes = adaptSteps(recipe.steps, { templateType, hasImages, hasLarge, proofs, collections });
778
856
  // Enrichissement APRÈS l'adaptation : c'est la page finale qu'on complète, sinon les
779
857
  // remplissages d'adaptSteps repoussaient les sections tirées derrière la FAQ.
858
+ const graine = graineDe(input.variation || input.name);
780
859
  const steps =
781
860
  templateType === 'index'
782
- ? enrichir(adaptes, { direction: directionId, graine: graineDe(input.variation || input.name), hasImages, proofs })
861
+ ? varier(enrichir(adaptes, { direction: directionId, graine, hasImages, proofs }), { graine })
783
862
  : adaptes;
784
863
  const { data } = buildFromRecipe(catalog, { ...recipe, steps }, { language: lang, base: recipe.base ? catalog.theme.templates.get(recipe.base) : null });
785
864
  const baseCount = recipe.base ? catalog.theme.templates.get(recipe.base).order.length : 0;
@@ -206,8 +206,9 @@ Formats conseillés :
206
206
 
207
207
  limites: {
208
208
  title: 'Ce que le MCP (V1) ne fait pas',
209
- body: `- Il ne se connecte à aucune boutique : il fabrique un thème (zip) à importer, il ne publie rien.
210
- - Il ne crée ni produits, ni collections, ni menus, ni pages, ni réductions, ni images : il les liste dans la check-list.
209
+ body: `- Il ne publie jamais un thème : il écrit dans un thème NON publié (push_to_shop) ou fabrique un zip à importer (export_theme) ; le marchand vérifie et publie lui-même.
210
+ - Sans l'appli Story Apps sur la boutique, il n'a accès à rien d'elle : ni ses produits, ni ses thèmes. Appelez list_shops pour le savoir.
211
+ - Il ne crée pas de lui-même produits, collections, menus, pages, réductions ni images : avec une boutique connectée, shop_graphql le permet ; sinon la check-list les liste.
211
212
  - Il ne modifie jamais le code du thème (Liquid, CSS, JS) : seulement les modèles, les groupes de sections et les réglages, comme l'éditeur de thème.
212
213
  - Il ne garantit pas le rendu visuel : prévisualisez toujours dans l'éditeur Shopify avant de publier.`,
213
214
  },
@@ -249,4 +250,40 @@ GUIDES.signatures = {
249
250
  **Règles** : 7 à 10 sections par page, alterner visuel et texte, une section colorée sur trois, un seul effet fort par écran. Les avis et chiffres d'une maquette sont des exemples à valider : écrivez-les, puis déclarez-les.`,
250
251
  };
251
252
 
253
+
254
+ GUIDES.reproduire = {
255
+ title: 'Reproduire une section vue ailleurs (capture d’écran, site concurrent)',
256
+ body: `Le marchand montre une capture et dit « refais-moi ça ». La tentation est d'écrire du Liquid : c'est
257
+ le plus rapide pour VOUS, et le plus coûteux pour LUI. Une section en code sort de l'éditeur Shopify —
258
+ plus un mot modifiable, plus de palette, plus d'animation, rien à traduire — et aucune mise à jour du
259
+ thème ne la corrigera. Le thème est fait pour éviter ça : 109 blocs publics, acceptés presque partout,
260
+ imbriqués jusqu'à 8 niveaux.
261
+
262
+ **L'ordre, sans exception**
263
+ 1. **Une section du thème qui fait déjà ça.** search_theme sur ce que vous voyez (« comparatif », « chiffres », « étapes »), puis get_section pour les presets. 60 sections, 200 presets : la plupart des maquettes y sont déjà.
264
+ 2. **advanced-custom-section** (« Section personnalisée », preset « Advanced custom section »). C'est le cadre vide qui accepte tous les blocs : vous posez la grille avec des groupes et vous la remplissez. Réglages utiles : direction, gap, align_horizontal, stack_on_mobile, color_palette, layout_width, padding, separators.
265
+ 3. **Le bloc custom_liquid en dernier recours, jamais de votre initiative** : dites ce qui manque, ce que le marchand y perd, et laissez-le décider. Le code reste légitime pour un script tiers (suivi, widget d'avis), pas pour une mise en page.
266
+
267
+ **Exemple : la bande « 6 chiffres » (emoji, gros chiffre, sous-titre, phrase) vue sur beaucoup de sites DTC**
268
+ advanced-custom-section (color_palette de la teinte voulue, direction horizontal, stack_on_mobile, gap)
269
+ └ heading — title en richtext, le dernier mot en **gras** pour la couleur d'accent
270
+ └ text — description, color_bold_words: accent-1 : les mots en gras se colorent tout seuls
271
+ └ group (direction horizontal, align_vertical top, gap)
272
+ └ 6 × counter — icon (ou image_custom pour un emoji), heading « -80% », subheading, description, heading_font_size, duration pour le comptage animé
273
+ Résultat : le rendu, ET chaque mot modifiable dans l'éditeur, ET la version mobile gérée, ET la traduction possible.
274
+
275
+ **Les détails qui n'existent pas dans le thème** (surlignage jaune façon feutre, filets en pointillés entre
276
+ colonnes, polices tierces) : ne les inventez pas en silence. Construisez les 90 % modifiables, puis une
277
+ ligne : « deux détails ne sont pas natifs — le surlignage et les pointillés ; je peux les ajouter en code,
278
+ mais cette section ne sera alors plus modifiable dans l'éditeur. »
279
+
280
+ **Grilles courantes, en blocs**
281
+ - Deux colonnes inégales : group horizontal + deux group width custom (8/12 et 4/12).
282
+ - Carte avec image de fond et texte dessus : content-over-image, ou group avec background_image + overlay_opacity.
283
+ - Onglets, carrousel : blocs tabs et carousel (ils acceptent eux aussi @theme).
284
+ - Bande de logos, d'avis, de badges : logo-list, testimonial, trust-badges, badge.
285
+ - Tableau comparatif : section comparison (preset « Us vs them ») plutôt qu'un tableau HTML.`,
286
+ };
287
+
288
+ // En dernier : la liste doit contenir TOUS les guides, y compris ceux ajoutés après l'objet.
252
289
  export const GUIDE_TOPICS = Object.keys(GUIDES);
package/src/server.js CHANGED
@@ -63,17 +63,35 @@ Variez la structure : donnez une direction artistique (build_store direction) et
63
63
 
64
64
  Dites aussi ce qui doit exister côté Shopify pour que ça marche : les réductions correspondant aux offres, les pages et leurs modèles, les menus, l'application d'avis.
65
65
 
66
+ Ne prétendez jamais avoir vu le rendu : le MCP écrit des fichiers, il n'affiche rien. Dites « à vérifier dans l'éditeur Shopify », jamais « je l'ai ouverte sur ordinateur et sur mobile ».
67
+
66
68
  Deux limites qui ne se négocient pas : ne publiez jamais un compte à rebours qui redémarre à l'infini ni un stock simulé (pratiques commerciales trompeuses, review_project les refuse), et prévenez que des avis inventés doivent être remplacés par de vrais avant la mise en ligne.
67
69
 
68
- ## 4. Proposez de brancher le MCP Shopify
70
+ ## 4. Reproduire une mise en page : avec les blocs du thème, jamais avec du code
71
+
72
+ Quand le marchand montre une capture d'écran, un site concurrent ou une section qu'il aime et demande « refais-moi ça », vous la composez avec le thème, dans cet ordre :
73
+
74
+ 1. **une section du thème qui fait déjà ça** — list_sections, search_theme et get_section donnent les vrais presets ;
75
+ 2. **sinon advanced-custom-section** (« Section personnalisée », preset « Advanced custom section »). Elle accepte TOUS les blocs du thème, et les groupes s'imbriquent les uns dans les autres : c'est ainsi que se fabrique n'importe quelle mise en page. Une bande de six colonnes « emoji + gros chiffre + sous-titre + phrase », ce sont six blocs counter dans un groupe horizontal (stack_on_mobile). Deux colonnes inégales, deux groupes de largeurs différentes. 109 blocs sont disponibles presque partout : c'est la force du thème, servez-vous-en ;
76
+ 3. **seulement si un détail reste impossible** : dites-le en une ligne et PROPOSEZ le bloc custom_liquid. Ne l'écrivez jamais de votre propre initiative.
77
+
78
+ Le code personnalisé a un coût que le marchand paiera sans le savoir : la section sort de l'éditeur Shopify — plus un texte modifiable, plus de palette, plus d'animation, rien à traduire — et les mises à jour du thème ne la corrigeront pas. Une reproduction à 90 % entièrement modifiable vaut mieux qu'une copie exacte figée : construisez les 90 %, puis dites quels 10 % manquent (un surlignage jaune, un pointillé de séparation) et laissez le marchand choisir.
69
79
 
70
- Le Story Thème affiche ; il ne crée ni produits, ni collections, ni pages, ni menus, ni réductions. Dès la V1, proposez au marchand de connecter aussi un MCP Shopify à son assistant : vous pourrez alors créer les pages et leur attribuer les modèles, monter les menus, créer les réductions des offres, déposer les photos dans Contenu > Fichiers, et lire ses vrais produits et collections. Sans lui, listez ce qu'il doit faire à la main.
80
+ Quelques blocs qui reviennent souvent : group (imbriquable, horizontal ou vertical, fond et image de fond), counter (icône ou image, gros chiffre animé, sous-titre, description), heading (titre en richtext), text (color_bold_words colore les mots en gras), icon, image, button, accordion, testimonial, specs, checklist, timeline. get_block donne les réglages réels. Avant d'envisager du code, appelez search_theme : neuf fois sur dix, le bloc existe.
81
+
82
+ ## 5. Ses vraies boutiques, avant tout le reste
83
+
84
+ Appelez list_shops AVANT de construire. Il dit aussi quelles boutiques sont BRANCHÉES en direct : sur celles-là, vous écrivez dans un thème sans que le marchand importe quoi que ce soit — push_to_shop({ shops, create: true }) crée un thème non publié à partir du projet, et push_to_shop({ shops, theme }) écrit les retouches suivantes dedans. Le marchand prévisualise et publie lui-même ; le thème publié est refusé sauf demande explicite. Si aucune boutique n'est branchée, list_shops donne la marche à suivre (deux minutes, une seule fois) : proposez-la, c'est ce qui évite l'aller-retour par un zip à chaque modification. Si le marchand a installé l'application Story Apps, ses boutiques sont là : vous lisez ses vrais produits, collections et pages, et vous écrivez dedans (shop_graphql) sans aucune clé Shopify. Une boutique réelle bat toujours un contenu inventé — servez-vous-en pour les noms de produits, les prix, les collections, les photos déjà en ligne.
85
+
86
+ Le Story Thème affiche ; il ne crée ni produits, ni collections, ni pages, ni menus, ni réductions. Si list_shops ne renvoie rien, proposez au marchand d'installer Story Apps (apps.story-theme.com) ou de brancher un MCP Shopify à son assistant : vous pourrez alors créer les pages et leur attribuer les modèles, monter les menus, créer les réductions des offres, et déposer les photos dans Contenu > Fichiers. Sans l'un ni l'autre, listez ce qu'il doit faire à la main.
71
87
 
72
88
  Si un connecteur Shopify est déjà disponible : lisez les collections et les produits réels, copiez 3 à 6 belles photos dans Contenu > Fichiers (read_guide images donne la requête fileCreate), puis passez leurs noms dans images (celles de 1600 px et plus dans images.large) et les handles dans collections.
73
89
 
74
- ## 5. Après la V1
90
+ ## 6. Après la V1
91
+
92
+ Résumez en quelques lignes, donnez le lien ou le chemin du zip, listez ce que vous avez inventé, puis posez 3 à 5 questions pour affiner.
75
93
 
76
- Résumez en quelques lignes, donnez le lien ou le chemin du zip, listez ce que vous avez inventé, puis posez 3 à 5 questions pour affiner. Chaque réponse s'applique au projet en cours sans le re-préciser : edit_template, update_theme_settings, apply_recipe, compose_page, puis export_theme.
94
+ Le zip ne sert qu'une fois : une fois le Story Thème présent dans la bibliothèque de la boutique, push_to_shop écrit les retouches suivantes directement dans un thème NON publié (le marchand prévisualise, puis publie lui-même). Proposez-le dès que list_shops renvoie une boutique — quatre gestes en moins à chaque aller-retour. Chaque réponse s'applique au projet en cours sans le re-préciser : edit_template, update_theme_settings, apply_recipe, compose_page, puis export_theme.
77
95
 
78
96
  Garde-fous : aucun type de section, de bloc ou d'identifiant de réglage inventé (get_section, get_block, search_theme donnent les vrais) ; tout fichier est validé avant d'être enregistré ; undo annule. Le MCP ne touche ni au code du thème ni à une boutique en ligne.
79
97
 
@@ -306,7 +324,7 @@ export function createServer(config) {
306
324
  '- Construire : create_project, update_brief, apply_recipe, localize_template, list_texts, edit_template, write_file, update_theme_settings, read_file, exclude_template',
307
325
  '- Contrôler : validate_json, review_project, undo, revert_file',
308
326
  '- Livrer : export_theme (zip + check-list) ; import_theme pour auditer un thème exporté d’une boutique',
309
- '- Vos boutiques en direct (appli Story Apps) : list_shops, shops_overview, shop_graphql',
327
+ '- Vos boutiques en direct (appli Story Apps) : list_shops, shops_overview, shop_graphql, push_to_shop (envoyer le projet dans un thème sans zip)',
310
328
  `Guides : ${GUIDE_TOPICS.join(', ')}`,
311
329
  '',
312
330
  `## Projets (${projects.length})`,
@@ -1446,7 +1464,7 @@ Read the file first (read_file) to get ids.`,
1446
1464
  }
1447
1465
 
1448
1466
  // Boutiques du compte, via l'appli Shopify Story Apps (src/shops/).
1449
- registerShopTools(server, config);
1467
+ registerShopTools(server, config, { ctx });
1450
1468
 
1451
1469
  return server;
1452
1470
  }
@@ -0,0 +1,187 @@
1
+ // =====================================================================
2
+ // Écrire dans la boutique du marchand, sans zip
3
+ // =====================================================================
4
+ // Première tentative : l'API GraphQL des thèmes de Shopify. Refusée — `themeFilesUpsert` et
5
+ // `themeCreate` exigent une dérogation accordée au cas par cas, y compris à une application
6
+ // personnalisée (vérifié en production : ACCESS_DENIED).
7
+ //
8
+ // L'API REST, elle, ne restreint que les applications distribuées sur l'App Store. Le marchand
9
+ // crée donc une application personnalisée dans sa propre administration, colle son jeton une
10
+ // fois dans son espace membre, et tout passe ensuite par là (vérifié : PUT assets.json → 200).
11
+ //
12
+ // Le MCP ne voit jamais ce jeton : il parle à l'espace membre, qui détient les jetons chiffrés
13
+ // et applique les règles — boutiques du compte seulement, fichiers autorisés seulement, thème
14
+ // publié refusé sauf demande explicite.
15
+
16
+ import { z } from 'zod';
17
+
18
+ const chemin = (config, suite) => `${String(config.apiBase || 'https://story-theme.com').replace(/\/+$/, '')}/api/v1/mcp${suite}`;
19
+
20
+ /** Un appel à l'espace membre, avec le jeton de la session. */
21
+ async function appel(config, suite, options = {}) {
22
+ if (!config.token) {
23
+ throw new Error(
24
+ 'Aucun jeton : connectez le MCP à votre espace Story Thème (bouton « Se connecter » du connecteur), ou ajoutez STORY_MCP_TOKEN.',
25
+ );
26
+ }
27
+ let reponse;
28
+ try {
29
+ reponse = await fetch(chemin(config, suite), {
30
+ ...options,
31
+ headers: { Authorization: `Bearer ${config.token}`, 'Content-Type': 'application/json', Accept: 'application/json', ...(options.headers || {}) },
32
+ signal: AbortSignal.timeout(120000),
33
+ });
34
+ } catch {
35
+ throw new Error('Espace Story Thème injoignable. Réessayez dans un instant.');
36
+ }
37
+ const data = await reponse.json().catch(() => ({}));
38
+ if (!reponse.ok) throw new Error(data.message || `Erreur ${reponse.status}`);
39
+ return data.data || {};
40
+ }
41
+
42
+ const AIDE_BRANCHEMENT = [
43
+ 'Pour brancher une boutique (une fois, deux minutes) :',
44
+ '1. Espace membre Story Thème > MCP & Appli > « Brancher une boutique ».',
45
+ '2. Le lien ouvre l’administration Shopify : Paramètres > Applications > Développer des applications > Créer une application.',
46
+ '3. Autorisations Admin : cochez write_themes et read_themes, puis installez.',
47
+ '4. Révélez le jeton (shpat_…) et collez-le dans l’espace membre.',
48
+ 'Ensuite, tout s’écrit directement dans la boutique : plus aucun zip à importer.',
49
+ ].join('\n');
50
+
51
+ export function registerDirectTools(server, config, { ctx, ok, garde }) {
52
+ server.registerTool(
53
+ 'list_shops',
54
+ {
55
+ title: 'Vos boutiques',
56
+ description:
57
+ 'Lists the Shopify stores declared in this Story Theme account, and says which ones are wired for direct writing. Call it before any store work.',
58
+ annotations: { readOnlyHint: true, openWorldHint: true },
59
+ },
60
+ garde(async () => {
61
+ const { boutiques = [] } = await appel(config, '/boutiques');
62
+ if (!boutiques.length) {
63
+ return ok(`Aucune boutique déclarée dans ce compte Story Thème (espace membre > Boutiques).\n\n${AIDE_BRANCHEMENT}`, { boutiques });
64
+ }
65
+ const lignes = boutiques.map((b) => `${b.branchee ? '●' : '○'} ${b.nom} — ${b.domaine}${b.branchee ? '' : ' (non branchée)'}`);
66
+ const branchees = boutiques.filter((b) => b.branchee).length;
67
+ return ok(
68
+ [
69
+ `${boutiques.length} boutique(s) · ${branchees} branchée(s) en direct`,
70
+ '',
71
+ ...lignes,
72
+ '',
73
+ branchees ? 'push_to_shop écrit directement dans une boutique branchée.' : '',
74
+ // Une seule boutique branchée sur trois, c'est deux marchands qui ignorent encore
75
+ // que c'est possible : la marche à suivre reste affichée tant qu'il en manque une.
76
+ branchees < boutiques.length ? `${branchees ? '\n' : ''}${AIDE_BRANCHEMENT}` : '',
77
+ ]
78
+ .filter((ligne) => ligne !== '')
79
+ .join('\n'),
80
+ { boutiques },
81
+ );
82
+ }),
83
+ );
84
+
85
+ server.registerTool(
86
+ 'push_to_shop',
87
+ {
88
+ title: 'Envoyer le projet dans une boutique',
89
+ description:
90
+ 'Writes the project files (page templates, section groups, theme settings) straight into a theme of one connected store — no zip, no manual import. Without `theme`, lists the store themes. With `create: true`, creates a brand new unpublished theme from the project and writes nothing else. The published theme is refused unless confirm: true. Theme code is never touched.',
91
+ inputSchema: {
92
+ shops: z.string().describe('UNE boutique : son domaine ou son nom (list_shops les donne).'),
93
+ project: z.string().optional().describe('Projet à envoyer (par défaut : le projet en cours).'),
94
+ theme: z.string().optional().describe('Thème de destination : son identifiant numérique. Sans lui, la liste des thèmes est renvoyée.'),
95
+ create: z.boolean().optional().describe('Créer un thème NON PUBLIÉ à partir du projet, au lieu d’écrire dans un thème existant.'),
96
+ confirm: z.boolean().optional().describe('Obligatoire pour écrire dans le thème PUBLIÉ. Seulement après demande explicite du marchand.'),
97
+ },
98
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
99
+ },
100
+ garde(async ({ shops, project, theme, create, confirm }) => {
101
+ const cible = String(shops || '').trim();
102
+ if (!cible || cible === 'all') throw new Error('push_to_shop écrit dans UNE boutique à la fois : donnez son domaine (list_shops).');
103
+
104
+ const { store, catalog } = ctx.get();
105
+ const slug = store.resolve(project);
106
+ const meta = store.meta(slug);
107
+ const encode = encodeURIComponent(cible);
108
+
109
+ // Créer un thème : l'espace membre fabrique le zip, Shopify vient le chercher.
110
+ if (create) {
111
+ if (typeof config.publishExport !== 'function') {
112
+ throw new Error(
113
+ 'La création de thème demande que le MCP soit servi par l’espace membre (connecteur). En local, exportez le zip (export_theme) et importez-le une fois.',
114
+ );
115
+ }
116
+ const { exportTheme } = await import('../project/export.js');
117
+ const resultat = exportTheme({ catalog, store, slug });
118
+ if (!resultat.ok) throw new Error(resultat.message);
119
+ // Le zip part sur notre stockage : c'est de là que Shopify viendra le chercher, par un
120
+ // lien signé que l'espace membre fabrique lui-même. Il ne passe jamais par l'assistant.
121
+ await config.publishExport({ zip: resultat.zip, checklist: resultat.checklist, project: slug, catalog: meta.theme?.version });
122
+ const fichier = resultat.zip.split('/').pop();
123
+
124
+ const { theme: cree } = await appel(config, `/boutiques/${encode}/themes`, {
125
+ method: 'POST',
126
+ body: JSON.stringify({ nom: meta.name || slug, projet: slug, fichier }),
127
+ });
128
+ return ok(
129
+ [
130
+ `Thème « ${cree.nom} » créé sur ${cible}, en NON PUBLIÉ (identifiant ${cree.id}).`,
131
+ cree.traitement ? 'Shopify le décompresse : comptez une minute avant qu’il soit consultable.' : '',
132
+ '',
133
+ `Prévisualiser : https://${cible}/?preview_theme_id=${cree.id}`,
134
+ 'Le marchand vérifie chaque page, puis publie lui-même (Boutique en ligne > Thèmes).',
135
+ 'Les retouches suivantes iront directement dedans : push_to_shop avec son identifiant.',
136
+ ]
137
+ .filter(Boolean)
138
+ .join('\n'),
139
+ { theme: cree },
140
+ );
141
+ }
142
+
143
+ // Sans thème : on montre ce qui existe, le marchand choisit.
144
+ if (!theme) {
145
+ const { themes = [], domaine } = await appel(config, `/boutiques/${encode}/themes`);
146
+ return ok(
147
+ [
148
+ `Thèmes de ${domaine || cible} :`,
149
+ ...themes.map((t) => `- ${t.nom} — identifiant ${t.id}${t.role === 'main' ? ' — PUBLIÉ (la vitrine actuelle)' : ''}`),
150
+ '',
151
+ `Écrire dedans : push_to_shop({ shops: "${cible}", theme: "<identifiant>" }).`,
152
+ `Créer un thème neuf à la place : push_to_shop({ shops: "${cible}", create: true }).`,
153
+ 'Choisissez un thème NON publié : le marchand prévisualise, puis publie lui-même.',
154
+ ].join('\n'),
155
+ { themes },
156
+ );
157
+ }
158
+
159
+ // Les fichiers du projet, et eux seuls : le serveur refuse tout le reste de toute façon.
160
+ const fichiers = [...store.effectiveFiles(slug)]
161
+ .filter(([, entree]) => entree.origin === 'project')
162
+ .map(([rel, entree]) => ({ chemin: rel, contenu: `${JSON.stringify(entree.data, null, 2)}\n` }));
163
+ if (!fichiers.length) throw new Error(`Le projet « ${slug} » n’a aucun fichier personnalisé : construisez la boutique d’abord.`);
164
+
165
+ const resultat = await appel(config, `/boutiques/${encode}/themes/${encodeURIComponent(theme)}`, {
166
+ method: 'PUT',
167
+ body: JSON.stringify({ fichiers, confirm: confirm === true }),
168
+ });
169
+
170
+ const exclus = store.excluded(slug);
171
+ return ok(
172
+ [
173
+ `${resultat.ecrits.length} fichier(s) écrits dans « ${resultat.theme.nom} » sur ${resultat.domaine}${resultat.theme.role === 'main' ? ' — THÈME PUBLIÉ, la boutique a changé' : ''}.`,
174
+ resultat.refuses?.length ? `Refusés : ${resultat.refuses.join(' · ')}` : '',
175
+ '',
176
+ `Prévisualiser : ${resultat.apercu}`,
177
+ resultat.theme.role === 'main' ? '' : 'Le marchand vérifie sur ordinateur et sur mobile, puis publie lui-même.',
178
+ exclus.length ? `Modèles exclus du projet mais toujours présents dans ce thème : ${exclus.join(', ')}.` : '',
179
+ 'Images, produits, collections, menus et réductions restent à créer côté Shopify : review_project en donne la liste.',
180
+ ]
181
+ .filter(Boolean)
182
+ .join('\n'),
183
+ resultat,
184
+ );
185
+ }),
186
+ );
187
+ }
@@ -8,6 +8,7 @@
8
8
  import { z } from 'zod';
9
9
  import { createShopsClient, appsBase, ShopsError } from './client.js';
10
10
  import { formatShopList, formatOverview, formatGraphql } from './format.js';
11
+ import { registerDirectTools } from './direct.js';
11
12
 
12
13
  const ok = (text, structured) => ({ content: [{ type: 'text', text }], ...(structured ? { structuredContent: structured } : {}) });
13
14
  const fail = (text) => ({ content: [{ type: 'text', text: `⛔ ${text}` }], isError: true });
@@ -24,10 +25,12 @@ const cibleSchema = z
24
25
  .union([z.string(), z.array(z.string())])
25
26
  .describe('Boutique(s) : handle (ex. "maison-lumen"), domaine, liste, ou "all" pour toutes.');
26
27
 
27
- export function registerShopTools(server, config, { fetchImpl } = {}) {
28
+ export function registerShopTools(server, config, { fetchImpl, ctx } = {}) {
28
29
  const client = createShopsClient({ token: config.token, base: appsBase(), fetchImpl });
29
30
 
30
- server.registerTool(
31
+ // `list_shops` est servi par direct.js quand un projet est disponible : il dit en plus
32
+ // quelles boutiques sont branchées pour l'écriture directe. Ici, c'est le repli.
33
+ if (!ctx) server.registerTool(
31
34
  'list_shops',
32
35
  {
33
36
  title: 'Vos boutiques',
@@ -74,4 +77,7 @@ export function registerShopTools(server, config, { fetchImpl } = {}) {
74
77
  return ok(formatGraphql(data), data);
75
78
  }),
76
79
  );
80
+
81
+ // L'envoi direct suppose un projet sous la main : seul le serveur complet passe `ctx`.
82
+ if (ctx) registerDirectTools(server, config, { ctx, ok, garde });
77
83
  }
@@ -135,7 +135,12 @@ export function lintTemplate(catalog, file, data, { language = 'fr' } = {}) {
135
135
  if (section.type === 'popup') popups += 1;
136
136
  if ((section.type === 'banner' && settings.media_type === 'video') || section.type === 'video') autoplayMedia += 1;
137
137
  if (settings.hide_desktop && settings.hide_mobile) add('info', id, 'section masquée sur ordinateur et sur mobile');
138
- if (section.type === 'custom-liquid') add('suggestion', id, 'code Liquid personnalisé : le marchand ne pourra pas le modifier sans développeur');
138
+ // Le code personnalisé est légitime pour un script tiers (suivi, widget) ; il ne l'est pas
139
+ // pour une mise en page. Le thème accepte ses 109 blocs presque partout, imbriqués les uns
140
+ // dans les autres : advanced-custom-section reproduit la même chose, et reste modifiable.
141
+ if (section.type === 'custom-liquid') {
142
+ add('suggestion', id, 'section en code Liquid : ni modifiable dans l’éditeur Shopify, ni traduisible, ni corrigée par les mises à jour du thème. Si elle sert une mise en page, refaites-la avec advanced-custom-section et des blocs ; gardez le code pour un script tiers.');
143
+ }
139
144
  if (position === 0 && templateType === 'index' && !HERO_TYPES.has(section.type)) {
140
145
  add('suggestion', id, `la page d'accueil commence par « ${section.type} » : un héro (banner, slideshow, video) pose la promesse de la marque dès l'arrivée`);
141
146
  }
@@ -165,6 +170,9 @@ export function lintTemplate(catalog, file, data, { language = 'fr' } = {}) {
165
170
  if (block.type === 'product-stock' && blockSettings.source === 'simulated') {
166
171
  add('important', path, 'stock simulé : afficher un faux stock est une pratique commerciale trompeuse. Utilisez le stock Shopify.');
167
172
  }
173
+ if (block.type === 'custom_liquid') {
174
+ add('suggestion', path, 'bloc en code Liquid : le marchand ne pourra en changer ni un mot ni une couleur depuis l’éditeur. Pour une mise en page, les blocs du thème (group imbriqué, counter, heading, text, icon, image) font la même chose en modifiable.');
175
+ }
168
176
  if (block.type === 'image' && !blockSettings.image) add('important', path, "image vide : un carré gris « image manquante » s'affiche. Placez une photo ou retirez le bloc");
169
177
  if (block.type === 'rating' || (block.type === 'product-rating' && blockSettings.source === 'manual')) {
170
178
  add('suggestion', path, 'note saisie à la main : elle doit refléter vos avis réels (sinon supprimez le bloc)');
package/src/version.js CHANGED
@@ -1,3 +1,3 @@
1
1
  // Version du serveur MCP. Ici et dans package.json — les deux doivent être identiques
2
2
  // (npm test le vérifie), et rien d'autre ne la redéclare.
3
- export const VERSION = '1.3.0';
3
+ export const VERSION = '1.5.0';