Skip to content

v0.3 scope — rtb-ai + rtb-mcp

Status: IMPLEMENTED — both slices have landed (rtb-ai v0.1 and rtb-mcp v0.1 per-crate specs). Parent contract: rust-tool-base.md §16 Roadmap. Driver: unblocking the AI-Q&A path for rtb-docs and the MCP-server path for tools that want to expose their commands to AI agents.


1. Motivation

v0.2 closed the "framework basics" loop — config / telemetry / docs / update / VCS releases — but stopped at the seams for AI and MCP:

  • rtb-docs ships an empty AiAnswerStream trait gated on the ai feature. docs ask returns DocsError::AiDisabled until v0.3 lands.
  • rtb-cli ships an mcp stub that returns FeatureDisabled.
  • The rtb umbrella defines ai and mcp features that currently activate empty placeholder crates.

v0.3 fills both seams.

2. In scope

2.1 rtb-ai v0.1 — multi-provider chat client + structured output

A unified AiClient over genai (multi-provider abstraction) plus a direct Anthropic Messages API path for features genai does not surface. Per CLAUDE.md and the framework spec, the Anthropic-direct path is mandatory because genai doesn't yet support:

  • Prompt caching at every stable point (system prompt, tools, static context).
  • Extended thinking ("Claude thinking" tokens).
  • Managed agents.
  • Citations.

Provider matrix:

Provider Path Why
Anthropic Claude (Cloud + Local) direct reqwest against the Messages API full feature surface (cache / thinking / agents / citations)
OpenAI (and OpenAI-compatible — Together / Fireworks / etc.) genai function calling + chat completions
Google Gemini genai Gemini 2.0+
Ollama (and any HTTP-compatible local model) genai local dev / airgap

Default model: Claude 4.7 (Opus 4.7 / Sonnet 4.6 / Haiku 4.5) — per the standing CLAUDE.md guidance.

Public API shape:

pub struct AiClient { /* … */ }

#[derive(Debug, Clone)]
pub struct Config {
    pub provider: Provider,
    pub model: String,
    pub base_url: Option<url::Url>,
    pub api_key: SecretString,
    pub allow_insecure_base_url: bool,  // wiremock-only
}

pub enum Provider { Anthropic, AnthropicLocal, OpenAi, Gemini, Ollama, OpenAiCompatible }

impl AiClient {
    pub fn new(config: Config) -> Result<Self, AiError>;

    /// Chat completion. Returns the assistant message + token counts.
    pub async fn chat(&self, req: ChatRequest) -> Result<ChatResponse, AiError>;

    /// Streaming chat completion — yields tokens as they arrive.
    pub async fn chat_stream(&self, req: ChatRequest) -> Result<ChatStream, AiError>;

    /// Structured output. Validates the response against `T`'s
    /// `schemars::JsonSchema` before deserialising.
    pub async fn chat_structured<T: DeserializeOwned + JsonSchema>(
        &self,
        req: ChatRequest,
    ) -> Result<T, AiError>;
}

Anthropic-direct extras (only on the Anthropic/AnthropicLocal path):

  • ChatRequest::cache_control: bool — enables prompt caching at the system prompt + tools + first turn.
  • ChatRequest::thinking: Option<ThinkingMode> — extended-thinking token budget.
  • ChatResponse::citations: Vec<Citation> — populated when the assistant cites sources.
  • AiClient::run_agent(agent_def) -> AgentRun — managed agent loop (subsequent slice — see § Open questions O3).

