Files
hq/02-DECISIONS/0188-a-modules-own-code-is-bundles-in-any-language-and-a-tools-bundle-speaks-mcp-to-the-runtime.md
T
jschoubben da8b4b4ee4 Renumber to ADR 0188: 0187 landed on main first, as the dead-tracker record
Two records shared 0187 (issue 155's collision); the branch landing last
renumbers, and this is it. Only the number changes.
2026-10-02 21:01:02 +02:00

8.4 KiB

topic, status, date, deciders, reconstructed, extends
topic status date deciders reconstructed extends
what runs on it accepted 2026-10-02 jochen false 02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md

188. A module's own code is bundles in any language, and a tools bundle speaks MCP to the runtime

Context

ADR 0175 put one tool runtime on every node and said a module brings its tools as a bundle. The runtime that exists is written in TypeScript and brings a bundle to life by importing it into its own process, which only JavaScript can be. The SDK (ADR 0039) is one TypeScript package. The builder knows three toolchains — TypeScript, Go, Python — and every one of the 35 catalogue modules with tools wraps them in a container on the runtime's TypeScript image. Nothing in the records says a module's code may be written in anything else, and nothing refuses a module that wraps its own code in an image to get around that.

The operator's direction, stated on 2026-10-02 and repeated: the SDK is the most important part; we must not limit developers; tools can be written in any possible language — Rust, C, Go, JavaScript. A service in Go or Rust as a systemd unit must be possible too. One module can deliver all kinds of bundles: one for its tools, one for a seat's implementation, one for a daemon. Support the bare minimum first, as a skeleton; a full implementation comes when the work requires it.

Measured against that: the bundle artifact kind already names a language and the process resource already runs a command from an unpacked bundle as a unit the host writes (ADR 0150), so a Go daemon as a native service is possible today and one module in the catalogue does it. What is not possible is a tool in any language but one, and what is not written is that any of this is the rule.

Considered Options

  1. One SDK, one language, as now. Rejected: it limits who can write a module to one ecosystem, which the operator declines, and it is what made every module's tools a container on one image.
  2. A full bus client per language. Each SDK speaks the bus itself; the runtime only supervises. Rejected: a transport in every SDK is what ADR 0039 refuses, and a bus change would then rebuild every module in every language — the cascade, multiplied.
  3. A tools bundle is a process the runtime launches and speaks a small local protocol to, and that protocol is MCP over stdio. Chosen. The runtime already speaks MCP outward (the console); speaking it inward to a child process is the same vocabulary. Every language that has an MCP server library can write a tools bundle today with no mesh SDK at all, and the mesh's own SDK for a language is a thin convenience over it. The transport stays in the runtime, so a bus change rebuilds nothing.
  4. A protocol of the mesh's own design. Rejected: a second way to describe a tool, its schema and its call, inventing what MCP already settled, for no gain.

Decision

1. A module's own code is bundles, in any language the mesh has a toolchain for, and never an image. A bundle names its language and what it is for. Images are for third-party software a module installs — a database, a forge — never for code the module wrote. One module may declare several bundles: its tools, its implementation of a seat's verbs, a daemon, a step. Each is built alone and delivered alone, as ADR 0156 already has it.

2. A bundle the runtime serves is a process that speaks MCP over stdio. The node's runtime launches it as the bundle names it — an interpreter and a file, or a binary — with the runtime's environment, asks tools/list, and answers each call on the bus by tools/call. A tool whose name is <seat>.<verb> is the module's implementation of that seat's verb; any other name is the module's own tool. Everything the runtime does with what it is told — subjects from the membership, a held seat's verbs, the tools answer, a bundle that fails named and the others serving — stays as ADR 0175 and ADR 0160 have it. A TypeScript bundle may still be imported into the runtime's own process; that is a shortcut over the same contract, not a second contract, and a TypeScript bundle written against the protocol is served the same way as any other.

3. A bundle that is a service is a process, run by the host as a unit, in whatever language it is compiled from, exactly as the host's own bundle already is. Nothing new is decided here; it is said so that it is the rule and not an example.

4. One thin SDK per language, and the test of ADR 0039 applies to each. An SDK for a language holds the MCP-over-stdio loop, the tool-definition type and the few primitives a module's code needs; it holds no transport, no module's client and nothing volatile. Where a language has a sound MCP library, the SDK wraps it rather than re-implementing it. The languages are those that make sense to write a module in; the first set is TypeScript, Go, Python, Rust and C, and the set grows when a module needs one, not before.

5. Skeleton first. Each piece — a toolchain, a launcher, an SDK — exists at the bare minimum that lets one bundle in that language be built, delivered and answer one tool on the live mesh. Anything beyond that is added when a module needs it. A skeleton that is not proven by one bundle answering is not a skeleton; it is a promise.

Consequences

  • The runtime gains a launcher beside its loader. The loader, the memberships, the seats and the failure handling built for ADR 0175 stand; the launcher is the one new step.
  • The builder gains a toolchain per language, each at the skeleton: compile, pack, name the entrypoint. Rust and C are new; a language that compiles to a binary says its operating system as a Go bundle already does.
  • An existing MCP server in any language is already a valid tools bundle. What the mesh adds is the subjects, the seats and the memberships around it.
  • The gate to-be 38 WP2 adds — refusing a tools container built on the runtime's image — widens: a module whose own code is an image artifact is refused at registration, naming this record.
  • What got harder: a tools bundle is now a process per module on the node rather than code in one process, so the runtime supervises children and restarts one that dies. The one-process shape ADR 0175 counted on for the TypeScript shortcut remains available for it.
  • ADR 0039's "what the SDK holds" now reads per language; its refusals are unchanged and are the reason option 2 was rejected.

How it is checked

Rule Checked by
A module's own code is never an image the catalogue's registration check: a manifest with a bundle kind of own code and an image artifact built from the module's own directory is refused, naming this record
A tools bundle in a language other than TypeScript answers on the bus the runtime's tests: a bundle written against the protocol in a second language, launched, its tool called over a real bus
A TypeScript bundle written against the protocol is served like any other the same tests, with the TypeScript shortcut off
Each SDK is thin each SDK's own README states what it holds under ADR 0039's test, and its size is in the mesh's records
Live a tool in a compiled language answers from the node's runtime on one machine

References