story-theme-mcp 1.0.0 → 1.1.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 +6 -1
- package/package.json +7 -3
- package/src/http.js +147 -0
- package/src/server.js +22 -2
- package/src/version.js +1 -1
package/README.md
CHANGED
|
@@ -13,7 +13,9 @@ IA : couleurs, polices, accueil de 9 sections, fiche produit, collection, page
|
|
|
13
13
|
|
|
14
14
|
## Installation
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
**Le plus simple — l'extension Claude.** Téléchargez `story-theme-mcp.mcpb` depuis votre espace membre (page MCP & Appli) et ouvrez le fichier : Claude installe le serveur et vous demande votre jeton dans un formulaire. Rien d'autre à installer, Node compris.
|
|
17
|
+
|
|
18
|
+
Les deux méthodes ci-dessous restent valables si vous préférez configurer à la main. Elles demandent Node 20 ou plus ([nodejs.org](https://nodejs.org)).
|
|
17
19
|
|
|
18
20
|
**Claude (application)** — Réglages > Développeur > Modifier la configuration, puis dans `claude_desktop_config.json` :
|
|
19
21
|
|
|
@@ -102,8 +104,11 @@ Pas de connexion à une boutique : le MCP fabrique un thème à importer, il ne
|
|
|
102
104
|
npm test # validateur, presets, recettes, couleurs, polices, projets, export, source, serveur MCP
|
|
103
105
|
npm run check # après une mise à jour du thème : presets, recettes et descriptions toujours valides ?
|
|
104
106
|
npm run fonts # recharge la bibliothèque de polices Shopify (shopify.dev)
|
|
107
|
+
npm run bundle # fabrique build/story-theme-mcp-<version>.mcpb (extension Claude, un clic)
|
|
105
108
|
```
|
|
106
109
|
|
|
110
|
+
Après `npm run bundle`, déposer le `.mcpb` dans le dossier `mcp/` de l'espace membre : la page MCP & Appli sert automatiquement la version la plus récente qu'elle y trouve.
|
|
111
|
+
|
|
107
112
|
Avec `STORY_THEME_DIR` sur le dépôt du thème, le MCP lit le thème en cours d'écriture et n'appelle pas l'espace membre. Les connaissances rédigées à la main vivent dans `src/knowledge/` ; tout le reste vient du thème.
|
|
108
113
|
|
|
109
114
|
Publication : `npm version <x.y.z>` (mettre à jour `src/version.js` à l'identique), `npm publish` — `prepublishOnly` vérifie l'ensemble.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "story-theme-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.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",
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
"test": "node --test test/*.test.js",
|
|
16
16
|
"check": "node scripts/check-theme.js",
|
|
17
17
|
"fonts": "node scripts/fetch-fonts.js",
|
|
18
|
-
"prepublishOnly": "npm test && node scripts/check-release.js"
|
|
18
|
+
"prepublishOnly": "npm test && node scripts/check-release.js",
|
|
19
|
+
"bundle": "node scripts/build-bundle.js"
|
|
19
20
|
},
|
|
20
21
|
"author": "New Story (SAS NS GROUP) <contact@story-theme.com>",
|
|
21
22
|
"license": "SEE LICENSE IN LICENSE",
|
|
@@ -42,5 +43,8 @@
|
|
|
42
43
|
"src",
|
|
43
44
|
"README.md",
|
|
44
45
|
"LICENSE"
|
|
45
|
-
]
|
|
46
|
+
],
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"express": "^5.2.1"
|
|
49
|
+
}
|
|
46
50
|
}
|
package/src/http.js
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// =====================================================================
|
|
2
|
+
// Le même MCP, servi en HTTP — pour les connecteurs distants
|
|
3
|
+
// =====================================================================
|
|
4
|
+
//
|
|
5
|
+
// L'extension .mcpb installe le serveur CHEZ le client : Claude prévient alors, à raison, que
|
|
6
|
+
// le programme aura accès à tout son ordinateur. Un connecteur distant ne s'installe pas — on
|
|
7
|
+
// colle une adresse, on clique « Se connecter », et rien ne descend sur la machine. C'est aussi
|
|
8
|
+
// la seule forme que ChatGPT accepte.
|
|
9
|
+
//
|
|
10
|
+
// Ce module ne réécrit rien : il prend le serveur MCP existant (src/server.js, ses 35 outils,
|
|
11
|
+
// ses garde-fous) et le branche sur le transport HTTP du SDK. Ce qui change, c'est QUI parle :
|
|
12
|
+
//
|
|
13
|
+
// stdio un seul utilisateur, sa configuration vient des variables d'environnement
|
|
14
|
+
// HTTP autant d'utilisateurs que de requêtes, chacun avec son compte, sa licence,
|
|
15
|
+
// ses projets — d'où `configPour(authInfo)`, appelé à chaque nouvelle session.
|
|
16
|
+
//
|
|
17
|
+
// Le thème, lui, est COMMUN : c'est la même version publiée pour tout le monde. Une seule
|
|
18
|
+
// copie sur le serveur, partagée par toutes les sessions (voir `sourcePartagee`), au lieu d'un
|
|
19
|
+
// téléchargement par client.
|
|
20
|
+
|
|
21
|
+
import crypto from 'node:crypto';
|
|
22
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
23
|
+
import { createServer } from './server.js';
|
|
24
|
+
|
|
25
|
+
/** En-tête de session du protocole : le client le renvoie à chaque requête suivante. */
|
|
26
|
+
const ENTETE_SESSION = 'mcp-session-id';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Une session MCP = un serveur + un transport, gardés en mémoire entre deux requêtes.
|
|
30
|
+
* Fermée au bout de `inactiviteMs` sans rien : un client qui ferme Claude ne laisse pas sa
|
|
31
|
+
* session ouverte indéfiniment.
|
|
32
|
+
*/
|
|
33
|
+
class Sessions {
|
|
34
|
+
constructor({ inactiviteMs = 30 * 60 * 1000, max = 500 } = {}) {
|
|
35
|
+
this.parId = new Map();
|
|
36
|
+
this.inactiviteMs = inactiviteMs;
|
|
37
|
+
this.max = max;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
get(id) {
|
|
41
|
+
const session = this.parId.get(id);
|
|
42
|
+
if (session) session.vueA = Date.now();
|
|
43
|
+
return session;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
set(id, session) {
|
|
47
|
+
this.parId.set(id, { ...session, vueA: Date.now() });
|
|
48
|
+
this.nettoyer();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
supprimer(id) {
|
|
52
|
+
const session = this.parId.get(id);
|
|
53
|
+
if (!session) return;
|
|
54
|
+
this.parId.delete(id);
|
|
55
|
+
session.transport.close?.().catch(() => {});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Purge à l'écriture plutôt qu'au minuteur : pas de timer qui tourne dans le vide. */
|
|
59
|
+
nettoyer() {
|
|
60
|
+
const limite = Date.now() - this.inactiviteMs;
|
|
61
|
+
for (const [id, session] of this.parId) {
|
|
62
|
+
if (session.vueA < limite) this.supprimer(id);
|
|
63
|
+
}
|
|
64
|
+
// Garde-fou de dernier recours : au-delà du plafond, les plus anciennes partent.
|
|
65
|
+
if (this.parId.size > this.max) {
|
|
66
|
+
const triees = [...this.parId.entries()].sort((a, b) => a[1].vueA - b[1].vueA);
|
|
67
|
+
for (const [id] of triees.slice(0, this.parId.size - this.max)) this.supprimer(id);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
get taille() {
|
|
72
|
+
return this.parId.size;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Le gestionnaire HTTP du MCP, à monter sur une route Express (POST, GET et DELETE).
|
|
78
|
+
*
|
|
79
|
+
* @param {object} options
|
|
80
|
+
* @param {(authInfo: object) => Promise<object>|object} options.configPour
|
|
81
|
+
* Rend la configuration du serveur MCP pour l'utilisateur authentifié : sa langue, son
|
|
82
|
+
* dossier de projets, la source du thème. C'est ici que l'espace membre vérifie la
|
|
83
|
+
* licence — lever une erreur refuse la session.
|
|
84
|
+
* @param {object} [options.sessions] — remplaçable dans les tests
|
|
85
|
+
* @returns {{ handler: Function, sessions: Sessions }}
|
|
86
|
+
*/
|
|
87
|
+
export function createHttpHandler({ configPour, sessions = new Sessions() }) {
|
|
88
|
+
const handler = async (req, res) => {
|
|
89
|
+
const idSession = req.headers[ENTETE_SESSION];
|
|
90
|
+
|
|
91
|
+
// Requête suivante d'une session connue : on repasse au transport qui la porte.
|
|
92
|
+
if (idSession && sessions.get(idSession)) {
|
|
93
|
+
const { transport } = sessions.get(idSession);
|
|
94
|
+
if (req.method === 'DELETE') sessions.supprimer(idSession);
|
|
95
|
+
return transport.handleRequest(req, res, req.body);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Un identifiant de session inconnu n'est pas une erreur du client : sa session a pu
|
|
99
|
+
// expirer pendant qu'il déjeunait. On le dit dans les termes du protocole, il rouvrira.
|
|
100
|
+
if (idSession) {
|
|
101
|
+
return res.status(404).json({
|
|
102
|
+
jsonrpc: '2.0',
|
|
103
|
+
error: { code: -32001, message: 'Session expirée. Reconnectez-vous.' },
|
|
104
|
+
id: null,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (req.method !== 'POST') {
|
|
109
|
+
return res.status(405).json({
|
|
110
|
+
jsonrpc: '2.0',
|
|
111
|
+
error: { code: -32000, message: 'Method Not Allowed' },
|
|
112
|
+
id: null,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Nouvelle session : c'est le seul moment où l'on regarde qui parle.
|
|
117
|
+
let config;
|
|
118
|
+
try {
|
|
119
|
+
config = await configPour(req.auth);
|
|
120
|
+
} catch (error) {
|
|
121
|
+
return res.status(403).json({
|
|
122
|
+
jsonrpc: '2.0',
|
|
123
|
+
error: { code: -32000, message: error.message },
|
|
124
|
+
id: null,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const transport = new StreamableHTTPServerTransport({
|
|
129
|
+
sessionIdGenerator: () => crypto.randomUUID(),
|
|
130
|
+
onsessioninitialized: (id) => sessions.set(id, { transport, server }),
|
|
131
|
+
// JSON plutôt que SSE : nos outils répondent d'un coup, aucun ne diffuse en continu.
|
|
132
|
+
enableJsonResponse: true,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const server = createServer(config);
|
|
136
|
+
transport.onclose = () => {
|
|
137
|
+
if (transport.sessionId) sessions.supprimer(transport.sessionId);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
await server.connect(transport);
|
|
141
|
+
return transport.handleRequest(req, res, req.body);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
return { handler, sessions };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export { Sessions };
|
package/src/server.js
CHANGED
|
@@ -26,6 +26,7 @@ import { GUIDES, GUIDE_TOPICS } from './knowledge/guides.js';
|
|
|
26
26
|
import { RECIPES, findRecipe } from './knowledge/recipes.js';
|
|
27
27
|
import { FONT_PAIRINGS, searchFonts, checkFontHandle, fontInfo, FONT_LIBRARY } from './knowledge/fonts.js';
|
|
28
28
|
import { ok, fail, parseContent, formatSettings, formatValidation, formatFindings } from './format.js';
|
|
29
|
+
import { registerShopTools } from './shops/tools.js';
|
|
29
30
|
|
|
30
31
|
export { VERSION } from './version.js';
|
|
31
32
|
import { VERSION } from './version.js';
|
|
@@ -257,6 +258,7 @@ export function createServer(config) {
|
|
|
257
258
|
'- Construire : create_project, update_brief, apply_recipe, localize_template, list_texts, edit_template, write_file, update_theme_settings, read_file, exclude_template',
|
|
258
259
|
'- Contrôler : validate_json, review_project, undo, revert_file',
|
|
259
260
|
'- Livrer : export_theme (zip + check-list) ; import_theme pour auditer un thème exporté d’une boutique',
|
|
261
|
+
'- Vos boutiques en direct (appli Story Apps) : list_shops, shops_overview, shop_graphql',
|
|
260
262
|
`Guides : ${GUIDE_TOPICS.join(', ')}`,
|
|
261
263
|
'',
|
|
262
264
|
`## Projets (${projects.length})`,
|
|
@@ -1221,14 +1223,29 @@ Read the file first (read_file) to get ids.`,
|
|
|
1221
1223
|
inputSchema: { project: PROJECT },
|
|
1222
1224
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
1223
1225
|
},
|
|
1224
|
-
run(({ project }) => {
|
|
1226
|
+
run(async ({ project }) => {
|
|
1225
1227
|
const { store, catalog } = ctx.get();
|
|
1226
1228
|
project = store.resolve(project);
|
|
1227
1229
|
const result = exportTheme({ catalog, store, slug: project });
|
|
1228
1230
|
if (!result.ok) return fail(`${result.message}\n${(result.errors || []).map((entry) => `${entry.file} :\n${entry.errors.map((error) => ` - ${error.path} : ${error.message}`).join('\n')}`).join('\n')}`);
|
|
1229
1231
|
const notReady = /pas prêt à publier/.test(result.report);
|
|
1230
1232
|
const migratedFiles = Object.keys(result.migrated || {});
|
|
1231
|
-
|
|
1233
|
+
|
|
1234
|
+
// Servi à distance (connecteur), le chemin du zip ne veut rien dire pour le marchand :
|
|
1235
|
+
// le fichier est sur le serveur, pas chez lui. `publishExport` le dépose alors dans son
|
|
1236
|
+
// espace et rend un lien de téléchargement, qui remplace le chemin dans la réponse.
|
|
1237
|
+
let livraison = `${result.zip} (${result.size_mb} Mo, ${result.files} fichiers, personnalisés : ${result.overlays.join(', ') || 'aucun'})\nCheck-list : ${result.checklist}`;
|
|
1238
|
+
if (typeof config.publishExport === 'function') {
|
|
1239
|
+
try {
|
|
1240
|
+
const lien = await config.publishExport({ zip: result.zip, checklist: result.checklist, project, catalog: catalog.version });
|
|
1241
|
+
if (lien) livraison = `${lien} (${result.size_mb} Mo, ${result.files} fichiers, personnalisés : ${result.overlays.join(', ') || 'aucun'})`;
|
|
1242
|
+
} catch (error) {
|
|
1243
|
+
// L'export a réussi : un dépôt raté ne doit pas le transformer en échec.
|
|
1244
|
+
livraison = `${result.zip} — ⚠️ mise en ligne du fichier impossible (${error.message})`;
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
return ok(`${migratedFiles.length ? `Projet mis à jour vers le thème ${catalog.version} : ${migratedFiles.map((file) => `${file} (${result.migrated[file].length})`).join(', ')} — réglages que le thème n'a plus, sans effet sur le rendu (undo possible).\n` : ''}${notReady ? '⚠️ Exporté, mais PAS PRÊT À PUBLIER : des points 🔴 restent (voir la revue ci-dessous). Importez-le sans le publier.\n' : ''}✅ Thème exporté : ${livraison}\n\n${result.report}`);
|
|
1232
1249
|
}),
|
|
1233
1250
|
);
|
|
1234
1251
|
|
|
@@ -1346,5 +1363,8 @@ Read the file first (read_file) to get ids.`,
|
|
|
1346
1363
|
);
|
|
1347
1364
|
}
|
|
1348
1365
|
|
|
1366
|
+
// Boutiques du compte, via l'appli Shopify Story Apps (src/shops/).
|
|
1367
|
+
registerShopTools(server, config);
|
|
1368
|
+
|
|
1349
1369
|
return server;
|
|
1350
1370
|
}
|
package/src/version.js
CHANGED