Skip to content

Commit 7921020

Browse files
committed
chore: simplify MCP configuration by removing unnecessary options, enhance project-local JSON handling for MCP resolution, and improve server log handling
1 parent d0dae79 commit 7921020

6 files changed

Lines changed: 102 additions & 307 deletions

File tree

example/nuxt-app/nuxt.config.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,6 @@ export default defineNuxtConfig({
55
modules: ['@browser-echo/nuxt'],
66
// optional browser-echo configuration:
77
browserEcho: {
8-
stackMode: 'condensed',
9-
mcpServers: {
10-
browserEcho: {
11-
type: 'http',
12-
url: 'http://localhost:5181/mcp'
13-
}
14-
}
8+
stackMode: 'condensed'
159
}
1610
})

packages/mcp/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ const cli = defineCommand({
4646
|| args.port !== '5179' || args.host !== '127.0.0.1'
4747
|| args.endpoint !== '/mcp' || args.logsRoute !== '/__client-logs';
4848

49-
// Start server with selected transport
49+
// Start server with selected transport. Prefer 5179 for ingest if stdio.
5050
await startServer(mcp, useHttp ? {
5151
type: 'http',
5252
host: String(args.host),

packages/mcp/src/server.ts

Lines changed: 15 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { createServer as createNodeServer } from 'node:http';
2+
import { writeFileSync, rmSync, renameSync } from 'node:fs';
3+
import { join as joinPath } from 'node:path';
24
import { randomUUID } from 'node:crypto';
35

46
import { createApp, createRouter, defineEventHandler, getQuery, readRawBody, setResponseStatus, toNodeListener } from 'h3';
@@ -86,6 +88,8 @@ export async function stopServer(server: McpServer) {
8688
console.error('Error occurred during server stop:', error);
8789
}
8890
finally {
91+
// Best-effort cleanup of project JSON
92+
try { rmSync(joinPath(process.cwd(), '.browser-echo.json'), { force: true }); } catch {}
8993
process.exit(0);
9094
}
9195
}
@@ -221,9 +225,8 @@ export async function startHttpServer(
221225
} catch {}
222226

223227
await new Promise<void>((resolve) => nodeServer.listen(opts.port, opts.host, () => resolve()));
224-
225-
// Advertise discovery so providers can auto-detect this server locally
226-
await advertiseDiscovery(opts.host, opts.port, opts.logsRoute, { projectRoot: process.env.BROWSER_ECHO_PROJECT_ROOT || process.cwd(), scope: 'http' });
228+
// Write project-local config for providers
229+
writeProjectJson(opts.host, opts.port, opts.logsRoute);
227230

228231
// eslint-disable-next-line no-console
229232
console.log(`MCP (Streamable HTTP) listening → http://${opts.host}:${opts.port}${opts.endpoint}`);
@@ -315,38 +318,22 @@ export async function startIngestOnlyServer(
315318
}
316319
}
317320

318-
// Advertise discovery for stdio transport. By default, only write project-local discovery.
319-
const requestedPort = opts.port;
320-
const isAggregator = requestedPort !== 0 && actualPort === requestedPort;
321-
await advertiseDiscovery(opts.host, actualPort, opts.logsRoute, { projectRoot: process.env.BROWSER_ECHO_PROJECT_ROOT || process.cwd(), scope: 'stdio', aggregator: isAggregator });
321+
// Write project-local config for providers
322+
writeProjectJson(opts.host, actualPort, opts.logsRoute);
322323

323324
// eslint-disable-next-line no-console
324325
console.error(`Log ingest endpoint → http://${opts.host}:${actualPort}${opts.logsRoute}`);
325326
}
326327

327-
async function advertiseDiscovery(host: string, port: number, logsRoute: `/${string}`, _meta?: { projectRoot?: string; token?: string; scope?: 'http' | 'stdio'; aggregator?: boolean }) {
328+
function writeProjectJson(host: string, port: number, route: `/${string}`) {
328329
try {
329-
const { writeFileSync } = await import('node:fs');
330-
const { join } = await import('node:path');
331-
332330
const baseUrl = `http://${host}:${port}`;
333-
const payload = JSON.stringify({
334-
url: baseUrl,
335-
routeLogs: logsRoute,
336-
timestamp: Date.now(),
337-
pid: typeof process !== 'undefined' ? process.pid : undefined,
338-
// Advertise additional metadata to allow project-scoped discovery
339-
projectRoot: _meta?.projectRoot ? String(_meta.projectRoot) : undefined,
340-
scope: _meta?.scope || undefined,
341-
aggregator: _meta?.aggregator || undefined,
342-
token: _meta?.token ? String(_meta.token) : undefined
343-
});
344-
345-
const file = join(process.cwd(), '.browser-echo-mcp.json');
346-
try { writeFileSync(file, payload); } catch {}
347-
} catch {
348-
// best-effort only
349-
}
331+
const payload = JSON.stringify({ url: baseUrl, route, timestamp: Date.now(), pid: typeof process !== 'undefined' ? process.pid : undefined });
332+
const file = joinPath(process.cwd(), '.browser-echo.json');
333+
const tmp = file + '.tmp';
334+
try { writeFileSync(tmp, payload); renameSync(tmp, file); }
335+
catch { try { writeFileSync(file, payload); } catch {} }
336+
} catch {}
350337
}
351338

352339
/** Create log ingest routes that can be attached to any H3 app */

packages/next/src/route.ts

Lines changed: 27 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import type { NextRequest } from 'next/server';
22
import { NextResponse } from 'next/server';
33

4-
const MCP_URL = (process.env.BROWSER_ECHO_MCP_URL || '').replace(/\/$/, '').replace(/\/mcp$/i, '');
5-
const MCP_LOGS_ROUTE = process.env.BROWSER_ECHO_MCP_LOGS_ROUTE || '/__client-logs';
4+
// Simplified: resolve MCP from project-local JSON once; fallback to 5179
65

76
export type BrowserLogLevel = 'log' | 'info' | 'warn' | 'error' | 'debug';
87
type Entry = { level: BrowserLogLevel | string; text: string; time?: number; stack?: string; source?: string; };
@@ -17,28 +16,14 @@ export async function POST(req: NextRequest) {
1716
catch { return new NextResponse('invalid JSON', { status: 400 }); }
1817
if (!payload || !Array.isArray(payload.entries)) return new NextResponse('invalid payload', { status: 400 });
1918

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-
}
19+
// Resolve MCP once: project JSON → 5179 fallback
20+
const mcp = await __resolveMcpFromProject();
3521

3622
// Forward to MCP server if available (fire-and-forget)
3723
if (mcp.url) {
3824
try {
39-
const route = (MCP_LOGS_ROUTE as `/${string}`) || (mcp.routeLogs as `/${string}`) || '/__client-logs';
25+
const route = (mcp.routeLogs as `/${string}`) || '/__client-logs';
4026
const headers: Record<string,string> = { 'content-type': 'application/json' };
41-
if (mcp.token) headers['x-be-token'] = mcp.token;
4227
fetch(`${mcp.url}${route}`, {
4328
method: 'POST',
4429
headers,
@@ -50,10 +35,7 @@ export async function POST(req: NextRequest) {
5035
}
5136

5237
// Dynamically decide whether to print to terminal
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);
38+
const shouldPrint = !mcp.url;
5739

5840
const sid = (payload.sessionId ?? 'anon').slice(0, 8);
5941
for (const entry of payload.entries) {
@@ -93,53 +75,34 @@ function color(level: BrowserLogLevel, msg: string) {
9375
}
9476
function dim(s: string) { return c.dim + s + c.reset; }
9577

96-
let __mcpDiscoveryCache: { url: string; token?: string; routeLogs?: `/${string}`; ts: number } | null = null;
97-
98-
async function __resolveMcpUrl(): Promise<{ url: string; token?: string; routeLogs?: `/${string}` }> {
99-
const now = Date.now();
100-
const CACHE_TTL_MS = 10_000;
101-
102-
if (__mcpDiscoveryCache && (now - __mcpDiscoveryCache.ts) < CACHE_TTL_MS) {
103-
return { url: __mcpDiscoveryCache.url, token: __mcpDiscoveryCache.token, routeLogs: __mcpDiscoveryCache.routeLogs };
104-
}
78+
let __mcpProjectCache: { url: string; routeLogs?: `/${string}` } | null = null;
10579

106-
const fromFile = await __readDiscoveryFromFile();
107-
if (fromFile.url) {
108-
if (await __pingHealth(`${fromFile.url}/health`, 300)) {
109-
__mcpDiscoveryCache = { url: fromFile.url, token: fromFile.token, routeLogs: fromFile.routeLogs, ts: now };
110-
return fromFile;
111-
}
112-
}
113-
114-
__mcpDiscoveryCache = { url: '', ts: now };
115-
return { url: '' };
116-
}
117-
118-
async function __readDiscoveryFromFile(): Promise<{ url: string; token?: string; routeLogs?: `/${string}` }> {
80+
async function __resolveMcpFromProject(): Promise<{ url: string; routeLogs?: `/${string}` }> {
81+
if (__mcpProjectCache) return __mcpProjectCache;
11982
try {
12083
const { readFileSync, existsSync } = await import('node:fs');
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 {}
84+
const { join } = await import('node:path');
85+
const p = join(process.cwd(), '.browser-echo.json');
86+
if (existsSync(p)) {
87+
const raw = readFileSync(p, 'utf-8');
88+
const data = JSON.parse(raw);
89+
const url = (data?.url ? String(data.url) : '').replace(/\/$/, '');
90+
const routeLogs = (data?.route ? String(data.route) : '/__client-logs') as `/${string}`;
91+
if (url && await __pingHealth(`${url}/health`, 250)) {
92+
__mcpProjectCache = { url, routeLogs };
93+
return __mcpProjectCache;
13694
}
137-
const parent = dirname(dir);
138-
if (parent === dir || parent === root) break;
139-
dir = parent;
14095
}
14196
} catch {}
142-
return { url: '' };
97+
// Fallback to default 5179
98+
for (const base of ['http://127.0.0.1:5179', 'http://localhost:5179']) {
99+
if (await __pingHealth(`${base}/health`, 250)) {
100+
__mcpProjectCache = { url: base, routeLogs: '/__client-logs' };
101+
return __mcpProjectCache;
102+
}
103+
}
104+
__mcpProjectCache = { url: '' } as any;
105+
return __mcpProjectCache;
143106
}
144107

145108
async function __pingHealth(url: string, timeoutMs: number): Promise<boolean> {
Lines changed: 23 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { defineEventHandler, readBody, setResponseStatus } from 'h3';
22

3-
const MCP_URL = (process.env.BROWSER_ECHO_MCP_URL || '').replace(/\/$/, '').replace(/\/mcp$/i, '');
4-
const MCP_LOGS_ROUTE = process.env.BROWSER_ECHO_MCP_LOGS_ROUTE || '/__client-logs';
3+
// Simplified: resolve MCP from project-local JSON once; fallback to 5179
54

65
type Level = 'log' | 'info' | 'warn' | 'error' | 'debug';
76
type Entry = { level: Level | string; text: string; time?: number; stack?: string; source?: string; };
@@ -16,31 +15,14 @@ export default defineEventHandler(async (event) => {
1615
setResponseStatus(event, 400); return 'invalid payload';
1716
}
1817

19-
// Resolve MCP URL: env var (if healthy) → port 5179 → local discovery file (dev only)
20-
let mcp = { url: '', token: '', routeLogs: '' as `/${string}` | '' } as { url: string; token?: string; routeLogs?: `/${string}` };
21-
if (MCP_URL) {
22-
if (await __pingHealthNuxt(`${MCP_URL}/health`, 300)) {
23-
mcp = { url: MCP_URL };
24-
}
25-
}
26-
// Prefer project-local discovery over port scan
27-
if (!mcp.url && process.env.NODE_ENV === 'development') {
28-
mcp = await __resolveMcpUrlNuxt();
29-
}
30-
if (!mcp.url && process.env.NODE_ENV === 'development' && process.env.BROWSER_ECHO_ALLOW_PORT_SCAN === '1') {
31-
// Try default port 5179 only as a last resort (gated)
32-
const candidates = ['http://127.0.0.1:5179', 'http://localhost:5179'];
33-
for (const base of candidates) {
34-
if (await __pingHealthNuxt(`${base}/health`, 300)) { mcp = { url: base }; break; }
35-
}
36-
}
18+
// Resolve MCP once: project JSON → 5179 fallback
19+
const mcp = await __resolveMcpFromProjectNuxt();
3720

3821
// Forward to MCP server if available (fire-and-forget)
3922
if (mcp.url) {
4023
try {
41-
const route = (MCP_LOGS_ROUTE as `/${string}`) || (mcp.routeLogs as `/${string}`) || '/__client-logs';
24+
const route = (mcp.routeLogs as `/${string}`) || '/__client-logs';
4225
const headers: Record<string,string> = { 'content-type': 'application/json' };
43-
if (mcp.token) headers['x-be-token'] = mcp.token;
4426
fetch(`${mcp.url}${route}`, {
4527
method: 'POST',
4628
headers,
@@ -51,11 +33,8 @@ export default defineEventHandler(async (event) => {
5133
} catch {}
5234
}
5335

54-
// Dynamically decide whether to print to terminal
55-
const envVal = process.env.BROWSER_ECHO_SUPPRESS_TERMINAL;
56-
const forceSuppress = envVal === '1';
57-
const forcePrint = envVal === '0';
58-
const shouldPrint = forcePrint ? true : (forceSuppress ? false : !mcp.url);
36+
// Suppress when forwarding active
37+
const shouldPrint = !mcp.url;
5938

6039
const sid = (payload.sessionId ?? 'anon').slice(0, 8);
6140
for (const entry of payload.entries) {
@@ -97,57 +76,29 @@ function color(level: Level, msg: string) {
9776
}
9877
function dim(s: string) { return c.dim + s + c.reset; }
9978

100-
let __mcpDiscoveryCacheNuxt: { url: string; token?: string; routeLogs?: `/${string}`; ts: number } | null = null;
101-
102-
async function __resolveMcpUrlNuxt(): Promise<{ url: string; token?: string; routeLogs?: `/${string}` }> {
103-
const now = Date.now();
104-
const CACHE_TTL_MS = 10_000;
105-
106-
if (__mcpDiscoveryCacheNuxt && (now - __mcpDiscoveryCacheNuxt.ts) < CACHE_TTL_MS) {
107-
return { url: __mcpDiscoveryCacheNuxt.url, token: __mcpDiscoveryCacheNuxt.token, routeLogs: __mcpDiscoveryCacheNuxt.routeLogs };
108-
}
109-
110-
const fromFile = await __readDiscoveryFromFileNuxt();
111-
if (fromFile.url) {
112-
// Require health and, if provided, project scoping match
113-
const healthy = await __pingHealthNuxt(`${fromFile.url}/health`, 300);
114-
const scopedOk = await __isInsideProjectNuxt(fromFile.projectRoot);
115-
if (healthy && scopedOk) {
116-
__mcpDiscoveryCacheNuxt = { url: fromFile.url, token: fromFile.token, routeLogs: fromFile.routeLogs, ts: now };
117-
return fromFile;
118-
}
119-
}
120-
121-
__mcpDiscoveryCacheNuxt = { url: '', ts: now } as any;
122-
return { url: '' };
123-
}
79+
let __mcpProjectCacheNuxt: { url: string; routeLogs?: `/${string}` } | null = null;
12480

125-
async function __readDiscoveryFromFileNuxt(): Promise<{ url: string; token?: string; routeLogs?: `/${string}`; projectRoot?: string }> {
81+
async function __resolveMcpFromProjectNuxt(): Promise<{ url: string; routeLogs?: `/${string}` }> {
82+
if (__mcpProjectCacheNuxt) return __mcpProjectCacheNuxt;
12683
try {
12784
const { readFileSync, existsSync } = await import('node:fs');
128-
const { join, dirname } = await import('node:path');
129-
let dir = process.cwd();
130-
const root = dirname('/');
131-
while (true) {
132-
const p = join(dir, '.browser-echo-mcp.json');
133-
if (existsSync(p)) {
134-
try {
135-
const raw = readFileSync(p, 'utf-8');
136-
const data = JSON.parse(raw);
137-
const url = (data?.url ? String(data.url) : '').replace(/\/$/, '');
138-
const ts = typeof data?.timestamp === 'number' ? data.timestamp : 0;
139-
const token = data?.token ? String(data.token) : undefined;
140-
const routeLogs = data?.routeLogs ? String(data.routeLogs) as `/${string}` : undefined;
141-
const projectRoot = data?.projectRoot ? String(data.projectRoot) : undefined;
142-
if (url && (Date.now() - ts) < 60_000) return { url, token, routeLogs, projectRoot };
143-
} catch {}
85+
const { join } = await import('node:path');
86+
const p = join(process.cwd(), '.browser-echo.json');
87+
if (existsSync(p)) {
88+
const raw = readFileSync(p, 'utf-8');
89+
const data = JSON.parse(raw);
90+
const url = (data?.url ? String(data.url) : '').replace(/\/$/, '');
91+
const routeLogs = (data?.route ? String(data.route) as `/${string}` : '/__client-logs');
92+
if (url && await __pingHealthNuxt(`${url}/health`, 300)) {
93+
__mcpProjectCacheNuxt = { url, routeLogs };
94+
return __mcpProjectCacheNuxt;
14495
}
145-
const parent = dirname(dir);
146-
if (parent === dir || parent === root) break;
147-
dir = parent;
14896
}
14997
} catch {}
150-
return { url: '' };
98+
for (const base of ['http://127.0.0.1:5179', 'http://localhost:5179']) {
99+
if (await __pingHealthNuxt(`${base}/health`, 300)) { __mcpProjectCacheNuxt = { url: base, routeLogs: '/__client-logs' }; return __mcpProjectCacheNuxt; }
100+
}
101+
__mcpProjectCacheNuxt = { url: '' } as any; return __mcpProjectCacheNuxt;
151102
}
152103

153104
async function __pingHealthNuxt(url: string, timeoutMs: number): Promise<boolean> {
@@ -161,17 +112,3 @@ async function __pingHealthNuxt(url: string, timeoutMs: number): Promise<boolean
161112
return false;
162113
}
163114
}
164-
165-
async function __isInsideProjectNuxt(root?: string): Promise<boolean> {
166-
if (!root) return true;
167-
try {
168-
const { realpathSync } = await import('node:fs');
169-
const { relative, isAbsolute, sep, dirname } = await import('node:path');
170-
const realRoot = realpathSync(root);
171-
const realCwd = realpathSync(process.cwd());
172-
const rel = relative(realRoot, realCwd);
173-
return rel === '' || (!rel.startsWith('..' + sep) && !isAbsolute(rel));
174-
} catch {
175-
return false;
176-
}
177-
}

0 commit comments

Comments
 (0)