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
[](https://www.python.org/downloads/release/python-3120/)
[](https://opensource.org/licenses/MIT)
[](https://github.com/psf/black)
[](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())