1#![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#[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 #[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 #[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#[derive(Debug, Clone, Serialize)]
54struct HealthStatus {
55 status: String,
57 timestamp: String,
59 checks: Vec<HealthCheck>,
61 uptime: Duration,
63}
64
65#[derive(Debug, Clone, Serialize)]
67struct HealthCheck {
68 name: String,
70 status: String,
72 duration_ms: u64,
74 message: Option<String>,
76 error: Option<String>,
78}
79
80pub struct HealthCheckToolImpl {
85 start_time: Instant,
87}
88
89impl HealthCheckToolImpl {
90 #[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 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 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 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 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 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}