@imenam/mcp-gui-interface 1.0.8 → 1.0.11
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 +15 -0
- package/dist/src/gui-launcher.d.ts +48 -1
- package/dist/src/gui-launcher.js +141 -3
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.js +1 -1
- package/dist/src/proxy-client.js +9 -1
- package/dist/src/types.d.ts +34 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -174,6 +174,21 @@ await proxy.unregister();
|
|
|
174
174
|
- Registration HTTP timeout: **1000 ms** (unregister: **2000 ms**).
|
|
175
175
|
- On failure, `RegisterResult.port` is **`0`** in the current implementation.
|
|
176
176
|
|
|
177
|
+
**Grouping (`group` / `APP_GROUP`):** pass an optional `group` in the register
|
|
178
|
+
options to place the app under a collapsible section (folded by default) in the
|
|
179
|
+
proxy dashboard. Apps sharing the same `group` are shown together; apps without
|
|
180
|
+
one land in a default "ungrouped" section. When `group` is omitted, `register`
|
|
181
|
+
falls back to the **`APP_GROUP`** environment variable, so an MCP can enable
|
|
182
|
+
grouping purely from its `.env` without touching code:
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
// Explicit:
|
|
186
|
+
await proxy.register({ path: "/my-app", name: "My App", group: "Google" });
|
|
187
|
+
|
|
188
|
+
// Or, with APP_GROUP=Google in the environment, simply:
|
|
189
|
+
await proxy.register({ path: "/my-app", name: "My App" });
|
|
190
|
+
```
|
|
191
|
+
|
|
177
192
|
> **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
193
|
|
|
179
194
|
**`getStatus()`:** `"connected" | "fallback" | "error"` — many failure paths set `"error"`.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ChildProcess } from "child_process";
|
|
2
2
|
import { IpcHub } from "./ipc-hub.js";
|
|
3
|
-
import type { GuiLauncherOptions, IpcMessage } from "./types.js";
|
|
3
|
+
import type { GuiLauncherOptions, GuiRuntimeStatus, IpcMessage, McpToolResult, ReconnectResult } from "./types.js";
|
|
4
4
|
export type { GuiLauncherOptions };
|
|
5
5
|
export declare class GuiLauncher {
|
|
6
6
|
private readonly options;
|
|
@@ -9,11 +9,58 @@ export declare class GuiLauncher {
|
|
|
9
9
|
private restartCount;
|
|
10
10
|
private readonly maxRestarts;
|
|
11
11
|
private readonly restartDelay;
|
|
12
|
+
private status;
|
|
13
|
+
private url;
|
|
14
|
+
private startWaiter;
|
|
12
15
|
constructor(options: GuiLauncherOptions);
|
|
16
|
+
/** Current lifecycle state of the GUI worker. */
|
|
17
|
+
getStatus(): GuiRuntimeStatus;
|
|
18
|
+
/** URL the GUI is registered under, once known. */
|
|
19
|
+
getUrl(): string | null;
|
|
20
|
+
/**
|
|
21
|
+
* Intercepts the GUI worker's lifecycle messages to keep local state in sync,
|
|
22
|
+
* then forwards every message to the consumer's onMessage handler unchanged.
|
|
23
|
+
*/
|
|
24
|
+
private handleMessage;
|
|
13
25
|
private registerSignalHandlers;
|
|
14
26
|
start(): ChildProcess;
|
|
15
27
|
getProcess(): ChildProcess | null;
|
|
16
28
|
getIpcHub(): IpcHub;
|
|
17
29
|
send(message: Omit<IpcMessage, "timestamp">): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Relaunch the GUI worker and wait for it to re-register with the proxy.
|
|
32
|
+
* Safe to call when the proxy was down at MCP startup: it re-reads PROXY_URL
|
|
33
|
+
* (via the reloadEnv hook), relaunches the worker and resolves once the worker
|
|
34
|
+
* confirms READY / ALREADY_RUNNING, exits, or the timeout elapses.
|
|
35
|
+
*/
|
|
36
|
+
reconnect(options?: {
|
|
37
|
+
force?: boolean;
|
|
38
|
+
timeoutMs?: number;
|
|
39
|
+
}): Promise<ReconnectResult>;
|
|
40
|
+
/**
|
|
41
|
+
* Runs reconnect() and adapts the result to the MCP tool-call return shape,
|
|
42
|
+
* so a server can wire the `reconnect_gui` tool in a single line:
|
|
43
|
+
* case "reconnect_gui": return launcher.handleReconnectTool(args);
|
|
44
|
+
*/
|
|
45
|
+
handleReconnectTool(args?: {
|
|
46
|
+
force?: boolean;
|
|
47
|
+
}): Promise<McpToolResult>;
|
|
18
48
|
cleanup(): Promise<void>;
|
|
19
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Ready-made MCP tool definition for `reconnect_gui`. Spread it into your
|
|
52
|
+
* server's tools list, then dispatch to `launcher.handleReconnectTool(args)`.
|
|
53
|
+
*/
|
|
54
|
+
export declare function reconnectGuiToolDefinition(): {
|
|
55
|
+
name: string;
|
|
56
|
+
description: string;
|
|
57
|
+
inputSchema: {
|
|
58
|
+
type: "object";
|
|
59
|
+
properties: {
|
|
60
|
+
force: {
|
|
61
|
+
type: string;
|
|
62
|
+
description: string;
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
};
|
package/dist/src/gui-launcher.js
CHANGED
|
@@ -2,6 +2,7 @@ import { fork } from "child_process";
|
|
|
2
2
|
import { createLogger } from "./logger.js";
|
|
3
3
|
import { IpcHub } from "./ipc-hub.js";
|
|
4
4
|
const logger = createLogger("GuiLauncher");
|
|
5
|
+
const RECONNECT_TIMEOUT_MS = 15000;
|
|
5
6
|
export class GuiLauncher {
|
|
6
7
|
options;
|
|
7
8
|
guiProcess = null;
|
|
@@ -9,12 +10,40 @@ export class GuiLauncher {
|
|
|
9
10
|
restartCount = 0;
|
|
10
11
|
maxRestarts;
|
|
11
12
|
restartDelay;
|
|
13
|
+
status = "disabled";
|
|
14
|
+
url = null;
|
|
15
|
+
startWaiter = null;
|
|
12
16
|
constructor(options) {
|
|
13
17
|
this.options = options;
|
|
14
18
|
this.maxRestarts = options.maxRestarts ?? 0;
|
|
15
19
|
this.restartDelay = options.restartDelay ?? 2000;
|
|
16
20
|
this.registerSignalHandlers();
|
|
17
21
|
}
|
|
22
|
+
/** Current lifecycle state of the GUI worker. */
|
|
23
|
+
getStatus() {
|
|
24
|
+
return this.status;
|
|
25
|
+
}
|
|
26
|
+
/** URL the GUI is registered under, once known. */
|
|
27
|
+
getUrl() {
|
|
28
|
+
return this.url;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Intercepts the GUI worker's lifecycle messages to keep local state in sync,
|
|
32
|
+
* then forwards every message to the consumer's onMessage handler unchanged.
|
|
33
|
+
*/
|
|
34
|
+
handleMessage(msg) {
|
|
35
|
+
if (msg.type === "ALREADY_RUNNING") {
|
|
36
|
+
this.status = "already_running";
|
|
37
|
+
this.url = msg.data?.url ?? this.url;
|
|
38
|
+
this.startWaiter?.({ outcome: "already_running", url: msg.data?.url });
|
|
39
|
+
}
|
|
40
|
+
else if (msg.type === "READY") {
|
|
41
|
+
this.status = "ready";
|
|
42
|
+
this.url = msg.data?.url ?? null;
|
|
43
|
+
this.startWaiter?.({ outcome: "ready", url: msg.data?.url });
|
|
44
|
+
}
|
|
45
|
+
this.options.onMessage?.(msg);
|
|
46
|
+
}
|
|
18
47
|
registerSignalHandlers() {
|
|
19
48
|
const syncCleanup = () => {
|
|
20
49
|
this.guiProcess?.kill();
|
|
@@ -40,19 +69,25 @@ export class GuiLauncher {
|
|
|
40
69
|
this.guiProcess.kill();
|
|
41
70
|
}
|
|
42
71
|
logger.info(`Starting GUI process: ${this.options.guiPath}`);
|
|
72
|
+
this.status = "starting";
|
|
43
73
|
this.guiProcess = fork(this.options.guiPath, [], {
|
|
44
74
|
stdio: ["ignore", "pipe", "inherit", "ipc"],
|
|
45
75
|
env: { ...process.env, ...this.options.env },
|
|
46
76
|
});
|
|
77
|
+
const child = this.guiProcess;
|
|
47
78
|
this.guiProcess.stdout?.on("data", (data) => {
|
|
48
79
|
process.stderr.write(data);
|
|
49
80
|
});
|
|
50
81
|
this.ipcHub = new IpcHub(this.guiProcess);
|
|
51
|
-
|
|
52
|
-
this.ipcHub.onMessage(this.options.onMessage);
|
|
53
|
-
}
|
|
82
|
+
this.ipcHub.onMessage((msg) => this.handleMessage(msg));
|
|
54
83
|
this.guiProcess.on("exit", (code, signal) => {
|
|
55
84
|
logger.info(`GUI process exited (code=${code}, signal=${signal})`);
|
|
85
|
+
// A later start() replaces the current process: only the active process
|
|
86
|
+
// exiting should flip state to "stopped". ALREADY_RUNNING means the GUI
|
|
87
|
+
// lives in another instance, so this worker's exit is expected.
|
|
88
|
+
if (this.guiProcess === child && this.status !== "already_running") {
|
|
89
|
+
this.status = "stopped";
|
|
90
|
+
}
|
|
56
91
|
// Only auto-restart on unexpected crashes (code !== 0 and code !== null)
|
|
57
92
|
const shouldRestart = code !== 0 && code !== null;
|
|
58
93
|
if (shouldRestart && this.restartCount < this.maxRestarts) {
|
|
@@ -82,6 +117,90 @@ export class GuiLauncher {
|
|
|
82
117
|
send(message) {
|
|
83
118
|
return this.ipcHub.send({ ...message, timestamp: new Date().toISOString() });
|
|
84
119
|
}
|
|
120
|
+
/**
|
|
121
|
+
* Relaunch the GUI worker and wait for it to re-register with the proxy.
|
|
122
|
+
* Safe to call when the proxy was down at MCP startup: it re-reads PROXY_URL
|
|
123
|
+
* (via the reloadEnv hook), relaunches the worker and resolves once the worker
|
|
124
|
+
* confirms READY / ALREADY_RUNNING, exits, or the timeout elapses.
|
|
125
|
+
*/
|
|
126
|
+
async reconnect(options) {
|
|
127
|
+
this.options.reloadEnv?.();
|
|
128
|
+
const proxyUrl = process.env.PROXY_URL;
|
|
129
|
+
if (!proxyUrl) {
|
|
130
|
+
this.status = "disabled";
|
|
131
|
+
return {
|
|
132
|
+
outcome: "disabled",
|
|
133
|
+
message: "PROXY_URL is not set — the GUI is disabled. Add PROXY_URL to the .env file, then call reconnect_gui again.",
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
const child = this.guiProcess;
|
|
137
|
+
const alive = !!child && child.exitCode === null && !child.killed;
|
|
138
|
+
const force = options?.force === true;
|
|
139
|
+
if (alive && !force) {
|
|
140
|
+
if (this.status === "starting") {
|
|
141
|
+
return {
|
|
142
|
+
outcome: "noop",
|
|
143
|
+
message: "GUI worker is already starting (proxy registration in progress). Wait a few seconds, then call reconnect_gui again if needed.",
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
outcome: this.status === "already_running" ? "already_running" : "ready",
|
|
148
|
+
url: this.url ?? undefined,
|
|
149
|
+
message: `GUI is already registered with the proxy${this.url ? ` at ${this.url}` : ""}. Nothing to do (use force=true to restart it anyway).`,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
logger.info(`reconnect: ${alive ? "restarting (force)" : "relaunching"} GUI worker...`);
|
|
153
|
+
const timeoutMs = options?.timeoutMs ?? RECONNECT_TIMEOUT_MS;
|
|
154
|
+
return await new Promise((resolve) => {
|
|
155
|
+
const newChild = this.start();
|
|
156
|
+
const cleanup = () => {
|
|
157
|
+
clearTimeout(timer);
|
|
158
|
+
newChild.off("exit", onExit);
|
|
159
|
+
this.startWaiter = null;
|
|
160
|
+
};
|
|
161
|
+
const onExit = (code) => {
|
|
162
|
+
cleanup();
|
|
163
|
+
resolve({
|
|
164
|
+
outcome: "exited",
|
|
165
|
+
code,
|
|
166
|
+
message: `GUI worker exited before completing registration (code=${code}). The proxy server is probably still unreachable at ${proxyUrl}. Start the proxy server, then call reconnect_gui again.`,
|
|
167
|
+
});
|
|
168
|
+
};
|
|
169
|
+
const timer = setTimeout(() => {
|
|
170
|
+
cleanup();
|
|
171
|
+
resolve({
|
|
172
|
+
outcome: "timeout",
|
|
173
|
+
message: `GUI worker started but did not confirm proxy registration within ${Math.round(timeoutMs / 1000)}s. Check the logs, then retry.`,
|
|
174
|
+
});
|
|
175
|
+
}, timeoutMs);
|
|
176
|
+
this.startWaiter = (r) => {
|
|
177
|
+
cleanup();
|
|
178
|
+
resolve({
|
|
179
|
+
outcome: r.outcome,
|
|
180
|
+
url: r.url,
|
|
181
|
+
message: r.outcome === "ready"
|
|
182
|
+
? `GUI reconnected and registered with the proxy. URL: ${r.url}`
|
|
183
|
+
: `GUI is already registered by another MCP instance. URL: ${r.url ?? "unknown"}`,
|
|
184
|
+
});
|
|
185
|
+
};
|
|
186
|
+
newChild.on("exit", onExit);
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Runs reconnect() and adapts the result to the MCP tool-call return shape,
|
|
191
|
+
* so a server can wire the `reconnect_gui` tool in a single line:
|
|
192
|
+
* case "reconnect_gui": return launcher.handleReconnectTool(args);
|
|
193
|
+
*/
|
|
194
|
+
async handleReconnectTool(args) {
|
|
195
|
+
const result = await this.reconnect({ force: args?.force === true });
|
|
196
|
+
const isError = result.outcome === "disabled" ||
|
|
197
|
+
result.outcome === "exited" ||
|
|
198
|
+
result.outcome === "timeout";
|
|
199
|
+
return {
|
|
200
|
+
content: [{ type: "text", text: result.message }],
|
|
201
|
+
...(isError ? { isError: true } : {}),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
85
204
|
async cleanup() {
|
|
86
205
|
if (!this.guiProcess)
|
|
87
206
|
return;
|
|
@@ -102,3 +221,22 @@ export class GuiLauncher {
|
|
|
102
221
|
logger.info("GUI process cleaned up.");
|
|
103
222
|
}
|
|
104
223
|
}
|
|
224
|
+
/**
|
|
225
|
+
* Ready-made MCP tool definition for `reconnect_gui`. Spread it into your
|
|
226
|
+
* server's tools list, then dispatch to `launcher.handleReconnectTool(args)`.
|
|
227
|
+
*/
|
|
228
|
+
export function reconnectGuiToolDefinition() {
|
|
229
|
+
return {
|
|
230
|
+
name: "reconnect_gui",
|
|
231
|
+
description: "Relaunch the GUI worker and re-register it with the proxy server. Use this when the proxy server was not running when the MCP server started (so the GUI registration failed), to redeploy the GUI without restarting the MCP server. Does nothing if the GUI is already registered, unless force=true.",
|
|
232
|
+
inputSchema: {
|
|
233
|
+
type: "object",
|
|
234
|
+
properties: {
|
|
235
|
+
force: {
|
|
236
|
+
type: "boolean",
|
|
237
|
+
description: "Restart the GUI worker even if it is already running and registered (default: false)",
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
}
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { GuiLauncher, type GuiLauncherOptions } from "./gui-launcher.js";
|
|
1
|
+
export { GuiLauncher, reconnectGuiToolDefinition, type GuiLauncherOptions, } from "./gui-launcher.js";
|
|
2
2
|
export { ProxyClient, type RegisterOptions, type RegisterResult } from "./proxy-client.js";
|
|
3
3
|
export { IpcHub, type IpcMessage } from "./ipc-hub.js";
|
|
4
4
|
export { setupLogging, createLogger } from "./logger.js";
|
|
5
|
-
export type { ProxyConfig, Logger, SetupLoggingOptions } from "./types.js";
|
|
5
|
+
export type { ProxyConfig, Logger, SetupLoggingOptions, GuiRuntimeStatus, ReconnectResult, McpToolResult, } from "./types.js";
|
package/dist/src/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { GuiLauncher } from "./gui-launcher.js";
|
|
1
|
+
export { GuiLauncher, reconnectGuiToolDefinition, } from "./gui-launcher.js";
|
|
2
2
|
export { ProxyClient } from "./proxy-client.js";
|
|
3
3
|
export { IpcHub } from "./ipc-hub.js";
|
|
4
4
|
export { setupLogging, createLogger } from "./logger.js";
|
package/dist/src/proxy-client.js
CHANGED
|
@@ -13,6 +13,14 @@ export class ProxyClient {
|
|
|
13
13
|
}
|
|
14
14
|
async register(options) {
|
|
15
15
|
this.registeredPath = options.path;
|
|
16
|
+
// The group can be provided explicitly or, more conveniently, through the
|
|
17
|
+
// APP_GROUP env var — so any MCP using this library can opt into grouping
|
|
18
|
+
// from its .env alone. An empty/whitespace value means "ungrouped".
|
|
19
|
+
const rawGroup = options.group ?? process.env.APP_GROUP;
|
|
20
|
+
const group = typeof rawGroup === "string" && rawGroup.trim() !== ""
|
|
21
|
+
? rawGroup.trim()
|
|
22
|
+
: undefined;
|
|
23
|
+
const payload = { ...options, ...(group ? { group } : {}) };
|
|
16
24
|
return new Promise((resolve) => {
|
|
17
25
|
let url;
|
|
18
26
|
try {
|
|
@@ -77,7 +85,7 @@ export class ProxyClient {
|
|
|
77
85
|
this.status = "error";
|
|
78
86
|
resolve({ success: false, port: 0, error: "Timeout" });
|
|
79
87
|
});
|
|
80
|
-
req.write(JSON.stringify(
|
|
88
|
+
req.write(JSON.stringify(payload));
|
|
81
89
|
req.end();
|
|
82
90
|
});
|
|
83
91
|
}
|
package/dist/src/types.d.ts
CHANGED
|
@@ -21,6 +21,16 @@ export interface RegisterOptions {
|
|
|
21
21
|
path: string;
|
|
22
22
|
name?: string;
|
|
23
23
|
port?: number;
|
|
24
|
+
/**
|
|
25
|
+
* Optional visual group. The proxy dashboard places every app sharing the
|
|
26
|
+
* same `group` under one collapsible section (folded by default). Apps left
|
|
27
|
+
* without a group land in a default "ungrouped" section.
|
|
28
|
+
*
|
|
29
|
+
* When omitted, {@link ProxyClient.register} falls back to the `APP_GROUP`
|
|
30
|
+
* environment variable, so an MCP can enable grouping purely from its `.env`
|
|
31
|
+
* without touching code.
|
|
32
|
+
*/
|
|
33
|
+
group?: string;
|
|
24
34
|
}
|
|
25
35
|
export interface Logger {
|
|
26
36
|
debug: (message: string, ...meta: unknown[]) => void;
|
|
@@ -38,4 +48,28 @@ export interface GuiLauncherOptions {
|
|
|
38
48
|
maxRestarts?: number;
|
|
39
49
|
restartDelay?: number;
|
|
40
50
|
onMessage?: (msg: IpcMessage) => void;
|
|
51
|
+
/**
|
|
52
|
+
* Optional hook invoked at the start of reconnect() before PROXY_URL is read.
|
|
53
|
+
* Use it to reload the .env file (e.g. dotenv.config({ override: true }) or a
|
|
54
|
+
* custom getConfig()) so a PROXY_URL added after startup is picked up.
|
|
55
|
+
*/
|
|
56
|
+
reloadEnv?: () => void;
|
|
57
|
+
}
|
|
58
|
+
/** Runtime lifecycle state of the GUI worker, tracked from its IPC messages. */
|
|
59
|
+
export type GuiRuntimeStatus = "disabled" | "starting" | "ready" | "already_running" | "stopped";
|
|
60
|
+
/** Structured outcome of a GuiLauncher.reconnect() call. */
|
|
61
|
+
export interface ReconnectResult {
|
|
62
|
+
outcome: "ready" | "already_running" | "exited" | "timeout" | "disabled" | "noop";
|
|
63
|
+
url?: string;
|
|
64
|
+
code?: number | null;
|
|
65
|
+
/** Human-readable message suitable for returning to an MCP client. */
|
|
66
|
+
message: string;
|
|
67
|
+
}
|
|
68
|
+
/** MCP tool result shape (structural — avoids depending on the MCP SDK types). */
|
|
69
|
+
export interface McpToolResult {
|
|
70
|
+
content: {
|
|
71
|
+
type: "text";
|
|
72
|
+
text: string;
|
|
73
|
+
}[];
|
|
74
|
+
isError?: boolean;
|
|
41
75
|
}
|
package/package.json
CHANGED