The runtime serves a tools verb per module with names, descriptions and schemas (design 34 §3), and refuses a module naming its own tool tools. Discovery asks catalog_modules then each module, naming what did not answer. One MCP handler over two transports: stdio (mesh mcp) and loopback HTTP (mesh serve, the mesh-console module, novox/hq ADR 0152); serve refuses any bind but loopback. tools/call may go through a running console with --console and no credential.
61 lines
2.4 KiB
TypeScript
61 lines
2.4 KiB
TypeScript
/**
|
|
* Every module's runtime answers `tools` for it (novox/hq ADR 0152, design 34 §3): the names,
|
|
* descriptions and schemas from the code that answers them. Against a real bus, because the claim is
|
|
* what a second connection gets back.
|
|
*
|
|
* 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/runtime-tools.test.ts
|
|
*/
|
|
import assert from "node:assert/strict";
|
|
import { test } from "node:test";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
import { resetTools } from "@novox/mesh-sdk/tools";
|
|
|
|
import { connectNats } from "../dist/broker-nats.js";
|
|
import { runTools } from "../dist/runtime.js";
|
|
|
|
const url = process.env.MESH_TEST_NATS;
|
|
const fixture = (name: string) => fileURLToPath(new URL(`./fixtures/${name}`, import.meta.url));
|
|
|
|
test("a module registering two tools answers three names, the third being what it serves", async (t) => {
|
|
if (!url) return t.skip("MESH_TEST_NATS unset");
|
|
resetTools();
|
|
const shop = await connectNats({ url, node: "one", module: "shop" });
|
|
const asker = await connectNats({ url, module: "person.ada" });
|
|
const stop = await runTools({ broker: shop, moduleEntrypoints: [fixture("shop-tools.mjs")] });
|
|
try {
|
|
const answer = await asker.request<Record<string, never>, { module: string; tools: { name: string; input: unknown }[] }>(
|
|
"shop.tools",
|
|
{},
|
|
);
|
|
assert.equal(answer.module, "shop");
|
|
assert.deepEqual(answer.tools.map((x) => x.name), ["price", "refund"]);
|
|
// The schema travels with the name: a name alone is not callable by something that has never
|
|
// seen the mesh before.
|
|
assert.deepEqual(answer.tools[0].input, { of: { type: "string", description: "the thing" } });
|
|
// And the tools themselves still answer beside it.
|
|
const priced = await asker.request<{ of: string }, { cost: number }>("shop.price", { of: "a hat" });
|
|
assert.equal(priced.cost, 12);
|
|
} finally {
|
|
stop();
|
|
await asker.close();
|
|
await shop.close();
|
|
}
|
|
});
|
|
|
|
test("a module naming a tool of its own `tools` is refused at load", async (t) => {
|
|
if (!url) return t.skip("MESH_TEST_NATS unset");
|
|
resetTools();
|
|
const clash = await connectNats({ url, node: "one", module: "clash" });
|
|
try {
|
|
await assert.rejects(
|
|
() => runTools({ broker: clash, moduleEntrypoints: [fixture("clash-tools.mjs")] }),
|
|
/names a tool "tools"/,
|
|
);
|
|
} finally {
|
|
resetTools();
|
|
await clash.close();
|
|
}
|
|
});
|