Skip to content

Commit d6322d7

Browse files
committed
feat: update MCP configuration to include discovery options and enhance log handling capabilities
1 parent 363a552 commit d6322d7

3 files changed

Lines changed: 137 additions & 15 deletions

File tree

.browser-echo-mcp.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"url":"http://127.0.0.1:5179","routeLogs":"/__client-logs","timestamp":1755789053743,"pid":36056}
1+
{"url":"http://127.0.0.1:5179","routeLogs":"/__client-logs","timestamp":1755797556236,"pid":33695}

example/vue-vite-app/vite.config.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ import browserEcho from '@browser-echo/vite'
66
export default defineConfig({
77
plugins: [vue(), browserEcho(
88
{
9-
stackMode: 'condensed'
9+
stackMode: 'condensed',
10+
mcp: {
11+
suppressTerminal: true
12+
}
1013
},
1114
)],
1215
})

packages/vite/src/index.ts

Lines changed: 132 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
// Avoid exporting Vite types to prevent cross-version type mismatches in consumers
22
import ansis from 'ansis';
33
import type { BrowserLogLevel } from '@browser-echo/core';
4-
import { mkdirSync, appendFileSync } from 'node:fs';
4+
import { mkdirSync, appendFileSync, existsSync, readFileSync } from 'node:fs';
55
import { dirname, join as joinPath } from 'node:path';
6+
import { tmpdir } from 'node:os';
67

78
export interface BrowserLogsToTerminalOptions {
89
enabled?: boolean;
@@ -18,12 +19,18 @@ export interface BrowserLogsToTerminalOptions {
1819
truncate?: number;
1920
fileLog?: { enabled?: boolean; dir?: string };
2021
mcp?: { url?: string; routeLogs?: `/${string}`; suppressTerminal?: boolean; headers?: Record<string,string> };
22+
/** Enable MCP auto-discovery (env → discovery file → port scan) */
23+
discoverMcp?: boolean;
24+
/** Refresh interval for discovery (ms) */
25+
discoveryRefreshMs?: number;
26+
/** Ports to scan for /health when discovering MCP */
27+
discoveryPorts?: number[];
2128
}
2229

2330
type ResolvedOptions = Required<Omit<BrowserLogsToTerminalOptions, 'batch' | 'fileLog' | 'mcp'>> & {
2431
batch: Required<NonNullable<BrowserLogsToTerminalOptions['batch']>>;
2532
fileLog: Required<NonNullable<BrowserLogsToTerminalOptions['fileLog']>>;
26-
mcp: { url: string; routeLogs: `/${string}`; suppressTerminal: boolean; headers: Record<string,string> };
33+
mcp: { url: string; routeLogs: `/${string}`; suppressTerminal: boolean; headers: Record<string,string>; suppressProvided: boolean };
2734
};
2835

2936
const DEFAULTS: ResolvedOptions = {
@@ -39,7 +46,10 @@ const DEFAULTS: ResolvedOptions = {
3946
batch: { size: 20, interval: 300 },
4047
truncate: 10_000,
4148
fileLog: { enabled: false, dir: 'logs/frontend' },
42-
mcp: { url: '', routeLogs: '/__client-logs', suppressTerminal: true, headers: {} }
49+
mcp: { url: '', routeLogs: '/__client-logs', suppressTerminal: true, headers: {} },
50+
discoverMcp: true,
51+
discoveryRefreshMs: 30_000,
52+
discoveryPorts: [5179, 5178, 3001, 4000, 5173]
4353
};
4454

4555
export default function browserEcho(opts: BrowserLogsToTerminalOptions = {}): any {
@@ -49,12 +59,11 @@ export default function browserEcho(opts: BrowserLogsToTerminalOptions = {}): an
4959
batch: { ...DEFAULTS.batch, ...(opts.batch ?? {}) },
5060
fileLog: { ...DEFAULTS.fileLog, ...(opts.fileLog ?? {}) },
5161
mcp: {
52-
url: opts.mcp?.url || process.env.BROWSER_ECHO_MCP_URL || '',
62+
url: normalizeMcpBaseUrl(opts.mcp?.url || process.env.BROWSER_ECHO_MCP_URL || ''),
5363
routeLogs: (opts.mcp?.routeLogs || (process.env.BROWSER_ECHO_MCP_LOGS_ROUTE as `/${string}`) || '/__client-logs') as `/${string}`,
54-
suppressTerminal: typeof opts.mcp?.suppressTerminal === 'boolean'
55-
? opts.mcp.suppressTerminal
56-
: !!(opts.mcp?.url || process.env.BROWSER_ECHO_MCP_URL) && process.env.BROWSER_ECHO_SUPPRESS_TERMINAL !== '0',
57-
headers: opts.mcp?.headers || {}
64+
suppressTerminal: typeof opts.mcp?.suppressTerminal === 'boolean' ? opts.mcp.suppressTerminal : false,
65+
headers: opts.mcp?.headers || {},
66+
suppressProvided: typeof opts.mcp?.suppressTerminal === 'boolean'
5867
}
5968
};
6069
const VIRTUAL_ID = '\0virtual:browser-echo';
@@ -84,12 +93,114 @@ export default function browserEcho(opts: BrowserLogsToTerminalOptions = {}): an
8493
};
8594
}
8695

