docker-mcp-server 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- docker_mcp/__init__.py +1 -0
- docker_mcp/client.py +13 -0
- docker_mcp/server.py +36 -0
- docker_mcp/tools/__init__.py +18 -0
- docker_mcp/tools/compose.py +89 -0
- docker_mcp/tools/containers.py +179 -0
- docker_mcp/tools/images.py +84 -0
- docker_mcp/tools/networks.py +76 -0
- docker_mcp/tools/system.py +92 -0
- docker_mcp/tools/volumes.py +54 -0
- docker_mcp/utils.py +14 -0
- docker_mcp_server-0.1.0.dist-info/METADATA +227 -0
- docker_mcp_server-0.1.0.dist-info/RECORD +15 -0
- docker_mcp_server-0.1.0.dist-info/WHEEL +4 -0
- docker_mcp_server-0.1.0.dist-info/entry_points.txt +3 -0
docker_mcp/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Docker MCP Server — Expose Docker operations as MCP tools."""
|
docker_mcp/client.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Docker client singleton."""
|
|
2
|
+
|
|
3
|
+
import docker
|
|
4
|
+
|
|
5
|
+
_client: docker.DockerClient | None = None
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_client() -> docker.DockerClient:
|
|
9
|
+
"""Return a shared Docker client instance."""
|
|
10
|
+
global _client
|
|
11
|
+
if _client is None:
|
|
12
|
+
_client = docker.from_env()
|
|
13
|
+
return _client
|
docker_mcp/server.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Docker MCP Server — main entry point."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
|
|
5
|
+
from mcp.server.fastmcp import FastMCP
|
|
6
|
+
|
|
7
|
+
from docker_mcp.tools import register_all_tools
|
|
8
|
+
|
|
9
|
+
mcp = FastMCP("docker-mcp")
|
|
10
|
+
register_all_tools(mcp)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def main():
|
|
14
|
+
parser = argparse.ArgumentParser(description="Docker MCP Server")
|
|
15
|
+
parser.add_argument(
|
|
16
|
+
"--transport",
|
|
17
|
+
choices=["stdio", "sse"],
|
|
18
|
+
default="stdio",
|
|
19
|
+
help="Transport mode (default: stdio)",
|
|
20
|
+
)
|
|
21
|
+
parser.add_argument(
|
|
22
|
+
"--port",
|
|
23
|
+
type=int,
|
|
24
|
+
default=8000,
|
|
25
|
+
help="Port for SSE transport (default: 8000)",
|
|
26
|
+
)
|
|
27
|
+
args = parser.parse_args()
|
|
28
|
+
|
|
29
|
+
if args.transport == "sse":
|
|
30
|
+
mcp.run(transport="sse", port=args.port)
|
|
31
|
+
else:
|
|
32
|
+
mcp.run(transport="stdio")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
if __name__ == "__main__":
|
|
36
|
+
main()
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Docker MCP tools."""
|
|
2
|
+
|
|
3
|
+
from docker_mcp.tools.containers import register as register_containers
|
|
4
|
+
from docker_mcp.tools.images import register as register_images
|
|
5
|
+
from docker_mcp.tools.compose import register as register_compose
|
|
6
|
+
from docker_mcp.tools.volumes import register as register_volumes
|
|
7
|
+
from docker_mcp.tools.networks import register as register_networks
|
|
8
|
+
from docker_mcp.tools.system import register as register_system
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def register_all_tools(mcp):
|
|
12
|
+
"""Register all tool modules with the MCP server."""
|
|
13
|
+
register_containers(mcp)
|
|
14
|
+
register_images(mcp)
|
|
15
|
+
register_compose(mcp)
|
|
16
|
+
register_volumes(mcp)
|
|
17
|
+
register_networks(mcp)
|
|
18
|
+
register_system(mcp)
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Docker Compose tools (subprocess-based)."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
|
|
5
|
+
from docker_mcp.utils import format_error
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _run_compose(args: list[str], cwd: str | None = None) -> str:
|
|
9
|
+
"""Run a docker compose command and return output."""
|
|
10
|
+
cmd = ["docker", "compose"] + args
|
|
11
|
+
result = subprocess.run(cmd, capture_output=True, text=True, cwd=cwd)
|
|
12
|
+
output = result.stdout
|
|
13
|
+
if result.returncode != 0:
|
|
14
|
+
output += f"\nSTDERR:\n{result.stderr}" if result.stderr else ""
|
|
15
|
+
return f"Command failed (exit {result.returncode}):\n{output}"
|
|
16
|
+
return output or "(no output)"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def register(mcp):
|
|
20
|
+
@mcp.tool()
|
|
21
|
+
def compose_up(project_directory: str, detach: bool = True, build: bool = False) -> str:
|
|
22
|
+
"""Start services from a docker-compose file. project_directory is the folder containing docker-compose.yml."""
|
|
23
|
+
try:
|
|
24
|
+
args = ["up"]
|
|
25
|
+
if detach:
|
|
26
|
+
args.append("-d")
|
|
27
|
+
if build:
|
|
28
|
+
args.append("--build")
|
|
29
|
+
return _run_compose(args, cwd=project_directory)
|
|
30
|
+
except Exception as e:
|
|
31
|
+
return format_error(e)
|
|
32
|
+
|
|
33
|
+
@mcp.tool()
|
|
34
|
+
def compose_down(project_directory: str, volumes: bool = False) -> str:
|
|
35
|
+
"""Stop and remove compose services."""
|
|
36
|
+
try:
|
|
37
|
+
args = ["down"]
|
|
38
|
+
if volumes:
|
|
39
|
+
args.append("-v")
|
|
40
|
+
return _run_compose(args, cwd=project_directory)
|
|
41
|
+
except Exception as e:
|
|
42
|
+
return format_error(e)
|
|
43
|
+
|
|
44
|
+
@mcp.tool()
|
|
45
|
+
def compose_ps(project_directory: str) -> str:
|
|
46
|
+
"""List compose services status."""
|
|
47
|
+
try:
|
|
48
|
+
return _run_compose(["ps"], cwd=project_directory)
|
|
49
|
+
except Exception as e:
|
|
50
|
+
return format_error(e)
|
|
51
|
+
|
|
52
|
+
@mcp.tool()
|
|
53
|
+
def compose_logs(project_directory: str, service: str | None = None, tail: int = 100) -> str:
|
|
54
|
+
"""Get logs from compose services. Optionally filter by service name."""
|
|
55
|
+
try:
|
|
56
|
+
args = ["logs", "--tail", str(tail)]
|
|
57
|
+
if service:
|
|
58
|
+
args.append(service)
|
|
59
|
+
return _run_compose(args, cwd=project_directory)
|
|
60
|
+
except Exception as e:
|
|
61
|
+
return format_error(e)
|
|
62
|
+
|
|
63
|
+
@mcp.tool()
|
|
64
|
+
def compose_build(
|
|
65
|
+
project_directory: str,
|
|
66
|
+
service: str | None = None,
|
|
67
|
+
no_cache: bool = False,
|
|
68
|
+
) -> str:
|
|
69
|
+
"""Build or rebuild compose services. Optionally target a specific service."""
|
|
70
|
+
try:
|
|
71
|
+
args = ["build"]
|
|
72
|
+
if no_cache:
|
|
73
|
+
args.append("--no-cache")
|
|
74
|
+
if service:
|
|
75
|
+
args.append(service)
|
|
76
|
+
return _run_compose(args, cwd=project_directory)
|
|
77
|
+
except Exception as e:
|
|
78
|
+
return format_error(e)
|
|
79
|
+
|
|
80
|
+
@mcp.tool()
|
|
81
|
+
def compose_restart(project_directory: str, service: str | None = None) -> str:
|
|
82
|
+
"""Restart compose services. Optionally target a specific service."""
|
|
83
|
+
try:
|
|
84
|
+
args = ["restart"]
|
|
85
|
+
if service:
|
|
86
|
+
args.append(service)
|
|
87
|
+
return _run_compose(args, cwd=project_directory)
|
|
88
|
+
except Exception as e:
|
|
89
|
+
return format_error(e)
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""Container management tools."""
|
|
2
|
+
|
|
3
|
+
from docker_mcp.client import get_client
|
|
4
|
+
from docker_mcp.utils import format_error
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def register(mcp):
|
|
8
|
+
@mcp.tool()
|
|
9
|
+
def list_containers(all: bool = False) -> str:
|
|
10
|
+
"""List Docker containers. Set all=True to include stopped containers."""
|
|
11
|
+
try:
|
|
12
|
+
client = get_client()
|
|
13
|
+
containers = client.containers.list(all=all)
|
|
14
|
+
if not containers:
|
|
15
|
+
return "No containers found."
|
|
16
|
+
lines = []
|
|
17
|
+
for c in containers:
|
|
18
|
+
lines.append(
|
|
19
|
+
f"ID: {c.short_id} | Name: {c.name} | Image: {c.image.tags[0] if c.image.tags else 'untagged'} | Status: {c.status}"
|
|
20
|
+
)
|
|
21
|
+
return "\n".join(lines)
|
|
22
|
+
except Exception as e:
|
|
23
|
+
return format_error(e)
|
|
24
|
+
|
|
25
|
+
@mcp.tool()
|
|
26
|
+
def create_container(image: str, name: str | None = None, command: str | None = None, ports: dict | None = None, environment: dict | None = None) -> str:
|
|
27
|
+
"""Create a new container from an image without starting it."""
|
|
28
|
+
try:
|
|
29
|
+
client = get_client()
|
|
30
|
+
kwargs = {"image": image}
|
|
31
|
+
if name:
|
|
32
|
+
kwargs["name"] = name
|
|
33
|
+
if command:
|
|
34
|
+
kwargs["command"] = command
|
|
35
|
+
if ports:
|
|
36
|
+
kwargs["ports"] = ports
|
|
37
|
+
if environment:
|
|
38
|
+
kwargs["environment"] = environment
|
|
39
|
+
container = client.containers.create(**kwargs)
|
|
40
|
+
return f"Container created: {container.short_id} ({container.name})"
|
|
41
|
+
except Exception as e:
|
|
42
|
+
return format_error(e)
|
|
43
|
+
|
|
44
|
+
@mcp.tool()
|
|
45
|
+
def start_container(container_id: str) -> str:
|
|
46
|
+
"""Start a stopped container by ID or name."""
|
|
47
|
+
try:
|
|
48
|
+
client = get_client()
|
|
49
|
+
container = client.containers.get(container_id)
|
|
50
|
+
container.start()
|
|
51
|
+
return f"Container {container.short_id} started."
|
|
52
|
+
except Exception as e:
|
|
53
|
+
return format_error(e)
|
|
54
|
+
|
|
55
|
+
@mcp.tool()
|
|
56
|
+
def stop_container(container_id: str, timeout: int = 10) -> str:
|
|
57
|
+
"""Stop a running container by ID or name."""
|
|
58
|
+
try:
|
|
59
|
+
client = get_client()
|
|
60
|
+
container = client.containers.get(container_id)
|
|
61
|
+
container.stop(timeout=timeout)
|
|
62
|
+
return f"Container {container.short_id} stopped."
|
|
63
|
+
except Exception as e:
|
|
64
|
+
return format_error(e)
|
|
65
|
+
|
|
66
|
+
@mcp.tool()
|
|
67
|
+
def restart_container(container_id: str, timeout: int = 10) -> str:
|
|
68
|
+
"""Restart a container by ID or name."""
|
|
69
|
+
try:
|
|
70
|
+
client = get_client()
|
|
71
|
+
container = client.containers.get(container_id)
|
|
72
|
+
container.restart(timeout=timeout)
|
|
73
|
+
return f"Container {container.short_id} restarted."
|
|
74
|
+
except Exception as e:
|
|
75
|
+
return format_error(e)
|
|
76
|
+
|
|
77
|
+
@mcp.tool()
|
|
78
|
+
def remove_container(container_id: str, force: bool = False) -> str:
|
|
79
|
+
"""Remove a container by ID or name. Use force=True to remove a running container."""
|
|
80
|
+
try:
|
|
81
|
+
client = get_client()
|
|
82
|
+
container = client.containers.get(container_id)
|
|
83
|
+
container.remove(force=force)
|
|
84
|
+
return f"Container {container_id} removed."
|
|
85
|
+
except Exception as e:
|
|
86
|
+
return format_error(e)
|
|
87
|
+
|
|
88
|
+
@mcp.tool()
|
|
89
|
+
def get_container_logs(container_id: str, tail: int = 100) -> str:
|
|
90
|
+
"""Get logs from a container. tail controls how many lines to return."""
|
|
91
|
+
try:
|
|
92
|
+
client = get_client()
|
|
93
|
+
container = client.containers.get(container_id)
|
|
94
|
+
logs = container.logs(tail=tail).decode("utf-8", errors="replace")
|
|
95
|
+
return logs or "(no logs)"
|
|
96
|
+
except Exception as e:
|
|
97
|
+
return format_error(e)
|
|
98
|
+
|
|
99
|
+
@mcp.tool()
|
|
100
|
+
def exec_in_container(container_id: str, command: str) -> str:
|
|
101
|
+
"""Execute a command inside a running container."""
|
|
102
|
+
try:
|
|
103
|
+
client = get_client()
|
|
104
|
+
container = client.containers.get(container_id)
|
|
105
|
+
exit_code, output = container.exec_run(command)
|
|
106
|
+
result = output.decode("utf-8", errors="replace")
|
|
107
|
+
return f"Exit code: {exit_code}\n{result}"
|
|
108
|
+
except Exception as e:
|
|
109
|
+
return format_error(e)
|
|
110
|
+
|
|
111
|
+
@mcp.tool()
|
|
112
|
+
def inspect_container(container_id: str) -> str:
|
|
113
|
+
"""Get detailed information about a container."""
|
|
114
|
+
try:
|
|
115
|
+
import json
|
|
116
|
+
client = get_client()
|
|
117
|
+
container = client.containers.get(container_id)
|
|
118
|
+
return json.dumps(container.attrs, indent=2, default=str)
|
|
119
|
+
except Exception as e:
|
|
120
|
+
return format_error(e)
|
|
121
|
+
|
|
122
|
+
@mcp.tool()
|
|
123
|
+
def run_container(
|
|
124
|
+
image: str,
|
|
125
|
+
name: str | None = None,
|
|
126
|
+
command: str | None = None,
|
|
127
|
+
ports: dict | None = None,
|
|
128
|
+
environment: dict | None = None,
|
|
129
|
+
detach: bool = True,
|
|
130
|
+
) -> str:
|
|
131
|
+
"""Create and start a container in one step (equivalent to docker run)."""
|
|
132
|
+
try:
|
|
133
|
+
client = get_client()
|
|
134
|
+
kwargs: dict = {"image": image, "detach": detach}
|
|
135
|
+
if name:
|
|
136
|
+
kwargs["name"] = name
|
|
137
|
+
if command:
|
|
138
|
+
kwargs["command"] = command
|
|
139
|
+
if ports:
|
|
140
|
+
kwargs["ports"] = ports
|
|
141
|
+
if environment:
|
|
142
|
+
kwargs["environment"] = environment
|
|
143
|
+
container = client.containers.run(**kwargs)
|
|
144
|
+
if detach:
|
|
145
|
+
return f"Container started: {container.short_id} ({container.name})"
|
|
146
|
+
output = container.decode("utf-8", errors="replace") if isinstance(container, bytes) else str(container)
|
|
147
|
+
return output
|
|
148
|
+
except Exception as e:
|
|
149
|
+
return format_error(e)
|
|
150
|
+
|
|
151
|
+
@mcp.tool()
|
|
152
|
+
def container_stats(container_id: str) -> str:
|
|
153
|
+
"""Get CPU and memory usage stats for a running container."""
|
|
154
|
+
try:
|
|
155
|
+
client = get_client()
|
|
156
|
+
container = client.containers.get(container_id)
|
|
157
|
+
stats = container.stats(stream=False)
|
|
158
|
+
|
|
159
|
+
cpu_delta = stats["cpu_stats"]["cpu_usage"]["total_usage"] - \
|
|
160
|
+
stats["precpu_stats"]["cpu_usage"]["total_usage"]
|
|
161
|
+
system_delta = stats["cpu_stats"].get("system_cpu_usage", 0) - \
|
|
162
|
+
stats["precpu_stats"].get("system_cpu_usage", 0)
|
|
163
|
+
num_cpus = stats["cpu_stats"].get("online_cpus") or \
|
|
164
|
+
len(stats["cpu_stats"]["cpu_usage"].get("percpu_usage", [1]))
|
|
165
|
+
cpu_pct = (cpu_delta / system_delta * num_cpus * 100.0) if system_delta > 0 else 0.0
|
|
166
|
+
|
|
167
|
+
mem = stats["memory_stats"]
|
|
168
|
+
mem_usage = mem.get("usage", 0)
|
|
169
|
+
mem_limit = mem.get("limit", 1)
|
|
170
|
+
mem_pct = mem_usage / mem_limit * 100.0
|
|
171
|
+
|
|
172
|
+
return (
|
|
173
|
+
f"Container: {container.name} ({container.short_id})\n"
|
|
174
|
+
f"CPU: {cpu_pct:.2f}%\n"
|
|
175
|
+
f"Memory: {mem_usage / (1024**2):.1f} MB / "
|
|
176
|
+
f"{mem_limit / (1024**2):.1f} MB ({mem_pct:.1f}%)"
|
|
177
|
+
)
|
|
178
|
+
except Exception as e:
|
|
179
|
+
return format_error(e)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Image management tools."""
|
|
2
|
+
|
|
3
|
+
from docker_mcp.client import get_client
|
|
4
|
+
from docker_mcp.utils import format_error
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def register(mcp):
|
|
8
|
+
@mcp.tool()
|
|
9
|
+
def list_images() -> str:
|
|
10
|
+
"""List all Docker images."""
|
|
11
|
+
try:
|
|
12
|
+
client = get_client()
|
|
13
|
+
images = client.images.list()
|
|
14
|
+
if not images:
|
|
15
|
+
return "No images found."
|
|
16
|
+
lines = []
|
|
17
|
+
for img in images:
|
|
18
|
+
tags = ", ".join(img.tags) if img.tags else "untagged"
|
|
19
|
+
size_mb = img.attrs.get("Size", 0) / (1024 * 1024)
|
|
20
|
+
lines.append(f"ID: {img.short_id} | Tags: {tags} | Size: {size_mb:.1f} MB")
|
|
21
|
+
return "\n".join(lines)
|
|
22
|
+
except Exception as e:
|
|
23
|
+
return format_error(e)
|
|
24
|
+
|
|
25
|
+
@mcp.tool()
|
|
26
|
+
def pull_image(image: str, tag: str = "latest") -> str:
|
|
27
|
+
"""Pull an image from a registry."""
|
|
28
|
+
try:
|
|
29
|
+
client = get_client()
|
|
30
|
+
img = client.images.pull(image, tag=tag)
|
|
31
|
+
tags = ", ".join(img.tags) if img.tags else "untagged"
|
|
32
|
+
return f"Pulled: {tags}"
|
|
33
|
+
except Exception as e:
|
|
34
|
+
return format_error(e)
|
|
35
|
+
|
|
36
|
+
@mcp.tool()
|
|
37
|
+
def build_image(path: str, tag: str | None = None, dockerfile: str = "Dockerfile") -> str:
|
|
38
|
+
"""Build an image from a Dockerfile. path is the build context directory."""
|
|
39
|
+
try:
|
|
40
|
+
client = get_client()
|
|
41
|
+
kwargs = {"path": path, "dockerfile": dockerfile}
|
|
42
|
+
if tag:
|
|
43
|
+
kwargs["tag"] = tag
|
|
44
|
+
image, build_logs = client.images.build(**kwargs)
|
|
45
|
+
log_output = ""
|
|
46
|
+
for chunk in build_logs:
|
|
47
|
+
if "stream" in chunk:
|
|
48
|
+
log_output += chunk["stream"]
|
|
49
|
+
tags = ", ".join(image.tags) if image.tags else image.short_id
|
|
50
|
+
return f"Built: {tags}\n{log_output}"
|
|
51
|
+
except Exception as e:
|
|
52
|
+
return format_error(e)
|
|
53
|
+
|
|
54
|
+
@mcp.tool()
|
|
55
|
+
def remove_image(image: str, force: bool = False) -> str:
|
|
56
|
+
"""Remove an image by name or ID."""
|
|
57
|
+
try:
|
|
58
|
+
client = get_client()
|
|
59
|
+
client.images.remove(image, force=force)
|
|
60
|
+
return f"Image {image} removed."
|
|
61
|
+
except Exception as e:
|
|
62
|
+
return format_error(e)
|
|
63
|
+
|
|
64
|
+
@mcp.tool()
|
|
65
|
+
def inspect_image(image: str) -> str:
|
|
66
|
+
"""Get detailed information about an image."""
|
|
67
|
+
try:
|
|
68
|
+
import json
|
|
69
|
+
client = get_client()
|
|
70
|
+
img = client.images.get(image)
|
|
71
|
+
return json.dumps(img.attrs, indent=2, default=str)
|
|
72
|
+
except Exception as e:
|
|
73
|
+
return format_error(e)
|
|
74
|
+
|
|
75
|
+
@mcp.tool()
|
|
76
|
+
def tag_image(image: str, repository: str, tag: str = "latest") -> str:
|
|
77
|
+
"""Tag an image with a new name. image is the source image name or ID."""
|
|
78
|
+
try:
|
|
79
|
+
client = get_client()
|
|
80
|
+
img = client.images.get(image)
|
|
81
|
+
img.tag(repository, tag=tag)
|
|
82
|
+
return f"Tagged {image} as {repository}:{tag}"
|
|
83
|
+
except Exception as e:
|
|
84
|
+
return format_error(e)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Network management tools."""
|
|
2
|
+
|
|
3
|
+
from docker_mcp.client import get_client
|
|
4
|
+
from docker_mcp.utils import format_error
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def register(mcp):
|
|
8
|
+
@mcp.tool()
|
|
9
|
+
def list_networks() -> str:
|
|
10
|
+
"""List all Docker networks."""
|
|
11
|
+
try:
|
|
12
|
+
client = get_client()
|
|
13
|
+
networks = client.networks.list()
|
|
14
|
+
if not networks:
|
|
15
|
+
return "No networks found."
|
|
16
|
+
lines = []
|
|
17
|
+
for n in networks:
|
|
18
|
+
driver = n.attrs.get("Driver", "unknown")
|
|
19
|
+
lines.append(f"ID: {n.short_id} | Name: {n.name} | Driver: {driver}")
|
|
20
|
+
return "\n".join(lines)
|
|
21
|
+
except Exception as e:
|
|
22
|
+
return format_error(e)
|
|
23
|
+
|
|
24
|
+
@mcp.tool()
|
|
25
|
+
def create_network(name: str, driver: str = "bridge") -> str:
|
|
26
|
+
"""Create a new network."""
|
|
27
|
+
try:
|
|
28
|
+
client = get_client()
|
|
29
|
+
network = client.networks.create(name=name, driver=driver)
|
|
30
|
+
return f"Network created: {network.name} ({network.short_id})"
|
|
31
|
+
except Exception as e:
|
|
32
|
+
return format_error(e)
|
|
33
|
+
|
|
34
|
+
@mcp.tool()
|
|
35
|
+
def remove_network(name: str) -> str:
|
|
36
|
+
"""Remove a network by name or ID."""
|
|
37
|
+
try:
|
|
38
|
+
client = get_client()
|
|
39
|
+
network = client.networks.get(name)
|
|
40
|
+
network.remove()
|
|
41
|
+
return f"Network {name} removed."
|
|
42
|
+
except Exception as e:
|
|
43
|
+
return format_error(e)
|
|
44
|
+
|
|
45
|
+
@mcp.tool()
|
|
46
|
+
def inspect_network(name: str) -> str:
|
|
47
|
+
"""Get detailed information about a network."""
|
|
48
|
+
try:
|
|
49
|
+
import json
|
|
50
|
+
client = get_client()
|
|
51
|
+
network = client.networks.get(name)
|
|
52
|
+
return json.dumps(network.attrs, indent=2, default=str)
|
|
53
|
+
except Exception as e:
|
|
54
|
+
return format_error(e)
|
|
55
|
+
|
|
56
|
+
@mcp.tool()
|
|
57
|
+
def connect_container_to_network(network_name: str, container_id: str) -> str:
|
|
58
|
+
"""Connect a container to a network."""
|
|
59
|
+
try:
|
|
60
|
+
client = get_client()
|
|
61
|
+
network = client.networks.get(network_name)
|
|
62
|
+
network.connect(container_id)
|
|
63
|
+
return f"Container {container_id} connected to network {network_name}."
|
|
64
|
+
except Exception as e:
|
|
65
|
+
return format_error(e)
|
|
66
|
+
|
|
67
|
+
@mcp.tool()
|
|
68
|
+
def disconnect_container_from_network(network_name: str, container_id: str) -> str:
|
|
69
|
+
"""Disconnect a container from a network."""
|
|
70
|
+
try:
|
|
71
|
+
client = get_client()
|
|
72
|
+
network = client.networks.get(network_name)
|
|
73
|
+
network.disconnect(container_id)
|
|
74
|
+
return f"Container {container_id} disconnected from network {network_name}."
|
|
75
|
+
except Exception as e:
|
|
76
|
+
return format_error(e)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""System-level Docker tools (info, prune)."""
|
|
2
|
+
|
|
3
|
+
from docker_mcp.client import get_client
|
|
4
|
+
from docker_mcp.utils import format_error
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def register(mcp):
|
|
8
|
+
@mcp.tool()
|
|
9
|
+
def system_info() -> str:
|
|
10
|
+
"""Get Docker system information: version, disk usage, and server info."""
|
|
11
|
+
try:
|
|
12
|
+
client = get_client()
|
|
13
|
+
version = client.version()
|
|
14
|
+
info = client.info()
|
|
15
|
+
df = client.df()
|
|
16
|
+
|
|
17
|
+
images_size = sum(img.get("Size", 0) for img in df.get("Images", []))
|
|
18
|
+
containers_size = sum(c.get("SizeRw", 0) for c in df.get("Containers", []))
|
|
19
|
+
volumes_size = sum(
|
|
20
|
+
v.get("UsageData", {}).get("Size", 0) for v in df.get("Volumes", [])
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
f"Docker version: {version.get('Version', 'unknown')}\n"
|
|
25
|
+
f"API version: {version.get('ApiVersion', 'unknown')}\n"
|
|
26
|
+
f"OS: {info.get('OperatingSystem', 'unknown')}\n"
|
|
27
|
+
f"Containers: {info.get('Containers', 0)} "
|
|
28
|
+
f"(running: {info.get('ContainersRunning', 0)}, "
|
|
29
|
+
f"stopped: {info.get('ContainersStopped', 0)})\n"
|
|
30
|
+
f"Images: {info.get('Images', 0)}\n"
|
|
31
|
+
f"Disk usage — images: {images_size / (1024**2):.1f} MB, "
|
|
32
|
+
f"containers: {containers_size / (1024**2):.1f} MB, "
|
|
33
|
+
f"volumes: {volumes_size / (1024**2):.1f} MB"
|
|
34
|
+
)
|
|
35
|
+
except Exception as e:
|
|
36
|
+
return format_error(e)
|
|
37
|
+
|
|
38
|
+
@mcp.tool()
|
|
39
|
+
def prune_containers(filters: dict | None = None) -> str:
|
|
40
|
+
"""Remove all stopped containers. Optionally pass filters dict."""
|
|
41
|
+
try:
|
|
42
|
+
client = get_client()
|
|
43
|
+
result = client.containers.prune(filters=filters or {})
|
|
44
|
+
deleted = result.get("ContainersDeleted") or []
|
|
45
|
+
space = result.get("SpaceReclaimed", 0)
|
|
46
|
+
return (
|
|
47
|
+
f"Removed {len(deleted)} container(s). "
|
|
48
|
+
f"Space reclaimed: {space / (1024**2):.1f} MB"
|
|
49
|
+
)
|
|
50
|
+
except Exception as e:
|
|
51
|
+
return format_error(e)
|
|
52
|
+
|
|
53
|
+
@mcp.tool()
|
|
54
|
+
def prune_images(filters: dict | None = None) -> str:
|
|
55
|
+
"""Remove unused (dangling) images. Optionally pass filters dict."""
|
|
56
|
+
try:
|
|
57
|
+
client = get_client()
|
|
58
|
+
result = client.images.prune(filters=filters or {})
|
|
59
|
+
deleted = result.get("ImagesDeleted") or []
|
|
60
|
+
space = result.get("SpaceReclaimed", 0)
|
|
61
|
+
return (
|
|
62
|
+
f"Removed {len(deleted)} image layer(s). "
|
|
63
|
+
f"Space reclaimed: {space / (1024**2):.1f} MB"
|
|
64
|
+
)
|
|
65
|
+
except Exception as e:
|
|
66
|
+
return format_error(e)
|
|
67
|
+
|
|
68
|
+
@mcp.tool()
|
|
69
|
+
def prune_volumes(filters: dict | None = None) -> str:
|
|
70
|
+
"""Remove unused volumes. Optionally pass filters dict."""
|
|
71
|
+
try:
|
|
72
|
+
client = get_client()
|
|
73
|
+
result = client.volumes.prune(filters=filters or {})
|
|
74
|
+
deleted = result.get("VolumesDeleted") or []
|
|
75
|
+
space = result.get("SpaceReclaimed", 0)
|
|
76
|
+
return (
|
|
77
|
+
f"Removed {len(deleted)} volume(s). "
|
|
78
|
+
f"Space reclaimed: {space / (1024**2):.1f} MB"
|
|
79
|
+
)
|
|
80
|
+
except Exception as e:
|
|
81
|
+
return format_error(e)
|
|
82
|
+
|
|
83
|
+
@mcp.tool()
|
|
84
|
+
def prune_networks(filters: dict | None = None) -> str:
|
|
85
|
+
"""Remove unused networks. Optionally pass filters dict."""
|
|
86
|
+
try:
|
|
87
|
+
client = get_client()
|
|
88
|
+
result = client.networks.prune(filters=filters or {})
|
|
89
|
+
deleted = result.get("NetworksDeleted") or []
|
|
90
|
+
return f"Removed {len(deleted)} network(s): {', '.join(deleted) or 'none'}"
|
|
91
|
+
except Exception as e:
|
|
92
|
+
return format_error(e)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Volume management tools."""
|
|
2
|
+
|
|
3
|
+
from docker_mcp.client import get_client
|
|
4
|
+
from docker_mcp.utils import format_error
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def register(mcp):
|
|
8
|
+
@mcp.tool()
|
|
9
|
+
def list_volumes() -> str:
|
|
10
|
+
"""List all Docker volumes."""
|
|
11
|
+
try:
|
|
12
|
+
client = get_client()
|
|
13
|
+
volumes = client.volumes.list()
|
|
14
|
+
if not volumes:
|
|
15
|
+
return "No volumes found."
|
|
16
|
+
lines = []
|
|
17
|
+
for v in volumes:
|
|
18
|
+
driver = v.attrs.get("Driver", "unknown")
|
|
19
|
+
lines.append(f"Name: {v.name} | Driver: {driver}")
|
|
20
|
+
return "\n".join(lines)
|
|
21
|
+
except Exception as e:
|
|
22
|
+
return format_error(e)
|
|
23
|
+
|
|
24
|
+
@mcp.tool()
|
|
25
|
+
def create_volume(name: str, driver: str = "local") -> str:
|
|
26
|
+
"""Create a new volume."""
|
|
27
|
+
try:
|
|
28
|
+
client = get_client()
|
|
29
|
+
volume = client.volumes.create(name=name, driver=driver)
|
|
30
|
+
return f"Volume created: {volume.name}"
|
|
31
|
+
except Exception as e:
|
|
32
|
+
return format_error(e)
|
|
33
|
+
|
|
34
|
+
@mcp.tool()
|
|
35
|
+
def remove_volume(name: str, force: bool = False) -> str:
|
|
36
|
+
"""Remove a volume by name."""
|
|
37
|
+
try:
|
|
38
|
+
client = get_client()
|
|
39
|
+
volume = client.volumes.get(name)
|
|
40
|
+
volume.remove(force=force)
|
|
41
|
+
return f"Volume {name} removed."
|
|
42
|
+
except Exception as e:
|
|
43
|
+
return format_error(e)
|
|
44
|
+
|
|
45
|
+
@mcp.tool()
|
|
46
|
+
def inspect_volume(name: str) -> str:
|
|
47
|
+
"""Get detailed information about a volume."""
|
|
48
|
+
try:
|
|
49
|
+
import json
|
|
50
|
+
client = get_client()
|
|
51
|
+
volume = client.volumes.get(name)
|
|
52
|
+
return json.dumps(volume.attrs, indent=2, default=str)
|
|
53
|
+
except Exception as e:
|
|
54
|
+
return format_error(e)
|
docker_mcp/utils.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Shared helpers for Docker MCP tools."""
|
|
2
|
+
|
|
3
|
+
from docker.errors import DockerException, NotFound, APIError
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def format_error(e: Exception) -> str:
|
|
7
|
+
"""Format a Docker exception into a user-friendly message."""
|
|
8
|
+
if isinstance(e, NotFound):
|
|
9
|
+
return f"Not found: {e.explanation}"
|
|
10
|
+
if isinstance(e, APIError):
|
|
11
|
+
return f"Docker API error: {e.explanation}"
|
|
12
|
+
if isinstance(e, DockerException):
|
|
13
|
+
return f"Docker error: {e}"
|
|
14
|
+
return f"Error: {e}"
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: docker-mcp-server
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Docker MCP server exposing Docker operations as MCP tools
|
|
5
|
+
Requires-Python: >=3.11,<4.0
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
11
|
+
Requires-Dist: docker (>=7.0,<8.0)
|
|
12
|
+
Requires-Dist: mcp (>=1.12,<2.0)
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# Docker MCP Server
|
|
16
|
+
|
|
17
|
+
A Model Context Protocol (MCP) server that exposes Docker operations as tools — manage containers, images, compose services, volumes, and networks directly from Claude.
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install docker-mcp
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Or with `uvx` (no install needed):
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
uvx docker-mcp
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## MCP Configuration
|
|
32
|
+
|
|
33
|
+
### Claude Desktop
|
|
34
|
+
|
|
35
|
+
Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"mcpServers": {
|
|
40
|
+
"docker": {
|
|
41
|
+
"command": "uvx",
|
|
42
|
+
"args": ["docker-mcp"]
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Claude Code
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
claude mcp add docker -- uvx docker-mcp
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Or add manually to your project's `.claude/settings.json`:
|
|
55
|
+
|
|
56
|
+
```json
|
|
57
|
+
{
|
|
58
|
+
"mcpServers": {
|
|
59
|
+
"docker": {
|
|
60
|
+
"command": "uvx",
|
|
61
|
+
"args": ["docker-mcp"]
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Docker (SSE mode)
|
|
68
|
+
|
|
69
|
+
Run the server as a container and connect over HTTP:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
docker run -d \
|
|
73
|
+
-p 8000:8000 \
|
|
74
|
+
-v /var/run/docker.sock:/var/run/docker.sock \
|
|
75
|
+
ghcr.io/firasmosbahi/docker-mcp:latest
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Then configure your MCP client to connect to `http://localhost:8000/sse`.
|
|
79
|
+
|
|
80
|
+
## Available Tools
|
|
81
|
+
|
|
82
|
+
### Containers
|
|
83
|
+
|
|
84
|
+
| Tool | Description |
|
|
85
|
+
|------|-------------|
|
|
86
|
+
| `list_containers` | List containers (`all=true` includes stopped) |
|
|
87
|
+
| `create_container` | Create a container without starting it |
|
|
88
|
+
| `run_container` | Create and start a container in one step |
|
|
89
|
+
| `start_container` | Start a stopped container |
|
|
90
|
+
| `stop_container` | Stop a running container |
|
|
91
|
+
| `restart_container` | Restart a container |
|
|
92
|
+
| `remove_container` | Remove a container (`force=true` for running) |
|
|
93
|
+
| `get_container_logs` | Get container logs (configurable tail lines) |
|
|
94
|
+
| `exec_in_container` | Execute a command inside a running container |
|
|
95
|
+
| `inspect_container` | Get full container details as JSON |
|
|
96
|
+
| `container_stats` | Get CPU and memory usage for a container |
|
|
97
|
+
|
|
98
|
+
**Examples:**
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
List all containers including stopped ones
|
|
102
|
+
→ list_containers(all=true)
|
|
103
|
+
|
|
104
|
+
Run an nginx container on port 8080
|
|
105
|
+
→ run_container(image="nginx", name="my-nginx", ports={"80/tcp": 8080}, detach=true)
|
|
106
|
+
|
|
107
|
+
Get the last 50 lines of logs from my-app
|
|
108
|
+
→ get_container_logs(container_id="my-app", tail=50)
|
|
109
|
+
|
|
110
|
+
Execute a command inside a container
|
|
111
|
+
→ exec_in_container(container_id="my-app", command="env")
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Images
|
|
115
|
+
|
|
116
|
+
| Tool | Description |
|
|
117
|
+
|------|-------------|
|
|
118
|
+
| `list_images` | List all local images with size |
|
|
119
|
+
| `pull_image` | Pull an image from a registry |
|
|
120
|
+
| `build_image` | Build an image from a Dockerfile |
|
|
121
|
+
| `tag_image` | Tag an image with a new name |
|
|
122
|
+
| `remove_image` | Remove an image |
|
|
123
|
+
| `inspect_image` | Get full image details as JSON |
|
|
124
|
+
|
|
125
|
+
**Examples:**
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
Pull the latest Alpine image
|
|
129
|
+
→ pull_image(image="alpine", tag="latest")
|
|
130
|
+
|
|
131
|
+
Build an image from the current directory
|
|
132
|
+
→ build_image(path="/my/project", tag="my-app:v1")
|
|
133
|
+
|
|
134
|
+
Tag an image for pushing to a registry
|
|
135
|
+
→ tag_image(image="my-app:v1", repository="myrepo/my-app", tag="v1.0.0")
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Compose
|
|
139
|
+
|
|
140
|
+
| Tool | Description |
|
|
141
|
+
|------|-------------|
|
|
142
|
+
| `compose_up` | Start services (detached by default) |
|
|
143
|
+
| `compose_down` | Stop and remove services |
|
|
144
|
+
| `compose_build` | Build or rebuild services |
|
|
145
|
+
| `compose_restart` | Restart services |
|
|
146
|
+
| `compose_ps` | List service status |
|
|
147
|
+
| `compose_logs` | Get service logs |
|
|
148
|
+
|
|
149
|
+
**Examples:**
|
|
150
|
+
|
|
151
|
+
```
|
|
152
|
+
Start all services in a project
|
|
153
|
+
→ compose_up(project_directory="/my/project")
|
|
154
|
+
|
|
155
|
+
Rebuild a specific service without cache
|
|
156
|
+
→ compose_build(project_directory="/my/project", service="api", no_cache=true)
|
|
157
|
+
|
|
158
|
+
Get logs for the web service
|
|
159
|
+
→ compose_logs(project_directory="/my/project", service="web", tail=100)
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Volumes
|
|
163
|
+
|
|
164
|
+
| Tool | Description |
|
|
165
|
+
|------|-------------|
|
|
166
|
+
| `list_volumes` | List all volumes |
|
|
167
|
+
| `create_volume` | Create a named volume |
|
|
168
|
+
| `remove_volume` | Remove a volume |
|
|
169
|
+
| `inspect_volume` | Get volume details |
|
|
170
|
+
|
|
171
|
+
### Networks
|
|
172
|
+
|
|
173
|
+
| Tool | Description |
|
|
174
|
+
|------|-------------|
|
|
175
|
+
| `list_networks` | List all networks |
|
|
176
|
+
| `create_network` | Create a network |
|
|
177
|
+
| `remove_network` | Remove a network |
|
|
178
|
+
| `inspect_network` | Get network details |
|
|
179
|
+
| `connect_container_to_network` | Connect a container to a network |
|
|
180
|
+
| `disconnect_container_from_network` | Disconnect a container from a network |
|
|
181
|
+
|
|
182
|
+
### System
|
|
183
|
+
|
|
184
|
+
| Tool | Description |
|
|
185
|
+
|------|-------------|
|
|
186
|
+
| `system_info` | Docker version, OS, container/image counts, disk usage |
|
|
187
|
+
| `prune_containers` | Remove all stopped containers |
|
|
188
|
+
| `prune_images` | Remove unused (dangling) images |
|
|
189
|
+
| `prune_volumes` | Remove unused volumes |
|
|
190
|
+
| `prune_networks` | Remove unused networks |
|
|
191
|
+
|
|
192
|
+
**Examples:**
|
|
193
|
+
|
|
194
|
+
```
|
|
195
|
+
Check Docker system status and disk usage
|
|
196
|
+
→ system_info()
|
|
197
|
+
|
|
198
|
+
Free up space by removing unused resources
|
|
199
|
+
→ prune_containers()
|
|
200
|
+
→ prune_images()
|
|
201
|
+
→ prune_volumes()
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Transport Modes
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
# stdio (default) — for MCP clients that manage the process
|
|
208
|
+
docker-mcp
|
|
209
|
+
|
|
210
|
+
# SSE — for HTTP-based MCP clients
|
|
211
|
+
docker-mcp --transport sse --port 8000
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## Requirements
|
|
215
|
+
|
|
216
|
+
- Python 3.11+
|
|
217
|
+
- Docker daemon running and accessible (Docker Desktop or Docker Engine)
|
|
218
|
+
|
|
219
|
+
## Development
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
git clone https://github.com/firasmosbahi/docker-mcp
|
|
223
|
+
cd docker-mcp
|
|
224
|
+
poetry install
|
|
225
|
+
poetry run pytest
|
|
226
|
+
```
|
|
227
|
+
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
docker_mcp/__init__.py,sha256=UC9qWtcWABjjeYSMrlqbsUcpuInIpMBPptSBmC3waEw,67
|
|
2
|
+
docker_mcp/client.py,sha256=ZmdVAUm6LGqPaWpowXeeaAraCRRcsFWdhsjJ5cNPUkQ,281
|
|
3
|
+
docker_mcp/server.py,sha256=Zrl4mpjnWMXP0M_BshRFUzTTrhD3U_N3ZUP2_p-Tl_8,792
|
|
4
|
+
docker_mcp/tools/__init__.py,sha256=eLzcpDQbBYHZstkk2QY2MKIfdDOCbQVesX2OMBqHxuk,671
|
|
5
|
+
docker_mcp/tools/compose.py,sha256=Yu9nrvJqjx-znRfpOQzCdzmmdmb3BjJZHSqHq620r14,3135
|
|
6
|
+
docker_mcp/tools/containers.py,sha256=NV_iPnsQftRZUVQlQiPzNbpQCUaJ4fh-6EkllHPP-es,7078
|
|
7
|
+
docker_mcp/tools/images.py,sha256=gQFSusM-ZO766VaIzQIuKdbHrq1ptXLuMZHL2sK6SbM,3047
|
|
8
|
+
docker_mcp/tools/networks.py,sha256=2EeXx8xSPac7aJYt5vuvSorBp6ostaH1QhTsS2fCxGY,2683
|
|
9
|
+
docker_mcp/tools/system.py,sha256=M-AMTd9CQHD58b_vHVGTZDjIq5u-UhdCeNKKEnyHZpw,3747
|
|
10
|
+
docker_mcp/tools/volumes.py,sha256=J7rHMHJ88xRO75_-JmTefU0KNqWZL2oA7lP4MxVy3KE,1739
|
|
11
|
+
docker_mcp/utils.py,sha256=_7lCCkP02lV5jNhGQhP_h0RwOrA4PWIw2niuDk30bSw,474
|
|
12
|
+
docker_mcp_server-0.1.0.dist-info/METADATA,sha256=ObmnB8aH7rQjxuRD7ikN6WbvO6b1tRLH7nm2TCm_LuY,5695
|
|
13
|
+
docker_mcp_server-0.1.0.dist-info/WHEEL,sha256=kJCRJT_g0adfAJzTx2GUMmS80rTJIVHRCfG0DQgLq3o,88
|
|
14
|
+
docker_mcp_server-0.1.0.dist-info/entry_points.txt,sha256=PPn1r_ZVhzThkGy3OGBdwL2sWf7Y7DyNZ2bg99yK7SE,53
|
|
15
|
+
docker_mcp_server-0.1.0.dist-info/RECORD,,
|