container-manager-mcp 0.0.10__tar.gz → 0.0.11__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.
- {container_manager_mcp-0.0.10/container_manager_mcp.egg-info → container_manager_mcp-0.0.11}/PKG-INFO +2 -2
- {container_manager_mcp-0.0.10 → container_manager_mcp-0.0.11}/README.md +1 -1
- {container_manager_mcp-0.0.10 → container_manager_mcp-0.0.11}/container_manager_mcp/container_manager.py +496 -39
- {container_manager_mcp-0.0.10 → container_manager_mcp-0.0.11/container_manager_mcp.egg-info}/PKG-INFO +2 -2
- {container_manager_mcp-0.0.10 → container_manager_mcp-0.0.11}/pyproject.toml +1 -1
- {container_manager_mcp-0.0.10 → container_manager_mcp-0.0.11}/LICENSE +0 -0
- {container_manager_mcp-0.0.10 → container_manager_mcp-0.0.11}/MANIFEST.in +0 -0
- {container_manager_mcp-0.0.10 → container_manager_mcp-0.0.11}/container_manager_mcp/__init__.py +0 -0
- {container_manager_mcp-0.0.10 → container_manager_mcp-0.0.11}/container_manager_mcp/__main__.py +0 -0
- {container_manager_mcp-0.0.10 → container_manager_mcp-0.0.11}/container_manager_mcp/container_manager_mcp.py +0 -0
- {container_manager_mcp-0.0.10 → container_manager_mcp-0.0.11}/container_manager_mcp.egg-info/SOURCES.txt +0 -0
- {container_manager_mcp-0.0.10 → container_manager_mcp-0.0.11}/container_manager_mcp.egg-info/dependency_links.txt +0 -0
- {container_manager_mcp-0.0.10 → container_manager_mcp-0.0.11}/container_manager_mcp.egg-info/entry_points.txt +0 -0
- {container_manager_mcp-0.0.10 → container_manager_mcp-0.0.11}/container_manager_mcp.egg-info/requires.txt +0 -0
- {container_manager_mcp-0.0.10 → container_manager_mcp-0.0.11}/container_manager_mcp.egg-info/top_level.txt +0 -0
- {container_manager_mcp-0.0.10 → container_manager_mcp-0.0.11}/requirements.txt +0 -0
- {container_manager_mcp-0.0.10 → container_manager_mcp-0.0.11}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: container-manager-mcp
|
3
|
-
Version: 0.0.
|
3
|
+
Version: 0.0.11
|
4
4
|
Summary: Container Manager manage Docker, Docker Swarm, and Podman containers as an MCP Server
|
5
5
|
Author-email: Audel Rouhi <knucklessg1@gmail.com>
|
6
6
|
License: MIT
|
@@ -48,7 +48,7 @@ Dynamic: license-file
|
|
48
48
|

|
49
49
|

|
50
50
|
|
51
|
-
*Version: 0.0.
|
51
|
+
*Version: 0.0.11*
|
52
52
|
|
53
53
|
Container Manager MCP Server provides a robust interface to manage Docker and Podman containers, networks, volumes, and Docker Swarm services through a FastMCP server, enabling programmatic and remote container management.
|
54
54
|
|
@@ -20,7 +20,7 @@
|
|
20
20
|

|
21
21
|

