railwayapp 4.54.0

Interact with Railway via CLI
use rmcp::{ErrorData as McpError, model::*};

use crate::{
    controllers::metrics::{
        FetchResourceMetricsParams, compute_http_metrics, fetch_http_logs_for_deployment,
        fetch_resource_metrics,
    },
    gql::queries,
};

use super::super::handler::RailwayMcp;
use super::super::params::{HttpObservabilityParams, ServiceMetricsParams};

impl RailwayMcp {
    pub(crate) async fn fetch_http_logs_for_service(
        &self,
        project_id: Option<String>,
        service_id: Option<String>,
        environment_id: Option<String>,
        deployment_id: Option<String>,
        lines: Option<i64>,
    ) -> Result<Vec<queries::http_logs::HttpLogFields>, McpError> {
        let ctx = self
            .resolve_service_context(project_id, service_id, environment_id)
            .await?;

        let deployment_id = match deployment_id {
            Some(did) => did,
            None => {
                self.get_latest_deployment_id(&ctx.project_id, &ctx.environment_id, &ctx.service_id)
                    .await?
            }
        };

        let backboard = self.configs.get_backboard();
        fetch_http_logs_for_deployment(
            &self.client,
            &backboard,
            deployment_id,
            lines.unwrap_or(200),
            None,
        )
        .await
        .map_err(|e| McpError::internal_error(format!("Failed to fetch HTTP logs: {e}"), None))
    }

    pub(crate) async fn do_service_metrics(
        &self,
        params: ServiceMetricsParams,
    ) -> Result<CallToolResult, McpError> {
        let ctx = self
            .resolve_service_context(params.project_id, params.service_id, params.environment_id)
            .await?;

        let hours_back = params.hours_back.unwrap_or(1);
        let start_date = chrono::Utc::now() - chrono::Duration::hours(hours_back);

        let measurement_strings = params
            .measurements
            .unwrap_or_else(|| vec!["CPU_USAGE".to_string(), "MEMORY_USAGE_GB".to_string()]);

        let measurements = measurement_strings
            .iter()
            .map(|m| match m.as_str() {
                "CPU_USAGE" => Ok(queries::metrics::MetricMeasurement::CPU_USAGE),
                "CPU_USAGE_2" => Ok(queries::metrics::MetricMeasurement::CPU_USAGE_2),
                "MEMORY_USAGE_GB" => Ok(queries::metrics::MetricMeasurement::MEMORY_USAGE_GB),
                "MEMORY_LIMIT_GB" => Ok(queries::metrics::MetricMeasurement::MEMORY_LIMIT_GB),
                "DISK_USAGE_GB" => Ok(queries::metrics::MetricMeasurement::DISK_USAGE_GB),
                "NETWORK_RX_GB" => Ok(queries::metrics::MetricMeasurement::NETWORK_RX_GB),
                "NETWORK_TX_GB" => Ok(queries::metrics::MetricMeasurement::NETWORK_TX_GB),
                "CPU_LIMIT" => Ok(queries::metrics::MetricMeasurement::CPU_LIMIT),
                other => Err(McpError::invalid_params(
                    format!(
                        "Unknown measurement '{other}'. Valid values: CPU_USAGE, CPU_USAGE_2, \
                         MEMORY_USAGE_GB, MEMORY_LIMIT_GB, DISK_USAGE_GB, NETWORK_RX_GB, \
                         NETWORK_TX_GB, CPU_LIMIT"
                    ),
                    None,
                )),
            })
            .collect::<Result<Vec<_>, _>>()?;

        let backboard = self.configs.get_backboard();
        let result = fetch_resource_metrics(FetchResourceMetricsParams {
            client: &self.client,
            backboard: &backboard,
            service_id: &ctx.service_id,
            environment_id: &ctx.environment_id,
            start_date,
            end_date: None,
            measurements,
            sample_rate_seconds: params.sample_rate_seconds,
            include_raw: false,
        })
        .await
        .map_err(|e| McpError::internal_error(format!("Failed to fetch metrics: {e}"), None))?;

        if result.metrics.is_empty() {
            return Ok(CallToolResult::success(vec![Content::text(
                "No metrics data available for this service.".to_string(),
            )]));
        }

        let mut output = String::new();
        for metric in &result.metrics {
            output.push_str(&format!("\n### {}\n", metric.measurement));
            if metric.data_points == 0 {
                output.push_str("No data points.\n");
            } else {
                output.push_str(&format!(
                    "  Current: {:.4}\n  Average ({}pts): {:.4}\n  Min: {:.4}\n  Max: {:.4}\n",
                    metric.current, metric.data_points, metric.average, metric.min, metric.max
                ));
            }
        }

        Ok(CallToolResult::success(vec![Content::text(output)]))
    }

    pub(crate) async fn do_http_requests(
        &self,
        params: HttpObservabilityParams,
    ) -> Result<CallToolResult, McpError> {
        let logs = self
            .fetch_http_logs_for_service(
                params.project_id,
                params.service_id,
                params.environment_id,
                params.deployment_id,
                params.lines,
            )
            .await?;

        match compute_http_metrics(&logs) {
            Some(result) => Ok(CallToolResult::success(vec![Content::text(format!(
                "HTTP Requests:\n  Total: {}\n  2xx: {}\n  3xx: {}\n  4xx: {}\n  5xx: {}",
                result.total,
                result.status_counts[2],
                result.status_counts[3],
                result.status_counts[4],
                result.status_counts[5]
            ))])),
            None => Ok(CallToolResult::success(vec![Content::text(
                "No HTTP logs found.".to_string(),
            )])),
        }
    }

    pub(crate) async fn do_http_error_rate(
        &self,
        params: HttpObservabilityParams,
    ) -> Result<CallToolResult, McpError> {
        let logs = self
            .fetch_http_logs_for_service(
                params.project_id,
                params.service_id,
                params.environment_id,
                params.deployment_id,
                params.lines,
            )
            .await?;

        match compute_http_metrics(&logs) {
            Some(result) => {
                let errors = result.status_counts[4] + result.status_counts[5];
                let error_rate = if result.total > 0 {
                    (errors as f64 / result.total as f64) * 100.0
                } else {
                    0.0
                };
                let success_rate = 100.0 - error_rate;
                let success = result.total - errors;
                Ok(CallToolResult::success(vec![Content::text(format!(
                    "HTTP Error Rate (sampled {} requests):\n  Errors (4xx+5xx): {} ({:.1}%)\n  Success: {} ({:.1}%)",
                    result.total, errors, error_rate, success, success_rate
                ))]))
            }
            None => Ok(CallToolResult::success(vec![Content::text(
                "No HTTP logs found.".to_string(),
            )])),
        }
    }

    pub(crate) async fn do_http_response_time(
        &self,
        params: HttpObservabilityParams,
    ) -> Result<CallToolResult, McpError> {
        let logs = self
            .fetch_http_logs_for_service(
                params.project_id,
                params.service_id,
                params.environment_id,
                params.deployment_id,
                params.lines,
            )
            .await?;

        match compute_http_metrics(&logs) {
            Some(result) => Ok(CallToolResult::success(vec![Content::text(format!(
                "HTTP Response Times (sampled {} requests):\n  p50: {}ms\n  p90: {}ms\n  p95: {}ms\n  p99: {}ms",
                result.total, result.p50_ms, result.p90_ms, result.p95_ms, result.p99_ms
            ))])),
            None => Ok(CallToolResult::success(vec![Content::text(
                "No HTTP logs found.".to_string(),
            )])),
        }
    }
}