diff --git a/README.md b/README.md index 1a5f42a..e0e8807 100644 --- a/README.md +++ b/README.md @@ -165,6 +165,31 @@ mc config volume remove /local/path Default volumes will be combined with any volumes specified using the `-v` flag when creating a session. +### Default MCP Servers Configuration + +You can configure default MCP servers that sessions will automatically connect to: + +```bash +# List default MCP servers +mc config mcp list + +# Add an MCP server to defaults +mc config mcp add github + +# Remove an MCP server from defaults +mc config mcp remove github +``` + +When adding new MCP servers, they are added to defaults by default. Use the `--no-default` flag to prevent this: + +```bash +# Add an MCP server without adding it to defaults +mc mcp add github ghcr.io/mcp/github:latest --no-default +mc mcp add-remote jira https://jira-mcp.example.com/sse --no-default +``` + +When creating sessions, if no MCP server is specified with `--mcp`, the default MCP servers will be used automatically. + ### External Network Connectivity MC containers can connect to external Docker networks, allowing them to communicate with other services in those networks: @@ -269,7 +294,7 @@ mc mcp remote add github http://my-mcp-server.example.com/sse --header "Authoriz mc mcp docker add github mcp/github:latest --command "github-mcp" --env GITHUB_TOKEN=ghp_123456 # Add a proxy-based MCP server (for stdio-to-SSE conversion) -mc mcp proxy add github ghcr.io/mcp/github:latest --proxy-image ghcr.io/sparfenyuk/mcp-proxy:latest --command "github-mcp" --sse-port 8080 +mc mcp add github ghcr.io/mcp/github:latest --proxy-image ghcr.io/sparfenyuk/mcp-proxy:latest --command "github-mcp" --sse-port 8080 --no-default ``` ### Using MCP Servers with Sessions diff --git a/mcontainer/cli.py b/mcontainer/cli.py index e3c3e67..f54affe 100644 --- a/mcontainer/cli.py +++ b/mcontainer/cli.py @@ -204,6 +204,15 @@ def create_session( # Combine default networks with user-specified networks, removing duplicates all_networks = list(set(default_networks + network)) + # Get default MCPs from user config if none specified + all_mcps = mcp if isinstance(mcp, list) else [] + if not all_mcps: + default_mcps = user_config.get("defaults.mcps", []) + all_mcps = default_mcps + + if default_mcps: + console.print(f"Using default MCP servers: {', '.join(default_mcps)}") + if all_networks: console.print(f"Networks: {', '.join(all_networks)}") @@ -222,7 +231,7 @@ def create_session( mount_local=not no_mount and user_config.get("defaults.mount_local", True), volumes=volume_mounts, networks=all_networks, - mcp=mcp, + mcp=all_mcps, ) if session: @@ -230,11 +239,6 @@ def create_session( console.print(f"Session ID: {session.id}") console.print(f"Driver: {session.driver}") - if session.mcps: - console.print("MCP Servers:") - for mcp in session.mcps: - console.print(f" - {mcp}") - if session.ports: console.print("Ports:") for container_port, host_port in session.ports.items(): @@ -392,6 +396,13 @@ def quick_create( # Use user config for defaults if not specified if not driver: driver = user_config.get("defaults.driver") + + # Get default MCPs if none specified + all_mcps = mcp if isinstance(mcp, list) else [] + if not all_mcps: + default_mcps = user_config.get("defaults.mcps", []) + if default_mcps: + all_mcps = default_mcps create_session( driver=driver, @@ -402,7 +413,7 @@ def quick_create( name=name, no_connect=no_connect, no_mount=no_mount, - mcp=mcp, + mcp=all_mcps, ) @@ -522,6 +533,64 @@ config_app.add_typer(network_app, name="network", no_args_is_help=True) volume_app = typer.Typer(help="Manage default volumes") config_app.add_typer(volume_app, name="volume", no_args_is_help=True) +# Create an MCP subcommand for config +config_mcp_app = typer.Typer(help="Manage default MCP servers") +config_app.add_typer(config_mcp_app, name="mcp", no_args_is_help=True) + +# MCP configuration commands +@config_mcp_app.command("list") +def list_default_mcps() -> None: + """List all default MCP servers""" + default_mcps = user_config.get("defaults.mcps", []) + + if not default_mcps: + console.print("No default MCP servers configured") + return + + table = Table(show_header=True, header_style="bold") + table.add_column("MCP Server") + + for mcp in default_mcps: + table.add_row(mcp) + + console.print(table) + +@config_mcp_app.command("add") +def add_default_mcp( + name: str = typer.Argument(..., help="MCP server name to add to defaults"), +) -> None: + """Add an MCP server to default MCPs""" + # First check if the MCP server exists + mcp = mcp_manager.get_mcp(name) + if not mcp: + console.print(f"[red]MCP server '{name}' not found[/red]") + return + + default_mcps = user_config.get("defaults.mcps", []) + + if name in default_mcps: + console.print(f"MCP server '{name}' is already in defaults") + return + + default_mcps.append(name) + user_config.set("defaults.mcps", default_mcps) + console.print(f"[green]Added MCP server '{name}' to defaults[/green]") + +@config_mcp_app.command("remove") +def remove_default_mcp( + name: str = typer.Argument(..., help="MCP server name to remove from defaults"), +) -> None: + """Remove an MCP server from default MCPs""" + default_mcps = user_config.get("defaults.mcps", []) + + if name not in default_mcps: + console.print(f"MCP server '{name}' is not in defaults") + return + + default_mcps.remove(name) + user_config.set("defaults.mcps", default_mcps) + console.print(f"[green]Removed MCP server '{name}' from defaults[/green]") + # Configuration commands @config_app.command("list") @@ -1252,6 +1321,9 @@ def add_mcp( env: List[str] = typer.Option( [], "--env", "-e", help="Environment variables (format: KEY=VALUE)" ), + no_default: bool = typer.Option( + False, "--no-default", help="Don't add MCP server to defaults" + ), ) -> None: """Add a proxy-based MCP server (default type)""" # Parse environment variables @@ -1282,6 +1354,7 @@ def add_mcp( proxy_options, environment, host_port, + add_as_default=not no_default, ) # Get the assigned port @@ -1292,6 +1365,11 @@ def add_mcp( console.print( f"Container port {sse_port} will be bound to host port {assigned_port}" ) + + if not no_default: + console.print(f"MCP server '{name}' added to defaults") + else: + console.print(f"MCP server '{name}' not added to defaults") except Exception as e: console.print(f"[red]Error adding MCP server: {e}[/red]") @@ -1304,6 +1382,9 @@ def add_remote_mcp( header: List[str] = typer.Option( [], "--header", "-H", help="HTTP headers (format: KEY=VALUE)" ), + no_default: bool = typer.Option( + False, "--no-default", help="Don't add MCP server to defaults" + ), ) -> None: """Add a remote MCP server""" # Parse headers @@ -1319,9 +1400,14 @@ def add_remote_mcp( try: with console.status(f"Adding remote MCP server '{name}'..."): - mcp_manager.add_remote_mcp(name, url, headers) + mcp_manager.add_remote_mcp(name, url, headers, add_as_default=not no_default) console.print(f"[green]Added remote MCP server '{name}'[/green]") + + if not no_default: + console.print(f"MCP server '{name}' added to defaults") + else: + console.print(f"MCP server '{name}' not added to defaults") except Exception as e: console.print(f"[red]Error adding remote MCP server: {e}[/red]") diff --git a/mcontainer/mcp.py b/mcontainer/mcp.py index 16160fc..e9df621 100644 --- a/mcontainer/mcp.py +++ b/mcontainer/mcp.py @@ -74,9 +74,19 @@ class MCPManager: return None def add_remote_mcp( - self, name: str, url: str, headers: Dict[str, str] = None + self, name: str, url: str, headers: Dict[str, str] = None, add_as_default: bool = True ) -> Dict[str, Any]: - """Add a remote MCP server.""" + """Add a remote MCP server. + + Args: + name: Name of the MCP server + url: URL of the remote MCP server + headers: HTTP headers to use when connecting + add_as_default: Whether to add this MCP to the default MCPs list + + Returns: + The MCP configuration dictionary + """ # Create the remote MCP configuration remote_mcp = RemoteMCP( name=name, @@ -91,17 +101,36 @@ class MCPManager: mcps = [mcp for mcp in mcps if mcp.get("name") != name] # Add the new MCP - mcps.append(remote_mcp.model_dump()) + mcp_config = remote_mcp.model_dump() + mcps.append(mcp_config) # Save the configuration self.config_manager.set("mcps", mcps) + + # Add to default MCPs if requested + if add_as_default: + default_mcps = self.config_manager.get("defaults.mcps", []) + if name not in default_mcps: + default_mcps.append(name) + self.config_manager.set("defaults.mcps", default_mcps) - return remote_mcp.model_dump() + return mcp_config def add_docker_mcp( - self, name: str, image: str, command: str, env: Dict[str, str] = None + self, name: str, image: str, command: str, env: Dict[str, str] = None, add_as_default: bool = True ) -> Dict[str, Any]: - """Add a Docker-based MCP server.""" + """Add a Docker-based MCP server. + + Args: + name: Name of the MCP server + image: Docker image for the MCP server + command: Command to run in the container + env: Environment variables to set in the container + add_as_default: Whether to add this MCP to the default MCPs list + + Returns: + The MCP configuration dictionary + """ # Create the Docker MCP configuration docker_mcp = DockerMCP( name=name, @@ -117,12 +146,20 @@ class MCPManager: mcps = [mcp for mcp in mcps if mcp.get("name") != name] # Add the new MCP - mcps.append(docker_mcp.model_dump()) + mcp_config = docker_mcp.model_dump() + mcps.append(mcp_config) # Save the configuration self.config_manager.set("mcps", mcps) + + # Add to default MCPs if requested + if add_as_default: + default_mcps = self.config_manager.get("defaults.mcps", []) + if name not in default_mcps: + default_mcps.append(name) + self.config_manager.set("defaults.mcps", default_mcps) - return docker_mcp.model_dump() + return mcp_config def add_proxy_mcp( self, @@ -133,8 +170,23 @@ class MCPManager: proxy_options: Dict[str, Any] = None, env: Dict[str, str] = None, host_port: Optional[int] = None, + add_as_default: bool = True, ) -> Dict[str, Any]: - """Add a proxy-based MCP server.""" + """Add a proxy-based MCP server. + + Args: + name: Name of the MCP server + base_image: Base Docker image running the actual MCP server + proxy_image: Docker image for the MCP proxy + command: Command to run in the container + proxy_options: Options for the MCP proxy + env: Environment variables to set in the container + host_port: Host port to bind the MCP server to (auto-assigned if not specified) + add_as_default: Whether to add this MCP to the default MCPs list + + Returns: + The MCP configuration dictionary + """ # If no host port specified, find the next available port starting from 5101 if host_port is None: # Get current MCPs and find highest assigned port @@ -171,15 +223,30 @@ class MCPManager: mcps = [mcp for mcp in mcps if mcp.get("name") != name] # Add the new MCP - mcps.append(proxy_mcp.model_dump()) + mcp_config = proxy_mcp.model_dump() + mcps.append(mcp_config) # Save the configuration self.config_manager.set("mcps", mcps) + + # Add to default MCPs if requested + if add_as_default: + default_mcps = self.config_manager.get("defaults.mcps", []) + if name not in default_mcps: + default_mcps.append(name) + self.config_manager.set("defaults.mcps", default_mcps) - return proxy_mcp.model_dump() + return mcp_config def remove_mcp(self, name: str) -> bool: - """Remove an MCP server configuration.""" + """Remove an MCP server configuration. + + Args: + name: Name of the MCP server to remove + + Returns: + True if the MCP was successfully removed, False otherwise + """ mcps = self.list_mcps() # Filter out the MCP with the specified name @@ -191,6 +258,12 @@ class MCPManager: # Save the updated configuration self.config_manager.set("mcps", updated_mcps) + + # Also remove from default MCPs if it's there + default_mcps = self.config_manager.get("defaults.mcps", []) + if name in default_mcps: + default_mcps.remove(name) + self.config_manager.set("defaults.mcps", default_mcps) # Stop and remove the container if it exists self.stop_mcp(name) diff --git a/mcontainer/user_config.py b/mcontainer/user_config.py index e8c1d60..b37b994 100644 --- a/mcontainer/user_config.py +++ b/mcontainer/user_config.py @@ -93,6 +93,7 @@ class UserConfigManager: "mount_local": True, "networks": [], # Default networks to connect to (besides mc-network) "volumes": [], # Default volumes to mount, format: "source:dest" + "mcps": [], # Default MCP servers to connect to }, "services": { "langfuse": {},