From e1da4cd78b916d701a6f807f178414503870e23f Mon Sep 17 00:00:00 2001 From: Gustavo Noronha Silva Date: Fri, 13 Jun 2025 17:23:17 -0300 Subject: [PATCH 1/2] Adjust test to the default format (markdown) --- tests/integration_tests.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index c1631df..5f8cde7 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -64,11 +64,11 @@ async fn test_end_to_end_crate_lookup() { assert!(result.is_ok()); let content = result.unwrap(); assert_eq!(content.len(), 1); - - // The response should be HTML from docs.rs + + // The response should be Markdown converted from docs.rs match &content[0] { mcp_core::Content::Text(text) => { - assert!(text.text.contains("")); + assert!(text.text.contains("[Docs.rs ](/)")); assert!(text.text.contains("serde")); }, _ => panic!("Expected text content"), From 8a8c602ad9984af1e8b1aec2ea652cb8f6a8d690 Mon Sep 17 00:00:00 2001 From: Gustavo Noronha Silva Date: Fri, 13 Jun 2025 17:25:44 -0300 Subject: [PATCH 2/2] Specil-case std and adjust url Factor out building URLs to helper methods that and improve code reuse, by creating item URLs from crate URLs. Special-case std and use docs.rust-lang.org as the base for that one. --- src/tools/docs/docs.rs | 68 +++++++++++++++++++-------- tests/integration_tests.rs | 95 +++++++++++++++++++++++++++++++++++++- 2 files changed, 143 insertions(+), 20 deletions(-) diff --git a/src/tools/docs/docs.rs b/src/tools/docs/docs.rs index 486a9f5..9f54861 100644 --- a/src/tools/docs/docs.rs +++ b/src/tools/docs/docs.rs @@ -62,6 +62,52 @@ impl DocRouter { } } + fn url_for_crate(&self, crate_name: &str, version: &Option) -> String { + if crate_name == "std" { + if let Some(ver) = version { + format!("https://doc.rust-lang.org/{}/std/", ver) + } else { + format!("https://doc.rust-lang.org/stable/std/") + } + } else { + if let Some(ver) = version { + format!("https://docs.rs/crate/{}/{}/", crate_name, ver) + } else { + format!("https://docs.rs/crate/{}/", crate_name) + } + } + } + + fn url_for_item( + &self, + crate_name: &str, + version: &Option, + module_path: &str, + item_type: &str, + item_name: &str, + ) -> String { + let mut url = self + .url_for_crate(crate_name, version) + .replace("crate/", ""); + + if crate_name != "std" { + if let Some(ver) = version { + url.push_str(&format!("{}/{}/", ver, crate_name)); + } else { + url.push_str(&format!("latest/{}/", crate_name)); + } + } + + let append = if module_path.is_empty() { + format!("{}.{}.html", item_type, item_name) + } else { + format!("{}/{}.{}.html", module_path, item_type, item_name) + }; + + url.push_str(&append); + url + } + // Fetch crate documentation from docs.rs async fn lookup_crate(&self, crate_name: String, version: Option) -> Result { // Check cache first @@ -76,11 +122,7 @@ impl DocRouter { } // Construct the docs.rs URL for the crate - let url = if let Some(ver) = version { - format!("https://docs.rs/crate/{}/{}/", crate_name, ver) - } else { - format!("https://docs.rs/crate/{}/", crate_name) - }; + let url = self.url_for_crate(&crate_name, &version); // Fetch the documentation page let response = self.client.get(&url) @@ -189,20 +231,8 @@ impl DocRouter { for item_type in item_types.iter() { // Construct the docs.rs URL for the specific item - let url = if let Some(ver) = version.clone() { - if module_path.is_empty() { - format!("https://docs.rs/{}/{}/{}/{}.{}.html", crate_name, ver, crate_name, item_type, item_name) - } else { - format!("https://docs.rs/{}/{}/{}/{}/{}.{}.html", crate_name, ver, crate_name, module_path, item_type, item_name) - } - } else { - if module_path.is_empty() { - format!("https://docs.rs/{}/latest/{}/{}.{}.html", crate_name, crate_name, item_type, item_name) - } else { - format!("https://docs.rs/{}/latest/{}/{}/{}.{}.html", crate_name, crate_name, module_path, item_type, item_name) - } - }; - + let url = self.url_for_item(&crate_name, &version, &module_path, item_type, &item_name); + // Try to fetch the documentation page let response = match self.client.get(&url) .header("User-Agent", "CrateDocs/0.1.0 (https://github.com/d6e/cratedocs-mcp)") diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 5f8cde7..edb21fd 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -70,7 +70,100 @@ async fn test_end_to_end_crate_lookup() { mcp_core::Content::Text(text) => { assert!(text.text.contains("[Docs.rs ](/)")); assert!(text.text.contains("serde")); - }, + } + _ => panic!("Expected text content"), + } +} + +// This test requires network access +#[tokio::test] +#[ignore = "Requires network access"] +async fn test_end_to_end_std_lookup() { + let router = DocRouter::new(); + + // Look up standard library as a crate + let result = router + .call_tool( + "lookup_crate", + json!({ + "crate_name": "std" + }), + ) + .await; + + assert!(result.is_ok()); + let content = result.unwrap(); + assert_eq!(content.len(), 1); + + // The response should be Markdown converted from docs.rs + match &content[0] { + mcp_core::Content::Text(text) => { + assert!(text.text.contains("The Rust Standard Library")); + } + _ => panic!("Expected text content"), + } +} + +// This test requires network access +#[tokio::test] +#[ignore = "Requires network access"] +async fn test_end_to_end_std_item_lookup() { + let router = DocRouter::new(); + + // Look up standard library as a crate + let result = router + .call_tool( + "lookup_item", + json!({ + "crate_name": "std", + "item_path": "sync::mpsc::Sender" + }), + ) + .await; + + assert!(result.is_ok()); + let content = result.unwrap(); + assert_eq!(content.len(), 1); + + // The response should be Markdown converted from docs.rs + match &content[0] { + mcp_core::Content::Text(text) => { + assert!(text.text.contains( + r"The sending-half of Rust’s asynchronous [`channel`](fn.channel.html) type." + )); + } + _ => panic!("Expected text content"), + } +} + +// This test requires network access +#[tokio::test] +#[ignore = "Requires network access"] +async fn test_end_to_end_item_lookup() { + let router = DocRouter::new(); + + // Look up standard library as a crate + let result = router + .call_tool( + "lookup_item", + json!({ + "crate_name": "tokio", + "item_path": "sync::mpsc::Sender" + }), + ) + .await; + + assert!(result.is_ok()); + let content = result.unwrap(); + assert_eq!(content.len(), 1); + + // The response should be Markdown converted from docs.rs + match &content[0] { + mcp_core::Content::Text(text) => { + assert!(text + .text + .contains(r"### impl\ [Sender](struct.Sender.html)\ ###")); + } _ => panic!("Expected text content"), } }