#![allow(missing_docs)]
use crate::tools::Tool;
use async_trait::async_trait;
use rust_mcp_sdk::macros;
use serde::{Deserialize, Serialize};
use std::time::{Duration, Instant};
#[macros::mcp_tool(
name = "health_check",
title = "Health Check",
description = "Check the health status of the server and external services (docs.rs, crates.io). Used for diagnosing connection issues and monitoring system availability.",
destructive_hint = false,
idempotent_hint = true,
open_world_hint = false,
read_only_hint = true,
execution(task_support = "optional"),
icons = [
(src = "https://img.icons8.com/color/96/000000/heart-health.png", mime_type = "image/png", sizes = ["96x96"], theme = "light"),
(src = "https://img.icons8.com/color/96/000000/heart-health.png", mime_type = "image/png", sizes = ["96x96"], theme = "dark")
]
)]
#[derive(Debug, Clone, Deserialize, Serialize, macros::JsonSchema)]
pub struct HealthCheckTool {
#[json_schema(
title = "Check Type",
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)",
default = "all"
)]
pub check_type: Option<String>,
#[json_schema(
title = "Verbose Output",
description = "Whether to show detailed output including response time for each check",
default = false
)]
pub verbose: Option<bool>,
}
#[derive(Debug, Clone, Serialize)]
struct HealthStatus {
status: String,
timestamp: String,
checks: Vec<HealthCheck>,
uptime: Duration,
}
#[derive(Debug, Clone, Serialize)]
struct HealthCheck {
name: String,
status: String,
duration_ms: u64,
message: Option<String>,
error: Option<String>,
}
pub struct HealthCheckToolImpl {
start_time: Instant,
}
impl HealthCheckToolImpl {
#[must_use]
pub fn new() -> Self {
Self {
start_time: Instant::now(),
}
}
#[allow(clippy::cast_possible_truncation)]
async fn check_http_service(
name: &'static str,
url: &str,
healthy_msg: &'static str,
) -> HealthCheck {
let start = Instant::now();
let client = match crate::utils::get_or_init_global_http_client() {
Ok(client) => client,
Err(e) => {
return HealthCheck {
name: name.to_string(),
status: "unhealthy".to_string(),
duration_ms: start.elapsed().as_millis() as u64,
message: None,
error: Some(format!("Failed to initialize HTTP client: {e}")),
};
}
};
match client
.get(url)
.header("User-Agent", format!("CratesDocsMCP/{}", crate::VERSION))
.timeout(Duration::from_secs(5))
.send()
.await
{
Ok(response) => {
let duration = start.elapsed();
if response.status().is_success() {
HealthCheck {
name: name.to_string(),
status: "healthy".to_string(),
duration_ms: duration.as_millis() as u64,
message: Some(healthy_msg.to_string()),
error: None,
}
} else {
HealthCheck {
name: name.to_string(),
status: "unhealthy".to_string(),
duration_ms: duration.as_millis() as u64,
message: None,
error: Some(format!("HTTP status code: {}", response.status())),
}
}
}
Err(e) => {
let duration = start.elapsed();
HealthCheck {
name: name.to_string(),
status: "unhealthy".to_string(),
duration_ms: duration.as_millis() as u64,
message: None,
error: Some(format!("Request failed: {e}")),
}
}
}
}
#[inline]
async fn check_docs_rs(&self) -> HealthCheck {
Self::check_http_service("docs.rs", "https://docs.rs/", "Service is healthy").await
}
#[inline]
async fn check_crates_io(&self) -> HealthCheck {
Self::check_http_service(
"crates.io",
"https://crates.io/api/v1/crates?q=serde&per_page=1",
"API is healthy",
)
.await
}
fn check_memory() -> HealthCheck {
HealthCheck {
name: "memory".to_string(),
status: "healthy".to_string(),
duration_ms: 0,
message: Some("Memory usage is normal".to_string()),
error: None,
}
}
async fn perform_checks(&self, check_type: &str, verbose: bool) -> HealthStatus {
let checks = match check_type {
"all" => {
let (docs_rs, crates_io) =
tokio::join!(self.check_docs_rs(), self.check_crates_io());
vec![docs_rs, crates_io, Self::check_memory()]
}
"external" => {
let (docs_rs, crates_io) =
tokio::join!(self.check_docs_rs(), self.check_crates_io());
vec![docs_rs, crates_io]
}
"internal" => vec![Self::check_memory()],
"docs_rs" => vec![self.check_docs_rs().await],
"crates_io" => vec![self.check_crates_io().await],
_ => vec![HealthCheck {
name: "unknown_check".to_string(),
status: "unknown".to_string(),
duration_ms: 0,
message: None,
error: Some(format!("Unknown check type: {check_type}")),
}],
};
let overall_status = if checks.iter().all(|c| c.status == "healthy") {
"healthy".to_string()
} else if checks.iter().any(|c| c.status == "unhealthy") {
"unhealthy".to_string()
} else {
"degraded".to_string()
};
HealthStatus {
status: overall_status,
timestamp: chrono::Utc::now().to_rfc3339(),
checks: if verbose {
checks
} else {
checks
.into_iter()
.filter(|c| c.status != "healthy")
.collect()
},
uptime: self.start_time.elapsed(),
}
}
}
#[async_trait]
impl Tool for HealthCheckToolImpl {
fn definition(&self) -> rust_mcp_sdk::schema::Tool {
HealthCheckTool::tool()
}
async fn execute(
&self,
arguments: serde_json::Value,
) -> std::result::Result<
rust_mcp_sdk::schema::CallToolResult,
rust_mcp_sdk::schema::CallToolError,
> {
let params: HealthCheckTool = serde_json::from_value(arguments).map_err(|e| {
rust_mcp_sdk::schema::CallToolError::invalid_arguments(
"health_check",
Some(format!("Parameter parsing failed: {e}")),
)
})?;
let check_type = params.check_type.unwrap_or_else(|| "all".to_string());
let verbose = params.verbose.unwrap_or(false);
let health_status = self.perform_checks(&check_type, verbose).await;
let content = if verbose {
serde_json::to_string_pretty(&health_status).map_err(|e| {
rust_mcp_sdk::schema::CallToolError::from_message(format!(
"JSON serialization failed: {e}"
))
})?
} else {
let mut summary = format!(
"Status: {}\nUptime: {:.2?}\nTimestamp: {}",
health_status.status, health_status.uptime, health_status.timestamp
);
if !health_status.checks.is_empty() {
use std::fmt::Write;
summary.push_str("\n\nCheck Results:");
for check in &health_status.checks {
write!(
summary,
"\n- {}: {} ({:.2}ms)",
check.name, check.status, check.duration_ms
)
.unwrap();
if let Some(ref msg) = check.message {
write!(summary, " - {msg}").unwrap();
}
if let Some(ref err) = check.error {
write!(summary, " [Error: {err}]").unwrap();
}
}
}
summary
};
Ok(rust_mcp_sdk::schema::CallToolResult::text_content(vec![
content.into(),
]))
}
}
impl Default for HealthCheckToolImpl {
fn default() -> Self {
Self::new()
}
}