Skip to content

Commit c630f90

Browse files
committed
refactor: env to args
1 parent bb5cbec commit c630f90

File tree

6 files changed

+114
-22
lines changed

6 files changed

+114
-22
lines changed

Cargo.lock

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,6 @@ futures = "0.3"
1919
bincode = { version = "2.0.1", features = ["serde"] } # Enable serde integration
2020
tiktoken-rs = "0.6.0"
2121
cargo = "0.86.0"
22+
tempfile = "3.19.1"
23+
xdg = { version = "2.5.2", features = ["serde"] }
24+
anyhow = "1.0.97"

flake.nix

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656
rustVersion = "stable";
5757
# Example configuration:
5858
# withTools = [ ]; # Will be prefixed with cargo-
59-
# extraPackages = [ ];
59+
extraPackages = [ pkgs.openssl.dev pkgs.pkg-config ]; # Add openssl dev libs and pkg-config
6060
# ide.type = "none";
6161
};
6262

src/doc_loader.rs

Lines changed: 68 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
use scraper::{Html, Selector};
22
use std::fs;
3-
use std::path::Path;
3+
use cargo::core::resolver::features::CliFeatures;
4+
// use cargo::core::SourceId; // Removed unused import
5+
// use cargo::util::Filesystem; // Removed unused import
6+
7+
use cargo::core::Workspace;
8+
use cargo::ops::{self, CompileOptions, DocOptions, Packages};
9+
use cargo::util::context::GlobalContext;
10+
use anyhow::Error as AnyhowError;
11+
// use std::process::Command; // Remove Command again
12+
use tempfile::tempdir;
413
use thiserror::Error;
514
use walkdir::WalkDir;
615

