container-manager-mcp 0.0.10__tar.gz → 0.0.12__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.12}/PKG-INFO +2 -2
  2. {container_manager_mcp-0.0.10 → container_manager_mcp-0.0.12}/README.md +1 -1
  3. {container_manager_mcp-0.0.10 → container_manager_mcp-0.0.12}/container_manager_mcp/container_manager.py +552 -139
  4. {container_manager_mcp-0.0.10 → container_manager_mcp-0.0.12/container_manager_mcp.egg-info}/PKG-INFO +2 -2
  5. {container_manager_mcp-0.0.10 → container_manager_mcp-0.0.12}/pyproject.toml +1 -1
  6. {container_manager_mcp-0.0.10 → container_manager_mcp-0.0.12}/LICENSE +0 -0
  7. {container_manager_mcp-0.0.10 → container_manager_mcp-0.0.12}/MANIFEST.in +0 -0
  8. {container_manager_mcp-0.0.10 → container_manager_mcp-0.0.12}/container_manager_mcp/__init__.py +0 -0
  9. {container_manager_mcp-0.0.10 → container_manager_mcp-0.0.12}/container_manager_mcp/__main__.py +0 -0
  10. {container_manager_mcp-0.0.10 → container_manager_mcp-0.0.12}/container_manager_mcp/container_manager_mcp.py +0 -0
  11. {container_manager_mcp-0.0.10 → container_manager_mcp-0.0.12}/container_manager_mcp.egg-info/SOURCES.txt +0 -0
  12. {container_manager_mcp-0.0.10 → container_manager_mcp-0.0.12}/container_manager_mcp.egg-info/dependency_links.txt +0 -0
  13. {container_manager_mcp-0.0.10 → container_manager_mcp-0.0.12}/container_manager_mcp.egg-info/entry_points.txt +0 -0
  14. {container_manager_mcp-0.0.10 → container_manager_mcp-0.0.12}/container_manager_mcp.egg-info/requires.txt +0 -0
  15. {container_manager_mcp-0.0.10 → container_manager_mcp-0.0.12}/container_manager_mcp.egg-info/top_level.txt +0 -0
  16. {container_manager_mcp-0.0.10 → container_manager_mcp-0.0.12}/requirements.txt +0 -0
  17. {container_manager_mcp-0.0.10 → container_manager_mcp-0.0.12}/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.12
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.12*
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.12*
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,8 @@ from typing import List, Dict, Optional, Any
9
9
  import getopt
10
10
  import json
11
11
  import subprocess
12
+ from datetime import datetime
13
+ import dateutil.parser
12
14
 
13
15
  try:
14
16
  import docker
@@ -26,6 +28,7 @@ except ImportError:
26
28
 
27
29
 
28
30
  class ContainerManagerBase(ABC):
31
+
29
32
  def __init__(self, silent: bool = False, log_file: str = None):
30
33
  self.silent = silent
31
34
  self.setup_logging(log_file)
@@ -55,6 +58,30 @@ class ContainerManagerBase(ABC):
55
58
  if error:
56
59
  self.logger.error(f"Error: {str(error)}")
57
60
 
61
+ def _format_size(self, size_bytes: int) -> str:
62
+ """Helper to format bytes to human-readable (e.g., 1.23GB)."""
63
+ for unit in ["B", "KB", "MB", "GB", "TB"]:
64
+ if size_bytes < 1024.0:
65
+ return (
66
+ f"{size_bytes:.2f}{unit}" if unit != "B" else f"{size_bytes}{unit}"
67
+ )
68
+ size_bytes /= 1024.0
69
+ return f"{size_bytes:.2f}PB"
70
+
71
+ def _parse_timestamp(self, timestamp: Any) -> str:
72
+ """Parse timestamp (integer or string) to ISO 8601 string."""
73
+ if not timestamp:
74
+ return "unknown"
75
+ if isinstance(timestamp, (int, float)):
76
+ return datetime.fromtimestamp(timestamp).strftime("%Y-%m-%dT%H:%M:%S")
77
+ if isinstance(timestamp, str):
78
+ try:
79
+ parsed = dateutil.parser.isoparse(timestamp)
80
+ return parsed.strftime("%Y-%m-%dT%H:%M:%S")
81
+ except ValueError:
82
+ return "unknown"
83
+ return "unknown"
84
+
58
85
  @abstractmethod
59
86
  def get_version(self) -> Dict:
60
87
  pass
@@ -136,7 +163,6 @@ class ContainerManagerBase(ABC):
136
163
  def remove_network(self, network_id: str) -> Dict:
137
164
  pass
138
165
 
139
- # Compose methods
140
166
  @abstractmethod
