mcp-servers/docker-mcp/docker_mcp.py
Zac Gaetano 39fff1e44a Add Memory Bank, Puppeteer, Sequential Thinking, and Docker MCP servers
New MCP servers added to the gateway stack:
- memory-bank-mcp (port 8700): Persistent key-value memory storage with tags, categories, and search
- puppeteer-mcp (port 8800): Headless browser automation via Pyppeteer (navigate, screenshot, click, JS eval, PDF gen)
- sequential-thinking-mcp (port 8900): Structured step-by-step reasoning with branching hypotheses and synthesis
- docker-mcp (port 9000): Docker container/image/network/volume management via Docker socket

All servers follow the existing Python/FastMCP pattern with streamable-http transport.
docker-compose.yml updated with service definitions and gateway backend routes.
2026-03-31 23:02:47 -04:00

532 lines
16 KiB
Python
Executable file

"""
Docker MCP Server
=================
MCP server providing Docker container and image management capabilities.
Supports listing, inspecting, starting, stopping, removing containers,
managing images, viewing logs, executing commands, and managing networks/volumes.
Connects to the Docker daemon via socket or TCP.
"""
import json
import os
from typing import Optional, List, Dict, Any
import docker
from docker.errors import DockerException, NotFound, APIError
from mcp.server.fastmcp import FastMCP
# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------
DOCKER_HOST = os.environ.get("DOCKER_HOST", "unix:///var/run/docker.sock")
# ---------------------------------------------------------------------------
# MCP Server
# ---------------------------------------------------------------------------
mcp = FastMCP("docker_mcp")
# ---------------------------------------------------------------------------
# Docker client
# ---------------------------------------------------------------------------
def _client() -> docker.DockerClient:
"""Get a Docker client instance."""
return docker.DockerClient(base_url=DOCKER_HOST, timeout=30)
def _safe(func):
"""Wrapper for safe Docker API calls."""
try:
return func()
except NotFound as e:
return {"error": f"Not found: {str(e)}"}
except APIError as e:
return {"error": f"Docker API error: {str(e)}"}
except DockerException as e:
return {"error": f"Docker error: {str(e)}"}
# ---------------------------------------------------------------------------
# Container tools
# ---------------------------------------------------------------------------
@mcp.tool()
async def list_containers(
all: bool = True,
filters: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
"""
List Docker containers.
Args:
all: Show all containers (including stopped). Default True.
filters: Optional filter dict (e.g., {"status": "running", "name": "my-app"})
"""
client = _client()
containers = client.containers.list(all=all, filters=filters or {})
result = []
for c in containers:
result.append({
"id": c.short_id,
"name": c.name,
"image": str(c.image.tags[0]) if c.image.tags else str(c.image.id[:12]),
"status": c.status,
"state": c.attrs.get("State", {}).get("Status"),
"created": c.attrs.get("Created"),
"ports": c.ports,
})
return {"containers": result, "total": len(result)}
@mcp.tool()
async def inspect_container(container_id: str) -> Dict[str, Any]:
"""
Get detailed information about a container.
Args:
container_id: Container ID or name
"""
client = _client()
try:
c = client.containers.get(container_id)
attrs = c.attrs
return {
"id": c.short_id,
"name": c.name,
"image": str(c.image.tags[0]) if c.image.tags else str(c.image.id[:12]),
"status": c.status,
"state": attrs.get("State", {}),
"config": {
"env": attrs.get("Config", {}).get("Env", []),
"cmd": attrs.get("Config", {}).get("Cmd"),
"entrypoint": attrs.get("Config", {}).get("Entrypoint"),
"working_dir": attrs.get("Config", {}).get("WorkingDir"),
},
"network": attrs.get("NetworkSettings", {}).get("Networks", {}),
"mounts": attrs.get("Mounts", []),
"ports": c.ports,
"created": attrs.get("Created"),
"restart_count": attrs.get("RestartCount", 0),
}
except NotFound:
return {"error": f"Container '{container_id}' not found"}
@mcp.tool()
async def container_logs(
container_id: str,
tail: int = 100,
since: Optional[str] = None,
timestamps: bool = False,
) -> Dict[str, Any]:
"""
Get logs from a container.
Args:
container_id: Container ID or name
tail: Number of lines from the end (default 100)
since: Show logs since timestamp (e.g., '2024-01-01T00:00:00')
timestamps: Include timestamps in log output
"""
client = _client()
try:
c = client.containers.get(container_id)
kwargs: Dict[str, Any] = {
"tail": tail,
"timestamps": timestamps,
"stdout": True,
"stderr": True,
}
if since:
kwargs["since"] = since
logs = c.logs(**kwargs)
log_text = logs.decode("utf-8", errors="replace")
return {
"container": container_id,
"lines": tail,
"logs": log_text,
}
except NotFound:
return {"error": f"Container '{container_id}' not found"}
@mcp.tool()
async def start_container(container_id: str) -> Dict[str, Any]:
"""
Start a stopped container.
Args:
container_id: Container ID or name
"""
client = _client()
try:
c = client.containers.get(container_id)
c.start()
c.reload()
return {"status": "started", "container": c.name, "state": c.status}
except NotFound:
return {"error": f"Container '{container_id}' not found"}
@mcp.tool()
async def stop_container(
container_id: str,
timeout: int = 10,
) -> Dict[str, Any]:
"""
Stop a running container.
Args:
container_id: Container ID or name
timeout: Seconds to wait before killing (default 10)
"""
client = _client()
try:
c = client.containers.get(container_id)
c.stop(timeout=timeout)
c.reload()
return {"status": "stopped", "container": c.name, "state": c.status}
except NotFound:
return {"error": f"Container '{container_id}' not found"}
@mcp.tool()
async def restart_container(
container_id: str,
timeout: int = 10,
) -> Dict[str, Any]:
"""
Restart a container.
Args:
container_id: Container ID or name
timeout: Seconds to wait before killing (default 10)
"""
client = _client()
try:
c = client.containers.get(container_id)
c.restart(timeout=timeout)
c.reload()
return {"status": "restarted", "container": c.name, "state": c.status}
except NotFound:
return {"error": f"Container '{container_id}' not found"}
@mcp.tool()
async def remove_container(
container_id: str,
force: bool = False,
v: bool = False,
) -> Dict[str, Any]:
"""
Remove a container.
Args:
container_id: Container ID or name
force: Force remove even if running
v: Remove associated volumes
"""
client = _client()
try:
c = client.containers.get(container_id)
name = c.name
c.remove(force=force, v=v)
return {"status": "removed", "container": name}
except NotFound:
return {"error": f"Container '{container_id}' not found"}
@mcp.tool()
async def exec_in_container(
container_id: str,
command: str,
workdir: Optional[str] = None,
) -> Dict[str, Any]:
"""
Execute a command inside a running container.
Args:
container_id: Container ID or name
command: Command to execute (shell string)
workdir: Optional working directory inside the container
"""
client = _client()
try:
c = client.containers.get(container_id)
kwargs: Dict[str, Any] = {"cmd": command, "stdout": True, "stderr": True}
if workdir:
kwargs["workdir"] = workdir
exit_code, output = c.exec_run(**kwargs)
return {
"container": container_id,
"command": command,
"exit_code": exit_code,
"output": output.decode("utf-8", errors="replace"),
}
except NotFound:
return {"error": f"Container '{container_id}' not found"}
@mcp.tool()
async def container_stats(container_id: str) -> Dict[str, Any]:
"""
Get resource usage stats for a container.
Args:
container_id: Container ID or name
"""
client = _client()
try:
c = client.containers.get(container_id)
stats = c.stats(stream=False)
# Parse CPU
cpu_delta = stats["cpu_stats"]["cpu_usage"]["total_usage"] - \
stats["precpu_stats"]["cpu_usage"]["total_usage"]
system_delta = stats["cpu_stats"]["system_cpu_usage"] - \
stats["precpu_stats"]["system_cpu_usage"]
num_cpus = len(stats["cpu_stats"]["cpu_usage"].get("percpu_usage", [1]))
cpu_percent = (cpu_delta / system_delta) * num_cpus * 100.0 if system_delta > 0 else 0.0
# Parse Memory
mem_usage = stats["memory_stats"].get("usage", 0)
mem_limit = stats["memory_stats"].get("limit", 1)
mem_percent = (mem_usage / mem_limit) * 100.0
return {
"container": container_id,
"cpu_percent": round(cpu_percent, 2),
"memory_usage_mb": round(mem_usage / (1024 * 1024), 2),
"memory_limit_mb": round(mem_limit / (1024 * 1024), 2),
"memory_percent": round(mem_percent, 2),
"network_rx_bytes": sum(
v.get("rx_bytes", 0) for v in stats.get("networks", {}).values()
),
"network_tx_bytes": sum(
v.get("tx_bytes", 0) for v in stats.get("networks", {}).values()
),
"pids": stats.get("pids_stats", {}).get("current", 0),
}
except NotFound:
return {"error": f"Container '{container_id}' not found"}
except (KeyError, ZeroDivisionError) as e:
return {"error": f"Failed to parse stats: {str(e)}"}
# ---------------------------------------------------------------------------
# Image tools
# ---------------------------------------------------------------------------
@mcp.tool()
async def list_images(
name: Optional[str] = None,
all: bool = False,
) -> Dict[str, Any]:
"""
List Docker images.
Args:
name: Optional filter by image name
all: Show all images including intermediate layers
"""
client = _client()
images = client.images.list(name=name, all=all)
result = []
for img in images:
result.append({
"id": img.short_id,
"tags": img.tags,
"size_mb": round(img.attrs.get("Size", 0) / (1024 * 1024), 2),
"created": img.attrs.get("Created"),
})
return {"images": result, "total": len(result)}
@mcp.tool()
async def pull_image(
image: str,
tag: str = "latest",
) -> Dict[str, Any]:
"""
Pull a Docker image from a registry.
Args:
image: Image name (e.g., 'nginx', 'python')
tag: Image tag (default: 'latest')
"""
client = _client()
try:
img = client.images.pull(image, tag=tag)
return {
"status": "pulled",
"image": f"{image}:{tag}",
"id": img.short_id,
"size_mb": round(img.attrs.get("Size", 0) / (1024 * 1024), 2),
}
except APIError as e:
return {"error": f"Failed to pull {image}:{tag}: {str(e)}"}
@mcp.tool()
async def remove_image(
image: str,
force: bool = False,
) -> Dict[str, Any]:
"""
Remove a Docker image.
Args:
image: Image ID or name:tag
force: Force removal
"""
client = _client()
try:
client.images.remove(image, force=force)
return {"status": "removed", "image": image}
except NotFound:
return {"error": f"Image '{image}' not found"}
# ---------------------------------------------------------------------------
# System tools
# ---------------------------------------------------------------------------
@mcp.tool()
async def docker_system_info() -> Dict[str, Any]:
"""Get Docker system-wide information."""
client = _client()
info = client.info()
return {
"docker_version": info.get("ServerVersion"),
"os": info.get("OperatingSystem"),
"arch": info.get("Architecture"),
"cpus": info.get("NCPU"),
"memory_gb": round(info.get("MemTotal", 0) / (1024**3), 2),
"containers_running": info.get("ContainersRunning"),
"containers_stopped": info.get("ContainersStopped"),
"containers_paused": info.get("ContainersPaused"),
"images": info.get("Images"),
"storage_driver": info.get("Driver"),
}
@mcp.tool()
async def docker_disk_usage() -> Dict[str, Any]:
"""Get Docker disk usage summary."""
client = _client()
df = client.df()
containers_size = sum(c.get("SizeRw", 0) for c in df.get("Containers", []))
images_size = sum(i.get("Size", 0) for i in df.get("Images", []))
volumes_size = sum(v.get("UsageData", {}).get("Size", 0) for v in df.get("Volumes", []))
return {
"containers_size_mb": round(containers_size / (1024 * 1024), 2),
"images_size_mb": round(images_size / (1024 * 1024), 2),
"volumes_size_mb": round(volumes_size / (1024 * 1024), 2),
"total_mb": round((containers_size + images_size + volumes_size) / (1024 * 1024), 2),
"images_count": len(df.get("Images", [])),
"containers_count": len(df.get("Containers", [])),
"volumes_count": len(df.get("Volumes", [])),
}
# ---------------------------------------------------------------------------
# Network tools
# ---------------------------------------------------------------------------
@mcp.tool()
async def list_networks() -> Dict[str, Any]:
"""List Docker networks."""
client = _client()
networks = client.networks.list()
result = []
for n in networks:
result.append({
"id": n.short_id,
"name": n.name,
"driver": n.attrs.get("Driver"),
"scope": n.attrs.get("Scope"),
"containers": len(n.attrs.get("Containers", {})),
})
return {"networks": result, "total": len(result)}
# ---------------------------------------------------------------------------
# Volume tools
# ---------------------------------------------------------------------------
@mcp.tool()
async def list_volumes() -> Dict[str, Any]:
"""List Docker volumes."""
client = _client()
volumes = client.volumes.list()
result = []
for v in volumes:
result.append({
"name": v.name,
"driver": v.attrs.get("Driver"),
"mountpoint": v.attrs.get("Mountpoint"),
"created": v.attrs.get("CreatedAt"),
})
return {"volumes": result, "total": len(result)}
@mcp.tool()
async def prune_system(
containers: bool = True,
images: bool = True,
volumes: bool = False,
networks: bool = True,
) -> Dict[str, Any]:
"""
Prune unused Docker resources.
Args:
containers: Prune stopped containers
images: Prune dangling images
volumes: Prune unused volumes (CAUTION: data loss)
networks: Prune unused networks
"""
client = _client()
results = {}
if containers:
r = client.containers.prune()
results["containers_deleted"] = len(r.get("ContainersDeleted", []) or [])
results["containers_space_mb"] = round(r.get("SpaceReclaimed", 0) / (1024 * 1024), 2)
if images:
r = client.images.prune()
results["images_deleted"] = len(r.get("ImagesDeleted", []) or [])
results["images_space_mb"] = round(r.get("SpaceReclaimed", 0) / (1024 * 1024), 2)
if volumes:
r = client.volumes.prune()
results["volumes_deleted"] = len(r.get("VolumesDeleted", []) or [])
results["volumes_space_mb"] = round(r.get("SpaceReclaimed", 0) / (1024 * 1024), 2)
if networks:
r = client.networks.prune()
results["networks_deleted"] = len(r.get("NetworksDeleted", []) or [])
return results