Expose a command over MCP¶
Opt the command in¶
mcp_exposed and mcp_input_schema are default methods on Command. Override
them on the commands you want reachable:
use schemars::JsonSchema;
#[derive(JsonSchema)]
struct DeployArgs {
region: Option<String>,
force: bool,
}
#[async_trait::async_trait]
impl Command for Deploy {
fn spec(&self) -> &CommandSpec { /* … */ }
fn mcp_exposed(&self) -> bool {
true
}
fn mcp_input_schema(&self) -> Option<serde_json::Value> {
serde_json::to_value(schemars::schema_for!(DeployArgs)).ok()
}
async fn run(&self, app: App) -> miette::Result<()> { /* … */ }
}
Both default to false and None, so nothing is exposed until you say so.
A schema that is not a JSON object is discarded and replaced with
{"type": "object"}, silently. Returning None does the same thing.
Make the command reachable¶
Something in your binary must reference rtb_mcp, or the mcp command will not
exist:
Confirm what is published¶
One JSON object per line — JSON Lines, not an array. mcp list walks
BUILTIN_COMMANDS directly and ignores the runtime Features set, so a command
whose feature is disabled still appears here. It also rejects --output; it is
already JSON.
Run the server¶
stdio is the default and the only implemented transport. --transport sse and
--transport http parse, demand a --bind, and then fail.
Point an MCP client that spawns servers as subprocesses at the binary with mcp
serve as its arguments.
Choose commands that fit what the server actually carries¶
Three constraints decide whether a command is worth exposing today:
- Arguments are not delivered. The published schema tells the client what to
send;
tools/callthen discards it. Your command runs with an emptyApp::trailing_args(). A command that needs input cannot get it. - Output is not returned. A successful call replies with the fixed text
<name> ok. Anything the command printed went to the process's stdout — which, over stdio, is the protocol stream. A command that prints will corrupt the session. - The effect has to be the point. Only commands that need no input, produce no stdout, and are useful for what they do work properly right now.
That is a narrow set, and it is worth knowing before designing around it.
Think about the blast radius¶
A CLI command runs because a person typed it. An MCP tool runs because a model decided to call it, possibly from text it read somewhere. Anything destructive, anything that spends money, anything writing to a shared system — decide about it one command at a time. That is why exposure is opt-in.
Related¶
mcpreference- rtb-mcp — how far the implementation goes.
- Add a command