1+ """
2+ Edit command for modifying server configurations
3+ """
4+
5+ import sys
6+ from typing import Any , Dict , Optional
7+
8+ import click
9+ from InquirerPy import inquirer
10+ from rich .console import Console
11+ from rich .table import Table
12+
13+ from mcpm .core .schema import RemoteServerConfig , STDIOServerConfig
14+ from mcpm .global_config import GlobalConfigManager
15+ from mcpm .utils .display import print_error
16+
17+ console = Console ()
18+ global_config_manager = GlobalConfigManager ()
19+
20+
21+ @click .command (name = "edit" , context_settings = dict (help_option_names = ["-h" , "--help" ]))
22+ @click .argument ("server_name" )
23+ def edit (server_name ):
24+ """Edit a server configuration.
25+
26+ Opens an interactive form editor that allows you to:
27+ - Change the server name with real-time validation
28+ - Modify server-specific properties (command, args, env for STDIO; URL, headers for remote)
29+ - Step through each field, press Enter to confirm, ESC to cancel
30+
31+ Examples:
32+
33+ \\ b
34+ mcpm edit time # Interactive form
35+ mcpm edit agentkit # Edit agentkit server
36+ mcpm edit remote-api # Edit remote server
37+ """
38+ # Get the existing server
39+ server_config = global_config_manager .get_server (server_name )
40+ if not server_config :
41+ print_error (f"Server '{ server_name } ' not found" , "Run 'mcpm ls' to see available servers" )
42+ raise click .ClickException (f"Server '{ server_name } ' not found" )
43+
44+ # Display current configuration
45+ console .print (f"\n [bold green]Current Configuration for '{ server_name } ':[/]" )
46+
47+ table = Table (show_header = True , header_style = "bold cyan" )
48+ table .add_column ("Property" , style = "yellow" )
49+ table .add_column ("Current Value" , style = "white" )
50+
51+ table .add_row ("Name" , server_config .name )
52+ table .add_row ("Type" , type (server_config ).__name__ )
53+
54+ if isinstance (server_config , STDIOServerConfig ):
55+ table .add_row ("Command" , server_config .command )
56+ table .add_row ("Arguments" , ", " .join (server_config .args ) if server_config .args else "[dim]None[/]" )
57+ table .add_row ("Environment" , ", " .join (f"{ k } ={ v } " for k , v in server_config .env .items ()) if server_config .env else "[dim]None[/]" )
58+ elif isinstance (server_config , RemoteServerConfig ):
59+ table .add_row ("URL" , server_config .url )
60+ table .add_row ("Headers" , ", " .join (f"{ k } ={ v } " for k , v in server_config .headers .items ()) if server_config .headers else "[dim]None[/]" )
61+
62+ table .add_row ("Profile Tags" , ", " .join (server_config .profile_tags ) if server_config .profile_tags else "[dim]None[/]" )
63+
64+ console .print (table )
65+ console .print ()
66+
67+ # Interactive mode
68+ console .print (f"[bold green]Opening Interactive Server Editor: [cyan]{ server_name } [/]" )
69+ console .print ("[dim]Type your answers, press Enter to confirm each field, ESC to cancel[/]" )
70+ console .print ()
71+
72+ try :
73+ result = interactive_server_edit (server_config )
74+
75+ if result is None :
76+ console .print ("[yellow]Interactive editing not available in this environment[/]" )
77+ console .print ("[dim]This command requires a terminal for interactive input[/]" )
78+ return 1
79+
80+ if result .get ("cancelled" , True ):
81+ console .print ("[yellow]Server editing cancelled[/]" )
82+ return 0
83+
84+ # Check if new name conflicts with existing servers (if changed)
85+ new_name = result ["answers" ]["name" ]
86+ if new_name != server_config .name and global_config_manager .get_server (new_name ):
87+ console .print (f"[red]Error: Server '[bold]{ new_name } [/]' already exists[/]" )
88+ return 1
89+
90+ # Apply the interactive changes
91+ original_name = server_config .name
92+ if not apply_interactive_changes (server_config , result ):
93+ console .print ("[red]Failed to apply changes[/]" )
94+ return 1
95+
96+ # Save the changes
97+ try :
98+ if new_name != original_name :
99+ # If name changed, we need to remove old and add new
100+ global_config_manager .remove_server (original_name )
101+ global_config_manager .add_server (server_config )
102+ console .print (f"[green]✅ Server renamed from '[cyan]{ original_name } [/]' to '[cyan]{ new_name } [/]'[/]" )
103+ else :
104+ # Just update in place by saving
105+ global_config_manager ._save_servers ()
106+ console .print (f"[green]✅ Server '[cyan]{ server_name } [/]' updated successfully[/]" )
107+ except Exception as e :
108+ print_error ("Failed to save changes" , str (e ))
109+ raise click .ClickException (f"Failed to save changes: { e } " )
110+
111+ return 0
112+
113+ except Exception as e :
114+ console .print (f"[red]Error running interactive editor: { e } [/]" )
115+ return 1
116+
117+
118+ def interactive_server_edit (server_config ) -> Optional [Dict [str , Any ]]:
119+ """Interactive server edit using InquirerPy forms."""
120+ # Check if we're in a terminal that supports interactive input
121+ if not sys .stdin .isatty ():
122+ return None
123+
124+ try :
125+ # Clear any remaining command line arguments to avoid conflicts
126+ original_argv = sys .argv [:]
127+ sys .argv = [sys .argv [0 ]] # Keep only script name
128+
129+ try :
130+ answers = {}
131+
132+ # Server name - always editable
133+ answers ["name" ] = inquirer .text (
134+ message = "Server name:" ,
135+ default = server_config .name ,
136+ validate = lambda text : len (text .strip ()) > 0 and not text .strip () != text .strip (),
137+ invalid_message = "Server name cannot be empty or contain leading/trailing spaces" ,
138+ keybindings = {"interrupt" : [{"key" : "escape" }]},
139+ ).execute ()
140+
141+ if isinstance (server_config , STDIOServerConfig ):
142+ # STDIO Server configuration
143+ console .print ("\n [cyan]STDIO Server Configuration[/]" )
144+
145+ answers ["command" ] = inquirer .text (
146+ message = "Command to execute:" ,
147+ default = server_config .command ,
148+ validate = lambda text : len (text .strip ()) > 0 ,
149+ invalid_message = "Command cannot be empty" ,
150+ keybindings = {"interrupt" : [{"key" : "escape" }]},
151+ ).execute ()
152+
153+ # Arguments as comma-separated string
154+ current_args = ", " .join (server_config .args ) if server_config .args else ""
155+ answers ["args" ] = inquirer .text (
156+ message = "Arguments (comma-separated):" ,
157+ default = current_args ,
158+ instruction = "(Leave empty for no arguments)" ,
159+ keybindings = {"interrupt" : [{"key" : "escape" }]},
160+ ).execute ()
161+
162+ # Environment variables
163+ current_env = ", " .join (f"{ k } ={ v } " for k , v in server_config .env .items ()) if server_config .env else ""
164+ answers ["env" ] = inquirer .text (
165+ message = "Environment variables (KEY=value,KEY2=value2):" ,
166+ default = current_env ,
167+ instruction = "(Leave empty for no environment variables)" ,
168+ keybindings = {"interrupt" : [{"key" : "escape" }]},
169+ ).execute ()
170+
171+ elif isinstance (server_config , RemoteServerConfig ):
172+ # Remote Server configuration
173+ console .print ("\n [cyan]Remote Server Configuration[/]" )
174+
175+ answers ["url" ] = inquirer .text (
176+ message = "Server URL:" ,
177+ default = server_config .url ,
178+ validate = lambda text : text .strip ().startswith (("http://" , "https://" )) or text .strip () == "" ,
179+ invalid_message = "URL must start with http:// or https://" ,
180+ keybindings = {"interrupt" : [{"key" : "escape" }]},
181+ ).execute ()
182+
183+ # Headers
184+ current_headers = ", " .join (f"{ k } ={ v } " for k , v in server_config .headers .items ()) if server_config .headers else ""
185+ answers ["headers" ] = inquirer .text (
186+ message = "HTTP headers (KEY=value,KEY2=value2):" ,
187+ default = current_headers ,
188+ instruction = "(Leave empty for no custom headers)" ,
189+ keybindings = {"interrupt" : [{"key" : "escape" }]},
190+ ).execute ()
191+ else :
192+ console .print ("[red]Cannot edit custom server configurations interactively[/]" )
193+ return None
194+
195+ # Confirmation
196+ console .print ("\n [bold]Summary of changes:[/]" )
197+ console .print (f"Name: [cyan]{ server_config .name } [/] → [cyan]{ answers ['name' ]} [/]" )
198+
199+ if isinstance (server_config , STDIOServerConfig ):
200+ console .print (f"Command: [cyan]{ server_config .command } [/] → [cyan]{ answers ['command' ]} [/]" )
201+ new_args = [arg .strip () for arg in answers ['args' ].split ("," ) if arg .strip ()] if answers ['args' ] else []
202+ console .print (f"Arguments: [cyan]{ server_config .args } [/] → [cyan]{ new_args } [/]" )
203+
204+ new_env = {}
205+ if answers ['env' ]:
206+ for env_pair in answers ['env' ].split ("," ):
207+ if "=" in env_pair :
208+ key , value = env_pair .split ("=" , 1 )
209+ new_env [key .strip ()] = value .strip ()
210+ console .print (f"Environment: [cyan]{ server_config .env } [/] → [cyan]{ new_env } [/]" )
211+
212+ elif isinstance (server_config , RemoteServerConfig ):
213+ console .print (f"URL: [cyan]{ server_config .url } [/] → [cyan]{ answers ['url' ]} [/]" )
214+
215+ new_headers = {}
216+ if answers ['headers' ]:
217+ for header_pair in answers ['headers' ].split ("," ):
218+ if "=" in header_pair :
219+ key , value = header_pair .split ("=" , 1 )
220+ new_headers [key .strip ()] = value .strip ()
221+ console .print (f"Headers: [cyan]{ server_config .headers } [/] → [cyan]{ new_headers } [/]" )
222+
223+ confirm = inquirer .confirm (
224+ message = "Apply these changes?" ,
225+ default = True ,
226+ keybindings = {"interrupt" : [{"key" : "escape" }]},
227+ ).execute ()
228+
229+ if not confirm :
230+ return {"cancelled" : True }
231+
232+ finally :
233+ # Restore original argv
234+ sys .argv = original_argv
235+
236+ return {
237+ "cancelled" : False ,
238+ "answers" : answers ,
239+ "server_type" : type (server_config ).__name__
240+ }
241+
242+ except (KeyboardInterrupt , EOFError ):
243+ console .print ("\n [yellow]Operation cancelled[/]" )
244+ return {"cancelled" : True }
245+ except Exception as e :
246+ console .print (f"[red]Error running interactive form: { e } [/]" )
247+ return None
248+
249+
250+ def apply_interactive_changes (server_config , interactive_result ):
251+ """Apply the changes from interactive editing to the server config."""
252+ if interactive_result .get ("cancelled" , True ):
253+ return False
254+
255+ answers = interactive_result ["answers" ]
256+
257+ # Update name
258+ server_config .name = answers ["name" ].strip ()
259+
260+ if isinstance (server_config , STDIOServerConfig ):
261+ # Update STDIO-specific fields
262+ server_config .command = answers ["command" ].strip ()
263+
264+ # Parse arguments
265+ if answers ["args" ].strip ():
266+ server_config .args = [arg .strip () for arg in answers ["args" ].split ("," ) if arg .strip ()]
267+ else :
268+ server_config .args = []
269+
270+ # Parse environment variables
271+ server_config .env = {}
272+ if answers ["env" ].strip ():
273+ for env_pair in answers ["env" ].split ("," ):
274+ if "=" in env_pair :
275+ key , value = env_pair .split ("=" , 1 )
276+ server_config .env [key .strip ()] = value .strip ()
277+
278+ elif isinstance (server_config , RemoteServerConfig ):
279+ # Update remote-specific fields
280+ server_config .url = answers ["url" ].strip ()
281+
282+ # Parse headers
283+ server_config .headers = {}
284+ if answers ["headers" ].strip ():
285+ for header_pair in answers ["headers" ].split ("," ):
286+ if "=" in header_pair :
287+ key , value = header_pair .split ("=" , 1 )
288+ server_config .headers [key .strip ()] = value .strip ()
289+
290+ return True
0 commit comments