Skip to content

Commit 1841f49

Browse files
committed
feat: improve MCP log ingestion handling and availability detection
- Enhanced the MCP log ingestion process by adding support for project-local discovery files and improved error handling for unavailable MCP endpoints. - Updated the logic to determine MCP availability, ensuring terminal output is suppressed when a valid MCP is configured and available. - Refactored the health probe mechanism to better manage MCP availability transitions and provide user feedback on log forwarding status. - Simplified the configuration of log ingestion routes and headers for better clarity and usability.
1 parent 9dcd94e commit 1841f49

1 file changed

Lines changed: 81 additions & 37 deletions

File tree

packages/vite/src/index.ts

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

77
export interface BrowserLogsToTerminalOptions {
@@ -53,9 +53,9 @@ export default function browserEcho(opts: BrowserLogsToTerminalOptions = {}): an
5353
fileLog: { ...DEFAULTS.fileLog, ...(opts.fileLog ?? {}) },
5454
mcp: {
5555
url: normalizeMcpBaseUrl(opts.mcp?.url || ''),
56-
routeLogs: (opts.mcp?.routeLogs || '/__client-logs') as `/${string}`,
57-
suppressTerminal: typeof opts.mcp?.suppressTerminal === 'boolean' ? opts.mcp.suppressTerminal : false,
58-
headers: opts.mcp?.headers || {},
56+
routeLogs: (opts.mcp?.routeLogs ?? DEFAULTS.mcp.routeLogs) as `/${string}`,
57+
suppressTerminal: (opts.mcp?.suppressTerminal ?? DEFAULTS.mcp.suppressTerminal) as boolean,
58+
headers: opts.mcp?.headers ?? {},
5959
suppressProvided: typeof opts.mcp?.suppressTerminal === 'boolean'
6060
}
6161
};
@@ -103,7 +103,7 @@ function attachMiddleware(server: any, options: ResolvedOptions) {
103103
// Single global server model: compute base; manage connection state
104104
let resolvedBase = '';
105105
let resolvedIngest = '';
106-
let hasForwardedSuccessfully = false; // suppress only after first confirmed 2xx
106+
let isRemoteAvailable = false; // reflects current availability of the configured/current MCP ingest
107107
let lastAnnouncement = '';
108108

109109
const announce = (msg: string) => {
@@ -114,20 +114,49 @@ function attachMiddleware(server: any, options: ResolvedOptions) {
114114
};
115115

116116
function computeBaseOnce() {
117-
const base = String(options.mcp.url || process.env.BROWSER_ECHO_MCP_URL || 'http://127.0.0.1:5179')
118-
.replace(/\/$/, '')
119-
.replace(/\/mcp$/i, '');
120-
resolvedBase = base;
121-
resolvedIngest = `${base}${options.mcp.routeLogs}`;
117+
// 1) Explicit URL provided via options or env has highest priority
118+
const explicit = String(options.mcp.url || process.env.BROWSER_ECHO_MCP_URL || '').trim();
119+
if (explicit) {
120+
const base = explicit.replace(/\/$/, '').replace(/\/mcp$/i, '');
121+
resolvedBase = base;
122+
resolvedIngest = `${base}${options.mcp.routeLogs}`;
123+
return;
124+
}
125+
126+
// 2) Project-local discovery file in CWD (do not default to port 5179)
127+
try {
128+
const discPath = joinPath(process.cwd(), '.browser-echo-mcp.json');
129+
if (existsSync(discPath)) {
130+
try {
131+
const txt = readFileSync(discPath, 'utf-8');
132+
const obj = JSON.parse(txt || '{}') as { url?: string; route?: string };
133+
if (obj && obj.url) {
134+
const base = String(obj.url).trim().replace(/\/$/, '').replace(/\/mcp$/i, '');
135+
const route = (obj as any).route || options.mcp.routeLogs || '/__client-logs';
136+
resolvedBase = base;
137+
resolvedIngest = `${base}${route as `/${string}`}`;
138+
return;
139+
}
140+
} catch {}
141+
}
142+
} catch {}
143+
144+
// 3) No configured or discovered MCP → keep base empty; do not probe, always print locally
145+
resolvedBase = '';
146+
resolvedIngest = '';
122147
}
123148

124149
computeBaseOnce();
150+
// If we have a configured/discovered MCP, assume available initially so terminal is suppressed.
151+
// We'll flip to unavailable on probe/forward failure and resume terminal printing.
152+
isRemoteAvailable = !!resolvedBase;
125153

126-
// Start a small background probe to detect MCP coming online after Vite
154+
// Start a small background probe to detect MCP availability transitions (only when a base is known)
127155
startHealthProbe();
128156

129157
async function probeHealth(): Promise<boolean> {
130158
try {
159+
if (!resolvedBase) return false;
131160
const ctrl = new AbortController();
132161
const t = setTimeout(() => ctrl.abort(), 400);
133162
const res = await fetch(`${resolvedBase}/health`, { signal: ctrl.signal as any, cache: 'no-store' as any });
@@ -144,14 +173,18 @@ function attachMiddleware(server: any, options: ResolvedOptions) {
144173
if (started) return;
145174
started = true;
146175
const interval = setInterval(async () => {
147-
if (hasForwardedSuccessfully) return; // already forwarding
176+
if (!resolvedBase) return; // nothing to probe without a known/current MCP
148177
const ok = await probeHealth();
149-
if (ok) {
150-
hasForwardedSuccessfully = true;
178+
if (ok && !isRemoteAvailable) {
179+
isRemoteAvailable = true;
151180
announce(`${options.tag} forwarding logs to MCP ingest at ${resolvedIngest}`);
181+
} else if (!ok && isRemoteAvailable) {
182+
// MCP became unavailable → resume terminal printing
183+
isRemoteAvailable = false;
184+
announce(`${options.tag} MCP ingest unavailable; printing logs locally`);
152185
}
153186
}, 1500);
154-
// NOTE: we intentionally do not clear the interval; it's cheap and guarded above.
187+
// NOTE: we intentionally do not clear the interval.
155188
}
156189

157190
server.middlewares.use(options.route, (req: import('http').IncomingMessage, res: import('http').ServerResponse, next: Function) => {
@@ -166,31 +199,42 @@ function attachMiddleware(server: any, options: ResolvedOptions) {
166199
// Mirror to MCP server (fire-and-forget) and update connection state
167200
const targetIngest = resolvedIngest;
168201
try {
169-
const projectName = (process.env.BROWSER_ECHO_PROJECT_NAME || (process.env.npm_package_name || '')).trim();
170-
const extraHeaders: Record<string,string> = {};
171-
if (projectName) extraHeaders['X-Browser-Echo-Project-Name'] = projectName;
172-
const ctrl = new AbortController();
173-
const timeout = setTimeout(() => ctrl.abort(), 500);
174-
fetch(targetIngest, {
175-
method: 'POST',
176-
headers: { 'content-type': 'application/json', ...extraHeaders, ...options.mcp.headers },
177-
body: JSON.stringify(payload),
178-
keepalive: true,
179-
cache: 'no-store',
180-
signal: ctrl.signal as any
181-
}).then((res) => {
182-
clearTimeout(timeout);
183-
const ok = !!res && res.ok;
184-
if (ok && !hasForwardedSuccessfully) {
185-
hasForwardedSuccessfully = true;
186-
announce(`${options.tag} forwarding logs to MCP ingest at ${targetIngest}`);
187-
}
188-
}).catch(() => { try { clearTimeout(timeout); } catch {} });
202+
if (targetIngest) {
203+
const projectName = (process.env.BROWSER_ECHO_PROJECT_NAME || (process.env.npm_package_name || '')).trim();
204+
const extraHeaders: Record<string,string> = {};
205+
if (projectName) extraHeaders['X-Browser-Echo-Project-Name'] = projectName;
206+
const ctrl = new AbortController();
207+
const timeout = setTimeout(() => ctrl.abort(), 500);
208+
fetch(targetIngest, {
209+
method: 'POST',
210+
headers: { 'content-type': 'application/json', ...extraHeaders, ...options.mcp.headers },
211+
body: JSON.stringify(payload),
212+
keepalive: true,
213+
cache: 'no-store',
214+
signal: ctrl.signal as any
215+
}).then((res) => {
216+
clearTimeout(timeout);
217+
const ok = !!res && res.ok;
218+
if (ok && !isRemoteAvailable) {
219+
isRemoteAvailable = true;
220+
announce(`${options.tag} forwarding logs to MCP ingest at ${targetIngest}`);
221+
} else if (!ok && isRemoteAvailable) {
222+
isRemoteAvailable = false;
223+
announce(`${options.tag} MCP ingest unavailable; printing logs locally`);
224+
}
225+
}).catch(() => {
226+
try { clearTimeout(timeout); } catch {}
227+
if (isRemoteAvailable) {
228+
isRemoteAvailable = false;
229+
announce(`${options.tag} MCP ingest unavailable; printing logs locally`);
230+
}
231+
});
232+
}
189233
} catch {}
190234

191235
const logger = server.config.logger;
192-
// Only suppress after first successful forward; never re-enable
193-
const shouldPrint = !hasForwardedSuccessfully;
236+
// Suppress when user requests it AND a current MCP endpoint is configured AND is considered available
237+
const shouldPrint = !options.mcp.suppressTerminal || !resolvedBase || !isRemoteAvailable;
194238
const sid = (payload.sessionId ?? 'anon').slice(0, 8);
195239
for (const entry of payload.entries) {
196240
const level = normalizeLevel(entry.level);

0 commit comments

Comments
 (0)