#![allow(unsafe_code)]
use std::ffi::OsString;
use std::net::SocketAddr;
use async_trait::async_trait;
use clap::{Parser, Subcommand, ValueEnum};
use linkme::distributed_slice;
use miette::miette;
use rtb_app::app::App;
use rtb_app::command::{Command, CommandSpec, BUILTIN_COMMANDS};
use rtb_app::features::Feature;
use crate::server::McpServer;
use crate::transport::Transport;
pub struct McpCmd;
#[async_trait]
impl Command for McpCmd {
fn spec(&self) -> &CommandSpec {
static SPEC: CommandSpec = CommandSpec {
name: "mcp",
about: "Expose registered commands as Model Context Protocol tools",
aliases: &[],
feature: Some(Feature::Mcp),
};
&SPEC
}
fn subcommand_passthrough(&self) -> bool {
true
}
async fn run(&self, app: App) -> miette::Result<()> {
let mut args: Vec<OsString> = std::env::args_os().collect();
if args.len() >= 2 {
args.drain(..2);
}
args.insert(0, OsString::from("mcp"));
let cli = match McpCli::try_parse_from(args) {
Ok(c) => c,
Err(e) => {
use clap::error::ErrorKind;
if matches!(e.kind(), ErrorKind::DisplayHelp | ErrorKind::DisplayVersion) {
print!("{e}");
return Ok(());
}
return Err(miette!("{e}"));
}
};
let sub = cli.command.unwrap_or_else(|| McpSub::Serve(ServeOpts::default()));
match sub {
McpSub::Serve(opts) => run_serve(app, opts).await,
McpSub::List(_) => {
run_list();
Ok(())
}
}
}
}
#[distributed_slice(BUILTIN_COMMANDS)]
fn __register_mcp() -> Box<dyn Command> {
Box::new(McpCmd)
}
#[derive(Debug, Parser)]
#[command(name = "mcp", about = "Expose registered commands as Model Context Protocol tools")]
struct McpCli {
#[command(subcommand)]
command: Option<McpSub>,
}
#[derive(Debug, Subcommand)]
enum McpSub {
Serve(ServeOpts),
List(ListOpts),
}
#[derive(Debug, Default, clap::Args)]
struct ServeOpts {
#[arg(long, value_enum, default_value_t = TransportArg::Stdio)]
transport: TransportArg,
#[arg(long, value_name = "ADDR")]
bind: Option<SocketAddr>,
}
#[derive(Debug, clap::Args)]
struct ListOpts {}
#[derive(Debug, Clone, Copy, ValueEnum, Default)]
enum TransportArg {
#[default]
Stdio,
Sse,
Http,
}
async fn run_serve(app: App, opts: ServeOpts) -> miette::Result<()> {
let transport = match opts.transport {
TransportArg::Stdio => Transport::Stdio,
TransportArg::Sse => Transport::Sse {
bind: opts
.bind
.ok_or_else(|| miette!("`mcp serve --transport sse` requires `--bind ADDR`"))?,
},
TransportArg::Http => Transport::Http {
bind: opts
.bind
.ok_or_else(|| miette!("`mcp serve --transport http` requires `--bind ADDR`"))?,
},
};
let server = McpServer::new(app, transport);
server.serve().await.map_err(miette::Report::new)
}
fn run_list() {
for factory in BUILTIN_COMMANDS {
let cmd = factory();
if !cmd.mcp_exposed() {
continue;
}
let spec = cmd.spec();
let schema =
cmd.mcp_input_schema().unwrap_or_else(|| serde_json::json!({"type": "object"}));
let entry = serde_json::json!({
"name": spec.name,
"description": spec.about,
"input_schema": schema,
});
println!("{entry}");
}
}