Merge pull request 'Describe the mesh MCP server as it runs and mark its design implemented (issue 1008)' (#400) from fix/1008-mcp-server-shipped into main
This commit was merged in pull request #400.
This commit is contained in:
@@ -1,69 +1,144 @@
|
|||||||
---
|
---
|
||||||
layer: as-is
|
layer: as-is
|
||||||
status: implemented
|
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]
|
code: [mesh-tools, mesh-host, mesh-controller]
|
||||||
updated: 2026-10-10
|
updated: 2026-10-17
|
||||||
decisions:
|
decisions:
|
||||||
- "ADR 0152"
|
- "ADR 0152"
|
||||||
- "ADR 0095"
|
- "ADR 0175"
|
||||||
- "ADR 0037"
|
- "ADR 0195"
|
||||||
|
- "ADR 0197"
|
||||||
|
- "ADR 0367"
|
||||||
- "ADR 0154"
|
- "ADR 0154"
|
||||||
---
|
---
|
||||||
|
|
||||||
# The mesh MCP server, as it runs
|
# The mesh MCP server, as it runs
|
||||||
|
|
||||||
**The mesh's tools reach a person through a module the mesh assigned to their machine.** Since
|
**The `mesh-console` container no longer exists.** Read 2026-10-17 from `mesh-tools` (package
|
||||||
2026-09-30 a workstation that is a node can be assigned `mesh-console`; the mesh mints a bus account
|
`node-tools`), `mesh-host` and `mesh-controller` on their forges' main, and from the live mesh
|
||||||
`<node>.mesh-console`, seals its credential to the machine, and the container binds
|
through `mesh_overview`, `mesh_machine`, `mesh_runtimes` and `mesh_describe`. Checked: the
|
||||||
`127.0.0.1:<port>` with the port the mesh assigned for the manifest's declared one. An agent on the
|
catalogue (`mesh-catalog`) carries no module named `mesh-console` any more — the name survives only
|
||||||
machine is pointed at `http://127.0.0.1:<port>/mcp` and sees the mesh's tools; a person uses the same
|
in the glossary's retired-words list and in a test fixture — and none of the four nodes answers a
|
||||||
endpoint. Nothing on the machine holds a credential a person had to carry.
|
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
|
## What it answers
|
||||||
|
|
||||||
`initialize`, `tools/list`, `tools/call`, over HTTP, one JSON body per request, no session and no event
|
`initialize`, `notifications/initialized`, `ping`, `tools/list`, `tools/call`, over HTTP, one JSON
|
||||||
stream. `tools/list` is what the running modules answered: every tool runner built on or after that day
|
body per request, no session and no event stream (`node-tools/internal/console/mcp.go`).
|
||||||
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.
|
|
||||||
|
|
||||||
`tools/call` reaches any tool by `<module>.<tool>`, listed or not. The mesh MCP server's grant is `*`, so what it
|
**It announces seven tools, not five** (ADR 0195, extended by ADR 0367): `mesh_overview`,
|
||||||
may call is every tool on the mesh; its account may publish nothing else and subscribes nothing.
|
`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
|
**Addresses, exactly as the handshake states them:** `<seat>.<verb>` for a seat held once for the
|
||||||
it, and cannot tell an agent from the operator (ADR 0259 §6, issue 340). An act waits for the operator's warrant
|
mesh (`mesh-controller.status`); `<node>/<seat>.<verb>` for a seat every machine holds
|
||||||
only where its module asks first, in its own code: the agent module's instruction proposals, and the
|
(`<node>/node-packet-filter.rules`); `<node>/<module>.<tool>` for a module assigned to one machine;
|
||||||
controller's settings proposals and the actions it offers on a condition.
|
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
|
## The mesh's own verbs
|
||||||
|
|
||||||
*Since 2026-09-30 evening (ADR 0154).*
|
**These already answer directly, not as a stated future.** Beside every assigned module's tools,
|
||||||
The mesh MCP server asks the `mesh-controller` seat's `tools` verb beside the modules and lists every role's
|
the tool runner asks the `mesh-controller` seat for its own verbs the same way (ADR 0154): measured
|
||||||
tools as `<seat>.<verb>` — `mesh-controller.status`, `mesh-controller.push` and the other ten. A call
|
live, the mesh-controller runtime is one instance, mesh-wide, and it answers 63 verbs for that one
|
||||||
to `<prefix>.<name>` reaches the seat when the prefix is a seat declaring that verb, and the module
|
seat (`status`, `push`, `assign`, `build`, `artifacts` and the rest), and the `mesh-controller`
|
||||||
otherwise; `seat:<seat>.<verb>` says so outright. When the controller does not answer, the list
|
*module* answers none of its own — it serves no module tools, only its seat's, exactly as design 33
|
||||||
names `mesh-controller (seat)` as not answering and carries the modules' tools regardless. The
|
described while it was still only designed. The 56-to-93 seat-verb counts each node's tool runner
|
||||||
`mesh-controller` *module* is always named as not answering: it serves no module tools, only its seat's.
|
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
|
**Every node, not a per-operator assignment.** Measured live: all four of the mesh's machines
|
||||||
person's account is; the mesh MCP server is the only module that declares it.
|
answer a `node-tools` runtime, each with its own module and tool count (between 294 and 312 tools,
|
||||||
- **`module check <file|dir>…`** on the controller's binary judges a manifest with no mesh: the strict
|
56 to 93 seat verbs, in this reading). This is a fact that shipped past the design it came from: the
|
||||||
parse, every per-manifest problem, and the rules between the manifests given. It prints what it cannot
|
to-be document still describes it as assigned to "whichever machines an operator sits at"; what
|
||||||
judge without a store rather than refusing. The mesh MCP server's own manifest was the first thing checked
|
actually runs is the opposite — every node the mesh holds carries one, because it is core
|
||||||
with it, and the whole catalogue passes.
|
infrastructure the mesh itself depends on (discovery, the controller's own verbs, every module's
|
||||||
- **The person's client remains.** `operator issue` and `mesh tools|call|mcp` with a credential file
|
tools), not a convenience assigned to a workstation. There is accordingly no assign or unassign verb
|
||||||
still work, for a machine that is not a node and for a mesh not yet able to assign anything.
|
for it: removing it from a node is not an operation the controller or the catalogue exposes.
|
||||||
`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.
|
|
||||||
|
|
||||||
## 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,
|
The document this replaces described `mesh-console` as a catalogue module served by a container and
|
||||||
not a place on the git seat: `--self` takes the forge path form (`<owner>/<repository>`), which the
|
assigned only to workstations. That module and its container are gone; the manifest is not — it
|
||||||
operator did not pass. The rebuild-on-merge matched the URL anyway.
|
moved to `node-tools/module.json`, still declares `invokes: ["*"]`, and gained `waits` and the
|
||||||
- Modules whose upgrade policy is *record* — the forge among them — answered `tools` only once
|
`mcp-endpoint` provision, described above. What also moved is which language builds it: the
|
||||||
something pushed their rebuilt runtime; until then they are listed as not answering while still
|
manifest's `build.artifacts` names a Go binary built from `cmd/node-tools`, and that is what a node
|
||||||
callable. That is the policy doing what it says, not a fault of the mesh MCP server.
|
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.
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
---
|
---
|
||||||
layer: to-be
|
layer: to-be
|
||||||
status: designed
|
status: implemented
|
||||||
code: [mesh-catalog, mesh-tools, mesh-controller]
|
code: [mesh-tools, mesh-host, mesh-controller]
|
||||||
updated: 2026-10-03
|
updated: 2026-10-17
|
||||||
decisions:
|
decisions:
|
||||||
- "ADR 0197"
|
- "ADR 0197"
|
||||||
- "ADR 0195"
|
- "ADR 0195"
|
||||||
|
- "ADR 0175"
|
||||||
|
- "ADR 0367"
|
||||||
- "ADR 0160"
|
- "ADR 0160"
|
||||||
- "ADR 0159"
|
- "ADR 0159"
|
||||||
- "ADR 0152"
|
- "ADR 0152"
|
||||||
@@ -163,11 +165,16 @@ matched it by URL.
|
|||||||
|
|
||||||
## What this does not settle
|
## 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: these now answer directly, through
|
||||||
- The mesh's own verbs on the bus. Design 33's third family; this document only says where they appear
|
the `mesh-controller` seat, under ADR 0154 — settled, not open. [As-is 13](../00-as-is/13-the-console.md)
|
||||||
once they exist.
|
records it live.
|
||||||
- A person's identity behind the mesh MCP server. The mesh sees the mesh MCP server's account; design 15 keeps the
|
- **Narrowing the tool runner's grant per assignment.** ADR 0046 makes it a setting; nothing reads
|
||||||
question open.
|
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
|
## References
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user