systemprompt_cli/commands/plugins/mcp/
list.rs1use 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}