|
22
22
|
|
23
|
-
*Version: 0.0.
|
23
|
+
*Version: 0.0.11*
|
24
24
|
|
25
25
|
Container Manager MCP Server provides a robust interface to manage Docker and Podman containers, networks, volumes, and Docker Swarm services through a FastMCP server, enabling programmatic and remote container management.
|
26
26
|
|
@@ -9,6 +9,7 @@ from typing import List, Dict, Optional, Any
|
|
9
9
|
import getopt
|
10
10
|
import json
|
11
11
|
import subprocess
|
12
|
+
from datetime import datetime # Added for consistent timestamp formatting
|
12
13
|
|
13
14
|
try:
|
14
15
|
import docker
|
@@ -55,6 +56,16 @@ class ContainerManagerBase(ABC):
|
|
55
56
|
if error:
|
56
57
|
self.logger.error(f"Error: {str(error)}")
|
57
58
|
|
59
|
+
def _format_size(self, size_bytes: int) -> str:
|
60
|
+
"""Helper to format bytes to human-readable (e.g., 1.23GB)."""
|
61
|
+
for unit in ["B", "KB", "MB", "GB", "TB"]:
|
62
|
+
if size_bytes < 1024.0:
|
63
|
+
return (
|
64
|
+
f"{size_bytes:.2f}{unit}" if unit != "B" else f"{size_bytes}{unit}"
|
65
|
+
)
|
66
|
+
size_bytes /= 1024.0
|
67
|
+
return f"{size_bytes:.2f}PB"
|
68
|
+
|
58
69
|
@abstractmethod
|
59
70
|
def get_version(self) -> Dict:
|
60
71
|
pass
|
@@ -136,7 +147,6 @@ class ContainerManagerBase(ABC):
|
|
136
147
|
def remove_network(self, network_id: str) -> Dict:
|
137
148
|
pass
|
138
149
|
|
139
|
-
# Compose methods
|
140
150
|
@abstractmethod
|
141
151
|
def compose_up(
|
142
152
|
self, compose_file: str, detach: bool = True, build: bool = False
|
@@ -155,19 +165,23 @@ class ContainerManagerBase(ABC):
|
|
155
165
|
def compose_logs(self, compose_file: str, service: Optional[str] = None) -> str:
|
156
166
|
pass
|
157
167
|
|
158
|
-
|
168
|
+
@abstractmethod
|
159
169
|
def init_swarm(self, advertise_addr: Optional[str] = None) -> Dict:
|
160
|
-
|
170
|
+
pass
|
161
171
|
|
172
|
+
@abstractmethod
|
162
173
|
def leave_swarm(self, force: bool = False) -> Dict:
|
163
|
-
|
174
|
+
pass
|
164
175
|
|
176
|
+
@abstractmethod
|
165
177
|
def list_nodes(self) -> List[Dict]:
|
166
|
-
|
178
|
+
pass
|
167
179
|
|
180
|
+
@abstractmethod
|
168
181
|
def list_services(self) -> List[Dict]:
|
169
|
-
|
182
|
+
pass
|
170
183
|
|
184
|
+
@abstractmethod
|
171
185
|
def create_service(
|
172
186
|
self,
|
173
187
|
name: str,
|
@@ -176,10 +190,11 @@ class ContainerManagerBase(ABC):
|
|
176
190
|
ports: Optional[Dict[str, str]] = None,
|
177
191
|
mounts: Optional[List[str]] = None,
|
178
192
|
) -> Dict:
|
179
|
-
|
193
|
+
pass
|
180
194
|
|
195
|
+
@abstractmethod
|
181
196
|
def remove_service(self, service_id: str) -> Dict:
|
182
|
-
|
197
|
+
pass
|
183
198
|
|
184
199
|
|
185
200
|
class DockerManager(ContainerManagerBase):
|
@@ -196,7 +211,14 @@ class DockerManager(ContainerManagerBase):
|
|
196
211
|
def get_version(self) -> Dict:
|
197
212
|
params = {}
|
198
213
|
try:
|
199
|
-
|
214
|
+
version = self.client.version()
|
215
|
+
result = {
|
216
|
+
"version": version.get("Version", "unknown"),
|
217
|
+
"api_version": version.get("ApiVersion", "unknown"),
|
218
|
+
"os": version.get("Os", "unknown"),
|
219
|
+
"arch": version.get("Arch", "unknown"),
|
220
|
+
"build_time": version.get("BuildTime", "unknown"),
|
221
|
+
}
|
200
222
|
self.log_action("get_version", params, result)
|
201
223
|
return result
|
202
224
|
except Exception as e:
|
@@ -206,7 +228,16 @@ class DockerManager(ContainerManagerBase):
|
|
206
228
|
def get_info(self) -> Dict:
|
207
229
|
params = {}
|
208
230
|
try:
|
209
|
-
|
231
|
+
info = self.client.info()
|
232
|
+
result = {
|
233
|
+
"containers_total": info.get("Containers", 0),
|
234
|
+
"containers_running": info.get("ContainersRunning", 0),
|
235
|
+
"images": info.get("Images", 0),
|
236
|
+
"driver": info.get("Driver", "unknown"),
|
237
|
+
"platform": f"{info.get('OperatingSystem', 'unknown')} {info.get('Architecture', 'unknown')}",
|
238
|
+
"memory_total": self._format_size(info.get("MemTotal", 0)),
|
239
|
+
"swap_total": self._format_size(info.get("SwapTotal", 0)),
|
240
|
+
}
|
210
241
|
self.log_action("get_info", params, result)
|
211
242
|
return result
|
212
243
|
except Exception as e:
|
@@ -217,7 +248,38 @@ class DockerManager(ContainerManagerBase):
|
|
217
248
|
params = {}
|
218
249
|
try:
|
219
250
|
images = self.client.images.list()
|
220
|
-
result = [
|
251
|
+
result = []
|
252
|
+
for img in images:
|
253
|
+
attrs = img.attrs
|
254
|
+
repo_tags = attrs.get("RepoTags", [])
|
255
|
+
repo_tag = repo_tags[0] if repo_tags else "<none>:<none>"
|
256
|
+
repository, tag = (
|
257
|
+
repo_tag.rsplit(":", 1) if ":" in repo_tag else ("<none>", "<none>")
|
258
|
+
)
|
259
|
+
|
260
|
+
created = attrs.get("Created", 0)
|
261
|
+
created_str = (
|
262
|
+
datetime.fromtimestamp(created).strftime("%Y-%m-%dT%H:%M:%S")
|
263
|
+
if created
|
264
|
+
else "unknown"
|
265
|
+
)
|
266
|
+
|
267
|
+
size_bytes = attrs.get("Size", 0)
|
268
|
+
size_str = self._format_size(size_bytes) if size_bytes else "0B"
|
269
|
+
|
270
|
+
simplified = {
|
271
|
+
"repository": repository,
|
272
|
+
"tag": tag,
|
273
|
+
"id": (
|
274
|
+
attrs.get("Id", "unknown")[7:19]
|
275
|
+
if attrs.get("Id")
|
276
|
+
else "unknown"
|
277
|
+
),
|
278
|
+
"created": created_str,
|
279
|
+
"size": size_str,
|
280
|
+
}
|
281
|
+
result.append(simplified)
|
282
|
+
|
221
283
|
self.log_action("list_images", params, result)
|
222
284
|
return result
|
223
285
|
except Exception as e:
|
@@ -230,7 +292,29 @@ class DockerManager(ContainerManagerBase):
|
|
230
292
|
params = {"image": image, "tag": tag, "platform": platform}
|
231
293
|
try:
|
232
294
|
img = self.client.images.pull(f"{image}:{tag}", platform=platform)
|
233
|
-
|
295
|
+
attrs = img.attrs
|
296
|
+
repo_tags = attrs.get("RepoTags", [])
|
297
|
+
repo_tag = repo_tags[0] if repo_tags else f"{image}:{tag}"
|
298
|
+
repository, tag = (
|
299
|
+
repo_tag.rsplit(":", 1) if ":" in repo_tag else (image, tag)
|
300
|
+
)
|
301
|
+
created = attrs.get("Created", 0)
|
302
|
+
created_str = (
|
303
|
+
datetime.fromtimestamp(created).strftime("%Y-%m-%dT%H:%M:%S")
|
304
|
+
if created
|
305
|
+
else "unknown"
|
306
|
+
)
|
307
|
+
size_bytes = attrs.get("Size", 0)
|
308
|
+
size_str = self._format_size(size_bytes) if size_bytes else "0B"
|
309
|
+
result = {
|
310
|
+
"repository": repository,
|
311
|
+
"tag": tag,
|
312
|
+
"id": (
|
313
|
+
attrs.get("Id", "unknown")[7:19] if attrs.get("Id") else "unknown"
|
314
|
+
),
|
315
|
+
"created": created_str,
|
316
|
+
"size": size_str,
|
317
|
+
}
|
234
318
|
self.log_action("pull_image", params, result)
|
235
319
|
return result
|
236
320
|
except Exception as e:
|
@@ -252,7 +336,32 @@ class DockerManager(ContainerManagerBase):
|
|
252
336
|
params = {"all": all}
|
253
337
|
try:
|
254
338
|
containers = self.client.containers.list(all=all)
|
255
|
-
result = [
|
339
|
+
result = []
|
340
|
+
for c in containers:
|
341
|
+
attrs = c.attrs
|
342
|
+
ports = attrs.get("NetworkSettings", {}).get("Ports", {})
|
343
|
+
port_mappings = []
|
344
|
+
for container_port, host_ports in ports.items():
|
345
|
+
if host_ports:
|
346
|
+
for hp in host_ports:
|
347
|
+
port_mappings.append(
|
348
|
+
f"{hp.get('HostIp', '0.0.0.0')}:{hp.get('HostPort')}->{container_port}"
|
349
|
+
)
|
350
|
+
created = attrs.get("Created", 0)
|
351
|
+
created_str = (
|
352
|
+
datetime.fromtimestamp(created).strftime("%Y-%m-%dT%H:%M:%S")
|
353
|
+
if created
|
354
|
+
else "unknown"
|
355
|
+
)
|
356
|
+
simplified = {
|
357
|
+
"id": attrs.get("Id", "unknown")[7:19],
|
358
|
+
"image": attrs.get("Config", {}).get("Image", "unknown"),
|
359
|
+
"name": attrs.get("Name", "unknown").lstrip("/"),
|
360
|
+
"status": attrs.get("State", {}).get("Status", "unknown"),
|
361
|
+
"ports": ", ".join(port_mappings) if port_mappings else "none",
|
362
|
+
"created": created_str,
|
363
|
+
}
|
364
|
+
result.append(simplified)
|
256
365
|
self.log_action("list_containers", params, result)
|
257
366
|
return result
|
258
367
|
except Exception as e:
|
@@ -288,9 +397,33 @@ class DockerManager(ContainerManagerBase):
|
|
288
397
|
volumes=volumes,
|
289
398
|
environment=environment,
|
290
399
|
)
|
291
|
-
|
292
|
-
|
400
|
+
if not detach:
|
401
|
+
result = {"output": container.decode("utf-8") if container else ""}
|
402
|
+
self.log_action("run_container", params, result)
|
403
|
+
return result
|
404
|
+
attrs = container.attrs
|
405
|
+
ports = attrs.get("NetworkSettings", {}).get("Ports", {})
|
406
|
+
port_mappings = []
|
407
|
+
for container_port, host_ports in ports.items():
|
408
|
+
if host_ports:
|
409
|
+
for hp in host_ports:
|
410
|
+
port_mappings.append(
|
411
|
+
f"{hp.get('HostIp', '0.0.0.0')}:{hp.get('HostPort')}->{container_port}"
|
412
|
+
)
|
413
|
+
created = attrs.get("Created", 0)
|
414
|
+
created_str = (
|
415
|
+
datetime.fromtimestamp(created).strftime("%Y-%m-%dT%H:%M:%S")
|
416
|
+
if created
|
417
|
+
else "unknown"
|
293
418
|
)
|
419
|
+
result = {
|
420
|
+
"id": attrs.get("Id", "unknown")[7:19],
|
421
|
+
"image": attrs.get("Config", {}).get("Image", image),
|
422
|
+
"name": attrs.get("Name", name or "unknown").lstrip("/"),
|
423
|
+
"status": attrs.get("State", {}).get("Status", "unknown"),
|
424
|
+
"ports": ", ".join(port_mappings) if port_mappings else "none",
|
425
|
+
"created": created_str,
|
426
|
+
}
|
294
427
|
self.log_action("run_container", params, result)
|
295
428
|
return result
|
296
429
|
except Exception as e:
|
@@ -326,7 +459,9 @@ class DockerManager(ContainerManagerBase):
|
|
326
459
|
try:
|
327
460
|
container = self.client.containers.get(container_id)
|
328
461
|
logs = container.logs(tail=tail).decode("utf-8")
|
329
|
-
self.log_action(
|
462
|
+
self.log_action(
|
463
|
+
"get_container_logs", params, logs[:1000]
|
464
|
+
) # Truncate for logging
|
330
465
|
return logs
|
331
466
|
except Exception as e:
|
332
467
|
self.log_action("get_container_logs", params, error=e)
|
@@ -341,7 +476,8 @@ class DockerManager(ContainerManagerBase):
|
|
341
476
|
exit_code, output = container.exec_run(command, detach=detach)
|
342
477
|
result = {
|
343
478
|
"exit_code": exit_code,
|
344
|
-
"output": output.decode("utf-8") if output else None,
|
479
|
+
"output": output.decode("utf-8") if output and not detach else None,
|
480
|
+
"command": command,
|
345
481
|
}
|
346
482
|
self.log_action("exec_in_container", params, result)
|
347
483
|
return result
|
@@ -353,7 +489,17 @@ class DockerManager(ContainerManagerBase):
|
|
353
489
|
params = {}
|
354
490
|
try:
|
355
491
|
volumes = self.client.volumes.list()
|
356
|
-
result = {
|
492
|
+
result = {
|
493
|
+
"volumes": [
|
494
|
+
{
|
495
|
+
"name": v.attrs.get("Name", "unknown"),
|
496
|
+
"driver": v.attrs.get("Driver", "unknown"),
|
497
|
+
"mountpoint": v.attrs.get("Mountpoint", "unknown"),
|
498
|
+
"created": v.attrs.get("CreatedAt", "unknown"),
|
499
|
+
}
|
500
|
+
for v in volumes
|
501
|
+
]
|
502
|
+
}
|
357
503
|
self.log_action("list_volumes", params, result)
|
358
504
|
return result
|
359
505
|
except Exception as e:
|
@@ -364,7 +510,13 @@ class DockerManager(ContainerManagerBase):
|
|
364
510
|
params = {"name": name}
|
365
511
|
try:
|
366
512
|
volume = self.client.volumes.create(name=name)
|
367
|
-
|
513
|
+
attrs = volume.attrs
|
514
|
+
result = {
|
515
|
+
"name": attrs.get("Name", name),
|
516
|
+
"driver": attrs.get("Driver", "unknown"),
|
517
|
+
"mountpoint": attrs.get("Mountpoint", "unknown"),
|
518
|
+
"created": attrs.get("CreatedAt", "unknown"),
|
519
|
+
}
|
368
520
|
self.log_action("create_volume", params, result)
|
369
521
|
return result
|
370
522
|
except Exception as e:
|
@@ -387,7 +539,28 @@ class DockerManager(ContainerManagerBase):
|
|
387
539
|
params = {}
|
388
540
|
try:
|
389
541
|
networks = self.client.networks.list()
|
390
|
-
result = [
|
542
|
+
result = []
|
543
|
+
for net in networks:
|
544
|
+
attrs = net.attrs
|
545
|
+
containers = len(attrs.get("Containers", {}))
|
546
|
+
created = attrs.get("Created", "unknown")
|
547
|
+
if isinstance(created, str):
|
548
|
+
created_str = created
|
549
|
+
else:
|
550
|
+
created_str = (
|
551
|
+
datetime.fromtimestamp(created).strftime("%Y-%m-%dT%H:%M:%S")
|
552
|
+
if created
|
553
|
+
else "unknown"
|
554
|
+
)
|
555
|
+
simplified = {
|
556
|
+
"id": attrs.get("Id", "unknown")[7:19],
|
557
|
+
"name": attrs.get("Name", "unknown"),
|
558
|
+
"driver": attrs.get("Driver", "unknown"),
|
559
|
+
"scope": attrs.get("Scope", "unknown"),
|
560
|
+
"containers": containers,
|
561
|
+
"created": created_str,
|
562
|
+
}
|
563
|
+
result.append(simplified)
|
391
564
|
self.log_action("list_networks", params, result)
|
392
565
|
return result
|
393
566
|
except Exception as e:
|
@@ -398,7 +571,23 @@ class DockerManager(ContainerManagerBase):
|
|
398
571
|
params = {"name": name, "driver": driver}
|
399
572
|
try:
|
400
573
|
network = self.client.networks.create(name, driver=driver)
|
401
|
-
|
574
|
+
attrs = network.attrs
|
575
|
+
created = attrs.get("Created", "unknown")
|
576
|
+
if isinstance(created, str):
|
577
|
+
created_str = created
|
578
|
+
else:
|
579
|
+
created_str = (
|
580
|
+
datetime.fromtimestamp(created).strftime("%Y-%m-%dT%H:%M:%S")
|
581
|
+
if created
|
582
|
+
else "unknown"
|
583
|
+
)
|
584
|
+
result = {
|
585
|
+
"id": attrs.get("Id", "unknown")[7:19],
|
586
|
+
"name": attrs.get("Name", name),
|
587
|
+
"driver": attrs.get("Driver", driver),
|
588
|
+
"scope": attrs.get("Scope", "unknown"),
|
589
|
+
"created": created_str,
|
590
|
+
}
|
402
591
|
self.log_action("create_network", params, result)
|
403
592
|
return result
|
404
593
|
except Exception as e:
|
@@ -503,7 +692,23 @@ class DockerManager(ContainerManagerBase):
|
|
503
692
|
params = {}
|
504
693
|
try:
|
505
694
|
nodes = self.client.nodes.list()
|
506
|
-
result = [
|
695
|
+
result = []
|
696
|
+
for node in nodes:
|
697
|
+
attrs = node.attrs
|
698
|
+
spec = attrs.get("Spec", {})
|
699
|
+
status = attrs.get("Status", {})
|
700
|
+
created = attrs.get("CreatedAt", "unknown")
|
701
|
+
updated = attrs.get("UpdatedAt", "unknown")
|
702
|
+
simplified = {
|
703
|
+
"id": attrs.get("ID", "unknown")[7:19],
|
704
|
+
"hostname": spec.get("Name", "unknown"),
|
705
|
+
"role": spec.get("Role", "unknown"),
|
706
|
+
"status": status.get("State", "unknown"),
|
707
|
+
"availability": spec.get("Availability", "unknown"),
|
708
|
+
"created": created,
|
709
|
+
"updated": updated,
|
710
|
+
}
|
711
|
+
result.append(simplified)
|
507
712
|
self.log_action("list_nodes", params, result)
|
508
713
|
return result
|
509
714
|
except Exception as e:
|
@@ -514,7 +719,33 @@ class DockerManager(ContainerManagerBase):
|
|
514
719
|
params = {}
|
515
720
|
try:
|
516
721
|
services = self.client.services.list()
|
517
|
-
result = [
|
722
|
+
result = []
|
723
|
+
for service in services:
|
724
|
+
attrs = service.attrs
|
725
|
+
spec = attrs.get("Spec", {})
|
726
|
+
endpoint = attrs.get("Endpoint", {})
|
727
|
+
ports = endpoint.get("Ports", [])
|
728
|
+
port_mappings = [
|
729
|
+
f"{p.get('PublishedPort')}->{p.get('TargetPort')}/{p.get('Protocol')}"
|
730
|
+
for p in ports
|
731
|
+
if p.get("PublishedPort")
|
732
|
+
]
|
733
|
+
created = attrs.get("CreatedAt", "unknown")
|
734
|
+
updated = attrs.get("UpdatedAt", "unknown")
|
735
|
+
simplified = {
|
736
|
+
"id": attrs.get("ID", "unknown")[7:19],
|
737
|
+
"name": spec.get("Name", "unknown"),
|
738
|
+
"image": spec.get("TaskTemplate", {})
|
739
|
+
.get("ContainerSpec", {})
|
740
|
+
.get("Image", "unknown"),
|
741
|
+
"replicas": spec.get("Mode", {})
|
742
|
+
.get("Replicated", {})
|
743
|
+
.get("Replicas", 0),
|
744
|
+
"ports": ", ".join(port_mappings) if port_mappings else "none",
|
745
|
+
"created": created,
|
746
|
+
"updated": updated,
|
747
|
+
}
|
748
|
+
result.append(simplified)
|
518
749
|
self.log_action("list_services", params, result)
|
519
750
|
return result
|
520
751
|
except Exception as e:
|
@@ -538,15 +769,46 @@ class DockerManager(ContainerManagerBase):
|
|
538
769
|
}
|
539
770
|
try:
|
540
771
|
mode = {"mode": "replicated", "replicas": replicas}
|
541
|
-
|
772
|
+
endpoint_spec = None
|
773
|
+
if ports:
|
774
|
+
port_list = [
|
775
|
+
{
|
776
|
+
"Protocol": "tcp",
|
777
|
+
"PublishedPort": int(host_port),
|
778
|
+
"TargetPort": int(container_port.split("/")[0]),
|
779
|
+
}
|
780
|
+
for container_port, host_port in ports.items()
|
781
|
+
]
|
782
|
+
endpoint_spec = docker.types.EndpointSpec(ports=port_list)
|
542
783
|
service = self.client.services.create(
|
543
784
|
image,
|
544
785
|
name=name,
|
545
786
|
mode=mode,
|
546
787
|
mounts=mounts,
|
547
|
-
endpoint_spec=
|
788
|
+
endpoint_spec=endpoint_spec,
|
548
789
|
)
|
549
|
-
|
790
|
+
attrs = service.attrs
|
791
|
+
spec = attrs.get("Spec", {})
|
792
|
+
endpoint = attrs.get("Endpoint", {})
|
793
|
+
ports = endpoint.get("Ports", [])
|
794
|
+
port_mappings = [
|
795
|
+
f"{p.get('PublishedPort')}->{p.get('TargetPort')}/{p.get('Protocol')}"
|
796
|
+
for p in ports
|
797
|
+
if p.get("PublishedPort")
|
798
|
+
]
|
799
|
+
created = attrs.get("CreatedAt", "unknown")
|
800
|
+
result = {
|
801
|
+
"id": attrs.get("ID", "unknown")[7:19],
|
802
|
+
"name": spec.get("Name", name),
|
803
|
+
"image": spec.get("TaskTemplate", {})
|
804
|
+
.get("ContainerSpec", {})
|
805
|
+
.get("Image", image),
|
806
|
+
"replicas": spec.get("Mode", {})
|
807
|
+
.get("Replicated", {})
|
808
|
+
.get("Replicas", replicas),
|
809
|
+
"ports": ", ".join(port_mappings) if port_mappings else "none",
|
810
|
+
"created": created,
|
811
|
+
}
|
550
812
|
self.log_action("create_service", params, result)
|
551
813
|
return result
|
552
814
|
except Exception as e:
|
@@ -580,7 +842,14 @@ class PodmanManager(ContainerManagerBase):
|
|
580
842
|
def get_version(self) -> Dict:
|
581
843
|
params = {}
|
582
844
|
try:
|
583
|
-
|
845
|
+
version = self.client.version()
|
846
|
+
result = {
|
847
|
+
"version": version.get("Version", "unknown"),
|
848
|
+
"api_version": version.get("APIVersion", "unknown"),
|
849
|
+
"os": version.get("Os", "unknown"),
|
850
|
+
"arch": version.get("Arch", "unknown"),
|
851
|
+
"build_time": version.get("BuildTime", "unknown"),
|
852
|
+
}
|
584
853
|
self.log_action("get_version", params, result)
|
585
854
|
return result
|
586
855
|
except Exception as e:
|
@@ -590,7 +859,17 @@ class PodmanManager(ContainerManagerBase):
|
|
590
859
|
def get_info(self) -> Dict:
|
591
860
|
params = {}
|
592
861
|
try:
|
593
|
-
|
862
|
+
info = self.client.info()
|
863
|
+
host = info.get("host", {})
|
864
|
+
result = {
|
865
|
+
"containers_total": info.get("store", {}).get("containers", 0),
|
866
|
+
"containers_running": host.get("runningContainers", 0),
|
867
|
+
"images": info.get("store", {}).get("images", 0),
|
868
|
+
"driver": host.get("graphDriverName", "unknown"),
|
869
|
+
"platform": f"{host.get('os', 'unknown')} {host.get('arch', 'unknown')}",
|
870
|
+
"memory_total": self._format_size(host.get("memTotal", 0)),
|
871
|
+
"swap_total": self._format_size(host.get("swapTotal", 0)),
|
872
|
+
}
|
594
873
|
self.log_action("get_info", params, result)
|
595
874
|
return result
|
596
875
|
except Exception as e:
|
@@ -601,7 +880,34 @@ class PodmanManager(ContainerManagerBase):
|
|
601
880
|
params = {}
|
602
881
|
try:
|
603
882
|
images = self.client.images.list()
|
604
|
-
result = [
|
883
|
+
result = []
|
884
|
+
for img in images:
|
885
|
+
attrs = img.attrs
|
886
|
+
repo_tags = attrs.get("Names", [])
|
887
|
+
repo_tag = repo_tags[0] if repo_tags else "<none>:<none>"
|
888
|
+
repository, tag = (
|
889
|
+
repo_tag.rsplit(":", 1) if ":" in repo_tag else ("<none>", "<none>")
|
890
|
+
)
|
891
|
+
created = attrs.get("Created", 0)
|
892
|
+
created_str = (
|
893
|
+
datetime.fromtimestamp(created).strftime("%Y-%m-%dT%H:%M:%S")
|
894
|
+
if created
|
895
|
+
else "unknown"
|
896
|
+
)
|
897
|
+
size_bytes = attrs.get("Size", 0)
|
898
|
+
size_str = self._format_size(size_bytes) if size_bytes else "0B"
|
899
|
+
simplified = {
|
900
|
+
"repository": repository,
|
901
|
+
"tag": tag,
|
902
|
+
"id": (
|
903
|
+
attrs.get("Id", "unknown")[7:19]
|
904
|
+
if attrs.get("Id")
|
905
|
+
else "unknown"
|
906
|
+
),
|
907
|
+
"created": created_str,
|
908
|
+
"size": size_str,
|
909
|
+
}
|
910
|
+
result.append(simplified)
|
605
911
|
self.log_action("list_images", params, result)
|
606
912
|
return result
|
607
913
|
except Exception as e:
|
@@ -614,7 +920,29 @@ class PodmanManager(ContainerManagerBase):
|
|
614
920
|
params = {"image": image, "tag": tag, "platform": platform}
|
615
921
|
try:
|
616
922
|
img = self.client.images.pull(f"{image}:{tag}", platform=platform)
|
617
|
-
|
923
|
+
attrs = img[0].attrs if isinstance(img, list) else img.attrs
|
924
|
+
repo_tags = attrs.get("Names", [])
|
925
|
+
repo_tag = repo_tags[0] if repo_tags else f"{image}:{tag}"
|
926
|
+
repository, tag = (
|
927
|
+
repo_tag.rsplit(":", 1) if ":" in repo_tag else (image, tag)
|
928
|
+
)
|
929
|
+
created = attrs.get("Created", 0)
|
930
|
+
created_str = (
|
931
|
+
datetime.fromtimestamp(created).strftime("%Y-%m-%dT%H:%M:%S")
|
932
|
+
if created
|
933
|
+
else "unknown"
|
934
|
+
)
|
935
|
+
size_bytes = attrs.get("Size", 0)
|
936
|
+
size_str = self._format_size(size_bytes) if size_bytes else "0B"
|
937
|
+
result = {
|
938
|
+
"repository": repository,
|
939
|
+
"tag": tag,
|
940
|
+
"id": (
|
941
|
+
attrs.get("Id", "unknown")[7:19] if attrs.get("Id") else "unknown"
|
942
|
+
),
|
943
|
+
"created": created_str,
|
944
|
+
"size": size_str,
|
945
|
+
}
|
618
946
|
self.log_action("pull_image", params, result)
|
619
947
|
return result
|
620
948
|
except Exception as e:
|
@@ -636,7 +964,30 @@ class PodmanManager(ContainerManagerBase):
|
|
636
964
|
params = {"all": all}
|
637
965
|
try:
|
638
966
|
containers = self.client.containers.list(all=all)
|
639
|
-
result = [
|
967
|
+
result = []
|
968
|
+
for c in containers:
|
969
|
+
attrs = c.attrs
|
970
|
+
ports = attrs.get("Ports", [])
|
971
|
+
port_mappings = [
|
972
|
+
f"{p.get('host_ip', '0.0.0.0')}:{p.get('host_port')}->{p.get('container_port')}/{p.get('protocol', 'tcp')}"
|
973
|
+
for p in ports
|
974
|
+
if p.get("host_port")
|
975
|
+
]
|
976
|
+
created = attrs.get("Created", 0)
|
977
|
+
created_str = (
|
978
|
+
datetime.fromtimestamp(created).strftime("%Y-%m-%dT%H:%M:%S")
|
979
|
+
if created
|
980
|
+
else "unknown"
|
981
|
+
)
|
982
|
+
simplified = {
|
983
|
+
"id": attrs.get("Id", "unknown")[7:19],
|
984
|
+
"image": attrs.get("Image", "unknown"),
|
985
|
+
"name": attrs.get("Names", ["unknown"])[0].lstrip("/"),
|
986
|
+
"status": attrs.get("State", "unknown"),
|
987
|
+
"ports": ", ".join(port_mappings) if port_mappings else "none",
|
988
|
+
"created": created_str,
|
989
|
+
}
|
990
|
+
result.append(simplified)
|
640
991
|
self.log_action("list_containers", params, result)
|
641
992
|
return result
|
642
993
|
except Exception as e:
|
@@ -672,9 +1023,31 @@ class PodmanManager(ContainerManagerBase):
|
|
672
1023
|
volumes=volumes,
|
673
1024
|
environment=environment,
|
674
1025
|
)
|
675
|
-
|
676
|
-
|
1026
|
+
if not detach:
|
1027
|
+
result = {"output": container.decode("utf-8") if container else ""}
|
1028
|
+
self.log_action("run_container", params, result)
|
1029
|
+
return result
|
1030
|
+
attrs = container.attrs
|
1031
|
+
ports = attrs.get("Ports", [])
|
1032
|
+
port_mappings = [
|
1033
|
+
f"{p.get('host_ip', '0.0.0.0')}:{p.get('host_port')}->{p.get('container_port')}/{p.get('protocol', 'tcp')}"
|
1034
|
+
for p in ports
|
1035
|
+
if p.get("host_port")
|
1036
|
+
]
|
1037
|
+
created = attrs.get("Created", 0)
|
1038
|
+
created_str = (
|
1039
|
+
datetime.fromtimestamp(created).strftime("%Y-%m-%dT%H:%M:%S")
|
1040
|
+
if created
|
1041
|
+
else "unknown"
|
677
1042
|
)
|
1043
|
+
result = {
|
1044
|
+
"id": attrs.get("Id", "unknown")[7:19],
|
1045
|
+
"image": attrs.get("Image", image),
|
1046
|
+
"name": attrs.get("Names", [name or "unknown"])[0].lstrip("/"),
|
1047
|
+
"status": attrs.get("State", "unknown"),
|
1048
|
+
"ports": ", ".join(port_mappings) if port_mappings else "none",
|
1049
|
+
"created": created_str,
|
1050
|
+
}
|
678
1051
|
self.log_action("run_container", params, result)
|
679
1052
|
return result
|
680
1053
|
except Exception as e:
|
@@ -710,7 +1083,9 @@ class PodmanManager(ContainerManagerBase):
|
|
710
1083
|
try:
|
711
1084
|
container = self.client.containers.get(container_id)
|
712
1085
|
logs = container.logs(tail=tail).decode("utf-8")
|
713
|
-
self.log_action(
|
1086
|
+
self.log_action(
|
1087
|
+
"get_container_logs", params, logs[:1000]
|
1088
|
+
) # Truncate for logging
|
714
1089
|
return logs
|
715
1090
|
except Exception as e:
|
716
1091
|
self.log_action("get_container_logs", params, error=e)
|
@@ -725,7 +1100,8 @@ class PodmanManager(ContainerManagerBase):
|
|
725
1100
|
exit_code, output = container.exec_run(command, detach=detach)
|
726
1101
|
result = {
|
727
1102
|
"exit_code": exit_code,
|
728
|
-
"output": output.decode("utf-8") if output else None,
|
1103
|
+
"output": output.decode("utf-8") if output and not detach else None,
|
1104
|
+
"command": command,
|
729
1105
|
}
|
730
1106
|
self.log_action("exec_in_container", params, result)
|
731
1107
|
return result
|
@@ -737,7 +1113,17 @@ class PodmanManager(ContainerManagerBase):
|
|
737
1113
|
params = {}
|
738
1114
|
try:
|
739
1115
|
volumes = self.client.volumes.list()
|
740
|
-
result = {
|
1116
|
+
result = {
|
1117
|
+
"volumes": [
|
1118
|
+
{
|
1119
|
+
"name": v.attrs.get("Name", "unknown"),
|
1120
|
+
"driver": v.attrs.get("Driver", "unknown"),
|
1121
|
+
"mountpoint": v.attrs.get("Mountpoint", "unknown"),
|
1122
|
+
"created": v.attrs.get("CreatedAt", "unknown"),
|
1123
|
+
}
|
1124
|
+
for v in volumes
|
1125
|
+
]
|
1126
|
+
}
|
741
1127
|
self.log_action("list_volumes", params, result)
|
742
1128
|
return result
|
743
1129
|
except Exception as e:
|
@@ -748,7 +1134,13 @@ class PodmanManager(ContainerManagerBase):
|
|
748
1134
|
params = {"name": name}
|
749
1135
|
try:
|
750
1136
|
volume = self.client.volumes.create(name=name)
|
751
|
-
|
1137
|
+
attrs = volume.attrs
|
1138
|
+
result = {
|
1139
|
+
"name": attrs.get("Name", name),
|
1140
|
+
"driver": attrs.get("Driver", "unknown"),
|
1141
|
+
"mountpoint": attrs.get("Mountpoint", "unknown"),
|
1142
|
+
"created": attrs.get("CreatedAt", "unknown"),
|
1143
|
+
}
|
752
1144
|
self.log_action("create_volume", params, result)
|
753
1145
|
return result
|
754
1146
|
except Exception as e:
|
@@ -771,7 +1163,28 @@ class PodmanManager(ContainerManagerBase):
|
|
771
1163
|
params = {}
|
772
1164
|
try:
|
773
1165
|
networks = self.client.networks.list()
|
774
|
-
result = [
|
1166
|
+
result = []
|
1167
|
+
for net in networks:
|
1168
|
+
attrs = net.attrs
|
1169
|
+
containers = len(attrs.get("Containers", {}))
|
1170
|
+
created = attrs.get("Created", "unknown")
|
1171
|
+
if isinstance(created, str):
|
1172
|
+
created_str = created
|
1173
|
+
else:
|
1174
|
+
created_str = (
|
1175
|
+
datetime.fromtimestamp(created).strftime("%Y-%m-%dT%H:%M:%S")
|
1176
|
+
if created
|
1177
|
+
else "unknown"
|
1178
|
+
)
|
1179
|
+
simplified = {
|
1180
|
+
"id": attrs.get("Id", "unknown")[7:19],
|
1181
|
+
"name": attrs.get("Name", "unknown"),
|
1182
|
+
"driver": attrs.get("Driver", "unknown"),
|
1183
|
+
"scope": attrs.get("Scope", "unknown"),
|
1184
|
+
"containers": containers,
|
1185
|
+
"created": created_str,
|
1186
|
+
}
|
1187
|
+
result.append(simplified)
|
775
1188
|
self.log_action("list_networks", params, result)
|
776
1189
|
return result
|
777
1190
|
except Exception as e:
|
@@ -782,7 +1195,23 @@ class PodmanManager(ContainerManagerBase):
|
|
782
1195
|
params = {"name": name, "driver": driver}
|
783
1196
|
try:
|
784
1197
|
network = self.client.networks.create(name, driver=driver)
|
785
|
-
|
1198
|
+
attrs = network.attrs
|
1199
|
+
created = attrs.get("Created", "unknown")
|
1200
|
+
if isinstance(created, str):
|
1201
|
+
created_str = created
|
1202
|
+
else:
|
1203
|
+
created_str = (
|
1204
|
+
datetime.fromtimestamp(created).strftime("%Y-%m-%dT%H:%M:%S")
|
1205
|
+
if created
|
1206
|
+
else "unknown"
|
1207
|
+
)
|
1208
|
+
result = {
|
1209
|
+
"id": attrs.get("Id", "unknown")[7:19],
|
1210
|
+
"name": attrs.get("Name", name),
|
1211
|
+
"driver": attrs.get("Driver", driver),
|
1212
|
+
"scope": attrs.get("Scope", "unknown"),
|
1213
|
+
"created": created_str,
|
1214
|
+
}
|
786
1215
|
self.log_action("create_network", params, result)
|
787
1216
|
return result
|
788
1217
|
except Exception as e:
|
@@ -861,6 +1290,34 @@ class PodmanManager(ContainerManagerBase):
|
|
861
1290
|
self.log_action("compose_logs", params, error=e)
|
862
1291
|
raise RuntimeError(f"Failed to compose logs: {str(e)}")
|
863
1292
|
|
1293
|
+
def init_swarm(self, advertise_addr: Optional[str] = None) -> Dict:
|
1294
|
+
raise NotImplementedError("Swarm not supported in Podman")
|
1295
|
+
|
1296
|
+
def leave_swarm(self, force: bool = False) -> Dict:
|
1297
|
+
raise NotImplementedError("Swarm not supported in Podman")
|
1298
|
+
|
1299
|
+
def list_nodes(self) -> List[Dict]:
|
1300
|
+
raise NotImplementedError("Swarm not supported in Podman")
|
1301
|
+
|
1302
|
+
def list_services(self) -> List[Dict]:
|
1303
|
+
raise NotImplementedError("Swarm not supported in Podman")
|
1304
|
+
|
1305
|
+
def create_service(
|
1306
|
+
self,
|
1307
|
+
name: str,
|
1308
|
+
image: str,
|
1309
|
+
replicas: int = 1,
|
1310
|
+
ports: Optional[Dict[str, str]] = None,
|
1311
|
+
mounts: Optional[List[str]] = None,
|
1312
|
+
) -> Dict:
|
1313
|
+
raise NotImplementedError("Swarm not supported in Podman")
|
1314
|
+
|
1315
|
+
def remove_service(self, service_id: str) -> Dict:
|
1316
|
+
raise NotImplementedError("Swarm not supported in Podman")
|
1317
|
+
|
1318
|
+
|
1319
|
+
# The rest of the file (create_manager, usage, container_manager) remains unchanged
|
1320
|
+
|
864
1321
|
|
865
1322
|
def create_manager(
|
866
1323
|
manager_type: str, silent: bool = False, log_file: str = None
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: container-manager-mcp
|
3
|
-
Version: 0.0.
|
3
|
+
Version: 0.0.11
|
4
4
|
Summary: Container Manager manage Docker, Docker Swarm, and Podman containers as an MCP Server
|
5
5
|
Author-email: Audel Rouhi <knucklessg1@gmail.com>
|
6
6
|
License: MIT
|
@@ -48,7 +48,7 @@ Dynamic: license-file
|
|
48
48
|

|
49
49
|

|
50
50
|
|
51
|
-
*Version: 0.0.
|
51
|
+
*Version: 0.0.11*
|
52
52
|
|
53
53
|
Container Manager MCP Server provides a robust interface to manage Docker and Podman containers, networks, volumes, and Docker Swarm services through a FastMCP server, enabling programmatic and remote container management.
|
54
54
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
4
4
|
|
5
5
|
[project]
|
6
6
|
name = "container-manager-mcp"
|
7
|
-
version = "0.0.
|
7
|
+
version = "0.0.11"
|
8
8
|
description = "Container Manager manage Docker, Docker Swarm, and Podman containers as an MCP Server"
|
9
9
|
readme = "README.md"
|
10
10
|
authors = [{ name = "Audel Rouhi", email = "knucklessg1@gmail.com" }]
|
File without changes
|
File without changes
|
{container_manager_mcp-0.0.10 → container_manager_mcp-0.0.11}/container_manager_mcp/__init__.py
RENAMED
File without changes
|
{container_manager_mcp-0.0.10 → container_manager_mcp-0.0.11}/container_manager_mcp/__main__.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|