Skip to content

Multi crate calling #14

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,7 @@ doc
.rust*
debug/
storage/
.roo/
.roo/

# claude
.mcp.json
33 changes: 19 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,26 +111,31 @@ necessary for crates that require specific features to be enabled for
`cargo doc` to succeed (e.g., crates requiring a runtime feature like
`async-stripe`).

```bash
# Set the API key (replace with your actual key)
export OPENAI_API_KEY="sk-..."

# Example: Run server for the latest 1.x version of serde
rustdocs_mcp_server "serde@^1.0"

# Example: Run server for a specific version of reqwest
rustdocs_mcp_server "[email protected]"
#### Multi-Crate Mode

# Example: Run server for the latest version of tokio
rustdocs_mcp_server tokio
You can also run a single server instance that provides documentation for multiple crates by listing them as arguments:

# Example: Run server for async-stripe, enabling a required runtime feature
rustdocs_mcp_server "[email protected]" -F runtime-tokio-hyper-rustls
```bash
# Set the API key (replace with your actual key)
export OPENAI_API_KEY="sk-..."

# Example: Run server for another crate with multiple features
rustdocs_mcp_server "[email protected]" --features feat1,feat2
# Example: Run server for multiple crates
rust-mcp-docs rmcp serde chrono:serde tokio openai tera dotenvy clap:derive:env

# This creates tools for each crate:
# - query_rmcp_docs
# - query_serde_docs
# - query_chrono_docs
# - query_tokio_docs
# - query_openai_docs
# - query_tera_docs
# - query_dotenvy_docs
# - query_clap_docs
```

In multi-crate mode, crate specifications can include features after a colon (e.g., `chrono:serde`, `clap:derive:env`). Each crate will have its own documentation tool named `query_{crate_name}_docs`.

On the first run for a specific crate version _and feature set_, the server
will:

Expand Down
60 changes: 43 additions & 17 deletions src/doc_loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,7 @@ edition = "2021"
// Iterate through subdirectories in `target/doc` and find the one containing `index.html`.
let base_doc_path = temp_dir_path.join("doc");

let mut target_docs_path: Option<PathBuf> = None;
let mut found_count = 0;
let mut found_paths: Vec<PathBuf> = Vec::new();

if base_doc_path.is_dir() {
for entry_result in fs::read_dir(&base_doc_path)? {
Expand All @@ -136,31 +135,58 @@ edition = "2021"
let dir_path = entry.path();
let index_html_path = dir_path.join("index.html");
if index_html_path.is_file() {
if target_docs_path.is_none() {
target_docs_path = Some(dir_path);
}
found_count += 1;
} else {
found_paths.push(dir_path);
}
}
}
eprintln!("[DEBUG] Found {} directories with index.html files", found_paths.len());
for (i, path) in found_paths.iter().enumerate() {
eprintln!("[DEBUG] Directory {}: {}", i + 1, path.display());
}
}

let docs_path = match (found_count, target_docs_path) {
(1, Some(path)) => {
path
},
(0, _) => {
let docs_path = match found_paths.len() {
0 => {
return Err(DocLoaderError::CargoLib(anyhow::anyhow!(
"Could not find any subdirectory containing index.html within '{}'. Cargo doc might have failed or produced unexpected output.",
base_doc_path.display()
)));
},
(count, _) => {
return Err(DocLoaderError::CargoLib(anyhow::anyhow!(
"Expected exactly one subdirectory containing index.html within '{}', but found {}. Cannot determine the correct documentation path.",
base_doc_path.display(), count
)));
1 => {
found_paths.into_iter().next().unwrap()
},
_ => {
// Multiple directories found - look specifically for the crate name directory
let crate_name_normalized = crate_name.replace('-', "_");
eprintln!("[DEBUG] Multiple directories found, looking for crate directory: {}", crate_name_normalized);

// Find the directory that matches the crate name
let matching_dir = found_paths.iter().find(|path| {
if let Some(dir_name) = path.file_name() {
if let Some(dir_name_str) = dir_name.to_str() {
return dir_name_str == crate_name_normalized;
}
}
false
});

match matching_dir {
Some(crate_dir) => {
eprintln!("[DEBUG] Found crate-specific directory: {}", crate_dir.display());
crate_dir.clone()
},
None => {
eprintln!("[DEBUG] Crate-specific directory '{}' not found in available directories:", crate_name_normalized);
for path in &found_paths {
if let Some(dir_name) = path.file_name() {
eprintln!("[DEBUG] - {}", dir_name.to_string_lossy());
}
}
// Fallback to the first directory found
eprintln!("[DEBUG] Using first available directory: {}", found_paths[0].display());
found_paths.into_iter().next().unwrap()
}
}
}
};
// --- End finding documentation directory ---
Expand Down
Loading