@imenam/mcp-gui-interface 1.0.5 → 1.0.6

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
@@ -1,288 +1,261 @@
1
- # @imenam/mcp-gui-interface
2
-
3
- Shared library for MCP GUI lifecycle management — spawning, IPC, proxy registration, and logging.
4
-
5
- This package encapsulates the boilerplate every MCP server needs when it runs a companion GUI: spawning a child process safely, communicating with it over IPC, registering it with the central proxy, and logging without polluting stdout.
6
-
7
- ---
8
-
9
- ## Installation
10
-
11
- ```bash
12
- npm install @imenam/mcp-gui-interface
13
- ```
14
-
15
- No runtime dependencies. Requires Node.js with ESM support (`"type": "module"`).
16
-
17
- ---
18
-
19
- ## Why this library exists
20
-
21
- MCP servers communicate with their host (Cursor, Claude Desktop, etc.) over **stdout** using JSON-RPC. Any `console.log` or stray output on stdout will corrupt the protocol and break the connection.
22
-
23
- When an MCP server also needs to serve a GUI (typically an Express server), it must:
24
-
25
- 1. Spawn the GUI as a **separate child process** so its stdout doesn't pollute the parent's JSON-RPC channel.
26
- 2. Keep the two processes in sync via **typed IPC messages**.
27
- 3. **Register the GUI** with a central proxy so it can be reached at a predictable URL.
28
- 4. **Log safely** — always to stderr and/or a log file, never to stdout.
29
-
30
- This library handles all four concerns.
31
-
32
- ---
33
-
34
- ## API Reference
35
-
36
- ### `GuiLauncher`
37
-
38
- Spawns and supervises a GUI child process.
39
-
40
- ```typescript
41
- import { GuiLauncher } from "@imenam/mcp-gui-interface";
42
-
43
- const launcher = new GuiLauncher({
44
- guiPath: "./dist/gui.js", // Path to the GUI entry point (forked with Node)
45
- env: { PORT: "3000" }, // Extra environment variables passed to the child
46
- maxRestarts: 3, // Max auto-restarts on crash (default: 3)
47
- restartDelay: 2000, // Delay in ms before restarting (default: 2000)
48
- onMessage: (msg) => { // Optional callback for incoming IPC messages
49
- console.error("Received:", msg);
50
- },
51
- });
52
-
53
- launcher.start();
54
- ```
55
-
56
- **Options (`GuiLauncherOptions`)**
57
-
58
- | Property | Type | Default | Description |
59
- |---|---|---|---|
60
- | `guiPath` | `string` | — | Absolute or relative path to the GUI script to fork |
61
- | `env` | `Record<string, string>` | `{}` | Additional env vars merged with `process.env` |
62
- | `maxRestarts` | `number` | `3` | Maximum number of auto-restarts after unexpected crashes |
63
- | `restartDelay` | `number` | `2000` | Milliseconds to wait before each restart attempt |
64
- | `onMessage` | `(msg: IpcMessage) => void` | — | Callback invoked when the GUI sends an IPC message |
65
-
66
- **Methods**
67
-
68
- | Method | Returns | Description |
69
- |---|---|---|
70
- | `start()` | `ChildProcess` | Spawns (or re-spawns) the GUI process |
71
- | `send(message)` | `boolean` | Fire-and-forget IPC message to the GUI |
72
- | `getIpcHub()` | `IpcHub` | Access the underlying `IpcHub` for advanced usage |
73
- | `getProcess()` | `ChildProcess \| null` | The current child process instance |
74
- | `cleanup()` | `Promise<void>` | Gracefully terminates the GUI (SIGTERM → SIGKILL after 3s) |
75
-
76
- The launcher automatically handles `SIGINT`, `SIGTERM`, and stdin `close` events so the GUI is always cleaned up when the MCP server exits.
77
-
78
- ---
79
-
80
- ### `IpcHub`
81
-
82
- Typed IPC messaging between the MCP server (parent) and the GUI (child).
83
-
84
- ```typescript
85
- import { IpcHub } from "@imenam/mcp-gui-interface";
86
-
87
- // Parent side (access via GuiLauncher.getIpcHub())
88
- const ipc = launcher.getIpcHub();
89
-
90
- // Fire-and-forget
91
- ipc.send({ type: "CONFIG_UPDATE", data: { theme: "dark" }, timestamp: new Date().toISOString() });
92
-
93
- // Request/response with correlation ID and timeout
94
- const response = await ipc.request({ type: "GET_STATUS" }, 3000);
95
- console.error(response.data);
96
-
97
- // Subscribe to all incoming messages
98
- ipc.onMessage((msg) => {
99
- if (msg.type === "READY") console.error("GUI is ready");
100
- });
101
- ```
102
-
103
- **`IpcMessage` interface**
104
-
105
- ```typescript
106
- interface IpcMessage {
107
- type: string; // Message type identifier
108
- correlationId?: string; // Auto-set by request() for matching responses
109
- data?: any; // Payload
110
- error?: string; // Error description (used in error responses)
111
- timestamp: string; // ISO 8601 timestamp
112
- }
113
- ```
114
-
115
- **Methods**
116
-
117
- | Method | Description |
118
- |---|---|
119
- | `send(message)` | Sends a typed message. Returns `false` if the process is not connected. |
120
- | `onMessage(callback)` | Subscribes to all incoming messages from the target process. |
121
- | `request(message, timeout?)` | Sends a message and waits for a response with a matching `correlationId`. Rejects on timeout (default: 2000ms) or if the process is disconnected. |
122
-
123
- ---
124
-
125
- ### `ProxyClient`
126
-
127
- Registers and unregisters the GUI with the central HTTP proxy ([`mcp-http-gateway`](https://github.com/Matthieu-Pesnot-Pin/mcp-http-gateway)).
128
-
129
- ```typescript
130
- import { ProxyClient } from "@imenam/mcp-gui-interface";
131
-
132
- const proxy = new ProxyClient("http://localhost:4242");
133
-
134
- // Register — proxy allocates a port and returns it
135
- const result = await proxy.register(
136
- { path: "/my-app", name: "my-app" },
137
- fallbackPort // Used if proxy is unreachable
138
- );
139
-
140
- if (result.success) {
141
- console.error(`GUI available at port ${result.port}`);
142
- } else {
143
- console.error(`Running on fallback port ${fallbackPort}`);
144
- }
145
-
146
- // Check current status
147
- proxy.getStatus(); // "connected" | "fallback" | "error"
148
-
149
- // Unregister on shutdown
150
- await proxy.unregister();
151
- ```
152
-
153
- **`RegisterOptions`**
154
-
155
- | Property | Type | Description |
156
- |---|---|---|
157
- | `path` | `string` | The URL path to register (e.g. `/my-app`) |
158
- | `name` | `string` | Optional display name for the app |
159
- | `port` | `number` | Optional preferred port |
160
-
161
- **`RegisterResult`**
162
-
163
- | Property | Type | Description |
164
- |---|---|---|
165
- | `success` | `boolean` | Whether registration succeeded |
166
- | `port` | `number` | Allocated port (or `fallbackPort` on failure) |
167
- | `url` | `string \| undefined` | Full URL returned by the proxy |
168
- | `error` | `string \| undefined` | Error description on failure |
169
-
170
- The proxy registration has a 1-second timeout. If the proxy is unreachable, `status` is set to `"fallback"` and the fallback port is used — the GUI still starts normally.
171
-
172
- ---
173
-
174
- ### `setupLogging` / `createLogger`
175
-
176
- Safe logging for MCP servers, keeping stdout clean for JSON-RPC.
177
-
178
- ```typescript
179
- import { setupLogging, createLogger } from "@imenam/mcp-gui-interface";
180
-
181
- // Call once at startup in the MCP server process
182
- setupLogging({
183
- processLabel: "MY-MCP", // Label used in log file entries
184
- logDir: "./logs", // Optional. Default: .mcp-gui/logs/
185
- });
186
-
187
- // Create scoped loggers anywhere in the codebase
188
- const logger = createLogger("MyModule");
189
-
190
- logger.info("Server started");
191
- logger.warn("Config missing, using defaults");
192
- logger.error("Failed to connect", { reason: "timeout" });
193
- logger.debug("Verbose detail");
194
- ```
195
-
196
- **What `setupLogging` does:**
197
- - Redirects `console.log`, `console.info`, `console.warn` → `console.error` (protecting stdout).
198
- - Patches `console.error` to also append every line to a rotating log file at `.mcp-gui/logs/server.log`.
199
- - Log entries include ISO timestamp, PID, and process label.
200
-
201
- **`SetupLoggingOptions`**
202
-
203
- | Property | Type | Default | Description |
204
- |---|---|---|---|
205
- | `processLabel` | `string` | — | Label shown in log file lines |
206
- | `logDir` | `string` | `.mcp-gui/logs` | Directory where `server.log` is written |
207
-
208
- **Log format:**
209
- ```
210
- [2026-03-10T12:00:00.000Z] [PID 1234] [MY-MCP] [INFO] [MyModule] Server started
211
- ```
212
-
213
- ---
214
-
215
- ## Complete usage example
216
-
217
- Here is a typical MCP server that uses all four features:
218
-
219
- ```typescript
220
- import { GuiLauncher, ProxyClient, setupLogging, createLogger } from "@imenam/mcp-gui-interface";
221
-
222
- // 1. Safe logging (must be called first)
223
- setupLogging({ processLabel: "MY-MCP" });
224
- const logger = createLogger("Main");
225
-
226
- // 2. Register the GUI with the proxy
227
- const proxy = new ProxyClient(process.env.PROXY_URL ?? "http://localhost:4242");
228
- const result = await proxy.register({ path: "/my-app", name: "my-app" }, 3001);
229
- const port = result.port;
230
-
231
- // 3. Launch the GUI child process
232
- const launcher = new GuiLauncher({
233
- guiPath: new URL("./gui.js", import.meta.url).pathname,
234
- env: { PORT: String(port) },
235
- onMessage: (msg) => {
236
- if (msg.type === "READY") logger.info("GUI reported ready");
237
- },
238
- });
239
-
240
- launcher.start();
241
- logger.info(`GUI process started on port ${port}`);
242
-
243
- // 4. Two-way IPC: request data from the GUI
244
- const ipc = launcher.getIpcHub();
245
- const status = await ipc.request({ type: "GET_STATUS" });
246
- logger.info("GUI status:", status.data);
247
- ```
248
-
249
- ---
250
-
251
- ## Type reference
252
-
253
- All public types are exported from the package root:
254
-
255
- ```typescript
256
- import type {
257
- GuiLauncherOptions,
258
- IpcMessage,
259
- RegisterOptions,
260
- RegisterResult,
261
- ProxyConfig,
262
- Logger,
263
- SetupLoggingOptions,
264
- } from "@imenam/mcp-gui-interface";
265
- ```
266
-
267
- ---
268
-
269
- ## Development
270
-
271
- ```bash
272
- # Build
273
- npm run build
274
-
275
- # Watch mode
276
- npm run build:watch
277
-
278
- # Release (patch bump, build, publish, push tag)
279
- npm run release
280
- ```
281
-
282
- The compiled output and type declarations are in `dist/`.
283
-
284
- ---
285
-
286
- ## License
287
-
288
- ISC © [Matthieu Pesnot-Pin](https://github.com/Matthieu-Pesnot-Pin)
1
+ # @imenam/mcp-gui-interface
2
+
3
+ Shared library for MCP GUI lifecycle management — spawning, IPC, proxy registration, and logging.
4
+
5
+ This package encapsulates the boilerplate many MCP servers need when they run a companion GUI: spawning a child process safely, communicating over IPC, registering with the central HTTP proxy, and logging without polluting **stdout** (reserved for MCP JSON-RPC on the parent process).
6
+
7
+ ---
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ npm install @imenam/mcp-gui-interface
13
+ ```
14
+
15
+ No runtime dependencies. Requires Node.js with ESM support (`"type": "module"` in consuming packages).
16
+
17
+ ---
18
+
19
+ ## Scope: Node only
20
+
21
+ This package targets **Node.js processes** (MCP master and GUI worker). It is **not** meant for browser or Vite bundles — do not add it as a dependency of a React/Vite front-end.
22
+
23
+ ---
24
+
25
+ ## How it fits together
26
+
27
+ 1. **MCP host** (e.g. Cursor) talks **only** to the **master** process over **stdio** (JSON-RPC). Nothing from this library runs in the host.
28
+ 2. The **master** may use **`GuiLauncher`** to **fork** a second Node process: the **GUI worker**. They talk over Node **IPC** (`process.send` / `message`).
29
+ 3. The **worker** starts an HTTP server (Express, Hono, …) and usually uses **`ProxyClient`** to register with your **HTTP proxy** so the UI gets a stable URL. It also serves the built SPA (static files).
30
+ 4. The **browser** loads that SPA. It talks to the worker via **HTTP** (`fetch`, SSE). It does **not** import this npm package—the worker is already a Node server.
31
+
32
+ So: **two Node processes** (master + worker) may both depend on this package; the **browser bundle** must not.
33
+
34
+ ```mermaid
35
+ flowchart LR
36
+ Host[MCP host]
37
+ Master[Master process]
38
+ Worker[GUI worker Node]
39
+ Proxy[HTTP proxy]
40
+ Browser[Browser]
41
+
42
+ Host <-->|stdio JSON-RPC| Master
43
+ Master -->|fork + IPC| Worker
44
+ Worker -->|register| Proxy
45
+ Browser -->|HTTP| Proxy
46
+ Proxy --> Worker
47
+ ```
48
+
49
+ For a longer French walkthrough, see **MCP Documentor** → project **`mcp-gui-interface`** → *Guide d’utilisation — @imenam/mcp-gui-interface*.
50
+
51
+ ---
52
+
53
+ ## Why this library exists
54
+
55
+ MCP servers talk to the host (Cursor, Claude Desktop, etc.) over **stdout** using JSON-RPC. Any stray `console.log` on stdout can corrupt the protocol.
56
+
57
+ When an MCP server also serves a GUI (Express, Hono, etc.), it typically must:
58
+
59
+ 1. **Fork** the GUI as a **separate child process** so the GUI’s stdout does not mix with the parent’s JSON-RPC channel.
60
+ 2. Exchange messages over **Node IPC** (`process.send` / `message`).
61
+ 3. **Register** the GUI with the central proxy ([mcp-http-gateway](https://github.com/Matthieu-Pesnot-Pin/mcp-http-gateway)) so it is reachable under a stable path.
62
+ 4. **Log safely** — stderr and/or a log file; never rely on stdout for logs in the master process.
63
+
64
+ This library supports these patterns via `GuiLauncher`, `IpcHub`, `ProxyClient`, and `setupLogging` / `createLogger`.
65
+
66
+ ---
67
+
68
+ ## API reference
69
+
70
+ ### `GuiLauncher`
71
+
72
+ Spawns and supervises a GUI child process (`fork` with `stdio: ['ignore', 'pipe', 'inherit', 'ipc']` — child stdout is piped to the parent’s stderr).
73
+
74
+ ```typescript
75
+ import { GuiLauncher } from "@imenam/mcp-gui-interface";
76
+
77
+ const launcher = new GuiLauncher({
78
+ guiPath: "./dist/gui-worker.js",
79
+ env: { ...process.env } as Record<string, string>,
80
+ maxRestarts: 0, // default: 0 — no auto-restart unless you opt in
81
+ restartDelay: 2000, // default: 2000 ms between restart attempts
82
+ onMessage: (msg) => {
83
+ console.error("Received:", msg);
84
+ },
85
+ });
86
+
87
+ launcher.start();
88
+ ```
89
+
90
+ **`GuiLauncherOptions`**
91
+
92
+ | Property | Type | Default | Description |
93
+ |----------------|------------------------------|---------|-------------|
94
+ | `guiPath` | `string` | — | Path to the GUI entry script (forked with Node) |
95
+ | `env` | `Record<string, string>` | — | Extra env vars merged with `process.env` |
96
+ | `maxRestarts` | `number` | **`0`** | Auto-restarts after unexpected exit (non-zero code) |
97
+ | `restartDelay` | `number` | `2000` | Ms to wait before each restart |
98
+ | `onMessage` | `(msg: IpcMessage) => void` | — | Incoming IPC from the child |
99
+
100
+ **Methods**
101
+
102
+ | Method | Returns | Description |
103
+ |------------------|----------------------|-------------|
104
+ | `start()` | `ChildProcess` | Spawns or respawns the GUI |
105
+ | `send(message)` | `boolean` | Sends IPC to the child (timestamp added internally) |
106
+ | `getIpcHub()` | `IpcHub` | Lower-level IPC helper |
107
+ | `getProcess()` | `ChildProcess \| null` | Current child, if any |
108
+ | `cleanup()` | `Promise<void>` | SIGTERM, then SIGKILL after 3s if needed |
109
+
110
+ The launcher registers handlers for `SIGINT`, `SIGTERM`, parent `exit`, and stdin `close` to tear down the child.
111
+
112
+ ---
113
+
114
+ ### `IpcHub`
115
+
116
+ Typed IPC between parent and child. Usually obtained via `launcher.getIpcHub()`.
117
+
118
+ ```typescript
119
+ const ipc = launcher.getIpcHub();
120
+
121
+ ipc.send({
122
+ type: "CONFIG_UPDATE",
123
+ data: { theme: "dark" },
124
+ timestamp: new Date().toISOString(),
125
+ });
126
+
127
+ const response = await ipc.request({ type: "GET_STATUS" }, 3000);
128
+ ```
129
+
130
+ **`IpcMessage`**
131
+
132
+ ```typescript
133
+ interface IpcMessage {
134
+ type: string;
135
+ correlationId?: string;
136
+ data?: any;
137
+ error?: string;
138
+ timestamp: string;
139
+ }
140
+ ```
141
+
142
+ **Methods**
143
+
144
+ | Method | Description |
145
+ |----------------------------|-------------|
146
+ | `send(message)` | Returns `false` if not connected |
147
+ | `onMessage(callback)` | Only delivers objects with a `type` field |
148
+ | `request(message, timeout?)` | Default timeout **2000 ms** |
149
+
150
+ ---
151
+
152
+ ### `ProxyClient`
153
+
154
+ Registers / unregisters the GUI with the proxy (`POST /proxy/register`, `DELETE /proxy/unregister`).
155
+
156
+ **Current API (source / recent releases):**
157
+
158
+ ```typescript
159
+ import { ProxyClient } from "@imenam/mcp-gui-interface";
160
+
161
+ const proxy = new ProxyClient(process.env.PROXY_URL!);
162
+
163
+ const result = await proxy.register({ path: "/my-app", name: "My App" });
164
+
165
+ if (result.success) {
166
+ console.error(`Listening on port ${result.port}`, result.url);
167
+ } else {
168
+ console.error(result.error, result.port); // port is 0 on failure
169
+ }
170
+
171
+ await proxy.unregister();
172
+ ```
173
+
174
+ - Registration HTTP timeout: **1000 ms** (unregister: **2000 ms**).
175
+ - On failure, `RegisterResult.port` is **`0`** in the current implementation.
176
+
177
+ > **Older npm versions** may have exposed `register(options, fallbackPort)`. Check `node_modules/@imenam/mcp-gui-interface/dist/src/proxy-client.d.ts` for the exact signature you have installed.
178
+
179
+ **`getStatus()`:** `"connected" | "fallback" | "error"` — many failure paths set `"error"`.
180
+
181
+ ---
182
+
183
+ ### `setupLogging` / `createLogger`
184
+
185
+ Safe logging for MCP servers: protect stdout, mirror errors to a file.
186
+
187
+ ```typescript
188
+ import { setupLogging, createLogger } from "@imenam/mcp-gui-interface";
189
+
190
+ setupLogging({
191
+ processLabel: "MY-MCP",
192
+ logDir: "./logs", // optional; overrides MCP_LOG_DIR env var and .mcp-gui/logs default
193
+ });
194
+
195
+ const logger = createLogger("MyModule");
196
+ logger.info("Server started");
197
+ ```
198
+
199
+ **`setupLogging`**
200
+
201
+ - Runs once per process (subsequent calls are no-ops).
202
+ - Creates `logDir`, appends to **`server.log`** (single file — **no built-in rotation**).
203
+ - Redirects `console.log` / `info` / `warn` → `console.error`.
204
+ - Patches `console.error` to also append to the log file.
205
+
206
+ **Log directory resolution order:**
207
+
208
+ 1. `logDir` option passed to `setupLogging`
209
+ 2. `MCP_LOG_DIR` environment variable
210
+ 3. `.mcp-gui/logs` relative to `process.cwd()` (default)
211
+
212
+ **`createLogger(scope)`** — logs to stderr with `[LEVEL] [scope] …`.
213
+
214
+ ---
215
+
216
+ ## Full-stack usage sketch
217
+
218
+ Typical split:
219
+
220
+ 1. **Master:** `setupLogging`, optional `GuiLauncher` + `start()` when `PROXY_URL` (or your policy) allows.
221
+ 2. **GUI worker:** `setupLogging`, `ProxyClient.register`, bind HTTP server to returned port, `unregister` on shutdown.
222
+
223
+ The master example in older docs that called `ProxyClient` in the same process as `GuiLauncher` is valid for simple layouts; **Hono-based** MCPs (e.g. MCP Documentor) often register the proxy **only** in the worker.
224
+
225
+ ---
226
+
227
+ ## Exported types
228
+
229
+ ```typescript
230
+ import type {
231
+ GuiLauncherOptions,
232
+ IpcMessage,
233
+ RegisterOptions,
234
+ RegisterResult,
235
+ ProxyConfig,
236
+ Logger,
237
+ SetupLoggingOptions,
238
+ } from "@imenam/mcp-gui-interface";
239
+ ```
240
+
241
+ ---
242
+
243
+ ## Development
244
+
245
+ ```bash
246
+ npm run build # tsc → dist/
247
+ npm run build:watch
248
+ npm run release # version patch, build, publish, push tags
249
+ ```
250
+
251
+ ---
252
+
253
+ ## Documentation interne
254
+
255
+ Un **guide d’utilisation** (comment intégrer la lib dans un MCP + GUI) est maintenu dans **MCP Documentor**, projet **`mcp-gui-interface`**, entrée *Guide d’utilisation — @imenam/mcp-gui-interface* (dossier `guide`).
256
+
257
+ ---
258
+
259
+ ## License
260
+
261
+ ISC © [Matthieu Pesnot-Pin](https://github.com/Matthieu-Pesnot-Pin)
@@ -40,7 +40,7 @@ export function setupLogging(options) {
40
40
  if (loggingInitialized)
41
41
  return;
42
42
  currentProcessLabel = options.processLabel;
43
- const resolvedLogDir = options.logDir ?? path.join(process.cwd(), ".mcp-gui", "logs");
43
+ const resolvedLogDir = options.logDir ?? process.env.MCP_LOG_DIR ?? path.join(process.cwd(), ".mcp-gui", "logs");
44
44
  fs.mkdirSync(resolvedLogDir, { recursive: true });
45
45
  const logFilePath = path.join(resolvedLogDir, "server.log");
46
46
  logStream = fs.createWriteStream(logFilePath, { flags: "a", encoding: "utf8" });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@imenam/mcp-gui-interface",
3
- "version": "1.0.5",
3
+ "version": "1.0.6",
4
4
  "description": "Shared library for MCP GUI lifecycle management — spawning, IPC, proxy registration, logging",
5
5
  "type": "module",
6
6
  "main": "./dist/src/index.js",