141
167
  def compose_up(
142
168
  self, compose_file: str, detach: bool = True, build: bool = False
@@ -155,19 +181,23 @@ class ContainerManagerBase(ABC):
155
181
  def compose_logs(self, compose_file: str, service: Optional[str] = None) -> str:
156
182
  pass
157
183
 
158
- # Swarm methods (to be implemented only in DockerManager)
184
+ @abstractmethod
159
185
  def init_swarm(self, advertise_addr: Optional[str] = None) -> Dict:
160
- raise NotImplementedError("Swarm not supported")
186
+ pass
161
187
 
188
+ @abstractmethod
162
189
  def leave_swarm(self, force: bool = False) -> Dict:
163
- raise NotImplementedError("Swarm not supported")
190
+ pass
164
191
 
192
+ @abstractmethod
165
193
  def list_nodes(self) -> List[Dict]:
166
- raise NotImplementedError("Swarm not supported")
194
+ pass
167
195
 
196
+ @abstractmethod
168
197
  def list_services(self) -> List[Dict]:
169
- raise NotImplementedError("Swarm not supported")
198
+ pass
170
199
 
200
+ @abstractmethod
171
201
  def create_service(
172
202
  self,
173
203
  name: str,
@@ -176,10 +206,11 @@ class ContainerManagerBase(ABC):
176
206
  ports: Optional[Dict[str, str]] = None,
177
207
  mounts: Optional[List[str]] = None,
178
208
  ) -> Dict:
179
- raise NotImplementedError("Swarm not supported")
209
+ pass
180
210
 
211
+ @abstractmethod
181
212
  def remove_service(self, service_id: str) -> Dict:
182
- raise NotImplementedError("Swarm not supported")
213
+ pass
183
214
 
184
215
 
185
216
  class DockerManager(ContainerManagerBase):
@@ -193,31 +224,38 @@ class DockerManager(ContainerManagerBase):
193
224
  self.logger.error(f"Failed to connect to Docker daemon: {str(e)}")
194
225
  raise RuntimeError(f"Failed to connect to Docker: {str(e)}")
195
226
 
196
- def get_version(self) -> Dict:
197
- params = {}
198
- try:
199
- result = self.client.version()
200
- self.log_action("get_version", params, result)
201
- return result
202
- except Exception as e:
203
- self.log_action("get_version", params, error=e)
204
- raise RuntimeError(f"Failed to get version: {str(e)}")
205
-
206
- def get_info(self) -> Dict:
207
- params = {}
208
- try:
209
- result = self.client.info()
210
- self.log_action("get_info", params, result)
211
- return result
212
- except Exception as e:
213
- self.log_action("get_info", params, error=e)
214
- raise RuntimeError(f"Failed to get info: {str(e)}")
215
-
216
227
  def list_images(self) -> List[Dict]:
217
228
  params = {}
218
229
  try:
219
230
  images = self.client.images.list()
220
- result = [img.attrs for img in images]
231
+ result = []
232
+ for img in images:
233
+ attrs = img.attrs
234
+ repo_tags = attrs.get("RepoTags", [])
235
+ repo_tag = repo_tags[0] if repo_tags else "<none>:<none>"
236
+ repository, tag = (
237
+ repo_tag.rsplit(":", 1) if ":" in repo_tag else ("<none>", "<none>")
238
+ )
239
+
240
+ created = attrs.get("Created", None)
241
+ created_str = self._parse_timestamp(created)
242
+
243
+ size_bytes = attrs.get("Size", 0)
244
+ size_str = self._format_size(size_bytes) if size_bytes else "0B"
245
+
246
+ simplified = {
247
+ "repository": repository,
248
+ "tag": tag,
249
+ "id": (
250
+ attrs.get("Id", "unknown")[7:19]
251
+ if attrs.get("Id")
252
+ else "unknown"
253
+ ),
254
+ "created": created_str,
255
+ "size": size_str,
256
+ }
257
+ result.append(simplified)
258
+
221
259
  self.log_action("list_images", params, result)
222
260
  return result
223
261
  except Exception as e:
@@ -230,29 +268,57 @@ class DockerManager(ContainerManagerBase):
230
268
  params = {"image": image, "tag": tag, "platform": platform}
231
269
  try:
232
270
  img = self.client.images.pull(f"{image}:{tag}", platform=platform)
233
- result = img.attrs
271
+ attrs = img.attrs
272
+ repo_tags = attrs.get("RepoTags", [])
273
+ repo_tag = repo_tags[0] if repo_tags else f"{image}:{tag}"
274
+ repository, tag = (
275
+ repo_tag.rsplit(":", 1) if ":" in repo_tag else (image, tag)
276
+ )
277
+ created = attrs.get("Created", None)
278
+ created_str = self._parse_timestamp(created)
279
+ size_bytes = attrs.get("Size", 0)
280
+ size_str = self._format_size(size_bytes) if size_bytes else "0B"
281
+ result = {
282
+ "repository": repository,
283
+ "tag": tag,
284
+ "id": (
285
+ attrs.get("Id", "unknown")[7:19] if attrs.get("Id") else "unknown"
286
+ ),
287
+ "created": created_str,
288
+ "size": size_str,
289
+ }
234
290
  self.log_action("pull_image", params, result)
235
291
  return result
236
292
  except Exception as e:
237
293
  self.log_action("pull_image", params, error=e)
238
294
  raise RuntimeError(f"Failed to pull image: {str(e)}")
239
295
 
240
- def remove_image(self, image: str, force: bool = False) -> Dict:
241
- params = {"image": image, "force": force}
242
- try:
243
- self.client.images.remove(image, force=force)
244
- result = {"removed": image}
245
- self.log_action("remove_image", params, result)
246
- return result
247
- except Exception as e:
248
- self.log_action("remove_image", params, error=e)
249
- raise RuntimeError(f"Failed to remove image: {str(e)}")
250
-
251
296
  def list_containers(self, all: bool = False) -> List[Dict]:
252
297
  params = {"all": all}
253
298
  try:
254
299
  containers = self.client.containers.list(all=all)
255
- result = [c.attrs for c in containers]
300
+ result = []
301
+ for c in containers:
302
+ attrs = c.attrs
303
+ ports = attrs.get("NetworkSettings", {}).get("Ports", {})
304
+ port_mappings = []
305
+ for container_port, host_ports in ports.items():
306
+ if host_ports:
307
+ for hp in host_ports:
308
+ port_mappings.append(
309
+ f"{hp.get('HostIp', '0.0.0.0')}:{hp.get('HostPort')}->{container_port}"
310
+ )
311
+ created = attrs.get("Created", None)
312
+ created_str = self._parse_timestamp(created)
313
+ simplified = {
314
+ "id": attrs.get("Id", "unknown")[7:19],
315
+ "image": attrs.get("Config", {}).get("Image", "unknown"),
316
+ "name": attrs.get("Name", "unknown").lstrip("/"),
317
+ "status": attrs.get("State", {}).get("Status", "unknown"),
318
+ "ports": ", ".join(port_mappings) if port_mappings else "none",
319
+ "created": created_str,
320
+ }
321
+ result.append(simplified)
256
322
  self.log_action("list_containers", params, result)
257
323
  return result
258
324
  except Exception as e:
@@ -288,15 +354,127 @@ class DockerManager(ContainerManagerBase):
288
354
  volumes=volumes,
289
355
  environment=environment,
290
356
  )
