From 4b7ae39bba7bb004c57eba7f95d28803ac864d56 Mon Sep 17 00:00:00 2001 From: Mathieu Virbel Date: Wed, 26 Mar 2025 16:44:35 +0100 Subject: [PATCH] feat(mcp): ensure inner mcp environemnt variables are passed --- README.md | 30 ++++- mcontainer/cli.py | 189 +++++++++++++++------------- mcontainer/container.py | 79 ++++++++---- mcontainer/drivers/goose/Dockerfile | 3 +- mcontainer/mcp.py | 122 ++++++++++++------ pyproject.toml | 1 + 6 files changed, 269 insertions(+), 155 deletions(-) diff --git a/README.md b/README.md index e0e8807..b359be5 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,14 @@ containers that run AI tools and development environments. It works with both local Docker and a dedicated remote web service that manages containers in a Docker-in-Docker (DinD) environment. MC also supports connecting to MCP (Model Control Protocol) servers to extend AI tools with additional capabilities. +## Quick Reference + +- `mc session create` - Create a new session +- `mcx` - Shortcut for `mc session create` (mount directories or clone repos) +- `mcx .` - Mount the current directory +- `mcx /path/to/dir` - Mount a specific directory +- `mcx https://github.com/user/repo` - Clone a repository + ## Requirements - [uv](https://docs.astral.sh/uv/) @@ -27,10 +35,12 @@ mc --help ## Basic Usage ```bash -# Create a new session with the default driver -# mc create session -- is the full command +# Show help message (displays available commands) mc +# Create a new session with the default driver +mc session create + # List all active sessions mc session list @@ -50,17 +60,27 @@ mc session create -e VAR1=value1 -e VAR2=value2 mc session create -v /local/path:/container/path mc session create -v ~/data:/data -v ./configs:/etc/app/config +# Mount a local directory (current directory or specific path) +mc session create . +mc session create /path/to/project + # Connect to external Docker networks mc session create --network teamnet --network dbnet # Connect to MCP servers for extended capabilities mc session create --mcp github --mcp jira -# Shorthand for creating a session with a project repository -mc github.com/username/repo +# Clone a Git repository +mc session create https://github.com/username/repo + +# Using the mcx shortcut (equivalent to mc session create) +mcx # Creates a session without mounting anything +mcx . # Mounts the current directory +mcx /path/to/project # Mounts the specified directory +mcx https://github.com/username/repo # Clones the repository # Shorthand with MCP servers -mc github.com/username/repo --mcp github +mcx https://github.com/username/repo --mcp github ``` ## Driver Management diff --git a/mcontainer/cli.py b/mcontainer/cli.py index f54affe..4141485 100644 --- a/mcontainer/cli.py +++ b/mcontainer/cli.py @@ -3,6 +3,7 @@ CLI for Monadical Container Tool. """ import os +import logging from typing import List, Optional import typer from rich.console import Console @@ -15,11 +16,18 @@ from .user_config import UserConfigManager from .session import SessionManager from .mcp import MCPManager -app = typer.Typer(help="Monadical Container Tool") -session_app = typer.Typer(help="Manage MC sessions") +# Configure logging - will only show logs if --verbose flag is used +logging.basicConfig( + level=logging.WARNING, # Default to WARNING level + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", + handlers=[logging.StreamHandler()], +) + +app = typer.Typer(help="Monadical Container Tool", no_args_is_help=True) +session_app = typer.Typer(help="Manage MC sessions", no_args_is_help=True) driver_app = typer.Typer(help="Manage MC drivers", no_args_is_help=True) -config_app = typer.Typer(help="Manage MC configuration") -mcp_app = typer.Typer(help="Manage MCP servers") +config_app = typer.Typer(help="Manage MC configuration", no_args_is_help=True) +mcp_app = typer.Typer(help="Manage MCP servers", no_args_is_help=True) app.add_typer(session_app, name="session", no_args_is_help=True) app.add_typer(driver_app, name="driver", no_args_is_help=True) app.add_typer(config_app, name="config", no_args_is_help=True) @@ -33,21 +41,21 @@ container_manager = ContainerManager(config_manager, session_manager, user_confi mcp_manager = MCPManager(config_manager=user_config) -@app.callback(invoke_without_command=True) -def main(ctx: typer.Context) -> None: - """Monadical Container Tool""" - # If no command is specified, create a session - if ctx.invoked_subcommand is None: - create_session( - driver=None, - project=None, - env=[], - volume=[], - network=[], - name=None, - no_connect=False, - no_mount=False, - ) +@app.callback() +def main( + ctx: typer.Context, + verbose: bool = typer.Option( + False, "--verbose", "-v", help="Enable verbose logging" + ), +) -> None: + """Monadical Container Tool + + Run 'mc session create' to create a new session. + Use 'mcx' as a shortcut for 'mc session create'. + """ + # Set log level based on verbose flag + if verbose: + logging.getLogger().setLevel(logging.INFO) @app.command() @@ -120,8 +128,10 @@ def list_sessions() -> None: @session_app.command("create") def create_session( driver: Optional[str] = typer.Option(None, "--driver", "-d", help="Driver to use"), - project: Optional[str] = typer.Option( - None, "--project", "-p", help="Project repository URL" + project: Optional[str] = typer.Argument( + None, + help="Local directory path to mount or repository URL to clone", + show_default=False, ), env: List[str] = typer.Option( [], "--env", "-e", help="Environment variables (KEY=VALUE)" @@ -136,11 +146,6 @@ def create_session( no_connect: bool = typer.Option( False, "--no-connect", help="Don't automatically connect to the session" ), - no_mount: bool = typer.Option( - False, - "--no-mount", - help="Don't mount local directory to /app (ignored if --project is used)", - ), mcp: List[str] = typer.Option( [], "--mcp", @@ -148,7 +153,12 @@ def create_session( help="Attach MCP servers to the session (can be specified multiple times)", ), ) -> None: - """Create a new MC session""" + """Create a new MC session + + If a local directory path is provided, it will be mounted at /app in the container. + If a repository URL is provided, it will be cloned into /app during initialization. + If no path or URL is provided, no local volume will be mounted. + """ # Use default driver from user configuration if not driver: driver = user_config.get( @@ -209,7 +219,7 @@ def create_session( 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)}") @@ -223,12 +233,18 @@ def create_session( console.print(f" {host_path} -> {mount_info['bind']}") with console.status(f"Creating session with driver '{driver}'..."): + # If project is a local directory, we should mount it + # If it's a Git URL or doesn't exist, handle accordingly + mount_local = False + if project and os.path.isdir(os.path.expanduser(project)): + mount_local = True + session = container_manager.create_session( driver_name=driver, project=project, environment=environment, session_name=name, - mount_local=not no_mount and user_config.get("defaults.mount_local", True), + mount_local=mount_local, volumes=volume_mounts, networks=all_networks, mcp=all_mcps, @@ -362,61 +378,6 @@ def stop() -> None: os.system("kill 1") # Send SIGTERM to PID 1 (container's init process) -# Main CLI entry point that handles project repository URLs -@app.command(name="") -def quick_create( - project: Optional[str] = typer.Argument(..., help="Project repository URL"), - driver: Optional[str] = typer.Option(None, "--driver", "-d", help="Driver to use"), - env: List[str] = typer.Option( - [], "--env", "-e", help="Environment variables (KEY=VALUE)" - ), - volume: List[str] = typer.Option( - [], "--volume", "-v", help="Mount volumes (LOCAL_PATH:CONTAINER_PATH)" - ), - network: List[str] = typer.Option( - [], "--network", "-N", help="Connect to additional Docker networks" - ), - name: Optional[str] = typer.Option(None, "--name", "-n", help="Session name"), - no_connect: bool = typer.Option( - False, "--no-connect", help="Don't automatically connect to the session" - ), - no_mount: bool = typer.Option( - False, - "--no-mount", - help="Don't mount local directory to /app (ignored if a project is specified)", - ), - mcp: List[str] = typer.Option( - [], - "--mcp", - "-m", - help="Attach MCP servers to the session (can be specified multiple times)", - ), -) -> None: - """Create a new MC session with a project repository""" - # 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, - project=project, - env=env, - volume=volume, - network=network, - name=name, - no_connect=no_connect, - no_mount=no_mount, - mcp=all_mcps, - ) - - @driver_app.command("list") def list_drivers() -> None: """List available MC drivers""" @@ -537,6 +498,7 @@ config_app.add_typer(volume_app, name="volume", no_args_is_help=True) 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: @@ -555,6 +517,7 @@ def list_default_mcps() -> None: console.print(table) + @config_mcp_app.command("add") def add_default_mcp( name: str = typer.Argument(..., help="MCP server name to add to defaults"), @@ -576,6 +539,7 @@ def add_default_mcp( 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"), @@ -1017,8 +981,15 @@ def mcp_status(name: str = typer.Argument(..., help="MCP server name")) -> None: def start_mcp( name: Optional[str] = typer.Argument(None, help="MCP server name"), all_servers: bool = typer.Option(False, "--all", help="Start all MCP servers"), + verbose: bool = typer.Option( + False, "--verbose", "-v", help="Enable verbose logging" + ), ) -> None: """Start an MCP server or all servers""" + # Set log level based on verbose flag + if verbose: + logging.getLogger().setLevel(logging.INFO) + # Check if we need to start all servers if all_servers: # Get all configured MCP servers @@ -1281,6 +1252,26 @@ def mcp_logs( def remove_mcp(name: str = typer.Argument(..., help="MCP server name")) -> None: """Remove an MCP server configuration""" try: + # Check if any active sessions might be using this MCP + active_sessions = container_manager.list_sessions() + affected_sessions = [] + + for session in active_sessions: + if session.mcps and name in session.mcps: + affected_sessions.append(session) + + # Just warn users about affected sessions + if affected_sessions: + console.print( + f"[yellow]Warning: Found {len(affected_sessions)} active sessions using MCP '{name}'[/yellow]" + ) + console.print( + "[yellow]You may need to restart these sessions for changes to take effect:[/yellow]" + ) + for session in affected_sessions: + console.print(f" - Session: {session.id} ({session.name})") + + # Remove the MCP from configuration with console.status(f"Removing MCP server '{name}'..."): result = mcp_manager.remove_mcp(name) @@ -1365,7 +1356,7 @@ 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: @@ -1400,10 +1391,12 @@ def add_remote_mcp( try: with console.status(f"Adding remote MCP server '{name}'..."): - mcp_manager.add_remote_mcp(name, url, headers, add_as_default=not no_default) + 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: @@ -1861,5 +1854,27 @@ exec npm start console.print("[green]MCP Inspector stopped[/green]") +def session_create_entry_point(): + """Entry point that directly invokes 'mc session create'. + + This provides a convenient shortcut: + - 'mcx' runs as if you typed 'mc session create' + - 'mcx .' mounts the current directory + - 'mcx /path/to/project' mounts the specified directory + - 'mcx repo-url' clones the repository + + All command-line options are passed through to 'session create'. + """ + import sys + + # Save the program name (e.g., 'mcx') + prog_name = sys.argv[0] + # Insert 'session' and 'create' commands before any other arguments + sys.argv.insert(1, "session") + sys.argv.insert(2, "create") + # Run the app with the modified arguments + app(prog_name=prog_name) + + if __name__ == "__main__": app() diff --git a/mcontainer/container.py b/mcontainer/container.py index 79e5837..a1acf58 100644 --- a/mcontainer/container.py +++ b/mcontainer/container.py @@ -141,7 +141,7 @@ class ContainerManager: project: Optional[str] = None, environment: Optional[Dict[str, str]] = None, session_name: Optional[str] = None, - mount_local: bool = True, + mount_local: bool = False, volumes: Optional[Dict[str, Dict[str, str]]] = None, networks: Optional[List[str]] = None, mcp: Optional[List[str]] = None, @@ -150,10 +150,10 @@ class ContainerManager: Args: driver_name: The name of the driver to use - project: Optional project repository URL + project: Optional project repository URL or local directory path environment: Optional environment variables session_name: Optional session name - mount_local: Whether to mount the current directory to /app + mount_local: Whether to mount the specified local directory to /app (ignored if project is None) volumes: Optional additional volumes to mount (dict of {host_path: {"bind": container_path, "mode": mode}}) networks: Optional list of additional Docker networks to connect to mcp: Optional list of MCP server names to attach to the session @@ -203,16 +203,29 @@ class ContainerManager: # Set up volume mounts session_volumes = {} - # If project URL is provided, don't mount local directory (will clone into /app) - # If no project URL and mount_local is True, mount local directory to /app - if not project and mount_local: - # Mount current directory to /app in the container - current_dir = os.getcwd() - session_volumes[current_dir] = {"bind": "/app", "mode": "rw"} - print(f"Mounting local directory {current_dir} to /app") - elif project: + # Determine if project is a local directory or a Git repository + is_local_directory = False + is_git_repo = False + + if project: + # Check if project is a local directory + if os.path.isdir(os.path.expanduser(project)): + is_local_directory = True + else: + # If not a local directory, assume it's a Git repo URL + is_git_repo = True + + # Handle mounting based on project type + if is_local_directory and mount_local: + # Mount the specified local directory to /app in the container + local_dir = os.path.abspath(os.path.expanduser(project)) + session_volumes[local_dir] = {"bind": "/app", "mode": "rw"} + print(f"Mounting local directory {local_dir} to /app") + # Clear project for container environment since we're mounting + project = None + elif is_git_repo: print( - f"Project URL provided - container will clone {project} into /app during initialization" + f"Git repository URL provided - container will clone {project} into /app during initialization" ) # Add user-specified volumes @@ -220,13 +233,13 @@ class ContainerManager: for host_path, mount_spec in volumes.items(): container_path = mount_spec["bind"] # Check for conflicts with /app mount - if container_path == "/app" and not project and mount_local: + if container_path == "/app" and is_local_directory and mount_local: print( - "[yellow]Warning: Volume mount to /app conflicts with automatic local directory mount. User-specified mount takes precedence.[/yellow]" + "[yellow]Warning: Volume mount to /app conflicts with local directory mount. User-specified mount takes precedence.[/yellow]" ) - # Remove the automatic mount if there's a conflict - if current_dir in session_volumes: - del session_volumes[current_dir] + # Remove the local directory mount if there's a conflict + if local_dir in session_volumes: + del session_volumes[local_dir] # Add the volume session_volumes[host_path] = mount_spec @@ -309,9 +322,11 @@ class ContainerManager: try: print(f"Ensuring MCP server '{mcp_name}' is running...") self.mcp_manager.start_mcp(mcp_name) - + # Store container name for later network connection - container_name = self.mcp_manager.get_mcp_container_name(mcp_name) + container_name = self.mcp_manager.get_mcp_container_name( + mcp_name + ) mcp_container_names.append(container_name) # Get MCP status to extract endpoint information @@ -438,25 +453,29 @@ class ContainerManager: ) except DockerException as e: print(f"Error connecting to network {network_name}: {e}") - + # Reload the container to get updated network information container.reload() - + # Connect directly to each MCP's dedicated network for mcp_name in mcp_names: try: # Get the dedicated network for this MCP dedicated_network_name = f"mc-mcp-{mcp_name}-network" - + try: network = self.client.networks.get(dedicated_network_name) - + # Connect the session container to the MCP's dedicated network network.connect(container, aliases=[session_name]) - print(f"Connected session to MCP '{mcp_name}' via dedicated network: {dedicated_network_name}") + print( + f"Connected session to MCP '{mcp_name}' via dedicated network: {dedicated_network_name}" + ) except DockerException as e: - print(f"Error connecting to MCP dedicated network '{dedicated_network_name}': {e}") - + print( + f"Error connecting to MCP dedicated network '{dedicated_network_name}': {e}" + ) + except Exception as e: print(f"Error connecting session to MCP '{mcp_name}': {e}") @@ -464,7 +483,13 @@ class ContainerManager: if networks: for network_name in networks: # Check if already connected to this network - if network_name not in [net.name for net in container.attrs.get("NetworkSettings", {}).get("Networks", {}).values()]: + # NetworkSettings.Networks contains a dict where keys are network names + existing_networks = ( + container.attrs.get("NetworkSettings", {}) + .get("Networks", {}) + .keys() + ) + if network_name not in existing_networks: try: # Get or create the network try: diff --git a/mcontainer/drivers/goose/Dockerfile b/mcontainer/drivers/goose/Dockerfile index dc5f774..5ef3301 100644 --- a/mcontainer/drivers/goose/Dockerfile +++ b/mcontainer/drivers/goose/Dockerfile @@ -42,7 +42,8 @@ COPY update-goose-config.sh /usr/local/bin/update-goose-config.sh # Extend env via bashrc # Make scripts executable -RUN chmod +x /mc-init.sh /entrypoint.sh /init-status.sh /usr/local/bin/update-goose-config.sh +RUN chmod +x /mc-init.sh /entrypoint.sh /init-status.sh \ + /usr/local/bin/update-goose-config.sh # Set up initialization status check on login RUN echo 'export PATH=/root/.local/bin:$PATH' >> /etc/bash.bashrc diff --git a/mcontainer/mcp.py b/mcontainer/mcp.py index e9df621..5495ccd 100644 --- a/mcontainer/mcp.py +++ b/mcontainer/mcp.py @@ -43,13 +43,13 @@ class MCPManager: if not networks: self.client.networks.create(network_name, driver="bridge") return network_name - + def _get_mcp_dedicated_network(self, mcp_name: str) -> str: """Get or create a dedicated network for direct session-to-MCP connections. - + Args: mcp_name: The name of the MCP server - + Returns: The name of the dedicated network """ @@ -74,16 +74,20 @@ class MCPManager: return None def add_remote_mcp( - self, name: str, url: str, headers: Dict[str, str] = None, add_as_default: bool = True + self, + name: str, + url: str, + headers: Dict[str, str] = None, + add_as_default: bool = True, ) -> Dict[str, Any]: """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 """ @@ -106,7 +110,7 @@ class MCPManager: # 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", []) @@ -117,17 +121,22 @@ class MCPManager: return mcp_config def add_docker_mcp( - self, name: str, image: str, command: str, env: Dict[str, str] = None, add_as_default: bool = True + 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. - + 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 """ @@ -151,7 +160,7 @@ class MCPManager: # 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", []) @@ -173,7 +182,7 @@ class MCPManager: add_as_default: bool = True, ) -> Dict[str, Any]: """Add a proxy-based MCP server. - + Args: name: Name of the MCP server base_image: Base Docker image running the actual MCP server @@ -183,7 +192,7 @@ class MCPManager: 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 """ @@ -228,7 +237,7 @@ class MCPManager: # 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", []) @@ -240,10 +249,10 @@ class MCPManager: def remove_mcp(self, name: str) -> bool: """Remove an MCP server configuration. - + Args: name: Name of the MCP server to remove - + Returns: True if the MCP was successfully removed, False otherwise """ @@ -258,7 +267,7 @@ 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: @@ -370,20 +379,22 @@ class MCPManager: }, ) - # Connect to the inspector network + # Connect to the inspector network network = self.client.networks.get(network_name) network.connect(container, aliases=[name]) logger.info( f"Connected MCP server '{name}' to inspector network {network_name} with alias '{name}'" ) - + # Create and connect to a dedicated network for session connections dedicated_network_name = self._get_mcp_dedicated_network(name) try: dedicated_network = self.client.networks.get(dedicated_network_name) except DockerException: - dedicated_network = self.client.networks.create(dedicated_network_name, driver="bridge") - + dedicated_network = self.client.networks.create( + dedicated_network_name, driver="bridge" + ) + dedicated_network.connect(container, aliases=[name]) logger.info( f"Connected MCP server '{name}' to dedicated network {dedicated_network_name} with alias '{name}'" @@ -400,6 +411,7 @@ class MCPManager: with tempfile.TemporaryDirectory() as tmp_dir: # Create entrypoint script for mcp-proxy that runs the base MCP image entrypoint_script = """#!/bin/sh +set -x echo "Starting MCP proxy with base image $MCP_BASE_IMAGE (command: $MCP_COMMAND) on port $SSE_PORT" # Verify if Docker socket is available @@ -453,19 +465,44 @@ echo "Running MCP server from image $MCP_BASE_IMAGE with command: $CMD" # Run the actual MCP server in the base image and pipe its I/O to mcp-proxy # Using docker run without -d to keep stdio connected + +# Build env vars string to pass through to the inner container +ENV_ARGS="" + +# Check if the environment variable names file exists +if [ -f "/mcp-envs.txt" ]; then + # Read env var names from file and pass them to docker + while read -r var_name; do + # Skip empty lines + if [ -n "$var_name" ]; then + # Simply add the env var - Docker will only pass it if it exists + ENV_ARGS="$ENV_ARGS -e $var_name" + fi + done < "/mcp-envs.txt" + + echo "Passing environment variables from mcp-envs.txt: $ENV_ARGS" +fi + exec mcp-proxy \ --sse-port "$SSE_PORT" \ --sse-host "$SSE_HOST" \ --allow-origin "$ALLOW_ORIGIN" \ --pass-environment \ -- \ - docker run --rm -i "$MCP_BASE_IMAGE" $CMD + docker run --rm -i $ENV_ARGS "$MCP_BASE_IMAGE" $CMD """ # Write the entrypoint script entrypoint_path = os.path.join(tmp_dir, "entrypoint.sh") with open(entrypoint_path, "w") as f: f.write(entrypoint_script) + # Create a file with environment variable names (no values) + env_names_path = os.path.join(tmp_dir, "mcp-envs.txt") + with open(env_names_path, "w") as f: + # Write one env var name per line + for env_name in mcp_config.get("env", {}).keys(): + f.write(f"{env_name}\n") + # Create a Dockerfile for the proxy dockerfile_content = f""" FROM {mcp_config["proxy_image"]} @@ -489,7 +526,8 @@ ENV DEBUG=1 # Add environment variables from the configuration {chr(10).join([f'ENV {k}="{v}"' for k, v in mcp_config.get("env", {}).items()])} -# Add entrypoint script +# Add env names file and entrypoint script +COPY mcp-envs.txt /mcp-envs.txt COPY entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh ENTRYPOINT ["/entrypoint.sh"] @@ -545,20 +583,22 @@ ENTRYPOINT ["/entrypoint.sh"] ports=port_bindings, # Bind the SSE port to the host if configured ) - # Connect to the inspector network + # Connect to the inspector network network = self.client.networks.get(network_name) network.connect(container, aliases=[name]) logger.info( f"Connected MCP server '{name}' to inspector network {network_name} with alias '{name}'" ) - + # Create and connect to a dedicated network for session connections dedicated_network_name = self._get_mcp_dedicated_network(name) try: dedicated_network = self.client.networks.get(dedicated_network_name) except DockerException: - dedicated_network = self.client.networks.create(dedicated_network_name, driver="bridge") - + dedicated_network = self.client.networks.create( + dedicated_network_name, driver="bridge" + ) + dedicated_network.connect(container, aliases=[name]) logger.info( f"Connected MCP server '{name}' to dedicated network {dedicated_network_name} with alias '{name}'" @@ -574,14 +614,25 @@ ENTRYPOINT ["/entrypoint.sh"] raise ValueError(f"Unsupported MCP type: {mcp_type}") def stop_mcp(self, name: str) -> bool: - """Stop an MCP server container.""" - if not self.client: - raise Exception("Docker client is not available") + """Stop an MCP server container. - # Get the MCP configuration + Args: + name: The name of the MCP server to stop + + Returns: + True if the operation was successful (including cases where the container doesn't exist) + """ + if not self.client: + logger.warning("Docker client is not available") + return False + + # Get the MCP configuration - don't raise an exception if not found mcp_config = self.get_mcp(name) if not mcp_config: - raise ValueError(f"MCP server '{name}' not found") + logger.warning( + f"MCP server '{name}' not found, but continuing with removal" + ) + return True # Remote MCPs don't have containers to stop if mcp_config.get("type") == "remote": @@ -605,12 +656,13 @@ ENTRYPOINT ["/entrypoint.sh"] return True except NotFound: - # Container doesn't exist + # Container doesn't exist - this is fine when removing logger.info(f"MCP container '{name}' not found, nothing to stop or remove") - return False + return True except Exception as e: + # Log the error but don't fail the removal operation logger.error(f"Error stopping/removing MCP container: {e}") - return False + return True # Return true anyway to continue with removal def restart_mcp(self, name: str) -> Dict[str, Any]: """Restart an MCP server container.""" diff --git a/pyproject.toml b/pyproject.toml index 40185ab..567a273 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,7 @@ dev = [ [project.scripts] mc = "mcontainer.cli:app" +mcx = "mcontainer.cli:session_create_entry_point" [tool.ruff] line-length = 88