systemprompt-cli 0.16.0

Unified CLI for systemprompt.io AI governance: agent orchestration, MCP governance, analytics, profiles, cloud deploy, and self-hosted operations.
Documentation
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_models::{McpServerConfig, ServicesConfig};
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 running_servers = manager
        .get_running_servers()
        .await
        .context("Failed to get running servers")?;

    let mut servers = collect_status_entries(
        &args,
        &services_config,
        &manager,
        &running_servers,
        &bin_path,
    )
    .await;

    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",
            "port",
            "enabled",
            "running",
            "pid",
            "release_binary",
            "debug_binary",
        ],
        &output.servers,
    )
    .with_title("MCP Server Status"))
}

async fn collect_status_entries(
    args: &StatusArgs,
    services_config: &ServicesConfig,
    manager: &McpOrchestrator,
    running_servers: &[McpServerConfig],
    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 is_running = running_servers.iter().any(|s| &s.name == name);

        let pid = manager
            .get_service_info(name)
            .await
            .ok()
            .flatten()
            .and_then(|info| info.pid.map(|p| p as u32));

        servers.push(McpStatusEntry {
            name: name.clone(),
            port: deployment.port,
            enabled: deployment.enabled,
            running: is_running,
            pid,
            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
}

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()
            }
        })
}