@imenam/mcp-gui-interface 1.0.3 → 1.0.5

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 ADDED
@@ -0,0 +1,288 @@
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)
@@ -11,7 +11,7 @@ export class GuiLauncher {
11
11
  restartDelay;
12
12
  constructor(options) {
13
13
  this.options = options;
14
- this.maxRestarts = options.maxRestarts ?? 3;
14
+ this.maxRestarts = options.maxRestarts ?? 0;
15
15
  this.restartDelay = options.restartDelay ?? 2000;
16
16
  this.registerSignalHandlers();
17
17
  }
@@ -6,6 +6,6 @@ export declare class ProxyClient {
6
6
  private status;
7
7
  constructor(proxyUrl: string);
8
8
  getStatus(): "connected" | "fallback" | "error";
9
- register(options: RegisterOptions, fallbackPort: number): Promise<RegisterResult>;
9
+ register(options: RegisterOptions): Promise<RegisterResult>;
10
10
  unregister(): Promise<boolean>;
11
11
  }
@@ -11,7 +11,7 @@ export class ProxyClient {
11
11
  getStatus() {
12
12
  return this.status;
13
13
  }
14
- async register(options, fallbackPort) {
14
+ async register(options) {
15
15
  this.registeredPath = options.path;
16
16
  return new Promise((resolve) => {
17
17
  let url;
@@ -21,7 +21,7 @@ export class ProxyClient {
21
21
  catch {
22
22
  logger.error(`Invalid Proxy URL: ${this.proxyUrl}`);
23
23
  this.status = "error";
24
- return resolve({ success: false, port: fallbackPort, error: "Invalid Proxy URL" });
24
+ return resolve({ success: false, port: 0, error: "Invalid Proxy URL" });
25
25
  }
26
26
  const timeout = 1000;
27
27
  logger.info(`Sending registration request to ${url.href}...`);
@@ -49,33 +49,33 @@ export class ProxyClient {
49
49
  }
50
50
  else {
51
51
  logger.error(`Invalid proxy response schema: ${data}`);
52
- this.status = "fallback";
53
- resolve({ success: false, port: fallbackPort, error: "Invalid proxy response schema" });
52
+ this.status = "error";
53
+ resolve({ success: false, port: 0, error: "Invalid proxy response schema" });
54
54
  }
55
55
  }
56
56
  catch {
57
57
  logger.error(`JSON parse error: ${data}`);
58
- this.status = "fallback";
59
- resolve({ success: false, port: fallbackPort, error: "Invalid JSON response" });
58
+ this.status = "error";
59
+ resolve({ success: false, port: 0, error: "Invalid JSON response" });
60
60
  }
61
61
  }
62
62
  else {
63
63
  logger.error(`Registration rejected with HTTP ${res.statusCode}`);
64
- this.status = "fallback";
65
- resolve({ success: false, port: fallbackPort, error: `HTTP ${res.statusCode}` });
64
+ this.status = "error";
65
+ resolve({ success: false, port: 0, error: `HTTP ${res.statusCode}` });
66
66
  }
67
67
  });
68
68
  });
69
69
  req.on("error", (err) => {
70
70
  logger.error(`Registration error: ${err.message}`);
71
- this.status = "fallback";
72
- resolve({ success: false, port: fallbackPort, error: err.message });
71
+ this.status = "error";
72
+ resolve({ success: false, port: 0, error: err.message });
73
73
  });
74
74
  req.on("timeout", () => {
75
75
  logger.error(`Registration timeout after ${timeout}ms`);
76
76
  req.destroy();
77
- this.status = "fallback";
78
- resolve({ success: false, port: fallbackPort, error: "Timeout" });
77
+ this.status = "error";
78
+ resolve({ success: false, port: 0, error: "Timeout" });
79
79
  });
80
80
  req.write(JSON.stringify(options));
81
81
  req.end();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@imenam/mcp-gui-interface",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
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",