1
- use std:: collections:: HashMap ;
1
+ use std:: collections:: {
2
+ BTreeMap ,
3
+ HashMap ,
4
+ } ;
2
5
use std:: io:: Write ;
3
6
use std:: path:: PathBuf ;
4
7
use std:: process:: ExitCode ;
@@ -8,6 +11,7 @@ use clap::{
8
11
Args ,
9
12
ValueEnum ,
10
13
} ;
14
+ use crossterm:: style:: Stylize ;
11
15
use crossterm:: {
12
16
execute,
13
17
style,
@@ -20,6 +24,7 @@ use eyre::{
20
24
use super :: agent:: {
21
25
Agent ,
22
26
Agents ,
27
+ DEFAULT_AGENT_NAME ,
23
28
McpServerConfig ,
24
29
} ;
25
30
use crate :: cli:: chat:: tool_manager:: {
@@ -33,15 +38,17 @@ use crate::cli::chat::tools::custom_tool::{
33
38
use crate :: os:: Os ;
34
39
use crate :: util:: directories;
35
40
36
- #[ derive( Debug , Copy , Clone , PartialEq , Eq , ValueEnum ) ]
41
+ #[ derive( Debug , Copy , Clone , PartialEq , Eq , PartialOrd , Ord , ValueEnum , Hash ) ]
37
42
pub enum Scope {
43
+ Default ,
38
44
Workspace ,
39
45
Global ,
40
46
}
41
47
42
48
impl std:: fmt:: Display for Scope {
43
49
fn fmt ( & self , f : & mut std:: fmt:: Formatter < ' _ > ) -> std:: fmt:: Result {
44
50
match self {
51
+ Scope :: Default => write ! ( f, "default" ) ,
45
52
Scope :: Workspace => write ! ( f, "workspace" ) ,
46
53
Scope :: Global => write ! ( f, "global" ) ,
47
54
}
@@ -247,31 +254,43 @@ impl RemoveArgs {
247
254
pub struct ListArgs {
248
255
#[ arg( value_enum) ]
249
256
pub scope : Option < Scope > ,
250
- #[ arg( long, hide = true ) ]
251
- pub profile : Option < String > ,
252
257
}
253
258
254
259
impl ListArgs {
255
260
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) ) ;
257
263
if configs. is_empty ( ) {
258
264
writeln ! ( output, "No MCP server configurations found.\n " ) ?;
259
265
return Ok ( ( ) ) ;
260
266
}
261
267
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
+ }
263
274
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
+ }
275
294
}
276
295
}
277
296
writeln ! ( output, "\n " ) ?;
@@ -337,50 +356,49 @@ pub struct StatusArgs {
337
356
338
357
impl StatusArgs {
339
358
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 ?;
341
360
let mut found = false ;
342
361
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
+ }
361
387
}
388
+ writeln ! ( output, "\n " ) ?;
362
389
}
363
- writeln ! ( output, "\n " ) ?;
364
390
365
391
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) ;
367
393
}
368
394
369
395
Ok ( ( ) )
370
396
}
371
397
}
372
398
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 ( ) ;
384
402
let mut stderr = std:: io:: stderr ( ) ;
385
403
let agents = Agents :: load ( os, None , true , & mut stderr) . await . 0 ;
386
404
let global_path = directories:: chat_global_agent_path ( os) ?;
@@ -391,21 +409,28 @@ async fn get_mcp_server_configs(
391
409
. is_some_and ( |p| p. parent ( ) . is_some_and ( |p| p == global_path) )
392
410
{
393
411
Scope :: Global
412
+ } else if agent. name == DEFAULT_AGENT_NAME {
413
+ Scope :: Default
394
414
} else {
395
415
Scope :: Workspace
396
416
} ;
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 ,
401
419
Some ( agent. mcp_servers ) ,
420
+ agent. use_legacy_mcp_json ,
402
421
) ) ;
403
422
}
423
+
424
+ for agents in results. values_mut ( ) {
425
+ agents. sort_by ( |a, b| a. 0 . cmp ( & b. 0 ) ) ;
426
+ }
427
+
404
428
Ok ( results)
405
429
}
406
430
407
431
fn scope_display ( scope : & Scope ) -> String {
408
432
match scope {
433
+ Scope :: Default => "🤖 default" . into ( ) ,
409
434
Scope :: Workspace => "📄 workspace" . into ( ) ,
410
435
Scope :: Global => "🌍 global" . into ( ) ,
411
436
}
@@ -480,7 +505,7 @@ mod tests {
480
505
assert_eq ! (
481
506
path. to_str( ) ,
482
507
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"
484
509
) ;
485
510
}
486
511
@@ -625,7 +650,6 @@ mod tests {
625
650
[ "mcp" , "list" , "global" ] ,
626
651
RootSubcommand :: Mcp ( McpSubcommand :: List ( ListArgs {
627
652
scope: Some ( Scope :: Global ) ,
628
- profile: None
629
653
} ) )
630
654
) ;
631
655
}
0 commit comments