@imenam/mcp-gui-interface 1.0.11 → 1.0.12
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 +327 -276
- package/dist/src/app-config.d.ts +116 -0
- package/dist/src/app-config.js +161 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/proxy-client.js +43 -10
- package/dist/src/types.d.ts +19 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,276 +1,327 @@
|
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
+
#### Path & name resolution (`APP_PATH`, `APP_PATH_PREFIX`, …)
|
|
178
|
+
|
|
179
|
+
`register({ path })` treats `path` as the MCP's **natural** path — the default
|
|
180
|
+
hardcoded in its own source. The effective path is resolved from the
|
|
181
|
+
environment, and comes back in `result.path`:
|
|
182
|
+
|
|
183
|
+
| Variable | Role |
|
|
184
|
+
|---|---|
|
|
185
|
+
| `APP_PATH` | **Canonical.** Explicit mount path. Replaces the natural path. |
|
|
186
|
+
| `PROXY_APP_PATH` | Fallback (Express MCPs), used when `APP_PATH` is unset. |
|
|
187
|
+
| `GUI_PATH` | Fallback (gitHubMCP), used when the two above are unset. |
|
|
188
|
+
| `APP_PATH_PREFIX` | Namespace prefixed to the **natural** path. |
|
|
189
|
+
| `APP_NAME` / `PROXY_APP_NAME` / `GUI_NAME` | Display name, same priority order. |
|
|
190
|
+
| `APP_GROUP` | Proxy dashboard group (empty value = ungrouped). |
|
|
191
|
+
|
|
192
|
+
**The rule, exactly:**
|
|
193
|
+
|
|
194
|
+
1. If any of `APP_PATH` → `PROXY_APP_PATH` → `GUI_PATH` is set to a non-empty
|
|
195
|
+
value, that value is the path, **taken as-is and never prefixed**.
|
|
196
|
+
2. Otherwise the natural path is used, with `APP_PATH_PREFIX` prepended.
|
|
197
|
+
3. Both are normalised: leading `/` added, `//` collapsed, trailing `/` removed.
|
|
198
|
+
An empty prefix, or `/`, has no effect.
|
|
199
|
+
4. Prefixing is idempotent — a path already under the prefix is left alone, so
|
|
200
|
+
resolving twice is safe.
|
|
201
|
+
|
|
202
|
+
**Why an explicit path is never prefixed.** `APP_PATH_PREFIX` exists for a
|
|
203
|
+
supervisor that namespaces the GUIs it starts without knowing their natural
|
|
204
|
+
paths. Someone who types `APP_PATH=/whatever` by hand is stating the *final*
|
|
205
|
+
path and must get exactly that; the prefix only namespaces a value nobody
|
|
206
|
+
chose. Setting `APP_PATH` is therefore also the way to opt out of prefixing.
|
|
207
|
+
|
|
208
|
+
```typescript
|
|
209
|
+
// APP_PATH_PREFIX=/abc123, no APP_PATH:
|
|
210
|
+
const r = await proxy.register({ path: "/documentor-gui" });
|
|
211
|
+
r.path; // "/abc123/documentor-gui"
|
|
212
|
+
r.url; // "http://localhost:3000/abc123/documentor-gui"
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
> **Always build the SPA's `<base href>` and any public URL from `result.path` /
|
|
216
|
+
> `result.url`**, never from the path you passed in — otherwise assets are
|
|
217
|
+
> requested one path segment too high and the page loads blank.
|
|
218
|
+
|
|
219
|
+
The same resolution is exported standalone, for code that needs the path before
|
|
220
|
+
registering:
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
import { resolveAppPath, resolveAppName, resolveAppGroup } from "@imenam/mcp-gui-interface";
|
|
224
|
+
|
|
225
|
+
const appPath = resolveAppPath("/documentor-gui"); // same rule, idempotent
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
**Grouping (`group` / `APP_GROUP`):** pass an optional `group` in the register
|
|
229
|
+
options to place the app under a collapsible section (folded by default) in the
|
|
230
|
+
proxy dashboard. Apps sharing the same `group` are shown together; apps without
|
|
231
|
+
one land in a default "ungrouped" section. When `group` is omitted, `register`
|
|
232
|
+
falls back to the **`APP_GROUP`** environment variable, so an MCP can enable
|
|
233
|
+
grouping purely from its `.env` without touching code:
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
// Explicit:
|
|
237
|
+
await proxy.register({ path: "/my-app", name: "My App", group: "Google" });
|
|
238
|
+
|
|
239
|
+
// Or, with APP_GROUP=Google in the environment, simply:
|
|
240
|
+
await proxy.register({ path: "/my-app", name: "My App" });
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
> **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.
|
|
244
|
+
|
|
245
|
+
**`getStatus()`:** `"connected" | "fallback" | "error"` — many failure paths set `"error"`.
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
### `setupLogging` / `createLogger`
|
|
250
|
+
|
|
251
|
+
Safe logging for MCP servers: protect stdout, mirror errors to a file.
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
import { setupLogging, createLogger } from "@imenam/mcp-gui-interface";
|
|
255
|
+
|
|
256
|
+
setupLogging({
|
|
257
|
+
processLabel: "MY-MCP",
|
|
258
|
+
logDir: "./logs", // optional; overrides MCP_LOG_DIR env var and .mcp-gui/logs default
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
const logger = createLogger("MyModule");
|
|
262
|
+
logger.info("Server started");
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
**`setupLogging`**
|
|
266
|
+
|
|
267
|
+
- Runs once per process (subsequent calls are no-ops).
|
|
268
|
+
- Creates `logDir`, appends to **`server.log`** (single file — **no built-in rotation**).
|
|
269
|
+
- Redirects `console.log` / `info` / `warn` → `console.error`.
|
|
270
|
+
- Patches `console.error` to also append to the log file.
|
|
271
|
+
|
|
272
|
+
**Log directory resolution order:**
|
|
273
|
+
|
|
274
|
+
1. `logDir` option passed to `setupLogging`
|
|
275
|
+
2. `MCP_LOG_DIR` environment variable
|
|
276
|
+
3. `.mcp-gui/logs` relative to `process.cwd()` (default)
|
|
277
|
+
|
|
278
|
+
**`createLogger(scope)`** — logs to stderr with `[LEVEL] [scope] …`.
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
## Full-stack usage sketch
|
|
283
|
+
|
|
284
|
+
Typical split:
|
|
285
|
+
|
|
286
|
+
1. **Master:** `setupLogging`, optional `GuiLauncher` + `start()` when `PROXY_URL` (or your policy) allows.
|
|
287
|
+
2. **GUI worker:** `setupLogging`, `ProxyClient.register`, bind HTTP server to returned port, `unregister` on shutdown.
|
|
288
|
+
|
|
289
|
+
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.
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
## Exported types
|
|
294
|
+
|
|
295
|
+
```typescript
|
|
296
|
+
import type {
|
|
297
|
+
GuiLauncherOptions,
|
|
298
|
+
IpcMessage,
|
|
299
|
+
RegisterOptions,
|
|
300
|
+
RegisterResult,
|
|
301
|
+
ProxyConfig,
|
|
302
|
+
Logger,
|
|
303
|
+
SetupLoggingOptions,
|
|
304
|
+
} from "@imenam/mcp-gui-interface";
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
---
|
|
308
|
+
|
|
309
|
+
## Development
|
|
310
|
+
|
|
311
|
+
```bash
|
|
312
|
+
npm run build # tsc → dist/
|
|
313
|
+
npm run build:watch
|
|
314
|
+
npm run release # version patch, build, publish, push tags
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
---
|
|
318
|
+
|
|
319
|
+
## Documentation interne
|
|
320
|
+
|
|
321
|
+
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`).
|
|
322
|
+
|
|
323
|
+
---
|
|
324
|
+
|
|
325
|
+
## License
|
|
326
|
+
|
|
327
|
+
ISC © [Matthieu Pesnot-Pin](https://github.com/Matthieu-Pesnot-Pin)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolution of the GUI's mount path / display name from the environment.
|
|
3
|
+
*
|
|
4
|
+
* Three variable conventions coexist in the ecosystem, for historical reasons:
|
|
5
|
+
*
|
|
6
|
+
* | Convention | Used by |
|
|
7
|
+
* |-------------------|-------------------------------------------|
|
|
8
|
+
* | `APP_*` | Hono-based MCPs (documentor, design) |
|
|
9
|
+
* | `PROXY_APP_*` | Express-based MCPs (scraper, ticket, …) |
|
|
10
|
+
* | `GUI_*` | gitHubMCP |
|
|
11
|
+
*
|
|
12
|
+
* `APP_PATH` / `APP_NAME` are now the **canonical** names. The two other
|
|
13
|
+
* conventions remain accepted as fallbacks, in decreasing priority order
|
|
14
|
+
* (`APP_*` → `PROXY_APP_*` → `GUI_*`), silently: they are legitimate today and
|
|
15
|
+
* emitting warnings would pollute every existing deployment's logs.
|
|
16
|
+
*
|
|
17
|
+
* ---
|
|
18
|
+
*
|
|
19
|
+
* ## `APP_PATH_PREFIX`
|
|
20
|
+
*
|
|
21
|
+
* A supervisor (the MCP hub) needs to namespace every GUI it starts, so that
|
|
22
|
+
* two gateways hosting the same MCP do not fight over the same proxy path
|
|
23
|
+
* (the proxy answers 409 on a live duplicate). The supervisor, however, does
|
|
24
|
+
* **not** know the "natural" path an MCP picks for itself — that value lives
|
|
25
|
+
* in the MCP's own source (`/documentor-gui`, `/ticket-manager`, …) and is
|
|
26
|
+
* deliberately not something the hub stores or asks the user to type.
|
|
27
|
+
*
|
|
28
|
+
* Hence `APP_PATH_PREFIX`: the supervisor writes `APP_PATH_PREFIX=/<slug>` and
|
|
29
|
+
* the MCP keeps choosing its own path.
|
|
30
|
+
*
|
|
31
|
+
* ### The exact rule
|
|
32
|
+
*
|
|
33
|
+
* 1. An **explicit** path — any of `APP_PATH`, `PROXY_APP_PATH`, `GUI_PATH`
|
|
34
|
+
* set to a non-empty value — is taken **as-is, never prefixed**.
|
|
35
|
+
* 2. Otherwise the MCP's **natural path** (the default hardcoded in its own
|
|
36
|
+
* code, passed here as `fallbackPath`) is prefixed with `APP_PATH_PREFIX`.
|
|
37
|
+
* 3. Normalisation, applied in both cases: a leading `/` is added if missing,
|
|
38
|
+
* runs of `/` are collapsed to one, and a trailing `/` is dropped (`/` alone
|
|
39
|
+
* survives). An empty prefix, or a prefix of `/`, has no effect at all.
|
|
40
|
+
* 4. Prefixing is **idempotent**: a path that already starts with the prefix
|
|
41
|
+
* (`/abc123` or `/abc123/…`) is returned unchanged, so resolving twice — for
|
|
42
|
+
* instance once by the consumer for its `<base href>` and once inside
|
|
43
|
+
* {@link ProxyClient.register} — never yields `/abc123/abc123/…`.
|
|
44
|
+
*
|
|
45
|
+
* ### Why rule 1
|
|
46
|
+
*
|
|
47
|
+
* Because the operator's word wins. Someone who types `APP_PATH=/whatever`
|
|
48
|
+
* into an MCP's env block is stating the *final* path they want to see in the
|
|
49
|
+
* proxy, and has no reason to expect an invisible prefix to be spliced in. The
|
|
50
|
+
* hub's prefix is an automatic, best-effort namespacing of a value nobody
|
|
51
|
+
* chose; a hand-written `APP_PATH` is a value somebody chose. Rule 1 also
|
|
52
|
+
* makes the behaviour trivially predictable — "the prefix applies to the
|
|
53
|
+
* default path, and only to it" — and leaves a built-in escape hatch: to opt
|
|
54
|
+
* out of prefixing, set `APP_PATH` explicitly.
|
|
55
|
+
*
|
|
56
|
+
* The trade-off is that an MCP whose *own* `.env` already pins
|
|
57
|
+
* `PROXY_APP_PATH` will not be namespaced by a supervisor. That is the
|
|
58
|
+
* intended reading of "manual override wins"; a supervisor that needs the
|
|
59
|
+
* namespace guaranteed must not let such a value through.
|
|
60
|
+
*/
|
|
61
|
+
/** Env var names for the GUI path, in decreasing priority order. */
|
|
62
|
+
export declare const APP_PATH_ENV_VARS: readonly ["APP_PATH", "PROXY_APP_PATH", "GUI_PATH"];
|
|
63
|
+
/** Env var names for the GUI display name, in decreasing priority order. */
|
|
64
|
+
export declare const APP_NAME_ENV_VARS: readonly ["APP_NAME", "PROXY_APP_NAME", "GUI_NAME"];
|
|
65
|
+
/** Env var holding the namespace prefix applied to the natural path. */
|
|
66
|
+
export declare const APP_PATH_PREFIX_ENV_VAR = "APP_PATH_PREFIX";
|
|
67
|
+
/** Env var holding the proxy dashboard group label. */
|
|
68
|
+
export declare const APP_GROUP_ENV_VAR = "APP_GROUP";
|
|
69
|
+
type Env = Record<string, string | undefined>;
|
|
70
|
+
/**
|
|
71
|
+
* Normalises a mount path: leading `/`, no duplicate `/`, no trailing `/`.
|
|
72
|
+
* An empty input yields `"/"`.
|
|
73
|
+
*/
|
|
74
|
+
export declare function normalizeAppPath(value: string | undefined | null): string;
|
|
75
|
+
/**
|
|
76
|
+
* Normalises a prefix. Returns `""` when the prefix is absent, empty or `/`,
|
|
77
|
+
* i.e. when it must have no effect.
|
|
78
|
+
*/
|
|
79
|
+
export declare function normalizeAppPathPrefix(value: string | undefined | null): string;
|
|
80
|
+
/**
|
|
81
|
+
* Prefixes `path` with `prefix` (see the module docblock for the exact rule).
|
|
82
|
+
* Idempotent: a path already under the prefix is returned unchanged.
|
|
83
|
+
*/
|
|
84
|
+
export declare function applyAppPathPrefix(path: string, prefix: string | undefined | null): string;
|
|
85
|
+
/** Detailed outcome of {@link resolveAppPath}, for logging or diagnostics. */
|
|
86
|
+
export interface ResolvedAppPath {
|
|
87
|
+
/** The path to register with the proxy and to use as `<base href>`. */
|
|
88
|
+
path: string;
|
|
89
|
+
/** Name of the env var the path came from, when it was set explicitly. */
|
|
90
|
+
source?: (typeof APP_PATH_ENV_VARS)[number];
|
|
91
|
+
/** Normalised prefix actually in play (`""` when none). */
|
|
92
|
+
prefix: string;
|
|
93
|
+
/** Whether {@link prefix} was applied to {@link path}. */
|
|
94
|
+
prefixed: boolean;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Resolves the GUI mount path.
|
|
98
|
+
*
|
|
99
|
+
* @param fallbackPath The MCP's natural path, hardcoded in its own source
|
|
100
|
+
* (e.g. `/documentor-gui`). Only this value gets prefixed.
|
|
101
|
+
* @param env Environment to read (defaults to `process.env`).
|
|
102
|
+
*/
|
|
103
|
+
export declare function resolveAppPathDetailed(fallbackPath: string, env?: Env): ResolvedAppPath;
|
|
104
|
+
/** Shorthand for {@link resolveAppPathDetailed}().path. */
|
|
105
|
+
export declare function resolveAppPath(fallbackPath: string, env?: Env): string;
|
|
106
|
+
/**
|
|
107
|
+
* Resolves the GUI display name from `APP_NAME` → `PROXY_APP_NAME` →
|
|
108
|
+
* `GUI_NAME`, falling back to `fallbackName`.
|
|
109
|
+
*/
|
|
110
|
+
export declare function resolveAppName(fallbackName: string | undefined, env?: Env): string | undefined;
|
|
111
|
+
/**
|
|
112
|
+
* Resolves the proxy dashboard group from `APP_GROUP`. An empty or
|
|
113
|
+
* whitespace-only value means "ungrouped" and yields `undefined`.
|
|
114
|
+
*/
|
|
115
|
+
export declare function resolveAppGroup(env?: Env): string | undefined;
|
|
116
|
+
export {};
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolution of the GUI's mount path / display name from the environment.
|
|
3
|
+
*
|
|
4
|
+
* Three variable conventions coexist in the ecosystem, for historical reasons:
|
|
5
|
+
*
|
|
6
|
+
* | Convention | Used by |
|
|
7
|
+
* |-------------------|-------------------------------------------|
|
|
8
|
+
* | `APP_*` | Hono-based MCPs (documentor, design) |
|
|
9
|
+
* | `PROXY_APP_*` | Express-based MCPs (scraper, ticket, …) |
|
|
10
|
+
* | `GUI_*` | gitHubMCP |
|
|
11
|
+
*
|
|
12
|
+
* `APP_PATH` / `APP_NAME` are now the **canonical** names. The two other
|
|
13
|
+
* conventions remain accepted as fallbacks, in decreasing priority order
|
|
14
|
+
* (`APP_*` → `PROXY_APP_*` → `GUI_*`), silently: they are legitimate today and
|
|
15
|
+
* emitting warnings would pollute every existing deployment's logs.
|
|
16
|
+
*
|
|
17
|
+
* ---
|
|
18
|
+
*
|
|
19
|
+
* ## `APP_PATH_PREFIX`
|
|
20
|
+
*
|
|
21
|
+
* A supervisor (the MCP hub) needs to namespace every GUI it starts, so that
|
|
22
|
+
* two gateways hosting the same MCP do not fight over the same proxy path
|
|
23
|
+
* (the proxy answers 409 on a live duplicate). The supervisor, however, does
|
|
24
|
+
* **not** know the "natural" path an MCP picks for itself — that value lives
|
|
25
|
+
* in the MCP's own source (`/documentor-gui`, `/ticket-manager`, …) and is
|
|
26
|
+
* deliberately not something the hub stores or asks the user to type.
|
|
27
|
+
*
|
|
28
|
+
* Hence `APP_PATH_PREFIX`: the supervisor writes `APP_PATH_PREFIX=/<slug>` and
|
|
29
|
+
* the MCP keeps choosing its own path.
|
|
30
|
+
*
|
|
31
|
+
* ### The exact rule
|
|
32
|
+
*
|
|
33
|
+
* 1. An **explicit** path — any of `APP_PATH`, `PROXY_APP_PATH`, `GUI_PATH`
|
|
34
|
+
* set to a non-empty value — is taken **as-is, never prefixed**.
|
|
35
|
+
* 2. Otherwise the MCP's **natural path** (the default hardcoded in its own
|
|
36
|
+
* code, passed here as `fallbackPath`) is prefixed with `APP_PATH_PREFIX`.
|
|
37
|
+
* 3. Normalisation, applied in both cases: a leading `/` is added if missing,
|
|
38
|
+
* runs of `/` are collapsed to one, and a trailing `/` is dropped (`/` alone
|
|
39
|
+
* survives). An empty prefix, or a prefix of `/`, has no effect at all.
|
|
40
|
+
* 4. Prefixing is **idempotent**: a path that already starts with the prefix
|
|
41
|
+
* (`/abc123` or `/abc123/…`) is returned unchanged, so resolving twice — for
|
|
42
|
+
* instance once by the consumer for its `<base href>` and once inside
|
|
43
|
+
* {@link ProxyClient.register} — never yields `/abc123/abc123/…`.
|
|
44
|
+
*
|
|
45
|
+
* ### Why rule 1
|
|
46
|
+
*
|
|
47
|
+
* Because the operator's word wins. Someone who types `APP_PATH=/whatever`
|
|
48
|
+
* into an MCP's env block is stating the *final* path they want to see in the
|
|
49
|
+
* proxy, and has no reason to expect an invisible prefix to be spliced in. The
|
|
50
|
+
* hub's prefix is an automatic, best-effort namespacing of a value nobody
|
|
51
|
+
* chose; a hand-written `APP_PATH` is a value somebody chose. Rule 1 also
|
|
52
|
+
* makes the behaviour trivially predictable — "the prefix applies to the
|
|
53
|
+
* default path, and only to it" — and leaves a built-in escape hatch: to opt
|
|
54
|
+
* out of prefixing, set `APP_PATH` explicitly.
|
|
55
|
+
*
|
|
56
|
+
* The trade-off is that an MCP whose *own* `.env` already pins
|
|
57
|
+
* `PROXY_APP_PATH` will not be namespaced by a supervisor. That is the
|
|
58
|
+
* intended reading of "manual override wins"; a supervisor that needs the
|
|
59
|
+
* namespace guaranteed must not let such a value through.
|
|
60
|
+
*/
|
|
61
|
+
/** Env var names for the GUI path, in decreasing priority order. */
|
|
62
|
+
export const APP_PATH_ENV_VARS = [
|
|
63
|
+
"APP_PATH",
|
|
64
|
+
"PROXY_APP_PATH",
|
|
65
|
+
"GUI_PATH",
|
|
66
|
+
];
|
|
67
|
+
/** Env var names for the GUI display name, in decreasing priority order. */
|
|
68
|
+
export const APP_NAME_ENV_VARS = [
|
|
69
|
+
"APP_NAME",
|
|
70
|
+
"PROXY_APP_NAME",
|
|
71
|
+
"GUI_NAME",
|
|
72
|
+
];
|
|
73
|
+
/** Env var holding the namespace prefix applied to the natural path. */
|
|
74
|
+
export const APP_PATH_PREFIX_ENV_VAR = "APP_PATH_PREFIX";
|
|
75
|
+
/** Env var holding the proxy dashboard group label. */
|
|
76
|
+
export const APP_GROUP_ENV_VAR = "APP_GROUP";
|
|
77
|
+
function firstNonEmpty(names, env) {
|
|
78
|
+
for (const name of names) {
|
|
79
|
+
const raw = env[name];
|
|
80
|
+
if (typeof raw === "string" && raw.trim() !== "")
|
|
81
|
+
return raw.trim();
|
|
82
|
+
}
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Normalises a mount path: leading `/`, no duplicate `/`, no trailing `/`.
|
|
87
|
+
* An empty input yields `"/"`.
|
|
88
|
+
*/
|
|
89
|
+
export function normalizeAppPath(value) {
|
|
90
|
+
const trimmed = (value ?? "").trim();
|
|
91
|
+
if (trimmed === "")
|
|
92
|
+
return "/";
|
|
93
|
+
const withLeading = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
94
|
+
const collapsed = withLeading.replace(/\/{2,}/g, "/");
|
|
95
|
+
const withoutTrailing = collapsed.replace(/\/+$/, "");
|
|
96
|
+
return withoutTrailing === "" ? "/" : withoutTrailing;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Normalises a prefix. Returns `""` when the prefix is absent, empty or `/`,
|
|
100
|
+
* i.e. when it must have no effect.
|
|
101
|
+
*/
|
|
102
|
+
export function normalizeAppPathPrefix(value) {
|
|
103
|
+
const normalized = normalizeAppPath(value);
|
|
104
|
+
return normalized === "/" ? "" : normalized;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Prefixes `path` with `prefix` (see the module docblock for the exact rule).
|
|
108
|
+
* Idempotent: a path already under the prefix is returned unchanged.
|
|
109
|
+
*/
|
|
110
|
+
export function applyAppPathPrefix(path, prefix) {
|
|
111
|
+
const normalizedPath = normalizeAppPath(path);
|
|
112
|
+
const normalizedPrefix = normalizeAppPathPrefix(prefix);
|
|
113
|
+
if (normalizedPrefix === "")
|
|
114
|
+
return normalizedPath;
|
|
115
|
+
if (normalizedPath === "/")
|
|
116
|
+
return normalizedPrefix;
|
|
117
|
+
if (normalizedPath === normalizedPrefix ||
|
|
118
|
+
normalizedPath.startsWith(`${normalizedPrefix}/`)) {
|
|
119
|
+
return normalizedPath;
|
|
120
|
+
}
|
|
121
|
+
return `${normalizedPrefix}${normalizedPath}`;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Resolves the GUI mount path.
|
|
125
|
+
*
|
|
126
|
+
* @param fallbackPath The MCP's natural path, hardcoded in its own source
|
|
127
|
+
* (e.g. `/documentor-gui`). Only this value gets prefixed.
|
|
128
|
+
* @param env Environment to read (defaults to `process.env`).
|
|
129
|
+
*/
|
|
130
|
+
export function resolveAppPathDetailed(fallbackPath, env = process.env) {
|
|
131
|
+
const prefix = normalizeAppPathPrefix(env[APP_PATH_PREFIX_ENV_VAR]);
|
|
132
|
+
for (const name of APP_PATH_ENV_VARS) {
|
|
133
|
+
const raw = env[name];
|
|
134
|
+
if (typeof raw === "string" && raw.trim() !== "") {
|
|
135
|
+
// Rule 1: an operator-set path is final — never prefixed.
|
|
136
|
+
return { path: normalizeAppPath(raw), source: name, prefix, prefixed: false };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const natural = normalizeAppPath(fallbackPath);
|
|
140
|
+
const path = applyAppPathPrefix(natural, prefix);
|
|
141
|
+
return { path, prefix, prefixed: path !== natural };
|
|
142
|
+
}
|
|
143
|
+
/** Shorthand for {@link resolveAppPathDetailed}().path. */
|
|
144
|
+
export function resolveAppPath(fallbackPath, env = process.env) {
|
|
145
|
+
return resolveAppPathDetailed(fallbackPath, env).path;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Resolves the GUI display name from `APP_NAME` → `PROXY_APP_NAME` →
|
|
149
|
+
* `GUI_NAME`, falling back to `fallbackName`.
|
|
150
|
+
*/
|
|
151
|
+
export function resolveAppName(fallbackName, env = process.env) {
|
|
152
|
+
return firstNonEmpty(APP_NAME_ENV_VARS, env) ?? fallbackName;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Resolves the proxy dashboard group from `APP_GROUP`. An empty or
|
|
156
|
+
* whitespace-only value means "ungrouped" and yields `undefined`.
|
|
157
|
+
*/
|
|
158
|
+
export function resolveAppGroup(env = process.env) {
|
|
159
|
+
const raw = env[APP_GROUP_ENV_VAR];
|
|
160
|
+
return typeof raw === "string" && raw.trim() !== "" ? raw.trim() : undefined;
|
|
161
|
+
}
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { GuiLauncher, reconnectGuiToolDefinition, type GuiLauncherOptions, } from "./gui-launcher.js";
|
|
2
2
|
export { ProxyClient, type RegisterOptions, type RegisterResult } from "./proxy-client.js";
|
|
3
|
+
export { APP_GROUP_ENV_VAR, APP_NAME_ENV_VARS, APP_PATH_ENV_VARS, APP_PATH_PREFIX_ENV_VAR, applyAppPathPrefix, normalizeAppPath, normalizeAppPathPrefix, resolveAppGroup, resolveAppName, resolveAppPath, resolveAppPathDetailed, type ResolvedAppPath, } from "./app-config.js";
|
|
3
4
|
export { IpcHub, type IpcMessage } from "./ipc-hub.js";
|
|
4
5
|
export { setupLogging, createLogger } from "./logger.js";
|
|
5
6
|
export type { ProxyConfig, Logger, SetupLoggingOptions, GuiRuntimeStatus, ReconnectResult, McpToolResult, } from "./types.js";
|
package/dist/src/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { GuiLauncher, reconnectGuiToolDefinition, } from "./gui-launcher.js";
|
|
2
2
|
export { ProxyClient } from "./proxy-client.js";
|
|
3
|
+
export { APP_GROUP_ENV_VAR, APP_NAME_ENV_VARS, APP_PATH_ENV_VARS, APP_PATH_PREFIX_ENV_VAR, applyAppPathPrefix, normalizeAppPath, normalizeAppPathPrefix, resolveAppGroup, resolveAppName, resolveAppPath, resolveAppPathDetailed, } from "./app-config.js";
|
|
3
4
|
export { IpcHub } from "./ipc-hub.js";
|
|
4
5
|
export { setupLogging, createLogger } from "./logger.js";
|
package/dist/src/proxy-client.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import http from "http";
|
|
2
2
|
import { createLogger } from "./logger.js";
|
|
3
|
+
import { resolveAppGroup, resolveAppName, resolveAppPathDetailed, } from "./app-config.js";
|
|
3
4
|
const logger = createLogger("ProxyClient");
|
|
5
|
+
/** `http://host:3000/` + `/abc/gui` → `http://host:3000/abc/gui`. */
|
|
6
|
+
function joinPublicUrl(proxyUrl, path) {
|
|
7
|
+
return `${proxyUrl.replace(/\/+$/, "")}${path}`;
|
|
8
|
+
}
|
|
4
9
|
export class ProxyClient {
|
|
5
10
|
proxyUrl;
|
|
6
11
|
registeredPath;
|
|
@@ -12,15 +17,31 @@ export class ProxyClient {
|
|
|
12
17
|
return this.status;
|
|
13
18
|
}
|
|
14
19
|
async register(options) {
|
|
15
|
-
|
|
20
|
+
// `options.path` is the MCP's natural path. The canonical APP_PATH (or its
|
|
21
|
+
// PROXY_APP_PATH / GUI_PATH fallbacks) overrides it; APP_PATH_PREFIX
|
|
22
|
+
// namespaces it. See ./app-config.ts for the exact rule. Resolution is
|
|
23
|
+
// idempotent, so a consumer that already resolved the path itself (to
|
|
24
|
+
// build its <base href>, say) can pass the resolved value back in safely.
|
|
25
|
+
const resolved = resolveAppPathDetailed(options.path);
|
|
26
|
+
const finalPath = resolved.path;
|
|
27
|
+
if (finalPath !== options.path) {
|
|
28
|
+
logger.info(`Mount path resolved to ${finalPath} (requested: ${options.path}${resolved.source ? `, from ${resolved.source}` : ""}${resolved.prefixed ? `, prefixed with ${resolved.prefix}` : ""})`);
|
|
29
|
+
}
|
|
30
|
+
this.registeredPath = finalPath;
|
|
16
31
|
// The group can be provided explicitly or, more conveniently, through the
|
|
17
32
|
// APP_GROUP env var — so any MCP using this library can opt into grouping
|
|
18
33
|
// from its .env alone. An empty/whitespace value means "ungrouped".
|
|
19
|
-
const rawGroup = options.group ??
|
|
34
|
+
const rawGroup = options.group ?? resolveAppGroup();
|
|
20
35
|
const group = typeof rawGroup === "string" && rawGroup.trim() !== ""
|
|
21
36
|
? rawGroup.trim()
|
|
22
37
|
: undefined;
|
|
23
|
-
const
|
|
38
|
+
const name = resolveAppName(options.name);
|
|
39
|
+
const payload = {
|
|
40
|
+
...options,
|
|
41
|
+
path: finalPath,
|
|
42
|
+
...(name ? { name } : {}),
|
|
43
|
+
...(group ? { group } : {}),
|
|
44
|
+
};
|
|
24
45
|
return new Promise((resolve) => {
|
|
25
46
|
let url;
|
|
26
47
|
try {
|
|
@@ -29,7 +50,7 @@ export class ProxyClient {
|
|
|
29
50
|
catch {
|
|
30
51
|
logger.error(`Invalid Proxy URL: ${this.proxyUrl}`);
|
|
31
52
|
this.status = "error";
|
|
32
|
-
return resolve({ success: false, port: 0, error: "Invalid Proxy URL" });
|
|
53
|
+
return resolve({ success: false, port: 0, path: finalPath, error: "Invalid Proxy URL" });
|
|
33
54
|
}
|
|
34
55
|
const timeout = 1000;
|
|
35
56
|
logger.info(`Sending registration request to ${url.href}...`);
|
|
@@ -53,37 +74,49 @@ export class ProxyClient {
|
|
|
53
74
|
typeof raw.port === "number") {
|
|
54
75
|
logger.info(`Registration verified. Allocated port: ${raw.port}`);
|
|
55
76
|
this.status = "connected";
|
|
56
|
-
resolve({
|
|
77
|
+
resolve({
|
|
78
|
+
success: true,
|
|
79
|
+
port: raw.port,
|
|
80
|
+
path: typeof raw.path === "string" && raw.path !== ""
|
|
81
|
+
? raw.path
|
|
82
|
+
: finalPath,
|
|
83
|
+
// The proxy does not return a public URL; synthesise it
|
|
84
|
+
// from the path it actually mounted, so consumers stop
|
|
85
|
+
// rebuilding it from their own (unprefixed) path.
|
|
86
|
+
url: typeof raw.url === "string" && raw.url !== ""
|
|
87
|
+
? raw.url
|
|
88
|
+
: joinPublicUrl(this.proxyUrl, finalPath),
|
|
89
|
+
});
|
|
57
90
|
}
|
|
58
91
|
else {
|
|
59
92
|
logger.error(`Invalid proxy response schema: ${data}`);
|
|
60
93
|
this.status = "error";
|
|
61
|
-
resolve({ success: false, port: 0, error: "Invalid proxy response schema" });
|
|
94
|
+
resolve({ success: false, port: 0, path: finalPath, error: "Invalid proxy response schema" });
|
|
62
95
|
}
|
|
63
96
|
}
|
|
64
97
|
catch {
|
|
65
98
|
logger.error(`JSON parse error: ${data}`);
|
|
66
99
|
this.status = "error";
|
|
67
|
-
resolve({ success: false, port: 0, error: "Invalid JSON response" });
|
|
100
|
+
resolve({ success: false, port: 0, path: finalPath, error: "Invalid JSON response" });
|
|
68
101
|
}
|
|
69
102
|
}
|
|
70
103
|
else {
|
|
71
104
|
logger.error(`Registration rejected with HTTP ${res.statusCode}`);
|
|
72
105
|
this.status = "error";
|
|
73
|
-
resolve({ success: false, port: 0, error: `HTTP ${res.statusCode}` });
|
|
106
|
+
resolve({ success: false, port: 0, path: finalPath, error: `HTTP ${res.statusCode}` });
|
|
74
107
|
}
|
|
75
108
|
});
|
|
76
109
|
});
|
|
77
110
|
req.on("error", (err) => {
|
|
78
111
|
logger.error(`Registration error: ${err.message}`);
|
|
79
112
|
this.status = "error";
|
|
80
|
-
resolve({ success: false, port: 0, error: err.message });
|
|
113
|
+
resolve({ success: false, port: 0, path: finalPath, error: err.message });
|
|
81
114
|
});
|
|
82
115
|
req.on("timeout", () => {
|
|
83
116
|
logger.error(`Registration timeout after ${timeout}ms`);
|
|
84
117
|
req.destroy();
|
|
85
118
|
this.status = "error";
|
|
86
|
-
resolve({ success: false, port: 0, error: "Timeout" });
|
|
119
|
+
resolve({ success: false, port: 0, path: finalPath, error: "Timeout" });
|
|
87
120
|
});
|
|
88
121
|
req.write(JSON.stringify(payload));
|
|
89
122
|
req.end();
|
package/dist/src/types.d.ts
CHANGED
|
@@ -14,13 +14,31 @@ export interface ProxyConfig {
|
|
|
14
14
|
export interface RegisterResult {
|
|
15
15
|
success: boolean;
|
|
16
16
|
port: number;
|
|
17
|
+
/**
|
|
18
|
+
* Path the GUI was actually registered under, after `APP_PATH` /
|
|
19
|
+
* `PROXY_APP_PATH` / `GUI_PATH` and `APP_PATH_PREFIX` were applied to the
|
|
20
|
+
* requested path (see `app-config.ts`). Use it — not the path you passed in —
|
|
21
|
+
* to build the SPA's `<base href>` and any public URL.
|
|
22
|
+
*/
|
|
23
|
+
path?: string;
|
|
17
24
|
url?: string;
|
|
18
25
|
error?: string;
|
|
19
26
|
}
|
|
20
27
|
export interface RegisterOptions {
|
|
28
|
+
/**
|
|
29
|
+
* The MCP's **natural** mount path (its own hardcoded default, e.g.
|
|
30
|
+
* `/documentor-gui`). `register()` resolves the final path from it: an
|
|
31
|
+
* explicit `APP_PATH` / `PROXY_APP_PATH` / `GUI_PATH` replaces it, otherwise
|
|
32
|
+
* `APP_PATH_PREFIX` is prepended to it. Resolution is idempotent, and the
|
|
33
|
+
* effective path comes back in {@link RegisterResult.path}.
|
|
34
|
+
*/
|
|
21
35
|
path: string;
|
|
22
|
-
name?: string;
|
|
23
36
|
port?: number;
|
|
37
|
+
/**
|
|
38
|
+
* Display name. Overridden by `APP_NAME` / `PROXY_APP_NAME` / `GUI_NAME`
|
|
39
|
+
* when one of them is set.
|
|
40
|
+
*/
|
|
41
|
+
name?: string;
|
|
24
42
|
/**
|
|
25
43
|
* Optional visual group. The proxy dashboard places every app sharing the
|
|
26
44
|
* same `group` under one collapsible section (folded by default). Apps left
|
package/package.json
CHANGED