container-manager-mcp 0.0.10__py3-none-any.whl → 0.0.12__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- container_manager_mcp/container_manager.py +552 -139
- {container_manager_mcp-0.0.10.dist-info → container_manager_mcp-0.0.12.dist-info}/METADATA +2 -2
- container_manager_mcp-0.0.12.dist-info/RECORD +10 -0
- container_manager_mcp-0.0.10.dist-info/RECORD +0 -10
- {container_manager_mcp-0.0.10.dist-info → container_manager_mcp-0.0.12.dist-info}/WHEEL +0 -0
- {container_manager_mcp-0.0.10.dist-info → container_manager_mcp-0.0.12.dist-info}/entry_points.txt +0 -0
- {container_manager_mcp-0.0.10.dist-info → container_manager_mcp-0.0.12.dist-info}/licenses/LICENSE +0 -0
- {container_manager_mcp-0.0.10.dist-info → container_manager_mcp-0.0.12.dist-info}/top_level.txt +0 -0
@@ -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
|
-
|
184
|
+
@abstractmethod
|
159
185
|
def init_swarm(self, advertise_addr: Optional[str] = None) -> Dict:
|
160
|
-
|
186
|
+
pass
|
161
187
|
|
188
|
+
@abstractmethod
|
162
189
|
def leave_swarm(self, force: bool = False) -> Dict:
|
163
|
-
|
190
|
+
pass
|
164
191
|
|
192
|
+
@abstractmethod
|
165
193
|
def list_nodes(self) -> List[Dict]:
|
166
|
-
|
194
|
+
pass
|
167
195
|
|
196
|
+
@abstractmethod
|
168
197
|
def list_services(self) -> List[Dict]:
|
169
|
-
|
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
|
-
|
209
|
+
pass
|
180
210
|
|
211
|
+
@abstractmethod
|
181
212
|
def remove_service(self, service_id: str) -> Dict:
|
182
|
-
|
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 = [
|
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
|
-
|
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 = [
|
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
|
-
|
292
|
-
|
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(
|
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 = {
|
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
|
-
|
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 = [
|
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 = [
|
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
|
-
|
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=
|
774
|
+
endpoint_spec=endpoint_spec,
|
548
775
|
)
|
549
|
-
|
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 = [
|
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
|
-
|
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 = [
|
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
|
-
|
676
|
-
|
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(
|
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 = {
|
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
|
-
|
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.
|
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
|

|
49
49
|

|
50
50
|
|
51
|
-
*Version: 0.0.
|
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
|
|
@@ -0,0 +1,10 @@
|
|
1
|
+
container_manager_mcp/__init__.py,sha256=N3bhKd_oh5YmBBl9N1omfZgaXhJyP0vOzH4VKxs68_g,506
|
2
|
+
container_manager_mcp/__main__.py,sha256=zic5tX336HG8LfdzQQ0sDVx-tMSOsgOZCtaxHWgJ4Go,134
|
3
|
+
container_manager_mcp/container_manager.py,sha256=hIdXyI7L2kFrJc_7bMOaQpKAPECxCVjaQoohqjmY-Iw,67859
|
4
|
+
container_manager_mcp/container_manager_mcp.py,sha256=cIAN8YGflQ9nZEHodkYdQKo2GECAuTH7WB-oMhnAd_0,37959
|
5
|
+
container_manager_mcp-0.0.12.dist-info/licenses/LICENSE,sha256=Z1xmcrPHBnGCETO_LLQJUeaSNBSnuptcDVTt4kaPUOE,1060
|
6
|
+
container_manager_mcp-0.0.12.dist-info/METADATA,sha256=ItKjQvWeRj9e8J26zMjGilRfuFPywXf6afihA_knBdA,8240
|
7
|
+
container_manager_mcp-0.0.12.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
8
|
+
container_manager_mcp-0.0.12.dist-info/entry_points.txt,sha256=I23pXcCgAShlfYbENzs3kbw3l1lU9Gy7lODPfRqeeiA,156
|
9
|
+
container_manager_mcp-0.0.12.dist-info/top_level.txt,sha256=B7QQLOd9mBdu0lsPKqyu4T8-zUtbqKzQJbMbtAzoozU,22
|
10
|
+
container_manager_mcp-0.0.12.dist-info/RECORD,,
|
@@ -1,10 +0,0 @@
|
|
1
|
-
container_manager_mcp/__init__.py,sha256=N3bhKd_oh5YmBBl9N1omfZgaXhJyP0vOzH4VKxs68_g,506
|
2
|
-
container_manager_mcp/__main__.py,sha256=zic5tX336HG8LfdzQQ0sDVx-tMSOsgOZCtaxHWgJ4Go,134
|
3
|
-
container_manager_mcp/container_manager.py,sha256=WJDWAJezb8bucHWYOXuSfT0MZDkpReuVs7sL26PO7nM,49900
|
4
|
-
container_manager_mcp/container_manager_mcp.py,sha256=cIAN8YGflQ9nZEHodkYdQKo2GECAuTH7WB-oMhnAd_0,37959
|
5
|
-
container_manager_mcp-0.0.10.dist-info/licenses/LICENSE,sha256=Z1xmcrPHBnGCETO_LLQJUeaSNBSnuptcDVTt4kaPUOE,1060
|
6
|
-
container_manager_mcp-0.0.10.dist-info/METADATA,sha256=kgTYfp-2ITrb0oGOLcRgIDchWITcMkyxpPP7fXlnmng,8240
|
7
|
-
container_manager_mcp-0.0.10.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
8
|
-
container_manager_mcp-0.0.10.dist-info/entry_points.txt,sha256=I23pXcCgAShlfYbENzs3kbw3l1lU9Gy7lODPfRqeeiA,156
|
9
|
-
container_manager_mcp-0.0.10.dist-info/top_level.txt,sha256=B7QQLOd9mBdu0lsPKqyu4T8-zUtbqKzQJbMbtAzoozU,22
|
10
|
-
container_manager_mcp-0.0.10.dist-info/RECORD,,
|
File without changes
|
{container_manager_mcp-0.0.10.dist-info → container_manager_mcp-0.0.12.dist-info}/entry_points.txt
RENAMED
File without changes
|
{container_manager_mcp-0.0.10.dist-info → container_manager_mcp-0.0.12.dist-info}/licenses/LICENSE
RENAMED
File without changes
|
{container_manager_mcp-0.0.10.dist-info → container_manager_mcp-0.0.12.dist-info}/top_level.txt
RENAMED
File without changes
|