22Edit command for modifying server configurations
33"""
44
5+ import os
6+ import subprocess
57import sys
68from typing import Any , Dict , Optional
79
1921
2022
2123@click .command (name = "edit" , context_settings = dict (help_option_names = ["-h" , "--help" ]))
22- @click .argument ("server_name" )
23- def edit (server_name ):
24+ @click .argument ("server_name" , required = False )
25+ @click .option ("-N" , "--new" , is_flag = True , help = "Create a new server configuration" )
26+ @click .option ("-e" , "--editor" , is_flag = True , help = "Open global config in external editor" )
27+ def edit (server_name , new , editor ):
2428 """Edit a server configuration.
2529
2630 Opens an interactive form editor that allows you to:
@@ -30,15 +34,33 @@ def edit(server_name):
3034
3135 Examples:
3236
33- \\ b
34- mcpm edit time # Interactive form
37+ mcpm edit time # Edit existing server
3538 mcpm edit agentkit # Edit agentkit server
36- mcpm edit remote-api # Edit remote server
39+ mcpm edit -N # Create new server
40+ mcpm edit -e # Open global config in editor
3741 """
42+ # Handle editor mode
43+ if editor :
44+ _open_global_config_in_editor ()
45+ return 0
46+
47+ # Handle new server mode
48+ if new :
49+ if server_name :
50+ print_error ("Cannot specify both server name and --new flag" , "Use either 'mcpm edit <server>' or 'mcpm edit --new'" )
51+ raise click .ClickException ("Cannot specify both server name and --new flag" )
52+ _create_new_server ()
53+ return 0
54+
55+ # Require server name for editing existing servers
56+ if not server_name :
57+ print_error ("Server name is required" , "Use 'mcpm edit <server>', 'mcpm edit --new', or 'mcpm edit --editor'" )
58+ raise click .ClickException ("Server name is required" )
59+
3860 # Get the existing server
3961 server_config = global_config_manager .get_server (server_name )
4062 if not server_config :
41- print_error (f"Server '{ server_name } ' not found" , "Run 'mcpm ls' to see available servers" )
63+ print_error (f"Server '{ server_name } ' not found" , "Run 'mcpm ls' to see available servers or use 'mcpm edit --new' to create one " )
4264 raise click .ClickException (f"Server '{ server_name } ' not found" )
4365
4466 # Display current configuration
@@ -287,4 +309,226 @@ def apply_interactive_changes(server_config, interactive_result):
287309 key , value = header_pair .split ("=" , 1 )
288310 server_config .headers [key .strip ()] = value .strip ()
289311
290- return True
312+ return True
313+
314+
315+ def _open_global_config_in_editor ():
316+ """Open the global MCPM configuration file in the default editor."""
317+ try :
318+ # Get the global config file path
319+ config_path = global_config_manager .config_path
320+
321+ if not os .path .exists (config_path ):
322+ console .print ("[yellow]No global configuration file found.[/]" )
323+ console .print ("[dim]Install a server first with 'mcpm install <server>' to create the config file.[/]" )
324+ return
325+
326+ console .print ("[bold green]Opening global MCPM configuration in your default editor...[/]" )
327+
328+ # Use appropriate command based on platform
329+ if os .name == "nt" : # Windows
330+ os .startfile (config_path )
331+ elif os .name == "posix" : # macOS and Linux
332+ subprocess .run (["open" , config_path ] if os .uname ().sysname == "Darwin" else ["xdg-open" , config_path ])
333+
334+ console .print (f"[italic]Global config file: { config_path } [/]" )
335+ console .print ("[dim]After editing, restart any running MCP servers for changes to take effect.[/]" )
336+ except Exception as e :
337+ print_error ("Error opening editor" , str (e ))
338+ console .print (f"You can manually edit the file at: { config_path } " )
339+
340+
341+ def _create_new_server ():
342+ """Create a new server configuration interactively."""
343+ console .print ("[bold green]Create New Server Configuration[/]" )
344+ console .print ("[dim]Type your answers, press Enter to confirm each field, ESC to cancel[/]" )
345+ console .print ()
346+
347+ try :
348+ result = _interactive_new_server_form ()
349+
350+ if result is None :
351+ console .print ("[yellow]Interactive editing not available in this environment[/]" )
352+ console .print ("[dim]This command requires a terminal for interactive input[/]" )
353+ return 1
354+
355+ if result .get ("cancelled" , True ):
356+ console .print ("[yellow]Server creation cancelled[/]" )
357+ return 0
358+
359+ # Check if server name already exists
360+ server_name = result ["answers" ]["name" ]
361+ if global_config_manager .get_server (server_name ):
362+ console .print (f"[red]Error: Server '[bold]{ server_name } [/]' already exists[/]" )
363+ return 1
364+
365+ # Create the server config based on type
366+ server_type = result ["answers" ]["type" ]
367+ if server_type == "stdio" :
368+ server_config = STDIOServerConfig (
369+ name = server_name ,
370+ command = result ["answers" ]["command" ],
371+ args = [arg .strip () for arg in result ["answers" ]["args" ].split ("," ) if arg .strip ()] if result ["answers" ]["args" ] else [],
372+ env = {}
373+ )
374+
375+ # Parse environment variables
376+ if result ["answers" ]["env" ]:
377+ for env_pair in result ["answers" ]["env" ].split ("," ):
378+ if "=" in env_pair :
379+ key , value = env_pair .split ("=" , 1 )
380+ server_config .env [key .strip ()] = value .strip ()
381+ else : # remote
382+ server_config = RemoteServerConfig (
383+ name = server_name ,
384+ url = result ["answers" ]["url" ],
385+ headers = {}
386+ )
387+
388+ # Parse headers
389+ if result ["answers" ]["headers" ]:
390+ for header_pair in result ["answers" ]["headers" ].split ("," ):
391+ if "=" in header_pair :
392+ key , value = header_pair .split ("=" , 1 )
393+ server_config .headers [key .strip ()] = value .strip ()
394+
395+ # Save the new server
396+ try :
397+ global_config_manager .add_server (server_config )
398+ console .print (f"[green]✅ Successfully created server '[cyan]{ server_name } [/]'[/]" )
399+ except Exception as e :
400+ print_error ("Failed to save new server" , str (e ))
401+ raise click .ClickException (f"Failed to save new server: { e } " )
402+
403+ return 0
404+
405+ except Exception as e :
406+ console .print (f"[red]Error creating new server: { e } [/]" )
407+ return 1
408+
409+
410+ def _interactive_new_server_form () -> Optional [Dict [str , Any ]]:
411+ """Interactive form for creating a new server."""
412+ # Check if we're in a terminal that supports interactive input
413+ if not sys .stdin .isatty ():
414+ return None
415+
416+ try :
417+ # Clear any remaining command line arguments to avoid conflicts
418+ original_argv = sys .argv [:]
419+ sys .argv = [sys .argv [0 ]] # Keep only script name
420+
421+ try :
422+ answers = {}
423+
424+ # Server name - required
425+ answers ["name" ] = inquirer .text (
426+ message = "Server name:" ,
427+ validate = lambda text : len (text .strip ()) > 0 and not text .strip () != text .strip (),
428+ invalid_message = "Server name cannot be empty or contain leading/trailing spaces" ,
429+ keybindings = {"interrupt" : [{"key" : "escape" }]},
430+ ).execute ()
431+
432+ # Server type
433+ answers ["type" ] = inquirer .select (
434+ message = "Server type:" ,
435+ choices = [
436+ {"name" : "STDIO Server (local command)" , "value" : "stdio" },
437+ {"name" : "Remote Server (HTTP/SSE)" , "value" : "remote" },
438+ ],
439+ keybindings = {"interrupt" : [{"key" : "escape" }]},
440+ ).execute ()
441+
442+ if answers ["type" ] == "stdio" :
443+ # STDIO Server configuration
444+ console .print ("\n [cyan]STDIO Server Configuration[/]" )
445+
446+ answers ["command" ] = inquirer .text (
447+ message = "Command to execute:" ,
448+ validate = lambda text : len (text .strip ()) > 0 ,
449+ invalid_message = "Command cannot be empty" ,
450+ keybindings = {"interrupt" : [{"key" : "escape" }]},
451+ ).execute ()
452+
453+ answers ["args" ] = inquirer .text (
454+ message = "Arguments (comma-separated):" ,
455+ instruction = "(Leave empty for no arguments)" ,
456+ keybindings = {"interrupt" : [{"key" : "escape" }]},
457+ ).execute ()
458+
459+ answers ["env" ] = inquirer .text (
460+ message = "Environment variables (KEY=value,KEY2=value2):" ,
461+ instruction = "(Leave empty for no environment variables)" ,
462+ keybindings = {"interrupt" : [{"key" : "escape" }]},
463+ ).execute ()
464+
465+ else : # remote
466+ # Remote Server configuration
467+ console .print ("\n [cyan]Remote Server Configuration[/]" )
468+
469+ answers ["url" ] = inquirer .text (
470+ message = "Server URL:" ,
471+ validate = lambda text : text .strip ().startswith (("http://" , "https://" )) if text .strip () else False ,
472+ invalid_message = "URL must start with http:// or https://" ,
473+ keybindings = {"interrupt" : [{"key" : "escape" }]},
474+ ).execute ()
475+
476+ answers ["headers" ] = inquirer .text (
477+ message = "HTTP headers (KEY=value,KEY2=value2):" ,
478+ instruction = "(Leave empty for no custom headers)" ,
479+ keybindings = {"interrupt" : [{"key" : "escape" }]},
480+ ).execute ()
481+
482+ # Confirmation
483+ console .print ("\n [bold]Summary of new server:[/]" )
484+ console .print (f"Name: [cyan]{ answers ['name' ]} [/]" )
485+ console .print (f"Type: [cyan]{ answers ['type' ].upper ()} [/]" )
486+
487+ if answers ["type" ] == "stdio" :
488+ console .print (f"Command: [cyan]{ answers ['command' ]} [/]" )
489+ new_args = [arg .strip () for arg in answers ['args' ].split ("," ) if arg .strip ()] if answers ['args' ] else []
490+ console .print (f"Arguments: [cyan]{ new_args } [/]" )
491+
492+ new_env = {}
493+ if answers ['env' ]:
494+ for env_pair in answers ['env' ].split ("," ):
495+ if "=" in env_pair :
496+ key , value = env_pair .split ("=" , 1 )
497+ new_env [key .strip ()] = value .strip ()
498+ console .print (f"Environment: [cyan]{ new_env } [/]" )
499+
500+ else : # remote
501+ console .print (f"URL: [cyan]{ answers ['url' ]} [/]" )
502+
503+ new_headers = {}
504+ if answers ['headers' ]:
505+ for header_pair in answers ['headers' ].split ("," ):
506+ if "=" in header_pair :
507+ key , value = header_pair .split ("=" , 1 )
508+ new_headers [key .strip ()] = value .strip ()
509+ console .print (f"Headers: [cyan]{ new_headers } [/]" )
510+
511+ confirm = inquirer .confirm (
512+ message = "Create this server?" ,
513+ default = True ,
514+ keybindings = {"interrupt" : [{"key" : "escape" }]},
515+ ).execute ()
516+
517+ if not confirm :
518+ return {"cancelled" : True }
519+
520+ finally :
521+ # Restore original argv
522+ sys .argv = original_argv
523+
524+ return {
525+ "cancelled" : False ,
526+ "answers" : answers ,
527+ }
528+
529+ except (KeyboardInterrupt , EOFError ):
530+ console .print ("\n [yellow]Operation cancelled[/]" )
531+ return {"cancelled" : True }
532+ except Exception as e :
533+ console .print (f"[red]Error running interactive form: { e } [/]" )
534+ return None
0 commit comments