@@ -10,10 +19,12 @@ pub enum DocLoaderError {
1019
Io(#[from] std::io::Error),
1120
#[error("WalkDir Error: {0}")]
1221
WalkDir(#[from] walkdir::Error),
13-
// #[error("Failed to parse HTML for file: {0}")] // Commented out unused variant
14-
// HtmlParsing(String), // Commented out unused variant
1522
#[error("CSS selector error: {0}")]
16-
Selector(String), // Using String as SelectorErrorKind is not easily clonable/convertible
23+
Selector(String),
24+
#[error("Temporary directory creation failed: {0}")]
25+
TempDirCreationFailed(std::io::Error),
26+
#[error("Cargo library error: {0}")]
27+
CargoLib(#[from] AnyhowError), // Re-add CargoLib variant
1728
}
1829

1930
// Simple struct to hold document content, maybe add path later if needed
@@ -23,11 +34,60 @@ pub struct Document {
2334
pub content: String,
2435
}
2536

26-
/// Loads and parses HTML documents from a given directory path.
37+
/// Generates documentation for a given crate in a temporary directory,
38+
/// then loads and parses the HTML documents.
2739
/// Extracts text content from the main content area of rustdoc generated HTML.
28-
pub fn load_documents(docs_path: &str) -> Result<Vec<Document>, DocLoaderError> {
40+
pub fn load_documents(crate_name: &str, _crate_version: &str) -> Result<Vec<Document>, DocLoaderError> { // Mark version as unused for now
2941
let mut documents = Vec::new();
30-
let docs_path = Path::new(docs_path);
42+
43+
let temp_dir = tempdir().map_err(DocLoaderError::TempDirCreationFailed)?;
44+
let temp_dir_path = temp_dir.path();
45+
46+
println!(
47+
"Generating documentation for crate '{}' in temporary directory: {}",
48+
crate_name,
49+
temp_dir_path.display()
50+
);
51+
52+
// Execute `cargo doc` using std::process::Command
53+
// --- Use Cargo API ---
54+
let config = GlobalContext::default()?;
55+
// config.shell().set_verbosity(Verbosity::Quiet); // Keep commented
56+
57+
let current_dir = std::env::current_dir()?;
58+
let mut ws = Workspace::new(&current_dir.join("Cargo.toml"), &config)?; // Make ws mutable
59+
// Set target_dir directly on Workspace
60+
ws.set_target_dir(cargo::util::Filesystem::new(temp_dir_path.to_path_buf()));
61+
62+
// Create CompileOptions, relying on ::new for BuildConfig
63+
let mut compile_opts = CompileOptions::new(&config, cargo::core::compiler::CompileMode::Doc { deps: false, json: false })?;
64+
// Specify the package explicitly
65+
let package_spec = crate_name.replace('-', "_"); // Just use name (with underscores)
66+
compile_opts.cli_features = CliFeatures::new_all(false); // Use new_all(false)
67+
compile_opts.spec = Packages::Packages(vec![package_spec]);
68+
69+
// Create DocOptions: Pass compile options
70+
let doc_opts = DocOptions {
71+
compile_opts,
72+
open_result: false, // Don't open in browser
73+
output_format: ops::OutputFormat::Html,
74+
};
75+
76+
ops::doc(&ws, &doc_opts).map_err(DocLoaderError::CargoLib)?; // Use ws
77+
// --- End Cargo API ---
78+
// Construct the path to the generated documentation within the temp directory
79+
// Cargo uses underscores in the directory path if the crate name has hyphens
80+
let crate_name_underscores = crate_name.replace('-', "_");
81+
let docs_path = temp_dir_path.join("doc").join(&crate_name_underscores);
82+
83+
if !docs_path.exists() || !docs_path.is_dir() {
84+
return Err(DocLoaderError::CargoLib(anyhow::anyhow!(
85+
"Generated documentation not found at expected path: {}. Check crate name and cargo doc output.",
86+
docs_path.display()
87+
)));
88+
}
89+
println!("Generated documentation path: {}", docs_path.display());
90+
3191

3292
// Define the CSS selector for the main content area in rustdoc HTML
3393
// This might need adjustment based on the exact rustdoc version/theme
@@ -39,7 +99,7 @@ pub fn load_documents(docs_path: &str) -> Result<Vec<Document>, DocLoaderError>
3999
for entry in WalkDir::new(docs_path)
40100
.into_iter()
41101
.filter_map(Result::ok) // Ignore errors during iteration for now
42-
.filter(|e| !e.file_type().is_dir() && e.path().extension().map_or(false, |ext| ext == "html"))
102+
.filter(|e| !e.file_type().is_dir() && e.path().extension().is_some_and(|ext| ext == "html"))
43103
{
44104
let path = entry.path();
45105
let path_str = path.to_string_lossy().to_string();

src/error.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ use crate::doc_loader::DocLoaderError; // Need to import DocLoaderError from the
66
pub enum ServerError {
77
#[error("Environment variable not set: {0}")]
88
MissingEnvVar(String),
9+
#[error("Missing command line argument: {0}")]
10+
MissingArgument(String),
911
#[error("MCP Service Error: {0}")]
1012
Mcp(#[from] ServiceError), // Use ServiceError
1113
#[error("IO Error: {0}")]
@@ -18,4 +20,6 @@ pub enum ServerError {
1820
Json(#[from] serde_json::Error), // Add error for JSON deserialization
1921
#[error("Tiktoken Error: {0}")]
2022
Tiktoken(String),
23+
#[error("XDG Directory Error: {0}")]
24+
Xdg(String),
2125
}

src/main.rs

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ use ndarray::Array1;
2727
use std::fs::{self, File};
2828
use std::io::BufReader; // Removed unused BufWriter
2929
use std::path::PathBuf; // Removed unused Path
30+
use xdg::BaseDirectories;
3031
use bincode::{
3132
config,
3233
// serde::OwnedSerdeDecoder, // No longer needed
@@ -40,17 +41,23 @@ async fn main() -> Result<(), ServerError> {
4041
// Load .env file if present
4142
dotenvy::dotenv().ok();
4243

43-
// Get required environment variables
44-
let crate_name =
45-
env::var("CRATE_NAME").map_err(|_| ServerError::MissingEnvVar("CRATE_NAME".to_string()))?;
46-
let docs_path =
47-
env::var("DOCS_PATH").map_err(|_| ServerError::MissingEnvVar("DOCS_PATH".to_string()))?;
44+
// Get crate name and version from command line arguments
45+
let mut args = env::args().skip(1); // Skip program name
46+
let crate_name = args.next().ok_or_else(|| {
47+
eprintln!("Usage: rustdocs_mcp_server <CRATE_NAME> <CRATE_VERSION>");
48+
ServerError::MissingArgument("CRATE_NAME".to_string())
49+
})?;
50+
let crate_version = args.next().ok_or_else(|| {
51+
eprintln!("Usage: rustdocs_mcp_server <CRATE_NAME> <CRATE_VERSION>");
52+
ServerError::MissingArgument("CRATE_VERSION".to_string())
53+
})?;
54+
4855
let _openai_api_key = env::var("OPENAI_API_KEY")
4956
.map_err(|_| ServerError::MissingEnvVar("OPENAI_API_KEY".to_string()))?; // Needed later
5057

51-
// Load documents
52-
println!("Loading documents from: {}", docs_path);
53-
let documents = load_documents(&docs_path)?;
58+
// Load documents by generating them dynamically
59+
println!("Loading documents for crate: {}", crate_name);
60+
let documents = load_documents(&crate_name, &crate_version)?; // Pass crate_name and crate_version
5461
println!("Loaded {} documents.", documents.len());
5562

5663
// Initialize OpenAI client and set it in the OnceLock
@@ -60,8 +67,16 @@ async fn main() -> Result<(), ServerError> {
6067
.expect("Failed to set OpenAI client");
6168

6269
// --- Persistence Logic ---
63-
let persist_dir = PathBuf::from("storage").join(&crate_name);
64-
let embeddings_file_path = persist_dir.join("embeddings.bin");
70+
// Use XDG Base Directory specification for data storage
71+
let xdg_dirs = BaseDirectories::with_prefix("rustdocs-mcp-server")
72+
.map_err(|e| ServerError::Xdg(format!("Failed to get XDG directories: {}", e)))?; // Use the new Xdg variant
73+
74+
// Construct the path within the XDG data directory, including the crate name
75+
let relative_path = PathBuf::from(&crate_name).join("embeddings.bin");
76+
77+
// Use place_data_file to get the full path and ensure parent directories exist
78+
let embeddings_file_path = xdg_dirs.place_data_file(relative_path)
79+
.map_err(ServerError::Io)?; // Map IO error if directory creation fails
6580

6681
let embeddings = if embeddings_file_path.exists() {
6782
println!("Loading embeddings from: {:?}", embeddings_file_path);
@@ -105,9 +120,7 @@ async fn main() -> Result<(), ServerError> {
105120
let embeddings = match embeddings {
106121
Some(e) => e,
107122
None => {
108-
// Ensure persist directory exists
109-
fs::create_dir_all(&persist_dir)
110-
.map_err(|e| ServerError::Io(e))?; // Map IO error
123+
// Directory creation is handled by xdg_dirs.place_data_file
111124

112125
// Generate embeddings
113126
println!("Generating embeddings...");

0 commit comments

Comments
 (0)