MCP exposure¶
Every Rust Tool Base command can opt itself into the Model Context Protocol surface. This page explains the mental model: what "exposing" a command means, where the schema comes from, and what an MCP client sees on the wire.
The shape of the contract¶
There are exactly two opt-in points on rtb_app::command::Command,
both default trait methods:
fn mcp_exposed(&self) -> bool { false }
fn mcp_input_schema(&self) -> Option<serde_json::Value> { None }
A command that wants to be reachable from an MCP client overrides
mcp_exposed to return true. It optionally returns a JSON Schema
that describes the call's argument shape. Commands that don't care
inherit the defaults and remain CLI-only — the framework treats
mcp_exposed = false and Command::mcp_input_schema = None as the
expected case for the majority of subcommands.
impl Command for Deploy {
fn mcp_exposed(&self) -> bool { true }
fn mcp_input_schema(&self) -> Option<serde_json::Value> {
Some(serde_json::to_value(schemars::schema_for!(DeployArgs)).unwrap())
}
/* … */
}
What the server does with that opt-in¶
rtb_mcp::McpServer::new walks BUILTIN_COMMANDS once at startup,
filters by mcp_exposed, and freezes a tool registry of
(name, about, schema, factory) tuples. The factory is the same
function pointer that built the Command for CLI dispatch, which
means a tools/call invocation:
- asks rmcp for the tool name from the wire request,
- looks it up in the frozen registry,
- calls the factory to build a fresh
Box<dyn Command>, - invokes
Command::run(self.app.clone()).await, and - forwards the result (or error) back to the client.
There is no second registry, no shadow trait, and no separate state
machine. The same App clone the CLI hands the command on dispatch
is what the MCP path hands it.
What the client sees¶
tools/list returns one entry per mcp_exposed command. The entry
carries:
- the command name (
CommandSpec::name), - the human-readable description (
CommandSpec::about), and - the JSON Schema returned from
mcp_input_schema— falling back to{"type": "object"}when the command's args struct is()or the author hasn't wired schema derivation yet.
tools/call returns either a success-marker text content
("<name> ok") or a structured is_error: true payload that
stringifies the underlying miette::Report. Either way, the result
is a single Content::text — there is no streaming today; that
seam is on the v0.3.x roadmap.
Where exposure does not happen¶
- Outside
Command. There is no separate "register a function as an MCP tool" macro. Everything goes throughCommand, which is the same type CLI commands implement. - In configuration.
mcp_exposedis compile-time. Config flags cannot toggle MCP exposure of an individual command. Operators turn the entire MCP surface on or off by toggling the runtimeFeature::Mcp(which gates themcpsubcommand itself). - In transport selection. The same registry serves stdio, SSE, and
HTTP transports identically.
Transportis purely about how bytes reach the server — what tools exist is decided once, at registry build time.
Pointer¶
For the public API surface and CLI subcommands, see the
rtb-mcp component page. For the
authoritative behavioural contract, see the
rtb-mcp v0.1 spec.