@graphty/remote-logger 1.2.2 → 1.3.0

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.
Files changed (84) hide show
  1. package/README.md +53 -2
  2. package/dist/bundle/browser-entry.d.ts +34 -0
  3. package/dist/bundle/browser-entry.d.ts.map +1 -0
  4. package/dist/bundle/browser-entry.js +129 -0
  5. package/dist/bundle/browser-entry.js.map +1 -0
  6. package/dist/client/RemoteLogClient.d.ts +2 -1
  7. package/dist/client/RemoteLogClient.d.ts.map +1 -1
  8. package/dist/client/RemoteLogClient.js +20 -10
  9. package/dist/client/RemoteLogClient.js.map +1 -1
  10. package/dist/client/types.d.ts +6 -0
  11. package/dist/client/types.d.ts.map +1 -1
  12. package/dist/mcp/index.d.ts +1 -1
  13. package/dist/mcp/index.d.ts.map +1 -1
  14. package/dist/mcp/index.js +20 -1
  15. package/dist/mcp/index.js.map +1 -1
  16. package/dist/mcp/mcp-server.d.ts +5 -0
  17. package/dist/mcp/mcp-server.d.ts.map +1 -1
  18. package/dist/mcp/mcp-server.js +54 -8
  19. package/dist/mcp/mcp-server.js.map +1 -1
  20. package/dist/mcp/tools/index.d.ts +6 -6
  21. package/dist/mcp/tools/index.d.ts.map +1 -1
  22. package/dist/mcp/tools/index.js +1 -1
  23. package/dist/mcp/tools/index.js.map +1 -1
  24. package/dist/mcp/tools/logs-clear.d.ts +1 -2
  25. package/dist/mcp/tools/logs-clear.d.ts.map +1 -1
  26. package/dist/mcp/tools/logs-get-all.d.ts +1 -2
  27. package/dist/mcp/tools/logs-get-all.d.ts.map +1 -1
  28. package/dist/mcp/tools/logs-get-errors.d.ts +1 -2
  29. package/dist/mcp/tools/logs-get-errors.d.ts.map +1 -1
  30. package/dist/mcp/tools/logs-get-file-path.d.ts +8 -2
  31. package/dist/mcp/tools/logs-get-file-path.d.ts.map +1 -1
  32. package/dist/mcp/tools/logs-get-file-path.js +1 -1
  33. package/dist/mcp/tools/logs-get-file-path.js.map +1 -1
  34. package/dist/mcp/tools/logs-receive.d.ts +9 -10
  35. package/dist/mcp/tools/logs-receive.d.ts.map +1 -1
  36. package/dist/mcp/tools/logs-search.d.ts +1 -2
  37. package/dist/mcp/tools/logs-search.d.ts.map +1 -1
  38. package/dist/mcp/tools/logs-status.d.ts.map +1 -1
  39. package/dist/mcp/tools/logs-status.js +4 -1
  40. package/dist/mcp/tools/logs-status.js.map +1 -1
  41. package/dist/remote-logger.browser.js +166 -0
  42. package/dist/remote-logger.browser.js.map +1 -0
  43. package/dist/server/dual-server.d.ts +7 -4
  44. package/dist/server/dual-server.d.ts.map +1 -1
  45. package/dist/server/dual-server.js +124 -50
  46. package/dist/server/dual-server.js.map +1 -1
  47. package/dist/server/log-server.d.ts +12 -2
  48. package/dist/server/log-server.d.ts.map +1 -1
  49. package/dist/server/log-server.js +109 -63
  50. package/dist/server/log-server.js.map +1 -1
  51. package/dist/server/log-storage.d.ts +4 -0
  52. package/dist/server/log-storage.d.ts.map +1 -1
  53. package/dist/server/log-storage.js.map +1 -1
  54. package/dist/server/proxy.d.ts +39 -0
  55. package/dist/server/proxy.d.ts.map +1 -0
  56. package/dist/server/proxy.js +290 -0
  57. package/dist/server/proxy.js.map +1 -0
  58. package/dist/server/self-signed-cert.d.ts +9 -0
  59. package/dist/server/self-signed-cert.d.ts.map +1 -1
  60. package/dist/server/self-signed-cert.js +9 -0
  61. package/dist/server/self-signed-cert.js.map +1 -1
  62. package/dist/ui/ConsoleCaptureUI.d.ts.map +1 -1
  63. package/dist/ui/ConsoleCaptureUI.js +6 -1
  64. package/dist/ui/ConsoleCaptureUI.js.map +1 -1
  65. package/package.json +7 -2
  66. package/src/bundle/browser-entry.ts +157 -0
  67. package/src/client/RemoteLogClient.ts +22 -11
  68. package/src/client/types.ts +6 -0
  69. package/src/mcp/index.ts +42 -0
  70. package/src/mcp/mcp-server.ts +54 -8
  71. package/src/mcp/tools/index.ts +7 -0
  72. package/src/mcp/tools/logs-clear.ts +1 -1
  73. package/src/mcp/tools/logs-get-all.ts +1 -1
  74. package/src/mcp/tools/logs-get-errors.ts +1 -1
  75. package/src/mcp/tools/logs-get-file-path.ts +2 -2
  76. package/src/mcp/tools/logs-receive.ts +1 -1
  77. package/src/mcp/tools/logs-search.ts +1 -1
  78. package/src/mcp/tools/logs-status.ts +4 -1
  79. package/src/server/dual-server.ts +165 -57
  80. package/src/server/log-server.ts +126 -68
  81. package/src/server/log-storage.ts +4 -0
  82. package/src/server/proxy.ts +357 -0
  83. package/src/server/self-signed-cert.ts +9 -0
  84. package/src/ui/ConsoleCaptureUI.ts +6 -1
