docker-mcp-server 0.1.0__tar.gz

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.
@@ -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,212 @@
1
+ # Docker MCP Server
2
+
3
+ A Model Context Protocol (MCP) server that exposes Docker operations as tools — manage containers, images, compose services, volumes, and networks directly from Claude.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install docker-mcp
9
+ ```
10
+
11
+ Or with `uvx` (no install needed):
12
+
13
+ ```bash
14
+ uvx docker-mcp
15
+ ```
16
+
17
+ ## MCP Configuration
18
+
19
+ ### Claude Desktop
20
+
21
+ Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
22
+
23
+ ```json
24
+ {
25
+ "mcpServers": {
26
+ "docker": {
27
+ "command": "uvx",
28
+ "args": ["docker-mcp"]
29
+ }
30
+ }
31
+ }
32
+ ```
33
+
34
+ ### Claude Code
35
+
36
+ ```bash
37
+ claude mcp add docker -- uvx docker-mcp
38
+ ```
39
+
40
+ Or add manually to your project's `.claude/settings.json`:
41
+
42
+ ```json
43
+ {
44
+ "mcpServers": {
45
+ "docker": {
46
+ "command": "uvx",
47
+ "args": ["docker-mcp"]
48
+ }
49
+ }
50
+ }
51
+ ```
52
+
53
+ ### Docker (SSE mode)
54
+
55
+ Run the server as a container and connect over HTTP:
56
+
57
+ ```bash
58
+ docker run -d \
59
+ -p 8000:8000 \
60
+ -v /var/run/docker.sock:/var/run/docker.sock \
61
+ ghcr.io/firasmosbahi/docker-mcp:latest
62
+ ```
63
+
64
+ Then configure your MCP client to connect to `http://localhost:8000/sse`.
65
+
66
+ ## Available Tools
67
+
68
+ ### Containers
69
+
70
+ | Tool | Description |
71
+ |------|-------------|
72
+ | `list_containers` | List containers (`all=true` includes stopped) |
73
+ | `create_container` | Create a container without starting it |
74
+ | `run_container` | Create and start a container in one step |
75
+ | `start_container` | Start a stopped container |
76
+ | `stop_container` | Stop a running container |
77
+ | `restart_container` | Restart a container |
78
+ | `remove_container` | Remove a container (`force=true` for running) |
79
+ | `get_container_logs` | Get container logs (configurable tail lines) |
80
+ | `exec_in_container` | Execute a command inside a running container |
81
+ | `inspect_container` | Get full container details as JSON |
82
+ | `container_stats` | Get CPU and memory usage for a container |
83
+
84
+ **Examples:**
85
+
86
+ ```
87
+ List all containers including stopped ones
88
+ → list_containers(all=true)
89
+
90
+ Run an nginx container on port 8080
91
+ → run_container(image="nginx", name="my-nginx", ports={"80/tcp": 8080}, detach=true)
92
+
93
+ Get the last 50 lines of logs from my-app
94
+ → get_container_logs(container_id="my-app", tail=50)
95
+
96
+ Execute a command inside a container
97
+ → exec_in_container(container_id="my-app", command="env")
98
+ ```
99
+
100
+ ### Images
101
+
102
+ | Tool | Description |
103
+ |------|-------------|
104
+ | `list_images` | List all local images with size |
105
+ | `pull_image` | Pull an image from a registry |
106
+ | `build_image` | Build an image from a Dockerfile |
107
+ | `tag_image` | Tag an image with a new name |
108
+ | `remove_image` | Remove an image |
109
+ | `inspect_image` | Get full image details as JSON |
110
+
111
+ **Examples:**
112
+
113
+ ```
114
+ Pull the latest Alpine image
115
+ → pull_image(image="alpine", tag="latest")
116
+
117
+ Build an image from the current directory
118
+ → build_image(path="/my/project", tag="my-app:v1")
119
+
120
+ Tag an image for pushing to a registry
121
+ → tag_image(image="my-app:v1", repository="myrepo/my-app", tag="v1.0.0")
122
+ ```
123
+
124
+ ### Compose
125
+
126
+ | Tool | Description |
127
+ |------|-------------|
128
+ | `compose_up` | Start services (detached by default) |
129
+ | `compose_down` | Stop and remove services |
130
+ | `compose_build` | Build or rebuild services |
131
+ | `compose_restart` | Restart services |
132
+ | `compose_ps` | List service status |
133
+ | `compose_logs` | Get service logs |
134
+
135
+ **Examples:**
136
+
137
+ ```
138
+ Start all services in a project
139
+ → compose_up(project_directory="/my/project")
140
+
141
+ Rebuild a specific service without cache
142
+ → compose_build(project_directory="/my/project", service="api", no_cache=true)
143
+
144
+ Get logs for the web service
145
+ → compose_logs(project_directory="/my/project", service="web", tail=100)
146
+ ```
147
+
148
+ ### Volumes
149
+
150
+ | Tool | Description |
151
+ |------|-------------|
152
+ | `list_volumes` | List all volumes |
153
+ | `create_volume` | Create a named volume |
154
+ | `remove_volume` | Remove a volume |
155
+ | `inspect_volume` | Get volume details |
156
+
157
+ ### Networks
158
+
159
+ | Tool | Description |
160
+ |------|-------------|
161
+ | `list_networks` | List all networks |
162
+ | `create_network` | Create a network |
163
+ | `remove_network` | Remove a network |
164
+ | `inspect_network` | Get network details |
165
+ | `connect_container_to_network` | Connect a container to a network |
166
+ | `disconnect_container_from_network` | Disconnect a container from a network |
167
+
168
+ ### System
169
+
170
+ | Tool | Description |
171
+ |------|-------------|
172
+ | `system_info` | Docker version, OS, container/image counts, disk usage |
173
+ | `prune_containers` | Remove all stopped containers |
174
+ | `prune_images` | Remove unused (dangling) images |
175
+ | `prune_volumes` | Remove unused volumes |
176
+ | `prune_networks` | Remove unused networks |
177
+
178
+ **Examples:**
179
+
180
+ ```
181
+ Check Docker system status and disk usage
182
+ → system_info()
183
+
184
+ Free up space by removing unused resources
185
+ → prune_containers()
186
+ → prune_images()
187
+ → prune_volumes()
188
+ ```
189
+
190
+ ## Transport Modes
191
+
192
+ ```bash
193
+ # stdio (default) — for MCP clients that manage the process
194
+ docker-mcp
195
+
196
+ # SSE — for HTTP-based MCP clients
197
+ docker-mcp --transport sse --port 8000
198
+ ```
199
+
200
+ ## Requirements
201
+
202
+ - Python 3.11+
203
+ - Docker daemon running and accessible (Docker Desktop or Docker Engine)
204
+
205
+ ## Development
206
+
207
+ ```bash
208
+ git clone https://github.com/firasmosbahi/docker-mcp
209
+ cd docker-mcp
210
+ poetry install
211
+ poetry run pytest
212
+ ```
@@ -0,0 +1,31 @@
1
+ [tool.poetry]
2
+ name = "docker-mcp-server"
3
+ version = "0.1.0"
4
+ description = "Docker MCP server exposing Docker operations as MCP tools"
5
+ authors = []
6
+ readme = "README.md"
7
+ packages = [{include = "docker_mcp", from = "src"}]
8
+
9
+ [tool.poetry.scripts]
10
+ docker-mcp = "docker_mcp.server:main"
11
+
12
+ [tool.poetry.dependencies]
13
+ python = "^3.11"
14
+ mcp = "^1.12"
15
+ docker = "^7.0"
16
+
17
+ [tool.poetry.group.dev.dependencies]
18
+ pytest = "^8.0"
19
+ pytest-asyncio = "^0.24"
20
+ ruff = "^0.8"
21
+
22
+ [build-system]
23
+ requires = ["poetry-core"]
24
+ build-backend = "poetry.core.masonry.api"
25
+
26
+ [tool.pytest.ini_options]
27
+ asyncio_mode = "auto"
28
+
29
+ [tool.ruff]
30
+ target-version = "py311"
31
+ line-length = 100
@@ -0,0 +1 @@
1
+ """Docker MCP Server — Expose Docker operations as MCP tools."""
@@ -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
@@ -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)
@@ -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}"