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 = 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}