1pub mod error;
4mod handler;
5mod render_template;
6mod schema;
7mod std_output;
8mod templates;
9mod types;
10mod utils;
11
12use rust_mcp_sdk::error::McpSdkError;
13use rust_mcp_sdk::mcp_client::McpClientOptions;
14use rust_mcp_sdk::{mcp_icon, ToMcpClientHandler};
15use serde_json::{to_value, Map, Value};
16pub use templates::OutputTemplate;
17pub use types::{
18 DiscoveryCommand, LogLevel, McpCapabilities, McpServerInfo, McpToolMeta, ParamTypes,
19 PrintOptions, Template, WriteOptions,
20};
21
22use crate::types::McpTaskSupport;
23use colored::Colorize;
24use error::{DiscoveryError, DiscoveryResult};
25use handler::MyClientHandler;
26use render_template::{detect_render_markers, render_template};
27use rust_mcp_sdk::schema::{
28 ClientCapabilities, ClientElicitation, ClientRoots, ClientSampling, ClientTaskElicitation,
29 ClientTaskSampling, ClientTasks, Implementation, InitializeRequestParams,
30 PaginatedRequestParams, Prompt, ProtocolVersion, Resource, ResourceTemplate,
31};
32use rust_mcp_sdk::{
33 error::SdkResult,
34 mcp_client::{client_runtime, ClientRuntime},
35 McpClient, StdioTransport, TransportOptions,
36};
37use schema::tool_params;
38use std::io::stdout;
39use std::sync::Arc;
40use std_output::{print_header, print_list, print_summary};
41
42pub struct McpDiscovery {
44 options: DiscoveryCommand,
46 pub server_info: Option<McpServerInfo>,
48}
49
50impl McpDiscovery {
51 pub fn new(options: DiscoveryCommand) -> Self {
52 Self {
53 options,
54 server_info: None,
55 }
56 }
57
58 pub async fn start(&mut self) -> DiscoveryResult<()> {
60 self.discover().await?;
63
64 match &self.options {
65 DiscoveryCommand::Create(create_options) => {
66 self.create_document(create_options).await?;
67 }
68 DiscoveryCommand::Update(update_options) => {
69 self.update_document(update_options).await?;
70 }
71 DiscoveryCommand::Print(print_options) => {
72 self.print_server_capabilities(print_options).await?;
73 }
74 };
75 Ok(())
76 }
77
78 pub async fn print_server_capabilities(
80 &self,
81 print_options: &PrintOptions,
82 ) -> DiscoveryResult<()> {
83 let server_info = self
84 .server_info
85 .as_ref()
86 .ok_or(DiscoveryError::NotDiscovered)?;
87
88 let template = print_options.match_template()?;
89
90 match template {
91 OutputTemplate::None => {
92 self.print_server_details()?;
93 }
94 _ => {
95 let content = template.render_template(server_info)?;
96 println!("{content}");
97 }
98 }
99
100 Ok(())
101 }
102
103 pub async fn create_document(&self, create_options: &WriteOptions) -> DiscoveryResult<()> {
105 tracing::trace!("Creating '{}' ", create_options.filename.to_string_lossy());
106
107 let server_info = self
108 .server_info
109 .as_ref()
110 .ok_or(DiscoveryError::NotDiscovered)?;
111
112 let template = create_options.match_template()?;
113
114 let content = template.render_template(server_info)?;
115
116 tokio::fs::write(&create_options.filename, content).await?;
117
118 tracing::info!(
119 "File '{}' was created successfully.",
120 create_options.filename.to_string_lossy(),
121 );
122 tracing::info!(
123 "Full path: {}",
124 create_options
125 .filename
126 .canonicalize()
127 .map(|f| f.to_string_lossy().into_owned())
128 .unwrap_or_else(|_| create_options.filename.to_string_lossy().into_owned())
129 );
130
131 Ok(())
132 }
133
134 pub async fn update_document(&self, update_options: &WriteOptions) -> DiscoveryResult<()> {
136 tracing::trace!("Updating '{}' ", update_options.filename.to_string_lossy());
137
138 let server_info = self
139 .server_info
140 .as_ref()
141 .ok_or(DiscoveryError::NotDiscovered)?;
142
143 update_options.validate()?;
144
145 let template_markers = detect_render_markers(update_options, server_info)?;
146 let mut content_lines: Vec<String> = template_markers
147 .content
148 .lines()
149 .map(|s| s.to_owned())
150 .collect();
151
152 for location in template_markers.render_locations.iter().rev() {
153 let new_lines: Vec<String> = location
154 .rendered_template
155 .lines()
156 .map(|s| s.to_owned())
157 .collect();
158
159 content_lines.splice(
160 location.render_location.0..location.render_location.1 - 1,
161 new_lines,
162 );
163 }
164
165 let updated_content = content_lines.join(&template_markers.line_ending);
166
167 std::fs::write(&update_options.filename, updated_content)?;
168 tracing::info!(
169 "File '{}' was updated successfully.",
170 update_options.filename.to_string_lossy()
171 );
172 Ok(())
173 }
174
175 fn print_summary(&self) -> DiscoveryResult<usize> {
177 let server_info = self
178 .server_info
179 .as_ref()
180 .ok_or(DiscoveryError::NotDiscovered)?;
181 Ok(print_summary(&mut stdout(), server_info)?)
182 }
183
184 fn print_server_details(&self) -> DiscoveryResult<()> {
186 let table_size = self.print_summary()?;
187
188 let server_info = self
189 .server_info
190 .as_ref()
191 .ok_or(DiscoveryError::NotDiscovered)?;
192
193 if let Some(tools) = &server_info.tools {
194 if !tools.is_empty() {
195 print_header(
196 &mut stdout(),
197 &format!("{}({})", "Tools".bold(), tools.len()),
198 table_size,
199 )?;
200 let mut tool_list: Vec<_> = tools
201 .iter()
202 .map(|item| {
203 (
204 item.name.clone(),
205 item.description.clone().unwrap_or_default(),
206 )
207 })
208 .collect();
209 tool_list.sort_by(|a, b| a.0.cmp(&b.0));
210 print_list(stdout(), tool_list)?;
211 }
212 }
213
214 if let Some(prompts) = &server_info.prompts {
215 if !prompts.is_empty() {
216 print_header(
217 &mut stdout(),
218 &format!("{}({})", "Prompts".bold(), prompts.len()),
219 table_size,
220 )?;
221 print_list(
222 stdout(),
223 prompts
224 .iter()
225 .map(|item| {
226 (
227 item.name.clone(),
228 item.description.clone().unwrap_or_default(),
229 )
230 })
231 .collect(),
232 )?;
233 }
234 }
235
236 if let Some(resources) = &server_info.resources {
237 if !resources.is_empty() {
238 print_header(
239 &mut stdout(),
240 &format!("{}({})", "Resources".bold(), resources.len()),
241 table_size,
242 )?;
243 print_list(
244 stdout(),
245 resources
246 .iter()
247 .map(|item| {
248 (
249 item.name.clone(),
250 format!(
251 "{}{}{}",
252 item.uri,
253 item.mime_type
254 .as_ref()
255 .map_or("".to_string(), |mime_type| format!(
256 " ({mime_type})"
257 ))
258 .dimmed(),
259 item.description.as_ref().map_or(
260 "".to_string(),
261 |description| format!("\n{}", description.dimmed())
262 )
263 ),
264 )
265 })
266 .collect(),
267 )?;
268 }
269 }
270
271 if let Some(resource_templates) = &server_info.resource_templates {
272 if !resource_templates.is_empty() {
273 print_header(
274 &mut stdout(),
275 &format!(
276 "{}({})",
277 "Resource Templates".bold(),
278 resource_templates.len()
279 ),
280 table_size,
281 )?;
282 print_list(
283 stdout(),
284 resource_templates
285 .iter()
286 .map(|item| {
287 (
288 item.name.clone(),
289 format!(
290 "{}{}{}",
291 item.uri_template,
292 item.mime_type
293 .as_ref()
294 .map_or("".to_string(), |mime_type| format!(
295 " ({mime_type})"
296 ))
297 .dimmed(),
298 item.description.as_ref().map_or(
299 "".to_string(),
300 |description| format!("\n{}", description.dimmed())
301 )
302 ),
303 )
304 })
305 .collect(),
306 )?;
307 }
308 }
309
310 Ok(())
311 }
312
313 pub async fn tools(
315 &self,
316 client: Arc<ClientRuntime>,
317 ) -> DiscoveryResult<Option<Vec<McpToolMeta>>> {
318 if !client.server_has_tools().unwrap_or(false) {
319 return Ok(None);
320 }
321
322 tracing::trace!("retrieving tools...");
323
324 let tools_result = client
325 .request_tool_list(Some(PaginatedRequestParams::default()))
326 .await?
327 .tools;
328
329 let mut tools: Vec<_> = tools_result
330 .iter()
331 .map(|tool| {
332 let root_schema: serde_json::Value =
333 to_value(&tool.input_schema).unwrap_or_else(|_| Value::Object(Map::new()));
334 let params = tool_params(&tool.input_schema.properties, &root_schema);
335
336 Ok::<McpToolMeta, DiscoveryError>(McpToolMeta {
337 name: tool.name.to_owned(),
338 title: tool.title.to_owned(),
339 icons: tool.icons.to_owned(),
340 execution: tool.execution.to_owned(),
341 annotations: tool.annotations.to_owned(),
342 description: tool.description.to_owned(),
343 params,
344 input_schema: tool.input_schema.clone(),
345 meta: tool.meta.to_owned(),
346 })
347 })
348 .filter_map(Result::ok)
349 .collect();
350 tools.sort_by(|a, b| a.name.cmp(&b.name));
351 Ok(Some(tools))
352 }
353
354 async fn prompts(&self, client: Arc<ClientRuntime>) -> DiscoveryResult<Option<Vec<Prompt>>> {
355 if !client.server_has_prompts().unwrap_or(false) {
356 return Ok(None);
357 }
358 tracing::trace!("retrieving prompts...");
359
360 let prompts: Vec<Prompt> = client
361 .request_prompt_list(Some(PaginatedRequestParams::default()))
362 .await?
363 .prompts;
364
365 Ok(Some(prompts))
366 }
367
368 async fn resources(
370 &self,
371 client: Arc<ClientRuntime>,
372 ) -> DiscoveryResult<Option<Vec<Resource>>> {
373 if !client.server_has_resources().unwrap_or(false) {
374 return Ok(None);
375 }
376
377 tracing::trace!("retrieving resources...");
378
379 let resources: Vec<Resource> = client
380 .request_resource_list(Some(PaginatedRequestParams::default()))
381 .await?
382 .resources;
383
384 Ok(Some(resources))
385 }
386
387 async fn resource_templates(
389 &self,
390 client: Arc<ClientRuntime>,
391 ) -> DiscoveryResult<Option<Vec<ResourceTemplate>>> {
392 if !client.server_has_resources().unwrap_or(false) {
393 return Ok(None);
394 }
395
396 tracing::trace!("retrieving resource templates...");
397
398 let result = client
399 .request_resource_template_list(Some(PaginatedRequestParams::default()))
400 .await;
401 match result {
402 Ok(data) => Ok(Some(data.resource_templates)),
403 Err(err) => {
404 tracing::trace!("Unable to retrieve resource templates : {}", err);
405 Ok(None)
406 }
407 }
408 }
409
410 pub async fn discover(&mut self) -> DiscoveryResult<&McpServerInfo> {
412 let client = self.try_launch_mcp_server().await?;
413
414 let server_version = client
415 .server_version()
416 .ok_or(DiscoveryError::ServerNotInitialized)?;
417
418 tracing::trace!(
419 "Server: {} v{}",
420 server_version.name,
421 server_version.version,
422 );
423
424 let capabilities: McpCapabilities = McpCapabilities {
425 tools: client
426 .server_has_tools()
427 .ok_or(DiscoveryError::ServerNotInitialized)?,
428 prompts: client
429 .server_has_prompts()
430 .ok_or(DiscoveryError::ServerNotInitialized)?,
431 resources: client
432 .server_has_resources()
433 .ok_or(DiscoveryError::ServerNotInitialized)?,
434 logging: client
435 .server_supports_logging()
436 .ok_or(DiscoveryError::ServerNotInitialized)?,
437 completions: client
438 .server_info()
439 .ok_or(DiscoveryError::ServerNotInitialized)?
440 .capabilities
441 .completions
442 .is_some(),
443 experimental: client
444 .server_has_experimental()
445 .ok_or(DiscoveryError::ServerNotInitialized)?,
446 task: McpTaskSupport {
447 tool_call_task: client
448 .server_capabilities()
449 .ok_or(DiscoveryError::ServerNotInitialized)?
450 .can_run_task_augmented_tools(),
451 list_task: client
452 .server_capabilities()
453 .ok_or(DiscoveryError::ServerNotInitialized)?
454 .can_list_tasks(),
455 cancel_task: client
456 .server_capabilities()
457 .ok_or(DiscoveryError::ServerNotInitialized)?
458 .can_cancel_tasks(),
459 },
460 };
461
462 tracing::trace!("Capabilities: {}", capabilities);
463
464 let tools = self.tools(Arc::clone(&client)).await?;
465 let prompts = self.prompts(Arc::clone(&client)).await?;
466 let resources = self.resources(Arc::clone(&client)).await?;
467 let resource_templates = self.resource_templates(Arc::clone(&client)).await?;
468
469 let server_info = McpServerInfo {
470 name: server_version.name,
471 version: server_version.version,
472 title: server_version.title,
473 description: server_version.description,
474 website_url: server_version.website_url,
475 capabilities,
476 tools,
477 prompts,
478 resources,
479 resource_templates,
480 };
481
482 self.server_info = Some(server_info);
483
484 Ok(self.server_info.as_ref().unwrap())
485 }
486
487 async fn try_launch_mcp_server(&self) -> SdkResult<Arc<ClientRuntime>> {
489 let protocol_versions = [
490 ProtocolVersion::V2025_11_25,
491 ProtocolVersion::V2025_06_18,
492 ProtocolVersion::V2025_03_26,
493 ];
494 for version in protocol_versions {
495 let current_version = format!("with protocol version: {}", version.to_string().bold(),);
496 println!("{}", current_version.bright_green());
497
498 match self.launch_mcp_server(version).await {
499 Ok(client) => return Ok(client),
500 Err(McpSdkError::Protocol { kind: _ }) => {}
501 Err(err) => return Err(err),
502 }
503 }
504 Err(McpSdkError::Internal {
505 description: "Failed to launch the server.".into(),
506 })
507 }
508
509 async fn launch_mcp_server(
511 &self,
512 protocol_version: ProtocolVersion,
513 ) -> SdkResult<Arc<ClientRuntime>> {
514 let client_details: InitializeRequestParams = InitializeRequestParams {
515 capabilities: ClientCapabilities{
516 elicitation: Some(ClientElicitation{ form: Some(Map::new()), url: Some(Map::new()) }),
517 experimental: None,
518 roots: Some(ClientRoots{ list_changed:Some(true) }),
519 sampling: Some(ClientSampling{ context: Some(Map::new()), tools: Some(Map::new()) }),
520 tasks: Some(ClientTasks{ cancel: Some(Map::new()), list:Some(Map::new()), requests: Some(rust_mcp_sdk::schema::ClientTaskRequest { elicitation: Some(ClientTaskElicitation { create: Some(Map::new()) }), sampling:Some(ClientTaskSampling { create_message: Some(Map::new()) }) }) })
521 },
522 client_info: Implementation {
523 title: Some("MCP Discovery - By Rust MCP Stack".to_string()),
524 name: env!("CARGO_PKG_NAME").to_string(),
525 version: env!("CARGO_PKG_VERSION").to_string(),
526 description: Some("A lightweight CLI tool built in Rust for discovering and documenting MCP server capabilities.".into()),
527 icons: vec![
528 mcp_icon!(
529 src = "https://rust-mcp-stack.github.io/mcp-discovery/_media/mcp-discovery.png",
530 mime_type = "image/png",
531 sizes = ["110x110"]
532 ),
533 ],
534 website_url: Some("https://rust-mcp-stack.github.io/mcp-discovery".into())
535 },
536 protocol_version: protocol_version.into(),
537 meta: None
538 };
539
540 tracing::trace!(
541 "Client : {} v{}",
542 client_details.client_info.name,
543 client_details.client_info.version
544 );
545
546 let (mcp_command, mcp_args) = self.options.mcp_launch_command().split_at(1);
547
548 tracing::trace!(
549 "launching command : {} {}",
550 mcp_command.first().map(String::as_ref).unwrap_or(""),
551 mcp_args.join(" ")
552 );
553
554 let transport = StdioTransport::create_with_server_launch(
555 mcp_command.first().unwrap(),
556 mcp_args.into(),
557 None,
558 TransportOptions::default(),
559 )?;
560
561 let handler = MyClientHandler {};
562
563 let client = client_runtime::create_client(McpClientOptions {
564 client_details,
565 transport,
566 handler: handler.to_mcp_client_handler(),
567 task_store: None,
568 server_task_store: None,
569 });
570
571 tracing::trace!("Launching MCP server ...");
572
573 client.clone().start().await?;
574
575 tracing::trace!("MCP server started successfully.");
576
577 Ok(client)
578 }
579}