docker_mcp.py•23.9 kB
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import subprocess
from fastmcp import FastMCP
from termcolor import colored
import sys
import os
import shlex  # Add shlex for proper command splitting
import json
import logging
# Set up logging to stderr for MCP compatibility
logging.basicConfig(
    level=logging.INFO,
    format="[DockerMCP] %(levelname)s: %(message)s",
    stream=sys.stderr
)
# CONSTANTS
SERVER_NAME = "DockerManager"
def print_colored(message, color="green", prefix="[DockerMCP]"):
    """Print colored messages to stderr for better compatibility with MCP protocol."""
    # Use stderr only to avoid interfering with stdout JSON communication
    print(colored(f"{prefix} {message}", color), file=sys.stderr)
# Create an MCP server instance
mcp = FastMCP(SERVER_NAME, disable_stdout_logging=True)
# When the server starts, check if Docker is available
try:
    docker_check = subprocess.run(
        ["docker", "--version"], 
        capture_output=True, 
        text=True
    )
    print_colored(f"Docker version: {docker_check.stdout.strip()}", "cyan")
except subprocess.SubprocessError:
    print_colored("Docker is not available. Please make sure Docker is installed and running.", "red")
    logging.error("Docker is not available. Please make sure Docker is installed and running.")
    sys.exit(1)
except Exception as e:
    print_colored(f"Error checking Docker: {str(e)}", "red")
    logging.error(f"Error checking Docker: {str(e)}")
    sys.exit(1)
