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
15const VALID_CHECK_TYPES: &[&str] = &["all", "external", "internal", "docs_rs", "crates_io"];
18
19#[macros::mcp_tool(
24 name = "health_check",
25 title = "Health Check",
26 description = "Check the health status of the server and external services (docs.rs, crates.io). Used for diagnosing connection issues and monitoring system availability.",
27 destructive_hint = false,
28 idempotent_hint = true,
29 open_world_hint = false,
30 read_only_hint = true,
31 icons = [
32 (src = "https://img.icons8.com/color/96/000000/heart-health.png", mime_type = "image/png", sizes = ["96x96"], theme = "light"),
33 (src = "https://img.icons8.com/color/96/000000/heart-health.png", mime_type = "image/png", sizes = ["96x96"], theme = "dark")
34 ]
35)]
36#[derive(Debug, Clone, Deserialize, Serialize, macros::JsonSchema)]
37pub struct HealthCheckTool {
38 #[json_schema(
40 title = "Check Type",
41 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)",
42 default = "all"
43 )]
44 pub check_type: Option<String>,
45
46 #[json_schema(
48 title = "Verbose Output",
49 description = "Whether to show detailed output including response time for each check",
50 default = false
51 )]
52 pub verbose: Option<bool>,
53}
54
55#[derive(Debug, Clone, Serialize)]
57struct HealthStatus {
58 status: String,
60 timestamp: String,
62 checks: Vec<HealthCheck>,
64 uptime: Duration,
66}
67
68#[derive(Debug, Clone, Serialize)]
70struct HealthCheck {
71 name: String,
73 status: String,
75 duration_ms: u64,
77 message: Option<String>,
79 error: Option<String>,
81}
82
83pub struct HealthCheckToolImpl {
88 start_time: Instant,
90}
91
92impl HealthCheckToolImpl {
93 #[must_use]
98 pub fn new() -> Self {
99 Self {
100 start_time: Instant::now(),
101 }
102 }
103
104 #[allow(clippy::cast_possible_truncation)]
105 async fn check_http_service(
106 name: &'static str,
107 url: &str,
108 healthy_msg: &'static str,
109 ) -> HealthCheck {
110 let start = Instant::now();
111 let client = match crate::utils::get_or_init_global_http_client() {
113 Ok(client) => client,
114 Err(e) => {
115 return HealthCheck {
116 name: name.to_string(),
117 status: "unhealthy".to_string(),
118 duration_ms: start.elapsed().as_millis() as u64,
119 message: None,
120 error: Some(format!("Failed to initialize HTTP client: {e}")),
121 };
122 }
123 };
124
125 match client
126 .get(url)
127 .header("User-Agent", crate::user_agent())
128 .timeout(Duration::from_secs(5))
129 .send()
130 .await
131 {
132 Ok(response) => {
133 let duration = start.elapsed();
134 if response.status().is_success() {
135 HealthCheck {
136 name: name.to_string(),
137 status: "healthy".to_string(),
138 duration_ms: duration.as_millis() as u64,
139 message: Some(healthy_msg.to_string()),
140 error: None,
141 }
142 } else {
143 HealthCheck {
144 name: name.to_string(),
145 status: "unhealthy".to_string(),
146 duration_ms: duration.as_millis() as u64,
147 message: None,
148 error: Some(format!("HTTP status code: {}", response.status())),
149 }
150 }
151 }
152 Err(e) => {
153 let duration = start.elapsed();
154 HealthCheck {
155 name: name.to_string(),
156 status: "unhealthy".to_string(),
157 duration_ms: duration.as_millis() as u64,
158 message: None,
159 error: Some(format!("Request failed: {e}")),
160 }
161 }
162 }
163 }
164
165 #[inline]
166 async fn check_docs_rs(&self) -> HealthCheck {
167 Self::check_http_service("docs.rs", "https://docs.rs/", "Service is healthy").await
168 }
169
170 #[inline]
171 async fn check_crates_io(&self) -> HealthCheck {
172 Self::check_http_service(
173 "crates.io",
174 "https://crates.io/api/v1/crates?q=serde&per_page=1",
175 "API is healthy",
176 )
177 .await
178 }
179
180 fn check_memory() -> HealthCheck {
187 let message = Self::memory_message();
188 HealthCheck {
189 name: "memory".to_string(),
190 status: "healthy".to_string(),
191 duration_ms: 0,
192 message: Some(message),
193 error: None,
194 }
195 }
196
197 #[cfg(target_os = "linux")]
198 fn memory_message() -> String {
199 match Self::read_process_rss_bytes() {
200 Some(bytes) => {
201 let mib = bytes / (1024 * 1024);
203 let frac = (bytes % (1024 * 1024)) * 10 / (1024 * 1024);
204 format!("Resident set size: {mib}.{frac} MiB")
205 }
206 None => "Memory metrics unavailable (could not read /proc/self/statm)".to_string(),
207 }
208 }
209
210 #[cfg(not(target_os = "linux"))]
211 fn memory_message() -> String {
212 "Memory metrics are not implemented on this platform".to_string()
213 }
214
215 #[cfg(target_os = "linux")]
217 fn read_process_rss_bytes() -> Option<u64> {
218 let statm = std::fs::read_to_string("/proc/self/statm").ok()?;
219 let resident_pages: u64 = statm.split_whitespace().nth(1)?.parse().ok()?;
221 let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
223 let page_size = u64::try_from(page_size).unwrap_or(4096);
224 Some(resident_pages.saturating_mul(page_size))
225 }
226
227 async fn perform_checks(&self, check_type: &str, verbose: bool) -> HealthStatus {
228 let checks = match check_type {
229 "all" => {
230 let (docs_rs, crates_io) =
231 tokio::join!(self.check_docs_rs(), self.check_crates_io());
232 vec![docs_rs, crates_io, Self::check_memory()]
233 }
234 "external" => {
235 let (docs_rs, crates_io) =
236 tokio::join!(self.check_docs_rs(), self.check_crates_io());
237 vec![docs_rs, crates_io]
238 }
239 "internal" => vec![Self::check_memory()],
240 "docs_rs" => vec![self.check_docs_rs().await],
241 "crates_io" => vec![self.check_crates_io().await],
242 _ => vec![HealthCheck {
243 name: "unknown_check".to_string(),
244 status: "unknown".to_string(),
245 duration_ms: 0,
246 message: None,
247 error: Some(format!("Unknown check type: {check_type}")),
248 }],
249 };
250
251 let overall_status = if checks.iter().all(|c| c.status == "healthy") {
253 "healthy".to_string()
254 } else if checks.iter().any(|c| c.status == "unhealthy") {
255 "unhealthy".to_string()
256 } else {
257 "degraded".to_string()
258 };
259
260 HealthStatus {
261 status: overall_status,
262 timestamp: chrono::Utc::now().to_rfc3339(),
263 checks: if verbose {
264 checks
265 } else {
266 checks
268 .into_iter()
269 .filter(|c| c.status != "healthy")
270 .collect()
271 },
272 uptime: self.start_time.elapsed(),
273 }
274 }
275
276 fn render_report(health_status: &HealthStatus, verbose: bool) -> String {
282 if verbose {
283 serde_json::to_string_pretty(health_status)
284 .unwrap_or_else(|e| format!("JSON serialization failed: {e}"))
285 } else {
286 let mut summary = format!(
287 "Status: {}\nUptime: {:.2?}\nTimestamp: {}",
288 health_status.status, health_status.uptime, health_status.timestamp
289 );
290
291 if !health_status.checks.is_empty() {
292 use std::fmt::Write;
293 summary.push_str("\n\nCheck Results:");
294 for check in &health_status.checks {
295 let _ = write!(
296 summary,
297 "\n- {}: {} ({:.2}ms)",
298 check.name, check.status, check.duration_ms
299 );
300 if let Some(ref msg) = check.message {
301 let _ = write!(summary, " - {msg}");
302 }
303 if let Some(ref err) = check.error {
304 let _ = write!(summary, " [Error: {err}]");
305 }
306 }
307 }
308
309 summary
310 }
311 }
312
313 pub async fn run_check_report(&self, check_type: &str, verbose: bool) -> (String, bool) {
320 let health_status = self.perform_checks(check_type, verbose).await;
321 let is_healthy = health_status.status == "healthy";
322 (Self::render_report(&health_status, verbose), is_healthy)
323 }
324}
325
326#[async_trait]
327impl Tool for HealthCheckToolImpl {
328 fn definition(&self) -> rust_mcp_sdk::schema::Tool {
329 HealthCheckTool::tool()
330 }
331
332 async fn execute(
333 &self,
334 arguments: serde_json::Value,
335 ) -> std::result::Result<
336 rust_mcp_sdk::schema::CallToolResult,
337 rust_mcp_sdk::schema::CallToolError,
338 > {
339 let params: HealthCheckTool = serde_json::from_value(arguments).map_err(|e| {
340 rust_mcp_sdk::schema::CallToolError::invalid_arguments(
341 "health_check",
342 Some(format!("Parameter parsing failed: {e}")),
343 )
344 })?;
345
346 let check_type = params.check_type.unwrap_or_else(|| "all".to_string());
347 if !VALID_CHECK_TYPES.contains(&check_type.as_str()) {
351 return Err(rust_mcp_sdk::schema::CallToolError::invalid_arguments(
352 "health_check",
353 Some(format!(
354 "Invalid check_type '{check_type}'. Expected one of: {}",
355 VALID_CHECK_TYPES.join(", ")
356 )),
357 ));
358 }
359 let verbose = params.verbose.unwrap_or(false);
360
361 let health_status = self.perform_checks(&check_type, verbose).await;
362
363 let content = Self::render_report(&health_status, verbose);
364
365 Ok(rust_mcp_sdk::schema::CallToolResult::text_content(vec![
366 content.into(),
367 ]))
368 }
369}
370
371impl Default for HealthCheckToolImpl {
372 fn default() -> Self {
373 Self::new()
374 }
375}