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:
+38
-2
@@ -6,9 +6,22 @@
|
||||
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 { collectTools, serveTools, listTools, toolKey } from "@novox/mesh-sdk/tools";
|
||||
import type { Broker } from "@novox/mesh-sdk/messaging";
|
||||
|
||||
/**
|
||||
* The one verb every module's runtime answers for it (novox/hq ADR 0152, design 34 §3): the
|
||||
* module's tool names, descriptions and argument schemas, from the code that answers them and
|
||||
* from nowhere else. Discovery asks the module, because a copy kept anywhere else drifts.
|
||||
*/
|
||||
export const TOOLS_VERB = "tools";
|
||||
|
||||
/** What `tools` answers for one module. */
|
||||
export interface ToolsAnswer {
|
||||
module: string;
|
||||
tools: { name: string; description: string; input: Readonly<Record<string, unknown>> }[];
|
||||
}
|
||||
|
||||
export interface RuntimeOptions {
|
||||
/** The mesh broker to serve over. */
|
||||
broker: Broker;
|
||||
@@ -30,6 +43,29 @@ export async function runTools(opts: RuntimeOptions): Promise<() => void> {
|
||||
// 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) : () => {};
|
||||
|
||||
// And, for every module that serves any, the verb that says what it serves. Refused before
|
||||
// anything is bound if a module named a tool of its own `tools`: one name answering two things
|
||||
// is the fault nobody can diagnose afterwards, and the runtime is the only place that sees both.
|
||||
const stops: Array<() => void> = [stop];
|
||||
for (const { module, tools: own } of collectTools()) {
|
||||
if (own.length === 0) continue;
|
||||
if (own.some((t) => t.name === TOOLS_VERB)) {
|
||||
stop();
|
||||
throw new Error(
|
||||
`${module} names a tool "${TOOLS_VERB}", which is the verb the runtime answers for every ` +
|
||||
"module with what it serves (novox/hq ADR 0152) — refused, rename it",
|
||||
);
|
||||
}
|
||||
const answer: ToolsAnswer = {
|
||||
module,
|
||||
tools: own.map((t) => ({ name: t.name, description: t.description, input: t.input })),
|
||||
};
|
||||
stops.push(await opts.broker.handle(toolKey(module, TOOLS_VERB), async () => answer));
|
||||
}
|
||||
|
||||
console.log(`[mesh-tools] serving ${tools.length} tool(s): ${tools.map((t) => t.name).join(", ") || "(none)"}`);
|
||||
return stop;
|
||||
return () => {
|
||||
for (const s of stops) s();
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user