mcp_discovery/
lib.rs

1//! A lightweight CLI tool for discovering and documenting MCP Server capabilities.
2
3pub mod error;
4mod handler;
5mod render_template;
6mod schema;
7mod std_output;
8mod templates;
9mod types;
10mod utils;
11
12pub use templates::OutputTemplate;
13pub use types::{
14    DiscoveryCommand, LogLevel, McpCapabilities, McpServerInfo, McpToolMeta, ParamTypes,
15    PrintOptions, Template, WriteOptions,
16};
17
18use colored::Colorize;
19use error::{DiscoveryError, DiscoveryResult};
20use render_template::{detect_render_markers, render_template};
21use schema::tool_params;
22use std::io::stdout;
23use std_output::{print_header, print_list, print_summary};
24
25use std::sync::Arc;
26
27use handler::MyClientHandler;
28use rust_mcp_schema::{
29    ClientCapabilities, Implementation, InitializeRequestParams, ListPromptsRequestParams,
30    ListResourceTemplatesRequestParams, ListResourcesRequestParams, ListToolsRequestParams, Prompt,
31    Resource, ResourceTemplate, JSONRPC_VERSION,
32};
33use rust_mcp_sdk::{
34    error::SdkResult,
35    mcp_client::{client_runtime, ClientRuntime},
36    McpClient, StdioTransport, TransportOptions,
37};
38
39/// Core struct representing the discovery mechanism for the MCP server.
40pub struct McpDiscovery {
41    /// Discovery action and its options
42    options: DiscoveryCommand,
43    /// Collected server capabilities and metadata
44    pub server_info: Option<McpServerInfo>,
45}
46
47impl McpDiscovery {
48    pub fn new(options: DiscoveryCommand) -> Self {
49        Self {
50            options,
51            server_info: None,
52        }
53    }
54
55    /// Entry point to execute the discovery workflow based on the command.
56    pub async fn start(&mut self) -> DiscoveryResult<()> {
57        // launch mcp server and discover capabilities
58
59        self.discover().await?;
60
61        match &self.options {
62            DiscoveryCommand::Create(create_options) => {
63                self.create_document(create_options).await?;
64            }
65            DiscoveryCommand::Update(update_options) => {
66                self.update_document(update_options).await?;
67            }
68            DiscoveryCommand::Print(print_options) => {
69                self.print_server_capabilities(print_options).await?;
70            }
71        };
72        Ok(())
73    }
74
75    /// Prints MCP server capabilities using a specific template or default view.
76    pub async fn print_server_capabilities(
77        &self,
78        print_options: &PrintOptions,
79    ) -> DiscoveryResult<()> {
80        let server_info = self
81            .server_info
82            .as_ref()
83            .ok_or(DiscoveryError::NotDiscovered)?;
84
85        let template = print_options.match_template()?;
86
87        match template {
88            OutputTemplate::None => {
89                self.print_server_details()?;
90            }
91            _ => {
92                let content = template.render_template(server_info)?;
93                println!("{}", content);
94            }
95        }
96
97        Ok(())
98    }
99
100    /// Creates a new file using a specified template and discovered server info.
101    pub async fn create_document(&self, create_options: &WriteOptions) -> DiscoveryResult<()> {
102        tracing::trace!("Creating '{}' ", create_options.filename.to_string_lossy());
103
104        let server_info = self
105            .server_info
106            .as_ref()
107            .ok_or(DiscoveryError::NotDiscovered)?;
108
109        let template = create_options.match_template()?;
110
111        let content = template.render_template(server_info)?;
112
113        tokio::fs::write(&create_options.filename, content).await?;
114
115        tracing::info!(
116            "File '{}' was created successfully.",
117            create_options.filename.to_string_lossy(),
118        );
119        tracing::info!(
120            "Full path: {}",
121            create_options
122                .filename
123                .canonicalize()
124                .map(|f| f.to_string_lossy().into_owned())
125                .unwrap_or_else(|_| create_options.filename.to_string_lossy().into_owned())
126        );
127
128        Ok(())
129    }
130
131    /// Updates an existing file by replacing only templated sections.
132    pub async fn update_document(&self, update_options: &WriteOptions) -> DiscoveryResult<()> {
133        tracing::trace!("Updating '{}' ", update_options.filename.to_string_lossy());
134
135        let server_info = self
136            .server_info
137            .as_ref()
138            .ok_or(DiscoveryError::NotDiscovered)?;
139
140        update_options.validate()?;
141
142        let template_markers = detect_render_markers(update_options, server_info)?;
143        let mut content_lines: Vec<String> = template_markers
144            .content
145            .lines()
146            .map(|s| s.to_owned())
147            .collect();
148
149        for location in template_markers.render_locations.iter().rev() {
150            let new_lines: Vec<String> = location
151                .rendered_template
152                .lines()
153                .map(|s| s.to_owned())
154                .collect();
155
156            content_lines.splice(
157                location.render_location.0..location.render_location.1 - 1,
158                new_lines,
159            );
160        }
161
162        let updated_content = content_lines.join(&template_markers.line_ending);
163
164        std::fs::write(&update_options.filename, updated_content)?;
165        tracing::info!(
166            "File '{}' was updated successfully.",
167            update_options.filename.to_string_lossy()
168        );
169        Ok(())
170    }
171
172    /// Print a brief summary of the discovered server information.
173    fn print_summary(&self) -> DiscoveryResult<usize> {
174        let server_info = self
175            .server_info
176            .as_ref()
177            .ok_or(DiscoveryError::NotDiscovered)?;
178        Ok(print_summary(stdout(), server_info)?)
179    }
180
181    /// Prints summary and then detailed info about tools, prompts, resources, and templates from server.
182    fn print_server_details(&self) -> DiscoveryResult<()> {
183        let table_size = self.print_summary()?;
184
185        let server_info = self
186            .server_info
187            .as_ref()
188            .ok_or(DiscoveryError::NotDiscovered)?;
189
190        if let Some(tools) = &server_info.tools {
191            print_header(
192                stdout(),
193                &format!("{}({})", "Tools".bold(), tools.len()),
194                table_size,
195            )?;
196            let mut tool_list: Vec<_> = tools
197                .iter()
198                .map(|item| {
199                    (
200                        item.name.clone(),
201                        item.description.clone().unwrap_or_default(),
202                    )
203                })
204                .collect();
205            tool_list.sort_by(|a, b| a.0.cmp(&b.0));
206            print_list(stdout(), tool_list)?;
207        }
208
209        if let Some(prompts) = &server_info.prompts {
210            print_header(
211                stdout(),
212                &format!("{}({})", "Prompts".bold(), prompts.len()),
213                table_size,
214            )?;
215            print_list(
216                stdout(),
217                prompts
218                    .iter()
219                    .map(|item| {
220                        (
221                            item.name.clone(),
222                            item.description.clone().unwrap_or_default(),
223                        )
224                    })
225                    .collect(),
226            )?;
227        }
228
229        if let Some(resources) = &server_info.resources {
230            print_header(
231                stdout(),
232                &format!("{}({})", "Resources".bold(), resources.len()),
233                table_size,
234            )?;
235            print_list(
236                stdout(),
237                resources
238                    .iter()
239                    .map(|item| {
240                        (
241                            item.name.clone(),
242                            format!(
243                                "{}{}{}",
244                                item.uri,
245                                item.mime_type
246                                    .as_ref()
247                                    .map_or("".to_string(), |mime_type| format!(" ({})", mime_type))
248                                    .dimmed(),
249                                item.description.as_ref().map_or(
250                                    "".to_string(),
251                                    |description| format!("\n{}", description.dimmed())
252                                )
253                            ),
254                        )
255                    })
256                    .collect(),
257            )?;
258        }
259
260        if let Some(resource_templates) = &server_info.resource_templates {
261            print_header(
262                stdout(),
263                &format!(
264                    "{}({})",
265                    "Resource Templates".bold(),
266                    resource_templates.len()
267                ),
268                table_size,
269            )?;
270            print_list(
271                stdout(),
272                resource_templates
273                    .iter()
274                    .map(|item| {
275                        (
276                            item.name.clone(),
277                            format!(
278                                "{}{}{}",
279                                item.uri_template,
280                                item.mime_type
281                                    .as_ref()
282                                    .map_or("".to_string(), |mime_type| format!(" ({})", mime_type))
283                                    .dimmed(),
284                                item.description.as_ref().map_or(
285                                    "".to_string(),
286                                    |description| format!("\n{}", description.dimmed())
287                                )
288                            ),
289                        )
290                    })
291                    .collect(),
292            )?;
293        }
294
295        Ok(())
296    }
297
298    /// Retrieves tools metadata from the MCP server.
299    pub async fn tools(
300        &self,
301        client: Arc<ClientRuntime>,
302    ) -> DiscoveryResult<Option<Vec<McpToolMeta>>> {
303        if !client.server_has_tools().unwrap_or(false) {
304            return Ok(None);
305        }
306
307        tracing::trace!("retrieving tools...");
308
309        let tools_result = client
310            .list_tools(Some(ListToolsRequestParams::default()))
311            .await?
312            .tools;
313
314        let mut tools: Vec<_> = tools_result
315            .iter()
316            .map(|tool| {
317                let params = tool_params(&tool.input_schema.properties);
318
319                Ok::<McpToolMeta, DiscoveryError>(McpToolMeta {
320                    name: tool.name.to_owned(),
321                    description: tool.description.to_owned(),
322                    params,
323                })
324            })
325            .filter_map(Result::ok)
326            .collect();
327        tools.sort_by(|a, b| a.name.cmp(&b.name));
328        Ok(Some(tools))
329    }
330
331    async fn prompts(&self, client: Arc<ClientRuntime>) -> DiscoveryResult<Option<Vec<Prompt>>> {
332        if !client.server_has_prompts().unwrap_or(false) {
333            return Ok(None);
334        }
335        tracing::trace!("retrieving prompts...");
336
337        let prompts: Vec<Prompt> = client
338            .list_prompts(Some(ListPromptsRequestParams::default()))
339            .await?
340            .prompts;
341
342        Ok(Some(prompts))
343    }
344
345    /// Retrieves resources from the server.
346    async fn resources(
347        &self,
348        client: Arc<ClientRuntime>,
349    ) -> DiscoveryResult<Option<Vec<Resource>>> {
350        if !client.server_has_resources().unwrap_or(false) {
351            return Ok(None);
352        }
353
354        tracing::trace!("retrieving resources...");
355
356        let resources: Vec<Resource> = client
357            .list_resources(Some(ListResourcesRequestParams::default()))
358            .await?
359            .resources;
360
361        Ok(Some(resources))
362    }
363
364    /// Retrieves resource templates from the server.
365    async fn resource_templates(
366        &self,
367        client: Arc<ClientRuntime>,
368    ) -> DiscoveryResult<Option<Vec<ResourceTemplate>>> {
369        if !client.server_has_resources().unwrap_or(false) {
370            return Ok(None);
371        }
372
373        tracing::trace!("retrieving resource templates...");
374
375        let result = client
376            .list_resource_templates(Some(ListResourceTemplatesRequestParams::default()))
377            .await;
378        match result {
379            Ok(data) => Ok(Some(data.resource_templates)),
380            Err(err) => {
381                tracing::trace!("Unable to retrieve resource templates : {}", err);
382                Ok(None)
383            }
384        }
385    }
386
387    /// Discovers all MCP server capabilities and stores them internally.
388    pub async fn discover(&mut self) -> DiscoveryResult<&McpServerInfo> {
389        let client = self.launch_mcp_server().await?;
390
391        let server_version = client
392            .server_version()
393            .ok_or(DiscoveryError::ServerNotInitialized)?;
394
395        tracing::trace!(
396            "Server: {} v{}",
397            server_version.name,
398            server_version.version
399        );
400
401        let capabilities: McpCapabilities = McpCapabilities {
402            tools: client
403                .server_has_tools()
404                .ok_or(DiscoveryError::ServerNotInitialized)?,
405            prompts: client
406                .server_has_prompts()
407                .ok_or(DiscoveryError::ServerNotInitialized)?,
408            resources: client
409                .server_has_resources()
410                .ok_or(DiscoveryError::ServerNotInitialized)?,
411            logging: client
412                .server_supports_logging()
413                .ok_or(DiscoveryError::ServerNotInitialized)?,
414            experimental: client
415                .server_has_experimental()
416                .ok_or(DiscoveryError::ServerNotInitialized)?,
417        };
418
419        tracing::trace!("Capabilities: {}", capabilities);
420
421        let tools = self.tools(Arc::clone(&client)).await?;
422        let prompts = self.prompts(Arc::clone(&client)).await?;
423        let resources = self.resources(Arc::clone(&client)).await?;
424        let resource_templates = self.resource_templates(Arc::clone(&client)).await?;
425
426        let server_info = McpServerInfo {
427            name: server_version.name,
428            version: server_version.version,
429            capabilities,
430            tools,
431            prompts,
432            resources,
433            resource_templates,
434        };
435
436        self.server_info = Some(server_info);
437
438        Ok(self.server_info.as_ref().unwrap())
439    }
440
441    /// Launches the MCP server as a subprocess and initializes the client.
442    async fn launch_mcp_server(&self) -> SdkResult<Arc<ClientRuntime>> {
443        let client_details: InitializeRequestParams = InitializeRequestParams {
444            capabilities: ClientCapabilities::default(),
445            client_info: Implementation {
446                name: env!("CARGO_PKG_NAME").to_string(),
447                version: env!("CARGO_PKG_VERSION").to_string(),
448            },
449            protocol_version: JSONRPC_VERSION.into(),
450        };
451
452        tracing::trace!(
453            "Client : {} v{}",
454            client_details.client_info.name,
455            client_details.client_info.version
456        );
457
458        let (mcp_command, mcp_args) = self.options.mcp_launch_command().split_at(1);
459
460        tracing::trace!(
461            "launching command : {} {}",
462            mcp_command.first().map(String::as_ref).unwrap_or(""),
463            mcp_args.join(" ")
464        );
465
466        let transport = StdioTransport::create_with_server_launch(
467            mcp_command.first().unwrap(),
468            mcp_args.into(),
469            None,
470            TransportOptions::default(),
471        )?;
472
473        let handler = MyClientHandler {};
474
475        let client = client_runtime::create_client(client_details, transport, handler);
476
477        tracing::trace!("Launching MCP server ...");
478
479        client.clone().start().await?;
480
481        tracing::trace!("MCP server started successfully.");
482
483        Ok(client)
484    }
485}