The console lists and calls a role's tools

seat:<seat>.<verb> addresses a role's tool (with @<node> for a node-scoped seat); the listing asks the
mesh-controller seat's tools verb beside the modules and marks a role's tools; <seat>.<verb> resolves
to the seat when the seat declares that verb, a module's own name otherwise (novox/hq ADR 0154).
This commit is contained in:
2026-09-30 17:41:56 +02:00
parent dea98e509a
commit 71965ef958
6 changed files with 156 additions and 23 deletions
+15 -5
View File
@@ -15,7 +15,7 @@
*/
import type { Broker } from "@novox/mesh-sdk/messaging";
import { callTool, toolsOn, whyItFailed, type Listing } from "./client.js";
import { callTool, seatsIn, toolsOn, whyItFailed, type Listing, type Seats } from "./client.js";
/** The protocol version this speaks. Stated, because a host that wants another should be told so
* rather than discovering it through a shape it did not expect. */
@@ -67,6 +67,16 @@ export function mcpSurface(bus: Broker, who: string): Surface {
}
return known.listing;
};
// The roles the last listing knew, so `<seat>.<verb>` resolves to the seat. Fetched once if a
// call arrives before any list did; a listing that failed leaves no roles, and the name is then
// a module's, which is the right fallback for a mesh whose control plane is away.
const roles = async (): Promise<Seats | undefined> => {
try {
return seatsIn(await listing());
} catch {
return undefined;
}
};
return {
async handle(request) {
@@ -86,9 +96,9 @@ export function mcpSurface(bus: Broker, who: string): Surface {
`These are the tools of a Novox mesh, reached as ${who}. Every call goes to the module ` +
`that serves it; what may be called was fixed when this account was issued, so a ` +
`refusal means the account, not the tool. The list is what the running modules ` +
`answered; a module that did not answer is named in the list's _meta and can still be ` +
`called by <module>.<tool>. The mesh's own verbs (status, push, assign) are not served ` +
`on the bus yet.`,
`answered, plus every role's tools from the mesh's records — the mesh's own verbs ` +
`(mesh-controller.status, .push, .assign …) among them; a module that did not answer ` +
`is named in the list's _meta and can still be called by <module>.<tool>.`,
});
case "notifications/initialized":
@@ -122,7 +132,7 @@ export function mcpSurface(bus: Broker, who: string): Surface {
const name = String(request.params?.name ?? "");
const args = request.params?.arguments ?? {};
try {
const result = await callTool(bus, name, args);
const result = await callTool(bus, name, args, await roles());
// Text, because that is what every host renders. The content is the module's answer
// as JSON, unshaped: an adapter that flattened it would be deciding what matters in
// somebody else's answer.