Security:

  • Config::base_url runs through validate_base_url (mirrors rtb-vcs's policy): HTTPS-only, no userinfo, no example.com placeholder. allow_insecure_base_url is #[serde(skip)] so config files can't downgrade.
  • Config::api_key is SecretString; never logged. Resolved via rtb-credentials (which lands its hookup here too — see § 4.1).
  • Every successful AiClient::new logs the endpoint hostname at INFO. Path + query never logged.
  • All free-form strings written to telemetry (AiError::Provider(...), retry diagnostics) flow through rtb_redact::string first.

Test plan:

  • wiremock for HTTP-level tests (every provider).
  • genai already has its own test infrastructure — borrow patterns where applicable.
  • BDD scenario for the structured-output happy path + the schema-mismatch error path.

2.2 rtb-mcp v0.1 — MCP server that exports registered commands

rmcp is the official Rust MCP SDK; rtb-mcp is a thin layer over it that:

  1. Walks BUILTIN_COMMANDS for entries marked mcp_exposed: true (a new optional flag on CommandSpec — additive trait-method or struct field, TBD via Open questions O1).
  2. For each, derives the input schema via schemars from the command's clap::Args struct.
  3. Registers each command as an MCP tool whose call invokes the existing Command::run.

Transports at v0.1: stdio (default), SSE, streamable HTTP. All of those are already in rmcp.

Public API shape:

pub struct McpServer { /* … */ }

#[derive(Debug, Clone)]
pub enum Transport { Stdio, Sse { bind: SocketAddr }, Http { bind: SocketAddr } }

impl McpServer {
    pub fn new(app: App, transport: Transport) -> Self;

    /// Run the server. Returns when `app.shutdown` fires or the
    /// transport closes.
    pub async fn serve(self) -> Result<(), McpError>;
}

mcp CLI subcommand (new in v0.3):

mytool mcp serve [--transport stdio|sse|http] [--bind 127.0.0.1:0]
mytool mcp list   # print every registered tool + its schema

Test plan:

  • rmcp ships an in-process test client; use it.
  • BDD: "Given a tool with one MCP-exposed command, When I call it via the test client, Then the command's Command::run body executes and the response shape matches the schema."

2.3 Hookup work in existing crates

Necessary cross-crate plumbing — not new crates, but blocking v0.3:

  • rtb-docs: complete crate::ai::AiAnswerStream impl backed by rtb_ai::AiClient. docs ask becomes functional (gated on the ai feature).
  • rtb-update: PAT auth. Today update runs unauthenticated — wire rtb-credentials::Resolver so private repos work. (Was deferred from v0.2 #18.)
  • rtb-cli: replace the McpStub placeholder with the real rtb-mcp registration; the umbrella's mcp feature flips to non-default → default-yes once shipped.
  • rtb-app::ReleaseSource: expand to the six variants in rtb-vcs::ReleaseSourceConfig (Bitbucket / Gitea / Codeberg). Was queued from #18's known issues.

3. Out of scope

  • rtb-tui — the Wizard / tables / spinners crate. Targeted v0.4. rtb-docs already uses ratatui directly; promotion to a shared crate happens once rtb-cli's init wizard work creates a second consumer.
  • MCP clientrtb-mcp v0.1 is server-only. A client that lets RTB-built tools consume other MCP servers is a v0.4 concern.
  • Local model weights bundled in the binary — out of scope per rtb-docs v0.1 spec § 2.6 and re-confirmed here. Local inference goes through Ollama or a self-hosted OpenAI-compatible endpoint.
  • Vector stores / RAG indexingrtb-docs already ships tantivy for text search; expanding to embeddings is its own crate.

4. Cross-cutting changes

4.1 rtb-credentials minor additions

A pre-implementation review of rtb-credentials v0.1 found the API already serves all three v0.3 consumers (rtb-vcs PAT, rtb-update auth, rtb-ai api_key) cleanly. The forecasted "consolidation" reduces to a small additive change folded into slice 1:

  • Resolver::with_platform_default() — convenience constructor that builds a Resolver over KeyringStore::new(). Saves the two-line Arc::new(KeyringStore::new()) boilerplate at every callsite.
  • Resolver::default()Default impl returning the same.

No standalone spec needed; documented inline in the rtb-ai v0.1 spec which is the first consumer.

4.2 Command::mcp_exposed (or equivalent)

To expose Command impls as MCP tools, we need a per-command opt-in. Options:

  • (a) Additive trait method fn mcp_exposed(&self) -> bool { false }. Pattern matches subcommand_passthrough. Most flexible.
  • (b) Field on CommandSpec. Breaking change — every existing static SPEC literal needs an extra field initialiser (27 sites). Avoided in v0.2 follow-ups.
  • © Marker trait McpExposed that downstream crates implement separately. Decouples MCP from the core trait but means the same command exists twice in two slices.

Recommendation: (a). Same trade-off we made on subcommand_passthrough in #17 — additive default method is the ergonomic minimum.

4.3 rtb umbrella feature wiring

[features]
default = ["cli", "update", "docs", "mcp", "credentials"]
mcp = ["rtb-mcp"]            # was empty — now activates the real crate
ai = ["rtb-ai"]              # was empty — now activates the real crate

mcp flips to default-yes once rtb-mcp ships, matching the update / docs pattern.

5. Slicing — two PRs

  1. rtb-ai v0.1 — bulk of v0.3. Multi-provider client (genai for OpenAI/Gemini/Ollama/OpenAI-compatible + Anthropic-direct path for caching/thinking/citations), structured output via schemars + jsonschema. Tucks in the Resolver::with_platform_default() helper (§4.1). Tucks in docs ask hookup. Tucks in the rtb-update PAT auth hookup.
  2. rtb-mcp v0.1 — server crate via rmcp + mcp CLI subcommand
  3. Command::mcp_exposed default trait method. Replaces the rtb-cli stub.

Each PR carries its own per-crate spec.

6. Open questions — resolved

All five questions resolved 2026-05-01:

  • O1Command::mcp_exposed ships as a default trait method (fn mcp_exposed(&self) -> bool { false }). Mirrors the subcommand_passthrough pattern landed in v0.2 — additive, no impact on the existing 27 impl Command sites.
  • O2Claude 4.7 is the literal default model. Tool authors can override via Config, but the unconfigured path picks the most-capable Claude family member. Less footgun for new tools.
  • O3Anthropic agents deferred to a v0.3.x point release. v0.3 ships chat + structured output + prompt caching + extended thinking + citations. Agents land cleanly once the simpler surface settles.
  • O4rmcp version pinned at implementation time rather than today. The crate is moving fast; locking-in now risks an outdated API by the time slice 3 ships.
  • O5rtb-docs ask writes streamed tokens to stdout. Mirrors go-tool-base's pattern. The TUI is reserved for docs browse; every other docs subcommand stays terminal-friendly. Future TUI integration of ask (split-pane Q&A inside browse) is a separate concern that doesn't block v0.3.

7. Approval gate

This addendum is implemented when (a) status flips to APPROVED, (b) open questions §6 are resolved (or explicitly deferred), © the three per-crate specs (rtb-credentials v0.2 / rtb-ai v0.1 / rtb-mcp v0.1) land as DRAFT documents, (d) § 16 of the framework spec gains a v0.3 Shipped entry once the three per-crate slices merge.