@graphty/remote-logger 1.2.1 → 1.2.3
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 +1 -1
- package/dist/client/RemoteLogClient.d.ts +2 -1
- package/dist/client/RemoteLogClient.d.ts.map +1 -1
- package/dist/client/RemoteLogClient.js +20 -10
- package/dist/client/RemoteLogClient.js.map +1 -1
- package/dist/client/types.d.ts +6 -0
- package/dist/client/types.d.ts.map +1 -1
- package/dist/mcp/index.d.ts +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +20 -1
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/mcp-server.d.ts +5 -0
- package/dist/mcp/mcp-server.d.ts.map +1 -1
- package/dist/mcp/mcp-server.js +6 -5
- package/dist/mcp/mcp-server.js.map +1 -1
- package/dist/server/dual-server.d.ts +7 -4
- package/dist/server/dual-server.d.ts.map +1 -1
- package/dist/server/dual-server.js +123 -44
- package/dist/server/dual-server.js.map +1 -1
- package/dist/server/log-server.d.ts +2 -2
- package/dist/server/log-server.d.ts.map +1 -1
- package/dist/server/log-server.js +32 -55
- package/dist/server/log-server.js.map +1 -1
- package/dist/server/self-signed-cert.d.ts +9 -0
- package/dist/server/self-signed-cert.d.ts.map +1 -1
- package/dist/server/self-signed-cert.js +9 -0
- package/dist/server/self-signed-cert.js.map +1 -1
- package/dist/ui/ConsoleCaptureUI.d.ts.map +1 -1
- package/dist/ui/ConsoleCaptureUI.js +6 -1
- package/dist/ui/ConsoleCaptureUI.js.map +1 -1
- package/package.json +3 -1
- package/src/client/RemoteLogClient.ts +22 -11
- package/src/client/types.ts +6 -0
- package/src/mcp/index.ts +42 -0
- package/src/mcp/mcp-server.ts +6 -5
- package/src/server/dual-server.ts +165 -51
- package/src/server/log-server.ts +35 -60
- package/src/server/self-signed-cert.ts +9 -0
- package/src/ui/ConsoleCaptureUI.ts +6 -1
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
|
|
9
9
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
10
10
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
11
|
-
import
|
|
12
|
-
import
|
|
11
|
+
import * as http from "http";
|
|
12
|
+
import * as https from "https";
|
|
13
|
+
import { internalIpV4Sync } from "internal-ip";
|
|
13
14
|
import * as net from "net";
|
|
14
15
|
import * as os from "os";
|
|
15
16
|
import * as path from "path";
|
|
@@ -35,6 +36,90 @@ const MAX_PORT_SCAN_ATTEMPTS = 100;
|
|
|
35
36
|
/** Maximum port number allowed (ports 9000-9099 per project guidelines) */
|
|
36
37
|
const MAX_PORT_NUMBER = 9099;
|
|
37
38
|
|
|
39
|
+
/** Maximum retries for binding after EADDRINUSE during listen */
|
|
40
|
+
const MAX_BIND_RETRIES = 10;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Attempt to bind an HTTP/HTTPS server to a port with retry on EADDRINUSE.
|
|
44
|
+
* This handles race conditions where a port becomes unavailable between
|
|
45
|
+
* port scanning and actual binding.
|
|
46
|
+
* @param serverFactory - Function to create a new server instance
|
|
47
|
+
* @param startPort - Starting port to try
|
|
48
|
+
* @param host - Host to bind to
|
|
49
|
+
* @param quiet - Suppress output messages
|
|
50
|
+
* @param maxPort - Maximum port number to scan up to (default: 9099)
|
|
51
|
+
* @returns Promise resolving to bound server and actual port
|
|
52
|
+
*/
|
|
53
|
+
async function tryBindWithRetry(
|
|
54
|
+
serverFactory: () => http.Server | https.Server,
|
|
55
|
+
startPort: number,
|
|
56
|
+
host: string,
|
|
57
|
+
quiet: boolean,
|
|
58
|
+
maxPort: number = MAX_PORT_NUMBER,
|
|
59
|
+
): Promise<{ server: http.Server | https.Server; port: number }> {
|
|
60
|
+
let port = startPort;
|
|
61
|
+
let attempts = 0;
|
|
62
|
+
|
|
63
|
+
while (attempts < MAX_BIND_RETRIES) {
|
|
64
|
+
if (port > maxPort) {
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const server = serverFactory();
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
await new Promise<void>((resolve, reject) => {
|
|
72
|
+
const errorHandler = (err: NodeJS.ErrnoException): void => {
|
|
73
|
+
if (err.code === "EADDRINUSE") {
|
|
74
|
+
if (!quiet) {
|
|
75
|
+
// eslint-disable-next-line no-console
|
|
76
|
+
console.log(
|
|
77
|
+
`${colors.yellow}Port ${port} claimed during bind, trying ${port + 1}...${colors.reset}`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
reject(err);
|
|
81
|
+
} else {
|
|
82
|
+
reject(err);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
server.once("error", errorHandler);
|
|
87
|
+
server.listen({ port, host, exclusive: false }, () => {
|
|
88
|
+
server.removeListener("error", errorHandler);
|
|
89
|
+
resolve();
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Success - server is bound
|
|
94
|
+
if (!quiet) {
|
|
95
|
+
// eslint-disable-next-line no-console
|
|
96
|
+
console.log(
|
|
97
|
+
`${colors.green}HTTP server listening on ${host}:${port}${colors.reset}`,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
return { server, port };
|
|
101
|
+
} catch (err) {
|
|
102
|
+
if ((err as NodeJS.ErrnoException).code === "EADDRINUSE") {
|
|
103
|
+
port++;
|
|
104
|
+
attempts++;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
// Re-throw non-EADDRINUSE errors
|
|
108
|
+
throw err;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Exhausted retries
|
|
113
|
+
const errorMsg = `Could not bind to any port after ${attempts} attempts. ` +
|
|
114
|
+
`Ports ${startPort}-${port - 1} are all in use or were claimed during bind.`;
|
|
115
|
+
|
|
116
|
+
if (!quiet) {
|
|
117
|
+
console.error(`${colors.red}${errorMsg}${colors.reset}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
throw new Error(errorMsg);
|
|
121
|
+
}
|
|
122
|
+
|
|
38
123
|
/**
|
|
39
124
|
* Check if a port is available for binding.
|
|
40
125
|
* @param port - Port number to check
|
|
@@ -72,16 +157,22 @@ async function isPortAvailable(port: number, host: string): Promise<boolean> {
|
|
|
72
157
|
* @param basePort - Starting port number
|
|
73
158
|
* @param host - Host to bind to
|
|
74
159
|
* @param quiet - Suppress output messages
|
|
160
|
+
* @param maxPort - Maximum port number to scan up to (default: 9099)
|
|
75
161
|
* @returns Promise resolving to available port number
|
|
76
162
|
* @throws Error if no available port found within MAX_PORT_SCAN_ATTEMPTS
|
|
77
163
|
*/
|
|
78
|
-
export async function findAvailablePort(
|
|
164
|
+
export async function findAvailablePort(
|
|
165
|
+
basePort: number,
|
|
166
|
+
host: string,
|
|
167
|
+
quiet: boolean = false,
|
|
168
|
+
maxPort: number = MAX_PORT_NUMBER,
|
|
169
|
+
): Promise<number> {
|
|
79
170
|
let port = basePort;
|
|
80
171
|
let attempts = 0;
|
|
81
172
|
|
|
82
173
|
while (attempts < MAX_PORT_SCAN_ATTEMPTS) {
|
|
83
|
-
if (port >
|
|
84
|
-
//
|
|
174
|
+
if (port > maxPort) {
|
|
175
|
+
// Stop if we exceed max port
|
|
85
176
|
break;
|
|
86
177
|
}
|
|
87
178
|
|
|
@@ -107,12 +198,12 @@ export async function findAvailablePort(basePort: number, host: string, quiet: b
|
|
|
107
198
|
}
|
|
108
199
|
|
|
109
200
|
// No available port found
|
|
110
|
-
const errorMsg = `Could not find available port after ${
|
|
111
|
-
`Ports ${basePort}-${port - 1} are all in use. ` +
|
|
201
|
+
const errorMsg = `Could not find available port after ${attempts} attempts starting from ${basePort}. ` +
|
|
202
|
+
`Ports ${basePort}-${Math.min(port - 1, maxPort)} are all in use. ` +
|
|
112
203
|
`Try killing existing processes: pkill -f "remote-log-server"`;
|
|
113
204
|
|
|
114
205
|
if (!quiet) {
|
|
115
|
-
|
|
206
|
+
|
|
116
207
|
console.error(`${colors.red}${errorMsg}${colors.reset}`);
|
|
117
208
|
}
|
|
118
209
|
|
|
@@ -125,7 +216,7 @@ export async function findAvailablePort(basePort: number, host: string, quiet: b
|
|
|
125
216
|
export interface DualServerOptions {
|
|
126
217
|
/** Port for HTTP server (default: 9080) */
|
|
127
218
|
httpPort?: number;
|
|
128
|
-
/** Host for HTTP server (default:
|
|
219
|
+
/** Host for HTTP server (default: 0.0.0.0) */
|
|
129
220
|
httpHost?: string;
|
|
130
221
|
/** Enable MCP server (default: true) */
|
|
131
222
|
mcpEnabled?: boolean;
|
|
@@ -145,6 +236,8 @@ export interface DualServerOptions {
|
|
|
145
236
|
jsonlWriter?: JsonlWriter;
|
|
146
237
|
/** Only serve /log POST and /health GET endpoints (default: false) */
|
|
147
238
|
logReceiveOnly?: boolean;
|
|
239
|
+
/** Maximum port number to scan up to (default: 9099) */
|
|
240
|
+
maxPortNumber?: number;
|
|
148
241
|
}
|
|
149
242
|
|
|
150
243
|
/**
|
|
@@ -176,7 +269,7 @@ export interface DualServerResult {
|
|
|
176
269
|
export async function createDualServer(options: DualServerOptions = {}): Promise<DualServerResult> {
|
|
177
270
|
const {
|
|
178
271
|
httpPort: requestedPort = 9080,
|
|
179
|
-
httpHost = "
|
|
272
|
+
httpHost = "0.0.0.0",
|
|
180
273
|
mcpEnabled = true,
|
|
181
274
|
httpEnabled = true,
|
|
182
275
|
quiet = false,
|
|
@@ -185,6 +278,7 @@ export async function createDualServer(options: DualServerOptions = {}): Promise
|
|
|
185
278
|
logReceiveOnly = false,
|
|
186
279
|
certPath,
|
|
187
280
|
keyPath,
|
|
281
|
+
maxPortNumber = MAX_PORT_NUMBER,
|
|
188
282
|
} = options;
|
|
189
283
|
|
|
190
284
|
// Create or use provided JSONL writer
|
|
@@ -198,53 +292,50 @@ export async function createDualServer(options: DualServerOptions = {}): Promise
|
|
|
198
292
|
let mcpServer: McpServer | undefined;
|
|
199
293
|
let actualHttpPort: number | undefined;
|
|
200
294
|
|
|
295
|
+
// Track active connections for graceful shutdown
|
|
296
|
+
const activeConnections = new Set<net.Socket>();
|
|
297
|
+
|
|
201
298
|
// Start HTTP server if enabled
|
|
202
299
|
if (httpEnabled) {
|
|
203
300
|
// Find an available port starting from the requested port
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
301
|
+
const startPort = await findAvailablePort(requestedPort, httpHost, quiet, maxPortNumber);
|
|
302
|
+
|
|
303
|
+
// Factory function to create server instances (for retry on bind failure)
|
|
304
|
+
const createServerInstance = (): http.Server | https.Server => {
|
|
305
|
+
return createLogServer({
|
|
306
|
+
port: startPort, // Port is set but not used until bind
|
|
307
|
+
host: httpHost,
|
|
308
|
+
storage,
|
|
309
|
+
quiet: true, // Quiet during factory, messages handled by tryBindWithRetry
|
|
310
|
+
logReceiveOnly,
|
|
311
|
+
certPath,
|
|
312
|
+
keyPath,
|
|
313
|
+
}).server;
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
// Bind with retry logic to handle race conditions
|
|
317
|
+
const bindResult = await tryBindWithRetry(
|
|
318
|
+
createServerInstance,
|
|
319
|
+
startPort,
|
|
320
|
+
httpHost,
|
|
210
321
|
quiet,
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
serverToStart.on("error", (err: NodeJS.ErrnoException) => {
|
|
222
|
-
// Provide helpful error message for port conflicts
|
|
223
|
-
if (err.code === "EADDRINUSE") {
|
|
224
|
-
const errorMsg = `Port ${portToUse} is already in use. ` +
|
|
225
|
-
`This shouldn't happen after port scanning - there may be a race condition. ` +
|
|
226
|
-
`Try again or kill existing processes: pkill -f "remote-log-server"`;
|
|
227
|
-
if (!quiet) {
|
|
228
|
-
|
|
229
|
-
console.error(`${colors.red}${errorMsg}${colors.reset}`);
|
|
230
|
-
}
|
|
231
|
-
reject(new Error(errorMsg));
|
|
232
|
-
} else {
|
|
233
|
-
reject(err);
|
|
234
|
-
}
|
|
235
|
-
});
|
|
236
|
-
serverToStart.listen({ port: portToUse, host: httpHost, exclusive: false }, () => {
|
|
237
|
-
serverToStart.removeListener("error", reject);
|
|
238
|
-
if (!quiet) {
|
|
239
|
-
// eslint-disable-next-line no-console
|
|
240
|
-
console.log(
|
|
241
|
-
`${colors.green}HTTP server listening on ${httpHost}:${portToUse}${colors.reset}`,
|
|
242
|
-
);
|
|
243
|
-
}
|
|
244
|
-
resolve();
|
|
322
|
+
maxPortNumber,
|
|
323
|
+
);
|
|
324
|
+
httpServer = bindResult.server;
|
|
325
|
+
actualHttpPort = bindResult.port;
|
|
326
|
+
|
|
327
|
+
// Track connections for graceful shutdown
|
|
328
|
+
httpServer.on("connection", (socket: net.Socket) => {
|
|
329
|
+
activeConnections.add(socket);
|
|
330
|
+
socket.once("close", () => {
|
|
331
|
+
activeConnections.delete(socket);
|
|
245
332
|
});
|
|
246
333
|
});
|
|
247
334
|
|
|
335
|
+
// Set short keep-alive timeout for tests (connections close faster)
|
|
336
|
+
httpServer.keepAliveTimeout = 1000;
|
|
337
|
+
httpServer.headersTimeout = 2000;
|
|
338
|
+
|
|
248
339
|
// Set server config in storage so MCP tools can report it
|
|
249
340
|
// Determine protocol based on whether valid cert files were provided
|
|
250
341
|
const useHttps = certPath && keyPath && certFilesExist(certPath, keyPath);
|
|
@@ -258,11 +349,28 @@ export async function createDualServer(options: DualServerOptions = {}): Promise
|
|
|
258
349
|
} else {
|
|
259
350
|
mode = "dual";
|
|
260
351
|
}
|
|
352
|
+
// When bound to all interfaces (0.0.0.0), detect the machine's IP address
|
|
353
|
+
// for the endpoint URL since 0.0.0.0 is not a routable address for clients
|
|
354
|
+
let endpointHost = httpHost;
|
|
355
|
+
if (httpHost === "0.0.0.0") {
|
|
356
|
+
const internalIp = internalIpV4Sync();
|
|
357
|
+
if (internalIp) {
|
|
358
|
+
endpointHost = internalIp;
|
|
359
|
+
} else {
|
|
360
|
+
// Fallback for local-only use - don't use hostname as it may not resolve
|
|
361
|
+
endpointHost = "127.0.0.1";
|
|
362
|
+
if (!quiet) {
|
|
363
|
+
console.warn(
|
|
364
|
+
`${colors.yellow}Could not detect LAN IP. Endpoint URL will only work locally.${colors.reset}`,
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
261
369
|
storage.setServerConfig({
|
|
262
370
|
httpPort: actualHttpPort,
|
|
263
371
|
httpHost,
|
|
264
372
|
protocol,
|
|
265
|
-
httpEndpoint: `${protocol}://${
|
|
373
|
+
httpEndpoint: `${protocol}://${endpointHost}:${actualHttpPort}/log`,
|
|
266
374
|
mode,
|
|
267
375
|
});
|
|
268
376
|
}
|
|
@@ -281,6 +389,12 @@ export async function createDualServer(options: DualServerOptions = {}): Promise
|
|
|
281
389
|
const shutdown = async (): Promise<void> => {
|
|
282
390
|
// Close HTTP server
|
|
283
391
|
if (httpServer?.listening) {
|
|
392
|
+
// Destroy all active connections to ensure clean shutdown
|
|
393
|
+
for (const socket of activeConnections) {
|
|
394
|
+
socket.destroy();
|
|
395
|
+
}
|
|
396
|
+
activeConnections.clear();
|
|
397
|
+
|
|
284
398
|
await new Promise<void>((resolve) => {
|
|
285
399
|
httpServer.close(() => { resolve(); });
|
|
286
400
|
});
|
package/src/server/log-server.ts
CHANGED
|
@@ -81,7 +81,7 @@ const colors = {
|
|
|
81
81
|
export interface LogServerOptions {
|
|
82
82
|
/** Port to listen on (default: 9080) */
|
|
83
83
|
port?: number;
|
|
84
|
-
/** Hostname to bind to (default:
|
|
84
|
+
/** Hostname to bind to (default: 0.0.0.0) */
|
|
85
85
|
host?: string;
|
|
86
86
|
/** Path to SSL certificate file (HTTPS only used if both certPath and keyPath provided) */
|
|
87
87
|
certPath?: string;
|
|
@@ -477,7 +477,7 @@ export function createLogServer(options: CreateLogServerOptions): CreateLogServe
|
|
|
477
477
|
*/
|
|
478
478
|
export function startLogServer(options: LogServerOptions = {}): http.Server | https.Server {
|
|
479
479
|
const port = options.port ?? 9080;
|
|
480
|
-
const host = options.host ?? "
|
|
480
|
+
const host = options.host ?? "0.0.0.0";
|
|
481
481
|
const quiet = options.quiet ?? false;
|
|
482
482
|
|
|
483
483
|
// Set up log file if specified
|
|
@@ -555,7 +555,7 @@ Usage:
|
|
|
555
555
|
|
|
556
556
|
Options:
|
|
557
557
|
--port, -p <port> Port to listen on (default: 9080)
|
|
558
|
-
--host, -h <host> Hostname to bind to (default:
|
|
558
|
+
--host, -h <host> Hostname to bind to (default: 0.0.0.0)
|
|
559
559
|
--cert, -c <path> Path to SSL certificate file (enables HTTPS)
|
|
560
560
|
--key, -k <path> Path to SSL private key file (enables HTTPS)
|
|
561
561
|
--log-file, -l <path> Write logs to file
|
|
@@ -677,87 +677,62 @@ export async function main(): Promise<void> {
|
|
|
677
677
|
|
|
678
678
|
const { options } = result;
|
|
679
679
|
|
|
680
|
+
// Common server options shared across all modes
|
|
681
|
+
const baseOptions = {
|
|
682
|
+
httpPort: options.port ?? 9080,
|
|
683
|
+
httpHost: options.host ?? "0.0.0.0",
|
|
684
|
+
quiet: options.quiet ?? false,
|
|
685
|
+
certPath: options.certPath,
|
|
686
|
+
keyPath: options.keyPath,
|
|
687
|
+
};
|
|
688
|
+
|
|
680
689
|
// Determine mode: mcp-only, http-only, or dual (default)
|
|
681
690
|
// All modes now use createDualServer with different options
|
|
682
691
|
const { createDualServer } = await import("./dual-server.js");
|
|
683
692
|
|
|
693
|
+
let dualServer;
|
|
694
|
+
let modeMessage: string;
|
|
695
|
+
|
|
684
696
|
if (options.mcpOnly) {
|
|
685
697
|
// MCP-only mode: HTTP only serves /log endpoint, MCP enabled
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
httpHost: options.host ?? "localhost",
|
|
698
|
+
dualServer = await createDualServer({
|
|
699
|
+
...baseOptions,
|
|
689
700
|
httpEnabled: true,
|
|
690
701
|
mcpEnabled: true,
|
|
691
|
-
quiet: options.quiet ?? false,
|
|
692
702
|
logReceiveOnly: true, // Only serve /log and /health endpoints
|
|
693
|
-
certPath: options.certPath,
|
|
694
|
-
keyPath: options.keyPath,
|
|
695
|
-
});
|
|
696
|
-
|
|
697
|
-
// Handle graceful shutdown
|
|
698
|
-
process.on("SIGINT", () => {
|
|
699
|
-
// eslint-disable-next-line no-console
|
|
700
|
-
console.log("\nShutting down...");
|
|
701
|
-
void dualServer.shutdown().then(() => {
|
|
702
|
-
process.exit(0);
|
|
703
|
-
});
|
|
704
703
|
});
|
|
705
|
-
|
|
706
|
-
if (!options.quiet) {
|
|
707
|
-
// eslint-disable-next-line no-console
|
|
708
|
-
console.log("MCP mode: Log receive endpoint and MCP tools running");
|
|
709
|
-
}
|
|
704
|
+
modeMessage = "MCP mode: Log receive endpoint and MCP tools running";
|
|
710
705
|
} else if (options.httpOnly) {
|
|
711
706
|
// HTTP-only mode: All HTTP endpoints, no MCP
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
httpHost: options.host ?? "localhost",
|
|
707
|
+
dualServer = await createDualServer({
|
|
708
|
+
...baseOptions,
|
|
715
709
|
httpEnabled: true,
|
|
716
710
|
mcpEnabled: false,
|
|
717
|
-
quiet: options.quiet ?? false,
|
|
718
|
-
certPath: options.certPath,
|
|
719
|
-
keyPath: options.keyPath,
|
|
720
711
|
logFile: options.logFile,
|
|
721
712
|
});
|
|
722
|
-
|
|
723
|
-
// Handle graceful shutdown
|
|
724
|
-
process.on("SIGINT", () => {
|
|
725
|
-
// eslint-disable-next-line no-console
|
|
726
|
-
console.log("\nShutting down...");
|
|
727
|
-
void dualServer.shutdown().then(() => {
|
|
728
|
-
process.exit(0);
|
|
729
|
-
});
|
|
730
|
-
});
|
|
731
|
-
|
|
732
|
-
if (!options.quiet) {
|
|
733
|
-
// eslint-disable-next-line no-console
|
|
734
|
-
console.log("HTTP-only mode: All HTTP endpoints running");
|
|
735
|
-
}
|
|
713
|
+
modeMessage = "HTTP-only mode: All HTTP endpoints running";
|
|
736
714
|
} else {
|
|
737
715
|
// Dual mode (default): All HTTP endpoints and MCP
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
httpHost: options.host ?? "localhost",
|
|
716
|
+
dualServer = await createDualServer({
|
|
717
|
+
...baseOptions,
|
|
741
718
|
httpEnabled: true,
|
|
742
719
|
mcpEnabled: true,
|
|
743
|
-
quiet: options.quiet ?? false,
|
|
744
|
-
certPath: options.certPath,
|
|
745
|
-
keyPath: options.keyPath,
|
|
746
720
|
logFile: options.logFile,
|
|
747
721
|
});
|
|
722
|
+
modeMessage = "Dual mode: HTTP and MCP servers running";
|
|
723
|
+
}
|
|
748
724
|
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
});
|
|
725
|
+
// Single SIGINT handler for all modes
|
|
726
|
+
process.on("SIGINT", () => {
|
|
727
|
+
// eslint-disable-next-line no-console
|
|
728
|
+
console.log("\nShutting down...");
|
|
729
|
+
void dualServer.shutdown().then(() => {
|
|
730
|
+
process.exit(0);
|
|
756
731
|
});
|
|
732
|
+
});
|
|
757
733
|
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
}
|
|
734
|
+
if (!options.quiet) {
|
|
735
|
+
// eslint-disable-next-line no-console
|
|
736
|
+
console.log(modeMessage);
|
|
762
737
|
}
|
|
763
738
|
}
|
|
@@ -14,8 +14,17 @@ export interface GeneratedCert {
|
|
|
14
14
|
/**
|
|
15
15
|
* Generate a self-signed certificate for HTTPS.
|
|
16
16
|
* The certificate is valid for localhost and common local development hostnames.
|
|
17
|
+
*
|
|
18
|
+
* NOTE: This function is currently unused because modern browsers reject
|
|
19
|
+
* self-signed certificates by default. It is retained for potential use cases:
|
|
20
|
+
* - Node.js clients that can disable certificate verification
|
|
21
|
+
* - Testing environments
|
|
22
|
+
* - Development setups where users manually trust the certificate
|
|
23
|
+
*
|
|
24
|
+
* For browser use, provide valid certificates via --cert and --key flags.
|
|
17
25
|
* @param hostname - Optional hostname to include in the certificate (default: localhost)
|
|
18
26
|
* @returns Object containing PEM-encoded certificate and private key
|
|
27
|
+
* @internal
|
|
19
28
|
*/
|
|
20
29
|
export function generateSelfSignedCert(hostname = "localhost"): GeneratedCert {
|
|
21
30
|
const attrs = [
|
|
@@ -313,6 +313,8 @@ export class ConsoleCaptureUI {
|
|
|
313
313
|
*/
|
|
314
314
|
private setupGlobalMethods(): void {
|
|
315
315
|
if (typeof window !== "undefined") {
|
|
316
|
+
// Create a function that returns current logs to avoid stale reference
|
|
317
|
+
const getCurrentLogs = (): CapturedLogEntry[] => [...this.logs];
|
|
316
318
|
window.__console__ = {
|
|
317
319
|
copy: () => this.copyLogs(),
|
|
318
320
|
download: () => {
|
|
@@ -322,7 +324,10 @@ export class ConsoleCaptureUI {
|
|
|
322
324
|
this.clearLogs();
|
|
323
325
|
},
|
|
324
326
|
get: () => this.getLogs(),
|
|
325
|
-
|
|
327
|
+
// Use getter to always return fresh copy of current logs
|
|
328
|
+
get logs() {
|
|
329
|
+
return getCurrentLogs();
|
|
330
|
+
},
|
|
326
331
|
};
|
|
327
332
|
}
|
|
328
333
|
}
|