systemprompt-cli 0.18.0

Unified CLI for systemprompt.io AI governance: agent orchestration, MCP governance, analytics, profiles, cloud deploy, and self-hosted operations.
Documentation
use std::collections::HashMap;
use std::path::Path;
use std::sync::Arc;

use anyhow::{Context, Result};
use clap::Args;

use super::types::{McpStatusEntry, McpStatusOutput, McpStatusSummary};
use crate::CliConfig;
use crate::shared::CommandOutput;
use systemprompt_loader::ConfigLoader;
use systemprompt_mcp::services::McpOrchestrator;
use systemprompt_mcp::{HealthStatus, McpServiceStatus};
use systemprompt_models::ServicesConfig;
use systemprompt_models::mcp::McpServerType;
use systemprompt_runtime::AppContext;

#[derive(Debug, Clone, Args)]
pub struct StatusArgs {
    #[arg(long, short, help = "Show detailed output including binary paths")]
    pub detailed: bool,

    #[arg(long, help = "Filter to specific server")]
    pub server: Option<String>,
}

pub(super) async fn execute(args: StatusArgs, _config: &CliConfig) -> Result<CommandOutput> {
    let services_config = ConfigLoader::load().context("Failed to load services configuration")?;

    let ctx = AppContext::new()
        .await
        .context("Failed to initialize application context")?;

    let bin_path = ctx.app_paths().build().bin().to_path_buf();

    let manager = McpOrchestrator::new(
        Arc::clone(ctx.db_pool()),
        Arc::clone(ctx.app_paths_arc()),
        ctx.mcp_registry().clone(),
    )
    .context("Failed to initialize MCP manager")?;
    let statuses = manager
        .service_statuses()
        .await
        .context("Failed to get MCP service statuses")?;
    let status_by_name: HashMap<&str, &McpServiceStatus> =
        statuses.iter().map(|s| (s.name.as_str(), s)).collect();

    let mut servers = collect_status_entries(&args, &services_config, &status_by_name, &bin_path);

    servers.sort_by(|a, b| a.name.cmp(&b.name));

    let running_count = servers.iter().filter(|s| s.running).count();
    let enabled_count = servers.iter().filter(|s| s.enabled).count();

    let output = McpStatusOutput {
        servers,
        summary: McpStatusSummary {
            total: services_config.mcp_servers.len(),
            enabled: enabled_count,
            running: running_count,
        },
    };

    Ok(CommandOutput::table_of(
        vec![
            "name",
            "server_type",
            "port",
            "enabled",
            "running",
            "health",
            "pid",
            "endpoint",
            "release_binary",
            "debug_binary",
        ],
        &output.servers,
    )
    .with_title("MCP Server Status"))
}

fn collect_status_entries(
    args: &StatusArgs,
    services_config: &ServicesConfig,
    status_by_name: &HashMap<&str, &McpServiceStatus>,
    bin_path: &Path,
) -> Vec<McpStatusEntry> {
    let mut servers = Vec::new();

    for (name, deployment) in &services_config.mcp_servers {
        if let Some(ref filter) = args.server
            && name != filter
        {
            continue;
        }

        let status = status_by_name.get(name.as_str());
        let health = status.map_or_else(|| "unknown".to_owned(), |s| s.health.as_str().to_owned());
        let running = status
            .is_some_and(|s| matches!(s.health, HealthStatus::Healthy | HealthStatus::Degraded));
        let pid = status.and_then(|s| s.pid);

        let entry = match deployment.server_type {
            McpServerType::External => McpStatusEntry {
                name: name.clone(),
                server_type: McpServerType::External.as_str().to_owned(),
                port: 0,
                enabled: deployment.enabled,
                running,
                health,
                pid: None,
                endpoint: status.and_then(|s| s.endpoint.clone()),
                binary: String::new(),
                release_binary: None,
                debug_binary: None,
            },
            McpServerType::Internal => McpStatusEntry {
                name: name.clone(),
                server_type: McpServerType::Internal.as_str().to_owned(),
                port: deployment.port,
                enabled: deployment.enabled,
                running,
                health,
                pid,
                endpoint: None,
                binary: deployment.binary.clone(),
                release_binary: binary_display(
                    bin_path,
                    "release",
                    &deployment.binary,
                    args.detailed,
                ),
                debug_binary: binary_display(bin_path, "debug", &deployment.binary, args.detailed),
            },
        };

        servers.push(entry);
    }

    servers
}

fn binary_display(
    bin_path: &Path,
    profile: &str,
    binary_name: &str,
    detailed: bool,
) -> Option<String> {
    bin_path
        .parent()
        .map(|p| p.join(profile).join(binary_name))
        .filter(|p| p.exists())
        .map(|p| {
            if detailed {
                p.display().to_string()
            } else {
                "exists".to_owned()
            }
        })
}