1#![allow(unsafe_code)]
20
21use std::ffi::OsString;
22use std::net::SocketAddr;
23
24use async_trait::async_trait;
25use clap::{Parser, Subcommand, ValueEnum};
26use linkme::distributed_slice;
27use miette::miette;
28use rtb_app::app::App;
29use rtb_app::command::{Command, CommandSpec, BUILTIN_COMMANDS};
30use rtb_app::features::Feature;
31
32use crate::server::McpServer;
33use crate::transport::Transport;
34
35pub struct McpCmd;
37
38#[async_trait]
39impl Command for McpCmd {
40 fn spec(&self) -> &CommandSpec {
41 static SPEC: CommandSpec = CommandSpec {
42 name: "mcp",
43 about: "Expose registered commands as Model Context Protocol tools",
44 feature: Some(Feature::Mcp),
45 ..CommandSpec::DEFAULT
46 };
47 &SPEC
48 }
49
50 fn subcommand_passthrough(&self) -> bool {
52 true
53 }
54
55 async fn run(&self, app: App) -> miette::Result<()> {
56 let mut args: Vec<OsString> = std::env::args_os().collect();
57 if args.len() >= 2 {
58 args.drain(..2);
59 }
60 args.insert(0, OsString::from("mcp"));
61 let cli = match McpCli::try_parse_from(args) {
62 Ok(c) => c,
63 Err(e) => {
64 use clap::error::ErrorKind;
65 if matches!(e.kind(), ErrorKind::DisplayHelp | ErrorKind::DisplayVersion) {
66 print!("{e}");
67 return Ok(());
68 }
69 return Err(miette!("{e}"));
70 }
71 };
72
73 let sub = cli.command.unwrap_or_else(|| McpSub::Serve(ServeOpts::default()));
74 match sub {
75 McpSub::Serve(opts) => run_serve(app, opts).await,
76 McpSub::List(_) => {
77 run_list();
78 Ok(())
79 }
80 }
81 }
82}
83
84#[distributed_slice(BUILTIN_COMMANDS)]
85fn __register_mcp() -> Box<dyn Command> {
86 Box::new(McpCmd)
87}
88
89#[derive(Debug, Parser)]
94#[command(name = "mcp", about = "Expose registered commands as Model Context Protocol tools")]
95struct McpCli {
96 #[command(subcommand)]
97 command: Option<McpSub>,
98}
99
100#[derive(Debug, Subcommand)]
101enum McpSub {
102 Serve(ServeOpts),
104 List(ListOpts),
106}
107
108#[derive(Debug, Default, clap::Args)]
109struct ServeOpts {
110 #[arg(long, value_enum, default_value_t = TransportArg::Stdio)]
112 transport: TransportArg,
113 #[arg(long, value_name = "ADDR")]
116 bind: Option<SocketAddr>,
117}
118
119#[derive(Debug, clap::Args)]
120struct ListOpts {}
121
122#[derive(Debug, Clone, Copy, ValueEnum, Default)]
123enum TransportArg {
124 #[default]
125 Stdio,
126 Sse,
127 Http,
128}
129
130async fn run_serve(app: App, opts: ServeOpts) -> miette::Result<()> {
135 let transport = match opts.transport {
136 TransportArg::Stdio => Transport::Stdio,
137 TransportArg::Sse => Transport::Sse {
138 bind: opts
139 .bind
140 .ok_or_else(|| miette!("`mcp serve --transport sse` requires `--bind ADDR`"))?,
141 },
142 TransportArg::Http => Transport::Http {
143 bind: opts
144 .bind
145 .ok_or_else(|| miette!("`mcp serve --transport http` requires `--bind ADDR`"))?,
146 },
147 };
148 let server = McpServer::new(app, transport);
149 server.serve().await.map_err(miette::Report::new)
150}
151
152fn run_list() {
153 for factory in BUILTIN_COMMANDS {
156 let cmd = factory();
157 if !cmd.mcp_exposed() {
158 continue;
159 }
160 let spec = cmd.spec();
161 let schema =
162 cmd.mcp_input_schema().unwrap_or_else(|| serde_json::json!({"type": "object"}));
163 let entry = serde_json::json!({
164 "name": spec.name,
165 "description": spec.about,
166 "input_schema": schema,
167 });
168 println!("{entry}");
169 }
170}