11use std:: borrow:: Cow ;
2+ use std:: collections:: HashMap ;
23use std:: io:: Write ;
34
45use clap:: Subcommand ;
@@ -11,7 +12,11 @@ use crossterm::{
1112 execute,
1213 queue,
1314} ;
14- use dialoguer:: Select ;
15+ use dialoguer:: {
16+ MultiSelect ,
17+ Select ,
18+ } ;
19+ use eyre:: Result ;
1520use syntect:: easy:: HighlightLines ;
1621use syntect:: highlighting:: {
1722 Style ,
@@ -26,8 +31,10 @@ use syntect::util::{
2631use crate :: cli:: agent:: {
2732 Agent ,
2833 Agents ,
34+ McpServerConfig ,
2935 create_agent,
3036} ;
37+ use crate :: cli:: chat:: conversation:: McpServerInfo ;
3138use crate :: cli:: chat:: {
3239 ChatError ,
3340 ChatSession ,
@@ -36,6 +43,10 @@ use crate::cli::chat::{
3643use crate :: database:: settings:: Setting ;
3744use crate :: os:: Os ;
3845use crate :: util:: directories:: chat_global_agent_path;
46+ use crate :: util:: {
47+ NullWriter ,
48+ directories,
49+ } ;
3950
4051#[ deny( missing_docs) ]
4152#[ derive( Debug , PartialEq , Subcommand ) ]
@@ -65,6 +76,8 @@ pub enum AgentSubcommand {
6576 #[ arg( long, short) ]
6677 from : Option < String > ,
6778 } ,
79+ /// Generate an agent configuration using AI
80+ Generate { } ,
6881 /// Delete the specified agent
6982 #[ command( hide = true ) ]
7083 Delete { name : String } ,
@@ -83,6 +96,22 @@ pub enum AgentSubcommand {
8396 Swap { name : Option < String > } ,
8497}
8598
99+ fn prompt_mcp_server_selection ( servers : & [ McpServerInfo ] ) -> eyre:: Result < Vec < & McpServerInfo > > {
100+ let items: Vec < String > = servers
101+ . iter ( )
102+ . map ( |server| format ! ( "{} ({})" , server. name, server. config. command) )
103+ . collect ( ) ;
104+
105+ let selections = MultiSelect :: new ( )
106+ . with_prompt ( "Select MCP servers (use Space to toggle, Enter to confirm)" )
107+ . items ( & items)
108+ . interact ( ) ?;
109+
110+ let selected_servers: Vec < & McpServerInfo > = selections. iter ( ) . filter_map ( |& i| servers. get ( i) ) . collect ( ) ;
111+
112+ Ok ( selected_servers)
113+ }
114+
86115impl AgentSubcommand {
87116 pub async fn execute ( self , os : & mut Os , session : & mut ChatSession ) -> Result < ChatState , ChatError > {
88117 let agents = & session. conversation . agents ;
@@ -146,8 +175,14 @@ impl AgentSubcommand {
146175 return Err ( ChatError :: Custom ( "Editor process did not exit with success" . into ( ) ) ) ;
147176 }
148177
149- let new_agent =
150- Agent :: load ( os, & path_with_file_name, & mut None , session. conversation . mcp_enabled ) . await ;
178+ let new_agent = Agent :: load (
179+ os,
180+ & path_with_file_name,
181+ & mut None ,
182+ session. conversation . mcp_enabled ,
183+ & mut session. stderr ,
184+ )
185+ . await ;
151186 match new_agent {
152187 Ok ( agent) => {
153188 session. conversation . agents . agents . insert ( agent. name . clone ( ) , agent) ;
@@ -184,6 +219,75 @@ impl AgentSubcommand {
184219 style:: SetForegroundColor ( Color :: Reset )
185220 ) ?;
186221 } ,
222+
223+ Self :: Generate { } => {
224+ let agent_name = match session. read_user_input ( "Enter agent name: " , false ) {
225+ Some ( input) => input. trim ( ) . to_string ( ) ,
226+ None => {
227+ return Ok ( ChatState :: PromptUser {
228+ skip_printing_tools : true ,
229+ } ) ;
230+ } ,
231+ } ;
232+
233+ let agent_description = match session. read_user_input ( "Enter agent description: " , false ) {
234+ Some ( input) => input. trim ( ) . to_string ( ) ,
235+ None => {
236+ return Ok ( ChatState :: PromptUser {
237+ skip_printing_tools : true ,
238+ } ) ;
239+ } ,
240+ } ;
241+
242+ let scope_options = vec ! [ "Local (current workspace)" , "Global (all workspaces)" ] ;
243+ let scope_selection = Select :: new ( )
244+ . with_prompt ( "Agent scope" )
245+ . items ( & scope_options)
246+ . default ( 0 )
247+ . interact ( )
248+ . map_err ( |e| ChatError :: Custom ( format ! ( "Failed to get scope selection: {}" , e) . into ( ) ) ) ?;
249+
250+ let is_global = scope_selection == 1 ;
251+
252+ let mcp_servers = get_enabled_mcp_servers ( os)
253+ . await
254+ . map_err ( |e| ChatError :: Custom ( e. to_string ( ) . into ( ) ) ) ?;
255+
256+ let selected_servers = if mcp_servers. is_empty ( ) {
257+ Vec :: new ( )
258+ } else {
259+ prompt_mcp_server_selection ( & mcp_servers) . map_err ( |e| ChatError :: Custom ( e. to_string ( ) . into ( ) ) ) ?
260+ } ;
261+
262+ let mcp_servers_json = if !selected_servers. is_empty ( ) {
263+ let servers: std:: collections:: HashMap < String , serde_json:: Value > = selected_servers
264+ . iter ( )
265+ . map ( |server| {
266+ (
267+ server. name . clone ( ) ,
268+ serde_json:: to_value ( & server. config ) . unwrap_or_default ( ) ,
269+ )
270+ } )
271+ . collect ( ) ;
272+ serde_json:: to_string ( & servers) . unwrap_or_default ( )
273+ } else {
274+ "{}" . to_string ( )
275+ } ;
276+ use schemars:: schema_for;
277+ let schema = schema_for ! ( Agent ) ;
278+ let schema_string = serde_json:: to_string_pretty ( & schema)
279+ . map_err ( |e| ChatError :: Custom ( format ! ( "Failed to serialize agent schema: {e}" ) . into ( ) ) ) ?;
280+ return session
281+ . generate_agent_config (
282+ os,
283+ & agent_name,
284+ & agent_description,
285+ & mcp_servers_json,
286+ & schema_string,
287+ is_global,
288+ )
289+ . await ;
290+ } ,
187291 Self :: Set { .. } | Self :: Delete { .. } => {
188292 // As part of the agent implementation, we are disabling the ability to
189293 // switch / create profile after a session has started.
@@ -285,6 +389,7 @@ impl AgentSubcommand {
285389 match self {
286390 Self :: List => "list" ,
287391 Self :: Create { .. } => "create" ,
392+ Self :: Generate { .. } => "generate" ,
288393 Self :: Delete { .. } => "delete" ,
289394 Self :: Set { .. } => "set" ,
290395 Self :: Schema => "schema" ,
@@ -311,3 +416,63 @@ fn highlight_json(output: &mut impl Write, json_str: &str) -> eyre::Result<()> {
311416
312417 Ok ( execute ! ( output, style:: ResetColor ) ?)
313418}
419+
420+ /// Searches all configuration sources for MCP servers and returns a deduplicated list.
421+ /// Priority order: Agent configs > Workspace legacy > Global legacy
422+ pub async fn get_all_available_mcp_servers ( os : & mut Os ) -> Result < Vec < McpServerInfo > > {
423+ let mut servers = HashMap :: < String , McpServerInfo > :: new ( ) ;
424+
425+ // 1. Load from agent configurations (highest priority)
426+ let mut null_writer = NullWriter ;
427+ let ( agents, _) = Agents :: load ( os, None , true , & mut null_writer, true ) . await ;
428+
429+ for ( _, agent) in agents. agents {
430+ for ( server_name, server_config) in agent. mcp_servers . mcp_servers {
431+ if !servers. values ( ) . any ( |s| s. config . command == server_config. command ) {
432+ servers. insert ( server_name. clone ( ) , McpServerInfo {
433+ name : server_name,
434+ config : server_config,
435+ } ) ;
436+ }
437+ }
438+ }
439+
440+ // 2. Load from workspace legacy config (medium priority)
441+ if let Ok ( workspace_path) = directories:: chat_legacy_workspace_mcp_config ( os) {
442+ if let Ok ( workspace_config) = McpServerConfig :: load_from_file ( os, workspace_path) . await {
443+ for ( server_name, server_config) in workspace_config. mcp_servers {
444+ if !servers. values ( ) . any ( |s| s. config . command == server_config. command ) {
445+ servers. insert ( server_name. clone ( ) , McpServerInfo {
446+ name : server_name,
447+ config : server_config,
448+ } ) ;
449+ }
450+ }
451+ }
452+ }
453+
454+ // 3. Load from global legacy config (lowest priority)
455+ if let Ok ( global_path) = directories:: chat_legacy_global_mcp_config ( os) {
456+ if let Ok ( global_config) = McpServerConfig :: load_from_file ( os, global_path) . await {
457+ for ( server_name, server_config) in global_config. mcp_servers {
458+ if !servers. values ( ) . any ( |s| s. config . command == server_config. command ) {
459+ servers. insert ( server_name. clone ( ) , McpServerInfo {
460+ name : server_name,
461+ config : server_config,
462+ } ) ;
463+ }
464+ }
465+ }
466+ }
467+
468+ Ok ( servers. into_values ( ) . collect ( ) )
469+ }
470+
471+ /// Get only enabled MCP servers (excludes disabled ones)
472+ pub async fn get_enabled_mcp_servers ( os : & mut Os ) -> Result < Vec < McpServerInfo > > {
473+ let all_servers = get_all_available_mcp_servers ( os) . await ?;
474+ Ok ( all_servers
475+ . into_iter ( )
476+ . filter ( |server| !server. config . disabled )
477+ . collect ( ) )
478+ }
0 commit comments