Skip to main content

crates_docs/
lib.rs

1//! Crates Docs MCP Server
2//!
3//! 一个高性能的 Rust crate 文档查询 MCP 服务器,支持多种传输协议和 OAuth 认证。
4//!
5//! # 主要功能
6//!
7//! - **Crate 文档查询**: 从 docs.rs 获取 crate 的完整文档
8//! - **Crate 搜索**: 从 crates.io 搜索 Rust crate
9//! - **项目文档查找**: 查找 crate 中的特定类型、函数或模块
10//! - **健康检查**: 检查服务器和外部服务状态
11//!
12//! # 传输协议支持
13//!
14//! - `stdio`: 标准输入输出(适合 MCP 客户端集成)
15//! - `http`: HTTP 传输(Streamable HTTP)
16//! - `sse`: Server-Sent Events
17//! - `hybrid`: 混合模式(HTTP + SSE)
18//!
19//! # 缓存支持
20//!
21//! - **内存缓存**: 基于 `moka` 的高性能内存缓存,支持 `TinyLFU` 淘汰策略和按条目 TTL
22//! - **Redis 缓存**: 支持分布式部署(需要启用 `cache-redis` feature)
23//!
24//! # 示例
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//!     // 使用默认配置创建服务器
32//!     let config = AppConfig::default();
33//!     let server = CratesDocsServer::new(config)?;
34//!
35//!     // 运行 HTTP 服务器
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/// 重新导出错误类型
60pub use crate::error::{Error, Result};
61/// 重新导出服务器类型
62pub use crate::server::CratesDocsServer;
63
64/// 服务器版本号
65///
66/// 从 `CARGO_PKG_VERSION` 环境变量获取
67pub const VERSION: &str = env!("CARGO_PKG_VERSION");
68
69/// 服务器名称
70pub const NAME: &str = "crates-docs";
71
72/// Initialize the logging system (simple version using boolean parameter)
73///
74/// # Errors
75/// Returns an error if logging system initialization fails
76#[deprecated(note = "Please use init_logging_with_config instead")]
77pub fn init_logging(debug: bool) -> Result<()> {
78    use tracing_subscriber::{fmt, prelude::*, EnvFilter};
79
80    let filter = if debug {
81        EnvFilter::new("debug")
82    } else {
83        EnvFilter::new("info")
84    };
85
86    let fmt_layer = fmt::layer()
87        .with_target(true)
88        .with_thread_ids(true)
89        .with_thread_names(true)
90        .compact();
91
92    tracing_subscriber::registry()
93        .with(filter)
94        .with(fmt_layer)
95        .try_init()
96        .map_err(|e| error::Error::initialization("logging", e.to_string()))?;
97
98    Ok(())
99}
100
101/// Initialize logging system with configuration
102///
103/// # Errors
104/// Returns an error if logging system initialization fails
105pub fn init_logging_with_config(config: &crate::config::LoggingConfig) -> Result<()> {
106    use tracing_subscriber::{fmt, prelude::*, EnvFilter};
107
108    /// Helper macro to create fmt layer with standard configuration
109    macro_rules! fmt_layer {
110        () => {
111            fmt::layer()
112                .with_target(true)
113                .with_thread_ids(true)
114                .with_thread_names(true)
115                .compact()
116        };
117        ($writer:expr) => {
118            fmt::layer()
119                .with_writer($writer)
120                .with_target(true)
121                .with_thread_ids(true)
122                .with_thread_names(true)
123                .compact()
124        };
125    }
126
127    /// Helper macro to initialize subscriber with error handling
128    macro_rules! try_init {
129        ($subscriber:expr) => {
130            $subscriber
131                .try_init()
132                .map_err(|e| error::Error::initialization("logging", e.to_string()))?
133        };
134    }
135
136    // Parse log level
137    let level = match config.level.to_lowercase().as_str() {
138        "trace" => "trace",
139        "debug" => "debug",
140        "warn" => "warn",
141        "error" => "error",
142        _ => "info",
143    };
144
145    let filter = EnvFilter::new(level);
146
147    // Build log layers based on configuration
148    match (config.enable_console, config.enable_file, &config.file_path) {
149        // Enable both console and file logging
150        (true, true, Some(file_path)) => {
151            let (log_dir, log_file_name) = parse_log_path(file_path);
152            ensure_log_directory(&log_dir)?;
153            let file_appender = tracing_appender::rolling::daily(&log_dir, log_file_name);
154
155            try_init!(tracing_subscriber::registry()
156                .with(filter)
157                .with(fmt_layer!())
158                .with(fmt_layer!(file_appender)));
159        }
160
161        // Enable console logging only
162        (true, _, _) | (false, false, _) => {
163            try_init!(tracing_subscriber::registry()
164                .with(filter)
165                .with(fmt_layer!()));
166        }
167
168        // Enable file logging only
169        (false, true, Some(file_path)) => {
170            let (log_dir, log_file_name) = parse_log_path(file_path);
171            ensure_log_directory(&log_dir)?;
172            let file_appender = tracing_appender::rolling::daily(&log_dir, log_file_name);
173
174            try_init!(tracing_subscriber::registry()
175                .with(filter)
176                .with(fmt_layer!(file_appender)));
177        }
178
179        // Other cases, use default console logging
180        _ => {
181            try_init!(tracing_subscriber::registry()
182                .with(filter)
183                .with(fmt_layer!()));
184        }
185    }
186
187    Ok(())
188}
189
190/// Parse log file path into directory and file name components
191fn parse_log_path(file_path: &str) -> (std::path::PathBuf, std::ffi::OsString) {
192    let path = std::path::Path::new(file_path);
193    let log_dir = path
194        .parent()
195        .filter(|p| !p.as_os_str().is_empty())
196        .map_or_else(|| std::path::PathBuf::from("."), std::path::PathBuf::from);
197    let log_file_name = path.file_name().map_or_else(
198        || std::ffi::OsString::from("crates-docs.log"),
199        std::ffi::OsString::from,
200    );
201    (log_dir, log_file_name)
202}
203
204/// Ensure log directory exists
205fn ensure_log_directory(log_dir: &std::path::Path) -> Result<()> {
206    std::fs::create_dir_all(log_dir).map_err(|e| {
207        error::Error::initialization("log_directory", format!("Failed to create: {e}"))
208    })
209}