/** * The console: the same surface over HTTP on loopback, started the way the mesh starts it — on the * module credential in MESH_BROKER_FILE (novox/hq ADR 0152, design 34 §2). * * docker run -d --rm --name t -p 14232:4222 nats:2.10-alpine -js * MESH_TEST_NATS=nats://127.0.0.1:14232 node --test --experimental-strip-types test/http.test.ts */ import assert from "node:assert/strict"; import { test } from "node:test"; import { spawn, type ChildProcess } from "node:child_process"; import { mkdtemp, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { connectNats } from "../dist/broker-nats.js"; import { serveMcpHttp } from "../dist/http.js"; const url = process.env.MESH_TEST_NATS; async function aMesh(t: { after: (fn: () => Promise | void) => void }) { const catalogue = await connectNats({ url: url!, module: "mesh-catalog" }); const shop = await connectNats({ url: url!, module: "shop" }); await catalogue.handle("catalog_modules", async () => ({ modules: [{ module: "shop" }] })); await shop.handle("tools", async () => ({ module: "shop", tools: [{ name: "price", description: "what something costs", input: {} }], })); await shop.handle("price", async (body: { of?: string }) => ({ of: body.of ?? "nothing", cost: 12 })); t.after(async () => { await catalogue.close(); await shop.close(); }); } /** `mesh serve` as the mesh runs it: MESH_BROKER_FILE, a listen address, nothing else. */ async function aConsole(t: { after: (fn: () => Promise | void) => void }): Promise { const dir = await mkdtemp("/tmp/mesh-console-"); const credential = join(dir, "broker"); await writeFile(credential, JSON.stringify({ url, node: "desk", module: "mesh-console", user: "desk.mesh-console", password: "x" })); const child: ChildProcess = spawn(process.execPath, ["dist/mesh.js", "serve", "--listen", "127.0.0.1:0"], { env: { ...process.env, MESH_BROKER_FILE: credential, MESH_CREDENTIAL: "" }, stdio: ["ignore", "pipe", "pipe"], }); t.after(() => { child.kill("SIGTERM"); }); return new Promise((resolve, reject) => { let out = ""; let err = ""; child.stdout!.on("data", (d) => { out += d.toString(); const m = /listening on (http:\/\/[^/]+\/mcp)/.exec(out); if (m) resolve(m[1]!); }); child.stderr!.on("data", (d) => (err += d.toString())); child.on("exit", (code) => reject(new Error(`serve exited ${code}: ${err}`))); }); } async function post(endpoint: string, body: unknown): Promise<{ status: number; json?: any }> { const res = await fetch(endpoint, { method: "POST", headers: { "content-type": "application/json", accept: "application/json" }, body: JSON.stringify(body), }); const text = await res.text(); return { status: res.status, json: text ? JSON.parse(text) : undefined }; } test("the console answers a host on loopback, as the account the mesh gave it", async (t) => { if (!url) return t.skip("MESH_TEST_NATS unset"); await aMesh(t); const endpoint = await aConsole(t); const hello = await post(endpoint, { jsonrpc: "2.0", id: 1, method: "initialize", params: {} }); assert.equal(hello.status, 200); assert.match(hello.json.result.instructions, /desk\.mesh-console/, "the handshake names the console's account"); const heard = await post(endpoint, { jsonrpc: "2.0", method: "notifications/initialized" }); assert.equal(heard.status, 202, "a notification is heard and not answered"); const listed = await post(endpoint, { jsonrpc: "2.0", id: 2, method: "tools/list" }); assert.deepEqual(listed.json.result.tools.map((x: { name: string }) => x.name), ["shop.price"]); const called = await post(endpoint, { jsonrpc: "2.0", id: 3, method: "tools/call", params: { name: "shop.price", arguments: { of: "a hat" } }, }); assert.deepEqual(JSON.parse(called.json.result.content[0].text), { of: "a hat", cost: 12 }); // A person's client through the same endpoint, with no credential of its own. const { main } = await import("../dist/mesh.js"); const logged: string[] = []; const was = console.log; console.log = (line: string) => logged.push(String(line)); try { assert.equal(await main(["tools", "--console", endpoint]), 0); } finally { console.log = was; } assert.ok(logged.some((l) => l.startsWith("shop.price")), `the client did not list through the console: ${logged}`); }); test("the console binds loopback and nowhere else", async (t) => { if (!url) return t.skip("MESH_TEST_NATS unset"); const bus = await connectNats({ url, module: "mesh-console", node: "desk" }); try { await assert.rejects(() => serveMcpHttp(bus, "desk.mesh-console", "0.0.0.0:0"), /loopback and nowhere else/); const up = await serveMcpHttp(bus, "desk.mesh-console", "127.0.0.1:0"); assert.match(up.address, /^127\.0\.0\.1:\d+$/); await up.close(); } finally { await bus.close(); } });