container-manager-mcp 0.0.2__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.
@@ -0,0 +1,1134 @@
1
+ #!/usr/bin/env python
2
+ # coding: utf-8
3
+
4
+ import getopt
5
+ import os
6
+ import sys
7
+ import logging
8
+ from typing import Optional, List, Dict
9
+
10
+ from fastmcp import FastMCP, Context
11
+ from pydantic import Field
12
+ from container_manager import create_manager
13
+
14
+
15
+ def setup_logging(
16
+ is_mcp_server: bool = False, log_file: str = "container_manager_mcp.log"
17
+ ):
18
+ logging.basicConfig(
19
+ filename=log_file,
20
+ level=logging.INFO,
21
+ format="%(asctime)s - %(levelname)s - %(message)s",
22
+ )
23
+ logger = logging.getLogger(__name__)
24
+ logger.info(f"MCP server logging initialized to {log_file}")
25
+
26
+
27
+ mcp = FastMCP(name="ContainerManagerServer")
28
+
29
+
30
+ def to_boolean(string):
31
+ normalized = str(string).strip().lower()
32
+ true_values = {"t", "true", "y", "yes", "1"}
33
+ false_values = {"f", "false", "n", "no", "0"}
34
+ if normalized in true_values:
35
+ return True
36
+ elif normalized in false_values:
37
+ return False
38
+ else:
39
+ raise ValueError(f"Cannot convert '{string}' to boolean")
40
+
41
+
42
+ environment_silent = os.environ.get("SILENT", False)
43
+ environment_log_file = os.environ.get("LOG_FILE", None)
44
+
45
+ if environment_silent:
46
+ environment_silent = to_boolean(environment_silent)
47
+
48
+ # Common tools
49
+
50
+
51
+ @mcp.tool(
52
+ annotations={
53
+ "title": "Get Version",
54
+ "readOnlyHint": True,
55
+ "destructiveHint": False,
56
+ "idempotentHint": True,
57
+ "openWorldHint": False,
58
+ },
59
+ tags={"container_management"},
60
+ )
61
+ async def get_version(
62
+ manager_type: str = Field(
63
+ description="Container manager: docker, podman", default="docker"
64
+ ),
65
+ silent: Optional[bool] = Field(
66
+ description="Suppress output", default=environment_silent
67
+ ),
68
+ log_file: Optional[str] = Field(
69
+ description="Path to log file", default=environment_log_file
70
+ ),
71
+ ctx: Context = Field(
72
+ description="MCP context for progress reporting", default=None
73
+ ),
74
+ ) -> Dict:
75
+ logger = logging.getLogger("ContainerManager")
76
+ logger.debug(
77
+ f"Getting version for {manager_type}, silent: {silent}, log_file: {log_file}"
78
+ )
79
+ try:
80
+ manager = create_manager(manager_type, silent, log_file)
81
+ return manager.get_version()
82
+ except Exception as e:
83
+ logger.error(f"Failed to get version: {str(e)}")
84
+ raise RuntimeError(f"Failed to get version: {str(e)}")
85
+
86
+
87
+ @mcp.tool(
88
+ annotations={
89
+ "title": "Get Info",
90
+ "readOnlyHint": True,
91
+ "destructiveHint": False,
92
+ "idempotentHint": True,
93
+ "openWorldHint": False,
94
+ },
95
+ tags={"container_management"},
96
+ )
97
+ async def get_info(
98
+ manager_type: str = Field(
99
+ description="Container manager: docker, podman", default="docker"
100
+ ),
101
+ silent: Optional[bool] = Field(
102
+ description="Suppress output", default=environment_silent
103
+ ),
104
+ log_file: Optional[str] = Field(
105
+ description="Path to log file", default=environment_log_file
106
+ ),
107
+ ctx: Context = Field(
108
+ description="MCP context for progress reporting", default=None
109
+ ),
110
+ ) -> Dict:
111
+ logger = logging.getLogger("ContainerManager")
112
+ logger.debug(
113
+ f"Getting info for {manager_type}, silent: {silent}, log_file: {log_file}"
114
+ )
115
+ try:
116
+ manager = create_manager(manager_type, silent, log_file)
117
+ return manager.get_info()
118
+ except Exception as e:
119
+ logger.error(f"Failed to get info: {str(e)}")
120
+ raise RuntimeError(f"Failed to get info: {str(e)}")
121
+
122
+
123
+ @mcp.tool(
124
+ annotations={
125
+ "title": "List Images",
126
+ "readOnlyHint": True,
127
+ "destructiveHint": False,
128
+ "idempotentHint": True,
129
+ "openWorldHint": False,
130
+ },
131
+ tags={"container_management"},
132
+ )
133
+ async def list_images(
134
+ manager_type: str = Field(
135
+ description="Container manager: docker, podman", default="docker"
136
+ ),
137
+ silent: Optional[bool] = Field(
138
+ description="Suppress output", default=environment_silent
139
+ ),
140
+ log_file: Optional[str] = Field(
141
+ description="Path to log file", default=environment_log_file
142
+ ),
143
+ ctx: Context = Field(
144
+ description="MCP context for progress reporting", default=None
145
+ ),
146
+ ) -> List[Dict]:
147
+ logger = logging.getLogger("ContainerManager")
148
+ logger.debug(
149
+ f"Listing images for {manager_type}, silent: {silent}, log_file: {log_file}"
150
+ )
151
+ try:
152
+ manager = create_manager(manager_type, silent, log_file)
153
+ return manager.list_images()
154
+ except Exception as e:
155
+ logger.error(f"Failed to list images: {str(e)}")
156
+ raise RuntimeError(f"Failed to list images: {str(e)}")
157
+
158
+
159
+ @mcp.tool(
160
+ annotations={
161
+ "title": "Pull Image",
162
+ "readOnlyHint": False,
163
+ "destructiveHint": False,
164
+ "idempotentHint": True,
165
+ "openWorldHint": False,
166
+ },
167
+ tags={"container_management"},
168
+ )
169
+ async def pull_image(
170
+ image: str = Field(description="Image name to pull"),
171
+ tag: str = Field(description="Image tag", default="latest"),
172
+ platform: Optional[str] = Field(
173
+ description="Platform (e.g., linux/amd64)", default=None
174
+ ),
175
+ manager_type: str = Field(
176
+ description="Container manager: docker, podman", default="docker"
177
+ ),
178
+ silent: Optional[bool] = Field(
179
+ description="Suppress output", default=environment_silent
180
+ ),
181
+ log_file: Optional[str] = Field(
182
+ description="Path to log file", default=environment_log_file
183
+ ),
184
+ ctx: Context = Field(
185
+ description="MCP context for progress reporting", default=None
186
+ ),
187
+ ) -> Dict:
188
+ logger = logging.getLogger("ContainerManager")
189
+ logger.debug(
190
+ f"Pulling image {image}:{tag} for {manager_type}, silent: {silent}, log_file: {log_file}"
191
+ )
192
+ try:
193
+ manager = create_manager(manager_type, silent, log_file)
194
+ return manager.pull_image(image, tag, platform)
195
+ except Exception as e:
196
+ logger.error(f"Failed to pull image: {str(e)}")
197
+ raise RuntimeError(f"Failed to pull image: {str(e)}")
198
+
199
+
200
+ @mcp.tool(
201
+ annotations={
202
+ "title": "Remove Image",
203
+ "readOnlyHint": False,
204
+ "destructiveHint": True,
205
+ "idempotentHint": True,
206
+ "openWorldHint": False,
207
+ },
208
+ tags={"container_management"},
209
+ )
210
+ async def remove_image(
211
+ image: str = Field(description="Image name or ID to remove"),
212
+ force: bool = Field(description="Force removal", default=False),
213
+ manager_type: str = Field(
214
+ description="Container manager: docker, podman", default="docker"
215
+ ),
216
+ silent: Optional[bool] = Field(
217
+ description="Suppress output", default=environment_silent
218
+ ),
219
+ log_file: Optional[str] = Field(
220
+ description="Path to log file", default=environment_log_file
221
+ ),
222
+ ctx: Context = Field(
223
+ description="MCP context for progress reporting", default=None
224
+ ),
225
+ ) -> Dict:
226
+ logger = logging.getLogger("ContainerManager")
227
+ logger.debug(
228
+ f"Removing image {image} for {manager_type}, silent: {silent}, log_file: {log_file}"
229
+ )
230
+ try:
231
+ manager = create_manager(manager_type, silent, log_file)
232
+ return manager.remove_image(image, force)
233
+ except Exception as e:
234
+ logger.error(f"Failed to remove image: {str(e)}")
235
+ raise RuntimeError(f"Failed to remove image: {str(e)}")
236
+
237
+
238
+ @mcp.tool(
239
+ annotations={
240
+ "title": "List Containers",
241
+ "readOnlyHint": True,
242
+ "destructiveHint": False,
243
+ "idempotentHint": True,
244
+ "openWorldHint": False,
245
+ },
246
+ tags={"container_management"},
247
+ )
248
+ async def list_containers(
249
+ all: bool = Field(
250
+ description="Show all containers (default running only)", default=False
251
+ ),
252
+ manager_type: str = Field(
253
+ description="Container manager: docker, podman", default="docker"
254
+ ),
255
+ silent: Optional[bool] = Field(
256
+ description="Suppress output", default=environment_silent
257
+ ),
258
+ log_file: Optional[str] = Field(
259
+ description="Path to log file", default=environment_log_file
260
+ ),
261
+ ctx: Context = Field(
262
+ description="MCP context for progress reporting", default=None
263
+ ),
264
+ ) -> List[Dict]:
265
+ logger = logging.getLogger("ContainerManager")
266
+ logger.debug(
267
+ f"Listing containers for {manager_type}, all: {all}, silent: {silent}, log_file: {log_file}"
268
+ )
269
+ try:
270
+ manager = create_manager(manager_type, silent, log_file)
271
+ return manager.list_containers(all)
272
+ except Exception as e:
273
+ logger.error(f"Failed to list containers: {str(e)}")
274
+ raise RuntimeError(f"Failed to list containers: {str(e)}")
275
+
276
+
277
+ @mcp.tool(
278
+ annotations={
279
+ "title": "Run Container",
280
+ "readOnlyHint": False,
281
+ "destructiveHint": True,
282
+ "idempotentHint": False,
283
+ "openWorldHint": False,
284
+ },
285
+ tags={"container_management"},
286
+ )
287
+ async def run_container(
288
+ image: str = Field(description="Image to run"),
289
+ name: Optional[str] = Field(description="Container name", default=None),
290
+ command: Optional[str] = Field(
291
+ description="Command to run in container", default=None
292
+ ),
293
+ detach: bool = Field(description="Run in detached mode", default=False),
294
+ ports: Optional[Dict[str, str]] = Field(
295
+ description="Port mappings {container_port: host_port}", default=None
296
+ ),
297
+ volumes: Optional[Dict[str, Dict]] = Field(
298
+ description="Volume mappings {/host/path: {bind: /container/path, mode: rw}}",
299
+ default=None,
300
+ ),
301
+ environment: Optional[Dict[str, str]] = Field(
302
+ description="Environment variables", default=None
303
+ ),
304
+ manager_type: str = Field(
305
+ description="Container manager: docker, podman", default="docker"
306
+ ),
307
+ silent: Optional[bool] = Field(
308
+ description="Suppress output", default=environment_silent
309
+ ),
310
+ log_file: Optional[str] = Field(
311
+ description="Path to log file", default=environment_log_file
312
+ ),
313
+ ctx: Context = Field(
314
+ description="MCP context for progress reporting", default=None
315
+ ),
316
+ ) -> Dict:
317
+ logger = logging.getLogger("ContainerManager")
318
+ logger.debug(
319
+ f"Running container from {image} for {manager_type}, silent: {silent}, log_file: {log_file}"
320
+ )
321
+ try:
322
+ manager = create_manager(manager_type, silent, log_file)
323
+ return manager.run_container(
324
+ image, name, command, detach, ports, volumes, environment
325
+ )
326
+ except Exception as e:
327
+ logger.error(f"Failed to run container: {str(e)}")
328
+ raise RuntimeError(f"Failed to run container: {str(e)}")
329
+
330
+
331
+ @mcp.tool(
332
+ annotations={
333
+ "title": "Stop Container",
334
+ "readOnlyHint": False,
335
+ "destructiveHint": True,
336
+ "idempotentHint": True,
337
+ "openWorldHint": False,
338
+ },
339
+ tags={"container_management"},
340
+ )
341
+ async def stop_container(
342
+ container_id: str = Field(description="Container ID or name"),
343
+ timeout: int = Field(description="Timeout in seconds", default=10),
344
+ manager_type: str = Field(
345
+ description="Container manager: docker, podman", default="docker"
346
+ ),
347
+ silent: Optional[bool] = Field(
348
+ description="Suppress output", default=environment_silent
349
+ ),
350
+ log_file: Optional[str] = Field(
351
+ description="Path to log file", default=environment_log_file
352
+ ),
353
+ ctx: Context = Field(
354
+ description="MCP context for progress reporting", default=None
355
+ ),
356
+ ) -> Dict:
357
+ logger = logging.getLogger("ContainerManager")
358
+ logger.debug(
359
+ f"Stopping container {container_id} for {manager_type}, silent: {silent}, log_file: {log_file}"
360
+ )
361
+ try:
362
+ manager = create_manager(manager_type, silent, log_file)
363
+ return manager.stop_container(container_id, timeout)
364
+ except Exception as e:
365
+ logger.error(f"Failed to stop container: {str(e)}")
366
+ raise RuntimeError(f"Failed to stop container: {str(e)}")
367
+
368
+
369
+ @mcp.tool(
370
+ annotations={
371
+ "title": "Remove Container",
372
+ "readOnlyHint": False,
373
+ "destructiveHint": True,
374
+ "idempotentHint": True,
375
+ "openWorldHint": False,
376
+ },
377
+ tags={"container_management"},
378
+ )
379
+ async def remove_container(
380
+ container_id: str = Field(description="Container ID or name"),
381
+ force: bool = Field(description="Force removal", default=False),
382
+ manager_type: str = Field(
383
+ description="Container manager: docker, podman", default="docker"
384
+ ),
385
+ silent: Optional[bool] = Field(
386
+ description="Suppress output", default=environment_silent
387
+ ),
388
+ log_file: Optional[str] = Field(
389
+ description="Path to log file", default=environment_log_file
390
+ ),
391
+ ctx: Context = Field(
392
+ description="MCP context for progress reporting", default=None
393
+ ),
394
+ ) -> Dict:
395
+ logger = logging.getLogger("ContainerManager")
396
+ logger.debug(
397
+ f"Removing container {container_id} for {manager_type}, silent: {silent}, log_file: {log_file}"
398
+ )
399
+ try:
400
+ manager = create_manager(manager_type, silent, log_file)
401
+ return manager.remove_container(container_id, force)
402
+ except Exception as e:
403
+ logger.error(f"Failed to remove container: {str(e)}")
404
+ raise RuntimeError(f"Failed to remove container: {str(e)}")
405
+
406
+
407
+ @mcp.tool(
408
+ annotations={
409
+ "title": "Get Container Logs",
410
+ "readOnlyHint": True,
411
+ "destructiveHint": False,
412
+ "idempotentHint": True,
413
+ "openWorldHint": False,
414
+ },
415
+ tags={"container_management"},
416
+ )
417
+ async def get_container_logs(
418
+ container_id: str = Field(description="Container ID or name"),
419
+ tail: str = Field(
420
+ description="Number of lines to show from the end (or 'all')", default="all"
421
+ ),
422
+ manager_type: str = Field(
423
+ description="Container manager: docker, podman", default="docker"
424
+ ),
425
+ silent: Optional[bool] = Field(
426
+ description="Suppress output", default=environment_silent
427
+ ),
428
+ log_file: Optional[str] = Field(
429
+ description="Path to log file", default=environment_log_file
430
+ ),
431
+ ctx: Context = Field(
432
+ description="MCP context for progress reporting", default=None
433
+ ),
434
+ ) -> str:
435
+ logger = logging.getLogger("ContainerManager")
436
+ logger.debug(
437
+ f"Getting logs for container {container_id} for {manager_type}, silent: {silent}, log_file: {log_file}"
438
+ )
439
+ try:
440
+ manager = create_manager(manager_type, silent, log_file)
441
+ return manager.get_container_logs(container_id, tail)
442
+ except Exception as e:
443
+ logger.error(f"Failed to get container logs: {str(e)}")
444
+ raise RuntimeError(f"Failed to get container logs: {str(e)}")
445
+
446
+
447
+ @mcp.tool(
448
+ annotations={
449
+ "title": "Exec in Container",
450
+ "readOnlyHint": False,
451
+ "destructiveHint": True,
452
+ "idempotentHint": False,
453
+ "openWorldHint": False,
454
+ },
455
+ tags={"container_management"},
456
+ )
457
+ async def exec_in_container(
458
+ container_id: str = Field(description="Container ID or name"),
459
+ command: List[str] = Field(description="Command to execute"),
460
+ detach: bool = Field(description="Detach execution", default=False),
461
+ manager_type: str = Field(
462
+ description="Container manager: docker, podman", default="docker"
463
+ ),
464
+ silent: Optional[bool] = Field(
465
+ description="Suppress output", default=environment_silent
466
+ ),
467
+ log_file: Optional[str] = Field(
468
+ description="Path to log file", default=environment_log_file
469
+ ),
470
+ ctx: Context = Field(
471
+ description="MCP context for progress reporting", default=None
472
+ ),
473
+ ) -> Dict:
474
+ logger = logging.getLogger("ContainerManager")
475
+ logger.debug(
476
+ f"Executing {command} in container {container_id} for {manager_type}, silent: {silent}, log_file: {log_file}"
477
+ )
478
+ try:
479
+ manager = create_manager(manager_type, silent, log_file)
480
+ return manager.exec_in_container(container_id, command, detach)
481
+ except Exception as e:
482
+ logger.error(f"Failed to exec in container: {str(e)}")
483
+ raise RuntimeError(f"Failed to exec in container: {str(e)}")
484
+
485
+
486
+ @mcp.tool(
487
+ annotations={
488
+ "title": "List Volumes",
489
+ "readOnlyHint": True,
490
+ "destructiveHint": False,
491
+ "idempotentHint": True,
492
+ "openWorldHint": False,
493
+ },
494
+ tags={"container_management"},
495
+ )
496
+ async def list_volumes(
497
+ manager_type: str = Field(
498
+ description="Container manager: docker, podman", default="docker"
499
+ ),
500
+ silent: Optional[bool] = Field(
501
+ description="Suppress output", default=environment_silent
502
+ ),
503
+ log_file: Optional[str] = Field(
504
+ description="Path to log file", default=environment_log_file
505
+ ),
506
+ ctx: Context = Field(
507
+ description="MCP context for progress reporting", default=None
508
+ ),
509
+ ) -> Dict:
510
+ logger = logging.getLogger("ContainerManager")
511
+ logger.debug(
512
+ f"Listing volumes for {manager_type}, silent: {silent}, log_file: {log_file}"
513
+ )
514
+ try:
515
+ manager = create_manager(manager_type, silent, log_file)
516
+ return manager.list_volumes()
517
+ except Exception as e:
518
+ logger.error(f"Failed to list volumes: {str(e)}")
519
+ raise RuntimeError(f"Failed to list volumes: {str(e)}")
520
+
521
+
522
+ @mcp.tool(
523
+ annotations={
524
+ "title": "Create Volume",
525
+ "readOnlyHint": False,
526
+ "destructiveHint": False,
527
+ "idempotentHint": True,
528
+ "openWorldHint": False,
529
+ },
530
+ tags={"container_management"},
531
+ )
532
+ async def create_volume(
533
+ name: str = Field(description="Volume name"),
534
+ manager_type: str = Field(
535
+ description="Container manager: docker, podman", default="docker"
536
+ ),
537
+ silent: Optional[bool] = Field(
538
+ description="Suppress output", default=environment_silent
539
+ ),
540
+ log_file: Optional[str] = Field(
541
+ description="Path to log file", default=environment_log_file
542
+ ),
543
+ ctx: Context = Field(
544
+ description="MCP context for progress reporting", default=None
545
+ ),
546
+ ) -> Dict:
547
+ logger = logging.getLogger("ContainerManager")
548
+ logger.debug(
549
+ f"Creating volume {name} for {manager_type}, silent: {silent}, log_file: {log_file}"
550
+ )
551
+ try:
552
+ manager = create_manager(manager_type, silent, log_file)
553
+ return manager.create_volume(name)
554
+ except Exception as e:
555
+ logger.error(f"Failed to create volume: {str(e)}")
556
+ raise RuntimeError(f"Failed to create volume: {str(e)}")
557
+
558
+
559
+ @mcp.tool(
560
+ annotations={
561
+ "title": "Remove Volume",
562
+ "readOnlyHint": False,
563
+ "destructiveHint": True,
564
+ "idempotentHint": True,
565
+ "openWorldHint": False,
566
+ },
567
+ tags={"container_management"},
568
+ )
569
+ async def remove_volume(
570
+ name: str = Field(description="Volume name"),
571
+ force: bool = Field(description="Force removal", default=False),
572
+ manager_type: str = Field(
573
+ description="Container manager: docker, podman", default="docker"
574
+ ),
575
+ silent: Optional[bool] = Field(
576
+ description="Suppress output", default=environment_silent
577
+ ),
578
+ log_file: Optional[str] = Field(
579
+ description="Path to log file", default=environment_log_file
580
+ ),
581
+ ctx: Context = Field(
582
+ description="MCP context for progress reporting", default=None
583
+ ),
584
+ ) -> Dict:
585
+ logger = logging.getLogger("ContainerManager")
586
+ logger.debug(
587
+ f"Removing volume {name} for {manager_type}, silent: {silent}, log_file: {log_file}"
588
+ )
589
+ try:
590
+ manager = create_manager(manager_type, silent, log_file)
591
+ return manager.remove_volume(name, force)
592
+ except Exception as e:
593
+ logger.error(f"Failed to remove volume: {str(e)}")
594
+ raise RuntimeError(f"Failed to remove volume: {str(e)}")
595
+
596
+
597
+ @mcp.tool(
598
+ annotations={
599
+ "title": "List Networks",
600
+ "readOnlyHint": True,
601
+ "destructiveHint": False,
602
+ "idempotentHint": True,
603
+ "openWorldHint": False,
604
+ },
605
+ tags={"container_management"},
606
+ )
607
+ async def list_networks(
608
+ manager_type: str = Field(
609
+ description="Container manager: docker, podman", default="docker"
610
+ ),
611
+ silent: Optional[bool] = Field(
612
+ description="Suppress output", default=environment_silent
613
+ ),
614
+ log_file: Optional[str] = Field(
615
+ description="Path to log file", default=environment_log_file
616
+ ),
617
+ ctx: Context = Field(
618
+ description="MCP context for progress reporting", default=None
619
+ ),
620
+ ) -> List[Dict]:
621
+ logger = logging.getLogger("ContainerManager")
622
+ logger.debug(
623
+ f"Listing networks for {manager_type}, silent: {silent}, log_file: {log_file}"
624
+ )
625
+ try:
626
+ manager = create_manager(manager_type, silent, log_file)
627
+ return manager.list_networks()
628
+ except Exception as e:
629
+ logger.error(f"Failed to list networks: {str(e)}")
630
+ raise RuntimeError(f"Failed to list networks: {str(e)}")
631
+
632
+
633
+ @mcp.tool(
634
+ annotations={
635
+ "title": "Create Network",
636
+ "readOnlyHint": False,
637
+ "destructiveHint": False,
638
+ "idempotentHint": True,
639
+ "openWorldHint": False,
640
+ },
641
+ tags={"container_management"},
642
+ )
643
+ async def create_network(
644
+ name: str = Field(description="Network name"),
645
+ driver: str = Field(description="Network driver (e.g., bridge)", default="bridge"),
646
+ manager_type: str = Field(
647
+ description="Container manager: docker, podman", default="docker"
648
+ ),
649
+ silent: Optional[bool] = Field(
650
+ description="Suppress output", default=environment_silent
651
+ ),
652
+ log_file: Optional[str] = Field(
653
+ description="Path to log file", default=environment_log_file
654
+ ),
655
+ ctx: Context = Field(
656
+ description="MCP context for progress reporting", default=None
657
+ ),
658
+ ) -> Dict:
659
+ logger = logging.getLogger("ContainerManager")
660
+ logger.debug(
661
+ f"Creating network {name} for {manager_type}, silent: {silent}, log_file: {log_file}"
662
+ )
663
+ try:
664
+ manager = create_manager(manager_type, silent, log_file)
665
+ return manager.create_network(name, driver)
666
+ except Exception as e:
667
+ logger.error(f"Failed to create network: {str(e)}")
668
+ raise RuntimeError(f"Failed to create network: {str(e)}")
669
+
670
+
671
+ @mcp.tool(
672
+ annotations={
673
+ "title": "Remove Network",
674
+ "readOnlyHint": False,
675
+ "destructiveHint": True,
676
+ "idempotentHint": True,
677
+ "openWorldHint": False,
678
+ },
679
+ tags={"container_management"},
680
+ )
681
+ async def remove_network(
682
+ network_id: str = Field(description="Network ID or name"),
683
+ manager_type: str = Field(
684
+ description="Container manager: docker, podman", default="docker"
685
+ ),
686
+ silent: Optional[bool] = Field(
687
+ description="Suppress output", default=environment_silent
688
+ ),
689
+ log_file: Optional[str] = Field(
690
+ description="Path to log file", default=environment_log_file
691
+ ),
692
+ ctx: Context = Field(
693
+ description="MCP context for progress reporting", default=None
694
+ ),
695
+ ) -> Dict:
696
+ logger = logging.getLogger("ContainerManager")
697
+ logger.debug(
698
+ f"Removing network {network_id} for {manager_type}, silent: {silent}, log_file: {log_file}"
699
+ )
700
+ try:
701
+ manager = create_manager(manager_type, silent, log_file)
702
+ return manager.remove_network(network_id)
703
+ except Exception as e:
704
+ logger.error(f"Failed to remove network: {str(e)}")
705
+ raise RuntimeError(f"Failed to remove network: {str(e)}")
706
+
707
+
708
+ # Swarm-specific tools
709
+
710
+
711
+ @mcp.tool(
712
+ annotations={
713
+ "title": "Init Swarm",
714
+ "readOnlyHint": False,
715
+ "destructiveHint": True,
716
+ "idempotentHint": False,
717
+ "openWorldHint": False,
718
+ },
719
+ tags={"container_management", "swarm"},
720
+ )
721
+ async def init_swarm(
722
+ advertise_addr: Optional[str] = Field(
723
+ description="Advertise address", default=None
724
+ ),
725
+ manager_type: str = Field(description="Must be docker for swarm", default="docker"),
726
+ silent: Optional[bool] = Field(
727
+ description="Suppress output", default=environment_silent
728
+ ),
729
+ log_file: Optional[str] = Field(
730
+ description="Path to log file", default=environment_log_file
731
+ ),
732
+ ctx: Context = Field(
733
+ description="MCP context for progress reporting", default=None
734
+ ),
735
+ ) -> Dict:
736
+ if manager_type != "docker":
737
+ raise ValueError("Swarm operations are only supported on Docker")
738
+ logger = logging.getLogger("ContainerManager")
739
+ logger.debug(
740
+ f"Initializing swarm for {manager_type}, silent: {silent}, log_file: {log_file}"
741
+ )
742
+ try:
743
+ manager = create_manager(manager_type, silent, log_file)
744
+ return manager.init_swarm(advertise_addr)
745
+ except Exception as e:
746
+ logger.error(f"Failed to init swarm: {str(e)}")
747
+ raise RuntimeError(f"Failed to init swarm: {str(e)}")
748
+
749
+
750
+ @mcp.tool(
751
+ annotations={
752
+ "title": "Leave Swarm",
753
+ "readOnlyHint": False,
754
+ "destructiveHint": True,
755
+ "idempotentHint": True,
756
+ "openWorldHint": False,
757
+ },
758
+ tags={"container_management", "swarm"},
759
+ )
760
+ async def leave_swarm(
761
+ force: bool = Field(description="Force leave", default=False),
762
+ manager_type: str = Field(description="Must be docker for swarm", default="docker"),
763
+ silent: Optional[bool] = Field(
764
+ description="Suppress output", default=environment_silent
765
+ ),
766
+ log_file: Optional[str] = Field(
767
+ description="Path to log file", default=environment_log_file
768
+ ),
769
+ ctx: Context = Field(
770
+ description="MCP context for progress reporting", default=None
771
+ ),
772
+ ) -> Dict:
773
+ if manager_type != "docker":
774
+ raise ValueError("Swarm operations are only supported on Docker")
775
+ logger = logging.getLogger("ContainerManager")
776
+ logger.debug(
777
+ f"Leaving swarm for {manager_type}, silent: {silent}, log_file: {log_file}"
778
+ )
779
+ try:
780
+ manager = create_manager(manager_type, silent, log_file)
781
+ return manager.leave_swarm(force)
782
+ except Exception as e:
783
+ logger.error(f"Failed to leave swarm: {str(e)}")
784
+ raise RuntimeError(f"Failed to leave swarm: {str(e)}")
785
+
786
+
787
+ @mcp.tool(
788
+ annotations={
789
+ "title": "List Nodes",
790
+ "readOnlyHint": True,
791
+ "destructiveHint": False,
792
+ "idempotentHint": True,
793
+ "openWorldHint": False,
794
+ },
795
+ tags={"container_management", "swarm"},
796
+ )
797
+ async def list_nodes(
798
+ manager_type: str = Field(description="Must be docker for swarm", default="docker"),
799
+ silent: Optional[bool] = Field(
800
+ description="Suppress output", default=environment_silent
801
+ ),
802
+ log_file: Optional[str] = Field(
803
+ description="Path to log file", default=environment_log_file
804
+ ),
805
+ ctx: Context = Field(
806
+ description="MCP context for progress reporting", default=None
807
+ ),
808
+ ) -> List[Dict]:
809
+ if manager_type != "docker":
810
+ raise ValueError("Swarm operations are only supported on Docker")
811
+ logger = logging.getLogger("ContainerManager")
812
+ logger.debug(
813
+ f"Listing nodes for {manager_type}, silent: {silent}, log_file: {log_file}"
814
+ )
815
+ try:
816
+ manager = create_manager(manager_type, silent, log_file)
817
+ return manager.list_nodes()
818
+ except Exception as e:
819
+ logger.error(f"Failed to list nodes: {str(e)}")
820
+ raise RuntimeError(f"Failed to list nodes: {str(e)}")
821
+
822
+
823
+ @mcp.tool(
824
+ annotations={
825
+ "title": "List Services",
826
+ "readOnlyHint": True,
827
+ "destructiveHint": False,
828
+ "idempotentHint": True,
829
+ "openWorldHint": False,
830
+ },
831
+ tags={"container_management", "swarm"},
832
+ )
833
+ async def list_services(
834
+ manager_type: str = Field(description="Must be docker for swarm", default="docker"),
835
+ silent: Optional[bool] = Field(
836
+ description="Suppress output", default=environment_silent
837
+ ),
838
+ log_file: Optional[str] = Field(
839
+ description="Path to log file", default=environment_log_file
840
+ ),
841
+ ctx: Context = Field(
842
+ description="MCP context for progress reporting", default=None
843
+ ),
844
+ ) -> List[Dict]:
845
+ if manager_type != "docker":
846
+ raise ValueError("Swarm operations are only supported on Docker")
847
+ logger = logging.getLogger("ContainerManager")
848
+ logger.debug(
849
+ f"Listing services for {manager_type}, silent: {silent}, log_file: {log_file}"
850
+ )
851
+ try:
852
+ manager = create_manager(manager_type, silent, log_file)
853
+ return manager.list_services()
854
+ except Exception as e:
855
+ logger.error(f"Failed to list services: {str(e)}")
856
+ raise RuntimeError(f"Failed to list services: {str(e)}")
857
+
858
+
859
+ @mcp.tool(
860
+ annotations={
861
+ "title": "Create Service",
862
+ "readOnlyHint": False,
863
+ "destructiveHint": True,
864
+ "idempotentHint": False,
865
+ "openWorldHint": False,
866
+ },
867
+ tags={"container_management", "swarm"},
868
+ )
869
+ async def create_service(
870
+ name: str = Field(description="Service name"),
871
+ image: str = Field(description="Image for the service"),
872
+ replicas: int = Field(description="Number of replicas", default=1),
873
+ ports: Optional[Dict[str, str]] = Field(
874
+ description="Port mappings {target: published}", default=None
875
+ ),
876
+ mounts: Optional[List[str]] = Field(
877
+ description="Mounts [source:target:mode]", default=None
878
+ ),
879
+ manager_type: str = Field(description="Must be docker for swarm", default="docker"),
880
+ silent: Optional[bool] = Field(
881
+ description="Suppress output", default=environment_silent
882
+ ),
883
+ log_file: Optional[str] = Field(
884
+ description="Path to log file", default=environment_log_file
885
+ ),
886
+ ctx: Context = Field(
887
+ description="MCP context for progress reporting", default=None
888
+ ),
889
+ ) -> Dict:
890
+ if manager_type != "docker":
891
+ raise ValueError("Swarm operations are only supported on Docker")
892
+ logger = logging.getLogger("ContainerManager")
893
+ logger.debug(
894
+ f"Creating service {name} for {manager_type}, silent: {silent}, log_file: {log_file}"
895
+ )
896
+ try:
897
+ manager = create_manager(manager_type, silent, log_file)
898
+ return manager.create_service(name, image, replicas, ports, mounts)
899
+ except Exception as e:
900
+ logger.error(f"Failed to create service: {str(e)}")
901
+ raise RuntimeError(f"Failed to create service: {str(e)}")
902
+
903
+
904
+ @mcp.tool(
905
+ annotations={
906
+ "title": "Remove Service",
907
+ "readOnlyHint": False,
908
+ "destructiveHint": True,
909
+ "idempotentHint": True,
910
+ "openWorldHint": False,
911
+ },
912
+ tags={"container_management", "swarm"},
913
+ )
914
+ async def remove_service(
915
+ service_id: str = Field(description="Service ID or name"),
916
+ manager_type: str = Field(description="Must be docker for swarm", default="docker"),
917
+ silent: Optional[bool] = Field(
918
+ description="Suppress output", default=environment_silent
919
+ ),
920
+ log_file: Optional[str] = Field(
921
+ description="Path to log file", default=environment_log_file
922
+ ),
923
+ ctx: Context = Field(
924
+ description="MCP context for progress reporting", default=None
925
+ ),
926
+ ) -> Dict:
927
+ if manager_type != "docker":
928
+ raise ValueError("Swarm operations are only supported on Docker")
929
+ logger = logging.getLogger("ContainerManager")
930
+ logger.debug(
931
+ f"Removing service {service_id} for {manager_type}, silent: {silent}, log_file: {log_file}"
932
+ )
933
+ try:
934
+ manager = create_manager(manager_type, silent, log_file)
935
+ return manager.remove_service(service_id)
936
+ except Exception as e:
937
+ logger.error(f"Failed to remove service: {str(e)}")
938
+ raise RuntimeError(f"Failed to remove service: {str(e)}")
939
+
940
+
941
+ @mcp.tool(
942
+ annotations={
943
+ "title": "Compose Up",
944
+ "readOnlyHint": False,
945
+ "destructiveHint": True,
946
+ "idempotentHint": False,
947
+ "openWorldHint": False,
948
+ },
949
+ tags={"container_management", "compose"},
950
+ )
951
+ async def compose_up(
952
+ compose_file: str = Field(description="Path to compose file"),
953
+ detach: bool = Field(description="Detach mode", default=True),
954
+ build: bool = Field(description="Build images", default=False),
955
+ manager_type: str = Field(
956
+ description="Container manager: docker, podman", default="docker"
957
+ ),
958
+ silent: Optional[bool] = Field(
959
+ description="Suppress output", default=environment_silent
960
+ ),
961
+ log_file: Optional[str] = Field(
962
+ description="Path to log file", default=environment_log_file
963
+ ),
964
+ ctx: Context = Field(
965
+ description="MCP context for progress reporting", default=None
966
+ ),
967
+ ) -> str:
968
+ logger = logging.getLogger("ContainerManager")
969
+ logger.debug(
970
+ f"Compose up {compose_file} for {manager_type}, silent: {silent}, log_file: {log_file}"
971
+ )
972
+ try:
973
+ manager = create_manager(manager_type, silent, log_file)
974
+ return manager.compose_up(compose_file, detach, build)
975
+ except Exception as e:
976
+ logger.error(f"Failed to compose up: {str(e)}")
977
+ raise RuntimeError(f"Failed to compose up: {str(e)}")
978
+
979
+
980
+ @mcp.tool(
981
+ annotations={
982
+ "title": "Compose Down",
983
+ "readOnlyHint": False,
984
+ "destructiveHint": True,
985
+ "idempotentHint": True,
986
+ "openWorldHint": False,
987
+ },
988
+ tags={"container_management", "compose"},
989
+ )
990
+ async def compose_down(
991
+ compose_file: str = Field(description="Path to compose file"),
992
+ manager_type: str = Field(
993
+ description="Container manager: docker, podman", default="docker"
994
+ ),
995
+ silent: Optional[bool] = Field(
996
+ description="Suppress output", default=environment_silent
997
+ ),
998
+ log_file: Optional[str] = Field(
999
+ description="Path to log file", default=environment_log_file
1000
+ ),
1001
+ ctx: Context = Field(
1002
+ description="MCP context for progress reporting", default=None
1003
+ ),
1004
+ ) -> str:
1005
+ logger = logging.getLogger("ContainerManager")
1006
+ logger.debug(
1007
+ f"Compose down {compose_file} for {manager_type}, silent: {silent}, log_file: {log_file}"
1008
+ )
1009
+ try:
1010
+ manager = create_manager(manager_type, silent, log_file)
1011
+ return manager.compose_down(compose_file)
1012
+ except Exception as e:
1013
+ logger.error(f"Failed to compose down: {str(e)}")
1014
+ raise RuntimeError(f"Failed to compose down: {str(e)}")
1015
+
1016
+
1017
+ @mcp.tool(
1018
+ annotations={
1019
+ "title": "Compose Ps",
1020
+ "readOnlyHint": True,
1021
+ "destructiveHint": False,
1022
+ "idempotentHint": True,
1023
+ "openWorldHint": False,
1024
+ },
1025
+ tags={"container_management", "compose"},
1026
+ )
1027
+ async def compose_ps(
1028
+ compose_file: str = Field(description="Path to compose file"),
1029
+ manager_type: str = Field(
1030
+ description="Container manager: docker, podman", default="docker"
1031
+ ),
1032
+ silent: Optional[bool] = Field(
1033
+ description="Suppress output", default=environment_silent
1034
+ ),
1035
+ log_file: Optional[str] = Field(
1036
+ description="Path to log file", default=environment_log_file
1037
+ ),
1038
+ ctx: Context = Field(
1039
+ description="MCP context for progress reporting", default=None
1040
+ ),
1041
+ ) -> str:
1042
+ logger = logging.getLogger("ContainerManager")
1043
+ logger.debug(
1044
+ f"Compose ps {compose_file} for {manager_type}, silent: {silent}, log_file: {log_file}"
1045
+ )
1046
+ try:
1047
+ manager = create_manager(manager_type, silent, log_file)
1048
+ return manager.compose_ps(compose_file)
1049
+ except Exception as e:
1050
+ logger.error(f"Failed to compose ps: {str(e)}")
1051
+ raise RuntimeError(f"Failed to compose ps: {str(e)}")
1052
+
1053
+
1054
+ @mcp.tool(
1055
+ annotations={
1056
+ "title": "Compose Logs",
1057
+ "readOnlyHint": True,
1058
+ "destructiveHint": False,
1059
+ "idempotentHint": True,
1060
+ "openWorldHint": False,
1061
+ },
1062
+ tags={"container_management", "compose"},
1063
+ )
1064
+ async def compose_logs(
1065
+ compose_file: str = Field(description="Path to compose file"),
1066
+ service: Optional[str] = Field(description="Specific service", default=None),
1067
+ manager_type: str = Field(
1068
+ description="Container manager: docker, podman", default="docker"
1069
+ ),
1070
+ silent: Optional[bool] = Field(
1071
+ description="Suppress output", default=environment_silent
1072
+ ),
1073
+ log_file: Optional[str] = Field(
1074
+ description="Path to log file", default=environment_log_file
1075
+ ),
1076
+ ctx: Context = Field(
1077
+ description="MCP context for progress reporting", default=None
1078
+ ),
1079
+ ) -> str:
1080
+ logger = logging.getLogger("ContainerManager")
1081
+ logger.debug(
1082
+ f"Compose logs {compose_file} for {manager_type}, silent: {silent}, log_file: {log_file}"
1083
+ )
1084
+ try:
1085
+ manager = create_manager(manager_type, silent, log_file)
1086
+ return manager.compose_logs(compose_file, service)
1087
+ except Exception as e:
1088
+ logger.error(f"Failed to compose logs: {str(e)}")
1089
+ raise RuntimeError(f"Failed to compose logs: {str(e)}")
1090
+
1091
+
1092
+ def container_manager_mcp(argv):
1093
+ transport = "stdio"
1094
+ host = "0.0.0.0"
1095
+ port = 8000
1096
+ try:
1097
+ opts, args = getopt.getopt(
1098
+ argv,
1099
+ "ht:h:p:",
1100
+ ["help", "transport=", "host=", "port="],
1101
+ )
1102
+ except getopt.GetoptError:
1103
+ logger = logging.getLogger("ContainerManager")
1104
+ logger.error("Incorrect arguments")
1105
+ sys.exit(2)
1106
+ for opt, arg in opts:
1107
+ if opt in ("-h", "--help"):
1108
+ sys.exit()
1109
+ elif opt in ("-t", "--transport"):
1110
+ transport = arg
1111
+ elif opt in ("-h", "--host"):
1112
+ host = arg
1113
+ elif opt in ("-p", "--port"):
1114
+ try:
1115
+ port = int(arg)
1116
+ if not (0 <= port <= 65535):
1117
+ print(f"Error: Port {arg} is out of valid range (0-65535).")
1118
+ sys.exit(1)
1119
+ except ValueError:
1120
+ print(f"Error: Port {arg} is not a valid integer.")
1121
+ sys.exit(1)
1122
+ setup_logging(is_mcp_server=True, log_file="container_manager_mcp.log")
1123
+ if transport == "stdio":
1124
+ mcp.run(transport="stdio")
1125
+ elif transport == "http":
1126
+ mcp.run(transport="http", host=host, port=port)
1127
+ else:
1128
+ logger = logging.getLogger("ContainerManager")
1129
+ logger.error("Transport not supported")
1130
+ sys.exit(1)
1131
+
1132
+
1133
+ if __name__ == "__main__":
1134
+ container_manager_mcp(sys.argv[1:])