Skip to content

Commit e11fd72

Browse files
committed
feat: integrate MCP discovery mechanism and enhance logging capabilities across applications
1 parent fa76a2b commit e11fd72

13 files changed

Lines changed: 210 additions & 25 deletions

File tree

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1-
export { POST, runtime, dynamic } from '@browser-echo/next/route';
1+
import { POST as originalPOST, runtime, dynamic } from '@browser-echo/next/route';
2+
3+
export { originalPOST as POST, runtime, dynamic };

example/next-app/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"react-dom": "19.1.1"
1515
},
1616
"devDependencies": {
17+
"@browser-echo/mcp": "workspace:*",
1718
"@browser-echo/next": "workspace:*",
1819
"@eslint/eslintrc": "^3",
1920
"@tailwindcss/postcss": "^4.1.12",

example/react-vite-app/vite.config.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import browserEcho from '@browser-echo/vite'
66
export default defineConfig({
77
plugins: [react(), browserEcho({
88
stackMode: 'condensed',
9-
// mcp: {
10-
// url: 'http://localhost:5179'
11-
// }
9+
mcp: {
10+
url: 'http://localhost:5179'
11+
}
1212
})],
1313
})

example/tanstack-app/postcss.config.mjs

Lines changed: 0 additions & 6 deletions
This file was deleted.

example/tanstack-app/vite.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,6 @@ export default defineConfig({
1414
}),
1515
tanstackStart({ customViteReactPlugin: true }),
1616
viteReact(),
17-
browserEcho({ injectHtml: false, stackMode: 'condensed' }),
17+
browserEcho({ injectHtml: false, stackMode: 'condensed', mcp: { url: 'http://localhost:5179' } }),
1818
],
1919
})

example/vue-vite-app/vite.config.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,12 @@ import browserEcho from '@browser-echo/vite'
44

55
// https://vite.dev/config/
66
export default defineConfig({
7-
plugins: [vue(), browserEcho()],
7+
plugins: [vue(), browserEcho(
8+
{
9+
stackMode: 'condensed',
10+
mcp: {
11+
url: 'http://localhost:5179'
12+
}
13+
},
14+
)],
815
})
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"url":"http://127.0.0.1:5179","routeLogs":"/__client-logs","timestamp":1755720549464,"pid":47674}

packages/mcp/bin/cli.mjs

100644100755
File mode changed.

packages/mcp/src/server.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,12 +171,43 @@ async function startHttpServer(mcp: McpServer, store: LogStore, opts: {
171171
} catch {}
172172

173173
await new Promise<void>((resolve) => nodeServer.listen(opts.port, opts.host, () => resolve()));
174+
175+
// Advertise discovery so providers can auto-detect this server locally
176+
await advertiseDiscovery(opts.host, opts.port, opts.logsRoute);
177+
174178
// eslint-disable-next-line no-console
175179
console.log(`MCP (Streamable HTTP) listening → http://${opts.host}:${opts.port}${opts.endpoint}`);
176180
// eslint-disable-next-line no-console
177181
console.log(`Log ingest endpoint → http://${opts.host}:${opts.port}${opts.logsRoute}`);
178182
}
179183

184+
async function advertiseDiscovery(host: string, port: number, logsRoute: `/${string}`) {
185+
try {
186+
const { writeFileSync } = await import('node:fs');
187+
const { join } = await import('node:path');
188+
const { tmpdir } = await import('node:os');
189+
190+
const baseUrl = `http://${host}:${port}`;
191+
const payload = JSON.stringify({
192+
url: baseUrl,
193+
routeLogs: logsRoute,
194+
timestamp: Date.now(),
195+
pid: typeof process !== 'undefined' ? process.pid : undefined
196+
});
197+
198+
const files = [
199+
join(process.cwd(), '.browser-echo-mcp.json'),
200+
join(tmpdir(), 'browser-echo-mcp.json')
201+
];
202+
203+
for (const f of files) {
204+
try { writeFileSync(f, payload); } catch {}
205+
}
206+
} catch {
207+
// best-effort only
208+
}
209+
}
210+
180211
/** Create log ingest routes that can be attached to any H3 app */
181212
function createLogIngestRoutes(store: LogStore, logsRoute: `/${string}`) {
182213
const router = createRouter();

packages/next/src/route.ts

Lines changed: 79 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -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
}
7376
function 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

Comments
 (0)