@@ -40,12 +40,28 @@ def parse_host_list(host_arg: str):
4040 return hosts
4141
4242def request_confirmation (message : str , auto_approve : bool = False ) -> bool :
43- """Request user confirmation following Hatch patterns."""
44- if auto_approve :
43+ """Request user confirmation with non-TTY support following Hatch patterns."""
44+ import os
45+ import sys
46+
47+ # Check for non-interactive mode indicators
48+ if (auto_approve or
49+ not sys .stdin .isatty () or
50+ os .getenv ('HATCH_AUTO_APPROVE' , '' ).lower () in ('1' , 'true' , 'yes' )):
4551 return True
4652
47- response = input (f"{ message } [y/N]: " )
48- return response .lower () in ['y' , 'yes' ]
53+ # Interactive mode - request user input
54+ try :
55+ while True :
56+ response = input (f"{ message } [y/N]: " ).strip ().lower ()
57+ if response in ['y' , 'yes' ]:
58+ return True
59+ elif response in ['n' , 'no' , '' ]:
60+ return False
61+ else :
62+ print ("Please enter 'y' for yes or 'n' for no." )
63+ except (EOFError , KeyboardInterrupt ):
64+ return False
4965
5066def get_package_mcp_server_config (env_manager : HatchEnvironmentManager , env_name : str , package_name : str ) -> MCPServerConfig :
5167 """Get MCP server configuration for a package using existing APIs."""
@@ -419,6 +435,156 @@ def handle_mcp_backup_clean(host: str, older_than_days: Optional[int] = None, ke
419435 print (f"Error cleaning backups: { e } " )
420436 return 1
421437
438+ def parse_env_vars (env_list : Optional [list ]) -> dict :
439+ """Parse environment variables from command line format."""
440+ if not env_list :
441+ return {}
442+
443+ env_dict = {}
444+ for env_var in env_list :
445+ if '=' not in env_var :
446+ print (f"Warning: Invalid environment variable format '{ env_var } '. Expected KEY=VALUE" )
447+ continue
448+ key , value = env_var .split ('=' , 1 )
449+ env_dict [key .strip ()] = value .strip ()
450+
451+ return env_dict
452+
453+ def parse_headers (headers_list : Optional [list ]) -> dict :
454+ """Parse HTTP headers from command line format."""
455+ if not headers_list :
456+ return {}
457+
458+ headers_dict = {}
459+ for header in headers_list :
460+ if '=' not in header :
461+ print (f"Warning: Invalid header format '{ header } '. Expected KEY=VALUE" )
462+ continue
463+ key , value = header .split ('=' , 1 )
464+ headers_dict [key .strip ()] = value .strip ()
465+
466+ return headers_dict
467+
468+ def handle_mcp_configure (host : str , server_name : str , command : str , args : list ,
469+ env : Optional [list ] = None , url : Optional [str ] = None ,
470+ headers : Optional [list ] = None , no_backup : bool = False ,
471+ dry_run : bool = False , auto_approve : bool = False ):
472+ """Handle 'hatch mcp configure' command."""
473+ try :
474+ # Validate host type
475+ try :
476+ host_type = MCPHostType (host )
477+ except ValueError :
478+ print (f"Error: Invalid host '{ host } '. Supported hosts: { [h .value for h in MCPHostType ]} " )
479+ return 1
480+
481+ # Parse environment variables and headers
482+ env_dict = parse_env_vars (env )
483+ headers_dict = parse_headers (headers )
484+
485+ # Create server configuration (only include headers if URL is provided)
486+ config_data = {
487+ 'name' : server_name ,
488+ 'command' : command ,
489+ 'args' : args or [],
490+ 'env' : env_dict ,
491+ 'url' : url
492+ }
493+
494+ # Only add headers if URL is provided (per MCPServerConfig validation)
495+ if url and headers_dict :
496+ config_data ['headers' ] = headers_dict
497+
498+ server_config = MCPServerConfig (** config_data )
499+
500+ if dry_run :
501+ print (f"[DRY RUN] Would configure MCP server '{ server_name } ' on host '{ host } ':" )
502+ print (f"[DRY RUN] Command: { command } " )
503+ if args :
504+ print (f"[DRY RUN] Args: { args } " )
505+ if env_dict :
506+ print (f"[DRY RUN] Environment: { env_dict } " )
507+ if url :
508+ print (f"[DRY RUN] URL: { url } " )
509+ if headers_dict :
510+ print (f"[DRY RUN] Headers: { headers_dict } " )
511+ print (f"[DRY RUN] Backup: { 'Disabled' if no_backup else 'Enabled' } " )
512+ return 0
513+
514+ # Confirm operation unless auto-approved
515+ if not request_confirmation (
516+ f"Configure MCP server '{ server_name } ' on host '{ host } '?" ,
517+ auto_approve
518+ ):
519+ print ("Operation cancelled." )
520+ return 0
521+
522+ # Perform configuration
523+ mcp_manager = MCPHostConfigurationManager ()
524+ result = mcp_manager .configure_server (
525+ server_config = server_config ,
526+ hostname = host ,
527+ no_backup = no_backup
528+ )
529+
530+ if result .success :
531+ print (f"✓ Successfully configured MCP server '{ server_name } ' on host '{ host } '" )
532+ if result .backup_path :
533+ print (f" Backup created: { result .backup_path } " )
534+ return 0
535+ else :
536+ print (f"✗ Failed to configure MCP server '{ server_name } ' on host '{ host } ': { result .error_message } " )
537+ return 1
538+
539+ except Exception as e :
540+ print (f"Error configuring MCP server: { e } " )
541+ return 1
542+
543+ def handle_mcp_remove (host : str , server_name : str , no_backup : bool = False ,
544+ dry_run : bool = False , auto_approve : bool = False ):
545+ """Handle 'hatch mcp remove' command."""
546+ try :
547+ # Validate host type
548+ try :
549+ host_type = MCPHostType (host )
550+ except ValueError :
551+ print (f"Error: Invalid host '{ host } '. Supported hosts: { [h .value for h in MCPHostType ]} " )
552+ return 1
553+
554+ if dry_run :
555+ print (f"[DRY RUN] Would remove MCP server '{ server_name } ' from host '{ host } '" )
556+ print (f"[DRY RUN] Backup: { 'Disabled' if no_backup else 'Enabled' } " )
557+ return 0
558+
559+ # Confirm operation unless auto-approved
560+ if not request_confirmation (
561+ f"Remove MCP server '{ server_name } ' from host '{ host } '?" ,
562+ auto_approve
563+ ):
564+ print ("Operation cancelled." )
565+ return 0
566+
567+ # Perform removal
568+ mcp_manager = MCPHostConfigurationManager ()
569+ result = mcp_manager .remove_server (
570+ server_name = server_name ,
571+ hostname = host ,
572+ no_backup = no_backup
573+ )
574+
575+ if result .success :
576+ print (f"✓ Successfully removed MCP server '{ server_name } ' from host '{ host } '" )
577+ if result .backup_path :
578+ print (f" Backup created: { result .backup_path } " )
579+ return 0
580+ else :
581+ print (f"✗ Failed to remove MCP server '{ server_name } ' from host '{ host } ': { result .error_message } " )
582+ return 1
583+
584+ except Exception as e :
585+ print (f"Error removing MCP server: { e } " )
586+ return 1
587+
422588def main ():
423589 """Main entry point for Hatch CLI.
424590
@@ -572,6 +738,27 @@ def main():
572738 mcp_backup_clean_parser .add_argument ("--dry-run" , action = "store_true" , help = "Preview cleanup operation without execution" )
573739 mcp_backup_clean_parser .add_argument ("--auto-approve" , action = "store_true" , help = "Skip confirmation prompts" )
574740
741+ # MCP direct management commands
742+ mcp_configure_parser = mcp_subparsers .add_parser ("configure" , help = "Configure MCP server directly on host" )
743+ mcp_configure_parser .add_argument ("host" , help = "Host platform to configure (e.g., claude-desktop, cursor)" )
744+ mcp_configure_parser .add_argument ("server_name" , help = "Name for the MCP server" )
745+ mcp_configure_parser .add_argument ("command" , help = "Command to execute the MCP server" )
746+ mcp_configure_parser .add_argument ("args" , nargs = "*" , help = "Arguments for the MCP server command" )
747+ mcp_configure_parser .add_argument ("--env" , "-e" , action = "append" , help = "Environment variables (format: KEY=VALUE)" )
748+ mcp_configure_parser .add_argument ("--url" , help = "Server URL for remote MCP servers" )
749+ mcp_configure_parser .add_argument ("--headers" , action = "append" , help = "HTTP headers for remote servers (format: KEY=VALUE)" )
750+ mcp_configure_parser .add_argument ("--no-backup" , action = "store_true" , help = "Skip backup creation before configuration" )
751+ mcp_configure_parser .add_argument ("--dry-run" , action = "store_true" , help = "Preview configuration without execution" )
752+ mcp_configure_parser .add_argument ("--auto-approve" , action = "store_true" , help = "Skip confirmation prompts" )
753+
754+ # Remove MCP server command
755+ mcp_remove_parser = mcp_subparsers .add_parser ("remove" , help = "Remove MCP server from host" )
756+ mcp_remove_parser .add_argument ("host" , help = "Host platform to remove from (e.g., claude-desktop, cursor)" )
757+ mcp_remove_parser .add_argument ("server_name" , help = "Name of the MCP server to remove" )
758+ mcp_remove_parser .add_argument ("--no-backup" , action = "store_true" , help = "Skip backup creation before removal" )
759+ mcp_remove_parser .add_argument ("--dry-run" , action = "store_true" , help = "Preview removal without execution" )
760+ mcp_remove_parser .add_argument ("--auto-approve" , action = "store_true" , help = "Skip confirmation prompts" )
761+
575762 # Package management commands
576763 pkg_subparsers = subparsers .add_parser ("package" , help = "Package management commands" ).add_subparsers (
577764 dest = "pkg_command" , help = "Package command to execute"
@@ -1046,6 +1233,20 @@ def main():
10461233 else :
10471234 print ("Unknown backup command" )
10481235 return 1
1236+
1237+ elif args .mcp_command == "configure" :
1238+ return handle_mcp_configure (
1239+ args .host , args .server_name , args .command , args .args ,
1240+ args .env , args .url , args .headers , args .no_backup ,
1241+ args .dry_run , args .auto_approve
1242+ )
1243+
1244+ elif args .mcp_command == "remove" :
1245+ return handle_mcp_remove (
1246+ args .host , args .server_name , args .no_backup ,
1247+ args .dry_run , args .auto_approve
1248+ )
1249+
10491250 else :
10501251 print ("Unknown MCP command" )
10511252 return 1
0 commit comments