// The tool runtime — the thin per-node process that makes a module's tools actually serve. It // binds the mesh broker, loads the assigned modules' tool entrypoints (each of which calls // registerModuleTools as it imports), and hands them to the sdk's serving harness. Everything hard // — dispatch, collection, duplicate-name safety — is the sdk's; this is the wrapper. import { pathToFileURL } from "node:url"; import { resolve } from "node:path"; import { useBroker } from "@novox/mesh-sdk/messaging"; import { serveTools, listTools } from "@novox/mesh-sdk/tools"; import type { Broker } from "@novox/mesh-sdk/messaging"; export interface RuntimeOptions { /** The mesh broker to serve over. */ broker: Broker; /** Absolute paths to the assigned modules' compiled tool entrypoints (e.g. .../umami/tools/index.js). */ moduleEntrypoints: string[]; } /** Load the modules, bind the broker, and serve. Returns a stop function that unhooks serving. */ export async function runTools(opts: RuntimeOptions): Promise<() => void> { useBroker(() => opts.broker); for (const entry of opts.moduleEntrypoints) { // Importing the entrypoint runs its registerModuleTools(...) — that is the whole handshake. await import(pathToFileURL(resolve(entry)).href); } // Serve the RPC endpoint only if a module actually registered a tool. A pure-events module (the // audit logger) registers none, and its scoped account may not declare the serve queue — so a // runtime that always served would fail for exactly the modules that never needed it. const tools = listTools(); const stop = tools.length > 0 ? await serveTools(opts.broker) : () => {}; console.log(`[mesh-tools] serving ${tools.length} tool(s): ${tools.map((t) => t.name).join(", ") || "(none)"}`); return stop; }