crates-docs 0.9.0

High-performance Rust crate documentation query MCP server, supports Stdio/HTTP/SSE transport and OAuth authentication
Documentation
//! Health check tool
//!
//! Provides functionality to check the health status of the server
//! and external services (docs.rs, crates.io). Used for diagnosing
//! connection issues and monitoring system availability.

#![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};

/// Parameters for the `health_check` tool
///
/// Defines the input parameters for performing health checks,
/// including the type of check to perform and verbosity level.
#[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 {
    /// Check type
    #[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>,

    /// Verbose output
    #[json_schema(
        title = "Verbose Output",
        description = "Whether to show detailed output including response time for each check",
        default = false
    )]
    pub verbose: Option<bool>,
}

/// Overall health check result containing all check results
#[derive(Debug, Clone, Serialize)]
struct HealthStatus {
    /// Overall status: "healthy", "unhealthy", or "degraded"
    status: String,
    /// Timestamp of the health check in RFC3339 format
    timestamp: String,
    /// Individual check results
    checks: Vec<HealthCheck>,
    /// Server uptime duration
    uptime: Duration,
}

/// Result of a single health check
#[derive(Debug, Clone, Serialize)]
struct HealthCheck {
    /// Name of the service checked
    name: String,
    /// Status: "healthy", "unhealthy", or "unknown"
    status: String,
    /// Duration of the check in milliseconds
    duration_ms: u64,
    /// Optional success message
    message: Option<String>,
    /// Optional error message if check failed
    error: Option<String>,
}

/// Implementation of the health check tool
///
/// Handles the execution of health checks for the server and external services,
/// including docs.rs and crates.io availability checks.
pub struct HealthCheckToolImpl {
    /// Server start time for uptime calculation
    start_time: Instant,
}

impl HealthCheckToolImpl {
    /// Creates a new health check tool instance
    ///
    /// Initializes the tool with the current time as the server start time
    /// for uptime calculation purposes.
    #[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();
        // Use global HTTP client singleton for connection pool reuse
        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
    }

    /// Check memory usage
    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}")),
            }],
        };

        // Determine overall status
        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 {
                // In non-verbose mode, only return checks with issues
                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 {
            // SAFETY: write! to String never fails (writes to memory buffer). unwrap() is safe here.
            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()
    }
}