291
- result = (
292
- container.attrs if detach else {"output": container.decode("utf-8")}
293
- )
357
+ if not detach:
358
+ result = {"output": container.decode("utf-8") if container else ""}
359
+ self.log_action("run_container", params, result)
360
+ return result
361
+ attrs = container.attrs
362
+ ports = attrs.get("NetworkSettings", {}).get("Ports", {})
363
+ port_mappings = []
364
+ for container_port, host_ports in ports.items():
365
+ if host_ports:
366
+ for hp in host_ports:
367
+ port_mappings.append(
368
+ f"{hp.get('HostIp', '0.0.0.0')}:{hp.get('HostPort')}->{container_port}"
369
+ )
370
+ created = attrs.get("Created", None)
371
+ created_str = self._parse_timestamp(created)
372
+ result = {
373
+ "id": attrs.get("Id", "unknown")[7:19],
374
+ "image": attrs.get("Config", {}).get("Image", image),
375
+ "name": attrs.get("Name", name or "unknown").lstrip("/"),
376
+ "status": attrs.get("State", {}).get("Status", "unknown"),
377
+ "ports": ", ".join(port_mappings) if port_mappings else "none",
378
+ "created": created_str,
379
+ }
294
380
  self.log_action("run_container", params, result)
295
381
  return result
296
382
  except Exception as e:
297
383
  self.log_action("run_container", params, error=e)
298
384
  raise RuntimeError(f"Failed to run container: {str(e)}")
299
385
 
386
+ def list_networks(self) -> List[Dict]:
387
+ params = {}
388
+ try:
389
+ networks = self.client.networks.list()
390
+ result = []
391
+ for net in networks:
392
+ attrs = net.attrs
393
+ containers = len(attrs.get("Containers", {}))
394
+ created = attrs.get("Created", None)
395
+ created_str = self._parse_timestamp(created)
396
+ simplified = {
397
+ "id": attrs.get("Id", "unknown")[7:19],
398
+ "name": attrs.get("Name", "unknown"),
399
+ "driver": attrs.get("Driver", "unknown"),
400
+ "scope": attrs.get("Scope", "unknown"),
401
+ "containers": containers,
402
+ "created": created_str,
403
+ }
404
+ result.append(simplified)
405
+ self.log_action("list_networks", params, result)
406
+ return result
407
+ except Exception as e:
408
+ self.log_action("list_networks", params, error=e)
409
+ raise RuntimeError(f"Failed to list networks: {str(e)}")
410
+
411
+ def create_network(self, name: str, driver: str = "bridge") -> Dict:
412
+ params = {"name": name, "driver": driver}
413
+ try:
414
+ network = self.client.networks.create(name, driver=driver)
415
+ attrs = network.attrs
416
+ created = attrs.get("Created", None)
417
+ created_str = self._parse_timestamp(created)
418
+ result = {
419
+ "id": attrs.get("Id", "unknown")[7:19],
420
+ "name": attrs.get("Name", name),
421
+ "driver": attrs.get("Driver", driver),
422
+ "scope": attrs.get("Scope", "unknown"),
423
+ "created": created_str,
424
+ }
425
+ self.log_action("create_network", params, result)
426
+ return result
427
+ except Exception as e:
428
+ self.log_action("create_network", params, error=e)
429
+ raise RuntimeError(f"Failed to create network: {str(e)}")
430
+
431
+ def get_version(self) -> Dict:
432
+ params = {}
433
+ try:
434
+ version = self.client.version()
435
+ result = {
436
+ "version": version.get("Version", "unknown"),
437
+ "api_version": version.get("ApiVersion", "unknown"),
438
+ "os": version.get("Os", "unknown"),
439
+ "arch": version.get("Arch", "unknown"),
440
+ "build_time": version.get("BuildTime", "unknown"),
441
+ }
442
+ self.log_action("get_version", params, result)
443
+ return result
444
+ except Exception as e:
445
+ self.log_action("get_version", params, error=e)
446
+ raise RuntimeError(f"Failed to get version: {str(e)}")
447
+
448
+ def get_info(self) -> Dict:
449
+ params = {}
450
+ try:
451
+ info = self.client.info()
452
+ result = {
453
+ "containers_total": info.get("Containers", 0),
454
+ "containers_running": info.get("ContainersRunning", 0),
455
+ "images": info.get("Images", 0),
456
+ "driver": info.get("Driver", "unknown"),
457
+ "platform": f"{info.get('OperatingSystem', 'unknown')} {info.get('Architecture', 'unknown')}",
458
+ "memory_total": self._format_size(info.get("MemTotal", 0)),
459
+ "swap_total": self._format_size(info.get("SwapTotal", 0)),
460
+ }
461
+ self.log_action("get_info", params, result)
462
+ return result
463
+ except Exception as e:
464
+ self.log_action("get_info", params, error=e)
465
+ raise RuntimeError(f"Failed to get info: {str(e)}")
466
+
467
+ def remove_image(self, image: str, force: bool = False) -> Dict:
468
+ params = {"image": image, "force": force}
469
+ try:
470
+ self.client.images.remove(image, force=force)
471
+ result = {"removed": image}
472
+ self.log_action("remove_image", params, result)
473
+ return result
474
+ except Exception as e:
475
+ self.log_action("remove_image", params, error=e)
476
+ raise RuntimeError(f"Failed to remove image: {str(e)}")
477
+
300
478
  def stop_container(self, container_id: str, timeout: int = 10) -> Dict:
