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-docsships an emptyAiAnswerStreamtrait gated on theaifeature.docs askreturnsDocsError::AiDisableduntil v0.3 lands.rtb-cliships anmcpstub that returnsFeatureDisabled.- The
rtbumbrella definesaiandmcpfeatures 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_urlruns throughvalidate_base_url(mirrorsrtb-vcs's policy): HTTPS-only, no userinfo, noexample.complaceholder.allow_insecure_base_urlis#[serde(skip)]so config files can't downgrade.Config::api_keyisSecretString; never logged. Resolved viartb-credentials(which lands its hookup here too — see § 4.1).- Every successful
AiClient::newlogs the endpoint hostname at INFO. Path + query never logged. - All free-form strings written to telemetry (
AiError::Provider(...), retry diagnostics) flow throughrtb_redact::stringfirst.
Test plan:
wiremockfor HTTP-level tests (every provider).genaialready 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:
- Walks
BUILTIN_COMMANDSfor entries markedmcp_exposed: true(a new optional flag onCommandSpec— additive trait-method or struct field, TBD via Open questions O1). - For each, derives the input schema via
schemarsfrom the command'sclap::Argsstruct. - Registers each command as an MCP tool whose
callinvokes the existingCommand::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:
rmcpships 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::runbody 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: completecrate::ai::AiAnswerStreamimpl backed byrtb_ai::AiClient.docs askbecomes functional (gated on theaifeature).rtb-update: PAT auth. Todayupdateruns unauthenticated — wirertb-credentials::Resolverso private repos work. (Was deferred from v0.2 #18.)rtb-cli: replace theMcpStubplaceholder with the realrtb-mcpregistration; the umbrella'smcpfeature flips to non-default → default-yes once shipped.rtb-app::ReleaseSource: expand to the six variants inrtb-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-docsalready usesratatuidirectly; promotion to a shared crate happens oncertb-cli'sinitwizard work creates a second consumer.- MCP client —
rtb-mcpv0.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-docsv0.1 spec § 2.6 and re-confirmed here. Local inference goes through Ollama or a self-hosted OpenAI-compatible endpoint. - Vector stores / RAG indexing —
rtb-docsalready shipstantivyfor 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 aResolveroverKeyringStore::new(). Saves the two-lineArc::new(KeyringStore::new())boilerplate at every callsite.Resolver::default()—Defaultimpl 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 matchessubcommand_passthrough. Most flexible. - (b) Field on
CommandSpec. Breaking change — every existingstatic SPECliteral needs an extra field initialiser (27 sites). Avoided in v0.2 follow-ups. - © Marker trait
McpExposedthat 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¶
rtb-aiv0.1 — bulk of v0.3. Multi-provider client (genaifor OpenAI/Gemini/Ollama/OpenAI-compatible + Anthropic-direct path for caching/thinking/citations), structured output viaschemars+jsonschema. Tucks in theResolver::with_platform_default()helper (§4.1). Tucks indocs askhookup. Tucks in the rtb-update PAT auth hookup.rtb-mcpv0.1 — server crate viarmcp+mcpCLI subcommandCommand::mcp_exposeddefault 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:
- O1 —
Command::mcp_exposedships as a default trait method (fn mcp_exposed(&self) -> bool { false }). Mirrors thesubcommand_passthroughpattern landed in v0.2 — additive, no impact on the existing 27impl Commandsites. - O2 — Claude 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. - O3 — Anthropic 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.
- O4 —
rmcpversion 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. - O5 —
rtb-docs askwrites streamed tokens to stdout. Mirrorsgo-tool-base's pattern. The TUI is reserved fordocs browse; every otherdocssubcommand stays terminal-friendly. Future TUI integration ofask(split-pane Q&A insidebrowse) 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.