1#![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#[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 #[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 #[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#[derive(Debug, Clone, Serialize)]
46struct HealthStatus {
47 status: String,
48 timestamp: String,
49 checks: Vec<HealthCheck>,
50 uptime: Duration,
51}
52
53#[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
63pub 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 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 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 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 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 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}