Skip to main content

systemprompt_cli/commands/plugins/mcp/
list.rs

1//! `plugins mcp list` command with binary/build status.
2//!
3//! Copyright (c) systemprompt.io — Business Source License 1.1.
4//! See <https://systemprompt.io> for licensing details.
5
6use anyhow::{Context, Result};
7use chrono::{DateTime, Utc};
8use clap::Args;
9use std::fs;
10use std::path::PathBuf;
11
12use super::types::{McpListOutput, McpServerSummary};
13use crate::CliConfig;
14use crate::shared::CommandOutput;
15use crate::shared::project::ProjectRoot;
16use systemprompt_loader::ConfigLoader;
17use systemprompt_models::mcp::{Deployment, McpServerType};
18
19#[derive(Debug, Clone, Copy, Args)]
20pub struct ListArgs {
21    #[arg(long, help = "Show only enabled servers")]
22    pub enabled: bool,
23
24    #[arg(long, help = "Show only disabled servers")]
25    pub disabled: bool,
26}
27
28pub(super) fn execute(args: ListArgs, _config: &CliConfig) -> Result<CommandOutput> {
29    let services_config = ConfigLoader::load().context("Failed to load services configuration")?;
30    let project_root = ProjectRoot::discover().ok();
31
32    let mut servers: Vec<McpServerSummary> = services_config
33        .mcp_servers
34        .iter()
35        .filter(|(_, server)| {
36            if args.enabled && args.disabled {
37                true
38            } else if args.enabled {
39                server.enabled
40            } else if args.disabled {
41                !server.enabled
42            } else {
43                true
44            }
45        })
46        .map(|(name, server)| {
47            summarize_server(
48                name,
49                server,
50                project_root.as_ref().map(ProjectRoot::as_path),
51            )
52        })
53        .collect();
54
55    servers.sort_by(|a, b| a.name.cmp(&b.name));
56
57    let output = McpListOutput { servers };
58
59    Ok(CommandOutput::table_of(
60        vec![
61            "name",
62            "server_type",
63            "port",
64            "enabled",
65            "status",
66            "endpoint",
67            "binary_debug",
68            "binary_release",
69        ],
70        &output.servers,
71    )
72    .with_title("MCP Servers"))
73}
74
75pub fn summarize_server(
76    name: &str,
77    server: &Deployment,
78    project_root: Option<&std::path::Path>,
79) -> McpServerSummary {
80    if server.server_type == McpServerType::External {
81        return McpServerSummary {
82            name: name.to_owned(),
83            display_name: name.to_owned(),
84            server_type: McpServerType::External.as_str().to_owned(),
85            port: 0,
86            enabled: server.enabled,
87            status: Some(if server.enabled {
88                "remote".to_owned()
89            } else {
90                "disabled".to_owned()
91            }),
92            endpoint: server.endpoint.clone(),
93            binary_debug: None,
94            binary_release: None,
95            debug_created_at: None,
96            release_created_at: None,
97            created_at: None,
98        };
99    }
100
101    let binary_name = if server.binary.is_empty() {
102        name.to_owned()
103    } else {
104        server.binary.clone()
105    };
106    let (binary_debug, debug_created_at) = get_binary_info(project_root, &binary_name, false);
107    let (binary_release, release_created_at) = get_binary_info(project_root, &binary_name, true);
108
109    McpServerSummary {
110        name: name.to_owned(),
111        display_name: name.to_owned(),
112        server_type: McpServerType::Internal.as_str().to_owned(),
113        port: server.port,
114        enabled: server.enabled,
115        status: Some(determine_status(
116            server.enabled,
117            binary_debug.as_deref(),
118            binary_release.as_deref(),
119        )),
120        endpoint: None,
121        binary_debug,
122        binary_release,
123        debug_created_at,
124        release_created_at,
125        created_at: None,
126    }
127}
128
129pub fn get_binary_info(
130    project_root: Option<&std::path::Path>,
131    binary_name: &str,
132    release: bool,
133) -> (Option<String>, Option<String>) {
134    let Some(root) = project_root else {
135        return (None, None);
136    };
137
138    let profile = if release { "release" } else { "debug" };
139    let binary_path: PathBuf = root.join("target").join(profile).join(binary_name);
140
141    if !binary_path.exists() {
142        return (None, None);
143    }
144
145    let path_str = Some(binary_path.display().to_string());
146
147    let created_at = fs::metadata(&binary_path)
148        .ok()
149        .and_then(|m| m.modified().ok())
150        .map(|t| {
151            let datetime: DateTime<Utc> = t.into();
152            datetime.format("%Y-%m-%d %H:%M:%S").to_string()
153        });
154
155    (path_str, created_at)
156}
157
158pub fn determine_status(enabled: bool, debug: Option<&str>, release: Option<&str>) -> String {
159    if !enabled {
160        return "disabled".to_owned();
161    }
162
163    match (debug.is_some(), release.is_some()) {
164        (true, true) => "ready".to_owned(),
165        (true, false) => "debug-only".to_owned(),
166        (false, true) => "release-only".to_owned(),
167        (false, false) => "not-built".to_owned(),
168    }
169}