11// Avoid exporting Vite types to prevent cross-version type mismatches in consumers
22import ansis from 'ansis' ;
33import type { BrowserLogLevel , NetworkCaptureOptions } from '@browser-echo/core' ;
4- import { mkdirSync , appendFileSync } from 'node:fs' ;
4+ import { mkdirSync , appendFileSync , existsSync , readFileSync } from 'node:fs' ;
55import { join as joinPath , dirname } from 'node:path' ;
66
77export 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 ( / \/ m c p $ / 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 ( / \/ m c p $ / 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 ( / \/ m c p $ / 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