Skip to main content

crates_docs/tools/
health.rs

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