301
479
  params = {"container_id": container_id, "timeout": timeout}
302
480
  try:
@@ -326,7 +504,9 @@ class DockerManager(ContainerManagerBase):
326
504
  try:
327
505
  container = self.client.containers.get(container_id)
328
506
  logs = container.logs(tail=tail).decode("utf-8")
329
- self.log_action("get_container_logs", params, logs)
507
+ self.log_action(
508
+ "get_container_logs", params, logs[:1000]
509
+ ) # Truncate for logging
330
510
  return logs
331
511
  except Exception as e:
332
512
  self.log_action("get_container_logs", params, error=e)
@@ -341,7 +521,8 @@ class DockerManager(ContainerManagerBase):
341
521
  exit_code, output = container.exec_run(command, detach=detach)
342
522
  result = {
343
523
  "exit_code": exit_code,
344
- "output": output.decode("utf-8") if output else None,
524
+ "output": output.decode("utf-8") if output and not detach else None,
525
+ "command": command,
345
526
  }
346
527
  self.log_action("exec_in_container", params, result)
347
528
  return result
@@ -353,7 +534,17 @@ class DockerManager(ContainerManagerBase):
353
534
  params = {}
354
535
  try:
355
536
  volumes = self.client.volumes.list()
356
- result = {"volumes": [v.attrs for v in volumes]}
537
+ result = {
538
+ "volumes": [
539
+ {
540
+ "name": v.attrs.get("Name", "unknown"),
541
+ "driver": v.attrs.get("Driver", "unknown"),
542
+ "mountpoint": v.attrs.get("Mountpoint", "unknown"),
543
+ "created": v.attrs.get("CreatedAt", "unknown"),
544
+ }
545
+ for v in volumes
546
+ ]
547
+ }
357
548
  self.log_action("list_volumes", params, result)
358
549
  return result
359
550
  except Exception as e:
@@ -364,7 +555,13 @@ class DockerManager(ContainerManagerBase):
364
555
  params = {"name": name}
365
556
  try:
366
557
  volume = self.client.volumes.create(name=name)
367
- result = volume.attrs
558
+ attrs = volume.attrs
559
+ result = {
560
+ "name": attrs.get("Name", name),
561
+ "driver": attrs.get("Driver", "unknown"),
562
+ "mountpoint": attrs.get("Mountpoint", "unknown"),
563
+ "created": attrs.get("CreatedAt", "unknown"),
564
+ }
368
565
  self.log_action("create_volume", params, result)
369
566
  return result
370
567
  except Exception as e:
@@ -383,28 +580,6 @@ class DockerManager(ContainerManagerBase):
383
580
  self.log_action("remove_volume", params, error=e)
384
581
  raise RuntimeError(f"Failed to remove volume: {str(e)}")
385
582
 
386
- def list_networks(self) -> List[Dict]:
387
- params = {}
388
- try:
389
- networks = self.client.networks.list()
390
- result = [net.attrs for net in networks]
391
- self.log_action("list_networks", params, result)
392
- return result
393
- except Exception as e:
394
- self.log_action("list_networks", params, error=e)
395
- raise RuntimeError(f"Failed to list networks: {str(e)}")
396
-
397
- def create_network(self, name: str, driver: str = "bridge") -> Dict:
398
- params = {"name": name, "driver": driver}
399
- try:
400
- network = self.client.networks.create(name, driver=driver)
401
- result = network.attrs
402
- self.log_action("create_network", params, result)
403
- return result
404
- except Exception as e:
405
- self.log_action("create_network", params, error=e)
406
- raise RuntimeError(f"Failed to create network: {str(e)}")
407
-
408
583
  def remove_network(self, network_id: str) -> Dict:
409
584
  params = {"network_id": network_id}
410
585
  try:
@@ -503,7 +678,23 @@ class DockerManager(ContainerManagerBase):
503
678
  params = {}
