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 error;
52pub mod metrics;
53pub mod server;
54pub mod tools;
55pub mod utils;
56
57pub use crate::config::{AppConfig, ServerConfig};
58/// 重新导出错误类型
59pub use crate::error::{Error, Result};
60/// 重新导出服务器类型
61pub use crate::server::CratesDocsServer;
62
63/// 服务器版本号
64///
65/// 从 `CARGO_PKG_VERSION` 环境变量获取
66pub const VERSION: &str = env!("CARGO_PKG_VERSION");
67
68/// 服务器名称
69pub const NAME: &str = "crates-docs";
70
71/// Initialize the logging system (simple version using boolean parameter)
72///
73/// # Errors
74/// Returns an error if logging system initialization fails
75#[deprecated(note = "Please use init_logging_with_config instead")]
76pub fn init_logging(debug: bool) -> Result<()> {
77    use tracing_subscriber::{fmt, prelude::*, EnvFilter};
78
79    let filter = if debug {
80        EnvFilter::new("debug")
81    } else {
82        EnvFilter::new("info")
83    };
84
85    let fmt_layer = fmt::layer()
86        .with_target(true)
87        .with_thread_ids(true)
88        .with_thread_names(true)
89        .compact();
90
91    tracing_subscriber::registry()
92        .with(filter)
93        .with(fmt_layer)
94        .try_init()
95        .map_err(|e| error::Error::initialization("logging", e.to_string()))?;
96
97    Ok(())
98}
99
100/// Initialize logging system with configuration
101///
102/// # Errors
103/// Returns an error if logging system initialization fails
104pub fn init_logging_with_config(config: &crate::config::LoggingConfig) -> Result<()> {
105    use tracing_subscriber::{fmt, prelude::*, EnvFilter};
106
107    /// Helper macro to create fmt layer with standard configuration
108    macro_rules! fmt_layer {
109        () => {
110            fmt::layer()
111                .with_target(true)
112                .with_thread_ids(true)
113                .with_thread_names(true)
114                .compact()
115        };
116        ($writer:expr) => {
117            fmt::layer()
118                .with_writer($writer)
119                .with_target(true)
120                .with_thread_ids(true)
121                .with_thread_names(true)
122                .compact()
123        };
124    }
125
126    /// Helper macro to initialize subscriber with error handling
127    macro_rules! try_init {
128        ($subscriber:expr) => {
129            $subscriber
130                .try_init()
131                .map_err(|e| error::Error::initialization("logging", e.to_string()))?
132        };
133    }
134
135    // Parse log level
136    let level = match config.level.to_lowercase().as_str() {
137        "trace" => "trace",
138        "debug" => "debug",
139        "warn" => "warn",
140        "error" => "error",
141        _ => "info",
142    };
143
144    let filter = EnvFilter::new(level);
145
146    // Build log layers based on configuration
147    match (config.enable_console, config.enable_file, &config.file_path) {
148        // Enable both console and file logging
149        (true, true, Some(file_path)) => {
150            let (log_dir, log_file_name) = parse_log_path(file_path);
151            ensure_log_directory(&log_dir)?;
152            let file_appender = tracing_appender::rolling::daily(&log_dir, log_file_name);
153
154            try_init!(tracing_subscriber::registry()
155                .with(filter)
156                .with(fmt_layer!())
157                .with(fmt_layer!(file_appender)));
158        }
159
160        // Enable console logging only
161        (true, _, _) | (false, false, _) => {
162            try_init!(tracing_subscriber::registry()
163                .with(filter)
164                .with(fmt_layer!()));
165        }
166
167        // Enable file logging only
168        (false, true, Some(file_path)) => {
169            let (log_dir, log_file_name) = parse_log_path(file_path);
170            ensure_log_directory(&log_dir)?;
171            let file_appender = tracing_appender::rolling::daily(&log_dir, log_file_name);
172
173            try_init!(tracing_subscriber::registry()
174                .with(filter)
175                .with(fmt_layer!(file_appender)));
176        }
177
178        // Other cases, use default console logging
179        _ => {
180            try_init!(tracing_subscriber::registry()
181                .with(filter)
182                .with(fmt_layer!()));
183        }
184    }
185
186    Ok(())
187}
188
189/// Parse log file path into directory and file name components
190fn parse_log_path(file_path: &str) -> (std::path::PathBuf, std::ffi::OsString) {
191    let path = std::path::Path::new(file_path);
192    let log_dir = path
193        .parent()
194        .filter(|p| !p.as_os_str().is_empty())
195        .map_or_else(|| std::path::PathBuf::from("."), std::path::PathBuf::from);
196    let log_file_name = path.file_name().map_or_else(
197        || std::ffi::OsString::from("crates-docs.log"),
198        std::ffi::OsString::from,
199    );
200    (log_dir, log_file_name)
201}
202
203/// Ensure log directory exists
204fn ensure_log_directory(log_dir: &std::path::Path) -> Result<()> {
205    std::fs::create_dir_all(log_dir).map_err(|e| {
206        error::Error::initialization("log_directory", format!("Failed to create: {e}"))
207    })
208}