66from pathlib import Path
77from typing import Annotated
88
9+ import tomlkit
910import typer
1011
1112from dispatch_cli .auth import get_api_key , get_api_key_from_keychain
@@ -26,6 +27,7 @@ class RegisterMode(StrEnum):
2627 AUTO = "auto"
2728 CLAUDE = "claude"
2829 CURSOR = "cursor"
30+ CODEX = "codex"
2931
3032
3133def find_git_root () -> Path | None :
@@ -68,6 +70,18 @@ def get_cursor_config_paths() -> list[Path]:
6870 return [project_config ] if cursor_dir .exists () else []
6971
7072
73+ def get_codex_config_paths () -> list [Path ]:
74+ """Get Codex MCP config file paths (project-level only)."""
75+ git_root = find_git_root ()
76+ if git_root :
77+ codex_dir = git_root / ".codex"
78+ else :
79+ codex_dir = Path (".codex" )
80+
81+ config_path = codex_dir / "config.toml"
82+ return [config_path ] if codex_dir .exists () else []
83+
84+
7185def find_mcp_config_files () -> list [tuple [str , Path ]]:
7286 """Find all existing MCP config files.
7387
@@ -85,9 +99,67 @@ def find_mcp_config_files() -> list[tuple[str, Path]]:
8599 if cursor_path .exists ():
86100 configs .append (("cursor" , cursor_path ))
87101
102+ # Check Codex configs (project-level only)
103+ for codex_path in get_codex_config_paths ():
104+ if codex_path .exists ():
105+ configs .append (("codex" , codex_path ))
106+
88107 return configs
89108
90109
110+ def write_json_mcp_config (
111+ config_path : Path , server_name : str , server_config : dict
112+ ) -> None :
113+ """Write an MCP server entry to a JSON config file (Claude, Cursor)."""
114+ if config_path .exists ():
115+ with open (config_path ) as f :
116+ config_data = json .load (f )
117+ else :
118+ config_data = {}
119+
120+ if "mcpServers" not in config_data :
121+ config_data ["mcpServers" ] = {}
122+
123+ config_data ["mcpServers" ][server_name ] = server_config
124+
125+ config_path .parent .mkdir (parents = True , exist_ok = True )
126+ with open (config_path , "w" ) as f :
127+ json .dump (config_data , f , indent = 2 )
128+ f .write ("\n " )
129+
130+
131+ def write_toml_mcp_config (
132+ config_path : Path , server_name : str , server_config : dict
133+ ) -> None :
134+ """Write an MCP server entry to a TOML config file (Codex)."""
135+ if config_path .exists ():
136+ with open (config_path ) as f :
137+ config_data = tomlkit .load (f )
138+ else :
139+ config_data = tomlkit .document ()
140+
141+ if "mcp_servers" not in config_data :
142+ config_data ["mcp_servers" ] = tomlkit .table (is_super_table = True )
143+
144+ mcp_servers = config_data ["mcp_servers" ]
145+ assert isinstance (mcp_servers , dict )
146+ mcp_servers [server_name ] = server_config
147+
148+ config_path .parent .mkdir (parents = True , exist_ok = True )
149+ with open (config_path , "w" ) as f :
150+ tomlkit .dump (config_data , f )
151+
152+
153+ def update_mcp_config (
154+ client_name : str , config_path : Path , server_name : str , server_config : dict
155+ ) -> None :
156+ """Write an MCP server entry to the appropriate config file format."""
157+ if client_name == "codex" :
158+ write_toml_mcp_config (config_path , server_name , server_config )
159+ else :
160+ write_json_mcp_config (config_path , server_name , server_config )
161+
162+
91163@serve_app .command ("agent" )
92164def serve_agent (
93165 namespace : Annotated [
@@ -168,43 +240,43 @@ def serve_agent(
168240 get_logger ().error ("No .cursor directory found" )
169241 raise typer .Exit (1 )
170242 configs_to_update = [("cursor" , cursor_paths [0 ])]
243+ case RegisterMode .CODEX :
244+ codex_paths = get_codex_config_paths ()
245+ if not codex_paths :
246+ # Explicit --register codex: create .codex/ dir
247+ git_root = find_git_root ()
248+ base = git_root if git_root else Path ("." )
249+ codex_paths = [base / ".codex" / "config.toml" ]
250+ configs_to_update = [("codex" , codex_paths [0 ])]
171251 case RegisterMode .AUTO :
172252 configs_to_update = find_mcp_config_files ()
173253 if not configs_to_update :
174254 get_logger ().error ("No MCP config files found" )
175255 raise typer .Exit (1 )
176256
257+ # Build server config
258+ server_config = {
259+ "command" : "dispatch" ,
260+ "args" : [
261+ "mcp" ,
262+ "serve" ,
263+ "agent" ,
264+ "--namespace" ,
265+ namespace ,
266+ "--agent" ,
267+ agent ,
268+ ]
269+ + (["--experimental-tasks" ] if experimental_tasks else []),
270+ }
271+
177272 # Update configs
178273 for client_name , config_path in configs_to_update :
179- if config_path .exists ():
180- with open (config_path ) as f :
181- config_data = json .load (f )
274+ if client_name == "codex" :
275+ server_name = f"dispatch_agent_{ namespace } _{ agent } "
182276 else :
183- config_data = {}
184-
185- if "mcpServers" not in config_data :
186- config_data ["mcpServers" ] = {}
187-
188- server_name = f"dispatch-agent-{ namespace } -{ agent } "
189- config_data ["mcpServers" ][server_name ] = {
190- "command" : "dispatch" ,
191- "args" : [
192- "mcp" ,
193- "serve" ,
194- "agent" ,
195- "--namespace" ,
196- namespace ,
197- "--agent" ,
198- agent ,
199- ]
200- + (["--experimental-tasks" ] if experimental_tasks else []),
201- }
202-
203- config_path .parent .mkdir (parents = True , exist_ok = True )
204- with open (config_path , "w" ) as f :
205- json .dump (config_data , f , indent = 2 )
206- f .write ("\n " )
277+ server_name = f"dispatch-agent-{ namespace } -{ agent } "
207278
279+ update_mcp_config (client_name , config_path , server_name , server_config )
208280 get_logger ().success (f"Updated { client_name } config: { config_path } " )
209281
210282 get_logger ().info ("" )
@@ -294,43 +366,47 @@ def serve_operator(
294366 get_logger ().error ("No .cursor directory found" )
295367 raise typer .Exit (1 )
296368 configs_to_update = [("cursor" , cursor_paths [0 ])]
369+ case RegisterMode .CODEX :
370+ codex_paths = get_codex_config_paths ()
371+ if not codex_paths :
372+ # Explicit --register codex: create .codex/ dir
373+ git_root = find_git_root ()
374+ base = git_root if git_root else Path ("." )
375+ codex_paths = [base / ".codex" / "config.toml" ]
376+ configs_to_update = [("codex" , codex_paths [0 ])]
297377 case RegisterMode .AUTO :
298378 configs_to_update = find_mcp_config_files ()
299379 if not configs_to_update :
300380 get_logger ().error ("No MCP config files found" )
301381 raise typer .Exit (1 )
302382
383+ # Build server config
384+ args = ["mcp" , "serve" , "operator" ]
385+ if namespace :
386+ args .extend (["--namespace" , namespace ])
387+
388+ server_config = {
389+ "command" : "dispatch" ,
390+ "args" : args ,
391+ }
392+
303393 # Update configs
304394 for client_name , config_path in configs_to_update :
305- if config_path .exists ():
306- with open (config_path ) as f :
307- config_data = json .load (f )
395+ if client_name == "codex" :
396+ server_name = (
397+ f"dispatch_operator_{ namespace } "
398+ if namespace
399+ else "dispatch_operator"
400+ )
308401 else :
309- config_data = {}
310-
311- if "mcpServers" not in config_data :
312- config_data ["mcpServers" ] = {}
313-
314- server_name = (
315- f"dispatch-operator-{ namespace } "
316- if namespace
317- else "dispatch-operator"
318- )
319- args = ["mcp" , "serve" , "operator" ]
320- if namespace :
321- args .extend (["--namespace" , namespace ])
322-
323- config_data ["mcpServers" ][server_name ] = {
324- "command" : "dispatch" ,
325- "args" : args ,
326- }
327-
328- config_path .parent .mkdir (parents = True , exist_ok = True )
329- with open (config_path , "w" ) as f :
330- json .dump (config_data , f , indent = 2 )
331- f .write ("\n " )
332-
333- print (f"[green]✓[/green] Updated { client_name } config: { config_path } " )
402+ server_name = (
403+ f"dispatch-operator-{ namespace } "
404+ if namespace
405+ else "dispatch-operator"
406+ )
407+
408+ update_mcp_config (client_name , config_path , server_name , server_config )
409+ get_logger ().success (f"Updated { client_name } config: { config_path } " )
334410
335411 get_logger ().info ("" )
336412 get_logger ().success ("Operator MCP server registered" )
0 commit comments