96+
function normalizeMcpBaseUrl(input: string | undefined): string {
97+
if (!input) return '';
98+
const raw = String(input).trim();
99+
if (!raw) return '';
100+
const noSlash = raw.replace(/\/+$/, '');
101+
// If a full MCP URL is provided (ending in /mcp), convert to base
102+
return noSlash.replace(/\/mcp$/i, '');
103+
}
104+
87105
function attachMiddleware(server: any, options: ResolvedOptions) {
88106
const sessionStamp = new Date().toISOString().replace(/[:.]/g, '-');
89107
const logFilePath = joinPath(options.fileLog.dir, `dev-${sessionStamp}.log`);
90108
if (options.fileLog.enabled) { try { mkdirSync(dirname(logFilePath), { recursive: true }); } catch {} }
91109

92-
const mcpUrl = options.mcp.url ? options.mcp.url.replace(/\/$/, '') : '';
110+
// Dynamic MCP ingest resolution (explicit → env → discovery file → port scan)
111+
let resolvedBase = options.mcp.url ? options.mcp.url.replace(/\/$/, '') : '';
112+
let resolvedIngest = resolvedBase ? `${resolvedBase}${options.mcp.routeLogs}` : '';
113+
let lastAnnouncement = '';
114+
115+
const refreshMs = Math.max(1_000, Number(options.discoveryRefreshMs || 30_000) | 0);
116+
117+
const announce = (msg: string) => {
118+
if (msg && msg !== lastAnnouncement) {
119+
try { server.config.logger.info(msg); } catch {}
120+
lastAnnouncement = msg;
121+
}
122+
};
123+
124+
async function tryPingHealth(base: string, timeoutMs = 400): Promise<boolean> {
125+
try {
126+
const ctrl = new AbortController();
127+
const t = setTimeout(() => ctrl.abort(), timeoutMs);
128+
const res = await fetch(`${base}/health`, { signal: ctrl.signal as any, cache: 'no-store' as any });
129+
clearTimeout(t);
130+
return !!res?.ok;
131+
} catch { return false; }
132+
}
133+
134+
function readDiscoveryFile(): { url: string; routeLogs?: string; ts?: number } | null {
135+
try {
136+
const candidates = [
137+
joinPath(process.cwd(), '.browser-echo-mcp.json'),
138+
joinPath(tmpdir(), 'browser-echo-mcp.json')
139+
];
140+
for (const p of candidates) {
141+
try {
142+
if (!existsSync(p)) continue;
143+
const raw = readFileSync(p, 'utf-8');
144+
const data = JSON.parse(raw);
145+
const url = (data?.url ? String(data.url) : '').replace(/\/$/, '');
146+
const routeLogs = data?.routeLogs ? String(data.routeLogs) : undefined;
147+
const ts = typeof data?.timestamp === 'number' ? data.timestamp : undefined;
148+
if (url) return { url, routeLogs, ts };
149+
} catch {}
150+
}
151+
} catch {}
152+
return null;
153+
}
154+
155+
async function resolveMcp() {
156+
if (!options.discoverMcp) return;
157+
// 1) Explicit (already normalized)
158+
const explicit = normalizeMcpBaseUrl(options.mcp.url || process.env.BROWSER_ECHO_MCP_URL || '');
159+
if (explicit) {
160+
const base = explicit.replace(/\/$/, '');
161+
resolvedBase = base;
162+
resolvedIngest = `${base}${options.mcp.routeLogs}`;
163+
announce(`${options.tag} forwarding logs to MCP ingest at ${resolvedIngest}`);
164+
return;
165+
}
166+
// 2) Discovery file (fresh within 60s)
167+
const disc = readDiscoveryFile();
168+
if (disc && disc.url) {
169+
const ageOk = !disc.ts || (Date.now() - Number(disc.ts)) < 60_000;
170+
if (ageOk) {
171+
const base = String(disc.url).replace(/\/$/, '');
172+
const routeLogs = (disc.routeLogs as `/${string}`) || options.mcp.routeLogs;
173+
resolvedBase = base;
174+
resolvedIngest = `${base}${routeLogs}`;
175+
announce(`${options.tag} forwarding logs to MCP ingest at ${resolvedIngest}`);
176+
return;
177+
}
178+
}
179+
// 3) Port scan common ports
180+
for (const port of options.discoveryPorts || []) {
181+
for (const host of [`http://127.0.0.1:${port}`, `http://localhost:${port}`]) {
182+
if (await tryPingHealth(host)) {
183+
resolvedBase = host;
184+
resolvedIngest = `${host}${options.mcp.routeLogs}`;
185+
announce(`${options.tag} forwarding logs to MCP ingest at ${resolvedIngest}`);
186+
return;
187+
}
188+
}
189+
}
190+
// 4) Not found
191+
if (resolvedIngest) {
192+
// lost connection → inform once
193+
announce(`${options.tag} no MCP detected; logging locally. To forward, set BROWSER_ECHO_MCP_URL or start MCP.`);
194+
}
195+
resolvedBase = '';
196+
resolvedIngest = '';
197+
}
198+
199+
// Kick off periodic discovery
200+
if (options.discoverMcp) {
201+
resolveMcp();
202+
try { const h = setInterval(resolveMcp, refreshMs); (h as any).unref?.(); } catch {}
203+
}
93204

94205
server.middlewares.use(options.route, (req, res, next) => {
95206
if (req.method !== 'POST') return next();
@@ -101,11 +212,11 @@ function attachMiddleware(server: any, options: ResolvedOptions) {
101212
if (!payload || !Array.isArray(payload.entries)) { res.statusCode = 400; res.end('invalid payload'); return; }
102213

103214
// Mirror to MCP server if configured
104-
if (mcpUrl) {
215+
const targetIngest = resolvedIngest || '';
216+
if (targetIngest) {
105217
try {
106-
const target = `${mcpUrl}${options.mcp.routeLogs}`;
107218
// do not await
108-
fetch(target, {
219+
fetch(targetIngest, {
109220
method: 'POST',
110221
headers: { 'content-type': 'application/json', ...options.mcp.headers },
111222
body: JSON.stringify(payload),
@@ -116,7 +227,15 @@ function attachMiddleware(server: any, options: ResolvedOptions) {
116227
}
117228

118229
const logger = server.config.logger;
119-
const shouldPrint = !(options.mcp.suppressTerminal && mcpUrl);
230+
const envVal = process.env.BROWSER_ECHO_SUPPRESS_TERMINAL;
231+
const forceSuppress = envVal === '1';
232+
const forcePrint = envVal === '0';
233+
let suppressTerminal: boolean;
234+
if (forceSuppress) suppressTerminal = true;
235+
else if (forcePrint) suppressTerminal = false;
236+
else if (options.mcp.suppressProvided) suppressTerminal = options.mcp.suppressTerminal && !!targetIngest;
237+
else suppressTerminal = !!targetIngest; // auto: suppress when forwarding active
238+
const shouldPrint = !suppressTerminal;
120239
const sid = (payload.sessionId ?? 'anon').slice(0, 8);
121240
for (const entry of payload.entries) {
122241
const level = normalizeLevel(entry.level);

0 commit comments

Comments
 (0)