A snapshot: read at commit 707a04c7ce8 on 2026-10-18T00:06:01Z by majordomus 0.29.0. Nothing on this page is read again. See it live
adr-0001
A Rust executable with a data-driven, read-only MCP surface over stdio
What it is
- URI
majordomus://adr/adr-0001- Kind
adr- Identity
adr-0001- Path
.ai/repo/adrs/0001-rust-cli-and-stdio-mcp.md- Provenance
- declared
- Media type
text/markdown- As a capability
- adr.adr-0001
Front matter
{
"schema": "adr/v1",
"id": "adr-0001",
"kind": "adr",
"title": "A Rust executable with a data-driven, read-only MCP surface over stdio",
"status": "accepted",
"date": "2026-09-05",
"tags": [
"rust",
"mcp",
"architecture"
],
"provenance": {
"origin": "authored"
}
}Content
---
schema: adr/v1
id: adr-0001
kind: adr
title: A Rust executable with a data-driven, read-only MCP surface over stdio
status: accepted
date: 2026-09-05
tags:
- rust
- mcp
- architecture
provenance:
origin: authored
---
# 1. A Rust executable with a data-driven, read-only MCP surface over stdio
## Context
Majordomus is a shell tool that validates, projects and enforces the repository's `.ai/`
layer. AI clients that speak the Model Context Protocol cannot read a shell tool; they
spawn a process and speak JSON-RPC to it. The repository's own documents refused an "MCP
surface" (README "What this is not", `CONTRIBUTING.md`, `docs/DESIGN.md` deferred list,
`docs/EXTRACTION_REPORT.md`) on the evidence that no daemon, server, database or queue
had solved a problem the file-based mechanisms had not. That evidence stands; what it
argued against was infrastructure, not a reader.
The operator asked for a first-class Rust executable, `majordomus`, whose first command
serves the layer to MCP clients, with the structure of what is served derived from the
repository's data rather than written into the code.
## Decision
- One Rust package, `apps/majordomus-cli/`, building a binary named `majordomus`, with
`clap` for the command line and one command, `mcp`. `apps/` is new: the executable is a
product surface, not a helper, so it does not belong under `scripts/` or `bin/`.
- Transport: stdio only, one process per client, alive as long as the client's pipe.
No port, no daemon, no state, no database, no network.
- Read-only, and proven so: a session leaves `git status` and every tracked blob
byte-identical.
- Data-driven: the root is the nearest `.ai/manifest.yaml`; the manifest names the
sections; `.ai/repo/knowledge/sources.yaml` maps pathspecs to kinds and discovery goes
through the git index as the knowledge contract prescribes; key contracts are the same
`share/allow/*.txt` the shell tool uses, embedded at build time; the executable ships
one declarative file of its own, `schema/kinds.yaml`, saying how each kind is read.
- The YAML reader implements the layer's documented subset, not general YAML, so both
executables accept and refuse the same files.
- No MCP library: the read-only subset a server needs is eight methods, kept in one
file behind the index, so that no third-party type reaches the domain and a library
can replace that file later without touching discovery or metadata.
- Failure policy, decided once: the manifest and `sources.yaml` are errors; any other
file that cannot become an object is excluded with a named diagnostic and the index is
`degraded` and still serves; `--strict` refuses a degraded index.
## Invariant
Rust carries mechanism: read a format, validate keys against a list, take an identity
from named fields, speak the protocol. The repository carries the knowledge: which files,
which kinds, which keys. Adding an object of a known kind, or a new class of a known kind,
is a data change; adding a kind is a schema entry plus, when the kind needs a new format
or identity rule, code.
## Against the "Intentionally Absent" list, point by point
| the list refuses | this decision |
|---|---|
| a daemon, server, background monitor | none: the process is the client's child and dies with it |
| a database, queue, vector store | none: one in-memory index per invocation |
| a registry or catalogue of named workers | none: the index holds the repository's own declared objects, nothing of its own |
| a finding without a reproduce command | every diagnostic carries a stable code and the path; `mcp --inspect` reproduces it |
| a self-report trusted without an independent check | `test/cases/72_rust_mcp.sh` speaks to the built binary over pipes in a repository `init` wrote |
| a number written where a command could compute it | the executable computes every count it reports |
## Alternatives rejected
- **Extend the shell tool with an MCP command.** JSON-RPC framing, a JSON serialiser and
a long-lived stdin loop in bash 3.2 would repeat the hand-rolled serialiser the
extraction report rejected; the shell tool's portability floor is a strength for hooks
and a liability for a protocol server.
- **Python.** No Python is in the repository's toolchain or CI; adding an interpreter
dependency for one command contradicts "nothing else" in the requirements.
- **A separate MCP product or daemon.** A second process with its own lifecycle is the
infrastructure the design refuses, and would need its own reader of the layer.
- **A hardcoded tool and resource table in Rust.** Would drift from the layer on the
first new rule and is exactly the second source of truth `28_no_hardcoded_values.sh`
exists to prevent.
- **An MCP crate.** The official Rust SDK is maintained and would work; it brings an
async runtime, a schema library and macros for a server that needs eight read-only
methods, and its types would sit where the domain's should. Reversible: the protocol
file is the only place wire shapes live.
- **A general YAML crate.** Accepts what the layer refuses (anchors, block scalars) and
refuses what the layer accepts (an unquoted scalar carrying `: `, a backtick-led
value); the two executables would disagree about the same file.
- **Any Prismatic dependency.** Refused by the clean-room rule; none exists at compile,
run, deployment or operational level.
## Consequences
Positive: one executable an MCP client can spawn; the surface grows with the layer
without a code change; every claim has a black-box test; provider-neutral by
construction, since provider names appear nowhere in the code.
Negative and accepted: `schema/kinds.yaml`, the resource URI scheme, the tool names and
the diagnostic codes become a compatibility surface; the executable embeds
`share/allow/` and so builds only from inside this repository; two executables now read
the layer, and `72_rust_mcp.sh` is the check that they agree; the documents that refused
an MCP surface now distinguish infrastructure (still refused) from a stdio reader.
## Unresolved, recorded rather than invented
Hierarchical context (root `README.md`, `AGENTS.md`, provider files, and whatever a
subdirectory may carry) has no merge semantics on `master`. The executable records each
document's directory and composes nothing. When the repository defines the contract, the
executable reads it from data like everything else.
## Testing strategy
Black-box first: the compiled binary is spawned for the CLI, discovery, the protocol, and
the add–remove–break extension sequence; the index is exercised through the library only
for determinism across the two enumerations. `test/cases/72_rust_mcp.sh` is the
cross-check against the shell tool's own skeleton, and CI runs `cargo fmt --check`,
`clippy -D warnings` and `cargo test` beside the behavioural suite.