@mcp.tool()
def create_container(image: str, container_name: str, dependencies: str = "") -> str:
    """
    Create and start a Docker container with optional dependencies.
    
    Parameters:
    • image: The Docker image to use (e.g., "ubuntu:latest", "node:16", "python:3.9-slim").
    • container_name: A unique name for the container.
    • dependencies: Space-separated list of packages to install (e.g., "numpy pandas matplotlib" or "express lodash").
    
    This tool uses 'docker run' in detached mode with a command that keeps the container running.
    If dependencies are specified, they will be installed after the container starts.
    Automatically detects appropriate package manager (pip, npm, apt, apk) based on the image.
    """
    print_colored(f"Creating container with name '{container_name}' from image '{image}'...")
    
    try:
        # Start the container in detached mode with an infinite sleep to keep it running.
        result = subprocess.run(
            ["docker", "run", "-d", "--name", container_name, image, "sleep", "infinity"],
            capture_output=True, text=True, check=True, timeout=30
        )
        container_id = result.stdout.strip()
        print_colored(f"Container created successfully. ID: {container_id}")
        
        # Install dependencies if specified
        if dependencies:
            print_colored(f"Installing dependencies: {dependencies}", "cyan")
            
            # Determine package manager based on image
            if "node" in image.lower() or "javascript" in image.lower():
                # For Node.js images, use npm
                print_colored("Detected Node.js image, using npm", "cyan")
                # First check if npm is available
                try:
                    npm_check = subprocess.run(
                        ["docker", "exec", container_name, "npm", "--version"],
                        capture_output=True, text=True, check=True, timeout=10
                    )
                    print_colored(f"npm version: {npm_check.stdout.strip()}", "cyan")
                    
                    # For npm, we'll install packages globally to avoid needing a package.json
                    install_cmd = f"npm install -g {dependencies}"
                except subprocess.CalledProcessError:
                    error_msg = "npm not found in container, defaulting to other package managers"
                    print_colored(error_msg, "yellow")
                    # Fall through to other package manager detection
            elif "python" in image.lower():
                # For Python images, use pip
                print_colored("Detected Python image, using pip", "cyan")
                install_cmd = f"pip install {dependencies}"
            elif any(distro in image.lower() for distro in ["ubuntu", "debian"]):
                # For Debian/Ubuntu images
                print_colored("Detected Debian/Ubuntu image, using apt-get", "cyan")
                install_cmd = f"apt-get update && apt-get install -y {dependencies}"
            elif "alpine" in image.lower():
                # For Alpine images
                print_colored("Detected Alpine image, using apk", "cyan")
                install_cmd = f"apk add --no-cache {dependencies}"
            else:
                # Default to pip for unknown images with a warning
                print_colored("Image type not recognized, attempting to detect available package managers", "yellow")
                
                # Try to detect available package managers
                package_managers = []
                
                # Check for npm
                try:
                    npm_check = subprocess.run(
                        ["docker", "exec", container_name, "npm", "--version"],
                        capture_output=True, text=True, check=False, timeout=5
                    )
                    if npm_check.returncode == 0:
                        package_managers.append("npm")
                except Exception:
                    pass
                
                # Check for pip
                try:
                    pip_check = subprocess.run(
                        ["docker", "exec", container_name, "pip", "--version"],
                        capture_output=True, text=True, check=False, timeout=5
                    )
                    if pip_check.returncode == 0:
                        package_managers.append("pip")
                except Exception:
                    pass
                
                # Choose package manager based on detection
                if "npm" in package_managers:
                    print_colored("npm found, using it for installation", "cyan")
                    install_cmd = f"npm install -g {dependencies}"
                elif "pip" in package_managers:
                    print_colored("pip found, using it for installation", "cyan")
                    install_cmd = f"pip install {dependencies}"
                else:
                    print_colored("No known package managers detected, defaulting to pip install", "yellow")
                    install_cmd = f"pip install {dependencies}"
            
            try:
                # Execute the installation command
                print_colored(f"Running: {install_cmd}", "cyan")
                install_result = subprocess.run(
                    ["docker", "exec", container_name, "sh", "-c", install_cmd],
                    capture_output=True, text=True, check=True, timeout=180  # Allow more time for installations
                )
                print_colored(f"Dependencies installed successfully", "green")
                return f"Container created with ID: {container_id}\nDependencies installed: {dependencies}"
            except subprocess.CalledProcessError as e:
                error_msg = f"Error installing dependencies: {e.stderr}"
                print_colored(error_msg, "red")
                return f"Container created with ID: {container_id}\n{error_msg}"
            except subprocess.TimeoutExpired:
                error_msg = "Timeout while installing dependencies. Operation took too long."
                print_colored(error_msg, "red")
                return f"Container created with ID: {container_id}\n{error_msg}"
            except Exception as e:
                error_msg = f"Unexpected error installing dependencies: {str(e)}"
                print_colored(error_msg, "red")
                return f"Container created with ID: {container_id}\n{error_msg}"
        
        return f"Container created with ID: {container_id}"
    except subprocess.CalledProcessError as e:
        error_msg = f"Error creating container: {e.stderr}"
        print_colored(error_msg, "red")
        return error_msg
    except subprocess.TimeoutExpired:
        error_msg = "Timeout while creating container. Operation took too long."
        print_colored(error_msg, "red")
        return error_msg
    except Exception as e:
        error_msg = f"Unexpected error: {str(e)}"
        print_colored(error_msg, "red")
        return error_msg
@mcp.tool()
def execute_code(container_name: str, command: str) -> str:
    """
    Execute a command inside a running Docker container.
    
    Parameters:
    • container_name: The name of the target container.
    • command: The command to execute inside the container.
    
    This tool uses 'docker exec' to run the command and returns the output.
    """
    print_colored(f"Executing command in container '{container_name}': {command}")
    
    try:
        # Check if this is a python -c command
        if command.startswith("python -c"):
            # For python -c commands, keep the entire string after -c as a single argument
            prefix, python_code = command.split("-c", 1)
            cmd_parts = ["python", "-c", python_code.strip()]
            print_colored(f"Detected Python code execution: {cmd_parts}", "cyan")
        else:
            # For other commands, use proper shell-like splitting
            cmd_parts = shlex.split(command)
            
        # Execute the command
        result = subprocess.run(
            ["docker", "exec", container_name] + cmd_parts,
            capture_output=True, text=True, check=True, timeout=30
        )
        print_colored(f"Command executed successfully")
        return f"Command output: {result.stdout}"
    except subprocess.CalledProcessError as e:
        error_msg = f"Error executing command: {e.stderr}"
        print_colored(error_msg, "red")
        return error_msg
    except subprocess.TimeoutExpired:
        error_msg = "Timeout while executing command. Operation took too long."
        print_colored(error_msg, "red")
        return error_msg
    except Exception as e:
        error_msg = f"Unexpected error: {str(e)}"
        print_colored(error_msg, "red")
        return error_msg
