Skip to main content

crates_docs/tools/
health.rs

1//! Health check tool
2#![allow(missing_docs)]
3
4use crate::tools::Tool;
5use async_trait::async_trait;
6use rust_mcp_sdk::macros;
7use serde::{Deserialize, Serialize};
8use std::time::{Duration, Instant};
9
10/// Health check tool parameters
11#[macros::mcp_tool(
12    name = "health_check",
13    title = "Health Check",
14    description = "Check the health status of the server and external services (docs.rs, crates.io). Used for diagnosing connection issues and monitoring system availability.",
15    destructive_hint = false,
16    idempotent_hint = true,
17    open_world_hint = false,
18    read_only_hint = true,
19    execution(task_support = "optional"),
20    icons = [
21        (src = "https://img.icons8.com/color/96/000000/heart-health.png", mime_type = "image/png", sizes = ["96x96"], theme = "light"),
22        (src = "https://img.icons8.com/color/96/000000/heart-health.png", mime_type = "image/png", sizes = ["96x96"], theme = "dark")
23    ]
24)]
25#[derive(Debug, Clone, Deserialize, Serialize, macros::JsonSchema)]
26pub struct HealthCheckTool {
27    /// Check type
28    #[json_schema(
29        title = "Check Type",
30        description = "Type of health check to perform: all (all checks), external (external services: docs.rs, crates.io), internal (internal state), docs_rs (docs.rs only), crates_io (crates.io only)",
31        default = "all"
32    )]
33    pub check_type: Option<String>,
34
35    /// Verbose output
36    #[json_schema(
37        title = "Verbose Output",
38        description = "Whether to show detailed output including response time for each check",
39        default = false
40    )]
41    pub verbose: Option<bool>,
42}
43
44/// Health check result
45#[derive(Debug, Clone, Serialize)]
46struct HealthStatus {
47    status: String,
48    timestamp: String,
49    checks: Vec<HealthCheck>,
50    uptime: Duration,
51}
52
53/// Single health check
54#[derive(Debug, Clone, Serialize)]
55struct HealthCheck {
56    name: String,
57    status: String,
58    duration_ms: u64,
59    message: Option<String>,
60    error: Option<String>,
61}
62
63/// Health check tool implementation
64pub struct HealthCheckToolImpl {
65    start_time: Instant,
66}
67
68impl HealthCheckToolImpl {
69    #[must_use]
70    pub fn new() -> Self {
71        Self {
72            start_time: Instant::now(),
73        }
74    }
75
76    #[allow(clippy::cast_possible_truncation)]
77    async fn check_http_service(
78        name: &'static str,
79        url: &str,
80        healthy_msg: &'static str,
81    ) -> HealthCheck {
82        let start = Instant::now();
83        // Use global HTTP client singleton for connection pool reuse
84        let client = match crate::utils::get_or_init_global_http_client() {
85            Ok(client) => client,
86            Err(e) => {
87                return HealthCheck {
88                    name: name.to_string(),
89                    status: "unhealthy".to_string(),
90                    duration_ms: start.elapsed().as_millis() as u64,
91                    message: None,
92                    error: Some(format!("Failed to initialize HTTP client: {e}")),
93                };
94            }
95        };
96
97        match client
98            .get(url)
99            .header("User-Agent", format!("CratesDocsMCP/{}", crate::VERSION))
100            .timeout(Duration::from_secs(5))
101            .send()
102            .await
103        {
104            Ok(response) => {
105                let duration = start.elapsed();
106                if response.status().is_success() {
107                    HealthCheck {
108                        name: name.to_string(),
109                        status: "healthy".to_string(),
110                        duration_ms: duration.as_millis() as u64,
111                        message: Some(healthy_msg.to_string()),
112                        error: None,
113                    }
114                } else {
115                    HealthCheck {
116                        name: name.to_string(),
117                        status: "unhealthy".to_string(),
118                        duration_ms: duration.as_millis() as u64,
119                        message: None,
120                        error: Some(format!("HTTP status code: {}", response.status())),
121                    }
122                }
123            }
124            Err(e) => {
125                let duration = start.elapsed();
126                HealthCheck {
127                    name: name.to_string(),
128                    status: "unhealthy".to_string(),
129                    duration_ms: duration.as_millis() as u64,
130                    message: None,
131                    error: Some(format!("Request failed: {e}")),
132                }
133            }
134        }
135    }
136
137    #[inline]
138    async fn check_docs_rs(&self) -> HealthCheck {
139        Self::check_http_service("docs.rs", "https://docs.rs/", "Service is healthy").await
140    }
141
142    #[inline]
143    async fn check_crates_io(&self) -> HealthCheck {
144        Self::check_http_service(
145            "crates.io",
146            "https://crates.io/api/v1/crates?q=serde&per_page=1",
147            "API is healthy",
148        )
149        .await
150    }
151
152    /// Check memory usage
153    fn check_memory() -> HealthCheck {
154        HealthCheck {
155            name: "memory".to_string(),
156            status: "healthy".to_string(),
157            duration_ms: 0,
158            message: Some("Memory usage is normal".to_string()),
159            error: None,
160        }
161    }
162
163    async fn perform_checks(&self, check_type: &str, verbose: bool) -> HealthStatus {
164        let checks = match check_type {
165            "all" => {
166                let (docs_rs, crates_io) =
167                    tokio::join!(self.check_docs_rs(), self.check_crates_io());
168                vec![docs_rs, crates_io, Self::check_memory()]
169            }
170            "external" => {
171                let (docs_rs, crates_io) =
172                    tokio::join!(self.check_docs_rs(), self.check_crates_io());
173                vec![docs_rs, crates_io]
174            }
175            "internal" => vec![Self::check_memory()],
176            "docs_rs" => vec![self.check_docs_rs().await],
177            "crates_io" => vec![self.check_crates_io().await],
178            _ => vec![HealthCheck {
179                name: "unknown_check".to_string(),
180                status: "unknown".to_string(),
181                duration_ms: 0,
182                message: None,
183                error: Some(format!("Unknown check type: {check_type}")),
184            }],
185        };
186
187        // Determine overall status
188        let overall_status = if checks.iter().all(|c| c.status == "healthy") {
189            "healthy".to_string()
190        } else if checks.iter().any(|c| c.status == "unhealthy") {
191            "unhealthy".to_string()
192        } else {
193            "degraded".to_string()
194        };
195
196        HealthStatus {
197            status: overall_status,
198            timestamp: chrono::Utc::now().to_rfc3339(),
199            checks: if verbose {
200                checks
201            } else {
202                // In non-verbose mode, only return checks with issues
203                checks
204                    .into_iter()
205                    .filter(|c| c.status != "healthy")
206                    .collect()
207            },
208            uptime: self.start_time.elapsed(),
209        }
210    }
211}
212
213#[async_trait]
214impl Tool for HealthCheckToolImpl {
215    fn definition(&self) -> rust_mcp_sdk::schema::Tool {
216        HealthCheckTool::tool()
217    }
218
219    async fn execute(
220        &self,
221        arguments: serde_json::Value,
222    ) -> std::result::Result<
223        rust_mcp_sdk::schema::CallToolResult,
224        rust_mcp_sdk::schema::CallToolError,
225    > {
226        let params: HealthCheckTool = serde_json::from_value(arguments).map_err(|e| {
227            rust_mcp_sdk::schema::CallToolError::invalid_arguments(
228                "health_check",
229                Some(format!("Parameter parsing failed: {e}")),
230            )
231        })?;
232
233        let check_type = params.check_type.unwrap_or_else(|| "all".to_string());
234        let verbose = params.verbose.unwrap_or(false);
235
236        let health_status = self.perform_checks(&check_type, verbose).await;
237
238        let content = if verbose {
239            // SAFETY: write! to String never fails (writes to memory buffer). unwrap() is safe here.
240            serde_json::to_string_pretty(&health_status).map_err(|e| {
241                rust_mcp_sdk::schema::CallToolError::from_message(format!(
242                    "JSON serialization failed: {e}"
243                ))
244            })?
245        } else {
246            let mut summary = format!(
247                "Status: {}\nUptime: {:.2?}\nTimestamp: {}",
248                health_status.status, health_status.uptime, health_status.timestamp
249            );
250
251            if !health_status.checks.is_empty() {
252                use std::fmt::Write;
253                summary.push_str("\n\nCheck Results:");
254                for check in &health_status.checks {
255                    write!(
256                        summary,
257                        "\n- {}: {} ({:.2}ms)",
258                        check.name, check.status, check.duration_ms
259                    )
260                    .unwrap();
261                    if let Some(ref msg) = check.message {
262                        write!(summary, " - {msg}").unwrap();
263                    }
264                    if let Some(ref err) = check.error {
265                        write!(summary, " [Error: {err}]").unwrap();
266                    }
267                }
268            }
269
270            summary
271        };
272
273        Ok(rust_mcp_sdk::schema::CallToolResult::text_content(vec![
274            content.into(),
275        ]))
276    }
277}
278
279impl Default for HealthCheckToolImpl {
280    fn default() -> Self {
281        Self::new()
282    }
283}