@@ -18,11 +18,13 @@ export async function POST(req: NextRequest) {
1818 catch { return new NextResponse ( 'invalid JSON' , { status : 400 } ) ; }
1919 if ( ! payload || ! Array . isArray ( payload . entries ) ) return new NextResponse ( 'invalid payload' , { status : 400 } ) ;
2020
21- // Forward to MCP server if configured (fire-and-forget)
22- if ( MCP_URL ) {
21+ // Resolve MCP URL: env var has priority, otherwise discover in development
22+ const mcpUrl = MCP_URL || ( process . env . NODE_ENV === 'development' ? await __resolveMcpUrl ( ) : '' ) ;
23+
24+ // Forward to MCP server if available (fire-and-forget)
25+ if ( mcpUrl ) {
2326 try {
24- // keepalive helps with page unloads
25- fetch ( `${ MCP_URL } ${ MCP_LOGS_ROUTE } ` , {
27+ fetch ( `${ mcpUrl } ${ MCP_LOGS_ROUTE } ` , {
2628 method : 'POST' ,
2729 headers : { 'content-type' : 'application/json' } ,
2830 body : JSON . stringify ( payload ) ,
@@ -32,7 +34,8 @@ export async function POST(req: NextRequest) {
3234 } catch { }
3335 }
3436
35- const shouldPrint = ! SUPPRESS_TERMINAL ;
37+ // Dynamically decide whether to print to terminal
38+ const shouldPrint = ! ( mcpUrl && process . env . BROWSER_ECHO_SUPPRESS_TERMINAL !== '0' ) ;
3639
3740 const sid = ( payload . sessionId ?? 'anon' ) . slice ( 0 , 8 ) ;
3841 for ( const entry of payload . entries ) {
@@ -71,3 +74,74 @@ function color(level: BrowserLogLevel, msg: string) {
7174 }
7275}
7376function dim ( s : string ) { return c . dim + s + c . reset ; }
77+
78+ let __mcpDiscoveryCache : { url : string ; ts : number } | null = null ;
79+
80+ async function __resolveMcpUrl ( ) : Promise < string > {
81+ // 1) Env var already handled by caller; only discover in dev here.
82+ const now = Date . now ( ) ;
83+ const CACHE_TTL_MS = 30_000 ;
84+
85+ // Use fresh cache if present
86+ if ( __mcpDiscoveryCache && ( now - __mcpDiscoveryCache . ts ) < CACHE_TTL_MS ) {
87+ return __mcpDiscoveryCache . url ;
88+ }
89+
90+ // 2) Discovery file (project root or OS tmp)
91+ const fromFile = await __readDiscoveryUrlFromFile ( ) ;
92+ if ( fromFile ) {
93+ __mcpDiscoveryCache = { url : fromFile , ts : now } ;
94+ return fromFile ;
95+ }
96+
97+ // 3) Port scan common local ports
98+ const ports = [ 5179 , 5178 , 3001 , 4000 , 5173 ] ;
99+ for ( const port of ports ) {
100+ const bases = [ `http://127.0.0.1:${ port } ` , `http://localhost:${ port } ` ] ;
101+ for ( const base of bases ) {
102+ if ( await __pingHealth ( `${ base } /health` , 400 ) ) {
103+ __mcpDiscoveryCache = { url : base , ts : now } ;
104+ return base ;
105+ }
106+ }
107+ }
108+
109+ __mcpDiscoveryCache = { url : '' , ts : now } ;
110+ return '' ;
111+ }
112+
113+ async function __readDiscoveryUrlFromFile ( ) : Promise < string > {
114+ try {
115+ const { readFileSync, existsSync } = await import ( 'node:fs' ) ;
116+ const { join } = await import ( 'node:path' ) ;
117+ const { tmpdir } = await import ( 'node:os' ) ;
118+ const candidates = [
119+ join ( process . cwd ( ) , '.browser-echo-mcp.json' ) ,
120+ join ( tmpdir ( ) , 'browser-echo-mcp.json' )
121+ ] ;
122+ for ( const p of candidates ) {
123+ try {
124+ if ( ! existsSync ( p ) ) continue ;
125+ const raw = readFileSync ( p , 'utf-8' ) ;
126+ const data = JSON . parse ( raw ) ;
127+ const url = ( data ?. url ? String ( data . url ) : '' ) . replace ( / \/ $ / , '' ) ;
128+ const ts = typeof data ?. timestamp === 'number' ? data . timestamp : 0 ;
129+ // Treat as fresh if updated within the last 60s
130+ if ( url && ( Date . now ( ) - ts ) < 60_000 ) return url ;
131+ } catch { }
132+ }
133+ } catch { }
134+ return '' ;
135+ }
136+
137+ async function __pingHealth ( url : string , timeoutMs : number ) : Promise < boolean > {
138+ try {
139+ const ctrl = new AbortController ( ) ;
140+ const t = setTimeout ( ( ) => ctrl . abort ( ) , timeoutMs ) ;
141+ const res = await fetch ( url , { signal : ctrl . signal , cache : 'no-store' as any } ) ;
142+ clearTimeout ( t ) ;
143+ return res . ok ;
144+ } catch {
145+ return false ;
146+ }
147+ }
0 commit comments