Skip to main content

crates_docs/
lib.rs

1//! Crates Docs MCP Server
2//!
3//! A high-performance Rust crate documentation query MCP server, supporting multiple transport protocols and OAuth authentication.
4//!
5//! # Main Features
6//!
7//! - **Crate Documentation Query**: Fetch complete documentation for a crate from docs.rs
8//! - **Crate Search**: Search Rust crates from crates.io
9//! - **Item Documentation Lookup**: Find specific types, functions, or modules in a crate
10//! - **Health Check**: Check server and external service status
11//!
12//! # Transport Protocol Support
13//!
14//! - `stdio`: Standard input/output (suitable for MCP client integration)
15//! - `http`: HTTP transport (Streamable HTTP)
16//! - `sse`: Server-Sent Events
17//! - `hybrid`: Hybrid mode (HTTP + SSE)
18//!
19//! # Cache Support
20//!
21//! - **Memory Cache**: High-performance memory cache based on `moka`, supporting `TinyLFU` eviction strategy and per-entry TTL
22//! - **Redis Cache**: Supports distributed deployment (requires `cache-redis` feature)
23//!
24//! # Example
25//!
26//! ```rust,no_run
27//! use crates_docs::{AppConfig, CratesDocsServer};
28//!
29//! #[tokio::main]
30//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
31//!     // Create server with default configuration
32//!     let config = AppConfig::default();
33//!     let server = CratesDocsServer::new(config)?;
34//!
35//!     // Run HTTP server
36//!     server.run_http().await?;
37//!
38//!     Ok(())
39//! }
40//! ```
41
42#![warn(missing_docs)]
43#![warn(clippy::pedantic)]
44#![allow(clippy::module_name_repetitions)]
45#![allow(clippy::missing_errors_doc)]
46#![allow(clippy::missing_panics_doc)]
47
48pub mod cache;
49pub mod cli;
50pub mod config;
51pub mod config_reload;
52pub mod error;
53pub mod metrics;
54pub mod server;
55pub mod tools;
56pub mod utils;
57
58pub use crate::config::{AppConfig, ServerConfig};
59/// Re-export error types
60pub use crate::error::{Error, Result};
61/// Re-export server types
62pub use crate::server::CratesDocsServer;
63
64/// Server version
65///
66/// Obtained from `CARGO_PKG_VERSION` environment variable
67pub const VERSION: &str = env!("CARGO_PKG_VERSION");
68
69/// Server name
70pub const NAME: &str = "crates-docs";
71
72/// Initialize logging system with configuration
73///
74/// # Errors
75/// Returns an error if logging system initialization fails
76pub fn init_logging_with_config(config: &crate::config::LoggingConfig) -> Result<()> {
77    use tracing_subscriber::{fmt, prelude::*, EnvFilter};
78
79    /// Helper macro to create fmt layer with standard configuration
80    macro_rules! fmt_layer {
81        () => {
82            fmt::layer()
83                .with_target(true)
84                .with_thread_ids(true)
85                .with_thread_names(true)
86                .compact()
87        };
88        ($writer:expr) => {
89            fmt::layer()
90                .with_writer($writer)
91                .with_target(true)
92                .with_thread_ids(true)
93                .with_thread_names(true)
94                .compact()
95        };
96    }
97
98    /// Helper macro to initialize subscriber with error handling
99    macro_rules! try_init {
100        ($subscriber:expr) => {
101            $subscriber
102                .try_init()
103                .map_err(|e| error::Error::initialization("logging", e.to_string()))?
104        };
105    }
106
107    // Parse log level
108    let level = config.level.to_lowercase();
109    let level = match level.as_str() {
110        "trace" | "debug" | "warn" | "error" => level.clone(),
111        _ => "info".to_string(),
112    };
113
114    let filter = EnvFilter::new(level);
115
116    // Build log layers based on configuration
117    match (config.enable_console, config.enable_file, &config.file_path) {
118        (true, true, Some(file_path)) => {
119            // Enable both console and file logging
120            let (log_dir, log_file_name) = parse_log_path(file_path);
121            ensure_log_directory(&log_dir)?;
122            let file_appender = tracing_appender::rolling::daily(&log_dir, log_file_name);
123
124            try_init!(tracing_subscriber::registry()
125                .with(filter)
126                .with(fmt_layer!())
127                .with(fmt_layer!(file_appender)));
128        }
129
130        (false, true, Some(file_path)) => {
131            // Enable file logging only
132            let (log_dir, log_file_name) = parse_log_path(file_path);
133            ensure_log_directory(&log_dir)?;
134            let file_appender = tracing_appender::rolling::daily(&log_dir, log_file_name);
135
136            try_init!(tracing_subscriber::registry()
137                .with(filter)
138                .with(fmt_layer!(file_appender)));
139        }
140
141        // Default: console logging (covers all other cases)
142        _ => {
143            try_init!(tracing_subscriber::registry()
144                .with(filter)
145                .with(fmt_layer!()));
146        }
147    }
148
149    Ok(())
150}
151
152/// Parse log file path into directory and file name components
153fn parse_log_path(file_path: &str) -> (std::path::PathBuf, std::ffi::OsString) {
154    let path = std::path::Path::new(file_path);
155    let log_dir = path
156        .parent()
157        .filter(|p| !p.as_os_str().is_empty())
158        .map_or_else(|| std::path::PathBuf::from("."), std::path::PathBuf::from);
159    let log_file_name = path.file_name().map_or_else(
160        || std::ffi::OsString::from("crates-docs.log"),
161        std::ffi::OsString::from,
162    );
163    (log_dir, log_file_name)
164}
165
166/// Ensure log directory exists
167fn ensure_log_directory(log_dir: &std::path::Path) -> Result<()> {
168    std::fs::create_dir_all(log_dir).map_err(|e| {
169        error::Error::initialization("log_directory", format!("Failed to create: {e}"))
170    })
171}