@mcp.tool()
def execute_python_script(container_name: str, script_content: str, script_args: str = "") -> str:
    """
    Execute a Python script inside a running Docker container.
    
    Parameters:
    • container_name: The name of the target container.
    • script_content: The full Python script content to execute.
    • script_args: Optional arguments to pass to the script (default: "").
    
    This tool writes the script to a file in the container and executes it.
    """
    print_colored(f"Executing Python script in container '{container_name}'")
    script_path = "/tmp/script.py"
    
    try:
        # Write the script content to a file in the container
        print_colored("Writing Python script to container...", "cyan")
        # Escape single quotes in the script content
        escaped_content = script_content.replace("'", "'\\''")
        
        write_result = subprocess.run(
            ["docker", "exec", container_name, "bash", "-c", f"echo '{escaped_content}' > {script_path} && chmod +x {script_path}"],
            capture_output=True, text=True, check=True, timeout=30
        )
        
        # Execute the script
        print_colored(f"Running Python script: {script_path} {script_args}", "cyan")
        cmd_parts = ["python", script_path]
        if script_args:
            cmd_parts.extend(shlex.split(script_args))
            
        result = subprocess.run(
            ["docker", "exec", container_name] + cmd_parts,
            capture_output=True, text=True, check=True, timeout=60  # Allow more time for script execution
        )
        
        print_colored("Python script executed successfully")
        return f"Command output: {result.stdout}"
    except subprocess.CalledProcessError as e:
        error_msg = f"Error executing Python script: {e.stderr}"
        print_colored(error_msg, "red")
        return error_msg
    except subprocess.TimeoutExpired:
        error_msg = "Timeout while executing Python script. Operation took too long."
        print_colored(error_msg, "red")
        return error_msg
    except Exception as e:
        error_msg = f"Unexpected error: {str(e)}"
        print_colored(error_msg, "red")
        return error_msg
@mcp.tool()
def cleanup_container(container_name: str) -> str:
    """
    Stop and remove a Docker container.
    
    Parameters:
    • container_name: The name of the container to stop and remove.
    
    This tool uses 'docker stop' followed by 'docker rm' to clean up the container.
    """
    print_colored(f"Cleaning up container '{container_name}'...")
    
    try:
        # Stop the container with a shorter timeout (5 seconds instead of default 10)
        print_colored(f"Stopping container '{container_name}'...", "yellow")
        stop_result = subprocess.run(
            ["docker", "stop", "--time", "5", container_name],
            capture_output=True, text=True, check=True, timeout=10
        )
        
        # Remove the container
        print_colored(f"Removing container '{container_name}'...", "yellow")
        rm_result = subprocess.run(
            ["docker", "rm", container_name],
            capture_output=True, text=True, check=True, timeout=10
        )
        
        print_colored(f"Container '{container_name}' has been successfully stopped and removed.")
        return f"Container '{container_name}' has been stopped and removed."
    except subprocess.CalledProcessError as e:
        error_msg = f"Error cleaning the container: {e.stderr}"
        print_colored(error_msg, "red")
        
        # If stop fails, try to force kill the container
        try:
            print_colored(f"Attempting to force kill container '{container_name}'...", "yellow")
            kill_result = subprocess.run(
                ["docker", "kill", container_name],
                capture_output=True, text=True, check=False, timeout=5
            )
            rm_result = subprocess.run(
                ["docker", "rm", "-f", container_name],
                capture_output=True, text=True, check=False, timeout=5
            )
            if rm_result.returncode == 0:
                print_colored(f"Container '{container_name}' has been forcibly removed.")
                return f"Container '{container_name}' has been forcibly removed after timeout."
        except Exception:
            pass
        
        return error_msg
    except subprocess.TimeoutExpired:
        error_msg = "Timeout while cleaning up container. Attempting to force remove..."
        print_colored(error_msg, "yellow")
        
        # If timeout occurs, try to force remove the container
        try:
            kill_result = subprocess.run(
                ["docker", "kill", container_name],
                capture_output=True, text=True, check=False, timeout=5
            )
            rm_result = subprocess.run(
                ["docker", "rm", "-f", container_name],
                capture_output=True, text=True, check=False, timeout=5
            )
            if rm_result.returncode == 0:
                print_colored(f"Container '{container_name}' has been forcibly removed after timeout.")
                return f"Container '{container_name}' has been forcibly removed after timeout."
            else:
                return "Failed to clean up container after timeout."
        except Exception as e:
            return f"Failed to force remove container: {str(e)}"
    except Exception as e:
        error_msg = f"Unexpected error: {str(e)}"
        print_colored(error_msg, "red")
        return error_msg
