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    /// Create a new health check tool
70    #[must_use]
71    pub fn new() -> Self {
72        Self {
73            start_time: Instant::now(),
74        }
75    }
76
77    /// Check docs.rs service
78    #[allow(clippy::cast_possible_truncation)]
79    async fn check_docs_rs(&self) -> HealthCheck {
80        let start = Instant::now();
81        let client = reqwest::Client::new();
82
83        match client
84            .get("https://docs.rs/")
85            .header("User-Agent", format!("CratesDocsMCP/{}", crate::VERSION))
86            .timeout(Duration::from_secs(5))
87            .send()
88            .await
89        {
90            Ok(response) => {
91                let duration = start.elapsed();
92                if response.status().is_success() {
93                    HealthCheck {
94                        name: "docs.rs".to_string(),
95                        status: "healthy".to_string(),
96                        duration_ms: duration.as_millis() as u64,
97                        message: Some("Service is healthy".to_string()),
98                        error: None,
99                    }
100                } else {
101                    HealthCheck {
102                        name: "docs.rs".to_string(),
103                        status: "unhealthy".to_string(),
104                        duration_ms: duration.as_millis() as u64,
105                        message: None,
106                        error: Some(format!("HTTP status code: {}", response.status())),
107                    }
108                }
109            }
110            Err(e) => {
111                let duration = start.elapsed();
112                HealthCheck {
113                    name: "docs.rs".to_string(),
114                    status: "unhealthy".to_string(),
115                    duration_ms: duration.as_millis() as u64,
116                    message: None,
117                    error: Some(format!("Request failed: {e}")),
118                }
119            }
120        }
121    }
122
123    /// Check crates.io service
124    #[allow(clippy::cast_possible_truncation)]
125    async fn check_crates_io(&self) -> HealthCheck {
126        let start = Instant::now();
127        let client = reqwest::Client::new();
128
129        match client
130            .get("https://crates.io/api/v1/crates?q=serde&per_page=1")
131            .header("User-Agent", format!("CratesDocsMCP/{}", crate::VERSION))
132            .timeout(Duration::from_secs(5))
133            .send()
134            .await
135        {
136            Ok(response) => {
137                let duration = start.elapsed();
138                if response.status().is_success() {
139                    HealthCheck {
140                        name: "crates.io".to_string(),
141                        status: "healthy".to_string(),
142                        duration_ms: duration.as_millis() as u64,
143                        message: Some("API is healthy".to_string()),
144                        error: None,
145                    }
146                } else {
147                    HealthCheck {
148                        name: "crates.io".to_string(),
149                        status: "unhealthy".to_string(),
150                        duration_ms: duration.as_millis() as u64,
151                        message: None,
152                        error: Some(format!("HTTP status code: {}", response.status())),
153                    }
154                }
155            }
156            Err(e) => {
157                let duration = start.elapsed();
158                HealthCheck {
159                    name: "crates.io".to_string(),
160                    status: "unhealthy".to_string(),
161                    duration_ms: duration.as_millis() as u64,
162                    message: None,
163                    error: Some(format!("Request failed: {e}")),
164                }
165            }
166        }
167    }
168
169    /// Check memory usage
170    fn check_memory() -> HealthCheck {
171        HealthCheck {
172            name: "memory".to_string(),
173            status: "healthy".to_string(),
174            duration_ms: 0,
175            message: Some("Memory usage is normal".to_string()),
176            error: None,
177        }
178    }
179
180    /// Perform all health checks
181    async fn perform_checks(&self, check_type: &str, verbose: bool) -> HealthStatus {
182        let mut checks = Vec::new();
183
184        match check_type {
185            "all" => {
186                checks.push(self.check_docs_rs().await);
187                checks.push(self.check_crates_io().await);
188                checks.push(Self::check_memory());
189            }
190            "external" => {
191                checks.push(self.check_docs_rs().await);
192                checks.push(self.check_crates_io().await);
193            }
194            "internal" => {
195                checks.push(Self::check_memory());
196            }
197            "docs_rs" => {
198                checks.push(self.check_docs_rs().await);
199            }
200            "crates_io" => {
201                checks.push(self.check_crates_io().await);
202            }
203            _ => {
204                checks.push(HealthCheck {
205                    name: "unknown_check".to_string(),
206                    status: "unknown".to_string(),
207                    duration_ms: 0,
208                    message: None,
209                    error: Some(format!("Unknown check type: {check_type}")),
210                });
211            }
212        }
213
214        // Determine overall status
215        let overall_status = if checks.iter().all(|c| c.status == "healthy") {
216            "healthy".to_string()
217        } else if checks.iter().any(|c| c.status == "unhealthy") {
218            "unhealthy".to_string()
219        } else {
220            "degraded".to_string()
221        };
222
223        HealthStatus {
224            status: overall_status,
225            timestamp: chrono::Utc::now().to_rfc3339(),
226            checks: if verbose {
227                checks
228            } else {
229                // In non-verbose mode, only return checks with issues
230                checks
231                    .into_iter()
232                    .filter(|c| c.status != "healthy")
233                    .collect()
234            },
235            uptime: self.start_time.elapsed(),
236        }
237    }
238}
239
240#[async_trait]
241impl Tool for HealthCheckToolImpl {
242    fn definition(&self) -> rust_mcp_sdk::schema::Tool {
243        HealthCheckTool::tool()
244    }
245
246    async fn execute(
247        &self,
248        arguments: serde_json::Value,
249    ) -> std::result::Result<
250        rust_mcp_sdk::schema::CallToolResult,
251        rust_mcp_sdk::schema::CallToolError,
252    > {
253        let params: HealthCheckTool = serde_json::from_value(arguments).map_err(|e| {
254            rust_mcp_sdk::schema::CallToolError::invalid_arguments(
255                "health_check",
256                Some(format!("Parameter parsing failed: {e}")),
257            )
258        })?;
259
260        let check_type = params.check_type.unwrap_or_else(|| "all".to_string());
261        let verbose = params.verbose.unwrap_or(false);
262
263        let health_status = self.perform_checks(&check_type, verbose).await;
264
265        let content = if verbose {
266            serde_json::to_string_pretty(&health_status).map_err(|e| {
267                rust_mcp_sdk::schema::CallToolError::from_message(format!(
268                    "JSON serialization failed: {e}"
269                ))
270            })?
271        } else {
272            let mut summary = format!(
273                "Status: {}\nUptime: {:.2?}\nTimestamp: {}",
274                health_status.status, health_status.uptime, health_status.timestamp
275            );
276
277            if !health_status.checks.is_empty() {
278                use std::fmt::Write;
279                summary.push_str("\n\nCheck Results:");
280                for check in &health_status.checks {
281                    write!(
282                        summary,
283                        "\n- {}: {} ({:.2}ms)",
284                        check.name, check.status, check.duration_ms
285                    )
286                    .unwrap();
287                    if let Some(ref msg) = check.message {
288                        write!(summary, " - {msg}").unwrap();
289                    }
290                    if let Some(ref err) = check.error {
291                        write!(summary, " [Error: {err}]").unwrap();
292                    }
293                }
294            }
295
296            summary
297        };
298
299        Ok(rust_mcp_sdk::schema::CallToolResult::text_content(vec![
300            content.into(),
301        ]))
302    }
303}
304
305impl Default for HealthCheckToolImpl {
306    fn default() -> Self {
307        Self::new()
308    }
309}