@@ -8,16 +8,18 @@
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 type * as http from "http";
12
- import type * as https from "https";
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";
16
17
 
17
18
  import { createMcpServer } from "../mcp/mcp-server.js";
18
19
  import { JsonlWriter } from "./jsonl-writer.js";
19
- import { createLogServer } from "./log-server.js";
20
+ import { createLogServer, setProxy } from "./log-server.js";
20
21
  import { LogStorage, type ServerMode } from "./log-storage.js";
22
+ import { createProxy, type ProxyInstance } from "./proxy.js";
21
23
  import { certFilesExist } from "./self-signed-cert.js";
22
24
 
23
25
  // ANSI color codes for terminal output
@@ -35,6 +37,90 @@ const MAX_PORT_SCAN_ATTEMPTS = 100;
35
37
  /** Maximum port number allowed (ports 9000-9099 per project guidelines) */
36
38
  const MAX_PORT_NUMBER = 9099;
37
39
 
40
+ /** Maximum retries for binding after EADDRINUSE during listen */
41
+ const MAX_BIND_RETRIES = 10;
42
+
43
+ /**
44
+ * Attempt to bind an HTTP/HTTPS server to a port with retry on EADDRINUSE.
45
+ * This handles race conditions where a port becomes unavailable between
46
+ * port scanning and actual binding.
47
+ * @param serverFactory - Function to create a new server instance
48
+ * @param startPort - Starting port to try
49
+ * @param host - Host to bind to
50
+ * @param quiet - Suppress output messages
51
+ * @param maxPort - Maximum port number to scan up to (default: 9099)
52
+ * @returns Promise resolving to bound server and actual port
53
+ */
54
+ async function tryBindWithRetry(
55
+ serverFactory: () => http.Server | https.Server,
56
+ startPort: number,
57
+ host: string,
58
+ quiet: boolean,
59
+ maxPort: number = MAX_PORT_NUMBER,
60
+ ): Promise<{ server: http.Server | https.Server; port: number }> {
61
+ let port = startPort;
62
+ let attempts = 0;
63
+
64
+ while (attempts < MAX_BIND_RETRIES) {
65
+ if (port > maxPort) {
66
+ break;
67
+ }
68
+
69
+ const server = serverFactory();
70
+
71
+ try {
72
+ await new Promise<void>((resolve, reject) => {
73
+ const errorHandler = (err: NodeJS.ErrnoException): void => {
74
+ if (err.code === "EADDRINUSE") {
75
+ if (!quiet) {
76
+ // eslint-disable-next-line no-console
77
+ console.log(
78
+ `${colors.yellow}Port ${port} claimed during bind, trying ${port + 1}...${colors.reset}`,
79
+ );
80
+ }
81
+ reject(err);
82
+ } else {
83
+ reject(err);
84
+ }
85
+ };
86
+
87
+ server.once("error", errorHandler);
88
+ server.listen({ port, host, exclusive: false }, () => {
89
+ server.removeListener("error", errorHandler);
90
+ resolve();
91
+ });
92
+ });
93
+
94
+ // Success - server is bound
95
+ if (!quiet) {
96
+ // eslint-disable-next-line no-console
97
+ console.log(
98
+ `${colors.green}HTTP server listening on ${host}:${port}${colors.reset}`,
99
+ );
100
+ }
101
+ return { server, port };
102
+ } catch (err) {
103
+ if ((err as NodeJS.ErrnoException).code === "EADDRINUSE") {
104
+ port++;
105
+ attempts++;
106
+ continue;
107
+ }
108
+ // Re-throw non-EADDRINUSE errors
109
+ throw err;
110
+ }
111
+ }
112
+
113
+ // Exhausted retries
114
+ const errorMsg = `Could not bind to any port after ${attempts} attempts. ` +
115
+ `Ports ${startPort}-${port - 1} are all in use or were claimed during bind.`;
116
+
117
+ if (!quiet) {
118
+ console.error(`${colors.red}${errorMsg}${colors.reset}`);
119
+ }
120
+
121
+ throw new Error(errorMsg);
122
+ }
123
+
38
124
  /**
39
125
  * Check if a port is available for binding.
40
126
  * @param port - Port number to check
@@ -72,16 +158,22 @@ async function isPortAvailable(port: number, host: string): Promise<boolean> {
72
158
  * @param basePort - Starting port number
73
159
  * @param host - Host to bind to
74
160
  * @param quiet - Suppress output messages
161
+ * @param maxPort - Maximum port number to scan up to (default: 9099)
75
162
  * @returns Promise resolving to available port number
76
163
  * @throws Error if no available port found within MAX_PORT_SCAN_ATTEMPTS
77
164
  */
