Repository: QuantGeekDev/docker-mcp Branch: main Commit: 6dada7feb355 Files: 9 Total size: 23.8 KB Directory structure: gitextract_dbxbtxq3/ ├── .gitignore ├── .python-version ├── LICENSE ├── README.md ├── pyproject.toml └── src/ └── docker_mcp/ ├── __init__.py ├── docker_executor.py ├── handlers.py └── server.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # Python-generated files __pycache__/ *.py[oc] build/ dist/ wheels/ *.egg-info # Virtual environments .venv conf work README_PRIVATE.md docker_compose_files ================================================ FILE: .python-version ================================================ 3.12 ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) [year] [fullname] Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # 🐳 docker-mcp [![Python 3.12](https://img.shields.io/badge/python-3.12-blue.svg)](https://www.python.org/downloads/release/python-3120/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) [![smithery badge](https://smithery.ai/badge/docker-mcp)](https://smithery.ai/protocol/docker-mcp) A powerful Model Context Protocol (MCP) server for Docker operations, enabling seamless container and compose stack management through Claude AI. ## ✨ Features - 🚀 Container creation and instantiation - 📦 Docker Compose stack deployment - 🔍 Container logs retrieval - 📊 Container listing and status monitoring ### 🎬 Demos #### Deploying a Docker Compose Stack https://github.com/user-attachments/assets/b5f6e40a-542b-4a39-ba12-7fdf803ee278 #### Analyzing Container Logs https://github.com/user-attachments/assets/da386eea-2fab-4835-82ae-896de955d934 ## 🚀 Quickstart To try this in Claude Desktop app, add this to your claude config files: ```json { "mcpServers": { "docker-mcp": { "command": "uvx", "args": [ "docker-mcp" ] } } } ``` ### Installing via Smithery To install Docker MCP for Claude Desktop automatically via [Smithery](https://smithery.ai/protocol/docker-mcp): ```bash npx @smithery/cli install docker-mcp --client claude ``` ### Prerequisites - UV (package manager) - Python 3.12+ - Docker Desktop or Docker Engine - Claude Desktop ### Installation #### Claude Desktop Configuration Add the server configuration to your Claude Desktop config file: **MacOS**: `~/Library/Application\ Support/Claude/claude_desktop_config.json` **Windows**: `%APPDATA%/Claude/claude_desktop_config.json`
💻 Development Configuration ```json { "mcpServers": { "docker-mcp": { "command": "uv", "args": [ "--directory", "", "run", "docker-mcp" ] } } } ```
🚀 Production Configuration ```json { "mcpServers": { "docker-mcp": { "command": "uvx", "args": [ "docker-mcp" ] } } } ```
## 🛠️ Development ### Local Setup 1. Clone the repository: ```bash git clone https://github.com/QuantGeekDev/docker-mcp.git cd docker-mcp ``` 2. Create and activate a virtual environment: ```bash python -m venv venv source venv/bin/activate # On Windows: venv\Scripts\activate ``` 3. Install dependencies: ```bash uv sync ``` ### 🔍 Debugging Launch the MCP Inspector for debugging: ```bash npx @modelcontextprotocol/inspector uv --directory run docker-mcp ``` The Inspector will provide a URL to access the debugging interface. ## 📝 Available Tools The server provides the following tools: ### create-container Creates a standalone Docker container ```json { "image": "image-name", "name": "container-name", "ports": {"80": "80"}, "environment": {"ENV_VAR": "value"} } ``` ### deploy-compose Deploys a Docker Compose stack ```json { "project_name": "example-stack", "compose_yaml": "version: '3.8'\nservices:\n service1:\n image: image1:latest\n ports:\n - '8080:80'" } ``` ### get-logs Retrieves logs from a specific container ```json { "container_name": "my-container" } ``` ### list-containers Lists all Docker containers ```json {} ``` ## 🚧 Current Limitations - No built-in environment variable support for containers - No volume management - No network management - No container health checks - No container restart policies - No container resource limits ## 🤝 Contributing 1. Fork the repository from [docker-mcp](https://github.com/QuantGeekDev/docker-mcp) 2. Create your feature branch 3. Commit your changes 4. Push to the branch 5. Open a Pull Request ## 📜 License This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. ## ✨ Authors - **Alex Andru** - *Initial work | Core contributor* - [@QuantGeekDev](https://github.com/QuantGeekDev) - **Ali Sadykov** - *Initial work | Core contributor* - [@md-archive](https://github.com/md-archive) --- Made with ❤️ ================================================ FILE: pyproject.toml ================================================ [project] name = "docker-mcp" version = "0.1.0" description = "A docker MCP server" readme = "README.md" requires-python = ">=3.12" dependencies = [ "httpx>=0.28.0", "mcp>=1.0.0", "python-dotenv>=1.0.1", "python-on-whales>=0.67.0", "pyyaml>=6.0.1" ] [[project.authors]] name = "Alex Andru" email = "alex007d@gmail.com" [build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project.scripts] docker-mcp = "docker_mcp:main" ================================================ FILE: src/docker_mcp/__init__.py ================================================ from . import server import asyncio def main(): """Main entry point for the package.""" asyncio.run(server.main()) # Optionally expose other important items at package level __all__ = ['main', 'server'] ================================================ FILE: src/docker_mcp/docker_executor.py ================================================ from typing import Tuple, Protocol, List import asyncio import os import platform import shutil from abc import ABC, abstractmethod class CommandExecutor(Protocol): async def execute(self, cmd: str | List[str]) -> Tuple[int, str, str]: pass class WindowsExecutor: async def execute(self, cmd: str) -> Tuple[int, str, str]: process = await asyncio.create_subprocess_shell( cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, shell=True ) stdout, stderr = await process.communicate() return process.returncode, stdout.decode(), stderr.decode() class UnixExecutor: async def execute(self, cmd: List[str]) -> Tuple[int, str, str]: process = await asyncio.create_subprocess_exec( *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE ) stdout, stderr = await process.communicate() return process.returncode, stdout.decode(), stderr.decode() class DockerExecutorBase(ABC): def __init__(self): self.docker_cmd = self._initialize_docker_cmd() self.executor = WindowsExecutor() if platform.system() == 'Windows' else UnixExecutor() @abstractmethod async def run_command(self, command: str, *args) -> Tuple[int, str, str]: pass def _initialize_docker_cmd(self) -> str: if platform.system() == 'Windows': docker_dir = r"C:\Program Files\Docker\Docker\resources\bin" docker_paths = [ os.path.join(docker_dir, "docker-compose.exe"), os.path.join(docker_dir, "docker.exe") ] for path in docker_paths: if os.path.exists(path): return path docker_cmd = shutil.which('docker') if not docker_cmd: raise RuntimeError("Docker executable not found") return docker_cmd class DockerComposeExecutor(DockerExecutorBase): def __init__(self, compose_file: str, project_name: str): super().__init__() self.compose_file = os.path.abspath(compose_file) self.project_name = project_name async def run_command(self, command: str, *args) -> Tuple[int, str, str]: if platform.system() == 'Windows': cmd = self._build_windows_command(command, *args) else: cmd = self._build_unix_command(command, *args) return await self.executor.execute(cmd) def _build_windows_command(self, command: str, *args) -> str: compose_file = self.compose_file.replace('\\', '/') return (f'cd "{os.path.dirname(compose_file)}" && docker compose ' f'-f "{os.path.basename(compose_file)}" ' f'-p {self.project_name} {command} {" ".join(args)}') def _build_unix_command(self, command: str, *args) -> list[str]: return [ self.docker_cmd, "compose", "-f", self.compose_file, "-p", self.project_name, command, *args ] async def down(self) -> Tuple[int, str, str]: return await self.run_command("down", "--volumes") async def pull(self) -> Tuple[int, str, str]: return await self.run_command("pull") async def up(self) -> Tuple[int, str, str]: return await self.run_command("up", "-d") async def ps(self) -> Tuple[int, str, str]: return await self.run_command("ps") ================================================ FILE: src/docker_mcp/handlers.py ================================================ from typing import List, Dict, Any import asyncio import os import yaml import platform from python_on_whales import DockerClient from mcp.types import TextContent, Tool, Prompt, PromptArgument, GetPromptResult, PromptMessage from .docker_executor import DockerComposeExecutor docker_client = DockerClient() async def parse_port_mapping(host_key: str, container_port: str | int) -> tuple[str, str] | tuple[str, str, str]: if '/' in str(host_key): host_port, protocol = host_key.split('/') if protocol.lower() == 'udp': return (str(host_port), str(container_port), 'udp') return (str(host_port), str(container_port)) if isinstance(container_port, str) and '/' in container_port: port, protocol = container_port.split('/') if protocol.lower() == 'udp': return (str(host_key), port, 'udp') return (str(host_key), port) return (str(host_key), str(container_port)) class DockerHandlers: TIMEOUT_AMOUNT = 200 @staticmethod async def handle_create_container(arguments: Dict[str, Any]) -> List[TextContent]: try: image = arguments["image"] container_name = arguments.get("name") ports = arguments.get("ports", {}) environment = arguments.get("environment", {}) if not image: raise ValueError("Image name cannot be empty") port_mappings = [] for host_key, container_port in ports.items(): mapping = await parse_port_mapping(host_key, container_port) port_mappings.append(mapping) async def pull_and_run(): if not docker_client.image.exists(image): await asyncio.to_thread(docker_client.image.pull, image) container = await asyncio.to_thread( docker_client.container.run, image, name=container_name, publish=port_mappings, envs=environment, detach=True ) return container container = await asyncio.wait_for(pull_and_run(), timeout=DockerHandlers.TIMEOUT_AMOUNT) return [TextContent(type="text", text=f"Created container '{container.name}' (ID: {container.id})")] except asyncio.TimeoutError: return [TextContent(type="text", text=f"Operation timed out after {DockerHandlers.TIMEOUT_AMOUNT} seconds")] except Exception as e: return [TextContent(type="text", text=f"Error creating container: {str(e)} | Arguments: {arguments}")] @staticmethod async def handle_deploy_compose(arguments: Dict[str, Any]) -> List[TextContent]: debug_info = [] try: compose_yaml = arguments.get("compose_yaml") project_name = arguments.get("project_name") if not compose_yaml or not project_name: raise ValueError( "Missing required compose_yaml or project_name") yaml_content = DockerHandlers._process_yaml( compose_yaml, debug_info) compose_path = DockerHandlers._save_compose_file( yaml_content, project_name) try: result = await DockerHandlers._deploy_stack(compose_path, project_name, debug_info) return [TextContent(type="text", text=result)] finally: DockerHandlers._cleanup_files(compose_path) except Exception as e: debug_output = "\n".join(debug_info) return [TextContent(type="text", text=f"Error deploying compose stack: {str(e)}\n\nDebug Information:\n{debug_output}")] @staticmethod def _process_yaml(compose_yaml: str, debug_info: List[str]) -> dict: debug_info.append("=== Original YAML ===") debug_info.append(compose_yaml) try: yaml_content = yaml.safe_load(compose_yaml) debug_info.append("\n=== Loaded YAML Structure ===") debug_info.append(str(yaml_content)) return yaml_content except yaml.YAMLError as e: raise ValueError(f"Invalid YAML format: {str(e)}") @staticmethod def _save_compose_file(yaml_content: dict, project_name: str) -> str: compose_dir = os.path.join(os.getcwd(), "docker_compose_files") os.makedirs(compose_dir, exist_ok=True) compose_yaml = yaml.safe_dump( yaml_content, default_flow_style=False, sort_keys=False) compose_path = os.path.join( compose_dir, f"{project_name}-docker-compose.yml") with open(compose_path, 'w', encoding='utf-8') as f: f.write(compose_yaml) f.flush() if platform.system() != 'Windows': os.fsync(f.fileno()) return compose_path @staticmethod async def _deploy_stack(compose_path: str, project_name: str, debug_info: List[str]) -> str: compose = DockerComposeExecutor(compose_path, project_name) for command in [compose.down, compose.up]: try: code, out, err = await command() debug_info.extend([ f"\n=== {command.__name__.capitalize()} Command ===", f"Return Code: {code}", f"Stdout: {out}", f"Stderr: {err}" ]) if code != 0 and command == compose.up: raise Exception(f"Deploy failed with code {code}: {err}") except Exception as e: if command != compose.down: raise e debug_info.append(f"Warning during { command.__name__}: {str(e)}") code, out, err = await compose.ps() service_info = out if code == 0 else "Unable to list services" return (f"Successfully deployed compose stack '{project_name}'\n" f"Running services:\n{service_info}\n\n" f"Debug Info:\n{chr(10).join(debug_info)}") @staticmethod def _cleanup_files(compose_path: str) -> None: try: if os.path.exists(compose_path): os.remove(compose_path) compose_dir = os.path.dirname(compose_path) if os.path.exists(compose_dir) and not os.listdir(compose_dir): os.rmdir(compose_dir) except Exception as e: print(f"Warning during cleanup: {str(e)}") @staticmethod async def handle_get_logs(arguments: Dict[str, Any]) -> List[TextContent]: debug_info = [] try: container_name = arguments.get("container_name") if not container_name: raise ValueError("Missing required container_name") debug_info.append(f"Fetching logs for container '{ container_name}'") logs = await asyncio.to_thread(docker_client.container.logs, container_name, tail=100) return [TextContent(type="text", text=f"Logs for container '{container_name}':\n{logs}\n\nDebug Info:\n{chr(10).join(debug_info)}")] except Exception as e: debug_output = "\n".join(debug_info) return [TextContent(type="text", text=f"Error retrieving logs: {str(e)}\n\nDebug Information:\n{debug_output}")] @staticmethod async def handle_list_containers(arguments: Dict[str, Any]) -> List[TextContent]: debug_info = [] try: debug_info.append("Listing all Docker containers") containers = await asyncio.to_thread(docker_client.container.list, all=True) container_list = "\n".join( [f"{c.id[:12]} - {c.name} - {c.state.status}" for c in containers]) return [TextContent(type="text", text=f"All Docker Containers:\n{container_list}\n\nDebug Info:\n{chr(10).join(debug_info)}")] except Exception as e: debug_output = "\n".join(debug_info) return [TextContent(type="text", text=f"Error listing containers: {str(e)}\n\nDebug Information:\n{debug_output}")] ================================================ FILE: src/docker_mcp/server.py ================================================ import asyncio import signal import sys from typing import List, Dict, Any import mcp.types as types from mcp.server import NotificationOptions, Server from mcp.server.models import InitializationOptions import mcp.server.stdio from .handlers import DockerHandlers server = Server("docker-mcp") @server.list_prompts() async def handle_list_prompts() -> List[types.Prompt]: return [ types.Prompt( name="deploy-stack", description="Generate and deploy a Docker stack based on requirements", arguments=[ types.PromptArgument( name="requirements", description="Description of the desired Docker stack", required=True ), types.PromptArgument( name="project_name", description="Name for the Docker Compose project", required=True ) ] ) ] @server.get_prompt() async def handle_get_prompt(name: str, arguments: Dict[str, str] | None) -> types.GetPromptResult: if name != "deploy-stack": raise ValueError(f"Unknown prompt: {name}") if not arguments or "requirements" not in arguments or "project_name" not in arguments: raise ValueError("Missing required arguments") system_message = ( "You are a Docker deployment specialist. Generate appropriate Docker Compose YAML or " "container configurations based on user requirements. For simple single-container " "deployments, use the create-container tool. For multi-container deployments, generate " "a docker-compose.yml and use the deploy-compose tool. To access logs, first use the " "list-containers tool to discover running containers, then use the get-logs tool to " "retrieve logs for a specific container." ) user_message = f"""Please help me deploy the following stack: Requirements: {arguments['requirements']} Project name: {arguments['project_name']} Analyze if this needs a single container or multiple containers. Then: 1. For single container: Use the create-container tool with format: {{ "image": "image-name", "name": "container-name", "ports": {{"80": "80"}}, "environment": {{"ENV_VAR": "value"}} }} 2. For multiple containers: Use the deploy-compose tool with format: {{ "project_name": "example-stack", "compose_yaml": "version: '3.8'\\nservices:\\n service1:\\n image: image1:latest\\n ports:\\n - '8080:80'" }}""" return types.GetPromptResult( description="Generate and deploy a Docker stack", messages=[ types.PromptMessage( role="system", content=types.TextContent( type="text", text=system_message ) ), types.PromptMessage( role="user", content=types.TextContent( type="text", text=user_message ) ) ] ) @server.list_tools() async def handle_list_tools() -> List[types.Tool]: return [ types.Tool( name="create-container", description="Create a new standalone Docker container", inputSchema={ "type": "object", "properties": { "image": {"type": "string"}, "name": {"type": "string"}, "ports": { "type": "object", "additionalProperties": {"type": "string"} }, "environment": { "type": "object", "additionalProperties": {"type": "string"} } }, "required": ["image"] } ), types.Tool( name="deploy-compose", description="Deploy a Docker Compose stack", inputSchema={ "type": "object", "properties": { "compose_yaml": {"type": "string"}, "project_name": {"type": "string"} }, "required": ["compose_yaml", "project_name"] } ), types.Tool( name="get-logs", description="Retrieve the latest logs for a specified Docker container", inputSchema={ "type": "object", "properties": { "container_name": {"type": "string"} }, "required": ["container_name"] } ), types.Tool( name="list-containers", description="List all Docker containers", inputSchema={ "type": "object", "properties": {} } ) ] @server.call_tool() async def handle_call_tool(name: str, arguments: Dict[str, Any] | None) -> List[types.TextContent]: if not arguments and name != "list-containers": raise ValueError("Missing arguments") try: if name == "create-container": return await DockerHandlers.handle_create_container(arguments) elif name == "deploy-compose": return await DockerHandlers.handle_deploy_compose(arguments) elif name == "get-logs": return await DockerHandlers.handle_get_logs(arguments) elif name == "list-containers": return await DockerHandlers.handle_list_containers(arguments) else: raise ValueError(f"Unknown tool: {name}") except Exception as e: return [types.TextContent(type="text", text=f"Error: {str(e)} | Arguments: {arguments}")] async def main(): signal.signal(signal.SIGINT, handle_shutdown) signal.signal(signal.SIGTERM, handle_shutdown) async with mcp.server.stdio.stdio_server() as (read_stream, write_stream): await server.run( read_stream, write_stream, InitializationOptions( server_name="docker-mcp", server_version="0.1.0", capabilities=server.get_capabilities( notification_options=NotificationOptions(), experimental_capabilities={}, ), ), ) def handle_shutdown(signum, frame): print("Shutting down gracefully...") sys.exit(0) if __name__ == "__main__": asyncio.run(main())