504
679
  try:
505
680
  nodes = self.client.nodes.list()
506
- result = [node.attrs for node in nodes]
681
+ result = []
682
+ for node in nodes:
683
+ attrs = node.attrs
684
+ spec = attrs.get("Spec", {})
685
+ status = attrs.get("Status", {})
686
+ created = attrs.get("CreatedAt", "unknown")
687
+ updated = attrs.get("UpdatedAt", "unknown")
688
+ simplified = {
689
+ "id": attrs.get("ID", "unknown")[7:19],
690
+ "hostname": spec.get("Name", "unknown"),
691
+ "role": spec.get("Role", "unknown"),
692
+ "status": status.get("State", "unknown"),
693
+ "availability": spec.get("Availability", "unknown"),
694
+ "created": created,
695
+ "updated": updated,
696
+ }
697
+ result.append(simplified)
507
698
  self.log_action("list_nodes", params, result)
508
699
  return result
509
700
  except Exception as e:
@@ -514,7 +705,33 @@ class DockerManager(ContainerManagerBase):
514
705
  params = {}
515
706
  try:
516
707
  services = self.client.services.list()
517
- result = [service.attrs for service in services]
708
+ result = []
709
+ for service in services:
710
+ attrs = service.attrs
711
+ spec = attrs.get("Spec", {})
712
+ endpoint = attrs.get("Endpoint", {})
713
+ ports = endpoint.get("Ports", [])
714
+ port_mappings = [
715
+ f"{p.get('PublishedPort')}->{p.get('TargetPort')}/{p.get('Protocol')}"
716
+ for p in ports
717
+ if p.get("PublishedPort")
718
+ ]
719
+ created = attrs.get("CreatedAt", "unknown")
720
+ updated = attrs.get("UpdatedAt", "unknown")
721
+ simplified = {
722
+ "id": attrs.get("ID", "unknown")[7:19],
723
+ "name": spec.get("Name", "unknown"),
724
+ "image": spec.get("TaskTemplate", {})
725
+ .get("ContainerSpec", {})
726
+ .get("Image", "unknown"),
727
+ "replicas": spec.get("Mode", {})
728
+ .get("Replicated", {})
729
+ .get("Replicas", 0),
730
+ "ports": ", ".join(port_mappings) if port_mappings else "none",
731
+ "created": created,
732
+ "updated": updated,
733
+ }
734
+ result.append(simplified)
518
735
  self.log_action("list_services", params, result)
519
736
  return result
520
737
  except Exception as e:
@@ -538,15 +755,46 @@ class DockerManager(ContainerManagerBase):
538
755
  }
539
756
  try:
540
757
  mode = {"mode": "replicated", "replicas": replicas}
541
- target_ports = [docker.types.EndpointSpec(ports=ports)] if ports else None
758
+ endpoint_spec = None
759
+ if ports:
760
+ port_list = [
761
+ {
762
+ "Protocol": "tcp",
763
+ "PublishedPort": int(host_port),
764
+ "TargetPort": int(container_port.split("/")[0]),
765
+ }
766
+ for container_port, host_port in ports.items()
767
+ ]
768
+ endpoint_spec = docker.types.EndpointSpec(ports=port_list)
542
769
  service = self.client.services.create(
543
770
  image,
544
771
  name=name,
545
772
  mode=mode,
546
773
  mounts=mounts,
547
- endpoint_spec=target_ports[0] if target_ports else None,
774
+ endpoint_spec=endpoint_spec,
548
775
  )
549
- result = service.attrs
776
+ attrs = service.attrs
777
+ spec = attrs.get("Spec", {})
778
+ endpoint = attrs.get("Endpoint", {})
779
+ ports = endpoint.get("Ports", [])
780
+ port_mappings = [
781
+ f"{p.get('PublishedPort')}->{p.get('TargetPort')}/{p.get('Protocol')}"
782
+ for p in ports
783
+ if p.get("PublishedPort")
784
+ ]
785
+ created = attrs.get("CreatedAt", "unknown")
786
+ result = {
787
+ "id": attrs.get("ID", "unknown")[7:19],
788
+ "name": spec.get("Name", name),
789
+ "image": spec.get("TaskTemplate", {})
790
+ .get("ContainerSpec", {})
791
+ .get("Image", image),
792
+ "replicas": spec.get("Mode", {})
793
+ .get("Replicated", {})
794
+ .get("Replicas", replicas),
795
+ "ports": ", ".join(port_mappings) if port_mappings else "none",
796
+ "created": created,
797
+ }
550
798
  self.log_action("create_service", params, result)
551
799
  return result
552
800
  except Exception as e:
@@ -577,31 +825,34 @@ class PodmanManager(ContainerManagerBase):
577
825
  self.logger.error(f"Failed to connect to Podman daemon: {str(e)}")
578
826
  raise RuntimeError(f"Failed to connect to Podman: {str(e)}")
579
827
 
580
- def get_version(self) -> Dict:
581
- params = {}
582
- try:
583
- result = self.client.version()
584
- self.log_action("get_version", params, result)
585
- return result
586
- except Exception as e:
587
- self.log_action("get_version", params, error=e)
588
- raise RuntimeError(f"Failed to get version: {str(e)}")
589
-
590
- def get_info(self) -> Dict:
591
- params = {}
592
- try:
593
- result = self.client.info()
594
- self.log_action("get_info", params, result)
595
- return result
596
- except Exception as e:
597
- self.log_action("get_info", params, error=e)
598
- raise RuntimeError(f"Failed to get info: {str(e)}")
599
-
600
828
  def list_images(self) -> List[Dict]:
