@@ -27,6 +27,7 @@ use super::agent::{
27
27
DEFAULT_AGENT_NAME ,
28
28
McpServerConfig ,
29
29
} ;
30
+ use super :: chat:: tools:: custom_tool:: TransportType ;
30
31
use crate :: cli:: chat:: tool_manager:: {
31
32
global_mcp_config_path,
32
33
workspace_mcp_config_path,
@@ -85,8 +86,11 @@ impl McpSubcommand {
85
86
}
86
87
}
87
88
88
- #[ derive( Debug , Clone , PartialEq , Eq , Args ) ]
89
+ #[ derive( Debug , Default , Clone , PartialEq , Eq , Args ) ]
89
90
pub struct AddArgs {
91
+ /// Transport type for the MCP server (e.g., stdio, http)
92
+ #[ arg( long) ]
93
+ pub r#type : Option < TransportType > ,
90
94
/// Name for the server
91
95
#[ arg( long) ]
92
96
pub name : String ,
@@ -95,7 +99,13 @@ pub struct AddArgs {
95
99
pub scope : Option < Scope > ,
96
100
/// The command used to launch the server
97
101
#[ arg( long) ]
98
- pub command : String ,
102
+ pub command : Option < String > ,
103
+ /// URL for HTTP-based MCP servers
104
+ #[ arg( long) ]
105
+ pub url : Option < String > ,
106
+ /// HTTP headers to include with requests for HTTP-based MCP servers
107
+ #[ arg( long, value_parser = parse_key_val_pair) ]
108
+ pub headers : Vec < HashMap < String , String > > ,
99
109
/// Arguments to pass to the command. Can be provided as:
100
110
/// 1. Multiple --args flags: --args arg1 --args arg2 --args "arg,with,commas"
101
111
/// 2. Comma-separated with escaping: --args "arg1,arg2,arg\,with\,commas"
@@ -107,7 +117,7 @@ pub struct AddArgs {
107
117
#[ arg( long) ]
108
118
pub agent : Option < String > ,
109
119
/// Environment variables to use when launching the server
110
- #[ arg( long, value_parser = parse_env_vars ) ]
120
+ #[ arg( long, value_parser = parse_key_val_pair ) ]
111
121
pub env : Vec < HashMap < String , String > > ,
112
122
/// Server launch timeout, in milliseconds
113
123
#[ arg( long) ]
@@ -121,11 +131,8 @@ pub struct AddArgs {
121
131
}
122
132
123
133
impl AddArgs {
124
- pub async fn execute ( self , os : & Os , output : & mut impl Write ) -> Result < ( ) > {
125
- // Process args to handle comma-separated values, escaping, and JSON arrays
126
- let processed_args = self . process_args ( ) ?;
127
-
128
- match self . agent . as_deref ( ) {
134
+ pub async fn execute ( mut self , os : & Os , output : & mut impl Write ) -> Result < ( ) > {
135
+ match self . agent . take ( ) . as_deref ( ) {
129
136
Some ( agent_name) => {
130
137
let ( mut agent, config_path) = Agent :: get_agent_by_name ( os, agent_name) . await ?;
131
138
let mcp_servers = & mut agent. mcp_servers . mcp_servers ;
@@ -139,19 +146,13 @@ impl AddArgs {
139
146
) ;
140
147
}
141
148
142
- let merged_env = self . env . into_iter ( ) . flatten ( ) . collect :: < HashMap < _ , _ > > ( ) ;
143
- let tool: CustomToolConfig = serde_json:: from_value ( serde_json:: json!( {
144
- "command" : self . command,
145
- "args" : processed_args,
146
- "env" : merged_env,
147
- "timeout" : self . timeout. unwrap_or( default_timeout( ) ) ,
148
- "disabled" : self . disabled,
149
- } ) ) ?;
149
+ let name = self . name . clone ( ) ;
150
+ let tool = self . into_custom_tool_config ( ) ?;
150
151
151
- mcp_servers. insert ( self . name . clone ( ) , tool) ;
152
+ mcp_servers. insert ( name. clone ( ) , tool) ;
152
153
let json = agent. to_str_pretty ( ) ?;
153
154
os. fs . write ( config_path, json) . await ?;
154
- writeln ! ( output, "✓ Added MCP server '{}' to agent {}\n " , self . name, agent_name) ?;
155
+ writeln ! ( output, "✓ Added MCP server '{}' to agent {}\n " , name, agent_name) ?;
155
156
} ,
156
157
None => {
157
158
let legacy_mcp_config_path = match self . scope {
@@ -172,21 +173,15 @@ impl AddArgs {
172
173
) ;
173
174
}
174
175
175
- let merged_env = self . env . into_iter ( ) . flatten ( ) . collect :: < HashMap < _ , _ > > ( ) ;
176
- let tool: CustomToolConfig = serde_json:: from_value ( serde_json:: json!( {
177
- "command" : self . command,
178
- "args" : processed_args,
179
- "env" : merged_env,
180
- "timeout" : self . timeout. unwrap_or( default_timeout( ) ) ,
181
- "disabled" : self . disabled,
182
- } ) ) ?;
176
+ let name = self . name . clone ( ) ;
177
+ let tool = self . into_custom_tool_config ( ) ?;
183
178
184
- mcp_servers. mcp_servers . insert ( self . name . clone ( ) , tool) ;
179
+ mcp_servers. mcp_servers . insert ( name. clone ( ) , tool) ;
185
180
mcp_servers. save_to_file ( os, & legacy_mcp_config_path) . await ?;
186
181
writeln ! (
187
182
output,
188
183
"✓ Added MCP server '{}' to global config in {}\n " ,
189
- self . name,
184
+ name,
190
185
legacy_mcp_config_path. display( )
191
186
) ?;
192
187
} ,
@@ -195,6 +190,45 @@ impl AddArgs {
195
190
Ok ( ( ) )
196
191
}
197
192
193
+ fn into_custom_tool_config ( self ) -> Result < CustomToolConfig > {
194
+ match self . r#type {
195
+ Some ( TransportType :: Http ) => {
196
+ if let Some ( url) = self . url {
197
+ let merged_headers = self . headers . into_iter ( ) . flatten ( ) . collect :: < HashMap < _ , _ > > ( ) ;
198
+ Ok ( CustomToolConfig {
199
+ r#type : TransportType :: Http ,
200
+ url,
201
+ headers : merged_headers,
202
+ timeout : self . timeout . unwrap_or ( default_timeout ( ) ) ,
203
+ disabled : self . disabled ,
204
+ ..Default :: default ( )
205
+ } )
206
+ } else {
207
+ bail ! ( "Transport type is specified to be http but url is not provided" ) ;
208
+ }
209
+ } ,
210
+ Some ( TransportType :: Stdio ) | None => {
211
+ if self . command . is_some ( ) {
212
+ let processed_args = self . process_args ( ) ?;
213
+ let merged_env = self . env . into_iter ( ) . flatten ( ) . collect :: < HashMap < _ , _ > > ( ) ;
214
+ Ok ( CustomToolConfig {
215
+ r#type : TransportType :: Stdio ,
216
+ // Doing this saves us an allocation and this is safe because we have
217
+ // already verified that command is Some
218
+ command : self . command . unwrap ( ) ,
219
+ args : processed_args,
220
+ env : Some ( merged_env) ,
221
+ timeout : self . timeout . unwrap_or ( default_timeout ( ) ) ,
222
+ disabled : self . disabled ,
223
+ ..Default :: default ( )
224
+ } )
225
+ } else {
226
+ bail ! ( "Transport type is specified to be stdio but command is not provided" )
227
+ }
228
+ } ,
229
+ }
230
+ }
231
+
198
232
fn process_args ( & self ) -> Result < Vec < String > > {
199
233
let mut processed_args = Vec :: new ( ) ;
200
234
@@ -504,7 +538,7 @@ async fn ensure_config_file(os: &Os, path: &PathBuf, output: &mut impl Write) ->
504
538
load_cfg ( os, path) . await
505
539
}
506
540
507
- fn parse_env_vars ( arg : & str ) -> Result < HashMap < String , String > > {
541
+ fn parse_key_val_pair ( arg : & str ) -> Result < HashMap < String , String > > {
508
542
let mut vars = HashMap :: new ( ) ;
509
543
510
544
for pair in arg. split ( "," ) {
@@ -640,7 +674,7 @@ mod tests {
640
674
AddArgs {
641
675
name : "local" . into ( ) ,
642
676
scope : None ,
643
- command : "echo hi" . into ( ) ,
677
+ command : Some ( "echo hi" . into ( ) ) ,
644
678
args : vec ! [
645
679
"awslabs.eks-mcp-server" . to_string( ) ,
646
680
"--allow-write" . to_string( ) ,
@@ -651,6 +685,7 @@ mod tests {
651
685
agent : None ,
652
686
disabled : false ,
653
687
force : false ,
688
+ ..Default :: default ( )
654
689
}
655
690
. execute ( & os, & mut vec ! [ ] )
656
691
. await
@@ -693,7 +728,7 @@ mod tests {
693
728
RootSubcommand :: Mcp ( McpSubcommand :: Add ( AddArgs {
694
729
name: "test_server" . to_string( ) ,
695
730
scope: None ,
696
- command: "test_command" . to_string( ) ,
731
+ command: Some ( "test_command" . to_string( ) ) ,
697
732
args: vec![ "awslabs.eks-mcp-server,--allow-write,--allow-sensitive-data-access" . to_string( ) , ] ,
698
733
agent: None ,
699
734
env: vec![
@@ -707,6 +742,7 @@ mod tests {
707
742
timeout: None ,
708
743
disabled: false ,
709
744
force: false ,
745
+ ..Default :: default ( )
710
746
} ) )
711
747
) ;
712
748
}
@@ -794,4 +830,93 @@ mod tests {
794
830
let result = parse_args ( r#"["invalid json"# ) ;
795
831
assert ! ( result. is_err( ) ) ;
796
832
}
833
+
834
+ #[ test]
835
+ fn test_parse_http_transport_type ( ) {
836
+ let add_args = AddArgs {
837
+ r#type : Some ( TransportType :: Http ) ,
838
+ name : "test_http_server" . to_string ( ) ,
839
+ url : Some ( "https://api.example.com" . to_string ( ) ) ,
840
+ headers : vec ! [
841
+ [ ( "Authorization" . to_string( ) , "Bearer token123" . to_string( ) ) ]
842
+ . into_iter( )
843
+ . collect( ) ,
844
+ [ ( "Content-Type" . to_string( ) , "application/json" . to_string( ) ) ]
845
+ . into_iter( )
846
+ . collect( ) ,
847
+ ] ,
848
+ timeout : Some ( 5000 ) ,
849
+ disabled : false ,
850
+ ..Default :: default ( )
851
+ } ;
852
+
853
+ let config = add_args. into_custom_tool_config ( ) . unwrap ( ) ;
854
+
855
+ assert_eq ! ( config. r#type, TransportType :: Http ) ;
856
+ assert_eq ! ( config. url, "https://api.example.com" ) ;
857
+ assert_eq ! ( config. timeout, 5000 ) ;
858
+ assert ! ( !config. disabled) ;
859
+ assert_eq ! (
860
+ config. headers. get( "Authorization" ) ,
861
+ Some ( & "Bearer token123" . to_string( ) )
862
+ ) ;
863
+ assert_eq ! (
864
+ config. headers. get( "Content-Type" ) ,
865
+ Some ( & "application/json" . to_string( ) )
866
+ ) ;
867
+ }
868
+
869
+ #[ test]
870
+ fn test_incorrect_transport_type_should_fail ( ) {
871
+ // Test HTTP transport without URL should fail
872
+ let add_args = AddArgs {
873
+ r#type : Some ( TransportType :: Http ) ,
874
+ name : "test_http_server" . to_string ( ) ,
875
+ url : None ,
876
+ ..Default :: default ( )
877
+ } ;
878
+
879
+ let result = add_args. into_custom_tool_config ( ) ;
880
+ assert ! ( result. is_err( ) ) ;
881
+ assert ! (
882
+ result
883
+ . unwrap_err( )
884
+ . to_string( )
885
+ . contains( "Transport type is specified to be http but url is not provided" )
886
+ ) ;
887
+
888
+ // Test STDIO transport without command should fail
889
+ let add_args = AddArgs {
890
+ r#type : Some ( TransportType :: Stdio ) ,
891
+ name : "test_stdio_server" . to_string ( ) ,
892
+ command : None ,
893
+ ..Default :: default ( )
894
+ } ;
895
+
896
+ let result = add_args. into_custom_tool_config ( ) ;
897
+ assert ! ( result. is_err( ) ) ;
898
+ assert ! (
899
+ result
900
+ . unwrap_err( )
901
+ . to_string( )
902
+ . contains( "Transport type is specified to be stdio but command is not provided" )
903
+ ) ;
904
+
905
+ // Test default (stdio) transport without command should fail
906
+ let add_args = AddArgs {
907
+ r#type : None ,
908
+ name : "test_default_server" . to_string ( ) ,
909
+ command : None ,
910
+ ..Default :: default ( )
911
+ } ;
912
+
913
+ let result = add_args. into_custom_tool_config ( ) ;
914
+ assert ! ( result. is_err( ) ) ;
915
+ assert ! (
916
+ result
917
+ . unwrap_err( )
918
+ . to_string( )
919
+ . contains( "Transport type is specified to be stdio but command is not provided" )
920
+ ) ;
921
+ }
797
922
}
0 commit comments