@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 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). **Sans elle, la GUI est simplement désactivée** — les outils MCP continuent de fonctionner. |
57
- | `APP_PATH` | Chemin de montage de la GUI derrière le proxy (défaut `/design-gui`). |
58
- | `APP_NAME` | Nom affiché dans le dashboard du proxy. |
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 vide = GUI désactivée), donc le fichier généré est
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: "http://localhost:",
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
- export function runSetupMcp({ cwd, force }) {
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
  }
@@ -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() : process.env.PROXY_URL ? "error" : "standalone",
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
- const baseHref = c.req.query("__direct") === "1" ? "/" : getHtmlBaseHref(configuredAppPath);
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 (!process.env.PROXY_URL) {
277
- process.stderr.write("[GUI] PROXY_URL non défini — arrêt.\n");
335
+ if (guiMode.kind === "disabled") {
336
+ process.stderr.write(`[GUI] ${guiMode.reason} — arrêt.\n`);
278
337
  process.exit(0);
279
338
  }
280
- const proxyUrl = process.env.PROXY_URL;
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
- const publicUrl = result.url || `${proxyUrl}${appPath}`;
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
- const serverInstance = serve({ fetch: app.fetch, port: finalPort, hostname: "0.0.0.0" }, (info) => {
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 { runSetupMcp } = await import("./cli/setup-mcp.js");
45
- process.exit(runSetupMcp({ cwd: process.cwd(), force: process.argv.slice(2).includes("--force") }));
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 (!process.env.PROXY_URL) {
229
- logger.info("[MASTER] PROXY_URL non défini — GUI désactivée.");
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 (!process.env.PROXY_URL) {
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 not set — the GUI is disabled. Add PROXY_URL to the .env file, then call reconnect_gui again.",
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 (proxy registration in progress). Wait a few seconds, then call reconnect_gui again if needed." },
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 registered with the proxy${guiUrl ? ` at ${guiUrl}` : ""}. Nothing to do (use force=true to restart it anyway).` },
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 and registered with the proxy. URL: ${result.url}` }] };
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 registered by another MCP instance. URL: ${result.url ?? "unknown"}` }] };
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 proxy registration within 15s. Check the logs (${resolveLogDir(rootDir)}), then retry.` },
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
  };
@@ -1 +1 @@
1
- export declare const APP_VERSION = "1.0.6";
1
+ export declare const APP_VERSION = "1.0.7";
@@ -1,2 +1,2 @@
1
1
  // Auto-generated version file - do not edit manually
2
- export const APP_VERSION = '1.0.6';
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.6",
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.11",
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')}`);