601
829
  params = {}
602
830
  try:
603
831
  images = self.client.images.list()
604
- result = [img.attrs for img in images]
832
+ result = []
833
+ for img in images:
834
+ attrs = img.attrs
835
+ repo_tags = attrs.get("Names", [])
836
+ repo_tag = repo_tags[0] if repo_tags else "<none>:<none>"
837
+ repository, tag = (
838
+ repo_tag.rsplit(":", 1) if ":" in repo_tag else ("<none>", "<none>")
839
+ )
840
+ created = attrs.get("Created", None)
841
+ created_str = self._parse_timestamp(created)
842
+ size_bytes = attrs.get("Size", 0)
843
+ size_str = self._format_size(size_bytes) if size_bytes else "0B"
844
+ simplified = {
845
+ "repository": repository,
846
+ "tag": tag,
847
+ "id": (
848
+ attrs.get("Id", "unknown")[7:19]
849
+ if attrs.get("Id")
850
+ else "unknown"
851
+ ),
852
+ "created": created_str,
853
+ "size": size_str,
854
+ }
855
+ result.append(simplified)
605
856
  self.log_action("list_images", params, result)
606
857
  return result
607
858
  except Exception as e:
@@ -614,29 +865,55 @@ class PodmanManager(ContainerManagerBase):
614
865
  params = {"image": image, "tag": tag, "platform": platform}
615
866
  try:
616
867
  img = self.client.images.pull(f"{image}:{tag}", platform=platform)
617
- result = img[0].attrs if isinstance(img, list) else img.attrs
868
+ attrs = img[0].attrs if isinstance(img, list) else img.attrs
869
+ repo_tags = attrs.get("Names", [])
870
+ repo_tag = repo_tags[0] if repo_tags else f"{image}:{tag}"
871
+ repository, tag = (
872
+ repo_tag.rsplit(":", 1) if ":" in repo_tag else (image, tag)
873
+ )
874
+ created = attrs.get("Created", None)
875
+ created_str = self._parse_timestamp(created)
876
+ size_bytes = attrs.get("Size", 0)
877
+ size_str = self._format_size(size_bytes) if size_bytes else "0B"
878
+ result = {
879
+ "repository": repository,
880
+ "tag": tag,
881
+ "id": (
882
+ attrs.get("Id", "unknown")[7:19] if attrs.get("Id") else "unknown"
883
+ ),
884
+ "created": created_str,
885
+ "size": size_str,
886
+ }
618
887
  self.log_action("pull_image", params, result)
619
888
  return result
620
889
  except Exception as e:
621
890
  self.log_action("pull_image", params, error=e)
622
891
  raise RuntimeError(f"Failed to pull image: {str(e)}")
623
892
 
624
- def remove_image(self, image: str, force: bool = False) -> Dict:
625
- params = {"image": image, "force": force}
626
- try:
627
- self.client.images.remove(image, force=force)
628
- result = {"removed": image}
629
- self.log_action("remove_image", params, result)
630
- return result
631
- except Exception as e:
632
- self.log_action("remove_image", params, error=e)
633
- raise RuntimeError(f"Failed to remove image: {str(e)}")
634
-
635
893
  def list_containers(self, all: bool = False) -> List[Dict]:
636
894
  params = {"all": all}
637
895
  try:
638
896
  containers = self.client.containers.list(all=all)
639
- result = [c.attrs for c in containers]
897
+ result = []
898
+ for c in containers:
899
+ attrs = c.attrs
900
+ ports = attrs.get("Ports", [])
901
+ port_mappings = [
902
+ f"{p.get('host_ip', '0.0.0.0')}:{p.get('host_port')}->{p.get('container_port')}/{p.get('protocol', 'tcp')}"
903
+ for p in ports
904
+ if p.get("host_port")
905
+ ]
906
+ created = attrs.get("Created", None)
907
+ created_str = self._parse_timestamp(created)
908
+ simplified = {
909
+ "id": attrs.get("Id", "unknown")[7:19],
910
+ "image": attrs.get("Image", "unknown"),
911
+ "name": attrs.get("Names", ["unknown"])[0].lstrip("/"),
912
+ "status": attrs.get("State", "unknown"),
913
+ "ports": ", ".join(port_mappings) if port_mappings else "none",
914
+ "created": created_str,
915
+ }
916
+ result.append(simplified)
640
917
  self.log_action("list_containers", params, result)
641
918
  return result
642
919
  except Exception as e:
@@ -672,15 +949,126 @@ class PodmanManager(ContainerManagerBase):
672
949
  volumes=volumes,
673
950
  environment=environment,
674
951
  )