@mcp.tool()
def add_dependencies(container_name: str, dependencies: str) -> str:
    """
    Install additional dependencies in an existing Docker container.
    
    Parameters:
    • container_name: The name of the target container.
    • dependencies: Space-separated list of packages to install (e.g., "numpy pandas matplotlib" or "express lodash").
    
    This tool automatically detects the appropriate package manager (pip, npm, apt, apk)
    available in the container and uses it to install the specified dependencies.
    """
    print_colored(f"Adding dependencies to container '{container_name}': {dependencies}")
    
    try:
        # Check if container exists and is running
        container_check = subprocess.run(
            ["docker", "container", "inspect", "-f", "{{.State.Running}}", container_name],
            capture_output=True, text=True, check=True, timeout=10
        )
        
        if container_check.stdout.strip() != "true":
            error_msg = f"Container '{container_name}' is not running or does not exist"
            print_colored(error_msg, "red")
            return error_msg
        
        # Try to detect available package managers
        package_managers = []
        
        # Check for npm
        try:
            npm_check = subprocess.run(
                ["docker", "exec", container_name, "npm", "--version"],
                capture_output=True, text=True, check=False, timeout=5
            )
            if npm_check.returncode == 0:
                package_managers.append("npm")
                print_colored(f"Found npm: {npm_check.stdout.strip()}", "cyan")
        except Exception:
            pass
        
        # Check for pip
        try:
            pip_check = subprocess.run(
                ["docker", "exec", container_name, "pip", "--version"],
                capture_output=True, text=True, check=False, timeout=5
            )
            if pip_check.returncode == 0:
                package_managers.append("pip")
                print_colored(f"Found pip: {pip_check.stdout.strip()}", "cyan")
        except Exception:
            pass
        
        # Check for apt-get
        try:
            apt_check = subprocess.run(
                ["docker", "exec", container_name, "apt-get", "--version"],
                capture_output=True, text=True, check=False, timeout=5
            )
            if apt_check.returncode == 0:
                package_managers.append("apt-get")
                print_colored("Found apt-get", "cyan")
        except Exception:
            pass
        
        # Check for apk
        try:
            apk_check = subprocess.run(
                ["docker", "exec", container_name, "apk", "--version"],
                capture_output=True, text=True, check=False, timeout=5
            )
            if apk_check.returncode == 0:
                package_managers.append("apk")
                print_colored("Found apk", "cyan")
        except Exception:
            pass
        
        # Choose package manager based on detection
        if not package_managers:
            error_msg = "No supported package managers found in the container"
            print_colored(error_msg, "red")
            return error_msg
        
        print_colored(f"Available package managers: {', '.join(package_managers)}", "cyan")
        
        # Choose the appropriate package manager
        if "npm" in package_managers:
            print_colored("Using npm for installation", "cyan")
            install_cmd = f"npm install -g {dependencies}"
        elif "pip" in package_managers:
            print_colored("Using pip for installation", "cyan")
            install_cmd = f"pip install {dependencies}"
        elif "apt-get" in package_managers:
            print_colored("Using apt-get for installation", "cyan")
            install_cmd = f"apt-get update && apt-get install -y {dependencies}"
        elif "apk" in package_managers:
            print_colored("Using apk for installation", "cyan")
            install_cmd = f"apk add --no-cache {dependencies}"
        else:
            error_msg = "No supported package managers found in the container"
            print_colored(error_msg, "red")
            return error_msg
        
        # Execute the installation command
        print_colored(f"Running: {install_cmd}", "cyan")
        install_result = subprocess.run(
            ["docker", "exec", container_name, "sh", "-c", install_cmd],
            capture_output=True, text=True, check=True, timeout=180  # Allow more time for installations
        )
        
        print_colored("Dependencies installed successfully", "green")
        return f"Dependencies installed in container '{container_name}': {dependencies}"
    except subprocess.CalledProcessError as e:
        error_msg = f"Error installing dependencies: {e.stderr}"
        print_colored(error_msg, "red")
        return error_msg
    except subprocess.TimeoutExpired:
        error_msg = "Timeout while installing dependencies. Operation took too long."
        print_colored(error_msg, "red")
        return error_msg
    except Exception as e:
        error_msg = f"Unexpected error: {str(e)}"
        print_colored(error_msg, "red")
        return error_msg
