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]
71 pub fn new() -> Self {
72 Self {
73 start_time: Instant::now(),
74 }
75 }
76
77 #[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 #[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 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 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 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 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}