@imenam/mcp-gui-interface 1.0.6 → 1.0.10
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/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/logger.js +12 -6
- package/dist/src/types.d.ts +24 -0
- package/package.json +1 -1
|
@@ -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/logger.js
CHANGED
|
@@ -41,12 +41,18 @@ export function setupLogging(options) {
|
|
|
41
41
|
return;
|
|
42
42
|
currentProcessLabel = options.processLabel;
|
|
43
43
|
const resolvedLogDir = options.logDir ?? process.env.MCP_LOG_DIR ?? path.join(process.cwd(), ".mcp-gui", "logs");
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
44
|
+
try {
|
|
45
|
+
fs.mkdirSync(resolvedLogDir, { recursive: true });
|
|
46
|
+
const logFilePath = path.join(resolvedLogDir, "server.log");
|
|
47
|
+
logStream = fs.createWriteStream(logFilePath, { flags: "a", encoding: "utf8" });
|
|
48
|
+
logStream.on("error", (error) => {
|
|
49
|
+
logStream = null;
|
|
50
|
+
originalConsoleError(`[LOGGER] Log file write failed, disabling file logging: ${serialize(error)}`);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
originalConsoleError(`[LOGGER] Could not initialize log file at "${resolvedLogDir}", disabling file logging: ${serialize(error)}`);
|
|
55
|
+
}
|
|
50
56
|
ensureConsoleErrorPatched();
|
|
51
57
|
// Redirect console.log/info/warn to console.error to protect stdout
|
|
52
58
|
// which is reserved for the MCP JSON-RPC protocol in the MASTER process.
|
package/dist/src/types.d.ts
CHANGED
|
@@ -38,4 +38,28 @@ export interface GuiLauncherOptions {
|
|
|
38
38
|
maxRestarts?: number;
|
|
39
39
|
restartDelay?: number;
|
|
40
40
|
onMessage?: (msg: IpcMessage) => void;
|
|
41
|
+
/**
|
|
42
|
+
* Optional hook invoked at the start of reconnect() before PROXY_URL is read.
|
|
43
|
+
* Use it to reload the .env file (e.g. dotenv.config({ override: true }) or a
|
|
44
|
+
* custom getConfig()) so a PROXY_URL added after startup is picked up.
|
|
45
|
+
*/
|
|
46
|
+
reloadEnv?: () => void;
|
|
47
|
+
}
|
|
48
|
+
/** Runtime lifecycle state of the GUI worker, tracked from its IPC messages. */
|
|
49
|
+
export type GuiRuntimeStatus = "disabled" | "starting" | "ready" | "already_running" | "stopped";
|
|
50
|
+
/** Structured outcome of a GuiLauncher.reconnect() call. */
|
|
51
|
+
export interface ReconnectResult {
|
|
52
|
+
outcome: "ready" | "already_running" | "exited" | "timeout" | "disabled" | "noop";
|
|
53
|
+
url?: string;
|
|
54
|
+
code?: number | null;
|
|
55
|
+
/** Human-readable message suitable for returning to an MCP client. */
|
|
56
|
+
message: string;
|
|
57
|
+
}
|
|
58
|
+
/** MCP tool result shape (structural — avoids depending on the MCP SDK types). */
|
|
59
|
+
export interface McpToolResult {
|
|
60
|
+
content: {
|
|
61
|
+
type: "text";
|
|
62
|
+
text: string;
|
|
63
|
+
}[];
|
|
64
|
+
isError?: boolean;
|
|
41
65
|
}
|
package/package.json
CHANGED