kubectl-mcp-server 1.15.0__py3-none-any.whl → 1.16.0__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,790 @@
1
+ """Argo Rollouts and Flagger progressive delivery toolset for kubectl-mcp-server.
2
+
3
+ Provides tools for managing canary deployments, blue-green deployments, and progressive delivery.
4
+ """
5
+
6
+ import subprocess
7
+ import json
8
+ from typing import Dict, Any, List
9
+ from datetime import datetime
10
+
11
+ try:
12
+ from fastmcp import FastMCP
13
+ from fastmcp.tools import ToolAnnotations
14
+ except ImportError:
15
+ from mcp.server.fastmcp import FastMCP
16
+ from mcp.types import ToolAnnotations
17
+
18
+ from ..k8s_config import _get_kubectl_context_args
19
+ from ..crd_detector import crd_exists
20
+
21
+
22
+ # Argo Rollouts CRDs
23
+ ARGO_ROLLOUT_CRD = "rollouts.argoproj.io"
24
+ ARGO_ANALYSIS_TEMPLATE_CRD = "analysistemplates.argoproj.io"
25
+ ARGO_CLUSTER_ANALYSIS_TEMPLATE_CRD = "clusteranalysistemplates.argoproj.io"
26
+ ARGO_ANALYSIS_RUN_CRD = "analysisruns.argoproj.io"
27
+ ARGO_EXPERIMENT_CRD = "experiments.argoproj.io"
28
+
29
+ # Flagger CRDs
30
+ FLAGGER_CANARY_CRD = "canaries.flagger.app"
31
+ FLAGGER_METRIC_TEMPLATE_CRD = "metrictemplates.flagger.app"
32
+ FLAGGER_ALERT_PROVIDER_CRD = "alertproviders.flagger.app"
33
+
34
+
35
+ def _run_kubectl(args: List[str], context: str = "") -> Dict[str, Any]:
36
+ """Run kubectl command and return result."""
37
+ cmd = ["kubectl"] + _get_kubectl_context_args(context) + args
38
+ try:
39
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
40
+ if result.returncode == 0:
41
+ return {"success": True, "output": result.stdout}
42
+ return {"success": False, "error": result.stderr}
43
+ except subprocess.TimeoutExpired:
44
+ return {"success": False, "error": "Command timed out"}
45
+ except Exception as e:
46
+ return {"success": False, "error": str(e)}
47
+
48
+
49
+ def _get_resources(kind: str, namespace: str = "", context: str = "", label_selector: str = "") -> List[Dict]:
50
+ """Get Kubernetes resources of a specific kind."""
51
+ args = ["get", kind, "-o", "json"]
52
+ if namespace:
53
+ args.extend(["-n", namespace])
54
+ else:
55
+ args.append("-A")
56
+ if label_selector:
57
+ args.extend(["-l", label_selector])
58
+
59
+ result = _run_kubectl(args, context)
60
+ if result["success"]:
61
+ try:
62
+ data = json.loads(result["output"])
63
+ return data.get("items", [])
64
+ except json.JSONDecodeError:
65
+ return []
66
+ return []
67
+
68
+
69
+ def _argo_rollouts_cli_available() -> bool:
70
+ """Check if kubectl-argo-rollouts plugin is available."""
71
+ try:
72
+ result = subprocess.run(["kubectl", "argo", "rollouts", "version"],
73
+ capture_output=True, timeout=5)
74
+ return result.returncode == 0
75
+ except Exception:
76
+ return False
77
+
78
+
79
+ # ============== Argo Rollouts Functions ==============
80
+
81
+ def rollouts_list(
82
+ namespace: str = "",
83
+ context: str = "",
84
+ label_selector: str = ""
85
+ ) -> Dict[str, Any]:
86
+ """List Argo Rollouts with their status.
87
+
88
+ Args:
89
+ namespace: Filter by namespace (empty for all namespaces)
90
+ context: Kubernetes context to use (optional)
91
+ label_selector: Label selector to filter rollouts
92
+
93
+ Returns:
94
+ List of Argo Rollouts with their status
95
+ """
96
+ if not crd_exists(ARGO_ROLLOUT_CRD, context):
97
+ return {
98
+ "success": False,
99
+ "error": "Argo Rollouts is not installed (rollouts.argoproj.io CRD not found)"
100
+ }
101
+
102
+ rollouts = []
103
+ for item in _get_resources("rollouts.argoproj.io", namespace, context, label_selector):
104
+ status = item.get("status", {})
105
+ spec = item.get("spec", {})
106
+
107
+ # Determine strategy
108
+ strategy_spec = spec.get("strategy", {})
109
+ if "canary" in strategy_spec:
110
+ strategy = "canary"
111
+ strategy_details = strategy_spec.get("canary", {})
112
+ elif "blueGreen" in strategy_spec:
113
+ strategy = "blueGreen"
114
+ strategy_details = strategy_spec.get("blueGreen", {})
115
+ else:
116
+ strategy = "unknown"
117
+ strategy_details = {}
118
+
119
+ # Get conditions
120
+ conditions = status.get("conditions", [])
121
+ available_cond = next((c for c in conditions if c.get("type") == "Available"), {})
122
+ progressing_cond = next((c for c in conditions if c.get("type") == "Progressing"), {})
123
+
124
+ rollouts.append({
125
+ "name": item["metadata"]["name"],
126
+ "namespace": item["metadata"]["namespace"],
127
+ "strategy": strategy,
128
+ "phase": status.get("phase", "Unknown"),
129
+ "message": status.get("message", ""),
130
+ "replicas": spec.get("replicas", 1),
131
+ "ready_replicas": status.get("readyReplicas", 0),
132
+ "available_replicas": status.get("availableReplicas", 0),
133
+ "current_step": status.get("currentStepIndex"),
134
+ "total_steps": len(strategy_details.get("steps", [])) if strategy == "canary" else None,
135
+ "stable_rs": status.get("stableRS", ""),
136
+ "canary_rs": status.get("canaryRS", "") if strategy == "canary" else None,
137
+ "active_rs": status.get("blueGreen", {}).get("activeSelector", "") if strategy == "blueGreen" else None,
138
+ "preview_rs": status.get("blueGreen", {}).get("previewSelector", "") if strategy == "blueGreen" else None,
139
+ "available": available_cond.get("status") == "True",
140
+ "progressing": progressing_cond.get("status") == "True",
141
+ "paused": status.get("pauseConditions") is not None,
142
+ "aborted": status.get("abort", False),
143
+ })
144
+
145
+ # Summary
146
+ healthy = sum(1 for r in rollouts if r["phase"] == "Healthy")
147
+ progressing = sum(1 for r in rollouts if r["phase"] == "Progressing")
148
+ paused = sum(1 for r in rollouts if r["paused"])
149
+
150
+ return {
151
+ "context": context or "current",
152
+ "total": len(rollouts),
153
+ "healthy": healthy,
154
+ "progressing": progressing,
155
+ "paused": paused,
156
+ "rollouts": rollouts,
157
+ }
158
+
159
+
160
+ def rollout_get(
161
+ name: str,
162
+ namespace: str,
163
+ context: str = ""
164
+ ) -> Dict[str, Any]:
165
+ """Get detailed information about an Argo Rollout.
166
+
167
+ Args:
168
+ name: Name of the Rollout
169
+ namespace: Namespace of the Rollout
170
+ context: Kubernetes context to use (optional)
171
+
172
+ Returns:
173
+ Detailed Rollout information
174
+ """
175
+ if not crd_exists(ARGO_ROLLOUT_CRD, context):
176
+ return {"success": False, "error": "Argo Rollouts is not installed"}
177
+
178
+ args = ["get", "rollouts.argoproj.io", name, "-n", namespace, "-o", "json"]
179
+ result = _run_kubectl(args, context)
180
+
181
+ if result["success"]:
182
+ try:
183
+ data = json.loads(result["output"])
184
+ return {
185
+ "success": True,
186
+ "context": context or "current",
187
+ "rollout": data,
188
+ }
189
+ except json.JSONDecodeError:
190
+ return {"success": False, "error": "Failed to parse response"}
191
+
192
+ return {"success": False, "error": result.get("error", "Unknown error")}
193
+
194
+
195
+ def rollout_status(
196
+ name: str,
197
+ namespace: str,
198
+ context: str = ""
199
+ ) -> Dict[str, Any]:
200
+ """Get current status of an Argo Rollout with step details.
201
+
202
+ Args:
203
+ name: Name of the Rollout
204
+ namespace: Namespace of the Rollout
205
+ context: Kubernetes context to use (optional)
206
+
207
+ Returns:
208
+ Rollout status with step information
209
+ """
210
+ result = rollout_get(name, namespace, context)
211
+ if not result.get("success"):
212
+ return result
213
+
214
+ rollout = result["rollout"]
215
+ status = rollout.get("status", {})
216
+ spec = rollout.get("spec", {})
217
+ strategy_spec = spec.get("strategy", {})
218
+
219
+ # Determine strategy
220
+ if "canary" in strategy_spec:
221
+ strategy = "canary"
222
+ steps = strategy_spec.get("canary", {}).get("steps", [])
223
+ elif "blueGreen" in strategy_spec:
224
+ strategy = "blueGreen"
225
+ steps = []
226
+ else:
227
+ strategy = "unknown"
228
+ steps = []
229
+
230
+ current_step = status.get("currentStepIndex", 0)
231
+
232
+ # Parse steps
233
+ step_info = []
234
+ for i, step in enumerate(steps):
235
+ step_type = list(step.keys())[0] if step else "unknown"
236
+ step_value = step.get(step_type)
237
+
238
+ step_info.append({
239
+ "index": i,
240
+ "type": step_type,
241
+ "value": step_value,
242
+ "current": i == current_step,
243
+ "completed": i < current_step,
244
+ })
245
+
246
+ return {
247
+ "success": True,
248
+ "context": context or "current",
249
+ "name": name,
250
+ "namespace": namespace,
251
+ "strategy": strategy,
252
+ "phase": status.get("phase", "Unknown"),
253
+ "message": status.get("message", ""),
254
+ "current_step": current_step,
255
+ "total_steps": len(steps),
256
+ "steps": step_info,
257
+ "paused": status.get("pauseConditions") is not None,
258
+ "pause_reasons": [p.get("reason") for p in (status.get("pauseConditions") or [])],
259
+ "canary_weight": status.get("canary", {}).get("weight", 0) if strategy == "canary" else None,
260
+ "stable_revision": status.get("stableRS", ""),
261
+ "canary_revision": status.get("canaryRS", ""),
262
+ }
263
+
264
+
265
+ def rollout_promote(
266
+ name: str,
267
+ namespace: str,
268
+ full: bool = False,
269
+ context: str = ""
270
+ ) -> Dict[str, Any]:
271
+ """Promote a paused Argo Rollout.
272
+
273
+ Args:
274
+ name: Name of the Rollout
275
+ namespace: Namespace of the Rollout
276
+ full: Promote to full healthy state (skip remaining steps)
277
+ context: Kubernetes context to use (optional)
278
+
279
+ Returns:
280
+ Promotion result
281
+ """
282
+ if not crd_exists(ARGO_ROLLOUT_CRD, context):
283
+ return {"success": False, "error": "Argo Rollouts is not installed"}
284
+
285
+ # Use kubectl plugin if available
286
+ if _argo_rollouts_cli_available():
287
+ cmd = ["kubectl", "argo", "rollouts", "promote", name, "-n", namespace]
288
+ if full:
289
+ cmd.append("--full")
290
+ if context:
291
+ cmd.extend(["--context", context])
292
+
293
+ try:
294
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
295
+ if result.returncode == 0:
296
+ return {
297
+ "success": True,
298
+ "context": context or "current",
299
+ "message": f"Promoted rollout {name}" + (" (full)" if full else ""),
300
+ "output": result.stdout,
301
+ }
302
+ return {"success": False, "error": result.stderr}
303
+ except Exception as e:
304
+ return {"success": False, "error": str(e)}
305
+
306
+ # Fallback to patching
307
+ patch = {"status": {"pauseConditions": None}}
308
+ if full:
309
+ patch["status"]["promoteFull"] = True # Full promotion to end
310
+
311
+ args = [
312
+ "patch", "rollouts.argoproj.io", name,
313
+ "-n", namespace,
314
+ "--type=merge",
315
+ "-p", json.dumps(patch)
316
+ ]
317
+ result = _run_kubectl(args, context)
318
+
319
+ if result["success"]:
320
+ return {
321
+ "success": True,
322
+ "context": context or "current",
323
+ "message": f"Promoted rollout {name}",
324
+ }
325
+
326
+ return {"success": False, "error": result.get("error", "Failed to promote")}
327
+
328
+
329
+ def rollout_abort(
330
+ name: str,
331
+ namespace: str,
332
+ context: str = ""
333
+ ) -> Dict[str, Any]:
334
+ """Abort an Argo Rollout.
335
+
336
+ Args:
337
+ name: Name of the Rollout
338
+ namespace: Namespace of the Rollout
339
+ context: Kubernetes context to use (optional)
340
+
341
+ Returns:
342
+ Abort result
343
+ """
344
+ if not crd_exists(ARGO_ROLLOUT_CRD, context):
345
+ return {"success": False, "error": "Argo Rollouts is not installed"}
346
+
347
+ if _argo_rollouts_cli_available():
348
+ cmd = ["kubectl", "argo", "rollouts", "abort", name, "-n", namespace]
349
+ if context:
350
+ cmd.extend(["--context", context])
351
+
352
+ try:
353
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
354
+ if result.returncode == 0:
355
+ return {
356
+ "success": True,
357
+ "context": context or "current",
358
+ "message": f"Aborted rollout {name}",
359
+ "output": result.stdout,
360
+ }
361
+ return {"success": False, "error": result.stderr}
362
+ except Exception as e:
363
+ return {"success": False, "error": str(e)}
364
+
365
+ # Fallback to patching
366
+ patch = {"status": {"abort": True}}
367
+ args = [
368
+ "patch", "rollouts.argoproj.io", name,
369
+ "-n", namespace,
370
+ "--type=merge",
371
+ "-p", json.dumps(patch)
372
+ ]
373
+ result = _run_kubectl(args, context)
374
+
375
+ if result["success"]:
376
+ return {
377
+ "success": True,
378
+ "context": context or "current",
379
+ "message": f"Aborted rollout {name}",
380
+ }
381
+
382
+ return {"success": False, "error": result.get("error", "Failed to abort")}
383
+
384
+
385
+ def rollout_retry(
386
+ name: str,
387
+ namespace: str,
388
+ context: str = ""
389
+ ) -> Dict[str, Any]:
390
+ """Retry an aborted Argo Rollout.
391
+
392
+ Args:
393
+ name: Name of the Rollout
394
+ namespace: Namespace of the Rollout
395
+ context: Kubernetes context to use (optional)
396
+
397
+ Returns:
398
+ Retry result
399
+ """
400
+ if not crd_exists(ARGO_ROLLOUT_CRD, context):
401
+ return {"success": False, "error": "Argo Rollouts is not installed"}
402
+
403
+ if _argo_rollouts_cli_available():
404
+ cmd = ["kubectl", "argo", "rollouts", "retry", name, "-n", namespace]
405
+ if context:
406
+ cmd.extend(["--context", context])
407
+
408
+ try:
409
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
410
+ if result.returncode == 0:
411
+ return {
412
+ "success": True,
413
+ "context": context or "current",
414
+ "message": f"Retried rollout {name}",
415
+ "output": result.stdout,
416
+ }
417
+ return {"success": False, "error": result.stderr}
418
+ except Exception as e:
419
+ return {"success": False, "error": str(e)}
420
+
421
+ # Fallback to patching (clear abort status)
422
+ patch = {"status": {"abort": False}}
423
+ args = [
424
+ "patch", "rollouts.argoproj.io", name,
425
+ "-n", namespace,
426
+ "--type=merge",
427
+ "-p", json.dumps(patch)
428
+ ]
429
+ result = _run_kubectl(args, context)
430
+
431
+ if result["success"]:
432
+ return {
433
+ "success": True,
434
+ "context": context or "current",
435
+ "message": f"Retried rollout {name}",
436
+ }
437
+
438
+ return {"success": False, "error": result.get("error", "Failed to retry")}
439
+
440
+
441
+ def rollout_restart(
442
+ name: str,
443
+ namespace: str,
444
+ context: str = ""
445
+ ) -> Dict[str, Any]:
446
+ """Restart an Argo Rollout.
447
+
448
+ Args:
449
+ name: Name of the Rollout
450
+ namespace: Namespace of the Rollout
451
+ context: Kubernetes context to use (optional)
452
+
453
+ Returns:
454
+ Restart result
455
+ """
456
+ if not crd_exists(ARGO_ROLLOUT_CRD, context):
457
+ return {"success": False, "error": "Argo Rollouts is not installed"}
458
+
459
+ if _argo_rollouts_cli_available():
460
+ cmd = ["kubectl", "argo", "rollouts", "restart", name, "-n", namespace]
461
+ if context:
462
+ cmd.extend(["--context", context])
463
+
464
+ try:
465
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
466
+ if result.returncode == 0:
467
+ return {
468
+ "success": True,
469
+ "context": context or "current",
470
+ "message": f"Restarted rollout {name}",
471
+ "output": result.stdout,
472
+ }
473
+ return {"success": False, "error": result.stderr}
474
+ except Exception as e:
475
+ return {"success": False, "error": str(e)}
476
+
477
+ # Fallback: patch the template to trigger restart
478
+ timestamp = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")
479
+ patch = {
480
+ "spec": {
481
+ "restartAt": timestamp
482
+ }
483
+ }
484
+ args = [
485
+ "patch", "rollouts.argoproj.io", name,
486
+ "-n", namespace,
487
+ "--type=merge",
488
+ "-p", json.dumps(patch)
489
+ ]
490
+ result = _run_kubectl(args, context)
491
+
492
+ if result["success"]:
493
+ return {
494
+ "success": True,
495
+ "context": context or "current",
496
+ "message": f"Restarted rollout {name}",
497
+ }
498
+
499
+ return {"success": False, "error": result.get("error", "Failed to restart")}
500
+
501
+
502
+ def analysis_runs_list(
503
+ namespace: str = "",
504
+ context: str = "",
505
+ label_selector: str = ""
506
+ ) -> Dict[str, Any]:
507
+ """List Argo Rollouts AnalysisRuns.
508
+
509
+ Args:
510
+ namespace: Filter by namespace (empty for all namespaces)
511
+ context: Kubernetes context to use (optional)
512
+ label_selector: Label selector to filter
513
+
514
+ Returns:
515
+ List of AnalysisRuns with their status
516
+ """
517
+ if not crd_exists(ARGO_ANALYSIS_RUN_CRD, context):
518
+ return {
519
+ "success": False,
520
+ "error": "AnalysisRuns CRD not found"
521
+ }
522
+
523
+ runs = []
524
+ for item in _get_resources("analysisruns.argoproj.io", namespace, context, label_selector):
525
+ status = item.get("status", {})
526
+ spec = item.get("spec", {})
527
+
528
+ runs.append({
529
+ "name": item["metadata"]["name"],
530
+ "namespace": item["metadata"]["namespace"],
531
+ "phase": status.get("phase", "Unknown"),
532
+ "message": status.get("message", ""),
533
+ "metrics_count": len(spec.get("metrics", [])),
534
+ "started_at": status.get("startedAt", ""),
535
+ "metric_results": [
536
+ {
537
+ "name": m.get("name"),
538
+ "phase": m.get("phase"),
539
+ "count": m.get("count", 0),
540
+ "successful": m.get("successful", 0),
541
+ "failed": m.get("failed", 0),
542
+ }
543
+ for m in status.get("metricResults", [])
544
+ ],
545
+ })
546
+
547
+ return {
548
+ "context": context or "current",
549
+ "total": len(runs),
550
+ "analysis_runs": runs,
551
+ }
552
+
553
+
554
+ # ============== Flagger Functions ==============
555
+
556
+ def flagger_canaries_list(
557
+ namespace: str = "",
558
+ context: str = "",
559
+ label_selector: str = ""
560
+ ) -> Dict[str, Any]:
561
+ """List Flagger Canary resources.
562
+
563
+ Args:
564
+ namespace: Filter by namespace (empty for all namespaces)
565
+ context: Kubernetes context to use (optional)
566
+ label_selector: Label selector to filter
567
+
568
+ Returns:
569
+ List of Flagger Canaries with their status
570
+ """
571
+ if not crd_exists(FLAGGER_CANARY_CRD, context):
572
+ return {
573
+ "success": False,
574
+ "error": "Flagger is not installed (canaries.flagger.app CRD not found)"
575
+ }
576
+
577
+ canaries = []
578
+ for item in _get_resources("canaries.flagger.app", namespace, context, label_selector):
579
+ status = item.get("status", {})
580
+ spec = item.get("spec", {})
581
+ analysis = spec.get("analysis", {})
582
+
583
+ canaries.append({
584
+ "name": item["metadata"]["name"],
585
+ "namespace": item["metadata"]["namespace"],
586
+ "phase": status.get("phase", "Unknown"),
587
+ "canary_weight": status.get("canaryWeight", 0),
588
+ "failed_checks": status.get("failedChecks", 0),
589
+ "iterations": status.get("iterations", 0),
590
+ "target_ref": spec.get("targetRef", {}),
591
+ "service": spec.get("service", {}),
592
+ "max_weight": analysis.get("maxWeight", 50),
593
+ "step_weight": analysis.get("stepWeight", 5),
594
+ "threshold": analysis.get("threshold", 5),
595
+ "interval": analysis.get("interval", "1m"),
596
+ "metrics_count": len(analysis.get("metrics", [])),
597
+ "last_transition_time": status.get("lastTransitionTime", ""),
598
+ })
599
+
600
+ # Summary
601
+ progressing = sum(1 for c in canaries if c["phase"] == "Progressing")
602
+ succeeded = sum(1 for c in canaries if c["phase"] == "Succeeded")
603
+ failed = sum(1 for c in canaries if c["phase"] == "Failed")
604
+
605
+ return {
606
+ "context": context or "current",
607
+ "total": len(canaries),
608
+ "progressing": progressing,
609
+ "succeeded": succeeded,
610
+ "failed": failed,
611
+ "canaries": canaries,
612
+ }
613
+
614
+
615
+ def flagger_canary_get(
616
+ name: str,
617
+ namespace: str,
618
+ context: str = ""
619
+ ) -> Dict[str, Any]:
620
+ """Get detailed information about a Flagger Canary.
621
+
622
+ Args:
623
+ name: Name of the Canary
624
+ namespace: Namespace of the Canary
625
+ context: Kubernetes context to use (optional)
626
+
627
+ Returns:
628
+ Detailed Canary information
629
+ """
630
+ if not crd_exists(FLAGGER_CANARY_CRD, context):
631
+ return {"success": False, "error": "Flagger is not installed"}
632
+
633
+ args = ["get", "canaries.flagger.app", name, "-n", namespace, "-o", "json"]
634
+ result = _run_kubectl(args, context)
635
+
636
+ if result["success"]:
637
+ try:
638
+ data = json.loads(result["output"])
639
+ return {
640
+ "success": True,
641
+ "context": context or "current",
642
+ "canary": data,
643
+ }
644
+ except json.JSONDecodeError:
645
+ return {"success": False, "error": "Failed to parse response"}
646
+
647
+ return {"success": False, "error": result.get("error", "Unknown error")}
648
+
649
+
650
+ def rollouts_detect(context: str = "") -> Dict[str, Any]:
651
+ """Detect which progressive delivery tools are installed.
652
+
653
+ Args:
654
+ context: Kubernetes context to use (optional)
655
+
656
+ Returns:
657
+ Detection results for Argo Rollouts and Flagger
658
+ """
659
+ return {
660
+ "context": context or "current",
661
+ "argo_rollouts": {
662
+ "installed": crd_exists(ARGO_ROLLOUT_CRD, context),
663
+ "cli_available": _argo_rollouts_cli_available(),
664
+ "crds": {
665
+ "rollouts": crd_exists(ARGO_ROLLOUT_CRD, context),
666
+ "analysistemplates": crd_exists(ARGO_ANALYSIS_TEMPLATE_CRD, context),
667
+ "clusteranalysistemplates": crd_exists(ARGO_CLUSTER_ANALYSIS_TEMPLATE_CRD, context),
668
+ "analysisruns": crd_exists(ARGO_ANALYSIS_RUN_CRD, context),
669
+ "experiments": crd_exists(ARGO_EXPERIMENT_CRD, context),
670
+ },
671
+ },
672
+ "flagger": {
673
+ "installed": crd_exists(FLAGGER_CANARY_CRD, context),
674
+ "crds": {
675
+ "canaries": crd_exists(FLAGGER_CANARY_CRD, context),
676
+ "metrictemplates": crd_exists(FLAGGER_METRIC_TEMPLATE_CRD, context),
677
+ "alertproviders": crd_exists(FLAGGER_ALERT_PROVIDER_CRD, context),
678
+ },
679
+ },
680
+ }
681
+
682
+
683
+ def register_rollouts_tools(mcp: FastMCP, non_destructive: bool = False):
684
+ """Register progressive delivery tools with the MCP server."""
685
+
686
+ # Argo Rollouts tools
687
+ @mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
688
+ def rollouts_list_tool(
689
+ namespace: str = "",
690
+ context: str = "",
691
+ label_selector: str = ""
692
+ ) -> str:
693
+ """List Argo Rollouts with their status."""
694
+ return json.dumps(rollouts_list(namespace, context, label_selector), indent=2)
695
+
696
+ @mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
697
+ def rollout_get_tool(
698
+ name: str,
699
+ namespace: str,
700
+ context: str = ""
701
+ ) -> str:
702
+ """Get detailed information about an Argo Rollout."""
703
+ return json.dumps(rollout_get(name, namespace, context), indent=2)
704
+
705
+ @mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
706
+ def rollout_status_tool(
707
+ name: str,
708
+ namespace: str,
709
+ context: str = ""
710
+ ) -> str:
711
+ """Get current status of an Argo Rollout with step details."""
712
+ return json.dumps(rollout_status(name, namespace, context), indent=2)
713
+
714
+ @mcp.tool()
715
+ def rollout_promote_tool(
716
+ name: str,
717
+ namespace: str,
718
+ full: bool = False,
719
+ context: str = ""
720
+ ) -> str:
721
+ """Promote a paused Argo Rollout to the next step or full."""
722
+ if non_destructive:
723
+ return json.dumps({"success": False, "error": "Operation blocked: non-destructive mode"})
724
+ return json.dumps(rollout_promote(name, namespace, full, context), indent=2)
725
+
726
+ @mcp.tool()
727
+ def rollout_abort_tool(
728
+ name: str,
729
+ namespace: str,
730
+ context: str = ""
731
+ ) -> str:
732
+ """Abort an Argo Rollout."""
733
+ if non_destructive:
734
+ return json.dumps({"success": False, "error": "Operation blocked: non-destructive mode"})
735
+ return json.dumps(rollout_abort(name, namespace, context), indent=2)
736
+
737
+ @mcp.tool()
738
+ def rollout_retry_tool(
739
+ name: str,
740
+ namespace: str,
741
+ context: str = ""
742
+ ) -> str:
743
+ """Retry an aborted Argo Rollout."""
744
+ if non_destructive:
745
+ return json.dumps({"success": False, "error": "Operation blocked: non-destructive mode"})
746
+ return json.dumps(rollout_retry(name, namespace, context), indent=2)
747
+
748
+ @mcp.tool()
749
+ def rollout_restart_tool(
750
+ name: str,
751
+ namespace: str,
752
+ context: str = ""
753
+ ) -> str:
754
+ """Restart an Argo Rollout."""
755
+ if non_destructive:
756
+ return json.dumps({"success": False, "error": "Operation blocked: non-destructive mode"})
757
+ return json.dumps(rollout_restart(name, namespace, context), indent=2)
758
+
759
+ @mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
760
+ def analysis_runs_list_tool(
761
+ namespace: str = "",
762
+ context: str = "",
763
+ label_selector: str = ""
764
+ ) -> str:
765
+ """List Argo Rollouts AnalysisRuns."""
766
+ return json.dumps(analysis_runs_list(namespace, context, label_selector), indent=2)
767
+
768
+ # Flagger tools
769
+ @mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
770
+ def flagger_canaries_list_tool(
771
+ namespace: str = "",
772
+ context: str = "",
773
+ label_selector: str = ""
774
+ ) -> str:
775
+ """List Flagger Canary resources."""
776
+ return json.dumps(flagger_canaries_list(namespace, context, label_selector), indent=2)
777
+
778
+ @mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
779
+ def flagger_canary_get_tool(
780
+ name: str,
781
+ namespace: str,
782
+ context: str = ""
783
+ ) -> str:
784
+ """Get detailed information about a Flagger Canary."""
785
+ return json.dumps(flagger_canary_get(name, namespace, context), indent=2)
786
+
787
+ @mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
788
+ def rollouts_detect_tool(context: str = "") -> str:
789
+ """Detect which progressive delivery tools are installed."""
790
+ return json.dumps(rollouts_detect(context), indent=2)