11import type { NextRequest } from 'next/server' ;
22import { NextResponse } from 'next/server' ;
33
4- const MCP_URL = ( process . env . BROWSER_ECHO_MCP_URL || '' ) . replace ( / \/ $ / , '' ) ;
4+ const MCP_URL = ( process . env . BROWSER_ECHO_MCP_URL || '' ) . replace ( / \/ $ / , '' ) . replace ( / \/ m c p $ / i , '' ) ;
55const MCP_LOGS_ROUTE = process . env . BROWSER_ECHO_MCP_LOGS_ROUTE || '/__client-logs' ;
6- const SUPPRESS_TERMINAL = MCP_URL && process . env . BROWSER_ECHO_SUPPRESS_TERMINAL !== '0' ;
76
87export type BrowserLogLevel = 'log' | 'info' | 'warn' | 'error' | 'debug' ;
98type Entry = { level : BrowserLogLevel | string ; text : string ; time ?: number ; stack ?: string ; source ?: string ; } ;
@@ -18,15 +17,31 @@ export async function POST(req: NextRequest) {
1817 catch { return new NextResponse ( 'invalid JSON' , { status : 400 } ) ; }
1918 if ( ! payload || ! Array . isArray ( payload . entries ) ) return new NextResponse ( 'invalid payload' , { status : 400 } ) ;
2019
21- // Resolve MCP URL: env var has priority, otherwise discover in development
22- const mcp = MCP_URL ? { url : MCP_URL , token : '' } : ( process . env . NODE_ENV === 'development' ? await __resolveMcpUrl ( ) : { url : '' , token : '' } ) ;
20+ // Resolve MCP URL: env (health-checked) → port 5179 (dev) → local discovery file (dev)
21+ let mcp = { url : '' , token : '' , routeLogs : '' as `/${string } ` | '' } as { url : string ; token ?: string ; routeLogs ?: `/${string } ` } ;
22+ if ( MCP_URL ) {
23+ if ( await __pingHealth ( `${ MCP_URL } /health` , 300 ) ) {
24+ mcp = { url : MCP_URL } ;
25+ }
26+ }
27+ if ( ! mcp . url && process . env . NODE_ENV === 'development' ) {
28+ for ( const base of [ 'http://127.0.0.1:5179' , 'http://localhost:5179' ] ) {
29+ if ( await __pingHealth ( `${ base } /health` , 300 ) ) { mcp = { url : base } ; break ; }
30+ }
31+ }
32+ if ( ! mcp . url && process . env . NODE_ENV === 'development' ) {
33+ mcp = await __resolveMcpUrl ( ) ;
34+ }
2335
2436 // Forward to MCP server if available (fire-and-forget)
2537 if ( mcp . url ) {
2638 try {
27- fetch ( `${ mcp . url } ${ MCP_LOGS_ROUTE } ` , {
39+ const route = ( MCP_LOGS_ROUTE as `/${string } `) || ( mcp . routeLogs as `/${string } `) || '/__client-logs' ;
40+ const headers : Record < string , string > = { 'content-type' : 'application/json' } ;
41+ if ( mcp . token ) headers [ 'x-be-token' ] = mcp . token ;
42+ fetch ( `${ mcp . url } ${ route } ` , {
2843 method : 'POST' ,
29- headers : { 'content-type' : 'application/json' } ,
44+ headers,
3045 body : JSON . stringify ( payload ) ,
3146 keepalive : true ,
3247 cache : 'no-store' ,
@@ -35,7 +50,10 @@ export async function POST(req: NextRequest) {
3550 }
3651
3752 // Dynamically decide whether to print to terminal
38- const shouldPrint = ! ( mcp . url && process . env . BROWSER_ECHO_SUPPRESS_TERMINAL !== '0' ) ;
53+ const envVal = process . env . BROWSER_ECHO_SUPPRESS_TERMINAL ;
54+ const forceSuppress = envVal === '1' ;
55+ const forcePrint = envVal === '0' ;
56+ const shouldPrint = forcePrint ? true : ( forceSuppress ? false : ! mcp . url ) ;
3957
4058 const sid = ( payload . sessionId ?? 'anon' ) . slice ( 0 , 8 ) ;
4159 for ( const entry of payload . entries ) {
@@ -75,72 +93,50 @@ function color(level: BrowserLogLevel, msg: string) {
7593}
7694function dim ( s : string ) { return c . dim + s + c . reset ; }
7795
78- let __mcpDiscoveryCache : { url : string ; token ?: string ; ts : number } | null = null ;
96+ let __mcpDiscoveryCache : { url : string ; token ?: string ; routeLogs ?: `/${ string } ` ; ts : number } | null = null ;
7997
80- async function __resolveMcpUrl ( ) : Promise < { url : string ; token ?: string } > {
81- // 1) Env var already handled by caller; only discover in dev here.
98+ async function __resolveMcpUrl ( ) : Promise < { url : string ; token ?: string ; routeLogs ?: `/${string } ` } > {
8299 const now = Date . now ( ) ;
83- const CACHE_TTL_MS = 30_000 ;
100+ const CACHE_TTL_MS = 10_000 ;
84101
85- // Use fresh cache if present
86102 if ( __mcpDiscoveryCache && ( now - __mcpDiscoveryCache . ts ) < CACHE_TTL_MS ) {
87- return { url : __mcpDiscoveryCache . url , token : __mcpDiscoveryCache . token } ;
103+ return { url : __mcpDiscoveryCache . url , token : __mcpDiscoveryCache . token , routeLogs : __mcpDiscoveryCache . routeLogs } ;
88104 }
89105
90- // 2) Discovery file (project root or OS tmp)
91106 const fromFile = await __readDiscoveryFromFile ( ) ;
92107 if ( fromFile . url ) {
93- // health check to ensure it's alive
94108 if ( await __pingHealth ( `${ fromFile . url } /health` , 300 ) ) {
95- __mcpDiscoveryCache = { url : fromFile . url , token : fromFile . token , ts : now } ;
109+ __mcpDiscoveryCache = { url : fromFile . url , token : fromFile . token , routeLogs : fromFile . routeLogs , ts : now } ;
96110 return fromFile ;
97111 }
98- // purge stale tmp discovery
99- try {
100- const { unlinkSync, existsSync } = await import ( 'node:fs' ) ;
101- const { join } = await import ( 'node:path' ) ;
102- const { tmpdir } = await import ( 'node:os' ) ;
103- const stale = join ( tmpdir ( ) , 'browser-echo-mcp.json' ) ;
104- if ( existsSync ( stale ) ) unlinkSync ( stale ) ;
105- } catch { }
106- }
107-
108- // 3) Port scan common local ports
109- const ports = [ 5179 , 5178 , 3001 , 4000 , 5173 ] ;
110- for ( const port of ports ) {
111- const bases = [ `http://127.0.0.1:${ port } ` , `http://localhost:${ port } ` ] ;
112- for ( const base of bases ) {
113- if ( await __pingHealth ( `${ base } /health` , 400 ) ) {
114- __mcpDiscoveryCache = { url : base , ts : now } ;
115- return { url : base } ;
116- }
117- }
118112 }
119113
120114 __mcpDiscoveryCache = { url : '' , ts : now } ;
121115 return { url : '' } ;
122116}
123117
124- async function __readDiscoveryFromFile ( ) : Promise < { url : string ; token ?: string } > {
118+ async function __readDiscoveryFromFile ( ) : Promise < { url : string ; token ?: string ; routeLogs ?: `/${ string } ` } > {
125119 try {
126120 const { readFileSync, existsSync } = await import ( 'node:fs' ) ;
127- const { join } = await import ( 'node:path' ) ;
128- const { tmpdir } = await import ( 'node:os' ) ;
129- const candidates = [
130- join ( process . cwd ( ) , '.browser-echo-mcp.json' ) ,
131- join ( tmpdir ( ) , 'browser-echo-mcp.json' )
132- ] ;
133- for ( const p of candidates ) {
134- try {
135- if ( ! existsSync ( p ) ) continue ;
136- const raw = readFileSync ( p , 'utf-8' ) ;
137- const data = JSON . parse ( raw ) ;
138- const url = ( data ?. url ? String ( data . url ) : '' ) . replace ( / \/ $ / , '' ) ;
139- const ts = typeof data ?. timestamp === 'number' ? data . timestamp : 0 ;
140- const token = data ?. token ? String ( data . token ) : undefined ;
141- // Treat as fresh if updated within the last 60s
142- if ( url && ( Date . now ( ) - ts ) < 60_000 ) return { url, token } ;
143- } catch { }
121+ const { join, dirname } = await import ( 'node:path' ) ;
122+ let dir = process . cwd ( ) ;
123+ const root = dirname ( '/' ) ;
124+ while ( true ) {
125+ const p = join ( dir , '.browser-echo-mcp.json' ) ;
126+ if ( existsSync ( p ) ) {
127+ try {
128+ const raw = readFileSync ( p , 'utf-8' ) ;
129+ const data = JSON . parse ( raw ) ;
130+ const url = ( data ?. url ? String ( data . url ) : '' ) . replace ( / \/ $ / , '' ) ;
131+ const ts = typeof data ?. timestamp === 'number' ? data . timestamp : 0 ;
132+ const token = data ?. token ? String ( data . token ) : undefined ;
133+ const routeLogs = data ?. routeLogs ? String ( data . routeLogs ) as `/${string } ` : undefined ;
134+ if ( url && ( Date . now ( ) - ts ) < 60_000 ) return { url, token, routeLogs } ;
135+ } catch { }
136+ }
137+ const parent = dirname ( dir ) ;
138+ if ( parent === dir || parent === root ) break ;
139+ dir = parent ;
144140 }
145141 } catch { }
146142 return { url : '' } ;
0 commit comments