# Rust Tool Base (RTB)
**A batteries-included application framework for building Rust CLI tools: idiomatic, composable and AI-ready.**
> **Status:** pre-1.0. The crates are implemented and published, but the
> public API is not frozen, so expect breaking changes within 0.x. The
> authoritative contract for each subsystem lives in
> [`0008-rust-tool-base`](https://gitlab.com/phpboyscout/rust-tool-base/-/wikis/specs/0008-rust-tool-base).
RTB is a Rust sibling of [Go Tool Base (GTB)](https://gitlab.com/phpboyscout/go-tool-base).
It is **not** a line-for-line port. Go Tool Base leans on Go's idioms
(`context.Context`, functional options, package-level `init()`,
`interface{}`-keyed containers); RTB leans on Rust's (`?` + `miette`,
typestate builders, `linkme` distributed slices, strongly-typed `serde`
config, `tokio` structured concurrency).
## What RTB gives you
* **Opinionated application scaffolding.** `Application::builder()` is a
typestate assembler that wires clap, figment, tracing, miette, tokio and
a signal-bound `CancellationToken` in one call.
* **Strongly-typed layered config.** Env vars > user files > embedded
defaults, materialised into *your* `serde::Deserialize` struct. Hot-reload
(the `hot-reload` feature of `rtb-config`) uses `notify` and `arc-swap`,
with a `watch::Receiver` for observers.
* **Embedded assets with overrides.** `rtb-assets` layers files compiled
in with `rust-embed`, directories on disk and in-memory maps behind one
read-only `Assets` API. A read takes the file from the last-registered
layer that has it; YAML and JSON can instead be deep-merged across every
layer (RFC 7396 merge-patch).
* **Diagnostic errors, not error handlers.** Every crate derives
`thiserror::Error + miette::Diagnostic`; `main()` returns
`miette::Result<()>` and a framework-installed hook renders them with
labels, help, and severity.
* **Self-update.** `self-replace` plus minisign-signed releases (the
prehashed `ED` variant, `ed25519(BLAKE2b-512(archive))`), verified with
`minisign-verify`. That is the crate `cargo-binstall` uses, so one
signature serves both. The staged binary is self-tested before an
atomic swap.
* **Release sources and git.** `rtb-forge` reads releases from GitHub,
GitLab, Bitbucket, Gitea, Codeberg or a bare HTTPS URL, which is where
`rtb update` looks for a new version, and its `Repo` type runs git
operations on `gix`.
* **AI.** `genai` covers OpenAI, Gemini, Ollama and OpenAI-compatible
endpoints; Anthropic goes through a direct `reqwest` client so prompt
caching, extended thinking and citations work. Structured output is
validated with `jsonschema`.
* **MCP.** Built on the official `rmcp` SDK. A command opts in by
overriding `Command::mcp_exposed()` to return `true`, and can describe
its arguments to MCP clients through `Command::mcp_input_schema()`.
`mcp serve` (stdio by default) and `mcp list` ship as built-in
subcommands.
* **TUI docs browser.** `ratatui` with `tui-markdown`, plus streaming AI Q&A.
* **Credentials.** `keyring` wrapped in a `CredentialStore` trait, with
`secrecy::SecretString` end-to-end, zeroed on drop.
* **Telemetry.** Opt-in, salted `machine-uid`, pluggable sinks
(noop, file, HTTP, OTLP).
## Workspace layout
Only the umbrella crate and the `rtb` binary live here. Every other
crate is a standalone module of the
[phpboyscout Rust toolkit](https://rust.phpboyscout.uk), consumed here
as an ordinary dependency:
* **Foundations:** `rtb-error`, `rtb-redact`, `rtb-config`,
`rtb-assets`, `rtb-app`, `rtb-tui`, `rtb-credentials`,
`rtb-telemetry`, `rtb-test-support`, `rtb-chat` (formerly `rtb-ai`),
`rtb-forge` (formerly `rtb-vcs`).
* **CLI family:** `rtb-cli`, `rtb-update`, `rtb-docs`, `rtb-mcp`,
in [`phpboyscout/rust/cli`](https://gitlab.com/phpboyscout/rust/cli).
```text
rust-tool-base/
├── Cargo.toml # workspace root
├── crates/
│ ├── rtb/ # umbrella crate (public API)
│ └── rtb-cli-bin/ # the `rtb` scaffolder binary
├── examples/
│ └── minimal/ # smoke-test tool
├── docs/ # user-facing documentation
│ # (specs live in the project wiki)
├── .gitlab-ci.yml # CI + the tagged `dist` release stage
├── deny.toml # cargo-deny policy
├── rustfmt.toml
├── rust-toolchain.toml # pinned stable
├── justfile # common tasks
├── LICENSE # MIT
└── SECURITY.md
```
## Quick start
A working end-to-end example lives at
[`examples/minimal`](examples/minimal/src/main.rs). Run it with:
```console
$ cargo run -p rtb-example-minimal -- version
$ cargo run -p rtb-example-minimal -- greet
$ cargo run -p rtb-example-minimal -- doctor
$ cargo run -p rtb-example-minimal -- --help
```
### Anatomy
A minimal RTB-based tool has three parts: (1) a custom `Command`
implementation, (2) a `linkme` registration that inserts it into the
framework's command slice, and (3) an `Application::builder()` call
in `main`.
```rust,no_run
use async_trait::async_trait;
use linkme::distributed_slice;
use rtb::core::app::App;
use rtb::core::command::{BUILTIN_COMMANDS, Command, CommandSpec};
use rtb::prelude::*;
// 1. Implement the Command trait.
struct Greet;
#[async_trait]
impl Command for Greet {
fn spec(&self) -> &CommandSpec {
static SPEC: CommandSpec = CommandSpec {
name: "greet",
about: "Print a friendly greeting",
aliases: &["hi"],
..CommandSpec::DEFAULT
};
&SPEC
}
async fn run(&self, app: App) -> miette::Result<()> {
println!("hello from {}", app.metadata.name);
Ok(())
}
}
// 2. Register into the framework's command slice at link time.
#[distributed_slice(BUILTIN_COMMANDS)]
fn register_greet() -> Box<dyn Command> { Box::new(Greet) }
// 3. Wire the Application.
#[tokio::main]
async fn main() -> miette::Result<()> {
rtb::cli::Application::builder()
.metadata(
ToolMetadata::builder()
.name("mytool")
.summary("my CLI tool")
.build(),
)
.version(version_info!())
.build()?
.run()
.await
}
```
`version_info!()` has to be a macro: it expands `env!("CARGO_PKG_VERSION")`
at *your* call site, so it reports your tool's version. A function
couldn't: its `env!` would expand inside the framework and report the
framework's version instead.
Downstream tools need the framework, `tokio`, `miette`, `async-trait` and
`linkme` as direct dependencies. The framework publishes as
`rust-tool-base` and the examples import it as `rtb`:
```toml
[dependencies]
rtb = { package = "rust-tool-base", version = "0.9" }
```
`linkme` must be direct because its `#[distributed_slice]` attribute
expands to `::linkme::…` paths.
## Development
```bash
just ci # fmt, clippy, rustdoc, tests, cargo-deny, coverage
just check # cargo check, all targets, default features
just rtb -- --help # run the scaffolder CLI
```
`just pre-push` is the merge-request gate without coverage. AGENTS.md
explains why it needs `cargo-nextest`.
## License
MIT. See [LICENSE](LICENSE).