The console: mesh serve on loopback, and every runtime answers tools
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.
This commit is contained in:
+23
-12
@@ -18,16 +18,22 @@ import { callTool, toolsOn, whyItFailed } from "../dist/client.js";
|
||||
|
||||
const url = process.env.MESH_TEST_NATS;
|
||||
|
||||
/** A module serving the catalogue's tool list and one tool of its own, so the client has a mesh to
|
||||
* talk to. Two connections, because a person and a module are different users even in a test. */
|
||||
/** A mesh as discovery sees it (design 34 §3): the catalogue holding three modules, two of them up
|
||||
* and answering `tools`, one held and not running. Two connections, because a person and a module
|
||||
* are different users even in a test. */
|
||||
async function aMeshWithTools() {
|
||||
const catalogue = await connectNats({ url: url!, module: "mesh-catalog" });
|
||||
const shop = await connectNats({ url: url!, module: "shop" });
|
||||
await catalogue.handle("catalog_tools", async () => ({
|
||||
tools: [
|
||||
{ module: "shop", name: "price", description: "what something costs", input: { type: "object" } },
|
||||
{ module: "mesh-catalog", name: "catalog_tools", description: "what tools the mesh has" },
|
||||
],
|
||||
await catalogue.handle("catalog_modules", async () => ({
|
||||
modules: [{ module: "shop" }, { module: "mesh-catalog" }, { module: "ghost" }],
|
||||
}));
|
||||
await catalogue.handle("tools", async () => ({
|
||||
module: "mesh-catalog",
|
||||
tools: [{ name: "catalog_modules", description: "what modules the mesh has", input: {} }],
|
||||
}));
|
||||
await shop.handle("tools", async () => ({
|
||||
module: "shop",
|
||||
tools: [{ name: "price", description: "what something costs", input: { type: "object" } }],
|
||||
}));
|
||||
await shop.handle("price", async (body: { of?: string }) => ({ of: body.of ?? "nothing", cost: 12 }));
|
||||
return {
|
||||
@@ -38,17 +44,22 @@ async function aMeshWithTools() {
|
||||
};
|
||||
}
|
||||
|
||||
test("a person sees what the catalogue says the mesh has, sorted", async (t) => {
|
||||
test("a person sees what the running modules answer, sorted, and who did not answer", async (t) => {
|
||||
if (!url) return t.skip("MESH_TEST_NATS unset");
|
||||
const mesh = await aMeshWithTools();
|
||||
const person = await connectNats({ url, module: "person.ada" });
|
||||
try {
|
||||
const tools = await toolsOn(person);
|
||||
const began = Date.now();
|
||||
const have = await toolsOn(person);
|
||||
assert.deepEqual(
|
||||
tools.map((x) => `${x.module}.${x.name}`),
|
||||
["mesh-catalog.catalog_tools", "shop.price"],
|
||||
"the list is what the catalogue answered, in a stable order",
|
||||
have.tools.map((x) => `${x.module}.${x.name}`),
|
||||
["mesh-catalog.catalog_modules", "shop.price"],
|
||||
"the list is what the modules answered, in a stable order",
|
||||
);
|
||||
// Silence is named, never dropped: a module the catalogue holds and nothing answered for.
|
||||
assert.deepEqual(have.notAnswering, ["ghost"]);
|
||||
// And at once: a module that is not running costs nothing, or the list is unusable.
|
||||
assert.ok(Date.now() - began < 5_000, "an absent module waited out the timeout");
|
||||
} finally {
|
||||
await person.close();
|
||||
await mesh.close();
|
||||
|
||||
Reference in New Issue
Block a user