systemprompt-cli 0.1.22

systemprompt.io OS - CLI for agent orchestration, AI operations, and system management
Documentation
use anyhow::{Context, Result};
use chrono::{DateTime, Utc};
use clap::Args;
use std::fs;
use std::path::PathBuf;

use super::types::{McpListOutput, McpServerSummary};
use crate::CliConfig;
use crate::shared::CommandResult;
use crate::shared::project::ProjectRoot;
use systemprompt_loader::ConfigLoader;

#[derive(Debug, Clone, Copy, Args)]
pub struct ListArgs {
    #[arg(long, help = "Show only enabled servers")]
    pub enabled: bool,

    #[arg(long, help = "Show only disabled servers")]
    pub disabled: bool,
}

pub fn execute(args: ListArgs, _config: &CliConfig) -> Result<CommandResult<McpListOutput>> {
    let services_config = ConfigLoader::load().context("Failed to load services configuration")?;
    let project_root = ProjectRoot::discover().ok();

    let mut servers: Vec<McpServerSummary> = services_config
        .mcp_servers
        .iter()
        .filter(|(_, server)| {
            if args.enabled && args.disabled {
                true
            } else if args.enabled {
                server.enabled
            } else if args.disabled {
                !server.enabled
            } else {
                true
            }
        })
        .map(|(name, server)| {
            let binary_name = if server.binary.is_empty() {
                name.clone()
            } else {
                server.binary.clone()
            };
            let (debug_binary, debug_created_at) =
                get_binary_info(project_root.as_ref(), &binary_name, false);
            let (release_binary, release_created_at) =
                get_binary_info(project_root.as_ref(), &binary_name, true);

            McpServerSummary {
                name: name.clone(),
                port: server.port,
                enabled: server.enabled,
                status: determine_status(
                    server.enabled,
                    debug_binary.as_deref(),
                    release_binary.as_deref(),
                ),
                debug_binary,
                debug_created_at,
                release_binary,
                release_created_at,
            }
        })
        .collect();

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

    let output = McpListOutput { servers };

    Ok(CommandResult::table(output)
        .with_title("MCP Servers")
        .with_columns(vec![
            "name".to_string(),
            "port".to_string(),
            "enabled".to_string(),
            "status".to_string(),
            "debug_binary".to_string(),
            "release_binary".to_string(),
        ]))
}

fn get_binary_info(
    project_root: Option<&ProjectRoot>,
    binary_name: &str,
    release: bool,
) -> (Option<String>, Option<String>) {
    let Some(root) = project_root else {
        return (None, None);
    };

    let profile = if release { "release" } else { "debug" };
    let binary_path: PathBuf = root
        .as_path()
        .join("target")
        .join(profile)
        .join(binary_name);

    if !binary_path.exists() {
        return (None, None);
    }

    let path_str = Some(binary_path.display().to_string());

    let created_at = fs::metadata(&binary_path)
        .ok()
        .and_then(|m| m.modified().ok())
        .map(|t| {
            let datetime: DateTime<Utc> = t.into();
            datetime.format("%Y-%m-%d %H:%M:%S").to_string()
        });

    (path_str, created_at)
}

fn determine_status(enabled: bool, debug: Option<&str>, release: Option<&str>) -> String {
    if !enabled {
        return "disabled".to_string();
    }

    match (debug.is_some(), release.is_some()) {
        (true, true) => "ready".to_string(),
        (true, false) => "debug-only".to_string(),
        (false, true) => "release-only".to_string(),
        (false, false) => "not-built".to_string(),
    }
}