@@ -66,8 +66,30 @@ use crate::util::{
6666/// An [Agent] is a declarative way of configuring a given instance of q chat. Currently, it is
6767/// impacting q chat in via influenicng [ContextManager] and [ToolManager].
6868/// Changes made to [ContextManager] and [ToolManager] do not persist across sessions.
69+ ///
70+ /// To increase the usability of the agent config, (both from the perspective of CLI and the users
71+ /// who would need to write these config), the agent config has two states of existence: "cold" and
72+ /// "warm".
73+ ///
74+ /// A "cold" state describes the config as it is written. And a "warm" state is an alternate form
75+ /// of the same config, modified for the convenience of the business logic that relies on it in the
76+ /// application.
77+ ///
78+ /// For example, the "cold" state does not require the field of "path" to be populated. This is
79+ /// because it would be redundant and tedious for user to have to write the path of the file they
80+ /// had created in said file. This field is thus populated during its parsing.
81+ ///
82+ /// Another example is the mcp config. To support backwards compatibility of users existing global
83+ /// mcp.json, we allow users to supply a flag to denote whether they would want to include servers
84+ /// from the legacy global mcp.json. If this flag exists, we would need to read the legacy mcp
85+ /// config and merge it with what is in the agent mcp servers field. Conversely, when we write this
86+ /// config to file, we would want to filter out the servers that belong only in the mcp.json.
87+ ///
88+ /// Where agents are instantiated from their config, we would need to convert them from "cold" to
89+ /// "warm".
6990#[ derive( Debug , Clone , Serialize , Deserialize , Eq , PartialEq , JsonSchema ) ]
7091#[ serde( rename_all = "camelCase" ) ]
92+ #[ schemars( description = "An Agent is a declarative way of configuring a given instance of q chat." ) ]
7193pub struct Agent {
7294 /// Agent names are derived from the file name. Thus they are skipped for
7395 /// serializing
@@ -105,6 +127,11 @@ pub struct Agent {
105127 #[ serde( default ) ]
106128 #[ schemars( schema_with = "tool_settings_schema" ) ]
107129 pub tools_settings : HashMap < ToolSettingTarget , serde_json:: Value > ,
130+ /// Whether or not to include the legacy ~/.aws/amazonq/mcp.json in the agent
131+ /// You can reference tools brought in by these servers as just as you would with the servers
132+ /// you configure in the mcpServers field in this config
133+ #[ serde( default ) ]
134+ pub use_legacy_mcp_json : bool ,
108135 #[ serde( skip) ]
109136 pub path : Option < PathBuf > ,
110137}
@@ -116,7 +143,7 @@ impl Default for Agent {
116143 description : Some ( "Default agent" . to_string ( ) ) ,
117144 prompt : Default :: default ( ) ,
118145 mcp_servers : Default :: default ( ) ,
119- tools : NATIVE_TOOLS . iter ( ) . copied ( ) . map ( str :: to_string) . collect :: < Vec < _ > > ( ) ,
146+ tools : vec ! [ "*" . to_string( ) ] ,
120147 tool_aliases : Default :: default ( ) ,
121148 allowed_tools : {
122149 let mut set = HashSet :: < String > :: new ( ) ;
@@ -130,20 +157,83 @@ impl Default for Agent {
130157 . collect :: < Vec < _ > > ( ) ,
131158 hooks : Default :: default ( ) ,
132159 tools_settings : Default :: default ( ) ,
160+ use_legacy_mcp_json : true ,
133161 path : None ,
134162 }
135163 }
136164}
137165
138166impl Agent {
167+ /// This function mutates the agent to a state that is writable.
168+ /// Practically this means reverting some fields back to their original values as they were
169+ /// written in the config.
170+ fn freeze ( & mut self ) -> eyre:: Result < ( ) > {
171+ let Self { mcp_servers, .. } = self ;
172+
173+ mcp_servers
174+ . mcp_servers
175+ . retain ( |_name, config| !config. is_from_legacy_mcp_json ) ;
176+
177+ Ok ( ( ) )
178+ }
179+
180+ /// This function mutates the agent to a state that is usable for runtime.
181+ /// Practically this means to convert some of the fields value to their usable counterpart.
182+ /// For example, we populate the agent with its file name, convert the mcp array to actual
183+ /// mcp config and populate the agent file path.
184+ fn thaw ( & mut self , path : & Path , global_mcp_config : Option < & McpServerConfig > ) -> eyre:: Result < ( ) > {
185+ let Self { mcp_servers, .. } = self ;
186+
187+ let name = path
188+ . file_stem ( )
189+ . ok_or ( eyre:: eyre!( "Missing valid file name" ) ) ?
190+ . to_string_lossy ( )
191+ . to_string ( ) ;
192+
193+ self . name = name. clone ( ) ;
194+
195+ if let ( true , Some ( global_mcp_config) ) = ( self . use_legacy_mcp_json , global_mcp_config) {
196+ let mut stderr = std:: io:: stderr ( ) ;
197+ for ( name, legacy_server) in & global_mcp_config. mcp_servers {
198+ if mcp_servers. mcp_servers . contains_key ( name) {
199+ let _ = queue ! (
200+ stderr,
201+ style:: SetForegroundColor ( Color :: Yellow ) ,
202+ style:: Print ( "WARNING: " ) ,
203+ style:: ResetColor ,
204+ style:: Print ( "MCP server '" ) ,
205+ style:: SetForegroundColor ( Color :: Green ) ,
206+ style:: Print ( name) ,
207+ style:: ResetColor ,
208+ style:: Print (
209+ "' is already configured in agent config. Skipping duplicate from legacy mcp.json.\n "
210+ )
211+ ) ;
212+ continue ;
213+ }
214+ let mut server_clone = legacy_server. clone ( ) ;
215+ server_clone. is_from_legacy_mcp_json = true ;
216+ mcp_servers. mcp_servers . insert ( name. clone ( ) , server_clone) ;
217+ }
218+ }
219+
220+ Ok ( ( ) )
221+ }
222+
223+ pub fn to_str_pretty ( & self ) -> eyre:: Result < String > {
224+ let mut agent_clone = self . clone ( ) ;
225+ agent_clone. freeze ( ) ?;
226+ Ok ( serde_json:: to_string_pretty ( & agent_clone) ?)
227+ }
228+
139229 /// Retrieves an agent by name. It does so via first seeking the given agent under local dir,
140230 /// and falling back to global dir if it does not exist in local.
141231 pub async fn get_agent_by_name ( os : & Os , agent_name : & str ) -> eyre:: Result < ( Agent , PathBuf ) > {
142232 let config_path: Result < PathBuf , PathBuf > = ' config: {
143233 // local first, and then fall back to looking at global
144- let local_config_dir = directories:: chat_local_agent_dir ( ) ?. join ( agent_name) ;
234+ let local_config_dir = directories:: chat_local_agent_dir ( ) ?. join ( format ! ( "{ agent_name}.json" ) ) ;
145235 if os. fs . exists ( & local_config_dir) {
146- break ' config Ok :: < PathBuf , PathBuf > ( local_config_dir) ;
236+ break ' config Ok ( local_config_dir) ;
147237 }
148238
149239 let global_config_dir = directories:: chat_global_agent_path ( os) ?. join ( format ! ( "{agent_name}.json" ) ) ;
@@ -157,23 +247,18 @@ impl Agent {
157247 match config_path {
158248 Ok ( config_path) => {
159249 let content = os. fs . read ( & config_path) . await ?;
160- Ok ( ( serde_json:: from_slice :: < Agent > ( & content) ?, config_path) )
161- } ,
162- Err ( global_config_dir) if agent_name == "default" => {
163- os. fs
164- . create_dir_all (
165- global_config_dir
166- . parent ( )
167- . ok_or ( eyre:: eyre!( "Failed to retrieve global agent config parent path" ) ) ?,
168- )
169- . await ?;
170- os. fs . create_new ( & global_config_dir) . await ?;
171-
172- let default_agent = Agent :: default ( ) ;
173- let content = serde_json:: to_string_pretty ( & default_agent) ?;
174- os. fs . write ( & global_config_dir, content. as_bytes ( ) ) . await ?;
175-
176- Ok ( ( default_agent, global_config_dir) )
250+ let mut agent = serde_json:: from_slice :: < Agent > ( & content) ?;
251+
252+ let global_mcp_path = directories:: chat_legacy_mcp_config ( os) ?;
253+ let global_mcp_config = match McpServerConfig :: load_from_file ( os, global_mcp_path) . await {
254+ Ok ( config) => Some ( config) ,
255+ Err ( e) => {
256+ tracing:: error!( "Error loading global mcp json path: {e}." ) ;
257+ None
258+ } ,
259+ } ;
260+ agent. thaw ( & config_path, global_mcp_config. as_ref ( ) ) ?;
261+ Ok ( ( agent, config_path) )
177262 } ,
178263 _ => bail ! ( "Agent {agent_name} does not exist" ) ,
179264 }
@@ -252,7 +337,8 @@ impl Agents {
252337 path : Some ( agent_path. clone ( ) ) ,
253338 ..Default :: default ( )
254339 } ;
255- let contents = serde_json:: to_string_pretty ( & agent)
340+ let contents = agent
341+ . to_str_pretty ( )
256342 . map_err ( |e| eyre:: eyre!( "Failed to serialize profile configuration: {}" , e) ) ?;
257343
258344 if let Some ( parent) = agent_path. parent ( ) {
@@ -307,6 +393,8 @@ impl Agents {
307393 vec ! [ ]
308394 } ;
309395
396+ let mut global_mcp_config = None :: < McpServerConfig > ;
397+
310398 let mut local_agents = ' local: {
311399 // We could be launching from the home dir, in which case the global and local agents
312400 // are the same set of agents. If that is the case, we simply skip this.
@@ -324,7 +412,7 @@ impl Agents {
324412 let Ok ( files) = os. fs . read_dir ( path) . await else {
325413 break ' local Vec :: < Agent > :: new ( ) ;
326414 } ;
327- load_agents_from_entries ( files) . await
415+ load_agents_from_entries ( files, os , & mut global_mcp_config ) . await
328416 } ;
329417
330418 let mut global_agents = ' global: {
@@ -342,7 +430,7 @@ impl Agents {
342430 break ' global Vec :: < Agent > :: new ( ) ;
343431 } ,
344432 } ;
345- load_agents_from_entries ( files) . await
433+ load_agents_from_entries ( files, os , & mut global_mcp_config ) . await
346434 }
347435 . into_iter ( )
348436 . chain ( new_agents)
@@ -385,7 +473,7 @@ impl Agents {
385473 } ,
386474 ..Default :: default ( )
387475 } ;
388- let Ok ( content) = serde_json :: to_string_pretty ( & example_agent) else {
476+ let Ok ( content) = example_agent. to_str_pretty ( ) else {
389477 error ! ( "Error serializing example agent config" ) ;
390478 break ' example_config;
391479 } ;
@@ -522,8 +610,13 @@ impl Agents {
522610 }
523611}
524612
525- async fn load_agents_from_entries ( mut files : ReadDir ) -> Vec < Agent > {
613+ async fn load_agents_from_entries (
614+ mut files : ReadDir ,
615+ os : & Os ,
616+ global_mcp_config : & mut Option < McpServerConfig > ,
617+ ) -> Vec < Agent > {
526618 let mut res = Vec :: < Agent > :: new ( ) ;
619+
527620 while let Ok ( Some ( file) ) = files. next_entry ( ) . await {
528621 let file_path = & file. path ( ) ;
529622 if file_path
@@ -539,6 +632,7 @@ async fn load_agents_from_entries(mut files: ReadDir) -> Vec<Agent> {
539632 continue ;
540633 } ,
541634 } ;
635+
542636 let mut agent = match serde_json:: from_slice :: < Agent > ( & content) {
543637 Ok ( mut agent) => {
544638 agent. path = Some ( file_path. clone ( ) ) ;
@@ -550,13 +644,34 @@ async fn load_agents_from_entries(mut files: ReadDir) -> Vec<Agent> {
550644 continue ;
551645 } ,
552646 } ;
553- if let Some ( name) = Path :: new ( & file. file_name ( ) ) . file_stem ( ) {
554- agent. name = name. to_string_lossy ( ) . to_string ( ) ;
555- res. push ( agent) ;
556- } else {
557- let file_path = file_path. to_string_lossy ( ) ;
558- tracing:: error!( "Unable to determine agent name from config file at {file_path}, skipping" ) ;
647+
648+ // The agent config could have use_legacy_mcp_json set to true but not have a valid
649+ // global mcp.json. We would still need to carry on loading the config.
650+ ' load_legacy_mcp_json: {
651+ if agent. use_legacy_mcp_json && global_mcp_config. is_none ( ) {
652+ let Ok ( global_mcp_path) = directories:: chat_legacy_mcp_config ( os) else {
653+ tracing:: error!( "Error obtaining legacy mcp json path. Skipping" ) ;
654+ break ' load_legacy_mcp_json;
655+ } ;
656+ let legacy_mcp_config = match McpServerConfig :: load_from_file ( os, global_mcp_path) . await {
657+ Ok ( config) => config,
658+ Err ( e) => {
659+ tracing:: error!( "Error loading global mcp json path: {e}. Skipping" ) ;
660+ break ' load_legacy_mcp_json;
661+ } ,
662+ } ;
663+ global_mcp_config. replace ( legacy_mcp_config) ;
664+ }
559665 }
666+
667+ if let Err ( e) = agent. thaw ( file_path, global_mcp_config. as_ref ( ) ) {
668+ tracing:: error!(
669+ "Error transforming agent at {} to usable state: {e}. Skipping" ,
670+ file_path. display( )
671+ ) ;
672+ } ;
673+
674+ res. push ( agent) ;
560675 }
561676 }
562677 res
@@ -806,11 +921,4 @@ mod tests {
806921 assert ! ( validate_agent_name( "invalid!" ) . is_err( ) ) ;
807922 assert ! ( validate_agent_name( "invalid space" ) . is_err( ) ) ;
808923 }
809-
810- #[ test]
811- fn test_schema_gen ( ) {
812- use schemars:: schema_for;
813- let schema = schema_for ! ( Agent ) ;
814- println ! ( "Schema for agent: {}" , serde_json:: to_string_pretty( & schema) . unwrap( ) ) ;
815- }
816924}
0 commit comments