675
- result = (
676
- container.attrs if detach else {"output": container.decode("utf-8")}
677
- )
952
+ if not detach:
953
+ result = {"output": container.decode("utf-8") if container else ""}
954
+ self.log_action("run_container", params, result)
955
+ return result
956
+ attrs = container.attrs
957
+ ports = attrs.get("Ports", [])
958
+ port_mappings = [
959
+ f"{p.get('host_ip', '0.0.0.0')}:{p.get('host_port')}->{p.get('container_port')}/{p.get('protocol', 'tcp')}"
960
+ for p in ports
961
+ if p.get("host_port")
962
+ ]
963
+ created = attrs.get("Created", None)
964
+ created_str = self._parse_timestamp(created)
965
+ result = {
966
+ "id": attrs.get("Id", "unknown")[7:19],
967
+ "image": attrs.get("Image", image),
968
+ "name": attrs.get("Names", [name or "unknown"])[0].lstrip("/"),
969
+ "status": attrs.get("State", "unknown"),
970
+ "ports": ", ".join(port_mappings) if port_mappings else "none",
971
+ "created": created_str,
972
+ }
678
973
  self.log_action("run_container", params, result)
679
974
  return result
680
975
  except Exception as e:
681
976
  self.log_action("run_container", params, error=e)
682
977
  raise RuntimeError(f"Failed to run container: {str(e)}")
683
978
 
979
+ def list_networks(self) -> List[Dict]:
980
+ params = {}
981
+ try:
982
+ networks = self.client.networks.list()
983
+ result = []
984
+ for net in networks:
985
+ attrs = net.attrs
986
+ containers = len(attrs.get("Containers", {}))
987
+ created = attrs.get("Created", None)
988
+ created_str = self._parse_timestamp(created)
989
+ simplified = {
990
+ "id": attrs.get("Id", "unknown")[7:19],
991
+ "name": attrs.get("Name", "unknown"),
992
+ "driver": attrs.get("Driver", "unknown"),
993
+ "scope": attrs.get("Scope", "unknown"),
994
+ "containers": containers,
995
+ "created": created_str,
996
+ }
997
+ result.append(simplified)
998
+ self.log_action("list_networks", params, result)
999
+ return result
1000
+ except Exception as e:
1001
+ self.log_action("list_networks", params, error=e)
1002
+ raise RuntimeError(f"Failed to list networks: {str(e)}")
1003
+
1004
+ def create_network(self, name: str, driver: str = "bridge") -> Dict:
1005
+ params = {"name": name, "driver": driver}
1006
+ try:
1007
+ network = self.client.networks.create(name, driver=driver)
1008
+ attrs = network.attrs
1009
+ created = attrs.get("Created", None)
1010
+ created_str = self._parse_timestamp(created)
1011
+ result = {
1012
+ "id": attrs.get("Id", "unknown")[7:19],
1013
+ "name": attrs.get("Name", name),
1014
+ "driver": attrs.get("Driver", driver),
1015
+ "scope": attrs.get("Scope", "unknown"),
1016
+ "created": created_str,
1017
+ }
1018
+ self.log_action("create_network", params, result)
1019
+ return result
1020
+ except Exception as e:
1021
+ self.log_action("create_network", params, error=e)
1022
+ raise RuntimeError(f"Failed to create network: {str(e)}")
1023
+
1024
+ def get_version(self) -> Dict:
1025
+ params = {}
1026
+ try:
1027
+ version = self.client.version()
1028
+ result = {
1029
+ "version": version.get("Version", "unknown"),
1030
+ "api_version": version.get("APIVersion", "unknown"),
1031
+ "os": version.get("Os", "unknown"),
1032
+ "arch": version.get("Arch", "unknown"),
1033
+ "build_time": version.get("BuildTime", "unknown"),
1034
+ }
1035
+ self.log_action("get_version", params, result)
1036
+ return result
1037
+ except Exception as e:
1038
+ self.log_action("get_version", params, error=e)
1039
+ raise RuntimeError(f"Failed to get version: {str(e)}")
1040
+
1041
+ def get_info(self) -> Dict:
1042
+ params = {}
1043
+ try:
1044
+ info = self.client.info()
1045
+ host = info.get("host", {})
1046
+ result = {
1047
+ "containers_total": info.get("store", {}).get("containers", 0),
1048
+ "containers_running": host.get("runningContainers", 0),
1049
+ "images": info.get("store", {}).get("images", 0),
1050
+ "driver": host.get("graphDriverName", "unknown"),
1051
+ "platform": f"{host.get('os', 'unknown')} {host.get('arch', 'unknown')}",
1052
+ "memory_total": self._format_size(host.get("memTotal", 0)),
1053
+ "swap_total": self._format_size(host.get("swapTotal", 0)),
1054
+ }
1055
+ self.log_action("get_info", params, result)
1056
+ return result
1057
+ except Exception as e:
1058
+ self.log_action("get_info", params, error=e)
1059
+ raise RuntimeError(f"Failed to get info: {str(e)}")
1060
+
1061
+ def remove_image(self, image: str, force: bool = False) -> Dict:
1062
+ params = {"image": image, "force": force}
1063
+ try:
1064
+ self.client.images.remove(image, force=force)
1065
+ result = {"removed": image}
1066
+ self.log_action("remove_image", params, result)
1067
+ return result
1068
+ except Exception as e:
1069
+ self.log_action("remove_image", params, error=e)
1070
+ raise RuntimeError(f"Failed to remove image: {str(e)}")
1071
+
684
1072
  def stop_container(self, container_id: str, timeout: int = 10) -> Dict:
685
1073
  params = {"container_id": container_id, "timeout": timeout}
686
1074
  try:
@@ -710,7 +1098,9 @@ class PodmanManager(ContainerManagerBase):
710
1098
  try:
711
1099
  container = self.client.containers.get(container_id)
712
1100
  logs = container.logs(tail=tail).decode("utf-8")
713
- self.log_action("get_container_logs", params, logs)
1101
+ self.log_action(
1102
+ "get_container_logs", params, logs[:1000]
1103
+ ) # Truncate for logging
714
1104
  return logs
715
1105
  except Exception as e:
716
1106
  self.log_action("get_container_logs", params, error=e)
@@ -725,7 +1115,8 @@ class PodmanManager(ContainerManagerBase):
725
1115
  exit_code, output = container.exec_run(command, detach=detach)
726
1116
  result = {
727
1117
  "exit_code": exit_code,
728
- "output": output.decode("utf-8") if output else None,
1118
+ "output": output.decode("utf-8") if output and not detach else None,
1119
+ "command": command,
729
1120
  }
730
1121
  self.log_action("exec_in_container", params, result)
731
1122
  return result
@@ -737,7 +1128,17 @@ class PodmanManager(ContainerManagerBase):
737
1128
  params = {}
738
1129
  try:
739
1130
  volumes = self.client.volumes.list()
740
- result = {"volumes": [v.attrs for v in volumes]}
1131
+ result = {
1132
+ "volumes": [
1133
+ {
1134
+ "name": v.attrs.get("Name", "unknown"),
1135
+ "driver": v.attrs.get("Driver", "unknown"),
1136
+ "mountpoint": v.attrs.get("Mountpoint", "unknown"),
1137
+ "created": v.attrs.get("CreatedAt", "unknown"),
1138
+ }
1139
+ for v in volumes
1140
+ ]
1141
+ }
741
1142
  self.log_action("list_volumes", params, result)
742
1143
  return result
743
1144
  except Exception as e:
@@ -748,7 +1149,13 @@ class PodmanManager(ContainerManagerBase):
748
1149
  params = {"name": name}
749
1150
  try:
750
1151
  volume = self.client.volumes.create(name=name)
751
- result = volume.attrs
1152
+ attrs = volume.attrs
1153
+ result = {
1154
+ "name": attrs.get("Name", name),
1155
+ "driver": attrs.get("Driver", "unknown"),
1156
+ "mountpoint": attrs.get("Mountpoint", "unknown"),
1157
+ "created": attrs.get("CreatedAt", "unknown"),
1158
+ }
752
1159
  self.log_action("create_volume", params, result)
753
1160
  return result
754
1161
  except Exception as e:
@@ -767,28 +1174,6 @@ class PodmanManager(ContainerManagerBase):
767
1174
  self.log_action("remove_volume", params, error=e)
768
1175
  raise RuntimeError(f"Failed to remove volume: {str(e)}")
769
1176
 
770
- def list_networks(self) -> List[Dict]:
771
- params = {}
772
- try:
773
- networks = self.client.networks.list()
774
- result = [net.attrs for net in networks]
775
- self.log_action("list_networks", params, result)
776
- return result
777
- except Exception as e:
778
- self.log_action("list_networks", params, error=e)
779
- raise RuntimeError(f"Failed to list networks: {str(e)}")
780
-
781
- def create_network(self, name: str, driver: str = "bridge") -> Dict:
782
- params = {"name": name, "driver": driver}
783
- try:
784
- network = self.client.networks.create(name, driver=driver)
785
- result = network.attrs
786
- self.log_action("create_network", params, result)
787
- return result
788
- except Exception as e:
789
- self.log_action("create_network", params, error=e)
790
- raise RuntimeError(f"Failed to create network: {str(e)}")
791
-
792
1177
  def remove_network(self, network_id: str) -> Dict:
793
1178
  params = {"network_id": network_id}
794
1179
  try:
@@ -861,6 +1246,34 @@ class PodmanManager(ContainerManagerBase):
861
1246
  self.log_action("compose_logs", params, error=e)
862
1247
  raise RuntimeError(f"Failed to compose logs: {str(e)}")
863
1248
 
1249
+ def init_swarm(self, advertise_addr: Optional[str] = None) -> Dict:
1250
+ raise NotImplementedError("Swarm not supported in Podman")
1251
+
1252
+ def leave_swarm(self, force: bool = False) -> Dict:
1253
+ raise NotImplementedError("Swarm not supported in Podman")
1254
+
1255
+ def list_nodes(self) -> List[Dict]:
1256
+ raise NotImplementedError("Swarm not supported in Podman")
1257
+
1258
+ def list_services(self) -> List[Dict]:
1259
+ raise NotImplementedError("Swarm not supported in Podman")
1260
+
1261
+ def create_service(
1262
+ self,
1263
+ name: str,
1264
+ image: str,
1265
+ replicas: int = 1,
1266
+ ports: Optional[Dict[str, str]] = None,
1267
+ mounts: Optional[List[str]] = None,
1268
+ ) -> Dict:
1269
+ raise NotImplementedError("Swarm not supported in Podman")
1270
+
1271
+ def remove_service(self, service_id: str) -> Dict:
1272
+ raise NotImplementedError("Swarm not supported in Podman")
1273
+
1274
+
1275
+ # The rest of the file (create_manager, usage, container_manager) remains unchanged
1276
+
864
1277
 
865
1278
  def create_manager(
866
1279
  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.12
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.12*
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.12"
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" }]