From f50374f10a5892a17316567a02b35f275b767ba6 Mon Sep 17 00:00:00 2001 From: Jon Redeker Date: Wed, 26 Nov 2025 20:01:05 -0500 Subject: [PATCH 1/7] feat: add improved non-interactive mode support for install/uninstall commands --- src/mcpm/commands/install.py | 16 +++++++++++++--- src/mcpm/commands/uninstall.py | 3 ++- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/mcpm/commands/install.py b/src/mcpm/commands/install.py index f526a41..84c21a5 100644 --- a/src/mcpm/commands/install.py +++ b/src/mcpm/commands/install.py @@ -20,6 +20,7 @@ from mcpm.profile.profile_config import ProfileConfigManager from mcpm.schemas.full_server_config import FullServerConfig from mcpm.utils.config import NODE_EXECUTABLES, ConfigManager +from mcpm.utils.non_interactive import should_force_operation, is_non_interactive from mcpm.utils.repository import RepositoryManager from mcpm.utils.rich_click_config import click @@ -84,6 +85,15 @@ def prompt_with_default(prompt_text, default="", hide_input=False, required=Fals Returns: The user's input or the default value if empty """ + # Check for non-interactive mode + if is_non_interactive() or should_force_operation(): + if default: + return default + if required: + # Cannot fulfill required argument without default in non-interactive mode + raise click.Abort() + return "" + # if default: # console.print(f"Default: [yellow]{default}[/]") @@ -161,7 +171,7 @@ def install(server_name, force=False, alias=None): # Confirm addition alias_text = f" as '{alias}'" if alias else "" - if not force and not Confirm.ask(f"Install this server to global configuration{alias_text}?"): + if not force and not should_force_operation() and not Confirm.ask(f"Install this server to global configuration{alias_text}?"): console.print("[yellow]Operation cancelled.[/]") return @@ -206,7 +216,7 @@ def install(server_name, force=False, alias=None): selected_method = installations[method_id] # If multiple methods are available and not forced, offer selection - if len(installations) > 1 and not force: + if len(installations) > 1 and not force and not should_force_operation(): console.print("\n[bold]Available installation methods:[/]") methods_list = [] @@ -426,7 +436,7 @@ def install(server_name, force=False, alias=None): ) # Add server to global configuration - success = global_add_server(full_server_config.to_server_config(), force) + success = global_add_server(full_server_config.to_server_config(), force or should_force_operation()) if success: # Server has been successfully added to the global configuration diff --git a/src/mcpm/commands/uninstall.py b/src/mcpm/commands/uninstall.py index 4e058f4..18ac7b4 100644 --- a/src/mcpm/commands/uninstall.py +++ b/src/mcpm/commands/uninstall.py @@ -7,6 +7,7 @@ from mcpm.global_config import GlobalConfigManager from mcpm.utils.display import print_server_config +from mcpm.utils.non_interactive import should_force_operation from mcpm.utils.rich_click_config import click console = Console() @@ -59,7 +60,7 @@ def uninstall(server_name, force): print_server_config(server_info) # Get confirmation if --force is not used - if not force: + if not force and not should_force_operation(): console.print(f"\n[bold yellow]Are you sure you want to remove:[/] {server_name}") console.print("[italic]To bypass this confirmation, use --force[/]") # Use Rich's Confirm for a better user experience From f57f3b85f28d93c211cf6f9821b195c9a31e5306 Mon Sep 17 00:00:00 2001 From: Jon Redeker Date: Wed, 26 Nov 2025 20:15:17 -0500 Subject: [PATCH 2/7] fix: use explicit env var check to avoid breaking tests in non-tty environments --- src/mcpm/commands/install.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/mcpm/commands/install.py b/src/mcpm/commands/install.py index 84c21a5..687edaa 100644 --- a/src/mcpm/commands/install.py +++ b/src/mcpm/commands/install.py @@ -85,8 +85,11 @@ def prompt_with_default(prompt_text, default="", hide_input=False, required=Fals Returns: The user's input or the default value if empty """ - # Check for non-interactive mode - if is_non_interactive() or should_force_operation(): + # Check for explicit non-interactive mode (Env Var) + # We do NOT check is_non_interactive() here because it includes isatty(), + # which returns True in tests (CliRunner), causing us to skip mocked prompts. + # Users desiring non-interactive behavior must set MCPM_NON_INTERACTIVE=true. + if os.getenv("MCPM_NON_INTERACTIVE", "").lower() == "true" or should_force_operation(): if default: return default if required: From c86cc953860438ae8942b40b1915f02ce390df94 Mon Sep 17 00:00:00 2001 From: Jon Redeker Date: Wed, 26 Nov 2025 20:19:01 -0500 Subject: [PATCH 3/7] refactor: consolidate force logic and improve error handling based on review --- src/mcpm/commands/install.py | 10 ++++++---- src/mcpm/commands/uninstall.py | 2 +- src/mcpm/utils/non_interactive.py | 10 +++++++--- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/mcpm/commands/install.py b/src/mcpm/commands/install.py index 687edaa..4624547 100644 --- a/src/mcpm/commands/install.py +++ b/src/mcpm/commands/install.py @@ -94,7 +94,9 @@ def prompt_with_default(prompt_text, default="", hide_input=False, required=Fals return default if required: # Cannot fulfill required argument without default in non-interactive mode - raise click.Abort() + raise click.UsageError( + "A required value has no default and cannot be prompted in non-interactive mode." + ) return "" # if default: @@ -174,7 +176,7 @@ def install(server_name, force=False, alias=None): # Confirm addition alias_text = f" as '{alias}'" if alias else "" - if not force and not should_force_operation() and not Confirm.ask(f"Install this server to global configuration{alias_text}?"): + if not should_force_operation(force) and not Confirm.ask(f"Install this server to global configuration{alias_text}?"): console.print("[yellow]Operation cancelled.[/]") return @@ -219,7 +221,7 @@ def install(server_name, force=False, alias=None): selected_method = installations[method_id] # If multiple methods are available and not forced, offer selection - if len(installations) > 1 and not force and not should_force_operation(): + if len(installations) > 1 and not should_force_operation(force): console.print("\n[bold]Available installation methods:[/]") methods_list = [] @@ -439,7 +441,7 @@ def install(server_name, force=False, alias=None): ) # Add server to global configuration - success = global_add_server(full_server_config.to_server_config(), force or should_force_operation()) + success = global_add_server(full_server_config.to_server_config(), should_force_operation(force)) if success: # Server has been successfully added to the global configuration diff --git a/src/mcpm/commands/uninstall.py b/src/mcpm/commands/uninstall.py index 18ac7b4..85525df 100644 --- a/src/mcpm/commands/uninstall.py +++ b/src/mcpm/commands/uninstall.py @@ -60,7 +60,7 @@ def uninstall(server_name, force): print_server_config(server_info) # Get confirmation if --force is not used - if not force and not should_force_operation(): + if not should_force_operation(force): console.print(f"\n[bold yellow]Are you sure you want to remove:[/] {server_name}") console.print("[italic]To bypass this confirmation, use --force[/]") # Use Rich's Confirm for a better user experience diff --git a/src/mcpm/utils/non_interactive.py b/src/mcpm/utils/non_interactive.py index c2d4efd..4670330 100644 --- a/src/mcpm/utils/non_interactive.py +++ b/src/mcpm/utils/non_interactive.py @@ -32,13 +32,17 @@ def is_non_interactive() -> bool: return False -def should_force_operation() -> bool: +def should_force_operation(cli_force_flag: bool = False) -> bool: """ Check if operations should be forced (skip confirmations). - Returns True if MCPM_FORCE environment variable is set to 'true'. + Args: + cli_force_flag: Boolean flag from CLI args (e.g. --force) + + Returns: + True if cli_force_flag is True OR MCPM_FORCE environment variable is set to 'true'. """ - return os.getenv("MCPM_FORCE", "").lower() == "true" + return cli_force_flag or os.getenv("MCPM_FORCE", "").lower() == "true" def should_output_json() -> bool: From ddf4ab922d569767f42b6f9f2a48b11a8a8abd85 Mon Sep 17 00:00:00 2001 From: Jon Redeker Date: Wed, 26 Nov 2025 20:19:01 -0500 Subject: [PATCH 4/7] refactor: consolidate force logic and improve error handling based on review --- src/mcpm/commands/install.py | 10 ++++++---- src/mcpm/commands/uninstall.py | 2 +- src/mcpm/utils/non_interactive.py | 10 +++++++--- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/mcpm/commands/install.py b/src/mcpm/commands/install.py index 687edaa..4624547 100644 --- a/src/mcpm/commands/install.py +++ b/src/mcpm/commands/install.py @@ -94,7 +94,9 @@ def prompt_with_default(prompt_text, default="", hide_input=False, required=Fals return default if required: # Cannot fulfill required argument without default in non-interactive mode - raise click.Abort() + raise click.UsageError( + "A required value has no default and cannot be prompted in non-interactive mode." + ) return "" # if default: @@ -174,7 +176,7 @@ def install(server_name, force=False, alias=None): # Confirm addition alias_text = f" as '{alias}'" if alias else "" - if not force and not should_force_operation() and not Confirm.ask(f"Install this server to global configuration{alias_text}?"): + if not should_force_operation(force) and not Confirm.ask(f"Install this server to global configuration{alias_text}?"): console.print("[yellow]Operation cancelled.[/]") return @@ -219,7 +221,7 @@ def install(server_name, force=False, alias=None): selected_method = installations[method_id] # If multiple methods are available and not forced, offer selection - if len(installations) > 1 and not force and not should_force_operation(): + if len(installations) > 1 and not should_force_operation(force): console.print("\n[bold]Available installation methods:[/]") methods_list = [] @@ -439,7 +441,7 @@ def install(server_name, force=False, alias=None): ) # Add server to global configuration - success = global_add_server(full_server_config.to_server_config(), force or should_force_operation()) + success = global_add_server(full_server_config.to_server_config(), should_force_operation(force)) if success: # Server has been successfully added to the global configuration diff --git a/src/mcpm/commands/uninstall.py b/src/mcpm/commands/uninstall.py index 18ac7b4..85525df 100644 --- a/src/mcpm/commands/uninstall.py +++ b/src/mcpm/commands/uninstall.py @@ -60,7 +60,7 @@ def uninstall(server_name, force): print_server_config(server_info) # Get confirmation if --force is not used - if not force and not should_force_operation(): + if not should_force_operation(force): console.print(f"\n[bold yellow]Are you sure you want to remove:[/] {server_name}") console.print("[italic]To bypass this confirmation, use --force[/]") # Use Rich's Confirm for a better user experience diff --git a/src/mcpm/utils/non_interactive.py b/src/mcpm/utils/non_interactive.py index c2d4efd..4670330 100644 --- a/src/mcpm/utils/non_interactive.py +++ b/src/mcpm/utils/non_interactive.py @@ -32,13 +32,17 @@ def is_non_interactive() -> bool: return False -def should_force_operation() -> bool: +def should_force_operation(cli_force_flag: bool = False) -> bool: """ Check if operations should be forced (skip confirmations). - Returns True if MCPM_FORCE environment variable is set to 'true'. + Args: + cli_force_flag: Boolean flag from CLI args (e.g. --force) + + Returns: + True if cli_force_flag is True OR MCPM_FORCE environment variable is set to 'true'. """ - return os.getenv("MCPM_FORCE", "").lower() == "true" + return cli_force_flag or os.getenv("MCPM_FORCE", "").lower() == "true" def should_output_json() -> bool: From 8fa0cd8320fc3dcd7290a9f45483528f2b248d51 Mon Sep 17 00:00:00 2001 From: Jon Redeker Date: Wed, 26 Nov 2025 20:54:05 -0500 Subject: [PATCH 5/7] refactor: consolidate force logic, improve error handling, and ensure tests pass --- src/mcpm/commands/install.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/mcpm/commands/install.py b/src/mcpm/commands/install.py index 4624547..2c084dc 100644 --- a/src/mcpm/commands/install.py +++ b/src/mcpm/commands/install.py @@ -73,7 +73,7 @@ def global_add_server(server_config: ServerConfig, force: bool = False) -> bool: return global_config_manager.add_server(server_config, force) -def prompt_with_default(prompt_text, default="", hide_input=False, required=False): +def prompt_with_default(prompt_text, default="", hide_input=False, required=False, force=False): """Prompt the user with a default value that can be edited directly. Args: @@ -81,6 +81,7 @@ def prompt_with_default(prompt_text, default="", hide_input=False, required=Fals default: The default value to show in the prompt hide_input: Whether to hide the input (for passwords) required: Whether this is a required field + force: Whether to force non-interactive mode Returns: The user's input or the default value if empty @@ -89,7 +90,7 @@ def prompt_with_default(prompt_text, default="", hide_input=False, required=Fals # We do NOT check is_non_interactive() here because it includes isatty(), # which returns True in tests (CliRunner), causing us to skip mocked prompts. # Users desiring non-interactive behavior must set MCPM_NON_INTERACTIVE=true. - if os.getenv("MCPM_NON_INTERACTIVE", "").lower() == "true" or should_force_operation(): + if os.getenv("MCPM_NON_INTERACTIVE", "").lower() == "true" or should_force_operation(force): if default: return default if required: From e446580ec110db0d33086fadfa81064137dd66d7 Mon Sep 17 00:00:00 2001 From: Jon Redeker Date: Thu, 27 Nov 2025 20:59:25 -0500 Subject: [PATCH 6/7] fix: address code review comments - Remove unused 'is_non_interactive' import in install.py - Use stderr for installation status message in install.py --- src/mcpm/commands/install.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/mcpm/commands/install.py b/src/mcpm/commands/install.py index 2c084dc..d508240 100644 --- a/src/mcpm/commands/install.py +++ b/src/mcpm/commands/install.py @@ -20,7 +20,7 @@ from mcpm.profile.profile_config import ProfileConfigManager from mcpm.schemas.full_server_config import FullServerConfig from mcpm.utils.config import NODE_EXECUTABLES, ConfigManager -from mcpm.utils.non_interactive import should_force_operation, is_non_interactive +from mcpm.utils.non_interactive import should_force_operation from mcpm.utils.repository import RepositoryManager from mcpm.utils.rich_click_config import click @@ -153,7 +153,8 @@ def install(server_name, force=False, alias=None): config_name = alias or server_name # All servers are installed to global configuration - console.print("[yellow]Installing server to global configuration...[/]") + console_stderr = Console(stderr=True) + console_stderr.print("[yellow]Installing server to global configuration...[/]") # Get server metadata from repository server_metadata = repo_manager.get_server_metadata(server_name) From 06901e82f039bee242cfb9bea194019d630b7db3 Mon Sep 17 00:00:00 2001 From: Jon Redeker Date: Fri, 28 Nov 2025 11:49:26 -0500 Subject: [PATCH 7/7] feat(core): Implement Auto-UVX Injection for Python servers - Upgrades execution to 'uv run' if uv is detected - Solves dependency isolation and Pydantic version conflicts - Eliminates need for pip install step for Python servers --- src/mcpm/commands/install.py | 43 ++++++++++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/src/mcpm/commands/install.py b/src/mcpm/commands/install.py index d508240..c1cc182 100644 --- a/src/mcpm/commands/install.py +++ b/src/mcpm/commands/install.py @@ -5,6 +5,7 @@ import json import os import re +import shutil from enum import Enum from prompt_toolkit import PromptSession @@ -95,9 +96,7 @@ def prompt_with_default(prompt_text, default="", hide_input=False, required=Fals return default if required: # Cannot fulfill required argument without default in non-interactive mode - raise click.UsageError( - "A required value has no default and cannot be prompted in non-interactive mode." - ) + raise click.UsageError("A required value has no default and cannot be prompted in non-interactive mode.") return "" # if default: @@ -178,7 +177,9 @@ def install(server_name, force=False, alias=None): # Confirm addition alias_text = f" as '{alias}'" if alias else "" - if not should_force_operation(force) and not Confirm.ask(f"Install this server to global configuration{alias_text}?"): + if not should_force_operation(force) and not Confirm.ask( + f"Install this server to global configuration{alias_text}?" + ): console.print("[yellow]Operation cancelled.[/]") return @@ -428,6 +429,40 @@ def install(server_name, force=False, alias=None): mcp_command = install_command mcp_args = processed_args + # --- Auto-UVX Injection Logic --- + # If 'uv' is available and we are using python/pip, try to upgrade to 'uv run' for isolation. + # This solves the "Pydantic Versioning" dependency hell by isolating servers. + if mcp_command in ["python", "python3", "pip"] and shutil.which("uv"): + # We need to determine the package name to run 'uv run --with ' + # If package_name was defined in the installation method, use it. + # If not, check if we are running 'python -m ' and guess package name from module? + # Or default to the server name if reasonable? + target_package = package_name + + # If args start with '-m', the next arg is the module. + # Often module == package (e.g. mcp_server_time -> mcp-server-time? No, dashes vs underscores). + # But 'uv run --with python -m ' usually works if PyPI name matches. + + if not target_package and mcp_args and mcp_args[0] == "-m" and len(mcp_args) > 1: + # Heuristic: Assume package name matches module name (with _ -> - maybe?) + # Ideally, the registry should provide 'package'. + # For now, we only auto-upgrade if we have a package name OR if we are brave. + # Let's rely on package_name variable extracted earlier from selected_method.get("package") + pass + + if target_package: + console.print(f"[bold blue]🚀 Auto-upgrading to 'uv run' for isolation (package: {target_package})[/]") + # Old: python -m module ... + # New: uv run --with package python -m module ... + + # We prepend 'run --with package' to the command execution + # mcp_command becomes 'uv' + # mcp_args becomes ['run', '--with', target_package, original_command] + mcp_args + + new_args = ["run", "--with", target_package, mcp_command] + mcp_args + mcp_command = "uv" + mcp_args = new_args + # Create server configuration using FullServerConfig full_server_config = FullServerConfig( name=config_name,