@@ -4,6 +4,7 @@ use anyhow::Context;
44use anyhow:: Result ;
55use anyhow:: anyhow;
66use anyhow:: bail;
7+ use clap:: ArgGroup ;
78use codex_common:: CliConfigOverrides ;
89use codex_core:: config:: Config ;
910use codex_core:: config:: ConfigOverrides ;
@@ -77,13 +78,61 @@ pub struct AddArgs {
7778 /// Name for the MCP server configuration.
7879 pub name : String ,
7980
80- /// Environment variables to set when launching the server.
81- # [ arg ( long , value_parser = parse_env_pair , value_name = "KEY=VALUE" ) ]
82- pub env : Vec < ( String , String ) > ,
81+ # [ command ( flatten ) ]
82+ pub transport_args : AddMcpTransportArgs ,
83+ }
8384
85+ #[ derive( Debug , clap:: Args ) ]
86+ #[ command(
87+ group(
88+ ArgGroup :: new( "transport" )
89+ . args( [ "command" , "url" ] )
90+ . required( true )
91+ . multiple( false )
92+ )
93+ ) ]
94+ pub struct AddMcpTransportArgs {
95+ #[ command( flatten) ]
96+ pub stdio : Option < AddMcpStdioArgs > ,
97+
98+ #[ command( flatten) ]
99+ pub streamable_http : Option < AddMcpStreamableHttpArgs > ,
100+ }
101+
102+ #[ derive( Debug , clap:: Args ) ]
103+ pub struct AddMcpStdioArgs {
84104 /// Command to launch the MCP server.
85- #[ arg( trailing_var_arg = true , num_args = 1 ..) ]
105+ /// Use --url for a streamable HTTP server.
106+ #[ arg(
107+ trailing_var_arg = true ,
108+ num_args = 0 ..,
109+ ) ]
86110 pub command : Vec < String > ,
111+
112+ /// Environment variables to set when launching the server.
113+ /// Only valid with stdio servers.
114+ #[ arg(
115+ long,
116+ value_parser = parse_env_pair,
117+ value_name = "KEY=VALUE" ,
118+ ) ]
119+ pub env : Vec < ( String , String ) > ,
120+ }
121+
122+ #[ derive( Debug , clap:: Args ) ]
123+ pub struct AddMcpStreamableHttpArgs {
124+ /// URL for a streamable HTTP MCP server.
125+ #[ arg( long) ]
126+ pub url : String ,
127+
128+ /// Optional environment variable to read for a bearer token.
129+ /// Only valid with streamable HTTP servers.
130+ #[ arg(
131+ long = "bearer-token-env-var" ,
132+ value_name = "ENV_VAR" ,
133+ requires = "url"
134+ ) ]
135+ pub bearer_token_env_var : Option < String > ,
87136}
88137
89138#[ derive( Debug , clap:: Parser ) ]
@@ -140,37 +189,51 @@ async fn run_add(config_overrides: &CliConfigOverrides, add_args: AddArgs) -> Re
140189 // Validate any provided overrides even though they are not currently applied.
141190 config_overrides. parse_overrides ( ) . map_err ( |e| anyhow ! ( e) ) ?;
142191
143- let AddArgs { name, env, command } = add_args;
192+ let AddArgs {
193+ name,
194+ transport_args,
195+ } = add_args;
144196
145197 validate_server_name ( & name) ?;
146198
147- let mut command_parts = command. into_iter ( ) ;
148- let command_bin = command_parts
149- . next ( )
150- . ok_or_else ( || anyhow ! ( "command is required" ) ) ?;
151- let command_args: Vec < String > = command_parts. collect ( ) ;
152-
153- let env_map = if env. is_empty ( ) {
154- None
155- } else {
156- let mut map = HashMap :: new ( ) ;
157- for ( key, value) in env {
158- map. insert ( key, value) ;
159- }
160- Some ( map)
161- } ;
162-
163199 let codex_home = find_codex_home ( ) . context ( "failed to resolve CODEX_HOME" ) ?;
164200 let mut servers = load_global_mcp_servers ( & codex_home)
165201 . await
166202 . with_context ( || format ! ( "failed to load MCP servers from {}" , codex_home. display( ) ) ) ?;
167203
168- let new_entry = McpServerConfig {
169- transport : McpServerTransportConfig :: Stdio {
170- command : command_bin,
171- args : command_args,
172- env : env_map,
204+ let transport = match transport_args {
205+ AddMcpTransportArgs {
206+ stdio : Some ( stdio) , ..
207+ } => {
208+ let mut command_parts = stdio. command . into_iter ( ) ;
209+ let command_bin = command_parts
210+ . next ( )
211+ . ok_or_else ( || anyhow ! ( "command is required" ) ) ?;
212+ let command_args: Vec < String > = command_parts. collect ( ) ;
213+
214+ let env_map = if stdio. env . is_empty ( ) {
215+ None
216+ } else {
217+ Some ( stdio. env . into_iter ( ) . collect :: < HashMap < _ , _ > > ( ) )
218+ } ;
219+ McpServerTransportConfig :: Stdio {
220+ command : command_bin,
221+ args : command_args,
222+ env : env_map,
223+ }
224+ }
225+ AddMcpTransportArgs {
226+ streamable_http : Some ( streamable_http) ,
227+ ..
228+ } => McpServerTransportConfig :: StreamableHttp {
229+ url : streamable_http. url ,
230+ bearer_token_env_var : streamable_http. bearer_token_env_var ,
173231 } ,
232+ AddMcpTransportArgs { .. } => bail ! ( "exactly one of --command or --url must be provided" ) ,
233+ } ;
234+
235+ let new_entry = McpServerConfig {
236+ transport,
174237 startup_timeout_sec : None ,
175238 tool_timeout_sec : None ,
176239 } ;
@@ -236,7 +299,7 @@ async fn run_login(config_overrides: &CliConfigOverrides, login_args: LoginArgs)
236299 _ => bail ! ( "OAuth login is only supported for streamable HTTP servers." ) ,
237300 } ;
238301
239- perform_oauth_login ( & name, & url) . await ?;
302+ perform_oauth_login ( & name, & url, config . mcp_oauth_credentials_store_mode ) . await ?;
240303 println ! ( "Successfully logged in to MCP server '{name}'." ) ;
241304 Ok ( ( ) )
242305}
@@ -259,7 +322,7 @@ async fn run_logout(config_overrides: &CliConfigOverrides, logout_args: LogoutAr
259322 _ => bail ! ( "OAuth logout is only supported for streamable_http transports." ) ,
260323 } ;
261324
262- match delete_oauth_tokens ( & name, & url) {
325+ match delete_oauth_tokens ( & name, & url, config . mcp_oauth_credentials_store_mode ) {
263326 Ok ( true ) => println ! ( "Removed OAuth credentials for '{name}'." ) ,
264327 Ok ( false ) => println ! ( "No OAuth credentials stored for '{name}'." ) ,
265328 Err ( err) => return Err ( anyhow ! ( "failed to delete OAuth credentials: {err}" ) ) ,
@@ -288,11 +351,14 @@ async fn run_list(config_overrides: &CliConfigOverrides, list_args: ListArgs) ->
288351 "args" : args,
289352 "env" : env,
290353 } ) ,
291- McpServerTransportConfig :: StreamableHttp { url, bearer_token } => {
354+ McpServerTransportConfig :: StreamableHttp {
355+ url,
356+ bearer_token_env_var,
357+ } => {
292358 serde_json:: json!( {
293359 "type" : "streamable_http" ,
294360 "url" : url,
295- "bearer_token " : bearer_token ,
361+ "bearer_token_env_var " : bearer_token_env_var ,
296362 } )
297363 }
298364 } ;
@@ -345,13 +411,15 @@ async fn run_list(config_overrides: &CliConfigOverrides, list_args: ListArgs) ->
345411 } ;
346412 stdio_rows. push ( [ name. clone ( ) , command. clone ( ) , args_display, env_display] ) ;
347413 }
348- McpServerTransportConfig :: StreamableHttp { url, bearer_token } => {
349- let has_bearer = if bearer_token. is_some ( ) {
350- "True"
351- } else {
352- "False"
353- } ;
354- http_rows. push ( [ name. clone ( ) , url. clone ( ) , has_bearer. into ( ) ] ) ;
414+ McpServerTransportConfig :: StreamableHttp {
415+ url,
416+ bearer_token_env_var,
417+ } => {
418+ http_rows. push ( [
419+ name. clone ( ) ,
420+ url. clone ( ) ,
421+ bearer_token_env_var. clone ( ) . unwrap_or ( "-" . to_string ( ) ) ,
422+ ] ) ;
355423 }
356424 }
357425 }
@@ -396,7 +464,7 @@ async fn run_list(config_overrides: &CliConfigOverrides, list_args: ListArgs) ->
396464 }
397465
398466 if !http_rows. is_empty ( ) {
399- let mut widths = [ "Name" . len ( ) , "Url" . len ( ) , "Has Bearer Token" . len ( ) ] ;
467+ let mut widths = [ "Name" . len ( ) , "Url" . len ( ) , "Bearer Token Env Var " . len ( ) ] ;
400468 for row in & http_rows {
401469 for ( i, cell) in row. iter ( ) . enumerate ( ) {
402470 widths[ i] = widths[ i] . max ( cell. len ( ) ) ;
@@ -407,7 +475,7 @@ async fn run_list(config_overrides: &CliConfigOverrides, list_args: ListArgs) ->
407475 "{:<name_w$} {:<url_w$} {:<token_w$}" ,
408476 "Name" ,
409477 "Url" ,
410- "Has Bearer Token" ,
478+ "Bearer Token Env Var " ,
411479 name_w = widths[ 0 ] ,
412480 url_w = widths[ 1 ] ,
413481 token_w = widths[ 2 ] ,
@@ -447,10 +515,13 @@ async fn run_get(config_overrides: &CliConfigOverrides, get_args: GetArgs) -> Re
447515 "args" : args,
448516 "env" : env,
449517 } ) ,
450- McpServerTransportConfig :: StreamableHttp { url, bearer_token } => serde_json:: json!( {
518+ McpServerTransportConfig :: StreamableHttp {
519+ url,
520+ bearer_token_env_var,
521+ } => serde_json:: json!( {
451522 "type" : "streamable_http" ,
452523 "url" : url,
453- "bearer_token " : bearer_token ,
524+ "bearer_token_env_var " : bearer_token_env_var ,
454525 } ) ,
455526 } ;
456527 let output = serde_json:: to_string_pretty ( & serde_json:: json!( {
@@ -493,11 +564,14 @@ async fn run_get(config_overrides: &CliConfigOverrides, get_args: GetArgs) -> Re
493564 } ;
494565 println ! ( " env: {env_display}" ) ;
495566 }
496- McpServerTransportConfig :: StreamableHttp { url, bearer_token } => {
567+ McpServerTransportConfig :: StreamableHttp {
568+ url,
569+ bearer_token_env_var,
570+ } => {
497571 println ! ( " transport: streamable_http" ) ;
498572 println ! ( " url: {url}" ) ;
499- let bearer = bearer_token . as_deref ( ) . unwrap_or ( "-" ) ;
500- println ! ( " bearer_token : {bearer }" ) ;
573+ let env_var = bearer_token_env_var . as_deref ( ) . unwrap_or ( "-" ) ;
574+ println ! ( " bearer_token_env_var : {env_var }" ) ;
501575 }
502576 }
503577 if let Some ( timeout) = server. startup_timeout_sec {
0 commit comments