@@ -13,6 +13,7 @@ use codex_core::config::find_codex_home;
13
13
use codex_core:: config:: load_global_mcp_servers;
14
14
use codex_core:: config:: write_global_mcp_servers;
15
15
use codex_core:: config_types:: McpServerConfig ;
16
+ use codex_core:: git_info:: resolve_root_git_project_for_trust;
16
17
17
18
/// [experimental] Launch Codex as an MCP server or manage configured MCP servers.
18
19
///
@@ -78,12 +79,20 @@ pub struct AddArgs {
78
79
/// Command to launch the MCP server.
79
80
#[ arg( trailing_var_arg = true , num_args = 1 ..) ]
80
81
pub command : Vec < String > ,
82
+
83
+ /// Write this server to the project's `.codex/config.toml` instead of global config.
84
+ #[ arg( long) ]
85
+ pub project : bool ,
81
86
}
82
87
83
88
#[ derive( Debug , clap:: Parser ) ]
84
89
pub struct RemoveArgs {
85
90
/// Name of the MCP server configuration to remove.
86
91
pub name : String ,
92
+
93
+ /// Remove from the project's `.codex/config.toml` instead of global config.
94
+ #[ arg( long) ]
95
+ pub project : bool ,
87
96
}
88
97
89
98
impl McpCli {
@@ -120,7 +129,12 @@ fn run_add(config_overrides: &CliConfigOverrides, add_args: AddArgs) -> Result<(
120
129
// Validate any provided overrides even though they are not currently applied.
121
130
config_overrides. parse_overrides ( ) . map_err ( |e| anyhow ! ( e) ) ?;
122
131
123
- let AddArgs { name, env, command } = add_args;
132
+ let AddArgs {
133
+ name,
134
+ env,
135
+ command,
136
+ project,
137
+ } = add_args;
124
138
125
139
validate_server_name ( & name) ?;
126
140
@@ -140,6 +154,26 @@ fn run_add(config_overrides: &CliConfigOverrides, add_args: AddArgs) -> Result<(
140
154
Some ( map)
141
155
} ;
142
156
157
+ if project {
158
+ let cwd = std:: env:: current_dir ( ) . context ( "failed to get current directory" ) ?;
159
+ let project_root = resolve_root_git_project_for_trust ( & cwd) . unwrap_or ( cwd) ;
160
+ add_project_mcp_server (
161
+ & project_root,
162
+ & name,
163
+ McpServerConfig {
164
+ command : command_bin,
165
+ args : command_args,
166
+ env : env_map,
167
+ startup_timeout_ms : None ,
168
+ } ,
169
+ ) ?;
170
+ println ! (
171
+ "Added project MCP server '{name}' in {}/.codex." ,
172
+ project_root. display( )
173
+ ) ;
174
+ return Ok ( ( ) ) ;
175
+ }
176
+
143
177
let codex_home = find_codex_home ( ) . context ( "failed to resolve CODEX_HOME" ) ?;
144
178
let mut servers = load_global_mcp_servers ( & codex_home)
145
179
. with_context ( || format ! ( "failed to load MCP servers from {}" , codex_home. display( ) ) ) ?;
@@ -164,10 +198,25 @@ fn run_add(config_overrides: &CliConfigOverrides, add_args: AddArgs) -> Result<(
164
198
fn run_remove ( config_overrides : & CliConfigOverrides , remove_args : RemoveArgs ) -> Result < ( ) > {
165
199
config_overrides. parse_overrides ( ) . map_err ( |e| anyhow ! ( e) ) ?;
166
200
167
- let RemoveArgs { name } = remove_args;
201
+ let RemoveArgs { name, project } = remove_args;
168
202
169
203
validate_server_name ( & name) ?;
170
204
205
+ if project {
206
+ let cwd = std:: env:: current_dir ( ) . context ( "failed to get current directory" ) ?;
207
+ let project_root = resolve_root_git_project_for_trust ( & cwd) . unwrap_or ( cwd) ;
208
+ let removed = remove_project_mcp_server ( & project_root, & name) ?;
209
+ if removed {
210
+ println ! (
211
+ "Removed project MCP server '{name}' in {}/.codex." ,
212
+ project_root. display( )
213
+ ) ;
214
+ } else {
215
+ println ! ( "No MCP server named '{name}' found." ) ;
216
+ }
217
+ return Ok ( ( ) ) ;
218
+ }
219
+
171
220
let codex_home = find_codex_home ( ) . context ( "failed to resolve CODEX_HOME" ) ?;
172
221
let mut servers = load_global_mcp_servers ( & codex_home)
173
222
. with_context ( || format ! ( "failed to load MCP servers from {}" , codex_home. display( ) ) ) ?;
@@ -188,6 +237,90 @@ fn run_remove(config_overrides: &CliConfigOverrides, remove_args: RemoveArgs) ->
188
237
Ok ( ( ) )
189
238
}
190
239
240
+ fn ensure_project_codex_path ( project_root : & std:: path:: Path ) -> Result < std:: path:: PathBuf > {
241
+ let codex_dir = project_root. join ( ".codex" ) ;
242
+ std:: fs:: create_dir_all ( & codex_dir)
243
+ . with_context ( || format ! ( "failed to create {}" , codex_dir. display( ) ) ) ?;
244
+ Ok ( codex_dir. join ( "config.toml" ) )
245
+ }
246
+
247
+ fn add_project_mcp_server (
248
+ project_root : & std:: path:: Path ,
249
+ name : & str ,
250
+ entry : McpServerConfig ,
251
+ ) -> Result < ( ) > {
252
+ use toml_edit:: { Array as TomlArray , DocumentMut , Item as TomlItem , Table as TomlTable , value} ;
253
+
254
+ let path = ensure_project_codex_path ( project_root) ?;
255
+ let mut doc = match std:: fs:: read_to_string ( & path) {
256
+ Ok ( contents) => contents. parse :: < DocumentMut > ( ) . map_err ( |e| anyhow ! ( e) ) ?,
257
+ Err ( e) if e. kind ( ) == std:: io:: ErrorKind :: NotFound => DocumentMut :: new ( ) ,
258
+ Err ( e) => return Err ( anyhow ! ( e) ) ,
259
+ } ;
260
+
261
+ if !doc. as_table ( ) . contains_key ( "mcp_servers" ) || doc[ "mcp_servers" ] . as_table ( ) . is_none ( ) {
262
+ let mut table = TomlTable :: new ( ) ;
263
+ table. set_implicit ( true ) ;
264
+ doc[ "mcp_servers" ] = TomlItem :: Table ( table) ;
265
+ }
266
+
267
+ let mut entry_tbl = TomlTable :: new ( ) ;
268
+ entry_tbl. set_implicit ( false ) ;
269
+ entry_tbl[ "command" ] = value ( entry. command ) ;
270
+
271
+ if !entry. args . is_empty ( ) {
272
+ let mut args = TomlArray :: new ( ) ;
273
+ for a in entry. args {
274
+ args. push ( a) ;
275
+ }
276
+ entry_tbl[ "args" ] = TomlItem :: Value ( args. into ( ) ) ;
277
+ }
278
+
279
+ if let Some ( env) = entry. env {
280
+ if !env. is_empty ( ) {
281
+ let mut env_tbl = TomlTable :: new ( ) ;
282
+ env_tbl. set_implicit ( false ) ;
283
+ let mut pairs: Vec < _ > = env. into_iter ( ) . collect ( ) ;
284
+ pairs. sort_by ( |( a, _) , ( b, _) | a. cmp ( & b) ) ;
285
+ for ( k, v) in pairs {
286
+ env_tbl. insert ( & k, value ( v) ) ;
287
+ }
288
+ entry_tbl[ "env" ] = TomlItem :: Table ( env_tbl) ;
289
+ }
290
+ }
291
+
292
+ if let Some ( timeout) = entry. startup_timeout_ms {
293
+ let timeout = i64:: try_from ( timeout) . context ( "startup_timeout_ms too large" ) ?;
294
+ entry_tbl[ "startup_timeout_ms" ] = value ( timeout) ;
295
+ }
296
+
297
+ doc[ "mcp_servers" ] [ name] = TomlItem :: Table ( entry_tbl) ;
298
+ std:: fs:: write ( & path, doc. to_string ( ) )
299
+ . with_context ( || format ! ( "failed to write {}" , path. display( ) ) ) ?;
300
+ Ok ( ( ) )
301
+ }
302
+
303
+ fn remove_project_mcp_server ( project_root : & std:: path:: Path , name : & str ) -> Result < bool > {
304
+ use toml_edit:: DocumentMut ;
305
+ let path = project_root. join ( ".codex" ) . join ( "config.toml" ) ;
306
+ let contents = match std:: fs:: read_to_string ( & path) {
307
+ Ok ( c) => c,
308
+ Err ( e) if e. kind ( ) == std:: io:: ErrorKind :: NotFound => return Ok ( false ) ,
309
+ Err ( e) => return Err ( anyhow ! ( e) ) ,
310
+ } ;
311
+
312
+ let mut doc = contents. parse :: < DocumentMut > ( ) . map_err ( |e| anyhow ! ( e) ) ?;
313
+ let Some ( mcp_tbl) = doc[ "mcp_servers" ] . as_table_mut ( ) else {
314
+ return Ok ( false ) ;
315
+ } ;
316
+ let removed = mcp_tbl. remove ( name) . is_some ( ) ;
317
+ if removed {
318
+ std:: fs:: write ( & path, doc. to_string ( ) )
319
+ . with_context ( || format ! ( "failed to write {}" , path. display( ) ) ) ?;
320
+ }
321
+ Ok ( removed)
322
+ }
323
+
191
324
fn run_list ( config_overrides : & CliConfigOverrides , list_args : ListArgs ) -> Result < ( ) > {
192
325
let overrides = config_overrides. parse_overrides ( ) . map_err ( |e| anyhow ! ( e) ) ?;
193
326
let config = Config :: load_with_cli_overrides ( overrides, ConfigOverrides :: default ( ) )
0 commit comments