diff --git a/src/mcpm/cli.py b/src/mcpm/cli.py index e651380d..0b0ac5a0 100644 --- a/src/mcpm/cli.py +++ b/src/mcpm/cli.py @@ -141,14 +141,10 @@ def main(ctx, help_flag): commands_table.add_row("[yellow]server[/]") commands_table.add_row(" [cyan]search[/]", "Search available MCP servers.") commands_table.add_row(" [cyan]add[/]", "Add an MCP server directly to a client.") - commands_table.add_row(" [cyan]cp[/]") - commands_table.add_row(" [cyan]copy[/]", "Copy a server from one client/profile to another.") - commands_table.add_row(" [cyan]mv[/]") - commands_table.add_row(" [cyan]move[/]", "Move a server from one client/profile to another.") - commands_table.add_row(" [cyan]rm[/]") - commands_table.add_row(" [cyan]remove[/]", "Remove an installed MCP server.") - commands_table.add_row(" [cyan]ls[/]") - commands_table.add_row(" [cyan]list[/]", "List all installed MCP servers.") + commands_table.add_row(" [cyan]cp[/]", "Copy a server from one client/profile to another.") + commands_table.add_row(" [cyan]mv[/]", "Move a server from one client/profile to another.") + commands_table.add_row(" [cyan]rm[/]", "Remove an installed MCP server.") + commands_table.add_row(" [cyan]ls[/]", "List all installed MCP servers.") commands_table.add_row(" [cyan]stash[/]", "Temporarily store a server configuration aside.") commands_table.add_row(" [cyan]pop[/]", "Restore a previously stashed server configuration.") @@ -167,15 +163,13 @@ def main(ctx, help_flag): # Additional helpful information console.print("") - console.print("[italic]Run [bold]mcpm -h[/] for more information on a command.[/]") + console.print("[italic]Run [bold]mcpm COMMAND -h[/] for more information on a command.[/]") # Register commands main.add_command(search.search) -main.add_command(remove.remove) main.add_command(remove.remove, name="rm") main.add_command(add.add) -main.add_command(list.list) main.add_command(list.list, name="ls") main.add_command(stash.stash) @@ -185,9 +179,7 @@ def main(ctx, help_flag): main.add_command(config.config) main.add_command(inspector.inspector, name="inspector") main.add_command(profile.profile, name="profile") -main.add_command(transfer.move) main.add_command(transfer.move, name="mv") -main.add_command(transfer.copy) main.add_command(transfer.copy, name="cp") main.add_command(profile.activate) main.add_command(profile.deactivate) diff --git a/src/mcpm/clients/base.py b/src/mcpm/clients/base.py index b6817dbe..0cb511a4 100644 --- a/src/mcpm/clients/base.py +++ b/src/mcpm/clients/base.py @@ -13,6 +13,8 @@ from ruamel.yaml import YAML from mcpm.schemas.server_config import ServerConfig, STDIOServerConfig +from mcpm.utils.config import ROUTER_SERVER_NAME +from mcpm.utils.router_server import format_server_url logger = logging.getLogger(__name__) @@ -132,6 +134,30 @@ def is_client_installed(self) -> bool: """ pass + @abc.abstractmethod + def activate_profile(self, profile_name: str, router_config: Dict[str, Any]) -> bool: + """ + Activate a profile in the client config + + Args: + profile_name: Name of the profile + router_config: Router configuration + + Returns: + bool: Success or failure + """ + pass + + @abc.abstractmethod + def deactivate_profile(self) -> bool: + """ + Deactivate a profile in the client config + + Returns: + bool: Success or failure + """ + pass + class JSONClientManager(BaseClientManager): """ @@ -350,6 +376,33 @@ def is_client_installed(self) -> bool: # Can be overridden by subclasses return os.path.isdir(os.path.dirname(self.config_path)) + def activate_profile(self, profile_name: str, router_config: Dict[str, Any]) -> bool: + """Activate a profile in the client config + + Args: + profile_name: Name of the profile + + Returns: + bool: Success or failure + """ + host = router_config["host"] + port = router_config["port"] + default_base_url = f"http://{host}:{port}/sse" + + server_config = self._format_router_server(profile_name, default_base_url) + return self.add_server(server_config) + + def _format_router_server(self, profile_name, base_url) -> ServerConfig: + return format_server_url(self.client_key, profile_name, base_url) + + def deactivate_profile(self) -> bool: + """Deactivate a profile in the client config + + Returns: + bool: Success or failure + """ + return self.remove_server(ROUTER_SERVER_NAME) + class YAMLClientManager(BaseClientManager): """ @@ -589,3 +642,30 @@ def is_client_installed(self) -> bool: """ # Check if the config directory exists return os.path.isdir(os.path.dirname(self.config_path)) + + def activate_profile(self, profile_name: str, router_config: Dict[str, Any]) -> bool: + """Activate a profile in the client config + + Args: + profile_name: Name of the profile + + Returns: + bool: Success or failure + """ + host = router_config["host"] + port = router_config["port"] + default_base_url = f"http://{host}:{port}/sse" + + server_config = self._format_router_server(profile_name, default_base_url) + return self.add_server(server_config) + + def _format_router_server(self, profile_name, base_url) -> ServerConfig: + return format_server_url(self.client_key, profile_name, base_url) + + def deactivate_profile(self) -> bool: + """Deactivate a profile in the client config + + Returns: + bool: Success or failure + """ + return self.remove_server(ROUTER_SERVER_NAME) diff --git a/src/mcpm/clients/client_registry.py b/src/mcpm/clients/client_registry.py index 1e8408de..bcd81995 100644 --- a/src/mcpm/clients/client_registry.py +++ b/src/mcpm/clients/client_registry.py @@ -17,6 +17,7 @@ from mcpm.clients.managers.fiveire import FiveireManager from mcpm.clients.managers.goose import GooseClientManager from mcpm.clients.managers.windsurf import WindsurfManager +from mcpm.utils.config import ConfigManager from mcpm.utils.scope import CLIENT_PREFIX, PROFILE_PREFIX logger = logging.getLogger(__name__) @@ -205,3 +206,37 @@ def determine_active_scope(cls) -> str | None: if client: return f"{CLIENT_PREFIX}{client}" return None + + @classmethod + def activate_profile(cls, client_name: str, profile_name: str) -> bool: + """ + Activate a profile in the client config + + Args: + client_name: Name of the client + profile_name: Name of the profile + + Returns: + bool: Success or failure + """ + router_config = ConfigManager().get_router_config() + client = cls.get_client_manager(client_name) + if client is None: + return False + return client.activate_profile(profile_name, router_config) + + @classmethod + def deactivate_profile(cls, client_name: str) -> bool: + """ + Deactivate a profile in the client config + + Args: + profile_name: Name of the profile + + Returns: + bool: Success or failure + """ + client = cls.get_client_manager(client_name) + if client is None: + return False + return client.deactivate_profile() diff --git a/src/mcpm/clients/managers/claude_desktop.py b/src/mcpm/clients/managers/claude_desktop.py index 7d0892b2..384c25ac 100644 --- a/src/mcpm/clients/managers/claude_desktop.py +++ b/src/mcpm/clients/managers/claude_desktop.py @@ -7,6 +7,8 @@ from typing import Any, Dict from mcpm.clients.base import JSONClientManager +from mcpm.schemas.server_config import ServerConfig +from mcpm.utils.router_server import format_server_url_with_proxy_param logger = logging.getLogger(__name__) @@ -111,6 +113,9 @@ def is_server_disabled(self, server_name: str) -> bool: config = self._load_config() return "disabledServers" in config and server_name in config["disabledServers"] + def _format_router_server(self, profile_name, base_url) -> ServerConfig: + return format_server_url_with_proxy_param(self.client_key, profile_name, base_url) + # Uses base class implementation of remove_server # Uses base class implementation of get_server diff --git a/src/mcpm/clients/managers/continue_extension.py b/src/mcpm/clients/managers/continue_extension.py index 73fd20c4..deb5d666 100644 --- a/src/mcpm/clients/managers/continue_extension.py +++ b/src/mcpm/clients/managers/continue_extension.py @@ -10,6 +10,7 @@ from mcpm.clients.base import YAMLClientManager from mcpm.schemas.server_config import ServerConfig, STDIOServerConfig +from mcpm.utils.router_server import format_server_url_with_proxy_headers logger = logging.getLogger(__name__) @@ -203,3 +204,6 @@ def from_client_format(self, server_name: str, client_config: Dict[str, Any]) -> } server_data.update(client_config) return TypeAdapter(ServerConfig).validate_python(server_data) + + def _format_router_server(self, profile_name, base_url) -> ServerConfig: + return format_server_url_with_proxy_headers(self.client_key, profile_name, base_url) diff --git a/src/mcpm/commands/config.py b/src/mcpm/commands/config.py index 96c86cc7..bf807817 100644 --- a/src/mcpm/commands/config.py +++ b/src/mcpm/commands/config.py @@ -12,6 +12,7 @@ @click.group() +@click.help_option("-h", "--help") def config(): """Manage MCPM configuration. @@ -21,6 +22,7 @@ def config(): @config.command() +@click.help_option("-h", "--help") def clear_cache(): """Clear the local repository cache. diff --git a/src/mcpm/commands/list.py b/src/mcpm/commands/list.py index ffae4af4..74678251 100644 --- a/src/mcpm/commands/list.py +++ b/src/mcpm/commands/list.py @@ -19,12 +19,15 @@ @click.command(name="list") @click.option("--target", "-t", help="Target to list servers from") +@click.help_option("-h", "--help") def list(target: str | None = None): """List all installed MCP servers. Examples: + + \b mcpm list - mcpm list -t + mcpm list -t @cursor """ if target is None: target = ClientRegistry.determine_active_scope() diff --git a/src/mcpm/commands/profile.py b/src/mcpm/commands/profile.py index eb2ad641..28084f7f 100644 --- a/src/mcpm/commands/profile.py +++ b/src/mcpm/commands/profile.py @@ -5,12 +5,14 @@ from mcpm.clients.client_registry import ClientRegistry from mcpm.profile.profile_config import ProfileConfigManager from mcpm.schemas.server_config import STDIOServerConfig +from mcpm.utils.config import ConfigManager profile_config_manager = ProfileConfigManager() console = Console() @click.group() +@click.help_option("-h", "--help") def profile(): """Manage MCPM profiles.""" pass @@ -18,8 +20,9 @@ def profile(): @click.command() @click.argument("profile_name") -@click.option("--client", "-c", default="client", help="Client of the profile") -def activate(profile_name, client): +@click.option("--client", "-c", help="Client of the profile") +@click.help_option("-h", "--help") +def activate(profile_name, client=None): """Activate a profile. Sets the specified profile as the active profile. @@ -31,17 +34,37 @@ def activate(profile_name, client): # Set the active profile client_registry = ClientRegistry() - if client_registry.set_active_profile(profile_name): + config_manager = ConfigManager() + + if client: + console.print(f"[bold cyan]Activating profile '{profile_name}' in client '{client}'...[/]") + client_manager = ClientRegistry.get_client_manager(client) + if client_manager is None: + console.print(f"[bold red]Error:[/] Client '{client}' not found.") + return + success = client_manager.activate_profile(profile_name, config_manager.get_router_config()) + else: + client = ClientRegistry.get_active_client() + if client is None: + console.print("[bold yellow]No active client found.[/]\n") + return + console.print(f"[bold cyan]Activating profile '{profile_name}' in active client '{client}'...[/]") + client_manager = ClientRegistry.get_client_manager(client) + if client_manager is None: + console.print(f"[bold red]Error:[/] Client '{client}' not found.") + return + success = client_manager.activate_profile(profile_name, config_manager.get_router_config()) + if success: + client_registry.set_active_profile(profile_name) console.print(f"\n[green]Profile '{profile_name}' activated successfully.[/]\n") else: console.print(f"[bold red]Error:[/] Failed to activate profile '{profile_name}'.") - # TODO: add url to the client config - @click.command() -@click.option("--client", "-c", default="client", help="Client of the profile") -def deactivate(client): +@click.option("--client", "-c", help="Client of the profile") +@click.help_option("-h", "--help") +def deactivate(client=None): """Deactivate a profile. Unsets the active profile. @@ -53,16 +76,35 @@ def deactivate(client): return console.print(f"\n[green]Deactivating profile '{active_profile}'...[/]") client_registry = ClientRegistry() - if client_registry.set_active_profile(None): - console.print(f"\n[green]Profile '{active_profile}' deactivated successfully.[/]\n") - else: - console.print(f"[bold red]Error:[/] Failed to deactivate profile '{active_profile}'.") - # TODO: remove url from the client config + if client: + console.print(f"[bold cyan]Deactivating profile '{active_profile}' in client '{client}'...[/]") + client_manager = ClientRegistry.get_client_manager(client) + if client_manager is None: + console.print(f"[bold red]Error:[/] Client '{client}' not found.") + return + success = client_manager.deactivate_profile() + else: + client = ClientRegistry.get_active_client() + if client is None: + console.print("[bold yellow]No active client found.[/]\n") + return + console.print(f"[bold cyan]Deactivating profile '{active_profile}' in active client '{client}'...[/]") + client_manager = ClientRegistry.get_client_manager(client) + if client_manager is None: + console.print(f"[bold red]Error:[/] Client '{client}' not found.") + return + success = client_manager.deactivate_profile() + if success: + client_registry.set_active_profile(None) + console.print(f"\n[yellow]Profile '{active_profile}' deactivated successfully.[/]\n") + else: + console.print(f"[bold red]Error:[/] Failed to deactivate profile '{active_profile}' in client '{client}'.") @profile.command(name="ls") @click.option("--verbose", "-v", is_flag=True, help="Show detailed server information") +@click.help_option("-h", "--help") def list(verbose=False): """List all MCPM profiles.""" profiles = profile_config_manager.list_profiles() @@ -93,6 +135,7 @@ def list(verbose=False): @profile.command() @click.argument("profile") @click.option("--force", is_flag=True, help="Force add even if profile already exists") +@click.help_option("-h", "--help") def add(profile, force=False): """Add a new MCPM profile.""" if profile_config_manager.get_profile(profile) is not None and not force: @@ -112,6 +155,7 @@ def add(profile, force=False): @profile.command() @click.argument("profile") @click.option("--server", "-s", required=True, help="Server to apply config to") +@click.help_option("-h", "--help") def apply(profile, server): """Apply an existing MCPM config to a profile.""" client_manager = ClientRegistry.get_active_client_manager() @@ -147,6 +191,7 @@ def apply(profile, server): @profile.command() @click.argument("profile_name") +@click.help_option("-h", "--help") def remove(profile_name): """Delete an MCPM profile.""" if not profile_config_manager.delete_profile(profile_name): @@ -157,6 +202,7 @@ def remove(profile_name): @profile.command() @click.argument("profile_name") +@click.help_option("-h", "--help") def rename(profile_name): """Rename an MCPM profile.""" new_profile_name = click.prompt("Enter new profile name", type=str) diff --git a/src/mcpm/commands/router.py b/src/mcpm/commands/router.py index 9960dbf8..fa0efba6 100644 --- a/src/mcpm/commands/router.py +++ b/src/mcpm/commands/router.py @@ -11,8 +11,10 @@ import click import psutil from rich.console import Console +from rich.prompt import Confirm -from mcpm.utils.config import ConfigManager +from mcpm.clients.client_registry import ClientRegistry +from mcpm.utils.config import ROUTER_SERVER_NAME, ConfigManager from mcpm.utils.platform import get_log_directory, get_pid_directory logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s") @@ -26,55 +28,6 @@ LOG_DIR = get_log_directory("mcpm") LOG_DIR.mkdir(parents=True, exist_ok=True) -# default config -DEFAULT_HOST = "localhost" -DEFAULT_PORT = 6276 # 6276 represents MCPM on a T9 keypad (6=M, 2=C, 7=P, 6=M) - - -def get_router_config(): - """get router configuration from config file, if not exists, flush default config""" - config_manager = ConfigManager() - config = config_manager.get_config() - - # check if router config exists - if "router" not in config: - # create default config and save - router_config = {"host": DEFAULT_HOST, "port": DEFAULT_PORT} - config_manager.set_config("router", router_config) - return router_config - - # get existing config - router_config = config.get("router", {}) - - # check if host and port exist, if not, set default values and update config - # user may only set a customized port while leave host undefined - updated = False - if "host" not in router_config: - router_config["host"] = DEFAULT_HOST - updated = True - if "port" not in router_config: - router_config["port"] = DEFAULT_PORT - updated = True - - # save config if updated - if updated: - config_manager.set_config("router", router_config) - - return router_config - - -def save_router_config(host, port): - """save router configuration to config file""" - config_manager = ConfigManager() - router_config = config_manager.get_config().get("router", {}) - - # update config - router_config["host"] = host - router_config["port"] = port - - # save config - return config_manager.set_config("router", router_config) - def is_process_running(pid): """check if the process is running""" @@ -121,16 +74,20 @@ def remove_pid_file(): @click.group(name="router") +@click.help_option("-h", "--help") def router(): """Manage MCP router service.""" pass @router.command(name="on") +@click.help_option("-h", "--help") def start_router(): """Start MCPRouter as a daemon process. Example: + + \b mcpm router on """ # check if there is a router already running @@ -141,7 +98,7 @@ def start_router(): return # get router config - config = get_router_config() + config = ConfigManager().get_router_config() host = config["host"] port = config["port"] @@ -190,6 +147,7 @@ def start_router(): @router.command(name="set") @click.option("-H", "--host", type=str, help="Host to bind the SSE server to") @click.option("-p", "--port", type=int, help="Port to bind the SSE server to") +@click.help_option("-h", "--help") def set_router_config(host, port): """Set MCPRouter global configuration. @@ -202,14 +160,15 @@ def set_router_config(host, port): return # get current config, make sure all field are filled by default value if not exists - current_config = get_router_config() + config_manager = ConfigManager() + current_config = config_manager.get_router_config() # if user does not specify a host, use current config host = host or current_config["host"] port = port or current_config["port"] # save config - if save_router_config(host, port): + if config_manager.save_router_config(host, port): console.print(f"[bold green]Router configuration updated:[/] host={host}, port={port}") console.print("The new configuration will be used next time you start the router.") @@ -221,13 +180,39 @@ def set_router_config(host, port): console.print(" mcpm router on") else: console.print("[bold red]Error:[/] Failed to save router configuration.") + return + + if Confirm.ask("Do you want to update router for all clients now?"): + active_profile = ClientRegistry.get_active_profile() + if not active_profile: + console.print("[yellow]No active profile found, skipped.[/]") + return + installed_clients = ClientRegistry.detect_installed_clients() + for client, installed in installed_clients.items(): + if not installed: + continue + client_manager = ClientRegistry.get_client_manager(client) + if client_manager is None: + console.print(f"[yellow]Client '{client}' not found.[/] Skipping...") + continue + if client_manager.get_server(ROUTER_SERVER_NAME): + console.print(f"[cyan]Updating profile router for {client}...[/]") + client_manager.deactivate_profile() + client_manager.activate_profile(active_profile, config_manager.get_router_config()) + console.print(f"[green]Profile router updated for {client}[/]") + console.print("[bold green]Success: Profile router updated for all clients[/]") + if pid: + console.print("[bold yellow]Restart MCPRouter to apply new settings.[/]\n") @router.command(name="off") +@click.help_option("-h", "--help") def stop_router(): """Stop the running MCPRouter daemon process. Example: + + \b mcpm router off """ # check if there is a router already running @@ -253,14 +238,17 @@ def stop_router(): @router.command(name="status") +@click.help_option("-h", "--help") def router_status(): """Check the status of the MCPRouter daemon process. Example: + + \b mcpm router status """ # get router config - config = get_router_config() + config = ConfigManager().get_router_config() host = config["host"] port = config["port"] diff --git a/src/mcpm/commands/search.py b/src/mcpm/commands/search.py index 627c33cd..21075f91 100644 --- a/src/mcpm/commands/search.py +++ b/src/mcpm/commands/search.py @@ -15,12 +15,15 @@ @click.command() @click.argument("query", required=False) @click.option("--detailed", is_flag=True, help="Show detailed server information") +@click.help_option("-h", "--help") def search(query, detailed=False): """Search available MCP servers. Searches the MCP registry for available servers. Without arguments, lists all available servers. Examples: + + \b mcpm search # List all available servers mcpm search github # Search for github server mcpm search --detailed # Show detailed information diff --git a/src/mcpm/commands/server_operations/add.py b/src/mcpm/commands/server_operations/add.py index b030cb3e..88a0d7bc 100644 --- a/src/mcpm/commands/server_operations/add.py +++ b/src/mcpm/commands/server_operations/add.py @@ -29,10 +29,13 @@ @click.option("--force", is_flag=True, help="Force reinstall if server is already installed") @click.option("--alias", help="Alias for the server", required=False) @click.option("--target", "-t", help="Target to add server to", required=False) +@click.help_option("-h", "--help") def add(server_name, force=False, alias=None, target: str | None = None): """Add an MCP server to a client configuration. Examples: + + \b mcpm add time mcpm add everything --force mcpm add youtube --alias yt diff --git a/src/mcpm/commands/server_operations/pop.py b/src/mcpm/commands/server_operations/pop.py index 4f01cf42..c56d0d9e 100644 --- a/src/mcpm/commands/server_operations/pop.py +++ b/src/mcpm/commands/server_operations/pop.py @@ -20,6 +20,7 @@ @click.command() @click.argument("server_name") +@click.help_option("-h", "--help") def pop(server_name): """Restore a previously stashed server configuration. @@ -27,7 +28,10 @@ def pop(server_name): restoring it to active status. Examples: + + \b mcpm pop memory + mcpm pop %profile/memory """ scope_type, scope, server_name = determine_target(server_name) if not scope_type or not scope or not server_name: diff --git a/src/mcpm/commands/server_operations/remove.py b/src/mcpm/commands/server_operations/remove.py index 2f952e50..011ca334 100644 --- a/src/mcpm/commands/server_operations/remove.py +++ b/src/mcpm/commands/server_operations/remove.py @@ -22,10 +22,13 @@ @click.command() @click.argument("server_name") @click.option("--force", is_flag=True, help="Force removal without confirmation") +@click.help_option("-h", "--help") def remove(server_name, force): """Remove an installed MCP server. Examples: + + \b mcpm rm filesystem mcpm rm @cursor/filesystem mcpm rm %profile/filesystem diff --git a/src/mcpm/commands/server_operations/stash.py b/src/mcpm/commands/server_operations/stash.py index b2b55eea..a133a83c 100644 --- a/src/mcpm/commands/server_operations/stash.py +++ b/src/mcpm/commands/server_operations/stash.py @@ -22,6 +22,7 @@ @click.command() @click.argument("server_name") +@click.help_option("-h", "--help") def stash(server_name): """Temporarily store a server configuration aside. @@ -29,7 +30,11 @@ def stash(server_name): configuration for later use. You can restore it with the 'pop' command. Examples: + + \b mcpm stash memory + mcpm stash @cursor/memory + mcpm stash %profile/memory """ scope_type, scope, server_name = determine_target(server_name) if not scope_type or not scope or not server_name: diff --git a/src/mcpm/commands/server_operations/transfer.py b/src/mcpm/commands/server_operations/transfer.py index d243a250..8a5b5106 100644 --- a/src/mcpm/commands/server_operations/transfer.py +++ b/src/mcpm/commands/server_operations/transfer.py @@ -52,12 +52,15 @@ def determine_source_and_destination( @click.command() @click.argument("source") @click.argument("destination") +@click.help_option("-h", "--help") @click.option("--force", is_flag=True, help="Force copy even if destination already exists") def copy(source, destination, force=False): """ Copy a server configuration from one client/profile to another. Examples: + + \b mcpm cp memory memory2 mcpm cp @cursor/memory @windsurf/memory """ @@ -108,11 +111,14 @@ def copy(source, destination, force=False): @click.argument("source") @click.argument("destination") @click.option("--force", is_flag=True, help="Force move even if destination already exists") +@click.help_option("-h", "--help") def move(source, destination, force=False): """ Move a server configuration from one client/profile to another. Examples: + + \b mcpm mv memory memory2 mcpm mv @cursor/memory @windsurf/memory """ diff --git a/src/mcpm/utils/config.py b/src/mcpm/utils/config.py index cf57478e..f3569676 100644 --- a/src/mcpm/utils/config.py +++ b/src/mcpm/utils/config.py @@ -12,6 +12,10 @@ # Default configuration paths DEFAULT_CONFIG_DIR = os.path.expanduser("~/.config/mcpm") DEFAULT_CONFIG_FILE = os.path.join(DEFAULT_CONFIG_DIR, "config.json") +# default router config +DEFAULT_HOST = "localhost" +DEFAULT_PORT = 6276 # 6276 represents MCPM on a T9 keypad (6=M, 2=C, 7=P, 6=M) +ROUTER_SERVER_NAME = "mcpm_router" class ConfigManager: @@ -83,3 +87,44 @@ def set_config(self, key: str, value: Any) -> bool: except Exception as e: logger.error(f"Error setting configuration {key}: {str(e)}") return False + + def get_router_config(self): + """get router configuration from config file, if not exists, flush default config""" + config = self.get_config() + + # check if router config exists + if "router" not in config: + # create default config and save + router_config = {"host": DEFAULT_HOST, "port": DEFAULT_PORT} + self.set_config("router", router_config) + return router_config + + # get existing config + router_config = config.get("router", {}) + + # check if host and port exist, if not, set default values and update config + # user may only set a customized port while leave host undefined + updated = False + if "host" not in router_config: + router_config["host"] = DEFAULT_HOST + updated = True + if "port" not in router_config: + router_config["port"] = DEFAULT_PORT + updated = True + + # save config if updated + if updated: + self.set_config("router", router_config) + + return router_config + + def save_router_config(self, host, port): + """save router configuration to config file""" + router_config = self.get_config().get("router", {}) + + # update config + router_config["host"] = host + router_config["port"] = port + + # save config + return self.set_config("router", router_config) diff --git a/src/mcpm/utils/router_server.py b/src/mcpm/utils/router_server.py new file mode 100644 index 00000000..4a2247ab --- /dev/null +++ b/src/mcpm/utils/router_server.py @@ -0,0 +1,27 @@ +from mcpm.clients.base import ROUTER_SERVER_NAME +from mcpm.schemas.server_config import ServerConfig, SSEServerConfig, STDIOServerConfig + + +def format_server_url(client: str, profile: str, router_url: str) -> ServerConfig: + return SSEServerConfig( + name=ROUTER_SERVER_NAME, + url=f"{router_url}?/client={client}&profile={profile}", + ) + + +def format_server_url_with_proxy_param(client: str, profile: str, router_url: str) -> ServerConfig: + result = STDIOServerConfig( + name=ROUTER_SERVER_NAME, + command="uvx", + args=["mcp-proxy", f"{router_url}?/client={client}&profile={profile}"], + ) + return result + + +def format_server_url_with_proxy_headers(client: str, profile: str, router_url: str) -> ServerConfig: + result = STDIOServerConfig( + name=ROUTER_SERVER_NAME, + command="uvx", + args=["mcp-proxy", router_url, "--headers", "profile", profile], + ) + return result diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 00000000..0f7c9ba9 --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,34 @@ +from collections import deque + +from click import Group +from click.testing import CliRunner + +from mcpm.cli import main + + +def test_cli_help(): + """Test that all commands have help options.""" + runner = CliRunner() + + def bfs(cmd): + queue = deque([cmd]) + commands = [] + while queue: + cmd = queue.popleft() + sub_cmds = cmd.commands.values() + for sub_cmd in sub_cmds: + commands.append(sub_cmd) + if isinstance(sub_cmd, Group): + queue.append(sub_cmd) + return commands + + all_commands = bfs(main) + for cmd in all_commands: + result = runner.invoke(cmd, ["--help"]) + assert result.exit_code == 0 + assert "Usage:" in result.output + + for cmd in all_commands: + result = runner.invoke(cmd, ["-h"]) + assert result.exit_code == 0 + assert "Usage:" in result.output