kubectl-mcp-server 1.14.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.
Files changed (37) hide show
  1. kubectl_mcp_server-1.16.0.dist-info/METADATA +1047 -0
  2. kubectl_mcp_server-1.16.0.dist-info/RECORD +61 -0
  3. kubectl_mcp_tool/__init__.py +1 -1
  4. kubectl_mcp_tool/crd_detector.py +247 -0
  5. kubectl_mcp_tool/k8s_config.py +304 -63
  6. kubectl_mcp_tool/mcp_server.py +27 -0
  7. kubectl_mcp_tool/tools/__init__.py +20 -0
  8. kubectl_mcp_tool/tools/backup.py +881 -0
  9. kubectl_mcp_tool/tools/capi.py +727 -0
  10. kubectl_mcp_tool/tools/certs.py +709 -0
  11. kubectl_mcp_tool/tools/cilium.py +582 -0
  12. kubectl_mcp_tool/tools/cluster.py +395 -121
  13. kubectl_mcp_tool/tools/core.py +157 -60
  14. kubectl_mcp_tool/tools/cost.py +97 -41
  15. kubectl_mcp_tool/tools/deployments.py +173 -56
  16. kubectl_mcp_tool/tools/diagnostics.py +40 -13
  17. kubectl_mcp_tool/tools/gitops.py +552 -0
  18. kubectl_mcp_tool/tools/helm.py +133 -46
  19. kubectl_mcp_tool/tools/keda.py +464 -0
  20. kubectl_mcp_tool/tools/kiali.py +652 -0
  21. kubectl_mcp_tool/tools/kubevirt.py +803 -0
  22. kubectl_mcp_tool/tools/networking.py +106 -32
  23. kubectl_mcp_tool/tools/operations.py +176 -50
  24. kubectl_mcp_tool/tools/pods.py +162 -50
  25. kubectl_mcp_tool/tools/policy.py +554 -0
  26. kubectl_mcp_tool/tools/rollouts.py +790 -0
  27. kubectl_mcp_tool/tools/security.py +89 -36
  28. kubectl_mcp_tool/tools/storage.py +35 -16
  29. tests/test_browser.py +2 -2
  30. tests/test_ecosystem.py +331 -0
  31. tests/test_tools.py +73 -10
  32. kubectl_mcp_server-1.14.0.dist-info/METADATA +0 -780
  33. kubectl_mcp_server-1.14.0.dist-info/RECORD +0 -49
  34. {kubectl_mcp_server-1.14.0.dist-info → kubectl_mcp_server-1.16.0.dist-info}/WHEEL +0 -0
  35. {kubectl_mcp_server-1.14.0.dist-info → kubectl_mcp_server-1.16.0.dist-info}/entry_points.txt +0 -0
  36. {kubectl_mcp_server-1.14.0.dist-info → kubectl_mcp_server-1.16.0.dist-info}/licenses/LICENSE +0 -0
  37. {kubectl_mcp_server-1.14.0.dist-info → kubectl_mcp_server-1.16.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,552 @@
1
+ """GitOps toolset for kubectl-mcp-server.
2
+
3
+ Provides tools for managing Flux and Argo CD applications.
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, require_any_crd
20
+
21
+
22
+ FLUX_KUSTOMIZATION_CRD = "kustomizations.kustomize.toolkit.fluxcd.io"
23
+ FLUX_HELMRELEASE_CRD = "helmreleases.helm.toolkit.fluxcd.io"
24
+ FLUX_GITREPO_CRD = "gitrepositories.source.toolkit.fluxcd.io"
25
+ FLUX_HELMREPO_CRD = "helmrepositories.source.toolkit.fluxcd.io"
26
+ ARGOCD_APP_CRD = "applications.argoproj.io"
27
+ ARGOCD_APPSET_CRD = "applicationsets.argoproj.io"
28
+
29
+
30
+ def _run_kubectl(args: List[str], context: str = "") -> Dict[str, Any]:
31
+ """Run kubectl command and return result."""
32
+ cmd = ["kubectl"] + _get_kubectl_context_args(context) + args
33
+ try:
34
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
35
+ if result.returncode == 0:
36
+ return {"success": True, "output": result.stdout}
37
+ return {"success": False, "error": result.stderr}
38
+ except subprocess.TimeoutExpired:
39
+ return {"success": False, "error": "Command timed out"}
40
+ except Exception as e:
41
+ return {"success": False, "error": str(e)}
42
+
43
+
44
+ def _get_resources(kind: str, namespace: str = "", context: str = "", label_selector: str = "") -> List[Dict]:
45
+ """Get Kubernetes resources of a specific kind."""
46
+ args = ["get", kind, "-o", "json"]
47
+ if namespace:
48
+ args.extend(["-n", namespace])
49
+ else:
50
+ args.append("-A")
51
+ if label_selector:
52
+ args.extend(["-l", label_selector])
53
+
54
+ result = _run_kubectl(args, context)
55
+ if result["success"]:
56
+ try:
57
+ data = json.loads(result["output"])
58
+ return data.get("items", [])
59
+ except json.JSONDecodeError:
60
+ return []
61
+ return []
62
+
63
+
64
+ def gitops_apps_list(
65
+ namespace: str = "",
66
+ context: str = "",
67
+ kind: str = "",
68
+ label_selector: str = ""
69
+ ) -> Dict[str, Any]:
70
+ """List GitOps applications from Flux or Argo CD.
71
+
72
+ Args:
73
+ namespace: Filter by namespace (empty for all namespaces)
74
+ context: Kubernetes context to use (optional, uses current context if not specified)
75
+ kind: Filter by kind (Kustomization, HelmRelease, Application)
76
+ label_selector: Label selector to filter resources
77
+
78
+ Returns:
79
+ List of GitOps applications with their status
80
+ """
81
+ apps = []
82
+
83
+ if not kind or kind.lower() == "kustomization":
84
+ if crd_exists(FLUX_KUSTOMIZATION_CRD, context):
85
+ for item in _get_resources("kustomizations.kustomize.toolkit.fluxcd.io", namespace, context, label_selector):
86
+ status = item.get("status", {})
87
+ conditions = status.get("conditions", [])
88
+ ready_cond = next((c for c in conditions if c.get("type") == "Ready"), {})
89
+ apps.append({
90
+ "name": item["metadata"]["name"],
91
+ "namespace": item["metadata"]["namespace"],
92
+ "kind": "Kustomization",
93
+ "engine": "flux",
94
+ "ready": ready_cond.get("status") == "True",
95
+ "status": ready_cond.get("reason", "Unknown"),
96
+ "message": ready_cond.get("message", ""),
97
+ "source": item.get("spec", {}).get("sourceRef", {}),
98
+ "path": item.get("spec", {}).get("path", ""),
99
+ "last_applied": status.get("lastAppliedRevision", ""),
100
+ })
101
+
102
+ if not kind or kind.lower() == "helmrelease":
103
+ if crd_exists(FLUX_HELMRELEASE_CRD, context):
104
+ for item in _get_resources("helmreleases.helm.toolkit.fluxcd.io", namespace, context, label_selector):
105
+ status = item.get("status", {})
106
+ conditions = status.get("conditions", [])
107
+ ready_cond = next((c for c in conditions if c.get("type") == "Ready"), {})
108
+ apps.append({
109
+ "name": item["metadata"]["name"],
110
+ "namespace": item["metadata"]["namespace"],
111
+ "kind": "HelmRelease",
112
+ "engine": "flux",
113
+ "ready": ready_cond.get("status") == "True",
114
+ "status": ready_cond.get("reason", "Unknown"),
115
+ "message": ready_cond.get("message", ""),
116
+ "chart": item.get("spec", {}).get("chart", {}),
117
+ "version": status.get("lastAppliedRevision", ""),
118
+ })
119
+
120
+ if not kind or kind.lower() == "application":
121
+ if crd_exists(ARGOCD_APP_CRD, context):
122
+ for item in _get_resources("applications.argoproj.io", namespace, context, label_selector):
123
+ status = item.get("status", {})
124
+ health = status.get("health", {})
125
+ sync = status.get("sync", {})
126
+ apps.append({
127
+ "name": item["metadata"]["name"],
128
+ "namespace": item["metadata"]["namespace"],
129
+ "kind": "Application",
130
+ "engine": "argocd",
131
+ "ready": health.get("status") == "Healthy" and sync.get("status") == "Synced",
132
+ "health": health.get("status", "Unknown"),
133
+ "sync_status": sync.get("status", "Unknown"),
134
+ "message": health.get("message", ""),
135
+ "source": item.get("spec", {}).get("source", {}),
136
+ "destination": item.get("spec", {}).get("destination", {}),
137
+ })
138
+
139
+ return {
140
+ "context": context or "current",
141
+ "total": len(apps),
142
+ "applications": apps,
143
+ }
144
+
145
+
146
+ def gitops_app_get(
147
+ name: str,
148
+ namespace: str,
149
+ kind: str,
150
+ context: str = ""
151
+ ) -> Dict[str, Any]:
152
+ """Get details of a specific GitOps application.
153
+
154
+ Args:
155
+ name: Name of the application
156
+ namespace: Namespace of the application
157
+ kind: Kind of application (Kustomization, HelmRelease, Application)
158
+ context: Kubernetes context to use (optional)
159
+
160
+ Returns:
161
+ Detailed application information
162
+ """
163
+ kind_map = {
164
+ "kustomization": "kustomizations.kustomize.toolkit.fluxcd.io",
165
+ "helmrelease": "helmreleases.helm.toolkit.fluxcd.io",
166
+ "application": "applications.argoproj.io",
167
+ }
168
+
169
+ k8s_kind = kind_map.get(kind.lower())
170
+ if not k8s_kind:
171
+ return {"success": False, "error": f"Unknown kind: {kind}"}
172
+
173
+ args = ["get", k8s_kind, name, "-n", namespace, "-o", "json"]
174
+ result = _run_kubectl(args, context)
175
+
176
+ if result["success"]:
177
+ try:
178
+ data = json.loads(result["output"])
179
+ return {
180
+ "success": True,
181
+ "context": context or "current",
182
+ "application": data,
183
+ }
184
+ except json.JSONDecodeError:
185
+ return {"success": False, "error": "Failed to parse response"}
186
+
187
+ return {"success": False, "error": result.get("error", "Unknown error")}
188
+
189
+
190
+ def gitops_app_sync(
191
+ name: str,
192
+ namespace: str,
193
+ kind: str,
194
+ context: str = ""
195
+ ) -> Dict[str, Any]:
196
+ """Trigger sync/reconciliation for a GitOps application.
197
+
198
+ Args:
199
+ name: Name of the application
200
+ namespace: Namespace of the application
201
+ kind: Kind of application (Kustomization, HelmRelease for Flux)
202
+ context: Kubernetes context to use (optional)
203
+
204
+ Returns:
205
+ Sync trigger result
206
+ """
207
+ kind_lower = kind.lower()
208
+
209
+ if kind_lower == "kustomization":
210
+ if not crd_exists(FLUX_KUSTOMIZATION_CRD, context):
211
+ return {"success": False, "error": "Flux Kustomization CRD not installed"}
212
+
213
+ annotation = "reconcile.fluxcd.io/requestedAt"
214
+ timestamp = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")
215
+
216
+ args = [
217
+ "annotate", "kustomizations.kustomize.toolkit.fluxcd.io",
218
+ name, "-n", namespace,
219
+ f"{annotation}={timestamp}", "--overwrite"
220
+ ]
221
+ result = _run_kubectl(args, context)
222
+
223
+ if result["success"]:
224
+ return {
225
+ "success": True,
226
+ "context": context or "current",
227
+ "message": f"Triggered reconciliation for Kustomization {name}",
228
+ "annotation": f"{annotation}={timestamp}",
229
+ }
230
+ return {"success": False, "error": result.get("error", "Failed to trigger sync")}
231
+
232
+ elif kind_lower == "helmrelease":
233
+ if not crd_exists(FLUX_HELMRELEASE_CRD, context):
234
+ return {"success": False, "error": "Flux HelmRelease CRD not installed"}
235
+
236
+ annotation = "reconcile.fluxcd.io/requestedAt"
237
+ timestamp = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")
238
+
239
+ args = [
240
+ "annotate", "helmreleases.helm.toolkit.fluxcd.io",
241
+ name, "-n", namespace,
242
+ f"{annotation}={timestamp}", "--overwrite"
243
+ ]
244
+ result = _run_kubectl(args, context)
245
+
246
+ if result["success"]:
247
+ return {
248
+ "success": True,
249
+ "context": context or "current",
250
+ "message": f"Triggered reconciliation for HelmRelease {name}",
251
+ "annotation": f"{annotation}={timestamp}",
252
+ }
253
+ return {"success": False, "error": result.get("error", "Failed to trigger sync")}
254
+
255
+ elif kind_lower == "application":
256
+ if not crd_exists(ARGOCD_APP_CRD, context):
257
+ return {"success": False, "error": "ArgoCD Application CRD not installed"}
258
+
259
+ annotation = "argocd.argoproj.io/refresh"
260
+ args = [
261
+ "annotate", "applications.argoproj.io",
262
+ name, "-n", namespace,
263
+ f"{annotation}=hard", "--overwrite"
264
+ ]
265
+ result = _run_kubectl(args, context)
266
+
267
+ if result["success"]:
268
+ return {
269
+ "success": True,
270
+ "context": context or "current",
271
+ "message": f"Triggered hard refresh for ArgoCD Application {name}",
272
+ }
273
+ return {"success": False, "error": result.get("error", "Failed to trigger sync")}
274
+
275
+ return {"success": False, "error": f"Unknown kind: {kind}"}
276
+
277
+
278
+ def gitops_app_status(
279
+ name: str,
280
+ namespace: str,
281
+ kind: str,
282
+ context: str = ""
283
+ ) -> Dict[str, Any]:
284
+ """Get sync status of a GitOps application.
285
+
286
+ Args:
287
+ name: Name of the application
288
+ namespace: Namespace of the application
289
+ kind: Kind of application (Kustomization, HelmRelease, Application)
290
+ context: Kubernetes context to use (optional)
291
+
292
+ Returns:
293
+ Application status information
294
+ """
295
+ result = gitops_app_get(name, namespace, kind, context)
296
+ if not result.get("success"):
297
+ return result
298
+
299
+ app = result["application"]
300
+ status = app.get("status", {})
301
+ kind_lower = kind.lower()
302
+
303
+ if kind_lower in ("kustomization", "helmrelease"):
304
+ conditions = status.get("conditions", [])
305
+ ready_cond = next((c for c in conditions if c.get("type") == "Ready"), {})
306
+ reconciling_cond = next((c for c in conditions if c.get("type") == "Reconciling"), {})
307
+
308
+ return {
309
+ "success": True,
310
+ "context": context or "current",
311
+ "name": name,
312
+ "namespace": namespace,
313
+ "kind": kind,
314
+ "ready": ready_cond.get("status") == "True",
315
+ "reason": ready_cond.get("reason", "Unknown"),
316
+ "message": ready_cond.get("message", ""),
317
+ "reconciling": reconciling_cond.get("status") == "True",
318
+ "last_applied_revision": status.get("lastAppliedRevision", ""),
319
+ "last_attempted_revision": status.get("lastAttemptedRevision", ""),
320
+ "observed_generation": status.get("observedGeneration"),
321
+ }
322
+
323
+ elif kind_lower == "application":
324
+ health = status.get("health", {})
325
+ sync = status.get("sync", {})
326
+ operation = status.get("operationState", {})
327
+
328
+ return {
329
+ "success": True,
330
+ "context": context or "current",
331
+ "name": name,
332
+ "namespace": namespace,
333
+ "kind": kind,
334
+ "health_status": health.get("status", "Unknown"),
335
+ "health_message": health.get("message", ""),
336
+ "sync_status": sync.get("status", "Unknown"),
337
+ "sync_revision": sync.get("revision", ""),
338
+ "operation_phase": operation.get("phase", ""),
339
+ "operation_message": operation.get("message", ""),
340
+ }
341
+
342
+ return {"success": False, "error": f"Unknown kind: {kind}"}
343
+
344
+
345
+ def gitops_sources_list(
346
+ namespace: str = "",
347
+ context: str = "",
348
+ kind: str = "",
349
+ label_selector: str = ""
350
+ ) -> Dict[str, Any]:
351
+ """List Flux source resources (GitRepositories, HelmRepositories).
352
+
353
+ Args:
354
+ namespace: Filter by namespace (empty for all namespaces)
355
+ context: Kubernetes context to use (optional)
356
+ kind: Filter by kind (GitRepository, HelmRepository)
357
+ label_selector: Label selector to filter resources
358
+
359
+ Returns:
360
+ List of source resources
361
+ """
362
+ sources = []
363
+
364
+ if not kind or kind.lower() == "gitrepository":
365
+ if crd_exists(FLUX_GITREPO_CRD, context):
366
+ for item in _get_resources("gitrepositories.source.toolkit.fluxcd.io", namespace, context, label_selector):
367
+ status = item.get("status", {})
368
+ conditions = status.get("conditions", [])
369
+ ready_cond = next((c for c in conditions if c.get("type") == "Ready"), {})
370
+ artifact = status.get("artifact", {})
371
+ sources.append({
372
+ "name": item["metadata"]["name"],
373
+ "namespace": item["metadata"]["namespace"],
374
+ "kind": "GitRepository",
375
+ "ready": ready_cond.get("status") == "True",
376
+ "status": ready_cond.get("reason", "Unknown"),
377
+ "url": item.get("spec", {}).get("url", ""),
378
+ "branch": item.get("spec", {}).get("ref", {}).get("branch", ""),
379
+ "revision": artifact.get("revision", ""),
380
+ "last_update": artifact.get("lastUpdateTime", ""),
381
+ })
382
+
383
+ if not kind or kind.lower() == "helmrepository":
384
+ if crd_exists(FLUX_HELMREPO_CRD, context):
385
+ for item in _get_resources("helmrepositories.source.toolkit.fluxcd.io", namespace, context, label_selector):
386
+ status = item.get("status", {})
387
+ conditions = status.get("conditions", [])
388
+ ready_cond = next((c for c in conditions if c.get("type") == "Ready"), {})
389
+ artifact = status.get("artifact", {})
390
+ sources.append({
391
+ "name": item["metadata"]["name"],
392
+ "namespace": item["metadata"]["namespace"],
393
+ "kind": "HelmRepository",
394
+ "ready": ready_cond.get("status") == "True",
395
+ "status": ready_cond.get("reason", "Unknown"),
396
+ "url": item.get("spec", {}).get("url", ""),
397
+ "revision": artifact.get("revision", ""),
398
+ "last_update": artifact.get("lastUpdateTime", ""),
399
+ })
400
+
401
+ return {
402
+ "context": context or "current",
403
+ "total": len(sources),
404
+ "sources": sources,
405
+ }
406
+
407
+
408
+ def gitops_source_get(
409
+ name: str,
410
+ namespace: str,
411
+ kind: str,
412
+ context: str = ""
413
+ ) -> Dict[str, Any]:
414
+ """Get details of a Flux source resource.
415
+
416
+ Args:
417
+ name: Name of the source
418
+ namespace: Namespace of the source
419
+ kind: Kind of source (GitRepository, HelmRepository)
420
+ context: Kubernetes context to use (optional)
421
+
422
+ Returns:
423
+ Detailed source information
424
+ """
425
+ kind_map = {
426
+ "gitrepository": "gitrepositories.source.toolkit.fluxcd.io",
427
+ "helmrepository": "helmrepositories.source.toolkit.fluxcd.io",
428
+ }
429
+
430
+ k8s_kind = kind_map.get(kind.lower())
431
+ if not k8s_kind:
432
+ return {"success": False, "error": f"Unknown kind: {kind}"}
433
+
434
+ args = ["get", k8s_kind, name, "-n", namespace, "-o", "json"]
435
+ result = _run_kubectl(args, context)
436
+
437
+ if result["success"]:
438
+ try:
439
+ data = json.loads(result["output"])
440
+ return {
441
+ "success": True,
442
+ "context": context or "current",
443
+ "source": data,
444
+ }
445
+ except json.JSONDecodeError:
446
+ return {"success": False, "error": "Failed to parse response"}
447
+
448
+ return {"success": False, "error": result.get("error", "Unknown error")}
449
+
450
+
451
+ def gitops_detect_engine(context: str = "") -> Dict[str, Any]:
452
+ """Detect which GitOps engines are installed in the cluster.
453
+
454
+ Args:
455
+ context: Kubernetes context to use (optional)
456
+
457
+ Returns:
458
+ Detection results for Flux and ArgoCD
459
+ """
460
+ flux_installed = any([
461
+ crd_exists(FLUX_KUSTOMIZATION_CRD, context),
462
+ crd_exists(FLUX_HELMRELEASE_CRD, context),
463
+ ])
464
+
465
+ argocd_installed = crd_exists(ARGOCD_APP_CRD, context)
466
+
467
+ return {
468
+ "context": context or "current",
469
+ "flux": {
470
+ "installed": flux_installed,
471
+ "kustomizations": crd_exists(FLUX_KUSTOMIZATION_CRD, context),
472
+ "helmreleases": crd_exists(FLUX_HELMRELEASE_CRD, context),
473
+ "gitrepositories": crd_exists(FLUX_GITREPO_CRD, context),
474
+ "helmrepositories": crd_exists(FLUX_HELMREPO_CRD, context),
475
+ },
476
+ "argocd": {
477
+ "installed": argocd_installed,
478
+ "applications": crd_exists(ARGOCD_APP_CRD, context),
479
+ "applicationsets": crd_exists(ARGOCD_APPSET_CRD, context),
480
+ },
481
+ }
482
+
483
+
484
+ def register_gitops_tools(mcp: FastMCP, non_destructive: bool = False):
485
+ """Register GitOps tools with the MCP server."""
486
+
487
+ @mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
488
+ def gitops_apps_list_tool(
489
+ namespace: str = "",
490
+ context: str = "",
491
+ kind: str = "",
492
+ label_selector: str = ""
493
+ ) -> str:
494
+ """List GitOps applications from Flux or Argo CD."""
495
+ return json.dumps(gitops_apps_list(namespace, context, kind, label_selector), indent=2)
496
+
497
+ @mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
498
+ def gitops_app_get_tool(
499
+ name: str,
500
+ namespace: str,
501
+ kind: str,
502
+ context: str = ""
503
+ ) -> str:
504
+ """Get details of a specific GitOps application."""
505
+ return json.dumps(gitops_app_get(name, namespace, kind, context), indent=2)
506
+
507
+ @mcp.tool()
508
+ def gitops_app_sync_tool(
509
+ name: str,
510
+ namespace: str,
511
+ kind: str,
512
+ context: str = ""
513
+ ) -> str:
514
+ """Trigger sync/reconciliation for a GitOps application."""
515
+ if non_destructive:
516
+ return json.dumps({"success": False, "error": "Operation blocked: non-destructive mode"})
517
+ return json.dumps(gitops_app_sync(name, namespace, kind, context), indent=2)
518
+
519
+ @mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
520
+ def gitops_app_status_tool(
521
+ name: str,
522
+ namespace: str,
523
+ kind: str,
524
+ context: str = ""
525
+ ) -> str:
526
+ """Get sync status of a GitOps application."""
527
+ return json.dumps(gitops_app_status(name, namespace, kind, context), indent=2)
528
+
529
+ @mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
530
+ def gitops_sources_list_tool(
531
+ namespace: str = "",
532
+ context: str = "",
533
+ kind: str = "",
534
+ label_selector: str = ""
535
+ ) -> str:
536
+ """List Flux source resources (GitRepositories, HelmRepositories)."""
537
+ return json.dumps(gitops_sources_list(namespace, context, kind, label_selector), indent=2)
538
+
539
+ @mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
540
+ def gitops_source_get_tool(
541
+ name: str,
542
+ namespace: str,
543
+ kind: str,
544
+ context: str = ""
545
+ ) -> str:
546
+ """Get details of a Flux source resource."""
547
+ return json.dumps(gitops_source_get(name, namespace, kind, context), indent=2)
548
+
549
+ @mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
550
+ def gitops_detect_engine_tool(context: str = "") -> str:
551
+ """Detect which GitOps engines are installed in the cluster."""
552
+ return json.dumps(gitops_detect_engine(context), indent=2)