@mcp.tool()
def list_containers(show_all: bool = True) -> str:
    """
    List all Docker containers with their details.
    
    Parameters:
    • show_all: Whether to show all containers including stopped ones (default: True).
                If False, only shows running containers.
    
    Returns information about containers including ID, name, status, and image.
    """
    print_colored(f"Listing {'all' if show_all else 'running'} containers...")
    
    try:
        cmd = ["docker", "ps"]
        if show_all:
            cmd.append("-a")  # Show all containers, not just running ones
            
        # Add format to get consistent, parseable output with specific fields
        cmd.extend([
            "--format", 
            "table {{.ID}}\t{{.Names}}\t{{.Status}}\t{{.Image}}\t{{.RunningFor}}"
        ])
        
        result = subprocess.run(
            cmd,
            capture_output=True, text=True, check=True, timeout=15
        )
        
        container_list = result.stdout.strip()
        
        if container_list and not container_list.startswith("CONTAINER ID"):
            # This means we have a table header but no data
            print_colored("No containers found", "yellow")
            return f"No {'existing' if show_all else 'running'} containers found."
        
        print_colored(f"Found containers:\n{container_list}", "cyan")
        
        # Add extra information about how to use this data
        usage_info = "\nYou can use these container names with other tools like add_dependencies, execute_code, etc."
        if show_all:
            usage_info += "\nNote: Only containers with 'Up' in their Status are currently running and can be interacted with."
        
        return f"{container_list}{usage_info}"
    except subprocess.CalledProcessError as e:
        error_msg = f"Error listing containers: {e.stderr}"
        print_colored(error_msg, "red")
        return error_msg
    except subprocess.TimeoutExpired:
        error_msg = "Timeout while listing containers. Operation took too long."
        print_colored(error_msg, "red")
        return error_msg
    except Exception as e:
        error_msg = f"Unexpected error: {str(e)}"
        print_colored(error_msg, "red")
        return error_msg 
# Main entry point for MCP server
if __name__ == "__main__":
    print_colored("Starting MCP Docker Manager server...", "green")
    try:
        # This starts the MCP server
        mcp.run()
    except Exception as e:
        error_message = f"Error starting MCP server: {str(e)}"
        logging.error(error_message)
        print(error_message, file=sys.stderr)
        sys.exit(1)