/** Read-only live API/MCP smoke check. Prints metadata, never credentials or records. */ import { loadEnvFile } from "node:process"; import { fileURLToPath } from "node:url"; import path from "node:path"; import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; const root = fileURLToPath(new URL("../", import.meta.url)); const envFile = path.resolve(root, process.env.MEGA_CONNECTION_ENV_FILE || ".env.local"); loadEnvFile(envFile); const argumentsList=process.argv.slice(2); const remoteIndex=argumentsList.indexOf("--mcp-url"); const remoteUrl=remoteIndex>=0?argumentsList.splice(remoteIndex,2)[1]:undefined; if(remoteIndex>=0&&!remoteUrl)throw new Error("--mcp-url requires an endpoint URL"); if(remoteUrl){const url=new URL(remoteUrl);if(!["https:","http:"].includes(url.protocol)||url.username||url.password||url.pathname!=="/api/mcp"||url.search||url.hash)throw new Error("Invalid MCP endpoint URL");} const origins = argumentsList; if (!origins.length) origins.push("http://127.0.0.1:6004"); for (const origin of origins) { const url = new URL(origin); if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.pathname !== "/" || url.search || url.hash) { throw new Error("Pass app origins only, for example https://mega.dash.apphub.co.za"); } } const secrets = Object.entries(process.env) .filter(([key, value]) => /SECRET|TOKEN|PASSWORD|API_KEY|ADMIN_EMAIL/.test(key) && value?.length >= 6) .map(([, value]) => value); const redact = (message) => secrets.reduce((text, value) => text.split(value).join("[redacted]"), String(message)); const report = { checkedAt: new Date().toISOString(), timezone: "Africa/Johannesburg", envFile: path.relative(root, envFile), localAuthMode: process.env.AUTH_MODE === "users" ? "users" : "pin", checks: [], }; function record(check, passed, details = {}) { report.checks.push({ check, passed, ...details }); } async function getJson(url, headers) { const response = await fetch(url, { headers, redirect: "manual", signal: AbortSignal.timeout(30000) }); const text = await response.text(); let body; try { body = JSON.parse(text); } catch { /* HTML response, e.g. a 404 */ } return { status: response.status, body, contentType: response.headers.get("content-type") }; } const transport = remoteUrl ? new StreamableHTTPClientTransport(new URL(remoteUrl),{requestInit:{headers:{Authorization:`Bearer ${process.env.MEGA_MCP_TOKEN||process.env.MEGA_MCP_READER_TOKEN||""}`}}}) : new StdioClientTransport({ command: process.execPath, args: ["--import", "tsx", `--env-file=${envFile}`, path.join(root, "src/mcp/server.ts")], cwd: root, stderr: "pipe", }); // Drain diagnostics; retain neither business records nor potentially sensitive SDK logs. if(transport instanceof StdioClientTransport)transport.stderr?.on("data", () => {}); const client = new Client({ name: "mega-connection-check", version: "1.0.0" }); let poNumber; try { await client.connect(transport, { timeout: 15000 }); record("MCP initialize", client.getServerVersion()?.name === "mega-infrastructure", { server: client.getServerVersion() }); const { tools } = await client.listTools({}, { timeout: 15000 }); record("MCP tools/list", tools.length > 0, { toolCount: tools.length, tools: tools.map(({ name, description, inputSchema }) => ({ name, description, inputSchema })), }); for (const [name, args] of [ ["list_sites", {}], ["list_employees", { limit: 1 }], ["list_purchase_orders", { limit: 1 }], ["get_pending_verification", {}], ]) { try { const result = await client.callTool({ name, arguments: args }, undefined, { timeout: 45000 }); const raw = result.content?.find((item) => item.type === "text")?.text; let data; try { data = JSON.parse(raw); } catch { /* Report invalid JSON as a failure. */ } const passed = result.isError !== true && data != null && !data.error; record(`MCP tools/call ${name}`, passed, { shape: Array.isArray(data) ? "array" : typeof data, ...(Array.isArray(data) ? { rowCount: data.length } : { keys: data ? Object.keys(data) : [] }), ...(!passed ? { error: redact(raw || "No JSON response") } : {}), }); if (name === "list_purchase_orders" && passed) poNumber = data.purchaseOrders?.[0]?.poNumber; } catch (error) { record(`MCP tools/call ${name}`, false, { error: redact(error.message) }); } } } catch (error) { record("MCP connection", false, { error: redact(error.message) }); } finally { await client.close(); } const pbBase = process.env.NEXT_PUBLIC_POCKETBASE_URL?.replace(/\/$/, ""); if (pbBase) { try { const result = await getJson(`${pbBase}/api/health`); record("PocketBase health", result.status === 200, { status: result.status }); if (process.env.POCKETBASE_ADMIN_TOKEN) { const read = await getJson(`${pbBase}/api/collections/sites/records?perPage=1&fields=id`, { Authorization: process.env.POCKETBASE_ADMIN_TOKEN }); record("PocketBase authenticated records GET", read.status === 200 && Array.isArray(read.body?.items), { status: read.status }); } } catch (error) { record("PocketBase connection", false, { error: redact(error.message) }); } } for (const origin of origins) { const base = new URL(origin).origin; const lookupPath = `/api/purchase-orders/by-number/${encodeURIComponent(poNumber || "CONNECTION-CHECK")}/items`; for (const [route, headers, accept, note] of [ ["/api/health", undefined, (r) => r.status === 200 && r.body?.status === "ok", "Public app health"], ["/api/dashboard-attention", undefined, (r) => [200, 401].includes(r.status), "200 means no login was required; 401 means login is required"], [lookupPath, undefined, (r) => r.status === 401, "Missing ingest secret must be rejected"], ...(process.env.N8N_INGEST_SECRET ? [[lookupPath, { "x-ingest-secret": process.env.N8N_INGEST_SECRET }, (r) => r.status === 200 && Array.isArray(r.body?.items), "Authenticated read-only PO lookup"]] : []), ["/api/mcp", undefined, (r) => [401, 404, 503].includes(r.status), "Unauthenticated MCP probe; initialization is checked separately"], ]) { try { const result = await getJson(`${base}${route}`, headers); record(`${base}: ${note}`, accept(result), { // A real PO number may be used for the request, but is never persisted. route: route === lookupPath ? "/api/purchase-orders/by-number/[poNumber]/items" : route, status: result.status, contentType: result.contentType, keys: result.body ? Object.keys(result.body) : [], ...(result.body?.error ? { error: redact(result.body.error) } : {}), }); } catch (error) { record(`${base}: ${note}`, false, { error: redact(error.message) }); } } } report.summary = { passed: report.checks.filter((check) => check.passed).length, failed: report.checks.filter((check) => !check.passed).length }; console.log(JSON.stringify(report, null, 2)); if (report.summary.failed) process.exitCode = 1;