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 = match ProjectRoot::discover() {
31        Ok(root) => Some(root),
32        Err(e) => {
33            tracing::debug!(error = %e, "No project root; binary details omitted");
34            None
35        },
36    };
37
38    let mut servers: Vec<McpServerSummary> = services_config
39        .mcp_servers
40        .iter()
41        .filter(|(_, server)| {
42            if args.enabled && args.disabled {
43                true
44            } else if args.enabled {
45                server.enabled
46            } else if args.disabled {
47                !server.enabled
48            } else {
49                true
50            }
51        })
52        .map(|(name, server)| {
53            summarize_server(
54                name,
55                server,
56                project_root.as_ref().map(ProjectRoot::as_path),
57            )
58        })
59        .collect();
60
61    servers.sort_by(|a, b| a.name.cmp(&b.name));
62
63    let output = McpListOutput { servers };
64
65    Ok(CommandOutput::table_of(
66        vec![
67            "name",
68            "server_type",
69            "port",
70            "enabled",
71            "status",
72            "endpoint",
73            "binary_debug",
74            "binary_release",
75        ],
76        &output.servers,
77    )
78    .with_title("MCP Servers"))
79}
80
81pub fn summarize_server(
82    name: &str,
83    server: &Deployment,
84    project_root: Option<&std::path::Path>,
85) -> McpServerSummary {
86    if server.server_type == McpServerType::External {
87        return McpServerSummary {
88            name: name.to_owned(),
89            display_name: name.to_owned(),
90            server_type: McpServerType::External.as_str().to_owned(),
91            port: None,
92            enabled: server.enabled,
93            status: Some(if server.enabled {
94                "remote".to_owned()
95            } else {
96                "disabled".to_owned()
97            }),
98            endpoint: server.endpoint.clone(),
99            binary_debug: None,
100            binary_release: None,
101            debug_created_at: None,
102            release_created_at: None,
103            created_at: None,
104        };
105    }
106
107    let binary_name = server
108        .binary
109        .as_deref()
110        .filter(|binary| !binary.is_empty())
111        .unwrap_or(name);
112    let (binary_debug, debug_created_at) = get_binary_info(project_root, binary_name, false);
113    let (binary_release, release_created_at) = get_binary_info(project_root, binary_name, true);
114
115    McpServerSummary {
116        name: name.to_owned(),
117        display_name: name.to_owned(),
118        server_type: McpServerType::Internal.as_str().to_owned(),
119        port: server.port,
120        enabled: server.enabled,
121        status: Some(determine_status(
122            server.enabled,
123            binary_debug.as_deref(),
124            binary_release.as_deref(),
125        )),
126        endpoint: None,
127        binary_debug,
128        binary_release,
129        debug_created_at,
130        release_created_at,
131        created_at: None,
132    }
133}
134
135pub fn get_binary_info(
136    project_root: Option<&std::path::Path>,
137    binary_name: &str,
138    release: bool,
139) -> (Option<String>, Option<String>) {
140    let Some(root) = project_root else {
141        return (None, None);
142    };
143
144    let profile = if release { "release" } else { "debug" };
145    let binary_path: PathBuf = root.join("target").join(profile).join(binary_name);
146
147    if !binary_path.exists() {
148        return (None, None);
149    }
150
151    let path_str = Some(binary_path.display().to_string());
152
153    let created_at = fs::metadata(&binary_path)
154        .ok()
155        .and_then(|m| m.modified().ok())
156        .map(|t| {
157            let datetime: DateTime<Utc> = t.into();
158            datetime.format("%Y-%m-%d %H:%M:%S").to_string()
159        });
160
161    (path_str, created_at)
162}
163
164pub fn determine_status(enabled: bool, debug: Option<&str>, release: Option<&str>) -> String {
165    if !enabled {
166        return "disabled".to_owned();
167    }
168
169    match (debug.is_some(), release.is_some()) {
170        (true, true) => "ready".to_owned(),
171        (true, false) => "debug-only".to_owned(),
172        (false, true) => "release-only".to_owned(),
173        (false, false) => "not-built".to_owned(),
174    }
175}