@imenam/mcp-design 1.0.6 → 1.0.7
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 +24 -3
- package/dist/src/cli/setup-mcp.d.ts +20 -1
- package/dist/src/cli/setup-mcp.js +64 -6
- package/dist/src/gui-worker.js +71 -23
- package/dist/src/index.js +28 -16
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/package.json +60 -60
- package/scripts/generate-icons.js +85 -85
package/README.md
CHANGED
|
@@ -28,6 +28,18 @@ npx -y @imenam/mcp-design --claude-setup-mcp
|
|
|
28
28
|
|
|
29
29
|
La commande crée (ou complète) le `.mcp.json` du répertoire courant avec une entrée `mcp-design` dont les variables sont pré-remplies. Ajoutez `--force` pour remplacer une entrée existante ; les autres serveurs sont préservés.
|
|
30
30
|
|
|
31
|
+
Deux flags permettent de choisir directement comment la GUI sera exposée, sans éditer le JSON ensuite :
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# GUI derrière le proxy central
|
|
35
|
+
npx -y @imenam/mcp-design --claude-setup-mcp --proxy_url=http://localhost:3000
|
|
36
|
+
|
|
37
|
+
# GUI en accès direct, sans proxy : http://localhost:4500
|
|
38
|
+
npx -y @imenam/mcp-design --claude-setup-mcp --app_port=4500
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Les deux flags sont exclusifs — la commande échoue si les deux sont passés.
|
|
42
|
+
|
|
31
43
|
Exemple d'entrée générée :
|
|
32
44
|
|
|
33
45
|
```json
|
|
@@ -38,6 +50,7 @@ Exemple d'entrée générée :
|
|
|
38
50
|
"args": ["-y", "@imenam/mcp-design"],
|
|
39
51
|
"env": {
|
|
40
52
|
"PROXY_URL": "http://localhost:3000",
|
|
53
|
+
"APP_PORT": "",
|
|
41
54
|
"APP_GROUP": "Mon Projet",
|
|
42
55
|
"APP_NAME": "Design Mon Projet",
|
|
43
56
|
"APP_PATH": "/design-mon-projet",
|
|
@@ -53,13 +66,21 @@ Exemple d'entrée générée :
|
|
|
53
66
|
|
|
54
67
|
| Variable | Rôle |
|
|
55
68
|
|---|---|
|
|
56
|
-
| `PROXY_URL` | URL du [proxy central](https://github.com/imenam/proxy-setup)
|
|
57
|
-
| `
|
|
58
|
-
| `
|
|
69
|
+
| `PROXY_URL` | URL du [proxy central](https://github.com/imenam/proxy-setup) : le proxy attribue le port et monte la GUI sous `APP_PATH`. |
|
|
70
|
+
| `APP_PORT` | Port local utilisé **quand `PROXY_URL` est absent** : la GUI est servie directement sur `http://localhost:<port>`, à la racine. Permet d'utiliser l'interface sans le hub de centralisation. |
|
|
71
|
+
| `APP_PATH` | Chemin de montage de la GUI derrière le proxy (défaut `/design-gui`). Ignoré en mode `APP_PORT`. `PROXY_APP_PATH` puis `GUI_PATH` sont acceptés en repli. |
|
|
72
|
+
| `APP_NAME` | Nom affiché dans le dashboard du proxy. `PROXY_APP_NAME` puis `GUI_NAME` sont acceptés en repli. |
|
|
73
|
+
| `APP_PATH_PREFIX` | Préfixe de namespace ajouté au chemin **par défaut** (ex. `/abc123` → `/abc123/design-gui`). Sert à isoler plusieurs instances sous un même proxy. |
|
|
59
74
|
| `APP_GROUP` | Section repliable du dashboard du proxy (facultatif). |
|
|
60
75
|
| `MCP_DESIGN_DATA_DIR` | Répertoire de stockage des designs. `MCP_DATA_DIR` est accepté en repli. Défaut : `<package>/.design-data`. |
|
|
61
76
|
| `MCP_LOG_DIR` | Répertoire des logs. Défaut : `<data>/logs`. |
|
|
62
77
|
|
|
78
|
+
`PROXY_URL` et `APP_PORT` s'excluent : `PROXY_URL` prime si les deux sont renseignés. **Si aucune des deux n'est définie, la GUI est simplement désactivée** — les outils MCP continuent de fonctionner.
|
|
79
|
+
|
|
80
|
+
`APP_PATH` / `APP_NAME` sont les noms canoniques ; `PROXY_APP_*` puis `GUI_*` restent acceptés, dans cet ordre de priorité décroissante. La règle de `APP_PATH_PREFIX` est simple : **il ne préfixe que le chemin par défaut**. Dès que `APP_PATH` (ou l'un de ses replis) est renseigné, cette valeur est prise telle quelle, sans préfixe — la saisie manuelle l'emporte toujours. Les chemins sont normalisés (un seul `/` de séparation, pas de `/` final) et le préfixage est idempotent. Le `<base href>` de l'interface suit le chemin réellement enregistré, assets compris.
|
|
81
|
+
|
|
82
|
+
La résolution est assurée par le paquet partagé [`@imenam/mcp-gui-interface`](https://www.npmjs.com/package/@imenam/mcp-gui-interface), qui documente la règle en détail.
|
|
83
|
+
|
|
63
84
|
Un design est un simple fichier JSON dans `<data>/designs/<id>.json` — lisible, diffable, versionnable.
|
|
64
85
|
|
|
65
86
|
---
|
|
@@ -1,5 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comment la GUI sera exposée. `undefined` des deux côtés = rien n'a été passé
|
|
3
|
+
* en ligne de commande : on retombe sur le placeholder PROXY_URL à compléter.
|
|
4
|
+
*/
|
|
5
|
+
export interface GuiExposure {
|
|
6
|
+
proxyUrl?: string;
|
|
7
|
+
appPort?: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Traduit `--proxy_url=` / `--app_port=` en mode d'exposition de la GUI.
|
|
11
|
+
* Les deux ensemble sont refusés : au runtime PROXY_URL primerait toujours,
|
|
12
|
+
* donc un fichier portant les deux serait silencieusement trompeur.
|
|
13
|
+
*/
|
|
14
|
+
export declare function parseGuiExposure(argv: string[]): {
|
|
15
|
+
exposure: GuiExposure;
|
|
16
|
+
} | {
|
|
17
|
+
error: string;
|
|
18
|
+
};
|
|
1
19
|
export interface SetupMcpOptions {
|
|
2
20
|
cwd: string;
|
|
3
21
|
force: boolean;
|
|
22
|
+
exposure?: GuiExposure;
|
|
4
23
|
}
|
|
5
|
-
export declare function runSetupMcp({ cwd, force }: SetupMcpOptions): number;
|
|
24
|
+
export declare function runSetupMcp({ cwd, force, exposure }: SetupMcpOptions): number;
|
|
@@ -16,16 +16,20 @@ function toDisplayName(folder) {
|
|
|
16
16
|
}
|
|
17
17
|
/**
|
|
18
18
|
* Valeurs volontairement vides : chaque variable est inactive tant qu'elle n'est
|
|
19
|
-
* pas remplie (PROXY_URL
|
|
20
|
-
* utilisable tel quel, jamais à moitié cassé.
|
|
19
|
+
* pas remplie (PROXY_URL et APP_PORT vides = GUI désactivée), donc le fichier
|
|
20
|
+
* généré est utilisable tel quel, jamais à moitié cassé.
|
|
21
21
|
*/
|
|
22
|
-
function buildServerEntry(codeRoot) {
|
|
22
|
+
function buildServerEntry(codeRoot, exposure) {
|
|
23
23
|
const folder = path.basename(codeRoot) || "design";
|
|
24
|
+
// Un port explicite désactive le placeholder proxy : les deux variables ne
|
|
25
|
+
// doivent jamais être renseignées ensemble, PROXY_URL primant au runtime.
|
|
26
|
+
const proxyUrl = exposure.appPort !== undefined ? "" : exposure.proxyUrl ?? "http://localhost:";
|
|
24
27
|
return {
|
|
25
28
|
command: "npx",
|
|
26
29
|
args: ["-y", PACKAGE_NAME],
|
|
27
30
|
env: {
|
|
28
|
-
PROXY_URL:
|
|
31
|
+
PROXY_URL: proxyUrl,
|
|
32
|
+
APP_PORT: exposure.appPort !== undefined ? String(exposure.appPort) : "",
|
|
29
33
|
APP_GROUP: capitalize(folder),
|
|
30
34
|
APP_NAME: `Design ${toDisplayName(folder)}`,
|
|
31
35
|
APP_PATH: `/design-${folder}`,
|
|
@@ -36,13 +40,60 @@ function buildServerEntry(codeRoot) {
|
|
|
36
40
|
}
|
|
37
41
|
const GENERATED_VARS = [
|
|
38
42
|
["PROXY_URL", "URL du proxy pour exposer la GUI (compléter le port)"],
|
|
43
|
+
["APP_PORT", "port local si aucun proxy — la GUI est servie sur http://localhost:<port>"],
|
|
39
44
|
["APP_GROUP", "groupe d'affichage dans le dashboard du proxy"],
|
|
40
45
|
["APP_NAME", "nom affiché de la GUI dans le dashboard du proxy"],
|
|
41
46
|
["APP_PATH", "chemin de montage de la GUI derrière le proxy"],
|
|
42
47
|
["MCP_DESIGN_DATA_DIR", "répertoire de stockage des designs (pré-rempli)"],
|
|
43
48
|
["MCP_LOG_DIR", "répertoire des logs (défaut : <data>/logs)"],
|
|
44
49
|
];
|
|
45
|
-
|
|
50
|
+
/** Lit `--flag=valeur` (tirets ou underscores acceptés). */
|
|
51
|
+
function readFlag(argv, name) {
|
|
52
|
+
const prefixes = [`--${name}=`, `--${name.replace(/_/g, "-")}=`];
|
|
53
|
+
for (const arg of argv) {
|
|
54
|
+
const prefix = prefixes.find((p) => arg.startsWith(p));
|
|
55
|
+
if (prefix)
|
|
56
|
+
return arg.slice(prefix.length).trim();
|
|
57
|
+
}
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Traduit `--proxy_url=` / `--app_port=` en mode d'exposition de la GUI.
|
|
62
|
+
* Les deux ensemble sont refusés : au runtime PROXY_URL primerait toujours,
|
|
63
|
+
* donc un fichier portant les deux serait silencieusement trompeur.
|
|
64
|
+
*/
|
|
65
|
+
export function parseGuiExposure(argv) {
|
|
66
|
+
const rawProxyUrl = readFlag(argv, "proxy_url");
|
|
67
|
+
const rawAppPort = readFlag(argv, "app_port");
|
|
68
|
+
if (rawProxyUrl !== undefined && rawAppPort !== undefined) {
|
|
69
|
+
return { error: "--proxy_url et --app_port sont exclusifs : la GUI passe par le proxy, ou écoute sur un port local." };
|
|
70
|
+
}
|
|
71
|
+
if (rawProxyUrl !== undefined) {
|
|
72
|
+
if (rawProxyUrl === "")
|
|
73
|
+
return { error: "--proxy_url attend une URL, ex : --proxy_url=http://localhost:3000" };
|
|
74
|
+
let parsed;
|
|
75
|
+
try {
|
|
76
|
+
parsed = new URL(rawProxyUrl);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return { error: `--proxy_url invalide : "${rawProxyUrl}" n'est pas une URL absolue (ex : http://localhost:3000).` };
|
|
80
|
+
}
|
|
81
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
82
|
+
return { error: `--proxy_url invalide : protocole "${parsed.protocol}" non supporté (http ou https attendu).` };
|
|
83
|
+
}
|
|
84
|
+
// Une barre finale casserait la concaténation PROXY_URL + APP_PATH.
|
|
85
|
+
return { exposure: { proxyUrl: rawProxyUrl.replace(/\/+$/, "") } };
|
|
86
|
+
}
|
|
87
|
+
if (rawAppPort !== undefined) {
|
|
88
|
+
const port = Number(rawAppPort);
|
|
89
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
90
|
+
return { error: `--app_port invalide : "${rawAppPort}" n'est pas un entier entre 1 et 65535.` };
|
|
91
|
+
}
|
|
92
|
+
return { exposure: { appPort: port } };
|
|
93
|
+
}
|
|
94
|
+
return { exposure: {} };
|
|
95
|
+
}
|
|
96
|
+
export function runSetupMcp({ cwd, force, exposure = {} }) {
|
|
46
97
|
const target = path.join(cwd, ".mcp.json");
|
|
47
98
|
let config = {};
|
|
48
99
|
const exists = fs.existsSync(target);
|
|
@@ -72,16 +123,23 @@ export function runSetupMcp({ cwd, force }) {
|
|
|
72
123
|
console.error("Relancez avec --force pour écraser cette entrée (les autres serveurs sont préservés).");
|
|
73
124
|
return 1;
|
|
74
125
|
}
|
|
75
|
-
config.mcpServers[SERVER_NAME] = buildServerEntry(cwd);
|
|
126
|
+
config.mcpServers[SERVER_NAME] = buildServerEntry(cwd, exposure);
|
|
76
127
|
fs.writeFileSync(target, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
77
128
|
const action = alreadyPresent ? "remplacée" : exists ? "ajoutée" : "créée";
|
|
78
129
|
console.log(`Configuration ${SERVER_NAME} ${action} dans ${target}`);
|
|
79
130
|
console.log("");
|
|
131
|
+
if (exposure.appPort !== undefined) {
|
|
132
|
+
console.log(`GUI en accès direct : http://localhost:${exposure.appPort} (sans proxy).`);
|
|
133
|
+
}
|
|
134
|
+
else if (exposure.proxyUrl !== undefined) {
|
|
135
|
+
console.log(`GUI exposée via le proxy ${exposure.proxyUrl}.`);
|
|
136
|
+
}
|
|
80
137
|
console.log('À compléter dans le champ "env" :');
|
|
81
138
|
for (const [name, description] of GENERATED_VARS) {
|
|
82
139
|
console.log(` ${name.padEnd(24)} ${description}`);
|
|
83
140
|
}
|
|
84
141
|
console.log("");
|
|
85
142
|
console.log("Une variable laissée vide est simplement inactive.");
|
|
143
|
+
console.log("PROXY_URL et APP_PORT s'excluent : PROXY_URL prime si les deux sont renseignés.");
|
|
86
144
|
return 0;
|
|
87
145
|
}
|
package/dist/src/gui-worker.js
CHANGED
|
@@ -21,6 +21,29 @@ function findProjectRoot(startDir) {
|
|
|
21
21
|
const rootDir = findProjectRoot(__dirname);
|
|
22
22
|
const absoluteStaticRoot = path.resolve(path.join(rootDir, "gui/dist"));
|
|
23
23
|
const configuredAppPath = process.env.APP_PATH || "/design-gui";
|
|
24
|
+
/**
|
|
25
|
+
* Chemin réellement monté par le proxy. Il peut différer de `configuredAppPath`
|
|
26
|
+
* quand `APP_PATH_PREFIX` est défini : le client partagé préfixe alors le chemin
|
|
27
|
+
* naturel et renvoie le chemin final dans `result.path`. Le `<base href>` doit
|
|
28
|
+
* suivre ce chemin, sinon les assets de la SPA sont demandés au mauvais endroit.
|
|
29
|
+
*/
|
|
30
|
+
let effectiveAppPath = configuredAppPath;
|
|
31
|
+
function resolveGuiMode() {
|
|
32
|
+
const proxyUrl = process.env.PROXY_URL?.trim();
|
|
33
|
+
if (proxyUrl)
|
|
34
|
+
return { kind: "proxy", proxyUrl };
|
|
35
|
+
const rawPort = process.env.APP_PORT?.trim();
|
|
36
|
+
if (!rawPort) {
|
|
37
|
+
return { kind: "disabled", reason: "PROXY_URL et APP_PORT non définis" };
|
|
38
|
+
}
|
|
39
|
+
const port = Number(rawPort);
|
|
40
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
41
|
+
return { kind: "disabled", reason: `APP_PORT invalide (${rawPort}) — attendu : entier entre 1 et 65535` };
|
|
42
|
+
}
|
|
43
|
+
return { kind: "standalone", port };
|
|
44
|
+
}
|
|
45
|
+
const guiMode = resolveGuiMode();
|
|
46
|
+
const isStandalone = guiMode.kind === "standalone";
|
|
24
47
|
setupLogging({ processLabel: "mcp-design:GUI", logDir: resolveLogDir(rootDir) });
|
|
25
48
|
const logger = createLogger("mcp-design:GUI");
|
|
26
49
|
process.on("uncaughtException", (err) => {
|
|
@@ -136,7 +159,7 @@ app.get("/proxy/health", (c) => {
|
|
|
136
159
|
const memory = process.memoryUsage();
|
|
137
160
|
return c.json({
|
|
138
161
|
status: "ok",
|
|
139
|
-
proxy: proxyClient ? proxyClient.getStatus() :
|
|
162
|
+
proxy: proxyClient ? proxyClient.getStatus() : isStandalone ? "standalone" : "error",
|
|
140
163
|
uptime: process.uptime(),
|
|
141
164
|
memory: { rss: memory.rss, heapUsed: memory.heapUsed },
|
|
142
165
|
ipc: process.connected ? "connected" : "disconnected",
|
|
@@ -262,7 +285,8 @@ app.get("/*", (c, next) => {
|
|
|
262
285
|
if (fs.existsSync(indexPath)) {
|
|
263
286
|
// ?__direct=1 : accès au worker sur son port local, sans passer par le
|
|
264
287
|
// proxy — le préfixe de montage ne doit alors pas préfixer les assets.
|
|
265
|
-
|
|
288
|
+
// En mode standalone la GUI est déjà servie à la racine : même règle.
|
|
289
|
+
const baseHref = isStandalone || c.req.query("__direct") === "1" ? "/" : getHtmlBaseHref(effectiveAppPath);
|
|
266
290
|
const html = fs.readFileSync(indexPath, "utf8").replace("<head>", `<head>\n <base href="${baseHref}" />`);
|
|
267
291
|
return c.html(html);
|
|
268
292
|
}
|
|
@@ -272,12 +296,51 @@ app.get("/*", (c, next) => {
|
|
|
272
296
|
return next();
|
|
273
297
|
}
|
|
274
298
|
});
|
|
299
|
+
/**
|
|
300
|
+
* Lance Hono sur `port` et prévient le master. `publicUrl` est l'URL que
|
|
301
|
+
* l'utilisateur doit ouvrir : via le proxy, ou directement en localhost.
|
|
302
|
+
*/
|
|
303
|
+
function listen(port, publicUrl, isProxied) {
|
|
304
|
+
const serverInstance = serve({ fetch: app.fetch, port, hostname: "0.0.0.0" }, (info) => {
|
|
305
|
+
logger.info(`Design canvas server listening on port ${info.port}`);
|
|
306
|
+
safeSend(IPCMessageSchema.parse({
|
|
307
|
+
type: "READY",
|
|
308
|
+
data: { port: info.port, isProxied, url: publicUrl },
|
|
309
|
+
timestamp: new Date().toISOString(),
|
|
310
|
+
}));
|
|
311
|
+
});
|
|
312
|
+
serverInstance.on("error", (err) => {
|
|
313
|
+
if (err.code === "EADDRINUSE") {
|
|
314
|
+
// Sortie propre (code 0) : le master ne doit pas relancer un worker de plus.
|
|
315
|
+
logger.error(`Port ${port} already in use — another GUI instance is running.`);
|
|
316
|
+
safeSend(IPCMessageSchema.parse({
|
|
317
|
+
type: "ALREADY_RUNNING",
|
|
318
|
+
data: { url: publicUrl },
|
|
319
|
+
timestamp: new Date().toISOString(),
|
|
320
|
+
}));
|
|
321
|
+
process.exit(0);
|
|
322
|
+
}
|
|
323
|
+
logger.error(`Server error: ${err.message}`);
|
|
324
|
+
process.exit(1);
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
/** Sans proxy : la GUI écoute sur APP_PORT et est servie à la racine. */
|
|
328
|
+
function startStandalone(port) {
|
|
329
|
+
const publicUrl = `http://localhost:${port}`;
|
|
330
|
+
logger.info(`Standalone mode (no PROXY_URL): serving GUI on ${publicUrl}`);
|
|
331
|
+
setupIpcErrorHandlers();
|
|
332
|
+
listen(port, publicUrl, false);
|
|
333
|
+
}
|
|
275
334
|
async function startServer() {
|
|
276
|
-
if (
|
|
277
|
-
process.stderr.write(
|
|
335
|
+
if (guiMode.kind === "disabled") {
|
|
336
|
+
process.stderr.write(`[GUI] ${guiMode.reason} — arrêt.\n`);
|
|
278
337
|
process.exit(0);
|
|
279
338
|
}
|
|
280
|
-
|
|
339
|
+
if (guiMode.kind === "standalone") {
|
|
340
|
+
startStandalone(guiMode.port);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
const proxyUrl = guiMode.proxyUrl;
|
|
281
344
|
const appPath = configuredAppPath;
|
|
282
345
|
const appName = process.env.APP_NAME || "MCP Design";
|
|
283
346
|
logger.info(`Starting server with proxyUrl=${proxyUrl}`);
|
|
@@ -311,7 +374,8 @@ async function startServer() {
|
|
|
311
374
|
return;
|
|
312
375
|
}
|
|
313
376
|
const finalPort = result.port;
|
|
314
|
-
|
|
377
|
+
effectiveAppPath = result.path ?? appPath;
|
|
378
|
+
const publicUrl = result.url || `${proxyUrl}${effectiveAppPath}`;
|
|
315
379
|
logger.info(`Registered with proxy. Public URL: ${publicUrl}, Local Port: ${finalPort}`);
|
|
316
380
|
const cleanupProxy = async () => {
|
|
317
381
|
if (proxyClient)
|
|
@@ -321,23 +385,7 @@ async function startServer() {
|
|
|
321
385
|
process.on("SIGTERM", cleanupProxy);
|
|
322
386
|
process.on("SIGINT", cleanupProxy);
|
|
323
387
|
setupIpcErrorHandlers();
|
|
324
|
-
|
|
325
|
-
logger.info(`Design canvas server listening on port ${info.port}`);
|
|
326
|
-
safeSend(IPCMessageSchema.parse({
|
|
327
|
-
type: "READY",
|
|
328
|
-
data: { port: info.port, isProxied: true, url: publicUrl },
|
|
329
|
-
timestamp: new Date().toISOString(),
|
|
330
|
-
}));
|
|
331
|
-
});
|
|
332
|
-
serverInstance.on("error", (err) => {
|
|
333
|
-
if (err.code === "EADDRINUSE") {
|
|
334
|
-
// Sortie propre (code 0) : le master ne doit pas relancer un worker de plus.
|
|
335
|
-
logger.error(`Port ${finalPort} already in use — another GUI instance is running.`);
|
|
336
|
-
process.exit(0);
|
|
337
|
-
}
|
|
338
|
-
logger.error(`Server error: ${err.message}`);
|
|
339
|
-
process.exit(1);
|
|
340
|
-
});
|
|
388
|
+
listen(finalPort, publicUrl, true);
|
|
341
389
|
}
|
|
342
390
|
startServer().catch((err) => {
|
|
343
391
|
logger.error(`Failed to start GUI Worker: ${err.message}`);
|
package/dist/src/index.js
CHANGED
|
@@ -41,8 +41,14 @@ const __dirname = path.dirname(__filename);
|
|
|
41
41
|
// Les flags CLI sont traités avant toute initialisation : ils s'exécutent dans le
|
|
42
42
|
// répertoire de l'utilisateur, pas dans un serveur MCP.
|
|
43
43
|
if (process.argv.slice(2).includes("--claude-setup-mcp")) {
|
|
44
|
-
const
|
|
45
|
-
|
|
44
|
+
const argv = process.argv.slice(2);
|
|
45
|
+
const { runSetupMcp, parseGuiExposure } = await import("./cli/setup-mcp.js");
|
|
46
|
+
const parsed = parseGuiExposure(argv);
|
|
47
|
+
if ("error" in parsed) {
|
|
48
|
+
console.error(`Erreur : ${parsed.error}`);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
process.exit(runSetupMcp({ cwd: process.cwd(), force: argv.includes("--force"), exposure: parsed.exposure }));
|
|
46
52
|
}
|
|
47
53
|
function findProjectRoot(startDir) {
|
|
48
54
|
let current = startDir;
|
|
@@ -62,7 +68,7 @@ const logger = createLogger("mcp-design");
|
|
|
62
68
|
function getConfig() {
|
|
63
69
|
if (fs.existsSync(packageEnvPath))
|
|
64
70
|
dotenv.config({ path: packageEnvPath, override: true });
|
|
65
|
-
return { proxyUrl: process.env.PROXY_URL, version: APP_VERSION };
|
|
71
|
+
return { proxyUrl: process.env.PROXY_URL, appPort: process.env.APP_PORT, version: APP_VERSION };
|
|
66
72
|
}
|
|
67
73
|
getConfig();
|
|
68
74
|
const store = new DesignStore(rootDir);
|
|
@@ -224,9 +230,13 @@ guiLauncher = new GuiLauncher({
|
|
|
224
230
|
maxRestarts: 0,
|
|
225
231
|
onMessage: handleIPCMessage,
|
|
226
232
|
});
|
|
233
|
+
/** La GUI a besoin soit d'un proxy, soit d'un port local (mode standalone). */
|
|
234
|
+
function guiIsConfigured() {
|
|
235
|
+
return !!(process.env.PROXY_URL?.trim() || process.env.APP_PORT?.trim());
|
|
236
|
+
}
|
|
227
237
|
function launchGUI() {
|
|
228
|
-
if (!
|
|
229
|
-
logger.info("[MASTER] PROXY_URL
|
|
238
|
+
if (!guiIsConfigured()) {
|
|
239
|
+
logger.info("[MASTER] Ni PROXY_URL ni APP_PORT défini — GUI désactivée.");
|
|
230
240
|
guiStatus = "disabled";
|
|
231
241
|
return;
|
|
232
242
|
}
|
|
@@ -234,13 +244,13 @@ function launchGUI() {
|
|
|
234
244
|
startGuiWorker();
|
|
235
245
|
}
|
|
236
246
|
async function reconnectGui(args) {
|
|
237
|
-
getConfig(); // relit le .env au cas où PROXY_URL aurait été ajouté après le démarrage
|
|
238
|
-
if (!
|
|
247
|
+
getConfig(); // relit le .env au cas où PROXY_URL/APP_PORT aurait été ajouté après le démarrage
|
|
248
|
+
if (!guiIsConfigured()) {
|
|
239
249
|
return {
|
|
240
250
|
content: [
|
|
241
251
|
{
|
|
242
252
|
type: "text",
|
|
243
|
-
text: "Error: PROXY_URL is
|
|
253
|
+
text: "Error: neither PROXY_URL nor APP_PORT is set — the GUI is disabled. Add PROXY_URL (to expose the GUI through the proxy) or APP_PORT (to serve it directly on localhost) to the .env file, then call reconnect_gui again.",
|
|
244
254
|
},
|
|
245
255
|
],
|
|
246
256
|
isError: true,
|
|
@@ -253,13 +263,13 @@ async function reconnectGui(args) {
|
|
|
253
263
|
if (guiStatus === "starting") {
|
|
254
264
|
return {
|
|
255
265
|
content: [
|
|
256
|
-
{ type: "text", text: "GUI worker is already starting
|
|
266
|
+
{ type: "text", text: "GUI worker is already starting. Wait a few seconds, then call reconnect_gui again if needed." },
|
|
257
267
|
],
|
|
258
268
|
};
|
|
259
269
|
}
|
|
260
270
|
return {
|
|
261
271
|
content: [
|
|
262
|
-
{ type: "text", text: `GUI is already
|
|
272
|
+
{ type: "text", text: `GUI is already running${guiUrl ? ` at ${guiUrl}` : ""}. Nothing to do (use force=true to restart it anyway).` },
|
|
263
273
|
],
|
|
264
274
|
};
|
|
265
275
|
}
|
|
@@ -285,23 +295,25 @@ async function reconnectGui(args) {
|
|
|
285
295
|
};
|
|
286
296
|
newChild.on("exit", onExit);
|
|
287
297
|
});
|
|
298
|
+
const proxyUrl = process.env.PROXY_URL?.trim();
|
|
288
299
|
if (result.outcome === "ready") {
|
|
289
|
-
return { content: [{ type: "text", text: `GUI reconnected
|
|
300
|
+
return { content: [{ type: "text", text: `GUI reconnected. URL: ${result.url}` }] };
|
|
290
301
|
}
|
|
291
302
|
if (result.outcome === "already_running") {
|
|
292
|
-
return { content: [{ type: "text", text: `GUI is already
|
|
303
|
+
return { content: [{ type: "text", text: `GUI is already served by another MCP instance. URL: ${result.url ?? "unknown"}` }] };
|
|
293
304
|
}
|
|
294
305
|
if (result.outcome === "exited") {
|
|
306
|
+
const hint = proxyUrl
|
|
307
|
+
? `The proxy server is probably still unreachable at ${proxyUrl}. Start the proxy server, then call reconnect_gui again.`
|
|
308
|
+
: `Port ${process.env.APP_PORT} is probably already taken by another process. Free it or set another APP_PORT, then call reconnect_gui again.`;
|
|
295
309
|
return {
|
|
296
|
-
content: [
|
|
297
|
-
{ type: "text", text: `GUI worker exited before completing registration (code=${result.code}). The proxy server is probably still unreachable at ${process.env.PROXY_URL}. Start the proxy server, then call reconnect_gui again.` },
|
|
298
|
-
],
|
|
310
|
+
content: [{ type: "text", text: `GUI worker exited before it was ready (code=${result.code}). ${hint}` }],
|
|
299
311
|
isError: true,
|
|
300
312
|
};
|
|
301
313
|
}
|
|
302
314
|
return {
|
|
303
315
|
content: [
|
|
304
|
-
{ type: "text", text: `GUI worker started but did not confirm
|
|
316
|
+
{ type: "text", text: `GUI worker started but did not confirm it was ready within 15s. Check the logs (${resolveLogDir(rootDir)}), then retry.` },
|
|
305
317
|
],
|
|
306
318
|
isError: true,
|
|
307
319
|
};
|
package/dist/src/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const APP_VERSION = "1.0.
|
|
1
|
+
export declare const APP_VERSION = "1.0.7";
|
package/dist/src/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Auto-generated version file - do not edit manually
|
|
2
|
-
export const APP_VERSION = '1.0.
|
|
2
|
+
export const APP_VERSION = '1.0.7';
|
package/package.json
CHANGED
|
@@ -1,60 +1,60 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@imenam/mcp-design",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"main": "dist/src/index.js",
|
|
5
|
-
"directories": {
|
|
6
|
-
"test": "test"
|
|
7
|
-
},
|
|
8
|
-
"scripts": {
|
|
9
|
-
"generate-version": "node scripts/generate-version.js",
|
|
10
|
-
"build:backend": "npm run generate-version && tsc",
|
|
11
|
-
"build:gui": "cd gui && npm install && npm run build",
|
|
12
|
-
"build": "npm run build:backend && npm run build:gui",
|
|
13
|
-
"start": "node dist/src/index.js",
|
|
14
|
-
"test": "node --test \"dist/test/*.test.js\"",
|
|
15
|
-
"prepublishOnly": "npm run build",
|
|
16
|
-
"release": "npm version patch && npm publish && git push --follow-tags",
|
|
17
|
-
"generate-icons": "node scripts/generate-icons.js"
|
|
18
|
-
},
|
|
19
|
-
"keywords": [
|
|
20
|
-
"mcp",
|
|
21
|
-
"model-context-protocol",
|
|
22
|
-
"design",
|
|
23
|
-
"canvas",
|
|
24
|
-
"ui",
|
|
25
|
-
"mockup",
|
|
26
|
-
"ai",
|
|
27
|
-
"agent"
|
|
28
|
-
],
|
|
29
|
-
"author": "Imena",
|
|
30
|
-
"license": "ISC",
|
|
31
|
-
"description": "MCP server providing a simple design canvas: the agent creates shapes and text, the user refines them in a GUI, and the agent picks the edits back up.",
|
|
32
|
-
"type": "module",
|
|
33
|
-
"bin": {
|
|
34
|
-
"mcp-design": "dist/src/index.js"
|
|
35
|
-
},
|
|
36
|
-
"publishConfig": {
|
|
37
|
-
"access": "public"
|
|
38
|
-
},
|
|
39
|
-
"files": [
|
|
40
|
-
"dist/src",
|
|
41
|
-
"dist/shared",
|
|
42
|
-
"gui/dist",
|
|
43
|
-
"README.md",
|
|
44
|
-
"scripts"
|
|
45
|
-
],
|
|
46
|
-
"dependencies": {
|
|
47
|
-
"@hono/node-server": "^2.1.1",
|
|
48
|
-
"@imenam/mcp-gui-interface": "^1.0.
|
|
49
|
-
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
50
|
-
"@resvg/resvg-js": "^2.6.2",
|
|
51
|
-
"dotenv": "^17.4.2",
|
|
52
|
-
"hono": "^4.13.4",
|
|
53
|
-
"zod": "^4.4.3"
|
|
54
|
-
},
|
|
55
|
-
"devDependencies": {
|
|
56
|
-
"@types/node": "^26.3.0",
|
|
57
|
-
"lucide-static": "^1.38.0",
|
|
58
|
-
"typescript": "^7.0.2"
|
|
59
|
-
}
|
|
60
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@imenam/mcp-design",
|
|
3
|
+
"version": "1.0.7",
|
|
4
|
+
"main": "dist/src/index.js",
|
|
5
|
+
"directories": {
|
|
6
|
+
"test": "test"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"generate-version": "node scripts/generate-version.js",
|
|
10
|
+
"build:backend": "npm run generate-version && tsc",
|
|
11
|
+
"build:gui": "cd gui && npm install && npm run build",
|
|
12
|
+
"build": "npm run build:backend && npm run build:gui",
|
|
13
|
+
"start": "node dist/src/index.js",
|
|
14
|
+
"test": "node --test \"dist/test/*.test.js\"",
|
|
15
|
+
"prepublishOnly": "npm run build",
|
|
16
|
+
"release": "npm version patch && npm publish && git push --follow-tags",
|
|
17
|
+
"generate-icons": "node scripts/generate-icons.js"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"mcp",
|
|
21
|
+
"model-context-protocol",
|
|
22
|
+
"design",
|
|
23
|
+
"canvas",
|
|
24
|
+
"ui",
|
|
25
|
+
"mockup",
|
|
26
|
+
"ai",
|
|
27
|
+
"agent"
|
|
28
|
+
],
|
|
29
|
+
"author": "Imena",
|
|
30
|
+
"license": "ISC",
|
|
31
|
+
"description": "MCP server providing a simple design canvas: the agent creates shapes and text, the user refines them in a GUI, and the agent picks the edits back up.",
|
|
32
|
+
"type": "module",
|
|
33
|
+
"bin": {
|
|
34
|
+
"mcp-design": "dist/src/index.js"
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"files": [
|
|
40
|
+
"dist/src",
|
|
41
|
+
"dist/shared",
|
|
42
|
+
"gui/dist",
|
|
43
|
+
"README.md",
|
|
44
|
+
"scripts"
|
|
45
|
+
],
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@hono/node-server": "^2.1.1",
|
|
48
|
+
"@imenam/mcp-gui-interface": "^1.0.12",
|
|
49
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
50
|
+
"@resvg/resvg-js": "^2.6.2",
|
|
51
|
+
"dotenv": "^17.4.2",
|
|
52
|
+
"hono": "^4.13.4",
|
|
53
|
+
"zod": "^4.4.3"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@types/node": "^26.3.0",
|
|
57
|
+
"lucide-static": "^1.38.0",
|
|
58
|
+
"typescript": "^7.0.2"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -1,85 +1,85 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Fige la bibliotheque d'icones dans `shared/icons/` a partir de `lucide-static`.
|
|
4
|
-
*
|
|
5
|
-
* Les fichiers produits sont commites : ni le serveur MCP publie, ni le bundle
|
|
6
|
-
* de la GUI ne dependent de lucide a l'execution, et la geometrie d'un design
|
|
7
|
-
* ne peut pas changer sous les pieds de l'utilisateur au gre d'un `npm install`.
|
|
8
|
-
* Relancer `npm run generate-icons` apres une mise a jour de lucide-static.
|
|
9
|
-
*/
|
|
10
|
-
import fs from 'fs';
|
|
11
|
-
import path from 'path';
|
|
12
|
-
import { createRequire } from 'module';
|
|
13
|
-
import { fileURLToPath } from 'url';
|
|
14
|
-
|
|
15
|
-
const require = createRequire(import.meta.url);
|
|
16
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
17
|
-
const rootDir = path.join(__dirname, '..');
|
|
18
|
-
const outDir = path.join(rootDir, 'shared', 'icons');
|
|
19
|
-
|
|
20
|
-
const iconNodes = require('lucide-static/icon-nodes.json');
|
|
21
|
-
const tags = require('lucide-static/tags.json');
|
|
22
|
-
const { version } = require('lucide-static/package.json');
|
|
23
|
-
|
|
24
|
-
/** Attributs de presentation portes par le noeud du design, jamais par l'icone elle-meme. */
|
|
25
|
-
const DROPPED_ATTRS = new Set(['stroke', 'stroke-width', 'stroke-linecap', 'stroke-linejoin', 'class', 'key']);
|
|
26
|
-
|
|
27
|
-
const names = Object.keys(iconNodes).sort();
|
|
28
|
-
|
|
29
|
-
const geometry = {};
|
|
30
|
-
for (const name of names) {
|
|
31
|
-
geometry[name] = iconNodes[name].map(([tag, attrs]) => {
|
|
32
|
-
const kept = {};
|
|
33
|
-
for (const [attr, value] of Object.entries(attrs ?? {})) {
|
|
34
|
-
if (!DROPPED_ATTRS.has(attr)) kept[attr] = String(value);
|
|
35
|
-
}
|
|
36
|
-
return [tag, kept];
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** Mots-cles de recherche : le nom eclate en segments, plus les synonymes de lucide. */
|
|
41
|
-
const keywords = {};
|
|
42
|
-
for (const name of names) {
|
|
43
|
-
const fromName = name.split('-');
|
|
44
|
-
const merged = [...new Set([...fromName, ...(tags[name] ?? [])])].filter((word) => word.length > 1);
|
|
45
|
-
if (merged.length > 0) keywords[name] = merged;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Les donnees sont embarquees comme une chaine JSON parsee au premier acces
|
|
50
|
-
* plutot que comme un litteral d'objet : un objet de 1800 entrees coute cher a
|
|
51
|
-
* analyser au demarrage, et le module est charge par le serveur stdio comme par
|
|
52
|
-
* le navigateur.
|
|
53
|
-
*/
|
|
54
|
-
function moduleSource(header, exports) {
|
|
55
|
-
const body = exports
|
|
56
|
-
.map(([name, value]) => `export const ${name} = ${JSON.stringify(JSON.stringify(value))};`)
|
|
57
|
-
.join('\n\n');
|
|
58
|
-
return `${header}\n${body}\n`;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const banner = (what) =>
|
|
62
|
-
`// Auto-generated by scripts/generate-icons.js - do not edit manually.\n` +
|
|
63
|
-
`// ${what}\n` +
|
|
64
|
-
`// Source: lucide-static ${version}, ISC licence (https://lucide.dev).\n`;
|
|
65
|
-
|
|
66
|
-
fs.mkdirSync(outDir, { recursive: true });
|
|
67
|
-
fs.writeFileSync(
|
|
68
|
-
path.join(outDir, 'icon-data.ts'),
|
|
69
|
-
moduleSource(banner('Geometry of every icon, in the 24x24 grid.'), [
|
|
70
|
-
['LUCIDE_VERSION', version],
|
|
71
|
-
['ICON_DATA_JSON', geometry],
|
|
72
|
-
]),
|
|
73
|
-
'utf8'
|
|
74
|
-
);
|
|
75
|
-
fs.writeFileSync(
|
|
76
|
-
path.join(outDir, 'icon-keywords.ts'),
|
|
77
|
-
moduleSource(banner('Search keywords per icon. Loaded on demand, never by the renderer.'), [
|
|
78
|
-
['ICON_KEYWORDS_JSON', keywords],
|
|
79
|
-
]),
|
|
80
|
-
'utf8'
|
|
81
|
-
);
|
|
82
|
-
|
|
83
|
-
console.log(`Generated ${names.length} icons from lucide-static ${version}`);
|
|
84
|
-
console.log(` ${path.join(outDir, 'icon-data.ts')}`);
|
|
85
|
-
console.log(` ${path.join(outDir, 'icon-keywords.ts')}`);
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Fige la bibliotheque d'icones dans `shared/icons/` a partir de `lucide-static`.
|
|
4
|
+
*
|
|
5
|
+
* Les fichiers produits sont commites : ni le serveur MCP publie, ni le bundle
|
|
6
|
+
* de la GUI ne dependent de lucide a l'execution, et la geometrie d'un design
|
|
7
|
+
* ne peut pas changer sous les pieds de l'utilisateur au gre d'un `npm install`.
|
|
8
|
+
* Relancer `npm run generate-icons` apres une mise a jour de lucide-static.
|
|
9
|
+
*/
|
|
10
|
+
import fs from 'fs';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import { createRequire } from 'module';
|
|
13
|
+
import { fileURLToPath } from 'url';
|
|
14
|
+
|
|
15
|
+
const require = createRequire(import.meta.url);
|
|
16
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const rootDir = path.join(__dirname, '..');
|
|
18
|
+
const outDir = path.join(rootDir, 'shared', 'icons');
|
|
19
|
+
|
|
20
|
+
const iconNodes = require('lucide-static/icon-nodes.json');
|
|
21
|
+
const tags = require('lucide-static/tags.json');
|
|
22
|
+
const { version } = require('lucide-static/package.json');
|
|
23
|
+
|
|
24
|
+
/** Attributs de presentation portes par le noeud du design, jamais par l'icone elle-meme. */
|
|
25
|
+
const DROPPED_ATTRS = new Set(['stroke', 'stroke-width', 'stroke-linecap', 'stroke-linejoin', 'class', 'key']);
|
|
26
|
+
|
|
27
|
+
const names = Object.keys(iconNodes).sort();
|
|
28
|
+
|
|
29
|
+
const geometry = {};
|
|
30
|
+
for (const name of names) {
|
|
31
|
+
geometry[name] = iconNodes[name].map(([tag, attrs]) => {
|
|
32
|
+
const kept = {};
|
|
33
|
+
for (const [attr, value] of Object.entries(attrs ?? {})) {
|
|
34
|
+
if (!DROPPED_ATTRS.has(attr)) kept[attr] = String(value);
|
|
35
|
+
}
|
|
36
|
+
return [tag, kept];
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Mots-cles de recherche : le nom eclate en segments, plus les synonymes de lucide. */
|
|
41
|
+
const keywords = {};
|
|
42
|
+
for (const name of names) {
|
|
43
|
+
const fromName = name.split('-');
|
|
44
|
+
const merged = [...new Set([...fromName, ...(tags[name] ?? [])])].filter((word) => word.length > 1);
|
|
45
|
+
if (merged.length > 0) keywords[name] = merged;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Les donnees sont embarquees comme une chaine JSON parsee au premier acces
|
|
50
|
+
* plutot que comme un litteral d'objet : un objet de 1800 entrees coute cher a
|
|
51
|
+
* analyser au demarrage, et le module est charge par le serveur stdio comme par
|
|
52
|
+
* le navigateur.
|
|
53
|
+
*/
|
|
54
|
+
function moduleSource(header, exports) {
|
|
55
|
+
const body = exports
|
|
56
|
+
.map(([name, value]) => `export const ${name} = ${JSON.stringify(JSON.stringify(value))};`)
|
|
57
|
+
.join('\n\n');
|
|
58
|
+
return `${header}\n${body}\n`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const banner = (what) =>
|
|
62
|
+
`// Auto-generated by scripts/generate-icons.js - do not edit manually.\n` +
|
|
63
|
+
`// ${what}\n` +
|
|
64
|
+
`// Source: lucide-static ${version}, ISC licence (https://lucide.dev).\n`;
|
|
65
|
+
|
|
66
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
67
|
+
fs.writeFileSync(
|
|
68
|
+
path.join(outDir, 'icon-data.ts'),
|
|
69
|
+
moduleSource(banner('Geometry of every icon, in the 24x24 grid.'), [
|
|
70
|
+
['LUCIDE_VERSION', version],
|
|
71
|
+
['ICON_DATA_JSON', geometry],
|
|
72
|
+
]),
|
|
73
|
+
'utf8'
|
|
74
|
+
);
|
|
75
|
+
fs.writeFileSync(
|
|
76
|
+
path.join(outDir, 'icon-keywords.ts'),
|
|
77
|
+
moduleSource(banner('Search keywords per icon. Loaded on demand, never by the renderer.'), [
|
|
78
|
+
['ICON_KEYWORDS_JSON', keywords],
|
|
79
|
+
]),
|
|
80
|
+
'utf8'
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
console.log(`Generated ${names.length} icons from lucide-static ${version}`);
|
|
84
|
+
console.log(` ${path.join(outDir, 'icon-data.ts')}`);
|
|
85
|
+
console.log(` ${path.join(outDir, 'icon-keywords.ts')}`);
|