1pub 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
39pub struct McpDiscovery {
41 options: DiscoveryCommand,
43 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 pub async fn start(&mut self) -> DiscoveryResult<()> {
57 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 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 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 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 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 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 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 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 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 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 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}