1010import argparse
1111import json
1212import logging
13+ import shlex
1314import sys
1415from pathlib import Path
1516from typing import Optional , List
@@ -567,13 +568,13 @@ def parse_env_vars(env_list: Optional[list]) -> dict:
567568
568569 return env_dict
569570
570- def parse_headers ( headers_list : Optional [list ]) -> dict :
571+ def parse_header ( header_list : Optional [list ]) -> dict :
571572 """Parse HTTP headers from command line format."""
572- if not headers_list :
573+ if not header_list :
573574 return {}
574575
575576 headers_dict = {}
576- for header in headers_list :
577+ for header in header_list :
577578 if '=' not in header :
578579 print (f"Warning: Invalid header format '{ header } '. Expected KEY=VALUE" )
579580 continue
@@ -582,7 +583,7 @@ def parse_headers(headers_list: Optional[list]) -> dict:
582583
583584 return headers_dict
584585
585- def parse_inputs ( inputs_list : Optional [list ]) -> Optional [list ]:
586+ def parse_input ( input_list : Optional [list ]) -> Optional [list ]:
586587 """Parse VS Code input variable definitions from command line format.
587588
588589 Format: type,id,description[,password=true]
@@ -591,11 +592,11 @@ def parse_inputs(inputs_list: Optional[list]) -> Optional[list]:
591592 Returns:
592593 List of input variable definition dictionaries, or None if no inputs provided.
593594 """
594- if not inputs_list :
595+ if not input_list :
595596 return None
596597
597598 parsed_inputs = []
598- for input_str in inputs_list :
599+ for input_str in input_list :
599600 parts = [p .strip () for p in input_str .split (',' )]
600601 if len (parts ) < 3 :
601602 print (f"Warning: Invalid input format '{ input_str } '. Expected: type,id,description[,password=true]" )
@@ -617,11 +618,11 @@ def parse_inputs(inputs_list: Optional[list]) -> Optional[list]:
617618
618619def handle_mcp_configure (host : str , server_name : str , command : str , args : list ,
619620 env : Optional [list ] = None , url : Optional [str ] = None ,
620- headers : Optional [list ] = None , timeout : Optional [int ] = None ,
621+ header : Optional [list ] = None , timeout : Optional [int ] = None ,
621622 trust : bool = False , cwd : Optional [str ] = None ,
622623 env_file : Optional [str ] = None , http_url : Optional [str ] = None ,
623624 include_tools : Optional [list ] = None , exclude_tools : Optional [list ] = None ,
624- inputs : Optional [list ] = None , no_backup : bool = False ,
625+ input : Optional [list ] = None , no_backup : bool = False ,
625626 dry_run : bool = False , auto_approve : bool = False ):
626627 """Handle 'hatch mcp configure' command with ALL host-specific arguments.
627628
@@ -637,23 +638,41 @@ def handle_mcp_configure(host: str, server_name: str, command: str, args: list,
637638 print (f"Error: Invalid host '{ host } '. Supported hosts: { [h .value for h in MCPHostType ]} " )
638639 return 1
639640
641+ # Validate Claude Desktop/Code transport restrictions (Issue 2)
642+ if host_type in (MCPHostType .CLAUDE_DESKTOP , MCPHostType .CLAUDE_CODE ):
643+ if url is not None :
644+ print (f"Error: { host } does not support remote servers (--url). Only local servers with --command are supported." )
645+ return 1
646+
640647 # Validate argument dependencies
641- if command and headers :
642- print ("Error: --headers can only be used with --url (remote servers), not with --command (local servers)" )
648+ if command and header :
649+ print ("Error: --header can only be used with --url or --http -url (remote servers), not with --command (local servers)" )
643650 return 1
644651
645- if url and args :
646- print ("Error: --args can only be used with --command (local servers), not with --url (remote servers)" )
652+ if ( url or http_url ) and args :
653+ print ("Error: --args can only be used with --command (local servers), not with --url or --http-url (remote servers)" )
647654 return 1
648655
649656 # NOTE: We do NOT validate host-specific arguments here.
650657 # The reporting system will show unsupported fields as "UNSUPPORTED" in the conversion report.
651658 # This allows users to see which fields are not supported by their target host without blocking the operation.
652659
660+ # Check if server exists (for partial update support)
661+ manager = MCPHostConfigurationManager ()
662+ existing_config = manager .get_server_config (host , server_name )
663+ is_update = existing_config is not None
664+
665+ # Conditional validation: Create requires command OR url OR http_url, update does not
666+ if not is_update :
667+ # Create operation: require command, url, or http_url
668+ if not command and not url and not http_url :
669+ print (f"Error: When creating a new server, you must provide either --command (for local servers), --url (for SSE remote servers), or --http-url (for HTTP remote servers, Gemini only)" )
670+ return 1
671+
653672 # Parse environment variables, headers, and inputs
654673 env_dict = parse_env_vars (env )
655- headers_dict = parse_headers ( headers )
656- inputs_list = parse_inputs ( inputs )
674+ headers_dict = parse_header ( header )
675+ inputs_list = parse_input ( input )
657676
658677 # Create Omni configuration (universal model)
659678 # Only include fields that have actual values to ensure model_dump(exclude_unset=True) works correctly
@@ -662,12 +681,24 @@ def handle_mcp_configure(host: str, server_name: str, command: str, args: list,
662681 if command is not None :
663682 omni_config_data ['command' ] = command
664683 if args is not None :
665- omni_config_data ['args' ] = args
684+ # Process args with shlex.split() to handle quoted strings (Issue 4)
685+ processed_args = []
686+ for arg in args :
687+ if arg : # Skip empty strings
688+ try :
689+ # Split quoted strings into individual arguments
690+ split_args = shlex .split (arg )
691+ processed_args .extend (split_args )
692+ except ValueError as e :
693+ # Handle invalid quotes gracefully
694+ print (f"Warning: Invalid quote in argument '{ arg } ': { e } " )
695+ processed_args .append (arg )
696+ omni_config_data ['args' ] = processed_args if processed_args else None
666697 if env_dict :
667698 omni_config_data ['env' ] = env_dict
668699 if url is not None :
669700 omni_config_data ['url' ] = url
670- if url and headers_dict :
701+ if headers_dict :
671702 omni_config_data ['headers' ] = headers_dict
672703
673704 # Host-specific fields (Gemini)
@@ -692,6 +723,29 @@ def handle_mcp_configure(host: str, server_name: str, command: str, args: list,
692723 if inputs_list is not None :
693724 omni_config_data ['inputs' ] = inputs_list
694725
726+ # Partial update merge logic
727+ if is_update :
728+ # Merge with existing configuration
729+ existing_data = existing_config .model_dump (exclude_unset = True , exclude = {'name' })
730+
731+ # Handle command/URL/httpUrl switching behavior
732+ # If switching from command to URL or httpUrl: clear command-based fields
733+ if (url is not None or http_url is not None ) and existing_config .command is not None :
734+ existing_data .pop ('command' , None )
735+ existing_data .pop ('args' , None )
736+ existing_data .pop ('type' , None ) # Clear type field when switching transports (Issue 1)
737+
738+ # If switching from URL/httpUrl to command: clear URL-based fields
739+ if command is not None and (existing_config .url is not None or getattr (existing_config , 'httpUrl' , None ) is not None ):
740+ existing_data .pop ('url' , None )
741+ existing_data .pop ('httpUrl' , None )
742+ existing_data .pop ('headers' , None )
743+ existing_data .pop ('type' , None ) # Clear type field when switching transports (Issue 1)
744+
745+ # Merge: new values override existing values
746+ merged_data = {** existing_data , ** omni_config_data }
747+ omni_config_data = merged_data
748+
695749 # Create Omni model
696750 omni_config = MCPServerConfigOmni (** omni_config_data )
697751
@@ -706,10 +760,11 @@ def handle_mcp_configure(host: str, server_name: str, command: str, args: list,
706760
707761 # Generate conversion report
708762 report = generate_conversion_report (
709- operation = 'create' ,
763+ operation = 'update' if is_update else ' create' ,
710764 server_name = server_name ,
711765 target_host = host_type ,
712766 omni = omni_config ,
767+ old_config = existing_config if is_update else None ,
713768 dry_run = dry_run
714769 )
715770
@@ -1208,25 +1263,25 @@ def main():
12081263 # Create mutually exclusive group for server type
12091264 server_type_group = mcp_configure_parser .add_mutually_exclusive_group (required = True )
12101265 server_type_group .add_argument ("--command" , dest = "server_command" , help = "Command to execute the MCP server (for local servers)" )
1211- server_type_group .add_argument ("--url" , help = "Server URL for remote MCP servers" )
1266+ server_type_group .add_argument ("--url" , help = "Server URL for remote MCP servers (SSE transport)" )
1267+ server_type_group .add_argument ("--http-url" , help = "HTTP streaming endpoint URL (Gemini only)" )
12121268
12131269 mcp_configure_parser .add_argument ("--args" , nargs = "*" , help = "Arguments for the MCP server command (only with --command)" )
12141270 mcp_configure_parser .add_argument ("--env-var" , action = "append" , help = "Environment variables (format: KEY=VALUE)" )
1215- mcp_configure_parser .add_argument ("--headers " , action = "append" , help = "HTTP headers for remote servers (format: KEY=VALUE, only with --url)" )
1271+ mcp_configure_parser .add_argument ("--header " , action = "append" , help = "HTTP headers for remote servers (format: KEY=VALUE, only with --url)" )
12161272
12171273 # Host-specific arguments (Gemini)
12181274 mcp_configure_parser .add_argument ("--timeout" , type = int , help = "Request timeout in milliseconds (Gemini)" )
12191275 mcp_configure_parser .add_argument ("--trust" , action = "store_true" , help = "Bypass tool call confirmations (Gemini)" )
12201276 mcp_configure_parser .add_argument ("--cwd" , help = "Working directory for stdio transport (Gemini)" )
1221- mcp_configure_parser .add_argument ("--http-url" , help = "HTTP streaming endpoint URL (Gemini)" )
12221277 mcp_configure_parser .add_argument ("--include-tools" , nargs = "*" , help = "Tool allowlist - only these tools will be available (Gemini)" )
12231278 mcp_configure_parser .add_argument ("--exclude-tools" , nargs = "*" , help = "Tool blocklist - these tools will be excluded (Gemini)" )
12241279
12251280 # Host-specific arguments (Cursor/VS Code/LM Studio)
12261281 mcp_configure_parser .add_argument ("--env-file" , help = "Path to environment file (Cursor, VS Code, LM Studio)" )
12271282
12281283 # Host-specific arguments (VS Code)
1229- mcp_configure_parser .add_argument ("--inputs " , action = "append" , help = "Input variable definitions in format: type,id,description[,password=true] (VS Code)" )
1284+ mcp_configure_parser .add_argument ("--input " , action = "append" , help = "Input variable definitions in format: type,id,description[,password=true] (VS Code)" )
12301285
12311286 mcp_configure_parser .add_argument ("--no-backup" , action = "store_true" , help = "Skip backup creation before configuration" )
12321287 mcp_configure_parser .add_argument ("--dry-run" , action = "store_true" , help = "Preview configuration without execution" )
@@ -2022,11 +2077,11 @@ def main():
20222077 elif args .mcp_command == "configure" :
20232078 return handle_mcp_configure (
20242079 args .host , args .server_name , args .server_command , args .args ,
2025- getattr (args , 'env_var' , None ), args .url , args .headers ,
2080+ getattr (args , 'env_var' , None ), args .url , args .header ,
20262081 getattr (args , 'timeout' , None ), getattr (args , 'trust' , False ),
20272082 getattr (args , 'cwd' , None ), getattr (args , 'env_file' , None ),
20282083 getattr (args , 'http_url' , None ), getattr (args , 'include_tools' , None ),
2029- getattr (args , 'exclude_tools' , None ), getattr (args , 'inputs ' , None ),
2084+ getattr (args , 'exclude_tools' , None ), getattr (args , 'input ' , None ),
20302085 args .no_backup , args .dry_run , args .auto_approve
20312086 )
20322087
0 commit comments