Files
mesh-tools/README.md
T
jochen c46f9502ee node-tools is a module beside mesh-tools: the runtime as a bundle, and serve is the console (hq ADR 0175, to-be 38 WP3)
One repository, two modules (ADR 0069). `node-tools/` holds the runtime — its code, tests, package
and the manifest of the module the controller composes a process for on every machine it is
assigned to: a bundle of `src/main.js`, the interpreter as a package, a place for the node's
credential, the loopback port the console declared, and leave to call every tool. Nothing about
how it runs: which bundles to load, where the credential is and whose machine it is are the
controller's to compose (WP2). The root module `mesh-tools` keeps the two images TypeScript
bundles are compiled in and a module's own service may run in; it is no longer how tools reach a
node.

As node-tools, `serve` is also the console (ADR 0175 §6): the same process answers MCP on
loopback for whoever is on the machine, through which the tools it serves can be called. A
module's own runtime in a container keeps serving without a listener.

The toolchain image now carries /app/runtime — a package.json saying the compiled files are ES
modules and the production node_modules — for the builder to copy into every TypeScript bundle,
so a bundle unpacked on a machine starts (ADR 0188 §5; the builder's side is the controller's).
Proven here by compiling node-tools with the toolchain's exact flags and starting the result.
The AMQP probe script is gone with the bus it probed.
2026-10-02 21:43:37 +02:00

5.0 KiB

mesh-tools

Two modules in one repository (novox/hq ADR 0069), one piece of software:

  • node-tools (node-tools/) — the node's tool runtime as a module (ADR 0175, to-be 38 WP3): one process per machine the host runs from this bundle, serving every assigned module's tools and every held seat's verbs on the bus, and answering MCP on the machine's loopback — the console (design 34). The code, its tests and the mesh client all live there.
  • mesh-tools (this directory) — the two images TypeScript bundles are compiled in and a module's own service may still run in. Built from the same code; no longer how tools reach a node.

The runtime:

  1. connects the mesh bus on the node's credential — a concrete implementation of the sdk's Broker contract;
  2. reads one membership per module it serves — what the mesh issued that module on this machine (ADR 0160): where its tools are answered, which seats it holds — and follows each live;
  3. imports each module's compiled tool entrypoints, each of which registers its tools as it loads, guarded: a bundle that throws is named, in the log and in what tools answers for its module, and the others serve;
  4. serves every module's tools on that module's subjects and every held seat's verbs on the seat's.

A bundle that is not plain JavaScript — a Go or Rust binary, a Python script, or a JavaScript file marked executable — is launched rather than imported (novox/hq ADR 0188): the runtime starts it as a child with its own environment and speaks MCP over stdio to it, tools/list once and tools/call per call. A tool it lists as <seat>.<verb> is the seat's implementation. A child that exits is named in the log and started again on its next call. So a tools bundle may be written in any language; the mesh's SDK for each is the stdio loop and nothing more (node-tools/src/launch.ts is the runtime's side of it).

Everything hard — dispatch, collection, duplicate-name safety — is the sdk's. This is the thin wrapper that binds the bus and loads the modules. Keeping the bus client here, out of the sdk, is deliberate: a bus-client change never rebuilds a module (ADR 0039). The runtime is module-agnostic: it knows bundles and subjects, nothing of what any module does. A tool that needs root escalates itself — root is the module's concern, not the runtime's.

Running it

MESH_BROKER_FILE        the node's sealed credential, as the mesh delivered it
MESH_TOOL_MODULES       alpha=/…/alpha/tools/index.js,beta=/…/beta/dist/index.js,…
                        the modules to serve and their compiled entrypoints; several entries may
                        name one module. A bare path is an entrypoint of the credential's own module
                        — the one-module form a per-module container still sets.
MESH_OPERATOR_ACCOUNT   whose machine this is, and MESH_OPERATOR_HOME where their home is; set by
                        the mesh when the node has an account, read by tools from their environment
MESH_BROKER_URL         a plain URL instead of the credential, for the bootstrap case

node dist/main.js. On a node the controller composes the variables and the host supervises the process like any other host-side workload (novox/hq to-be 38). As node-tools the same process is the console: MCP on 127.0.0.1:4270 (or MESH_CONSOLE_LISTEN). The container (Dockerfile) is how a module's own service may still be built; it is no longer how tools reach a node.

mesh — the tools for whoever is on a machine

The same package carries the client (novox/hq design 25 §7, design 34): mesh tools, mesh call <module>.<tool> [json], mesh mcp (an MCP server over stdio for a program a person starts) and mesh serve (the console: MCP over HTTP on a machine's loopback, started by the mesh as the mesh-console module on the credential in MESH_BROKER_FILE — novox/hq ADR 0152). mesh serve refuses to bind anything but loopback. With --console <url>, tools and call go through a console already on the machine and need no credential.

Discovery asks the modules: every runtime answers a tools verb for each module it serves, with names, descriptions and schemas from the code that answers them, and the console asks the catalogue which modules the mesh holds and each module what it serves. A module that does not answer is named, never dropped. A module may not name a tool of its own tools; the runtime refuses it at load.

Verified

npm test runs against a real NATS server with JetStream (MESH_TEST_NATS, see any test's header for the one-line docker run) and proves the whole path over the wire: the runtime serves a registered tool, a separate connection invokes it by name and gets the result, an unknown tool is refused, a runtime serves exactly the subjects it is issued and re-serves on a new membership, and the node's runtime serves three modules' bundles on one credential — one of them broken, named and not fatal — with every seat verb answering where the membership put it.