1use std::{env, ffi::OsString};
19
20use clap::{Arg, Command};
21pub use mcp_utils::server_prelude::ServerBuilder;
22use mcp_utils::server_prelude::ToolBox;
23use rust_mcp_sdk::{
24 error::McpSdkError,
25 schema::{CallToolRequestParams, schema_utils::CallToolError},
26};
27
28const DEFAULT_PORT: u16 = 8080;
29
30const ARG_TIMEOUT: &str = "timeout";
31const ARG_HOST: &str = "host";
32const ARG_PORT: &str = "port";
33
34pub fn run<T>(builder: ServerBuilder) -> Result<(), String>
84where
85 T: ToolBox + TryFrom<CallToolRequestParams, Error = CallToolError> + Send + Sync + 'static,
86{
87 match inner_run::<T, _>(builder, env::args_os()) {
88 Ok(Ok(())) => Ok(()),
89 Ok(Err(start_error)) => {
90 eprintln!(
91 "{}",
92 start_error
93 .rpc_error_message()
94 .unwrap_or(&start_error.to_string())
95 );
96 Err(start_error.to_string())
97 }
98 Err(clap_err) => clap_err.exit(),
99 }
100}
101
102fn inner_run<T, IntoArg>(
103 mut builder: ServerBuilder,
104 args: impl IntoIterator<Item = IntoArg>,
105) -> Result<Result<(), McpSdkError>, clap::Error>
106where
107 T: ToolBox + TryFrom<CallToolRequestParams, Error = CallToolError> + Send + Sync + 'static,
108 IntoArg: Into<OsString> + Clone,
109{
110 let bold = clap::builder::styling::Style::new().bold();
111 let underlined = clap::builder::styling::Style::new().underline();
112 let dimmed = clap::builder::styling::Style::new().dimmed();
113
114 let tools = T::get_tools();
115 let mut tool_names: Vec<_> = tools
116 .iter()
117 .enumerate()
118 .map(|(i, tool)| {
119 if let Some(description) = tool.description.as_ref() {
120 format!(
121 "{}. {underlined}{}{underlined:#}\n {}",
122 i + 1,
123 tool.title.as_ref().unwrap_or(&tool.name).as_str(),
124 description
125 )
126 } else {
127 format!(
128 "{}. {underlined}{}{underlined:#}: {dimmed}no description available{dimmed:#}",
129 i + 1,
130 tool.title.as_ref().unwrap_or(&tool.name).as_str(),
131 )
132 }
133 })
134 .collect();
135 tool_names.sort();
136
137 let matches = Command::new(builder.name().to_owned())
138 .about(format!(
139 r#"{underlined}{}{underlined:#}
140
141Start the MCP server in stdio mode by running the command:
142 {bold}{}{bold:#}
143
144To use SSE (Server-Sent Events), pass the --host and/or the --port options
145 {bold}{} --port 8080{bold:#}
146"#,
147 builder.title(),
148 builder.name(),
149 builder.name(),
150 ))
151 .version(builder.version().to_owned())
152 .after_long_help(format!(
153 "MCP server: {}\n\n{bold}Instructions:{bold:#}\n{}\n\n{bold}Tools:{bold:#}\n{}",
154 builder.title(),
155 builder.instructions(),
156 tool_names.join("\n")
157 ))
158 .arg(
159 Arg::new(ARG_TIMEOUT)
160 .help("Timeout for requests made (in humantime format, see <https://docs.rs/humantime/latest/humantime/>)")
161 .default_value("60s")
162 .long("timeout")
163 .value_parser(clap::value_parser!(humantime::Duration)),
164 )
165 .arg(
166 Arg::new(ARG_HOST)
167 .help("Host to bind the server to")
168 .long("host")
169 .value_parser(clap::value_parser!(String)),
170 )
171 .arg(
172 Arg::new(ARG_PORT)
173 .help("Port to bind the server to")
174 .long("port")
175 .short('p')
176 .value_parser(clap::value_parser!(u16)),
177 )
178 .try_get_matches_from(args)?;
179
180 let timeout = matches
181 .get_one::<humantime::Duration>(ARG_TIMEOUT)
182 .cloned()
183 .map(Into::into)
184 .unwrap_or_else(|| std::time::Duration::from_secs(60));
185
186 builder.set_timeout(timeout);
187
188 let host = matches.get_one::<String>(ARG_HOST).cloned();
189 let port = matches.get_one::<u16>(ARG_PORT).cloned();
190
191 tokio::runtime::Builder::new_multi_thread()
192 .enable_all()
193 .build()
194 .unwrap()
195 .block_on(async {
196 Ok(match (host, port) {
197 (None, None) => builder.start_stdio::<T>().await,
198 (host, port) => {
199 builder
200 .start_server::<T>(
201 host.as_deref().unwrap_or("127.0.0.1"),
202 port.unwrap_or(DEFAULT_PORT),
203 )
204 .await
205 }
206 })
207 })
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213 use mcp_utils::server_prelude::setup_tools;
214 use mcp_utils::tool_prelude::*;
215
216 #[mcp_tool(
217 name = "test_tool",
218 description = "A test tool for demonstration",
219 title = "Test Tool"
220 )]
221 #[derive(Debug, JsonSchema, Serialize, Deserialize)]
222 pub struct TestTool {
223 pub message: String,
225 }
226
227 impl StructuredTool for TestTool {
228 type Output = String;
229
230 fn call(&self) -> Self::Output {
231 format!("Processed: {}", self.message)
232 }
233 }
234
235 #[mcp_tool(name = "another_tool", description = "A tool that doubles a number")]
236 #[derive(Debug, JsonSchema, Serialize, Deserialize)]
237 pub struct AnotherTool {
238 pub value: i32,
240 }
241
242 impl StructuredTool for AnotherTool {
243 type Output = i32;
244
245 fn call(&self) -> Self::Output {
246 self.value * 2
247 }
248 }
249
250 setup_tools!(pub TestTools, [
252 structured(TestTool),
253 structured(AnotherTool),
254 ]);
255
256 fn get_builder() -> ServerBuilder {
257 ServerBuilder::new()
258 .with_name("test-server")
259 .with_title("Test MCP Server")
260 .with_version("1.0.0")
261 .with_instructions("This is a test server for demonstration purposes")
262 }
263
264 #[test]
265 fn test_help_command_snapshot() {
266 let builder = get_builder();
267
268 let help_output = match inner_run::<TestTools, _>(builder, ["test-server", "--help"]) {
269 Err(e) => e.to_string(),
270 Ok(_) => panic!("Expected help error, but inner_run succeeded"),
271 };
272
273 insta::assert_snapshot!("help_output", help_output);
274 }
275
276 #[test]
277 fn test_short_help_command_snapshot() {
278 let builder = get_builder();
279
280 let err = match inner_run::<TestTools, _>(builder, ["test-server", "-h"]) {
281 Err(e) => e.to_string(),
282 Ok(_) => panic!("Expected help error, but inner_run succeeded"),
283 };
284
285 insta::assert_snapshot!("help_short_output", err);
286 }
287
288 #[test]
289 fn test_version_command_snapshot() {
290 let builder = get_builder();
291
292 let err = match inner_run::<TestTools, _>(builder, ["test-server", "--version"]) {
293 Err(e) => e.to_string(),
294 Ok(_) => panic!("Expected help error, but inner_run succeeded"),
295 };
296
297 insta::assert_snapshot!("version_output", err);
298 }
299}