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.
Files changed (17) hide show
  1. {container_manager_mcp-0.0.10/container_manager_mcp.egg-info → container_manager_mcp-0.0.11}/PKG-INFO +2 -2
  2. {container_manager_mcp-0.0.10 → container_manager_mcp-0.0.11}/README.md +1 -1
  3. {container_manager_mcp-0.0.10 → container_manager_mcp-0.0.11}/container_manager_mcp/container_manager.py +496 -39
  4. {container_manager_mcp-0.0.10 → container_manager_mcp-0.0.11/container_manager_mcp.egg-info}/PKG-INFO +2 -2
  5. {container_manager_mcp-0.0.10 → container_manager_mcp-0.0.11}/pyproject.toml +1 -1
  6. {container_manager_mcp-0.0.10 → container_manager_mcp-0.0.11}/LICENSE +0 -0
  7. {container_manager_mcp-0.0.10 → container_manager_mcp-0.0.11}/MANIFEST.in +0 -0
  8. {container_manager_mcp-0.0.10 → container_manager_mcp-0.0.11}/container_manager_mcp/__init__.py +0 -0
  9. {container_manager_mcp-0.0.10 → container_manager_mcp-0.0.11}/container_manager_mcp/__main__.py +0 -0
  10. {container_manager_mcp-0.0.10 → container_manager_mcp-0.0.11}/container_manager_mcp/container_manager_mcp.py +0 -0
  11. {container_manager_mcp-0.0.10 → container_manager_mcp-0.0.11}/container_manager_mcp.egg-info/SOURCES.txt +0 -0
  12. {container_manager_mcp-0.0.10 → container_manager_mcp-0.0.11}/container_manager_mcp.egg-info/dependency_links.txt +0 -0
  13. {container_manager_mcp-0.0.10 → container_manager_mcp-0.0.11}/container_manager_mcp.egg-info/entry_points.txt +0 -0
  14. {container_manager_mcp-0.0.10 → container_manager_mcp-0.0.11}/container_manager_mcp.egg-info/requires.txt +0 -0
  15. {container_manager_mcp-0.0.10 → container_manager_mcp-0.0.11}/container_manager_mcp.egg-info/top_level.txt +0 -0
  16. {container_manager_mcp-0.0.10 → container_manager_mcp-0.0.11}/requirements.txt +0 -0
  17. {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.10
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
  ![PyPI - Wheel](https://img.shields.io/pypi/wheel/container-manager-mcp)
49
49
  ![PyPI - Implementation](https://img.shields.io/pypi/implementation/container-manager-mcp)
50
50
 
51
- *Version: 0.0.10*
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
  ![PyPI - Wheel](https://img.shields.io/pypi/wheel/container-manager-mcp)
21
21
  ![PyPI - Implementation](https://img.shields.io/pypi/implementation/container-manager-mcp)
22
22
 
23
- *Version: 0.0.10*
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
- # Swarm methods (to be implemented only in DockerManager)
168
+ @abstractmethod
159
169
  def init_swarm(self, advertise_addr: Optional[str] = None) -> Dict:
160
- raise NotImplementedError("Swarm not supported")
170
+ pass
161
171
 
172
+ @abstractmethod
162
173
  def leave_swarm(self, force: bool = False) -> Dict:
163
- raise NotImplementedError("Swarm not supported")
174
+ pass
164
175
 
176
+ @abstractmethod
165
177
  def list_nodes(self) -> List[Dict]:
166
- raise NotImplementedError("Swarm not supported")
178
+ pass
167
179
 
180
+ @abstractmethod
168
181
  def list_services(self) -> List[Dict]:
169
- raise NotImplementedError("Swarm not supported")
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
- raise NotImplementedError("Swarm not supported")
193
+ pass
180
194
 
195
+ @abstractmethod
181
196
  def remove_service(self, service_id: str) -> Dict:
182
- raise NotImplementedError("Swarm not supported")
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
- result = self.client.version()
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
- result = self.client.info()
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 = [img.attrs for img in images]
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
- result = img.attrs
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 = [c.attrs for c in containers]
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
- result = (
292
- container.attrs if detach else {"output": container.decode("utf-8")}
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("get_container_logs", params, logs)
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 = {"volumes": [v.attrs for v in volumes]}
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
- result = volume.attrs
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 = [net.attrs for net in networks]
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
- result = network.attrs
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 = [node.attrs for node in nodes]
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 = [service.attrs for service in services]
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
- target_ports = [docker.types.EndpointSpec(ports=ports)] if ports else None
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=target_ports[0] if target_ports else None,
788
+ endpoint_spec=endpoint_spec,
548
789
  )
549
- result = service.attrs
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
- result = self.client.version()
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
- result = self.client.info()
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 = [img.attrs for img in images]
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
- result = img[0].attrs if isinstance(img, list) else img.attrs
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 = [c.attrs for c in containers]
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
- result = (
676
- container.attrs if detach else {"output": container.decode("utf-8")}
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("get_container_logs", params, logs)
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 = {"volumes": [v.attrs for v in volumes]}
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
- result = volume.attrs
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 = [net.attrs for net in networks]
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
- result = network.attrs
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.10
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
  ![PyPI - Wheel](https://img.shields.io/pypi/wheel/container-manager-mcp)
49
49
  ![PyPI - Implementation](https://img.shields.io/pypi/implementation/container-manager-mcp)
50
50
 
51
- *Version: 0.0.10*
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.10"
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" }]