Describe the mesh MCP server as it runs and mark its design implemented (issue 1008) #400

Merged
mesh-tools merged 2 commits from fix/1008-mcp-server-shipped into main 2026-10-17 11:43:04 +00:00
2 changed files with 137 additions and 55 deletions
+122 -47
View File
@@ -1,69 +1,144 @@
---
layer: as-is
status: implemented
code: [mesh-catalog modules/mesh-console, mesh-tools src/mesh.ts, mesh-tools src/http.ts, mesh-tools src/runtime.ts, mesh-controller internal/broker, mesh-controller cmd/mesh-controller/check.go, mesh-controller cmd/mesh-controller/seatverbs.go]
updated: 2026-10-10
code: [mesh-tools, mesh-host, mesh-controller]
updated: 2026-10-17
decisions:
- "ADR 0152"
- "ADR 0095"
- "ADR 0037"
- "ADR 0175"
- "ADR 0195"
- "ADR 0197"
- "ADR 0367"
- "ADR 0154"
---
# The mesh MCP server, as it runs
**The mesh's tools reach a person through a module the mesh assigned to their machine.** Since
2026-09-30 a workstation that is a node can be assigned `mesh-console`; the mesh mints a bus account
`<node>.mesh-console`, seals its credential to the machine, and the container binds
`127.0.0.1:<port>` with the port the mesh assigned for the manifest's declared one. An agent on the
machine is pointed at `http://127.0.0.1:<port>/mcp` and sees the mesh's tools; a person uses the same
endpoint. Nothing on the machine holds a credential a person had to carry.
**The `mesh-console` container no longer exists.** Read 2026-10-17 from `mesh-tools` (package
`node-tools`), `mesh-host` and `mesh-controller` on their forges' main, and from the live mesh
through `mesh_overview`, `mesh_machine`, `mesh_runtimes` and `mesh_describe`. Checked: the
catalogue (`mesh-catalog`) carries no module named `mesh-console` any more — the name survives only
in the glossary's retired-words list and in a test fixture — and none of the four nodes answers a
module of that name.
## What it is now
**The tool runner is one of the mesh's core components**, named `node-tools` beside
`mesh-controller`, `mesh-host` (the node-engine) and `nats` (the bus) — the mesh's own repositories'
check treats the four the same way, as the core whose health the mesh itself defines rather than a
module's own. `mesh-host` and `mesh-controller` supervise, compose and account for it as a core
process rather than as an ordinary assignment (`ComponentNodeTools`/`BusNodeTools` in both, the
witness and lease code that restarts and rolls it back, the account named `<node>.node-tools` on
the bus).
**It still carries a manifest, `node-tools/module.json` in `mesh-tools`,** and that manifest is
where its grants live: `invokes` names `*`, so its account may call every tool on the mesh;
`waits` names the five event families `mesh_wait` may subscribe to (the knowledge base's reviews,
mesh-delivery's transitions, the forge's status, the controller's checked event, and the operator
channel's decision for the agent module) — the bus refuses a subscription outside this list
(ADR 0367 §2); and it
`provides` one provision, the MCP endpoint, at node scope, and `listens` on the port it serves from,
opened only to the machine itself. Its code lives under `node-tools/` in the `mesh-tools`
repository (`internal/console`, `internal/announce`, `internal/bus`).
**One runtime per node, run by the node-engine as a sibling process, never a container**
(ADR 0175). It holds the node's one bus credential and may call every tool on the mesh; its account
publishes nothing else. It binds `127.0.0.1:<port>` and refuses to bind anything else, said before
binding rather than after (`node-tools/internal/console/http.go`, citing ADR 0152): whoever is on
the machine owns the mesh there, and nobody else may reach this port. An agent or a person on the
node is pointed at `http://127.0.0.1:<port>/mcp` once; the `serverInfo.name` the handshake answers
is `mesh`.
## What it answers
`initialize`, `tools/list`, `tools/call`, over HTTP, one JSON body per request, no session and no event
stream. `tools/list` is what the running modules answered: every tool runner built on or after that day
serves a `tools` verb for its module, and the mesh MCP server asks the catalogue for the roster and each module
for its tools. A module that did not answer is named in the list's `_meta.notAnswering`. On the day it
shipped that was 36 of 51 modules — those that serve no tools at all, and those whose rebuilt runtime the
mesh records rather than rolls out — and 62 tools from the rest.
`initialize`, `notifications/initialized`, `ping`, `tools/list`, `tools/call`, over HTTP, one JSON
body per request, no session and no event stream (`node-tools/internal/console/mcp.go`).
`tools/call` reaches any tool by `<module>.<tool>`, listed or not. The mesh MCP server's grant is `*`, so what it
may call is every tool on the mesh; its account may publish nothing else and subscribes nothing.
**It announces seven tools, not five** (ADR 0195, extended by ADR 0367): `mesh_overview`,
`mesh_machine`, `mesh_search`, `mesh_describe`, `mesh_call`, `mesh_runtimes` and `mesh_wait`. The
handshake's `instructions` text names all seven and the four address forms. `mesh_overview` is
where the mesh-wide count of modules and tools is read live — a number copied into this document
would drift; asked on 2026-10-17 it answered 103 modules and 645 tools across the whole mesh, with
two module instances (`route-proxy` on two of the nodes) named as not answering rather than silently
absent.
No call is refused for want of a person's approval: a tool runner serves every call it is issued, whoever made
it, and cannot tell an agent from the operator (ADR 0259 §6, issue 340). An act waits for the operator's warrant
only where its module asks first, in its own code: the agent module's instruction proposals, and the
controller's settings proposals and the actions it offers on a condition.
**Addresses, exactly as the handshake states them:** `<seat>.<verb>` for a seat held once for the
mesh (`mesh-controller.status`); `<node>/<seat>.<verb>` for a seat every machine holds
(`<node>/node-packet-filter.rules`); `<node>/<module>.<tool>` for a module assigned to one machine;
and `<module>.<tool>` also for a module whose instances are interchangeable. A tool not listed can
still be called by address — listing is discovery, calling is the grant — and a module that is not
interchangeable is refused with the machines it runs on rather than guessed at.
**What exists is what announced itself** (ADR 0197). Every node's tool runner answers the NATS services
protocol's `$SRV.PING`, `$SRV.INFO` and `$SRV.STATS`, one service per process, naming in each
endpoint's metadata which module, seat, scope and machine serves it. `mesh_overview` and
`mesh_machine` gather one request's answers: at least 750ms so a runtime that only just answered
`$SRV.PING` is still waited for, up to 5s for one that said it is there but has not yet sent its
full announcement — a fixed shorter window lost the largest runtime's listing once a live
measurement found how slow its round trip over the bus could be. An announcement unchanged since
last asked (by its digest) is not re-fetched, which is why `mesh_runtimes` can show a runtime
answering in under 100ms for hundreds of kilobytes of discovery.
## mesh_wait
**The seventh tool, added by ADR 0367**, lets a session wait on the bus for an event instead of
polling: a call names one or more granted event families and an optional filter, and either answers
the first match or says it timed out. The tool runner's own bus account hears only the families its
manifest's `waits` list names; an event it hears is never taken as the state — a session reads the
state from the verb that owns it once woken. It is meant to be called in a loop from a background
command, not inline, because one call blocks until it answers or times out.
## The mesh's own verbs
*Since 2026-09-30 evening (ADR 0154).*
The mesh MCP server asks the `mesh-controller` seat's `tools` verb beside the modules and lists every role's
tools as `<seat>.<verb>` — `mesh-controller.status`, `mesh-controller.push` and the other ten. A call
to `<prefix>.<name>` reaches the seat when the prefix is a seat declaring that verb, and the module
otherwise; `seat:<seat>.<verb>` says so outright. When the controller does not answer, the list
names `mesh-controller (seat)` as not answering and carries the modules' tools regardless. The
`mesh-controller` *module* is always named as not answering: it serves no module tools, only its seat's.
**These already answer directly, not as a stated future.** Beside every assigned module's tools,
the tool runner asks the `mesh-controller` seat for its own verbs the same way (ADR 0154): measured
live, the mesh-controller runtime is one instance, mesh-wide, and it answers 63 verbs for that one
seat (`status`, `push`, `assign`, `build`, `artifacts` and the rest), and the `mesh-controller`
*module* answers none of its own — it serves no module tools, only its seat's, exactly as design 33
described while it was still only designed. The 56-to-93 seat-verb counts each node's tool runner
answers (below) are a different thing: the tool runner serves the verbs of whichever node-scoped
seats are held on that node (`node-backup`, `node-build-agent`, `node-service-manager` among them),
on behalf of the modules holding them, not the mesh-controller seat's — that one is served by the
mesh-controller runtime itself, a separate process, and the tool runner only asks it and relays what
it answers.
## Around it
## Where it runs
- **`invokes`** in a manifest is the grant. It is composed into the bus's user list exactly as a
person's account is; the mesh MCP server is the only module that declares it.
- **`module check <file|dir>…`** on the controller's binary judges a manifest with no mesh: the strict
parse, every per-manifest problem, and the rules between the manifests given. It prints what it cannot
judge without a store rather than refusing. The mesh MCP server's own manifest was the first thing checked
with it, and the whole catalogue passes.
- **The person's client remains.** `operator issue` and `mesh tools|call|mcp` with a credential file
still work, for a machine that is not a node and for a mesh not yet able to assign anything.
`mesh tools --console <url>` goes through a running mesh MCP server with no credential; it is covered by the
runtime repository's tests and was not exercised on the live mesh.
**Every node, not a per-operator assignment.** Measured live: all four of the mesh's machines
answer a `node-tools` runtime, each with its own module and tool count (between 294 and 312 tools,
56 to 93 seat verbs, in this reading). This is a fact that shipped past the design it came from: the
to-be document still describes it as assigned to "whichever machines an operator sits at"; what
actually runs is the opposite — every node the mesh holds carries one, because it is core
infrastructure the mesh itself depends on (discovery, the controller's own verbs, every module's
tools), not a convenience assigned to a workstation. There is accordingly no assign or unassign verb
for it: removing it from a node is not an operation the controller or the catalogue exposes.
## What shipped bent
## What this repository's prior text got wrong
- A module registered by hand from the catalogue with `--source <url> --path modules/<m>` records a URL,
not a place on the git seat: `--self` takes the forge path form (`<owner>/<repository>`), which the
operator did not pass. The rebuild-on-merge matched the URL anyway.
- Modules whose upgrade policy is *record* — the forge among them — answered `tools` only once
something pushed their rebuilt runtime; until then they are listed as not answering while still
callable. That is the policy doing what it says, not a fault of the mesh MCP server.
The document this replaces described `mesh-console` as a catalogue module served by a container and
assigned only to workstations. That module and its container are gone; the manifest is not — it
moved to `node-tools/module.json`, still declares `invokes: ["*"]`, and gained `waits` and the
`mcp-endpoint` provision, described above. What also moved is which language builds it: the
manifest's `build.artifacts` names a Go binary built from `cmd/node-tools`, and that is what a node
runs. The repository still carries the earlier TypeScript sources (`node-tools/src/mesh.ts`,
`http.ts`, `runtime.ts`, `announce.ts`, `broker-nats.ts`, `client.ts`, `launch.ts`, `main.ts`,
`mcp.ts`) on main, but the manifest does not build from them. The quirks the prior text recorded
from the `mesh-console` era (a module registered by hand recording its source as a URL; a
*record*-policy module staying listed as not-answering until its rebuilt runtime is pushed) are not
re-checked here: they were about the `mesh-console` container's registration path, which no longer
exists.
## How it is checked
- `mesh_overview` and `mesh_machine <node>`: the live tool and seat-verb counts, and which modules
are named not-answering.
- `mesh_runtimes`: which runtimes answered discovery, how long they took, how large their
announcement was, and whether it was shortened to fit the bus — read this when an address is said
to be missing or on another machine.
- `mesh_describe <address>` for one tool's schema; a call through `mesh_call` reaches the module or
seat over the bus and the answer is its own, unshaped.
- The code: `mesh-tools` (`node-tools/internal/console`, `internal/announce`, `internal/bus`),
`mesh-host` and `mesh-controller`'s core-component handling of `node-tools` (witness, lease,
bus-user naming). Their unit tests hold the loopback-only refusal, the seven-tool handshake, the
discovery gather window and `mesh_wait`'s event-then-read-state rule.
- `knowledge-base.get` with `kind: decision` and number 152, 175, 195, 197, 367 or 154.
+15 -8
View File
@@ -1,11 +1,13 @@
---
layer: to-be
status: designed
code: [mesh-catalog, mesh-tools, mesh-controller]
updated: 2026-10-03
status: implemented
code: [mesh-tools, mesh-host, mesh-controller]
updated: 2026-10-17
decisions:
- "ADR 0197"
- "ADR 0195"
- "ADR 0175"
- "ADR 0367"
- "ADR 0160"
- "ADR 0159"
- "ADR 0152"
@@ -163,11 +165,16 @@ matched it by URL.
## What this does not settle
- Narrowing a mesh MCP server's grant per assignment. ADR 0046 makes it a setting; nothing reads one yet.
- The mesh's own verbs on the bus. Design 33's third family; this document only says where they appear
once they exist.
- A person's identity behind the mesh MCP server. The mesh sees the mesh MCP server's account; design 15 keeps the
question open.
- **The mesh's own verbs on the bus.** Design 33's third family: these now answer directly, through
the `mesh-controller` seat, under ADR 0154 — settled, not open. [As-is 13](../00-as-is/13-the-console.md)
records it live.
- **Narrowing the tool runner's grant per assignment.** ADR 0046 makes it a setting; nothing reads
one yet. Owned by [design 58](58-agent-work-as-the-meshs-own.md) milestone 6 (issue 883): an
identity asked for each assignment is one of that milestone's parts.
- **A person's identity behind the mesh MCP server.** The mesh still sees only the tool runner's own
account, never the caller's; design 15 raised the question and leaves it open. Owned by the same
milestone (design 58 milestone 6, issue 883): its outcome is that every person, agent and module
becomes an identity with a role, and a call carries which one made it.
## References