78
- export async function findAvailablePort(basePort: number, host: string, quiet: boolean = false): Promise<number> {
165
+ export async function findAvailablePort(
166
+ basePort: number,
167
+ host: string,
168
+ quiet: boolean = false,
169
+ maxPort: number = MAX_PORT_NUMBER,
170
+ ): Promise<number> {
79
171
  let port = basePort;
80
172
  let attempts = 0;
81
173
 
82
174
  while (attempts < MAX_PORT_SCAN_ATTEMPTS) {
83
- if (port > MAX_PORT_NUMBER) {
84
- // Wrap around if we exceed max port (shouldn't happen normally)
175
+ if (port > maxPort) {
176
+ // Stop if we exceed max port
85
177
  break;
86
178
  }
87
179
 
@@ -107,12 +199,12 @@ export async function findAvailablePort(basePort: number, host: string, quiet: b
107
199
  }
108
200
 
109
201
  // No available port found
110
- const errorMsg = `Could not find available port after ${MAX_PORT_SCAN_ATTEMPTS} attempts starting from ${basePort}. ` +
111
- `Ports ${basePort}-${port - 1} are all in use. ` +
202
+ const errorMsg = `Could not find available port after ${attempts} attempts starting from ${basePort}. ` +
203
+ `Ports ${basePort}-${Math.min(port - 1, maxPort)} are all in use. ` +
112
204
  `Try killing existing processes: pkill -f "remote-log-server"`;
113
205
 
114
206
  if (!quiet) {
115
-
207
+
116
208
  console.error(`${colors.red}${errorMsg}${colors.reset}`);
117
209
  }
118
210
 
@@ -125,7 +217,7 @@ export async function findAvailablePort(basePort: number, host: string, quiet: b
125
217
  export interface DualServerOptions {
126
218
  /** Port for HTTP server (default: 9080) */
127
219
  httpPort?: number;
128
- /** Host for HTTP server (default: localhost) */
220
+ /** Host for HTTP server (default: 0.0.0.0) */
129
221
  httpHost?: string;
130
222
  /** Enable MCP server (default: true) */
131
223
  mcpEnabled?: boolean;
@@ -145,6 +237,8 @@ export interface DualServerOptions {
145
237
  jsonlWriter?: JsonlWriter;
146
238
  /** Only serve /log POST and /health GET endpoints (default: false) */
147
239
  logReceiveOnly?: boolean;
240
+ /** Maximum port number to scan up to (default: 9099) */
241
+ maxPortNumber?: number;
148
242
  }
149
243
 
150
244
  /**
@@ -176,7 +270,7 @@ export interface DualServerResult {
176
270
  export async function createDualServer(options: DualServerOptions = {}): Promise<DualServerResult> {
177
271
  const {
178
272
  httpPort: requestedPort = 9080,
179
- httpHost = "localhost",
273
+ httpHost = "0.0.0.0",
180
274
  mcpEnabled = true,
181
275
  httpEnabled = true,
182
276
  quiet = false,
@@ -185,6 +279,7 @@ export async function createDualServer(options: DualServerOptions = {}): Promise
185
279
  logReceiveOnly = false,
186
280
  certPath,
187
281
  keyPath,
282
+ maxPortNumber = MAX_PORT_NUMBER,
188
283
  } = options;
189
284
 
190
285
  // Create or use provided JSONL writer
@@ -197,6 +292,7 @@ export async function createDualServer(options: DualServerOptions = {}): Promise
197
292
  let httpServer: http.Server | https.Server | undefined;
198
293
  let mcpServer: McpServer | undefined;
199
294
  let actualHttpPort: number | undefined;
295
+ let proxyInstance: ProxyInstance | undefined;
200
296
 
201
297
  // Track active connections for graceful shutdown
202
298
  const activeConnections = new Set<net.Socket>();
@@ -204,18 +300,31 @@ export async function createDualServer(options: DualServerOptions = {}): Promise
204
300
  // Start HTTP server if enabled
205
301
  if (httpEnabled) {
206
302
  // Find an available port starting from the requested port
207
- actualHttpPort = await findAvailablePort(requestedPort, httpHost, quiet);
208
-
209
- const result = createLogServer({
210
- port: actualHttpPort,
211
- host: httpHost,
212
- storage,
303
+ const startPort = await findAvailablePort(requestedPort, httpHost, quiet, maxPortNumber);
304
+
305
+ // Factory function to create server instances (for retry on bind failure)
306
+ const createServerInstance = (): http.Server | https.Server => {
307
+ return createLogServer({
308
+ port: startPort, // Port is set but not used until bind
309
+ host: httpHost,
310
+ storage,
311
+ quiet: true, // Quiet during factory, messages handled by tryBindWithRetry
312
+ logReceiveOnly,
313
+ certPath,
314
+ keyPath,
315
+ }).server;
316
+ };
317
+
318
+ // Bind with retry logic to handle race conditions
319
+ const bindResult = await tryBindWithRetry(
320
+ createServerInstance,
321
+ startPort,
322
+ httpHost,
213
323
  quiet,
214
- logReceiveOnly,
215
- certPath,
216
- keyPath,
217
- });
218
- httpServer = result.server;
324
+ maxPortNumber,
325
+ );
326
+ httpServer = bindResult.server;
327
+ actualHttpPort = bindResult.port;
219
328
 
220
329
  // Track connections for graceful shutdown
221
330
  httpServer.on("connection", (socket: net.Socket) => {
@@ -225,40 +334,8 @@ export async function createDualServer(options: DualServerOptions = {}): Promise
225
334
  });
226
335
  });
227
336
 
228
- // Set short keep-alive timeout for tests (connections close faster)
229
- httpServer.keepAliveTimeout = 1000;
230
- httpServer.headersTimeout = 2000;
231
-
232
- // Wait for HTTP server to be listening
233
- const serverToStart = httpServer;
234
- const portToUse = actualHttpPort;
235
- await new Promise<void>((resolve, reject) => {
236
- serverToStart.on("error", (err: NodeJS.ErrnoException) => {
237
- // Provide helpful error message for port conflicts
238
- if (err.code === "EADDRINUSE") {
239
- const errorMsg = `Port ${portToUse} is already in use. ` +
240
- `This shouldn't happen after port scanning - there may be a race condition. ` +
241
- `Try again or kill existing processes: pkill -f "remote-log-server"`;
242
- if (!quiet) {
243
-
244
- console.error(`${colors.red}${errorMsg}${colors.reset}`);
245
- }
246
- reject(new Error(errorMsg));
247
- } else {
248
- reject(err);
249
- }
250
- });
251
- serverToStart.listen({ port: portToUse, host: httpHost, exclusive: false }, () => {
252
- serverToStart.removeListener("error", reject);
253
- if (!quiet) {
254
- // eslint-disable-next-line no-console
255
- console.log(
256
- `${colors.green}HTTP server listening on ${httpHost}:${portToUse}${colors.reset}`,
257
- );
258
- }
259
- resolve();
260
- });
261
- });
337
+ httpServer.keepAliveTimeout = 30000;
338
+ httpServer.headersTimeout = 35000;
262
339
 
263
340
  // Set server config in storage so MCP tools can report it
264
341
  // Determine protocol based on whether valid cert files were provided
@@ -273,13 +350,39 @@ export async function createDualServer(options: DualServerOptions = {}): Promise
273
350
  } else {
274
351
  mode = "dual";
275
352
  }
353
+ // When bound to all interfaces (0.0.0.0), detect the machine's IP address
354
+ // for the endpoint URL since 0.0.0.0 is not a routable address for clients
355
+ let endpointHost = httpHost;
356
+ if (httpHost === "0.0.0.0") {
357
+ const internalIp = internalIpV4Sync();
358
+ if (internalIp) {
359
+ endpointHost = internalIp;
360
+ } else {
361
+ // Fallback for local-only use - don't use hostname as it may not resolve
362
+ endpointHost = "127.0.0.1";
363
+ if (!quiet) {
364
+ console.warn(
365
+ `${colors.yellow}Could not detect LAN IP. Endpoint URL will only work locally.${colors.reset}`,
366
+ );
367
+ }
368
+ }
369
+ }
370
+ const scriptUrl = `${protocol}://${endpointHost}:${actualHttpPort}/remote-logger.js`;
371
+ const proxyBaseUrl = `/proxy/`;
372
+
276
373
  storage.setServerConfig({
277
374
  httpPort: actualHttpPort,
278
375
  httpHost,
279
376
  protocol,
280
- httpEndpoint: `${protocol}://${httpHost}:${actualHttpPort}/log`,
377
+ httpEndpoint: `${protocol}://${endpointHost}:${actualHttpPort}/log`,
378
+ scriptUrl,
379
+ proxyBaseUrl: `${protocol}://${endpointHost}:${actualHttpPort}${proxyBaseUrl}`,
281
380
  mode,
282
381
  });
382
+
383
+ // Create and register the proxy instance
384
+ proxyInstance = createProxy(proxyBaseUrl, quiet);
385
+ setProxy(proxyInstance);
283
386
  }
284
387
 
285
388
  // Create and connect MCP server if enabled
@@ -307,6 +410,11 @@ export async function createDualServer(options: DualServerOptions = {}): Promise
307
410
  });
308
411
  }
309
412
 
413
+ // Close proxy
414
+ if (proxyInstance) {
415
+ proxyInstance.close();
416
+ }
417
+
310
418
  // Close JSONL writer only if we created it internally
311
419
  if (ownsJsonlWriter) {
312
420
  await jsonlWriter.close();
@@ -18,10 +18,11 @@ import * as http from "http";
18
18
  import * as https from "https";
19
19
  import * as os from "os";
20
20
  import * as path from "path";
21
- import { URL } from "url";
21
+ import { fileURLToPath,URL } from "url";
22
22
 
23
23
  import { JsonlWriter } from "./jsonl-writer.js";
24
24
  import { type LogEntry, LogStorage } from "./log-storage.js";
25
+ import type { ProxyInstance } from "./proxy.js";
25
26
  import { certFilesExist, readCertFiles } from "./self-signed-cert.js";
26
27
 
27
28
  // Shared log storage instance
@@ -62,6 +63,55 @@ export function setLogStorage(storage: LogStorage): void {
62
63
  sharedStorage = storage;
63
64
  }
64
65
 
66
+ // Shared proxy instance
67
+ let sharedProxy: ProxyInstance | null = null;
68
+
69
+ /**
70
+ * Set the shared proxy instance.
71
+ * @param proxy - The proxy instance to use
72
+ */
73
+ export function setProxy(proxy: ProxyInstance): void {
74
+ sharedProxy = proxy;
75
+ }
76
+
77
+
78
+ // Browser bundle cache (loaded on first request)
79
+ let browserBundleCache: string | null | undefined;
80
+
81
+ function loadBrowserBundle(): string | null {
82
+ if (browserBundleCache !== undefined) {
83
+ return browserBundleCache;
84
+ }
85
+
86
+ const thisDir = path.dirname(fileURLToPath(import.meta.url));
87
+
88
+ // Try multiple locations: dist/ (when running from built output)
89
+ // and the package root dist/ (when running from source during tests)
90
+ const candidates = [
91
+ path.resolve(thisDir, "..", "remote-logger.browser.js"),
92
+ path.resolve(thisDir, "..", "..", "dist", "remote-logger.browser.js"),
93
+ ];
94
+
95
+ for (const candidate of candidates) {
96
+ try {
97
+ browserBundleCache = fs.readFileSync(candidate, "utf-8");
98
+ return browserBundleCache;
99
+ } catch {
100
+ // Try next candidate
101
+ }
102
+ }
103
+
104
+ browserBundleCache = null;
105
+ return browserBundleCache;
106
+ }
107
+
108
+ /**
109
+ * Reset the browser bundle cache. Used for testing.
110
+ */
111
+ export function resetBrowserBundleCache(): void {
112
+ browserBundleCache = undefined;
113
+ }
114
+
65
115
  // ANSI color codes for terminal output
66
116
  const colors = {
67
117
  reset: "\x1b[0m",
@@ -81,7 +131,7 @@ const colors = {
81
131
  export interface LogServerOptions {
82
132
  /** Port to listen on (default: 9080) */
83
133
  port?: number;
84
- /** Hostname to bind to (default: localhost) */
134
+ /** Hostname to bind to (default: 0.0.0.0) */
85
135
  host?: string;
86
136
  /** Path to SSL certificate file (HTTPS only used if both certPath and keyPath provided) */
87
137
  certPath?: string;
@@ -269,7 +319,36 @@ function handleRequest(
269
319
  return;
270
320
  }
271
321
 
272
- // In logReceiveOnly mode, only /log and /health are available
322
+ // Serve browser-ready script bundle (available in all modes including logReceiveOnly)
323
+ if ((url === "/remote-logger.js" || url.startsWith("/remote-logger.js?")) && req.method === "GET") {
324
+ const bundle = loadBrowserBundle();
325
+ if (!bundle) {
326
+ res.writeHead(404, { "Content-Type": "application/json" });
327
+ res.end(JSON.stringify({ error: "Browser bundle not found. Run 'npm run build' first." }));
328
+ return;
329
+ }
330
+
331
+ const hostHeader = req.headers.host ?? `${host}:${port}`;
332
+ const serverUrl = `${protocol}://${hostHeader}`;
333
+ const configPrefix = `window.__REMOTE_LOG_SERVER_URL__="${serverUrl}";\n`;
334
+
335
+ res.writeHead(200, {
336
+ "Content-Type": "application/javascript",
337
+ "Cache-Control": "no-cache",
338
+ });
339
+ res.end(configPrefix + bundle);
340
+ return;
341
+ }
342
+
343
+ // Reverse proxy with auto-injection (available in all modes including logReceiveOnly)
344
+ if (url.startsWith("/proxy/") && sharedProxy) {
345
+ const targetUrl = decodeURIComponent(url.substring("/proxy/".length));
346
+ const hostHeader = req.headers.host ?? `${host}:${port}`;
347
+ sharedProxy.handleRequest(req, res, targetUrl, protocol, hostHeader);
348
+ return;
349
+ }
350
+
351
+ // In logReceiveOnly mode, only /log, /health, /remote-logger.js, and /proxy/ are available
273
352
  if (logReceiveOnly) {
274
353
  res.writeHead(404, { "Content-Type": "application/json" });
275
354
  res.end(JSON.stringify({ error: "Not found (log receive only mode)" }));
@@ -377,17 +456,21 @@ function printBanner(host: string, port: number, useHttps: boolean): void {
377
456
  // eslint-disable-next-line no-console
378
457
  console.log(`${colors.yellow}API Endpoints:${colors.reset}`);
379
458
  // eslint-disable-next-line no-console
380
- console.log(` ${colors.cyan}POST /log ${colors.reset} - Receive logs from browser`);
459
+ console.log(` ${colors.cyan}POST /log ${colors.reset} - Receive logs from browser`);
460
+ // eslint-disable-next-line no-console
461
+ console.log(` ${colors.cyan}GET /remote-logger.js ${colors.reset} - Browser-ready auto-config script`);
381
462
  // eslint-disable-next-line no-console
382
- console.log(` ${colors.cyan}GET /logs ${colors.reset} - Get all logs as JSON`);
463
+ console.log(` ${colors.cyan}* /proxy/<url> ${colors.reset} - Reverse proxy with script injection`);
383
464
  // eslint-disable-next-line no-console
384
- console.log(` ${colors.cyan}GET /logs/recent ${colors.reset} - Get last 50 logs (?n=100 for more)`);
465
+ console.log(` ${colors.cyan}GET /logs ${colors.reset} - Get all logs as JSON`);
385
466
  // eslint-disable-next-line no-console
386
- console.log(` ${colors.cyan}GET /logs/errors ${colors.reset} - Get only error logs`);
467
+ console.log(` ${colors.cyan}GET /logs/recent ${colors.reset} - Get last 50 logs (?n=100 for more)`);
387
468
  // eslint-disable-next-line no-console
388
- console.log(` ${colors.cyan}POST /logs/clear ${colors.reset} - Clear all logs`);
469
+ console.log(` ${colors.cyan}GET /logs/errors ${colors.reset} - Get only error logs`);
389
470
  // eslint-disable-next-line no-console
390
- console.log(` ${colors.cyan}GET /health ${colors.reset} - Health check`);
471
+ console.log(` ${colors.cyan}POST /logs/clear ${colors.reset} - Clear all logs`);
472
+ // eslint-disable-next-line no-console
473
+ console.log(` ${colors.cyan}GET /health ${colors.reset} - Health check`);
391
474
  // eslint-disable-next-line no-console
392
475
  console.log("");
393
476
  // eslint-disable-next-line no-console
@@ -477,7 +560,7 @@ export function createLogServer(options: CreateLogServerOptions): CreateLogServe
477
560
  */
478
561
  export function startLogServer(options: LogServerOptions = {}): http.Server | https.Server {
479
562
  const port = options.port ?? 9080;
480
- const host = options.host ?? "localhost";
563
+ const host = options.host ?? "0.0.0.0";
481
564
  const quiet = options.quiet ?? false;
482
565
 
483
566
  // Set up log file if specified
@@ -555,7 +638,7 @@ Usage:
555
638
 
556
639
  Options:
557
640
  --port, -p <port> Port to listen on (default: 9080)
558
- --host, -h <host> Hostname to bind to (default: localhost)
641
+ --host, -h <host> Hostname to bind to (default: 0.0.0.0)
559
642
  --cert, -c <path> Path to SSL certificate file (enables HTTPS)
560
643
  --key, -k <path> Path to SSL private key file (enables HTTPS)
561
644
  --log-file, -l <path> Write logs to file
@@ -677,87 +760,62 @@ export async function main(): Promise<void> {
677
760
 
678
761
  const { options } = result;
679
762
 
763
+ // Common server options shared across all modes
764
+ const baseOptions = {
765
+ httpPort: options.port ?? 9080,
766
+ httpHost: options.host ?? "0.0.0.0",
767
+ quiet: options.quiet ?? false,
768
+ certPath: options.certPath,
769
+ keyPath: options.keyPath,
770
+ };
771
+
680
772
  // Determine mode: mcp-only, http-only, or dual (default)
681
773
  // All modes now use createDualServer with different options
682
774
  const { createDualServer } = await import("./dual-server.js");
683
775
 
776
+ let dualServer;
777
+ let modeMessage: string;
778
+
684
779
  if (options.mcpOnly) {
685
780
  // MCP-only mode: HTTP only serves /log endpoint, MCP enabled
686
- const dualServer = await createDualServer({
687
- httpPort: options.port ?? 9080,
688
- httpHost: options.host ?? "localhost",
781
+ dualServer = await createDualServer({
782
+ ...baseOptions,
689
783
  httpEnabled: true,
690
784
  mcpEnabled: true,
691
- quiet: options.quiet ?? false,
692
785
  logReceiveOnly: true, // Only serve /log and /health endpoints
693
- certPath: options.certPath,
694
- keyPath: options.keyPath,
695
786
  });
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
- });
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
- }
787
+ modeMessage = "MCP mode: Log receive endpoint and MCP tools running";
710
788
  } else if (options.httpOnly) {
711
789
  // HTTP-only mode: All HTTP endpoints, no MCP
712
- const dualServer = await createDualServer({
713
- httpPort: options.port ?? 9080,
714
- httpHost: options.host ?? "localhost",
790
+ dualServer = await createDualServer({
791
+ ...baseOptions,
715
792
  httpEnabled: true,
716
793
  mcpEnabled: false,
717
- quiet: options.quiet ?? false,
718
- certPath: options.certPath,
719
- keyPath: options.keyPath,
720
794
  logFile: options.logFile,
721
795
  });
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
- }
796
+ modeMessage = "HTTP-only mode: All HTTP endpoints running";
736
797
  } else {
737
798
  // Dual mode (default): All HTTP endpoints and MCP
738
- const dualServer = await createDualServer({
739
- httpPort: options.port ?? 9080,
740
- httpHost: options.host ?? "localhost",
799
+ dualServer = await createDualServer({
800
+ ...baseOptions,
741
801
  httpEnabled: true,
742
802
  mcpEnabled: true,
743
- quiet: options.quiet ?? false,
744
- certPath: options.certPath,
745
- keyPath: options.keyPath,
746
803
  logFile: options.logFile,
747
804
  });
805
+ modeMessage = "Dual mode: HTTP and MCP servers running";
806
+ }
748
807
 
749
- // Handle graceful shutdown
750
- process.on("SIGINT", () => {
751
- // eslint-disable-next-line no-console
752
- console.log("\nShutting down...");
753
- void dualServer.shutdown().then(() => {
754
- process.exit(0);
755
- });
808
+ // Single SIGINT handler for all modes
809
+ process.on("SIGINT", () => {
810
+ // eslint-disable-next-line no-console
811
+ console.log("\nShutting down...");
812
+ void dualServer.shutdown().then(() => {
813
+ process.exit(0);
756
814
  });
815
+ });
757
816
 
758
- if (!options.quiet) {
759
- // eslint-disable-next-line no-console
760
- console.log("Dual mode: HTTP and MCP servers running");
761
- }
817
+ if (!options.quiet) {
818
+ // eslint-disable-next-line no-console
819
+ console.log(modeMessage);
762
820
  }
763
821
  }
@@ -141,6 +141,10 @@ export interface ServerConfig {
141
141
  protocol: "http" | "https";
142
142
  /** Full URL for browser clients to send logs to */
143
143
  httpEndpoint: string;
144
+ /** URL to the browser-ready auto-config script */
145
+ scriptUrl?: string;
146
+ /** Base URL for the reverse proxy (e.g., "http://host:port/proxy/") */
147
+ proxyBaseUrl?: string;
144
148
  /** Server mode (mcp-only, http-only, or dual) */
145
149
  mode: ServerMode;
146
150
  }