Skip to content

Commit 9df8471

Browse files
fix: mcp list and mcp status subcommands (#2475)
1 parent b4b4221 commit 9df8471

File tree

1 file changed

+79
-55
lines changed

1 file changed

+79
-55
lines changed

crates/chat-cli/src/cli/mcp.rs

Lines changed: 79 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
use std::collections::HashMap;
1+
use std::collections::{
2+
BTreeMap,
3+
HashMap,
4+
};
25
use std::io::Write;
36
use std::path::PathBuf;
47
use std::process::ExitCode;
@@ -8,6 +11,7 @@ use clap::{
811
Args,
912
ValueEnum,
1013
};
14+
use crossterm::style::Stylize;
1115
use crossterm::{
1216
execute,
1317
style,
@@ -20,6 +24,7 @@ use eyre::{
2024
use super::agent::{
2125
Agent,
2226
Agents,
27+
DEFAULT_AGENT_NAME,
2328
McpServerConfig,
2429
};
2530
use crate::cli::chat::tool_manager::{
@@ -33,15 +38,17 @@ use crate::cli::chat::tools::custom_tool::{
3338
use crate::os::Os;
3439
use crate::util::directories;
3540

36-
#[derive(Debug, Copy, Clone, PartialEq, Eq, ValueEnum)]
41+
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Hash)]
3742
pub enum Scope {
43+
Default,
3844
Workspace,
3945
Global,
4046
}
4147

4248
impl std::fmt::Display for Scope {
4349
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4450
match self {
51+
Scope::Default => write!(f, "default"),
4552
Scope::Workspace => write!(f, "workspace"),
4653
Scope::Global => write!(f, "global"),
4754
}
@@ -247,31 +254,43 @@ impl RemoveArgs {
247254
pub struct ListArgs {
248255
#[arg(value_enum)]
249256
pub scope: Option<Scope>,
250-
#[arg(long, hide = true)]
251-
pub profile: Option<String>,
252257
}
253258

254259
impl ListArgs {
255260
pub async fn execute(self, os: &mut Os, output: &mut impl Write) -> Result<()> {
256-
let configs = get_mcp_server_configs(os, self.scope).await?;
261+
let mut configs = get_mcp_server_configs(os).await?;
262+
configs.retain(|k, _| self.scope.is_none_or(|s| s == *k));
257263
if configs.is_empty() {
258264
writeln!(output, "No MCP server configurations found.\n")?;
259265
return Ok(());
260266
}
261267

262-
for (scope, path, cfg_opt) in configs {
268+
for (scope, agents) in configs {
269+
if let Some(s) = self.scope {
270+
if scope != s {
271+
continue;
272+
}
273+
}
263274
writeln!(output)?;
264-
writeln!(output, "{}:\n {}", scope_display(&scope), path.display())?;
265-
match cfg_opt {
266-
Some(cfg) if !cfg.mcp_servers.is_empty() => {
267-
for (name, tool_cfg) in &cfg.mcp_servers {
268-
let status = if tool_cfg.disabled { " (disabled)" } else { "" };
269-
writeln!(output, " • {name:<12} {}{}", tool_cfg.command, status)?;
270-
}
271-
},
272-
_ => {
273-
writeln!(output, " (empty)")?;
274-
},
275+
writeln!(output, "{}:\n", scope_display(&scope))?;
276+
for (agent_name, cfg_opt, _) in agents {
277+
writeln!(output, " {}", agent_name.bold())?;
278+
match cfg_opt {
279+
Some(cfg) if !cfg.mcp_servers.is_empty() => {
280+
// Sorting servers by name since HashMap is unordered, and having a bunch
281+
// of agents with the same global MCP servers included with different
282+
// ordering looks weird.
283+
let mut servers = cfg.mcp_servers.into_iter().collect::<Vec<_>>();
284+
servers.sort_by(|a, b| a.0.cmp(&b.0));
285+
for (name, tool_cfg) in &servers {
286+
let status = if tool_cfg.disabled { " (disabled)" } else { "" };
287+
writeln!(output, " • {name:<12} {}{}", tool_cfg.command, status)?;
288+
}
289+
},
290+
_ => {
291+
writeln!(output, " (empty)")?;
292+
},
293+
}
275294
}
276295
}
277296
writeln!(output, "\n")?;
@@ -337,50 +356,49 @@ pub struct StatusArgs {
337356

338357
impl StatusArgs {
339358
pub async fn execute(self, os: &mut Os, output: &mut impl Write) -> Result<()> {
340-
let configs = get_mcp_server_configs(os, None).await?;
359+
let configs = get_mcp_server_configs(os).await?;
341360
let mut found = false;
342361

343-
for (sc, path, cfg_opt) in configs {
344-
if let Some(cfg) = cfg_opt.and_then(|c| c.mcp_servers.get(&self.name).cloned()) {
345-
found = true;
346-
execute!(
347-
output,
348-
style::Print("\n─────────────\n"),
349-
style::Print(format!("Scope : {}\n", scope_display(&sc))),
350-
style::Print(format!("File : {}\n", path.display())),
351-
style::Print(format!("Command : {}\n", cfg.command)),
352-
style::Print(format!("Timeout : {} ms\n", cfg.timeout)),
353-
style::Print(format!("Disabled: {}\n", cfg.disabled)),
354-
style::Print(format!(
355-
"Env Vars: {}\n",
356-
cfg.env
357-
.as_ref()
358-
.map_or_else(|| "(none)".into(), |e| e.keys().cloned().collect::<Vec<_>>().join(", "))
359-
)),
360-
)?;
362+
for (sc, agents) in configs {
363+
for (name, cfg_opt, _) in agents {
364+
if let Some(cfg) = cfg_opt.and_then(|c| c.mcp_servers.get(&self.name).cloned()) {
365+
found = true;
366+
execute!(
367+
output,
368+
style::Print("\n─────────────\n"),
369+
style::Print(format!("Scope : {}\n", scope_display(&sc))),
370+
style::Print(format!("Agent : {}\n", name)),
371+
style::Print(format!("Command : {}\n", cfg.command)),
372+
style::Print(format!("Timeout : {} ms\n", cfg.timeout)),
373+
style::Print(format!("Disabled: {}\n", cfg.disabled)),
374+
style::Print(format!(
375+
"Env Vars: {}\n",
376+
cfg.env.as_ref().map_or_else(
377+
|| "(none)".into(),
378+
|e| e
379+
.iter()
380+
.map(|(k, v)| format!("{}={}", k, v))
381+
.collect::<Vec<_>>()
382+
.join(", ")
383+
)
384+
)),
385+
)?;
386+
}
361387
}
388+
writeln!(output, "\n")?;
362389
}
363-
writeln!(output, "\n")?;
364390

365391
if !found {
366-
bail!("No MCP server named '{}' found in any scope/profile\n", self.name);
392+
bail!("No MCP server named '{}' found in any agent\n", self.name);
367393
}
368394

369395
Ok(())
370396
}
371397
}
372398

373-
async fn get_mcp_server_configs(
374-
os: &mut Os,
375-
scope: Option<Scope>,
376-
) -> Result<Vec<(Scope, PathBuf, Option<McpServerConfig>)>> {
377-
let mut targets = Vec::new();
378-
match scope {
379-
Some(scope) => targets.push(scope),
380-
None => targets.extend([Scope::Workspace, Scope::Global]),
381-
}
382-
383-
let mut results = Vec::new();
399+
/// Returns a [BTreeMap] for consistent key iteration.
400+
async fn get_mcp_server_configs(os: &mut Os) -> Result<BTreeMap<Scope, Vec<(String, Option<McpServerConfig>, bool)>>> {
401+
let mut results = BTreeMap::new();
384402
let mut stderr = std::io::stderr();
385403
let agents = Agents::load(os, None, true, &mut stderr).await.0;
386404
let global_path = directories::chat_global_agent_path(os)?;
@@ -391,21 +409,28 @@ async fn get_mcp_server_configs(
391409
.is_some_and(|p| p.parent().is_some_and(|p| p == global_path))
392410
{
393411
Scope::Global
412+
} else if agent.name == DEFAULT_AGENT_NAME {
413+
Scope::Default
394414
} else {
395415
Scope::Workspace
396416
};
397-
398-
results.push((
399-
scope,
400-
agent.path.ok_or(eyre::eyre!("Agent missing path info"))?,
417+
results.entry(scope).or_insert(Vec::new()).push((
418+
agent.name,
401419
Some(agent.mcp_servers),
420+
agent.use_legacy_mcp_json,
402421
));
403422
}
423+
424+
for agents in results.values_mut() {
425+
agents.sort_by(|a, b| a.0.cmp(&b.0));
426+
}
427+
404428
Ok(results)
405429
}
406430

407431
fn scope_display(scope: &Scope) -> String {
408432
match scope {
433+
Scope::Default => "🤖 default".into(),
409434
Scope::Workspace => "📄 workspace".into(),
410435
Scope::Global => "🌍 global".into(),
411436
}
@@ -480,7 +505,7 @@ mod tests {
480505
assert_eq!(
481506
path.to_str(),
482507
workspace_mcp_config_path(&os).unwrap().to_str(),
483-
"No scope or profile should default to the workspace path"
508+
"No scope should default to the workspace path"
484509
);
485510
}
486511

@@ -625,7 +650,6 @@ mod tests {
625650
["mcp", "list", "global"],
626651
RootSubcommand::Mcp(McpSubcommand::List(ListArgs {
627652
scope: Some(Scope::Global),
628-
profile: None
629653
}))
630654
);
631655
}

0 commit comments

Comments
 (0)