Fix: CORS + relative URL + Langfuse tracing wired into gateway

Three fixes:
1. CORS headers on all gateway responses (browser dashboard was
   blocked by same-origin policy)
2. Dashboard JS uses window.location.origin instead of hardcoded
   localhost:3700 (LAN browsers couldn't reach it)
3. Langfuse tracing wired into every gateway request — api() wrapper
   creates spans for each lakehouse call, logGeneration for LLM calls.
   Week simulation now produces 34 observations per run visible in
   Langfuse UI.

7 traces confirmed in Langfuse after restart. Every /sql, /search,
/vram, /simulation call is tracked with timing + inputs + outputs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
root 2026-04-17 00:53:18 -05:00
parent 4a2bfce6e0
commit cd1fda3e21
2 changed files with 56 additions and 5 deletions

View File

@ -1,4 +1,5 @@
const GW = "http://localhost:3700"; // Use the same host the browser loaded the page from — works on LAN + localhost
const GW = window.location.origin;
let simData: any = null; let simData: any = null;
let currentDay = 0; let currentDay = 0;

View File

@ -17,19 +17,48 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import { z } from "zod"; import { z } from "zod";
import { startTrace, logSpan, logGeneration, scoreTrace, flush as flushTraces } from "./tracing.js";
const BASE = process.env.LAKEHOUSE_URL || "http://localhost:3100"; const BASE = process.env.LAKEHOUSE_URL || "http://localhost:3100";
const PORT = parseInt(process.env.MCP_PORT || "3700"); const PORT = parseInt(process.env.MCP_PORT || "3700");
const MODE = process.env.MCP_TRANSPORT || "http"; // "stdio" or "http" const MODE = process.env.MCP_TRANSPORT || "http"; // "stdio" or "http"
// Active trace for the current request — set per-request in the HTTP handler
let activeTrace: ReturnType<typeof startTrace> | null = null;
async function api(method: string, path: string, body?: any) { async function api(method: string, path: string, body?: any) {
const t0 = Date.now();
const resp = await fetch(`${BASE}${path}`, { const resp = await fetch(`${BASE}${path}`, {
method, method,
headers: body ? { "Content-Type": "application/json" } : {}, headers: body ? { "Content-Type": "application/json" } : {},
body: body ? JSON.stringify(body) : undefined, body: body ? JSON.stringify(body) : undefined,
}); });
const text = await resp.text(); const text = await resp.text();
try { return JSON.parse(text); } catch { return { raw: text, status: resp.status }; } const ms = Date.now() - t0;
let parsed: any;
try { parsed = JSON.parse(text); } catch { parsed = { raw: text, status: resp.status }; }
// Trace the call if we have an active trace
if (activeTrace) {
const isGen = path.includes("/generate");
if (isGen) {
logGeneration(activeTrace, `lakehouse${path}`, {
model: body?.model || "unknown",
prompt: typeof body?.prompt === "string" ? body.prompt.slice(0, 500) : JSON.stringify(body).slice(0, 300),
completion: typeof parsed?.text === "string" ? parsed.text.slice(0, 500) : JSON.stringify(parsed).slice(0, 300),
duration_ms: ms,
tokens_in: parsed?.prompt_eval_count,
tokens_out: parsed?.eval_count,
});
} else {
logSpan(activeTrace, `lakehouse${path}`, body, {
rows: parsed?.row_count, sources: parsed?.sources?.length,
sql_matches: parsed?.sql_matches, method: parsed?.method,
}, ms);
}
}
return parsed;
} }
const server = new McpServer({ name: "lakehouse", version: "1.0.0" }); const server = new McpServer({ name: "lakehouse", version: "1.0.0" });
@ -267,13 +296,27 @@ async function main() {
async fetch(req) { async fetch(req) {
const url = new URL(req.url); const url = new URL(req.url);
const json = async () => req.method === "POST" ? await req.json() : {}; const json = async () => req.method === "POST" ? await req.json() : {};
const ok = (data: any) => Response.json(data);
const err = (msg: string, status = 400) => Response.json({ error: msg }, { status }); // CORS — dashboard runs in the browser, gateway is a different origin
const cors = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
};
if (req.method === "OPTIONS") return new Response(null, { status: 204, headers: cors });
const ok = (data: any) => Response.json(data, { headers: cors });
const err = (msg: string, status = 400) => Response.json({ error: msg }, { status, headers: cors });
try { try {
// Health // Health — no trace needed
if (url.pathname === "/health") return ok({ status: "ok", lakehouse: BASE, tools: 11 }); if (url.pathname === "/health") return ok({ status: "ok", lakehouse: BASE, tools: 11 });
// Start a Langfuse trace for every non-static request
if (req.method === "POST" || !["/","/dashboard","/dashboard.css","/dashboard.ts","/dashboard.js"].includes(url.pathname)) {
activeTrace = startTrace(`gw:${url.pathname}`, { method: req.method, path: url.pathname });
}
// Self-orientation: any agent calls this first to understand the system // Self-orientation: any agent calls this first to understand the system
if (url.pathname === "/context") { if (url.pathname === "/context") {
const instructions = await Bun.file("/home/profit/lakehouse/mcp-server/AGENT_INSTRUCTIONS.md").text().catch(() => ""); const instructions = await Bun.file("/home/profit/lakehouse/mcp-server/AGENT_INSTRUCTIONS.md").text().catch(() => "");
@ -431,9 +474,16 @@ async function main() {
return ok(await runWeekSimulation()); return ok(await runWeekSimulation());
} }
activeTrace = null;
return err("Unknown path. Available: / /health /search /sql /match /worker/:id /ask /log /playbooks /profile/:id /vram /context /verify /simulation/run", 404); return err("Unknown path. Available: / /health /search /sql /match /worker/:id /ask /log /playbooks /profile/:id /vram /context /verify /simulation/run", 404);
} catch (e: any) { } catch (e: any) {
if (activeTrace) { scoreTrace(activeTrace, "error", 0, e.message); }
activeTrace = null;
return err(e.message || String(e), 500); return err(e.message || String(e), 500);
} finally {
// Flush traces async — don't block the response
flushTraces().catch(() => {});
activeTrace = null;
} }
}, },
}); });