import docker from docker.errors import APIError, NotFound from mcp.server.fastmcp import FastMCP mcp = FastMCP("Docker MCP", instructions="MCP server for managing Docker containers.") _client: docker.DockerClient | None = None def get_client() -> docker.DockerClient: global _client if _client is None: _client = docker.from_env() return _client def _container_summary(c) -> dict: return { "id": c.short_id, "name": c.name, "image": str(c.image.tags[0] if c.image.tags else c.image.short_id), "status": c.status, "ports": c.ports, } @mcp.tool() def list_containers(all: bool = False) -> list[dict]: """List Docker containers. Args: all: If True, include stopped containers. Default is only running. """ client = get_client() containers = client.containers.list(all=all) return [_container_summary(c) for c in containers] @mcp.tool() def inspect_container(container_id: str) -> dict: """Get detailed information about a container. Args: container_id: Container ID or name. """ client = get_client() try: c = client.containers.get(container_id) except NotFound: return {"error": f"Container '{container_id}' not found."} return { "id": c.id, "short_id": c.short_id, "name": c.name, "status": c.status, "image": str(c.image.tags[0] if c.image.tags else c.image.short_id), "labels": c.labels, "ports": c.ports, "created": str(c.attrs.get("Created", "")), "platform": c.attrs.get("Platform", ""), "state": c.attrs.get("State", {}), "network_settings": { k: v.get("IPAddress", "") if isinstance(v, dict) else v for k, v in (c.attrs.get("NetworkSettings", {}).get("Networks", {})).items() }, "mounts": [ { "source": m.get("Source", ""), "destination": m.get("Destination", ""), "mode": m.get("Mode", ""), } for m in c.attrs.get("Mounts", []) ], "env": c.attrs.get("Config", {}).get("Env", []), "cmd": c.attrs.get("Config", {}).get("Cmd", []), } @mcp.tool() def start_container(container_id: str) -> str: """Start a stopped container. Args: container_id: Container ID or name. """ client = get_client() try: c = client.containers.get(container_id) c.start() return f"Container '{c.name}' started." except NotFound: return f"Container '{container_id}' not found." except APIError as e: return f"Failed to start container: {e.explanation}" @mcp.tool() def stop_container(container_id: str, timeout: int = 10) -> str: """Stop a running container. Args: container_id: Container ID or name. timeout: Seconds to wait before killing. Default 10. """ client = get_client() try: c = client.containers.get(container_id) c.stop(timeout=timeout) return f"Container '{c.name}' stopped." except NotFound: return f"Container '{container_id}' not found." except APIError as e: return f"Failed to stop container: {e.explanation}" @mcp.tool() def restart_container(container_id: str, timeout: int = 10) -> str: """Restart a container. Args: container_id: Container ID or name. timeout: Seconds to wait before killing during restart. Default 10. """ client = get_client() try: c = client.containers.get(container_id) c.restart(timeout=timeout) return f"Container '{c.name}' restarted." except NotFound: return f"Container '{container_id}' not found." except APIError as e: return f"Failed to restart container: {e.explanation}" @mcp.tool() def remove_container(container_id: str, force: bool = False) -> str: """Remove a container. Args: container_id: Container ID or name. force: Force remove a running container. Default False. """ client = get_client() try: c = client.containers.get(container_id) name = c.name c.remove(force=force) return f"Container '{name}' removed." except NotFound: return f"Container '{container_id}' not found." except APIError as e: return f"Failed to remove container: {e.explanation}" @mcp.tool() def container_logs(container_id: str, tail: int = 100, timestamps: bool = False) -> str: """Get logs from a container. Args: container_id: Container ID or name. tail: Number of lines from the end. Default 100. timestamps: Include timestamps in output. Default False. """ client = get_client() try: c = client.containers.get(container_id) logs = c.logs(tail=tail, timestamps=timestamps) return logs.decode("utf-8", errors="replace") except NotFound: return f"Container '{container_id}' not found." except APIError as e: return f"Failed to get logs: {e.explanation}" @mcp.tool() def exec_in_container(container_id: str, command: str) -> str: """Execute a command inside a running container. Args: container_id: Container ID or name. command: The command to run (e.g. "ls -la /app"). """ client = get_client() try: c = client.containers.get(container_id) exit_code, output = c.exec_run(command) decoded = output.decode("utf-8", errors="replace") return f"exit_code={exit_code}\n{decoded}" except NotFound: return f"Container '{container_id}' not found." except APIError as e: return f"Failed to exec command: {e.explanation}" @mcp.tool() def run_container( image: str, name: str | None = None, command: str | None = None, detach: bool = True, ports: dict[str, int] | None = None, environment: dict[str, str] | None = None, volumes: dict[str, dict[str, str]] | None = None, ) -> str: """Run a new container from an image. Args: image: Docker image to run (e.g. "nginx:latest"). name: Optional container name. command: Optional command to run. detach: Run in background. Default True. ports: Port mapping, e.g. {"80/tcp": 8080}. environment: Environment variables, e.g. {"KEY": "value"}. volumes: Volume mounts, e.g. {"/host/path": {"bind": "/container/path", "mode": "rw"}}. """ client = get_client() try: c = client.containers.run( image, command=command, name=name, detach=detach, ports=ports, environment=environment, volumes=volumes, ) if detach: return f"Container '{c.name}' ({c.short_id}) started from image '{image}'." return c.decode("utf-8", errors="replace") if isinstance(c, bytes) else str(c) except APIError as e: return f"Failed to run container: {e.explanation}" @mcp.tool() def container_stats(container_id: str) -> dict: """Get resource usage statistics for a container (CPU, memory, network I/O). Args: container_id: Container ID or name. """ client = get_client() try: c = client.containers.get(container_id) stats = c.stats(stream=False) # CPU cpu_delta = ( stats["cpu_stats"]["cpu_usage"]["total_usage"] - stats["precpu_stats"]["cpu_usage"]["total_usage"] ) system_delta = stats["cpu_stats"].get("system_cpu_usage", 0) - stats[ "precpu_stats" ].get("system_cpu_usage", 0) num_cpus = stats["cpu_stats"].get("online_cpus", 1) cpu_percent = ( (cpu_delta / system_delta) * num_cpus * 100.0 if system_delta > 0 else 0.0 ) # 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 # Network net_rx = 0 net_tx = 0 for iface_stats in stats.get("networks", {}).values(): net_rx += iface_stats.get("rx_bytes", 0) net_tx += iface_stats.get("tx_bytes", 0) return { "container": c.name, "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_mb": round(net_rx / (1024 * 1024), 2), "network_tx_mb": round(net_tx / (1024 * 1024), 2), } except NotFound: return {"error": f"Container '{container_id}' not found."} except APIError as e: return {"error": f"Failed to get stats: {e.explanation}"} except KeyError, ZeroDivisionError: return {"error": "Stats unavailable for this container."} if __name__ == "__main__": mcp.run()