@@ -61,12 +61,15 @@ export async function startServer(
6161
6262 // Always bring up an ingest-only HTTP endpoint so clients/frameworks can POST logs
6363 const host = options . host || '127.0.0.1' ;
64- const port = ( options . port ?? 5179 ) | 0 ;
64+ // Use env override if provided, otherwise let OS pick an ephemeral port
65+ const envIngest = process . env . BROWSER_ECHO_INGEST_PORT ;
66+ const requested = Number ( envIngest || 0 ) | 0 ;
67+ const port = requested > 0 ? requested : 0 ;
6568 const logsRoute = options . logsRoute || '/__client-logs' ;
6669 await startIngestOnlyServer ( store , { host, port, logsRoute } ) ;
6770
6871 // eslint-disable-next-line no-console
69- console . log ( 'MCP (stdio) listening on stdio (ingest HTTP active)' ) ;
72+ console . error ( 'MCP (stdio) listening on stdio (ingest HTTP active)' ) ;
7073 return ;
7174 }
7275
@@ -152,19 +155,36 @@ export async function startHttpServer(
152155
153156 // Normalize body for POST
154157 let bodyBuf : Buffer | undefined ;
158+ let isInitialize = false ;
155159 if ( method === 'POST' || method === 'PUT' || method === 'PATCH' ) {
156160 const raw = await readRawBody ( event ) ;
157161 if ( raw && typeof raw === 'string' ) bodyBuf = Buffer . from ( raw ) ;
158162 else if ( raw && ( raw as any ) instanceof Uint8Array ) bodyBuf = Buffer . from ( raw as any ) ;
159163
160- // Allow tool calls without explicit session id by assigning a default
164+ // Validate MCP-Protocol-Version if provided (allow missing for backwards-compat)
161165 try {
162- const reqHeaders : Record < string , string | string [ ] | undefined > = event . node . req . headers as any ;
163- const existingSid = reqHeaders [ 'mcp-session-id' ] || reqHeaders [ 'Mcp-Session-Id' ] ;
164- if ( ! existingSid ) {
165- ( event . node . req . headers as any ) [ 'mcp-session-id' ] = 'default-session' ;
166+ const ver = String ( ( event . node . req . headers [ 'mcp-protocol-version' ] as any ) || '' ) . trim ( ) ;
167+ if ( ver && ! [ '2025-06-18' , '2025-03-26' , '2024-11-05' ] . includes ( ver ) ) {
168+ setResponseStatus ( event , 400 ) ;
169+ try { event . node . res . setHeader ( 'content-type' , 'application/json' ) ; } catch { }
170+ return JSON . stringify ( { jsonrpc : '2.0' , error : { code : - 32600 , message : 'Unsupported MCP-Protocol-Version' } , id : null } ) ;
166171 }
167172 } catch { }
173+
174+ // Enforce session id for non-initialize requests
175+ try {
176+ const parsed = bodyBuf ? JSON . parse ( bodyBuf . toString ( 'utf-8' ) ) : undefined ;
177+ const rpcMethod = parsed && typeof parsed === 'object' ? String ( parsed . method || '' ) : '' ;
178+ isInitialize = rpcMethod === 'initialize' ;
179+ } catch { }
180+ if ( ! isInitialize ) {
181+ const sidHeader = ( event . node . req . headers [ 'mcp-session-id' ] as string | undefined ) || '' ;
182+ if ( ! sidHeader ) {
183+ setResponseStatus ( event , 400 ) ;
184+ try { event . node . res . setHeader ( 'content-type' , 'application/json' ) ; } catch { }
185+ return JSON . stringify ( { jsonrpc : '2.0' , error : { code : - 32000 , message : 'Missing Mcp-Session-Id header' } , id : null } ) ;
186+ }
187+ }
168188 }
169189
170190 // Delegate to the SDK transport (writes to the Node response directly)
@@ -177,7 +197,7 @@ export async function startHttpServer(
177197 }
178198 } ) ) ;
179199
180- // Attach log ingest routes
200+ // Attach log ingest routes (with optional token validation via header)
181201 app . use ( createLogIngestRoutes ( store , opts . logsRoute ) ) ;
182202
183203 // Health
@@ -203,7 +223,7 @@ export async function startHttpServer(
203223 await new Promise < void > ( ( resolve ) => nodeServer . listen ( opts . port , opts . host , ( ) => resolve ( ) ) ) ;
204224
205225 // Advertise discovery so providers can auto-detect this server locally
206- await advertiseDiscovery ( opts . host , opts . port , opts . logsRoute ) ;
226+ await advertiseDiscovery ( opts . host , opts . port , opts . logsRoute , { projectRoot : process . cwd ( ) , scope : 'http' } ) ;
207227
208228 // eslint-disable-next-line no-console
209229 console . log ( `MCP (Streamable HTTP) listening → http://${ opts . host } :${ opts . port } ${ opts . endpoint } ` ) ;
@@ -247,14 +267,18 @@ export async function startIngestOnlyServer(
247267
248268 await new Promise < void > ( ( resolve ) => nodeServer . listen ( opts . port , opts . host , ( ) => resolve ( ) ) ) ;
249269
270+ // Determine the actual port assigned (when listening on port 0)
271+ const addr = nodeServer . address ( ) as any ;
272+ const actualPort = ( addr && typeof addr === 'object' && 'port' in addr ) ? ( addr . port as number ) : opts . port ;
273+
250274 // Advertise discovery for tooling that wants to auto-detect the ingest endpoint
251- await advertiseDiscovery ( opts . host , opts . port , opts . logsRoute ) ;
275+ await advertiseDiscovery ( opts . host , actualPort , opts . logsRoute , { projectRoot : process . cwd ( ) , scope : 'stdio' } ) ;
252276
253277 // eslint-disable-next-line no-console
254- console . log ( `Log ingest endpoint → http://${ opts . host } :${ opts . port } ${ opts . logsRoute } ` ) ;
278+ console . error ( `Log ingest endpoint → http://${ opts . host } :${ actualPort } ${ opts . logsRoute } ` ) ;
255279}
256280
257- async function advertiseDiscovery ( host : string , port : number , logsRoute : `/${string } `) {
281+ async function advertiseDiscovery ( host : string , port : number , logsRoute : `/${string } `, meta ?: { projectRoot ?: string ; token ?: string ; scope ?: 'http' | 'stdio' } ) {
258282 try {
259283 const { writeFileSync } = await import ( 'node:fs' ) ;
260284 const { join } = await import ( 'node:path' ) ;
@@ -265,13 +289,14 @@ async function advertiseDiscovery(host: string, port: number, logsRoute: `/${str
265289 url : baseUrl ,
266290 routeLogs : logsRoute ,
267291 timestamp : Date . now ( ) ,
268- pid : typeof process !== 'undefined' ? process . pid : undefined
292+ pid : typeof process !== 'undefined' ? process . pid : undefined ,
293+ projectRoot : meta ?. projectRoot || process . cwd ( ) ,
294+ token : meta ?. token || undefined
269295 } ) ;
270296
271- const files = [
272- join ( process . cwd ( ) , '.browser-echo-mcp.json' ) ,
273- join ( tmpdir ( ) , 'browser-echo-mcp.json' )
274- ] ;
297+ const files = meta ?. scope === 'http'
298+ ? [ join ( tmpdir ( ) , 'browser-echo-mcp.json' ) ]
299+ : [ join ( process . cwd ( ) , '.browser-echo-mcp.json' ) , join ( tmpdir ( ) , 'browser-echo-mcp.json' ) ] ;
275300
276301 for ( const f of files ) {
277302 try { writeFileSync ( f , payload ) ; } catch { }
@@ -299,6 +324,30 @@ function createLogIngestRoutes(store: LogStore, logsRoute: `/${string}`) {
299324 // Log ingest (POST)
300325 router . post ( logsRoute , defineEventHandler ( async ( event ) => {
301326 try {
327+ // Optional token check if discovery provided one (best-effort; disabled by default in dev)
328+ try {
329+ const { readFileSync, existsSync } = await import ( 'node:fs' ) ;
330+ const { join } = await import ( 'node:path' ) ;
331+ const { tmpdir } = await import ( 'node:os' ) ;
332+ const candidates = [ join ( process . cwd ( ) , '.browser-echo-mcp.json' ) , join ( tmpdir ( ) , 'browser-echo-mcp.json' ) ] ;
333+ let requiredToken = '' ;
334+ for ( const p of candidates ) {
335+ try {
336+ if ( ! existsSync ( p ) ) continue ;
337+ const raw = readFileSync ( p , 'utf-8' ) ;
338+ const data = JSON . parse ( raw ) ;
339+ if ( data ?. token ) { requiredToken = String ( data . token ) ; break ; }
340+ } catch { }
341+ }
342+ if ( requiredToken && process . env . BROWSER_ECHO_REQUIRE_TOKEN === '1' ) {
343+ const got = String ( ( event . node . req . headers [ 'x-be-token' ] as any ) || '' ) . trim ( ) ;
344+ if ( ! got || got !== requiredToken ) {
345+ setResponseStatus ( event , 401 ) ;
346+ return 'unauthorized' ;
347+ }
348+ }
349+ } catch { }
350+
302351 const raw = await readRawBody ( event ) ;
303352 const payload = typeof raw === 'string' ? JSON . parse ( raw ) : ( raw ? JSON . parse ( Buffer . from ( raw as any ) . toString ( 'utf-8' ) ) : undefined ) ;
304353 if ( ! payload || ! Array . isArray ( payload . entries ) ) {
0 commit comments